packages feed

yesod-auth-oauth2 0.7.1.0 → 0.7.1.1

raw patch · 31 files changed

+668/−593 lines, 31 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Network.OAuth.OAuth2.Compat: data OAuth2Error a
- Network.OAuth.OAuth2.Compat: type Errors = OAuth2Error Errors
+ Network.OAuth.OAuth2.Compat: type Errors = TokenRequestError
- Yesod.Auth.OAuth2.Prelude: throwIO :: (MonadThrow m, Exception e) => e -> m a
+ Yesod.Auth.OAuth2.Prelude: throwIO :: (HasCallStack, MonadThrow m, Exception e) => e -> m a

Files

CHANGELOG.md view
@@ -1,4 +1,8 @@-## [_Unreleased_](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.1.0...main)+## [_Unreleased_](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.1.1...main)++## [v0.7.1.1](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.1.0...v0.7.1.1)++- Support `mtl-2.3`, which no longer re-exports `Control.Monad`  ## [v0.7.1.0](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.0.3...v0.7.1.0) 
example/Main.hs view
@@ -13,7 +13,7 @@ import Data.ByteString.Lazy (fromStrict, toStrict) import qualified Data.Map as M import Data.Maybe (fromJust)-import Data.String (IsString(fromString))+import Data.String (IsString (fromString)) import Data.Text (Text, pack) import qualified Data.Text as T import Data.Text.Encoding (decodeUtf8)@@ -46,13 +46,15 @@   , appAuthPlugins :: [AuthPlugin App]   } -mkYesod "App" [parseRoutes|+mkYesod+  "App"+  [parseRoutes|     / RootR GET     /auth AuthR Auth getAuth |]  instance Yesod App where-    -- see https://github.com/thoughtbot/yesod-auth-oauth2/issues/87+  -- see https://github.com/thoughtbot/yesod-auth-oauth2/issues/87   approot = ApprootStatic "http://localhost:3000"  instance YesodAuth App where@@ -65,9 +67,9 @@    -- Copy the Creds response into the session for viewing after   authenticate c = do-    mapM_ (uncurry setSession)-      $ [("credsIdent", credsIdent c), ("credsPlugin", credsPlugin c)]-      ++ credsExtra c+    mapM_ (uncurry setSession) $+      [("credsIdent", credsIdent c), ("credsPlugin", credsPlugin c)]+        ++ credsExtra c      return $ Authenticated "1" @@ -80,23 +82,24 @@  getRootR :: Handler Html getRootR = do-    sess <- getSession+  sess <- getSession -    let-        prettify-            = decodeUtf8-            . toStrict-            . encodePretty-            . fromJust-            . decode @Value-            . fromStrict+  let+    prettify =+      decodeUtf8+        . toStrict+        . encodePretty+        . fromJust+        . decode @Value+        . fromStrict -        mCredsIdent = decodeUtf8 <$> M.lookup "credsIdent" sess-        mCredsPlugin = decodeUtf8 <$> M.lookup "credsPlugin" sess-        mAccessToken = decodeUtf8 <$> M.lookup "accessToken" sess-        mUserResponse = prettify <$> M.lookup "userResponse" sess+    mCredsIdent = decodeUtf8 <$> M.lookup "credsIdent" sess+    mCredsPlugin = decodeUtf8 <$> M.lookup "credsPlugin" sess+    mAccessToken = decodeUtf8 <$> M.lookup "accessToken" sess+    mUserResponse = prettify <$> M.lookup "userResponse" sess -    defaultLayout [whamlet|+  defaultLayout+    [whamlet|         <h1>Yesod Auth OAuth2 Example         <h2>             <a href=@{AuthR LoginR}>Log in@@ -123,32 +126,33 @@   azureTenant <- getEnv "AZURE_ADV2_TENANT_ID"    appHttpManager <- newManager tlsManagerSettings-  appAuthPlugins <- sequence+  appAuthPlugins <-+    sequence       -- When Providers are added, add them here and update .env.example.       -- Nothing else should need changing.       --       -- FIXME: oauth2BattleNet is quite annoying!       ---    [ loadPlugin oauth2AzureAD "AZURE_AD"-    , loadPlugin (oauth2AzureADv2 $ pack azureTenant) "AZURE_ADV2"-    , loadPlugin (oauth2Auth0Host $ fromString auth0Host) "AUTH0"-    , loadPlugin (oauth2BattleNet [whamlet|TODO|] "en") "BATTLE_NET"-    , loadPlugin oauth2Bitbucket "BITBUCKET"-    , loadPlugin oauth2ClassLink "CLASSLINK"-    , loadPlugin (oauth2Eve Plain) "EVE_ONLINE"-    , loadPlugin oauth2GitHub "GITHUB"-    , loadPlugin oauth2GitLab "GITLAB"-    , loadPlugin oauth2Google "GOOGLE"-    , loadPlugin oauth2Nylas "NYLAS"-    , loadPlugin oauth2Salesforce "SALES_FORCE"-    , loadPlugin oauth2Slack "SLACK"-    , loadPlugin (oauth2Spotify []) "SPOTIFY"-    , loadPlugin oauth2Twitch "TWITCH"-    , loadPlugin oauth2WordPressDotCom "WORDPRESS_DOT_COM"-    , loadPlugin oauth2Upcase "UPCASE"-    ]+      [ loadPlugin oauth2AzureAD "AZURE_AD"+      , loadPlugin (oauth2AzureADv2 $ pack azureTenant) "AZURE_ADV2"+      , loadPlugin (oauth2Auth0Host $ fromString auth0Host) "AUTH0"+      , loadPlugin (oauth2BattleNet [whamlet|TODO|] "en") "BATTLE_NET"+      , loadPlugin oauth2Bitbucket "BITBUCKET"+      , loadPlugin oauth2ClassLink "CLASSLINK"+      , loadPlugin (oauth2Eve Plain) "EVE_ONLINE"+      , loadPlugin oauth2GitHub "GITHUB"+      , loadPlugin oauth2GitLab "GITLAB"+      , loadPlugin oauth2Google "GOOGLE"+      , loadPlugin oauth2Nylas "NYLAS"+      , loadPlugin oauth2Salesforce "SALES_FORCE"+      , loadPlugin oauth2Slack "SLACK"+      , loadPlugin (oauth2Spotify []) "SPOTIFY"+      , loadPlugin oauth2Twitch "TWITCH"+      , loadPlugin oauth2WordPressDotCom "WORDPRESS_DOT_COM"+      , loadPlugin oauth2Upcase "UPCASE"+      ] -  return App { .. }+  return App {..}  where   loadPlugin f prefix = do     clientId <- getEnv $ prefix <> "_CLIENT_ID"
src/Network/OAuth/OAuth2/Compat.hs view
@@ -1,17 +1,17 @@ {-# LANGUAGE CPP #-}  module Network.OAuth.OAuth2.Compat-    ( OAuth2(..)-    , OAuth2Result-    , Errors-    , authorizationUrl-    , fetchAccessToken-    , fetchAccessToken2-    , authGetBS+  ( OAuth2 (..)+  , OAuth2Result+  , Errors+  , authorizationUrl+  , fetchAccessToken+  , fetchAccessToken2+  , authGetBS      -- * Re-exports-    , module Network.OAuth.OAuth2-    ) where+  , module Network.OAuth.OAuth2+  ) where  import Data.ByteString.Lazy (ByteString) import Data.Text (Text)@@ -39,12 +39,12 @@ #endif  data OAuth2 = OAuth2-    { oauth2ClientId :: Text-    , oauth2ClientSecret :: Maybe Text-    , oauth2AuthorizeEndpoint :: URIRef Absolute-    , oauth2TokenEndpoint :: URIRef Absolute-    , oauth2RedirectUri :: Maybe (URIRef Absolute)-    }+  { oauth2ClientId :: Text+  , oauth2ClientSecret :: Maybe Text+  , oauth2AuthorizeEndpoint :: URIRef Absolute+  , oauth2TokenEndpoint :: URIRef Absolute+  , oauth2RedirectUri :: Maybe (URIRef Absolute)+  }  #if MIN_VERSION_hoauth2(2,7,0) type Errors = TokenRequestError@@ -58,17 +58,17 @@ authorizationUrl = OAuth2.authorizationUrl . getOAuth2  fetchAccessToken-    :: Manager-    -> OAuth2-    -> ExchangeToken-    -> IO (OAuth2Result Errors OAuth2Token)+  :: Manager+  -> OAuth2+  -> ExchangeToken+  -> IO (OAuth2Result Errors OAuth2Token) fetchAccessToken = fetchAccessTokenBasic  fetchAccessToken2-    :: Manager-    -> OAuth2-    -> ExchangeToken-    -> IO (OAuth2Result Errors OAuth2Token)+  :: Manager+  -> OAuth2+  -> ExchangeToken+  -> IO (OAuth2Result Errors OAuth2Token) fetchAccessToken2 = fetchAccessTokenPost  authGetBS :: Manager -> AccessToken -> URI -> IO (Either ByteString ByteString)@@ -141,12 +141,12 @@ -- directly.  fetchAccessTokenBasic-    :: Manager-    -> OAuth2-    -> ExchangeToken-    -> IO (OAuth2Result Errors OAuth2Token)+  :: Manager+  -> OAuth2+  -> ExchangeToken+  -> IO (OAuth2Result Errors OAuth2Token) fetchAccessTokenBasic m o e = runOAuth2 $ f m (getOAuth2 o) e-  where+ where #if MIN_VERSION_hoauth2(2,6,0)     f = OAuth2.fetchAccessTokenWithAuthMethod OAuth2.ClientSecretBasic #elif MIN_VERSION_hoauth2(2,3,0)@@ -156,12 +156,12 @@ #endif  fetchAccessTokenPost-    :: Manager-    -> OAuth2-    -> ExchangeToken-    -> IO (OAuth2Result Errors OAuth2Token)+  :: Manager+  -> OAuth2+  -> ExchangeToken+  -> IO (OAuth2Result Errors OAuth2Token) fetchAccessTokenPost m o e = runOAuth2 $ f m (getOAuth2 o) e-  where+ where #if MIN_VERSION_hoauth2(2, 6, 0)     f = OAuth2.fetchAccessTokenWithAuthMethod OAuth2.ClientSecretPost #elif MIN_VERSION_hoauth2(2,3,0)
src/URI/ByteString/Extension.hs view
@@ -1,9 +1,10 @@ {-# LANGUAGE FlexibleInstances #-} {-# OPTIONS_GHC -fno-warn-orphans #-}+ module URI.ByteString.Extension where  import Data.ByteString (ByteString)-import Data.String (IsString(..))+import Data.String (IsString (..)) import Data.Text (Text) import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Lens.Micro@@ -41,9 +42,12 @@ fromRelative s h = flip withHost h . toAbsolute s  withHost :: URIRef a -> Host -> URIRef a-withHost u h = u & authorityL %~ maybe-  (Just $ Authority Nothing h Nothing)-  (\a -> Just $ a & authorityHostL .~ h)+withHost u h =+  u+    & authorityL+      %~ maybe+        (Just $ Authority Nothing h Nothing)+        (\a -> Just $ a & authorityHostL .~ h)  withPath :: URIRef a -> ByteString -> URIRef a withPath u p = u & pathL .~ p
src/UnliftIO/Except.hs view
@@ -1,9 +1,9 @@ {-# OPTIONS_GHC -Wno-orphans #-} -module UnliftIO.Except-  () where+module UnliftIO.Except () where -import Control.Monad.Except+import Control.Monad ((<=<))+import Control.Monad.Except (ExceptT (..), runExceptT) import UnliftIO  instance (MonadUnliftIO m, Exception e) => MonadUnliftIO (ExceptT e m) where
src/Yesod/Auth/OAuth2.hs view
@@ -1,18 +1,18 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}+ -- | -- -- Generic OAuth2 plugin for Yesod -- -- See @"Yesod.Auth.OAuth2.GitHub"@ for example usage.--- module Yesod.Auth.OAuth2-  ( OAuth2(..)+  ( OAuth2 (..)   , FetchCreds   , Manager-  , OAuth2Token(..)-  , Creds(..)+  , OAuth2Token (..)+  , Creds (..)   , oauth2Url   , authOAuth2   , authOAuth2Widget@@ -46,14 +46,12 @@ -- | Create an @'AuthPlugin'@ for the given OAuth2 provider -- -- Presents a generic @"Login via #{name}"@ link--- authOAuth2 :: YesodAuth m => Text -> OAuth2 -> FetchCreds m -> AuthPlugin m authOAuth2 name = authOAuth2Widget [whamlet|Login via #{name}|] name  -- | A version of 'authOAuth2' that uses 'fetchAccessToken2' -- -- See <https://github.com/thoughtbot/yesod-auth-oauth2/pull/129>--- authOAuth2' :: YesodAuth m => Text -> OAuth2 -> FetchCreds m -> AuthPlugin m authOAuth2' name = authOAuth2Widget' [whamlet|Login via #{name}|] name @@ -61,7 +59,6 @@ -- -- Allows passing a custom widget for the login link. See @'oauth2Eve'@ for an -- example.--- authOAuth2Widget   :: YesodAuth m   => WidgetFor m ()@@ -74,7 +71,6 @@ -- | A version of 'authOAuth2Widget' that uses 'fetchAccessToken2' -- -- See <https://github.com/thoughtbot/yesod-auth-oauth2/pull/129>--- authOAuth2Widget'   :: YesodAuth m   => WidgetFor m ()@@ -92,11 +88,13 @@   -> OAuth2   -> FetchCreds m   -> AuthPlugin m-buildPlugin getToken widget name oauth getCreds = AuthPlugin-  name-  (dispatchAuthRequest name oauth getToken getCreds)-  login-  where login tm = [whamlet|<a href=@{tm $ oauth2Url name}>^{widget}|]+buildPlugin getToken widget name oauth getCreds =+  AuthPlugin+    name+    (dispatchAuthRequest name oauth getToken getCreds)+    login+ where+  login tm = [whamlet|<a href=@{tm $ oauth2Url name}>^{widget}|]  -- | Read the @'AccessToken'@ from the values set via @'setExtra'@ getAccessToken :: Creds m -> Maybe AccessToken@@ -105,7 +103,6 @@ -- | Read the @'RefreshToken'@ from the values set via @'setExtra'@ -- -- N.B. not all providers supply this value.--- getRefreshToken :: Creds m -> Maybe RefreshToken getRefreshToken = (RefreshToken <$>) . lookup "refreshToken" . credsExtra 
src/Yesod/Auth/OAuth2/Auth0.hs view
@@ -1,10 +1,10 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- OAuth2 plugin for <https://auth0.com> -- -- * Authenticates against specific auth0 tenant -- * Uses Auth0 user id (a.k.a [sub](https://auth0.com/docs/api/authentication#get-user-info)) as credentials identifier--- module Yesod.Auth.OAuth2.Auth0   ( oauth2Auth0HostScopes   , oauth2Auth0Host@@ -13,8 +13,8 @@  import Data.Aeson as Aeson import qualified Data.Text as T-import Prelude import Yesod.Auth.OAuth2.Prelude+import Prelude  -- | https://auth0.com/docs/api/authentication#get-user-info newtype User = User T.Text@@ -36,22 +36,25 @@   :: YesodAuth m => URI -> [Text] -> Text -> Text -> AuthPlugin m oauth2Auth0HostScopes host scopes clientId clientSecret =   authOAuth2 pluginName oauth2 $ \manager token -> do-    (User uid, userResponse) <- authGetProfile-      pluginName-      manager-      token-      (host `withPath` "/userinfo")-    pure Creds-      { credsPlugin = pluginName-      , credsIdent = uid-      , credsExtra = setExtra token userResponse-      }+    (User uid, userResponse) <-+      authGetProfile+        pluginName+        manager+        token+        (host `withPath` "/userinfo")+    pure+      Creds+        { credsPlugin = pluginName+        , credsIdent = uid+        , credsExtra = setExtra token userResponse+        }  where-  oauth2 = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint =-      host `withPath` "/authorize" `withQuery` [scopeParam " " scopes]-    , oauth2TokenEndpoint = host `withPath` "/oauth/token"-    , oauth2RedirectUri = Nothing-    }+  oauth2 =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint =+          host `withPath` "/authorize" `withQuery` [scopeParam " " scopes]+      , oauth2TokenEndpoint = host `withPath` "/oauth/token"+      , oauth2RedirectUri = Nothing+      }
src/Yesod/Auth/OAuth2/AzureAD.hs view
@@ -1,18 +1,18 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- -- OAuth2 plugin for Azure AD. -- -- * Authenticates against Azure AD -- * Uses email as credentials identifier--- module Yesod.Auth.OAuth2.AzureAD   ( oauth2AzureAD   , oauth2AzureADScoped   ) where -import Prelude import Yesod.Auth.OAuth2.Prelude+import Prelude  newtype User = User Text @@ -31,26 +31,29 @@ oauth2AzureADScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2AzureADScoped scopes clientId clientSecret =   authOAuth2 pluginName oauth2 $ \manager token -> do-    (User userId, userResponse) <- authGetProfile-      pluginName-      manager-      token-      "https://graph.microsoft.com/v1.0/me"+    (User userId, userResponse) <-+      authGetProfile+        pluginName+        manager+        token+        "https://graph.microsoft.com/v1.0/me" -    pure Creds-      { credsPlugin = pluginName-      , credsIdent = userId-      , credsExtra = setExtra token userResponse-      }+    pure+      Creds+        { credsPlugin = pluginName+        , credsIdent = userId+        , credsExtra = setExtra token userResponse+        }  where-  oauth2 = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint =-      "https://login.windows.net/common/oauth2/authorize"-        `withQuery` [ scopeParam "," scopes-                    , ("resource", "https://graph.microsoft.com")-                    ]-    , oauth2TokenEndpoint = "https://login.windows.net/common/oauth2/token"-    , oauth2RedirectUri = Nothing-    }+  oauth2 =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint =+          "https://login.windows.net/common/oauth2/authorize"+            `withQuery` [ scopeParam "," scopes+                        , ("resource", "https://graph.microsoft.com")+                        ]+      , oauth2TokenEndpoint = "https://login.windows.net/common/oauth2/token"+      , oauth2RedirectUri = Nothing+      }
src/Yesod/Auth/OAuth2/AzureADv2.hs view
@@ -1,18 +1,18 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- -- OAuth2 plugin for Azure AD using the new v2 endpoints. -- -- * Authenticates against Azure AD -- * Uses email as credentials identifier--- module Yesod.Auth.OAuth2.AzureADv2   ( oauth2AzureADv2   , oauth2AzureADv2Scoped   ) where -import Prelude import Yesod.Auth.OAuth2.Prelude+import Prelude  import Data.String import Data.Text (unpack)@@ -34,49 +34,55 @@   -- ^ Tenant Id   --   -- If using a multi-tenant App, @common@ can be given here.-  ---  -> Text -- ^ Client Id-  -> Text -- ^ Client secret+  -> Text+  -- ^ Client Id+  -> Text+  -- ^ Client secret   -> AuthPlugin m oauth2AzureADv2 = oauth2AzureADv2Scoped defaultScopes  oauth2AzureADv2Scoped   :: YesodAuth m-  => [Text] -- ^ Scopes+  => [Text]+  -- ^ Scopes   -> Text   -- ^ Tenant Id   --   -- If using a multi-tenant App, @common@ can be given here.-  ---  -> Text -- ^ Client Id-  -> Text -- ^ Client Secret+  -> Text+  -- ^ Client Id+  -> Text+  -- ^ Client Secret   -> AuthPlugin m oauth2AzureADv2Scoped scopes tenantId clientId clientSecret =   authOAuth2 pluginName oauth2 $ \manager token -> do-    (User userId, userResponse) <- authGetProfile-      pluginName-      manager-      token-      "https://graph.microsoft.com/v1.0/me"+    (User userId, userResponse) <-+      authGetProfile+        pluginName+        manager+        token+        "https://graph.microsoft.com/v1.0/me" -    pure Creds-      { credsPlugin = pluginName-      , credsIdent = userId-      , credsExtra = setExtra token userResponse-      }+    pure+      Creds+        { credsPlugin = pluginName+        , credsIdent = userId+        , credsExtra = setExtra token userResponse+        }  where-  oauth2 = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint =-      tenantUrl "/authorize" `withQuery` [scopeParam " " scopes]-    , oauth2TokenEndpoint = tenantUrl "/token"-    , oauth2RedirectUri = Nothing-    }+  oauth2 =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint =+          tenantUrl "/authorize" `withQuery` [scopeParam " " scopes]+      , oauth2TokenEndpoint = tenantUrl "/token"+      , oauth2RedirectUri = Nothing+      }    tenantUrl path =-    fromString-      $ "https://login.microsoftonline.com/"-      <> unpack tenantId-      <> "/oauth2/v2.0"-      <> path+    fromString $+      "https://login.microsoftonline.com/"+        <> unpack tenantId+        <> "/oauth2/v2.0"+        <> path
src/Yesod/Auth/OAuth2/BattleNet.hs view
@@ -7,7 +7,6 @@ -- * Authenticates against battle.net. -- * Uses user's id as credentials identifier. -- * Returns user's battletag in extras.--- module Yesod.Auth.OAuth2.BattleNet   ( oauth2BattleNet   , oAuth2BattleNet@@ -28,32 +27,37 @@  oauth2BattleNet   :: YesodAuth m-  => WidgetFor m () -- ^ Login widget-  -> Text -- ^ User region (e.g. "eu", "cn", "us")-  -> Text -- ^ Client ID-  -> Text -- ^ Client Secret+  => WidgetFor m ()+  -- ^ Login widget+  -> Text+  -- ^ User region (e.g. "eu", "cn", "us")+  -> Text+  -- ^ Client ID+  -> Text+  -- ^ Client Secret   -> AuthPlugin m oauth2BattleNet widget region clientId clientSecret =   authOAuth2Widget widget pluginName oauth2 $ \manager token -> do     (User userId, userResponse) <--      authGetProfile pluginName manager token-        $ fromRelative "https" (apiHost $ T.toLower region) "/account/user"+      authGetProfile pluginName manager token $+        fromRelative "https" (apiHost $ T.toLower region) "/account/user" -    pure Creds-      { credsPlugin = pluginName-      , credsIdent = T.pack $ show userId-      , credsExtra = setExtra token userResponse-      }+    pure+      Creds+        { credsPlugin = pluginName+        , credsIdent = T.pack $ show userId+        , credsExtra = setExtra token userResponse+        }  where   host = wwwHost $ T.toLower region-  oauth2 = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint = fromRelative "https" host "/oauth/authorize"-    , oauth2TokenEndpoint = fromRelative "https" host "/oauth/token"-    , oauth2RedirectUri = Nothing-    }-+  oauth2 =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint = fromRelative "https" host "/oauth/authorize"+      , oauth2TokenEndpoint = fromRelative "https" host "/oauth/token"+      , oauth2RedirectUri = Nothing+      }  apiHost :: Text -> Host apiHost "cn" = "api.battlenet.com.cn"
src/Yesod/Auth/OAuth2/Bitbucket.hs view
@@ -1,11 +1,11 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- -- OAuth2 plugin for http://bitbucket.com -- -- * Authenticates against bitbucket -- * Uses bitbucket uuid as credentials identifier--- module Yesod.Auth.OAuth2.Bitbucket   ( oauth2Bitbucket   , oauth2BitbucketScoped@@ -32,30 +32,33 @@ oauth2BitbucketScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2BitbucketScoped scopes clientId clientSecret =   authOAuth2 pluginName oauth2 $ \manager token -> do-    (User userId, userResponse) <- authGetProfile-      pluginName-      manager-      token-      "https://api.bitbucket.com/2.0/user"+    (User userId, userResponse) <-+      authGetProfile+        pluginName+        manager+        token+        "https://api.bitbucket.com/2.0/user" -    pure Creds-      { credsPlugin = pluginName-        -- FIXME: Preserved bug. This should just be userId (it's already-        -- a Text), but because this code was shipped, folks likely have-        -- Idents in their database like @"\"...\""@, and if we fixed this-        -- they would need migrating. We're keeping it for now as it's a-        -- minor wart. Breaking typed APIs is one thing, causing data to go-        -- invalid is another.-      , credsIdent = T.pack $ show userId-      , credsExtra = setExtra token userResponse-      }+    pure+      Creds+        { credsPlugin = pluginName+        , -- FIXME: Preserved bug. This should just be userId (it's already+          -- a Text), but because this code was shipped, folks likely have+          -- Idents in their database like @"\"...\""@, and if we fixed this+          -- they would need migrating. We're keeping it for now as it's a+          -- minor wart. Breaking typed APIs is one thing, causing data to go+          -- invalid is another.+          credsIdent = T.pack $ show userId+        , credsExtra = setExtra token userResponse+        }  where-  oauth2 = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint =-      "https://bitbucket.com/site/oauth2/authorize"-        `withQuery` [scopeParam "," scopes]-    , oauth2TokenEndpoint = "https://bitbucket.com/site/oauth2/access_token"-    , oauth2RedirectUri = Nothing-    }+  oauth2 =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint =+          "https://bitbucket.com/site/oauth2/authorize"+            `withQuery` [scopeParam "," scopes]+      , oauth2TokenEndpoint = "https://bitbucket.com/site/oauth2/access_token"+      , oauth2RedirectUri = Nothing+      }
src/Yesod/Auth/OAuth2/ClassLink.hs view
@@ -26,24 +26,27 @@ oauth2ClassLinkScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2ClassLinkScoped scopes clientId clientSecret =   authOAuth2 pluginName oauth2 $ \manager token -> do-    (User userId, userResponse) <- authGetProfile-      pluginName-      manager-      token-      "https://nodeapi.classlink.com/v2/my/info"+    (User userId, userResponse) <-+      authGetProfile+        pluginName+        manager+        token+        "https://nodeapi.classlink.com/v2/my/info" -    pure Creds-      { credsPlugin = pluginName-      , credsIdent = T.pack $ show userId-      , credsExtra = setExtra token userResponse-      }+    pure+      Creds+        { credsPlugin = pluginName+        , credsIdent = T.pack $ show userId+        , credsExtra = setExtra token userResponse+        }  where-  oauth2 = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint =-      "https://launchpad.classlink.com/oauth2/v2/auth"-        `withQuery` [scopeParam "," scopes]-    , oauth2TokenEndpoint = "https://launchpad.classlink.com/oauth2/v2/token"-    , oauth2RedirectUri = Nothing-    }+  oauth2 =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint =+          "https://launchpad.classlink.com/oauth2/v2/auth"+            `withQuery` [scopeParam "," scopes]+      , oauth2TokenEndpoint = "https://launchpad.classlink.com/oauth2/v2/token"+      , oauth2RedirectUri = Nothing+      }
src/Yesod/Auth/OAuth2/Dispatch.hs view
@@ -12,14 +12,15 @@   , dispatchAuthRequest   ) where -import Control.Monad.Except+import Control.Monad (unless)+import Control.Monad.Except (MonadError (..)) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Network.HTTP.Conduit (Manager) import Network.OAuth.OAuth2.Compat-import UnliftIO.Exception import URI.ByteString.Extension+import UnliftIO.Exception import Yesod.Auth hiding (ServerError) import Yesod.Auth.OAuth2.DispatchError import Yesod.Auth.OAuth2.ErrorResponse@@ -29,21 +30,26 @@ -- | How to fetch an @'OAuth2Token'@ -- -- This will be 'fetchAccessToken' or 'fetchAccessToken2'----type FetchToken-  = Manager -> OAuth2 -> ExchangeToken -> IO (OAuth2Result Errors OAuth2Token)+type FetchToken =+  Manager -> OAuth2 -> ExchangeToken -> IO (OAuth2Result Errors OAuth2Token)  -- | How to take an @'OAuth2Token'@ and retrieve user credentials type FetchCreds m = Manager -> OAuth2Token -> IO (Creds m)  -- | Dispatch the various OAuth2 handshake routes dispatchAuthRequest-  :: Text             -- ^ Name-  -> OAuth2           -- ^ Service details-  -> FetchToken       -- ^ How to get a token-  -> FetchCreds m     -- ^ How to get credentials-  -> Text             -- ^ Method-  -> [Text]           -- ^ Path pieces+  :: Text+  -- ^ Name+  -> OAuth2+  -- ^ Service details+  -> FetchToken+  -- ^ How to get a token+  -> FetchCreds m+  -- ^ How to get credentials+  -> Text+  -- ^ Method+  -> [Text]+  -- ^ Path pieces   -> AuthHandler m TypedContent dispatchAuthRequest name oauth2 _ _ "GET" ["forward"] =   handleDispatchError $ dispatchForward name oauth2@@ -55,7 +61,6 @@ -- -- 1. Set a random CSRF token in our session -- 2. Redirect to the Provider's authorization URL--- dispatchForward   :: (MonadError DispatchError m, MonadAuthHandler site m)   => Text@@ -71,7 +76,6 @@ -- 1. Verify the URL's CSRF token matches our session -- 2. Use the code parameter to fetch an AccessToken for the Provider -- 3. Use the AccessToken to construct a @'Creds'@ value for the Provider--- dispatchCallback   :: (MonadError DispatchError m, MonadAuthHandler site m)   => Text@@ -85,12 +89,13 @@   code <- requireGetParam "code"   manager <- authHttpManager   oauth2' <- withCallbackAndState name oauth2 csrf-  token <- either (throwError . OAuth2ResultError) pure-    =<< liftIO (getToken manager oauth2' $ ExchangeToken code)+  token <-+    either (throwError . OAuth2ResultError) pure+      =<< liftIO (getToken manager oauth2' $ ExchangeToken code)   creds <-     liftIO (getCreds manager token)-    `catch` (throwError . FetchCredsIOException)-    `catch` (throwError . FetchCredsYesodOAuth2Exception)+      `catch` (throwError . FetchCredsIOException)+      `catch` (throwError . FetchCredsYesodOAuth2Exception)   setCredsRedirect creds  withCallbackAndState@@ -102,29 +107,30 @@ withCallbackAndState name oauth2 csrf = do   uri <- ($ PluginR name ["callback"]) <$> getParentUrlRender   callback <- maybe (throwError $ InvalidCallbackUri uri) pure $ fromText uri-  pure oauth2-    { oauth2RedirectUri = Just callback-    , oauth2AuthorizeEndpoint =-      oauth2AuthorizeEndpoint oauth2 `withQuery` [("state", encodeUtf8 csrf)]-    }+  pure+    oauth2+      { oauth2RedirectUri = Just callback+      , oauth2AuthorizeEndpoint =+          oauth2AuthorizeEndpoint oauth2 `withQuery` [("state", encodeUtf8 csrf)]+      }  getParentUrlRender :: MonadHandler m => m (Route (SubHandlerSite m) -> Text) getParentUrlRender = (.) <$> getUrlRender <*> getRouteToParent --- | Set a random, ~30-character value in the session+-- | Set a random, ~64-byte value in the session -- -- Some (but not all) providers decode a @+@ in the state token as a space when -- sending it back to us. We don't expect this and fail. And if we did code for -- it, we'd then fail on the providers that /don't/ do that. -- -- Therefore, we just exclude @+@ in our tokens, which means this function may--- return slightly less than 30 characters.---+-- return slightly fewer than 64 bytes. setSessionCSRF :: MonadHandler m => Text -> m Text setSessionCSRF sessionKey = do   csrfToken <- liftIO randomToken   csrfToken <$ setSession sessionKey csrfToken-  where randomToken = T.filter (/= '+') <$> randomText 64+ where+  randomToken = T.filter (/= '+') <$> randomText 64  -- | Verify the callback provided the same CSRF token as in our session verifySessionCSRF@@ -133,9 +139,10 @@   token <- requireGetParam "state"   sessionToken <- lookupSession sessionKey   deleteSession sessionKey-  token <$ unless-    (sessionToken == Just token)-    (throwError $ InvalidStateToken sessionToken token)+  token+    <$ unless+      (sessionToken == Just token)+      (throwError $ InvalidStateToken sessionToken token)  requireGetParam   :: (MonadError DispatchError m, MonadHandler m) => Text -> m Text
src/Yesod/Auth/OAuth2/DispatchError.hs view
@@ -9,7 +9,7 @@ {-# LANGUAGE TypeFamilies #-}  module Yesod.Auth.OAuth2.DispatchError-  ( DispatchError(..)+  ( DispatchError (..)   , handleDispatchError   , onDispatchError   ) where@@ -26,34 +26,33 @@ import Yesod.Core hiding (ErrorResponse)  data DispatchError-    = MissingParameter Text-    | InvalidStateToken (Maybe Text) Text-    | InvalidCallbackUri Text-    | OAuth2HandshakeError ErrorResponse-    | OAuth2ResultError Errors-    | FetchCredsIOException IOException-    | FetchCredsYesodOAuth2Exception YesodOAuth2Exception-    | OtherDispatchError Text-    deriving stock Show-    deriving anyclass Exception+  = MissingParameter Text+  | InvalidStateToken (Maybe Text) Text+  | InvalidCallbackUri Text+  | OAuth2HandshakeError ErrorResponse+  | OAuth2ResultError Errors+  | FetchCredsIOException IOException+  | FetchCredsYesodOAuth2Exception YesodOAuth2Exception+  | OtherDispatchError Text+  deriving stock (Show)+  deriving anyclass (Exception)  -- | User-friendly message for any given 'DispatchError' -- -- Most of these are opaque to the user. The exception details are present for -- the server logs.--- dispatchErrorMessage :: DispatchError -> Text dispatchErrorMessage = \case   MissingParameter name ->     "Parameter '" <> name <> "' is required, but not present in the URL"-  InvalidStateToken{} -> "State token is invalid, please try again"-  InvalidCallbackUri{} ->+  InvalidStateToken {} -> "State token is invalid, please try again"+  InvalidCallbackUri {} ->     "Callback URI was not valid, this server may be misconfigured (no approot)"   OAuth2HandshakeError er -> "OAuth2 handshake failure: " <> erUserMessage er-  OAuth2ResultError{} -> "Login failed, please try again"-  FetchCredsIOException{} -> "Login failed, please try again"-  FetchCredsYesodOAuth2Exception{} -> "Login failed, please try again"-  OtherDispatchError{} -> "Login failed, please try again"+  OAuth2ResultError {} -> "Login failed, please try again"+  FetchCredsIOException {} -> "Login failed, please try again"+  FetchCredsYesodOAuth2Exception {} -> "Login failed, please try again"+  OtherDispatchError {} -> "Login failed, please try again"  handleDispatchError   :: MonadAuthHandler site m
src/Yesod/Auth/OAuth2/ErrorResponse.hs view
@@ -1,12 +1,12 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | OAuth callback error response -- -- <https://tools.ietf.org/html/rfc6749#section-4.1.2.1>--- module Yesod.Auth.OAuth2.ErrorResponse-  ( ErrorResponse(..)+  ( ErrorResponse (..)   , erUserMessage-  , ErrorName(..)+  , ErrorName (..)   , onErrorResponse   , unknownError   ) where@@ -17,22 +17,22 @@ import Yesod.Core (MonadHandler, lookupGetParam)  data ErrorName-    = InvalidRequest-    | UnauthorizedClient-    | AccessDenied-    | UnsupportedResponseType-    | InvalidScope-    | ServerError-    | TemporarilyUnavailable-    | Unknown Text-    deriving Show+  = InvalidRequest+  | UnauthorizedClient+  | AccessDenied+  | UnsupportedResponseType+  | InvalidScope+  | ServerError+  | TemporarilyUnavailable+  | Unknown Text+  deriving (Show)  data ErrorResponse = ErrorResponse   { erName :: ErrorName   , erDescription :: Maybe Text   , erURI :: Maybe Text   }-  deriving Show+  deriving (Show)  -- | Textual value suitable for display to a User erUserMessage :: ErrorResponse -> Text@@ -48,13 +48,12 @@  unknownError :: Text -> ErrorResponse unknownError x =-  ErrorResponse { erName = Unknown x, erDescription = Nothing, erURI = Nothing }+  ErrorResponse {erName = Unknown x, erDescription = Nothing, erURI = Nothing}  -- | Check query parameters for an error, if found run the given action -- -- The action is expected to use a short-circuit response function like -- @'permissionDenied'@, hence this returning @()@.--- onErrorResponse :: MonadHandler m => (ErrorResponse -> m a) -> m () onErrorResponse f = traverse_ f =<< checkErrorResponse 
src/Yesod/Auth/OAuth2/EveOnline.hs view
@@ -1,16 +1,16 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}+ -- | -- -- OAuth2 plugin for http://eveonline.com -- -- * Authenticates against eveonline -- * Uses EVEs unique account-user-char-hash as credentials identifier--- module Yesod.Auth.OAuth2.EveOnline   ( oauth2Eve   , oauth2EveScoped-  , WidgetType(..)+  , WidgetType (..)   ) where  import Yesod.Auth.OAuth2.Prelude@@ -24,23 +24,24 @@   parseJSON = withObject "User" $ \o -> User <$> o .: "CharacterOwnerHash"  data WidgetType m-    = Plain -- ^ Simple "Login via eveonline" text-    | BigWhite-    | SmallWhite-    | BigBlack-    | SmallBlack-    | Custom (WidgetFor m ())+  = -- | Simple "Login via eveonline" text+    Plain+  | BigWhite+  | SmallWhite+  | BigBlack+  | SmallBlack+  | Custom (WidgetFor m ())  asWidget :: YesodAuth m => WidgetType m -> WidgetFor m () asWidget Plain = [whamlet|Login via eveonline|] asWidget BigWhite =   [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/4PTzeiAshqiM8osU2giO0Y/5cc4cb60bac52422da2e45db87b6819c/EVE_SSO_Login_Buttons_Large_White.png?w=270&h=45">|]-asWidget BigBlack-  = [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/4fSjj56uD6CYwYyus4KmES/4f6385c91e6de56274d99496e6adebab/EVE_SSO_Login_Buttons_Large_Black.png?w=270&h=45">|]-asWidget SmallWhite-  = [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/18BxKSXCymyqY4QKo8KwKe/c2bdded6118472dd587c8107f24104d7/EVE_SSO_Login_Buttons_Small_White.png?w=195&h=30">|]-asWidget SmallBlack-  = [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/12vrPsIMBQi28QwCGOAqGk/33234da7672c6b0cdca394fc8e0b1c2b/EVE_SSO_Login_Buttons_Small_Black.png?w=195&h=30">|]+asWidget BigBlack =+  [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/4fSjj56uD6CYwYyus4KmES/4f6385c91e6de56274d99496e6adebab/EVE_SSO_Login_Buttons_Large_Black.png?w=270&h=45">|]+asWidget SmallWhite =+  [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/18BxKSXCymyqY4QKo8KwKe/c2bdded6118472dd587c8107f24104d7/EVE_SSO_Login_Buttons_Small_White.png?w=195&h=30">|]+asWidget SmallBlack =+  [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/12vrPsIMBQi28QwCGOAqGk/33234da7672c6b0cdca394fc8e0b1c2b/EVE_SSO_Login_Buttons_Small_Black.png?w=195&h=30">|] asWidget (Custom a) = a  pluginName :: Text@@ -57,25 +58,28 @@ oauth2EveScoped scopes widgetType clientId clientSecret =   authOAuth2Widget (asWidget widgetType) pluginName oauth2 $ \manager token ->     do-      (User userId, userResponse) <- authGetProfile-        pluginName-        manager-        token-        "https://login.eveonline.com/oauth/verify"+      (User userId, userResponse) <-+        authGetProfile+          pluginName+          manager+          token+          "https://login.eveonline.com/oauth/verify" -      pure Creds-        { credsPlugin = "eveonline"-          -- FIXME: Preserved bug. See similar comment in Bitbucket provider.-        , credsIdent = T.pack $ show userId-        , credsExtra = setExtra token userResponse-        }+      pure+        Creds+          { credsPlugin = "eveonline"+          , -- FIXME: Preserved bug. See similar comment in Bitbucket provider.+            credsIdent = T.pack $ show userId+          , credsExtra = setExtra token userResponse+          }  where-  oauth2 = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint =-      "https://login.eveonline.com/oauth/authorize"-        `withQuery` [("response_type", "code"), scopeParam " " scopes]-    , oauth2TokenEndpoint = "https://login.eveonline.com/oauth/token"-    , oauth2RedirectUri = Nothing-    }+  oauth2 =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint =+          "https://login.eveonline.com/oauth/authorize"+            `withQuery` [("response_type", "code"), scopeParam " " scopes]+      , oauth2TokenEndpoint = "https://login.eveonline.com/oauth/token"+      , oauth2RedirectUri = Nothing+      }
src/Yesod/Auth/OAuth2/Exception.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE DeriveDataTypeable #-}  module Yesod.Auth.OAuth2.Exception-  ( YesodOAuth2Exception(..)+  ( YesodOAuth2Exception (..)   ) where  import Control.Exception.Safe@@ -9,21 +9,18 @@ import Data.Text (Text)  data YesodOAuth2Exception-    = OAuth2Error Text ByteString-    -- ^ HTTP error during OAuth2 handshake+  = -- | HTTP error during OAuth2 handshake     --     -- Plugin name and JSON-encoded @OAuth2Error@ from @hoauth2@.-    ---    | JSONDecodingError Text String-    -- ^ User profile was not as expected+    OAuth2Error Text ByteString+  | -- | User profile was not as expected     --     -- Plugin name and Aeson parse error message.-    ---    | GenericError Text String-    -- ^ Other error conditions+    JSONDecodingError Text String+  | -- | Other error conditions     --     -- Plugin name and error message.-    ---    deriving (Show, Typeable)+    GenericError Text String+  deriving (Show, Typeable)  instance Exception YesodOAuth2Exception
src/Yesod/Auth/OAuth2/GitHub.hs view
@@ -1,11 +1,11 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- -- OAuth2 plugin for http://github.com -- -- * Authenticates against github -- * Uses github user id as credentials identifier--- module Yesod.Auth.OAuth2.GitHub   ( oauth2GitHub   , oauth2GitHubScoped@@ -32,24 +32,27 @@ oauth2GitHubScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2GitHubScoped scopes clientId clientSecret =   authOAuth2 pluginName oauth2 $ \manager token -> do-    (User userId, userResponse) <- authGetProfile-      pluginName-      manager-      token-      "https://api.github.com/user"+    (User userId, userResponse) <-+      authGetProfile+        pluginName+        manager+        token+        "https://api.github.com/user" -    pure Creds-      { credsPlugin = pluginName-      , credsIdent = T.pack $ show userId-      , credsExtra = setExtra token userResponse-      }+    pure+      Creds+        { credsPlugin = pluginName+        , credsIdent = T.pack $ show userId+        , credsExtra = setExtra token userResponse+        }  where-  oauth2 = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint =-      "https://github.com/login/oauth/authorize"-        `withQuery` [scopeParam "," scopes]-    , oauth2TokenEndpoint = "https://github.com/login/oauth/access_token"-    , oauth2RedirectUri = Nothing-    }+  oauth2 =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint =+          "https://github.com/login/oauth/authorize"+            `withQuery` [scopeParam "," scopes]+      , oauth2TokenEndpoint = "https://github.com/login/oauth/access_token"+      , oauth2RedirectUri = Nothing+      }
src/Yesod/Auth/OAuth2/GitLab.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-}+ module Yesod.Auth.OAuth2.GitLab   ( oauth2GitLab   , oauth2GitLabHostScopes@@ -32,7 +33,6 @@ -- -- > oauth2GitLabHostScopes defaultHost ["api", "read_user"] -- > oauth2GitLabHostScopes "https://gitlab.example.com" defaultScopes--- oauth2GitLab :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2GitLab = oauth2GitLabHostScopes defaultHost defaultScopes @@ -43,17 +43,19 @@     (User userId, userResponse) <-       authGetProfile pluginName manager token $ host `withPath` "/api/v4/user" -    pure Creds-      { credsPlugin = pluginName-      , credsIdent = T.pack $ show userId-      , credsExtra = setExtra token userResponse-      }+    pure+      Creds+        { credsPlugin = pluginName+        , credsIdent = T.pack $ show userId+        , credsExtra = setExtra token userResponse+        }  where-  oauth2 = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint =-      host `withPath` "/oauth/authorize" `withQuery` [scopeParam " " scopes]-    , oauth2TokenEndpoint = host `withPath` "/oauth/token"-    , oauth2RedirectUri = Nothing-    }+  oauth2 =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint =+          host `withPath` "/oauth/authorize" `withQuery` [scopeParam " " scopes]+      , oauth2TokenEndpoint = host `withPath` "/oauth/token"+      , oauth2RedirectUri = Nothing+      }
src/Yesod/Auth/OAuth2/Google.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}+ -- | -- -- OAuth2 plugin for http://www.google.com@@ -23,7 +24,6 @@ -- >         updatedCreds = creds { credsIdent = email } -- > -- >     -- continue normally with updatedCreds--- module Yesod.Auth.OAuth2.Google   ( oauth2Google   , oauth2GoogleWidget@@ -38,9 +38,10 @@  instance FromJSON User where   parseJSON =-    withObject "User" $ \o -> User-      -- Required for data backwards-compatibility-                                   <$> (("google-uid:" <>) <$> o .: "sub")+    withObject "User" $ \o ->+      User+        -- Required for data backwards-compatibility+        <$> (("google-uid:" <>) <$> o .: "sub")  pluginName :: Text pluginName = "google"@@ -63,24 +64,27 @@   :: YesodAuth m => WidgetFor m () -> [Text] -> Text -> Text -> AuthPlugin m oauth2GoogleScopedWidget widget scopes clientId clientSecret =   authOAuth2Widget widget pluginName oauth2 $ \manager token -> do-    (User userId, userResponse) <- authGetProfile-      pluginName-      manager-      token-      "https://www.googleapis.com/oauth2/v3/userinfo"+    (User userId, userResponse) <-+      authGetProfile+        pluginName+        manager+        token+        "https://www.googleapis.com/oauth2/v3/userinfo" -    pure Creds-      { credsPlugin = pluginName-      , credsIdent = userId-      , credsExtra = setExtra token userResponse-      }+    pure+      Creds+        { credsPlugin = pluginName+        , credsIdent = userId+        , credsExtra = setExtra token userResponse+        }  where-  oauth2 = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint =-      "https://accounts.google.com/o/oauth2/auth"-        `withQuery` [scopeParam " " scopes]-    , oauth2TokenEndpoint = "https://www.googleapis.com/oauth2/v3/token"-    , oauth2RedirectUri = Nothing-    }+  oauth2 =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint =+          "https://accounts.google.com/o/oauth2/auth"+            `withQuery` [scopeParam " " scopes]+      , oauth2TokenEndpoint = "https://www.googleapis.com/oauth2/v3/token"+      , oauth2RedirectUri = Nothing+      }
src/Yesod/Auth/OAuth2/Nylas.hs view
@@ -26,41 +26,45 @@ oauth2Nylas :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2Nylas clientId clientSecret =   authOAuth2 pluginName oauth $ \manager token -> do-    req <- applyBasicAuth (encodeUtf8 $ atoken $ accessToken token) ""-      <$> parseRequest "https://api.nylas.com/account"+    req <-+      applyBasicAuth (encodeUtf8 $ atoken $ accessToken token) ""+        <$> parseRequest "https://api.nylas.com/account"     resp <- httpLbs req manager     let userResponse = responseBody resp      -- FIXME: was this working? I'm 95% sure that the client will throw its     -- own exception on unsuccessful status codes.-    unless (HT.statusIsSuccessful $ responseStatus resp)-      $ throwIO-      $ YesodOAuth2Exception.GenericError pluginName-      $ "Unsuccessful HTTP response: "-      <> BL8.unpack userResponse+    unless (HT.statusIsSuccessful $ responseStatus resp) $+      throwIO $+        YesodOAuth2Exception.GenericError pluginName $+          "Unsuccessful HTTP response: "+            <> BL8.unpack userResponse      either-        (throwIO . YesodOAuth2Exception.JSONDecodingError pluginName)-        (\(User userId) -> pure Creds-          { credsPlugin = pluginName-          , credsIdent = userId-          , credsExtra = setExtra token userResponse-          }-        )+      (throwIO . YesodOAuth2Exception.JSONDecodingError pluginName)+      ( \(User userId) ->+          pure+            Creds+              { credsPlugin = pluginName+              , credsIdent = userId+              , credsExtra = setExtra token userResponse+              }+      )       $ eitherDecode userResponse  where-  oauth = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint =-      "https://api.nylas.com/oauth/authorize"-        `withQuery` [ ("response_type", "code")-                    , ("client_id", encodeUtf8 clientId)-          -- N.B. The scopes delimeter is unknown/untested. Verify that before-          -- extracting this to an argument and offering a Scoped function. In-          -- its current state, it doesn't matter because it's only one scope.-                    , scopeParam "," defaultScopes-                    ]-    , oauth2TokenEndpoint = "https://api.nylas.com/oauth/token"-    , oauth2RedirectUri = Nothing-    }+  oauth =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint =+          "https://api.nylas.com/oauth/authorize"+            `withQuery` [ ("response_type", "code")+                        , ("client_id", encodeUtf8 clientId)+                        , -- N.B. The scopes delimeter is unknown/untested. Verify that before+                          -- extracting this to an argument and offering a Scoped function. In+                          -- its current state, it doesn't matter because it's only one scope.+                          scopeParam "," defaultScopes+                        ]+      , oauth2TokenEndpoint = "https://api.nylas.com/oauth/token"+      , oauth2RedirectUri = Nothing+      }
src/Yesod/Auth/OAuth2/Prelude.hs view
@@ -1,10 +1,10 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TupleSections #-}+ -- | -- -- Modules and support functions required by most or all provider -- implementations. May also be useful for writing local providers.--- module Yesod.Auth.OAuth2.Prelude   ( authGetProfile   , scopeParam@@ -20,8 +20,8 @@   , (.:?)   , (.=)   , (<>)-  , FromJSON(..)-  , ToJSON(..)+  , FromJSON (..)+  , ToJSON (..)   , eitherDecode   , withObject @@ -29,22 +29,22 @@   , throwIO      -- * OAuth2-  , OAuth2(..)-  , OAuth2Token(..)-  , AccessToken(..)-  , RefreshToken(..)+  , OAuth2 (..)+  , OAuth2Token (..)+  , AccessToken (..)+  , RefreshToken (..)      -- * HTTP   , Manager      -- * Yesod-  , YesodAuth(..)-  , AuthPlugin(..)-  , Creds(..)+  , YesodAuth (..)+  , AuthPlugin (..)+  , Creds (..)      -- * Bytestring URI types   , URI-  , Host(..)+  , Host (..)      -- * Bytestring URI extensions   , module URI.ByteString.Extension@@ -74,7 +74,6 @@ -- The response should be parsed only far enough to read the required -- @'credsIdent'@. Additional information should either be re-parsed by or -- fetched via additional requests by consumers.--- authGetProfile   :: FromJSON a   => Text@@ -101,7 +100,7 @@  -- | A tuple of @\"scope\"@ and the given scopes separated by a delimiter scopeParam :: Text -> [Text] -> (ByteString, ByteString)-scopeParam d = ("scope", ) . encodeUtf8 . T.intercalate d+scopeParam d = ("scope",) . encodeUtf8 . T.intercalate d  -- brittany-disable-next-binding @@ -115,10 +114,9 @@ -- May set the following keys: -- -- - @refreshToken@: if the provider supports refreshing the @accessToken@--- setExtra :: OAuth2Token -> BL.ByteString -> [(Text, Text)] setExtra token userResponse =-    [ ("accessToken", atoken $ accessToken token)-    , ("userResponse", decodeUtf8 $ BL.toStrict userResponse)-    ]-    <> maybe [] (pure . ("refreshToken", ) . rtoken) (refreshToken token)+  [ ("accessToken", atoken $ accessToken token)+  , ("userResponse", decodeUtf8 $ BL.toStrict userResponse)+  ]+    <> maybe [] (pure . ("refreshToken",) . rtoken) (refreshToken token)
src/Yesod/Auth/OAuth2/Random.hs view
@@ -5,7 +5,7 @@   ) where  import Crypto.Random (MonadRandom, getRandomBytes)-import Data.ByteArray.Encoding (Base(Base64), convertToBase)+import Data.ByteArray.Encoding (Base (Base64), convertToBase) import Data.ByteString (ByteString) import Data.Text (Text) import Data.Text.Encoding (decodeUtf8)@@ -13,7 +13,7 @@ randomText   :: MonadRandom m   => Int-    -- ^ Size in Bytes (note necessarily characters)+  -- ^ Size in Bytes (not necessarily characters)   -> m Text randomText size =   decodeUtf8 . convertToBase @ByteString Base64 <$> getRandomBytes size
src/Yesod/Auth/OAuth2/Salesforce.hs view
@@ -1,11 +1,11 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- -- OAuth2 plugin for http://login.salesforce.com -- -- * Authenticates against Salesforce (or sandbox) -- * Uses Salesforce user id as credentials identifier--- module Yesod.Auth.OAuth2.Salesforce   ( oauth2Salesforce   , oauth2SalesforceScoped@@ -30,47 +30,54 @@ oauth2Salesforce = oauth2SalesforceScoped defaultScopes  oauth2SalesforceScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m-oauth2SalesforceScoped = salesforceHelper-  pluginName-  "https://login.salesforce.com/services/oauth2/userinfo"-  "https://login.salesforce.com/services/oauth2/authorize"-  "https://login.salesforce.com/services/oauth2/token"+oauth2SalesforceScoped =+  salesforceHelper+    pluginName+    "https://login.salesforce.com/services/oauth2/userinfo"+    "https://login.salesforce.com/services/oauth2/authorize"+    "https://login.salesforce.com/services/oauth2/token"  oauth2SalesforceSandbox :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2SalesforceSandbox = oauth2SalesforceSandboxScoped defaultScopes  oauth2SalesforceSandboxScoped   :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m-oauth2SalesforceSandboxScoped = salesforceHelper-  (pluginName <> "-sandbox")-  "https://test.salesforce.com/services/oauth2/userinfo"-  "https://test.salesforce.com/services/oauth2/authorize"-  "https://test.salesforce.com/services/oauth2/token"+oauth2SalesforceSandboxScoped =+  salesforceHelper+    (pluginName <> "-sandbox")+    "https://test.salesforce.com/services/oauth2/userinfo"+    "https://test.salesforce.com/services/oauth2/authorize"+    "https://test.salesforce.com/services/oauth2/token"  salesforceHelper   :: YesodAuth m   => Text-  -> URI -- ^ User profile-  -> URI -- ^ Authorize-  -> URI -- ^ Token+  -> URI+  -- ^ User profile+  -> URI+  -- ^ Authorize+  -> URI+  -- ^ Token   -> [Text]   -> Text   -> Text   -> AuthPlugin m-salesforceHelper name profileUri authorizeUri tokenUri scopes clientId clientSecret-  = authOAuth2 name oauth2 $ \manager token -> do+salesforceHelper name profileUri authorizeUri tokenUri scopes clientId clientSecret =+  authOAuth2 name oauth2 $ \manager token -> do     (User userId, userResponse) <- authGetProfile name manager token profileUri -    pure Creds-      { credsPlugin = pluginName-      , credsIdent = userId-      , credsExtra = setExtra token userResponse-      }+    pure+      Creds+        { credsPlugin = pluginName+        , credsIdent = userId+        , credsExtra = setExtra token userResponse+        }  where-  oauth2 = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint = authorizeUri `withQuery` [scopeParam " " scopes]-    , oauth2TokenEndpoint = tokenUri-    , oauth2RedirectUri = Nothing-    }+  oauth2 =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint = authorizeUri `withQuery` [scopeParam " " scopes]+      , oauth2TokenEndpoint = tokenUri+      , oauth2RedirectUri = Nothing+      }
src/Yesod/Auth/OAuth2/Slack.hs view
@@ -1,12 +1,12 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- OAuth2 plugin for https://slack.com/ -- -- * Authenticates against slack -- * Uses slack user id as credentials identifier--- module Yesod.Auth.OAuth2.Slack-  ( SlackScope(..)+  ( SlackScope (..)   , oauth2Slack   , oauth2SlackScoped   ) where@@ -14,14 +14,18 @@ import Yesod.Auth.OAuth2.Prelude  import Network.HTTP.Client-    (httpLbs, parseUrlThrow, responseBody, setQueryString)+  ( httpLbs+  , parseUrlThrow+  , responseBody+  , setQueryString+  ) import Yesod.Auth.OAuth2.Exception as YesodOAuth2Exception  data SlackScope-    = SlackBasicScope-    | SlackEmailScope-    | SlackTeamScope-    | SlackAvatarScope+  = SlackBasicScope+  | SlackEmailScope+  | SlackTeamScope+  | SlackAvatarScope  scopeText :: SlackScope -> Text scopeText SlackBasicScope = "identity.basic"@@ -50,26 +54,30 @@ oauth2SlackScoped scopes clientId clientSecret =   authOAuth2 pluginName oauth2 $ \manager token -> do     let param = encodeUtf8 $ atoken $ accessToken token-    req <- setQueryString [("token", Just param)]-      <$> parseUrlThrow "https://slack.com/api/users.identity"+    req <-+      setQueryString [("token", Just param)]+        <$> parseUrlThrow "https://slack.com/api/users.identity"     userResponse <- responseBody <$> httpLbs req manager      either-        (throwIO . YesodOAuth2Exception.JSONDecodingError pluginName)-        (\(User userId) -> pure Creds-          { credsPlugin = pluginName-          , credsIdent = userId-          , credsExtra = setExtra token userResponse-          }-        )+      (throwIO . YesodOAuth2Exception.JSONDecodingError pluginName)+      ( \(User userId) ->+          pure+            Creds+              { credsPlugin = pluginName+              , credsIdent = userId+              , credsExtra = setExtra token userResponse+              }+      )       $ eitherDecode userResponse  where-  oauth2 = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint =-      "https://slack.com/oauth/authorize"-        `withQuery` [scopeParam "," $ map scopeText scopes]-    , oauth2TokenEndpoint = "https://slack.com/api/oauth.access"-    , oauth2RedirectUri = Nothing-    }+  oauth2 =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint =+          "https://slack.com/oauth/authorize"+            `withQuery` [scopeParam "," $ map scopeText scopes]+      , oauth2TokenEndpoint = "https://slack.com/api/oauth.access"+      , oauth2RedirectUri = Nothing+      }
src/Yesod/Auth/OAuth2/Spotify.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- -- OAuth2 plugin for http://spotify.com--- module Yesod.Auth.OAuth2.Spotify   ( oauth2Spotify   ) where@@ -20,24 +20,27 @@ oauth2Spotify :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2Spotify scopes clientId clientSecret =   authOAuth2 pluginName oauth2 $ \manager token -> do-    (User userId, userResponse) <- authGetProfile-      pluginName-      manager-      token-      "https://api.spotify.com/v1/me"+    (User userId, userResponse) <-+      authGetProfile+        pluginName+        manager+        token+        "https://api.spotify.com/v1/me" -    pure Creds-      { credsPlugin = pluginName-      , credsIdent = userId-      , credsExtra = setExtra token userResponse-      }+    pure+      Creds+        { credsPlugin = pluginName+        , credsIdent = userId+        , credsExtra = setExtra token userResponse+        }  where-  oauth2 = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint =-      "https://accounts.spotify.com/authorize"-        `withQuery` [scopeParam " " scopes]-    , oauth2TokenEndpoint = "https://accounts.spotify.com/api/token"-    , oauth2RedirectUri = Nothing-    }+  oauth2 =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint =+          "https://accounts.spotify.com/authorize"+            `withQuery` [scopeParam " " scopes]+      , oauth2TokenEndpoint = "https://accounts.spotify.com/api/token"+      , oauth2RedirectUri = Nothing+      }
src/Yesod/Auth/OAuth2/Twitch.hs view
@@ -1,11 +1,11 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- -- OAuth2 plugin for http://twitch.tv -- -- * Authenticates against twitch -- * Uses twitch user id as credentials identifier--- module Yesod.Auth.OAuth2.Twitch   ( oauth2Twitch   , oauth2TwitchScoped@@ -32,28 +32,31 @@ oauth2TwitchScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2TwitchScoped scopes clientId clientSecret =   authOAuth2 pluginName oauth2 $ \manager token -> do-    (User userId, userResponse) <- authGetProfile-      pluginName-      manager-      token-      "https://id.twitch.tv/oauth2/validate"+    (User userId, userResponse) <-+      authGetProfile+        pluginName+        manager+        token+        "https://id.twitch.tv/oauth2/validate" -    pure Creds-      { credsPlugin = pluginName-      , credsIdent = userId-      , credsExtra = setExtra token userResponse-      }+    pure+      Creds+        { credsPlugin = pluginName+        , credsIdent = userId+        , credsExtra = setExtra token userResponse+        }  where-  oauth2 = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint =-      "https://id.twitch.tv/oauth2/authorize"-        `withQuery` [scopeParam " " scopes]-    , oauth2TokenEndpoint =-      "https://id.twitch.tv/oauth2/token"-        `withQuery` [ ("client_id", T.encodeUtf8 clientId)-                    , ("client_secret", T.encodeUtf8 clientSecret)-                    ]-    , oauth2RedirectUri = Nothing-    }+  oauth2 =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint =+          "https://id.twitch.tv/oauth2/authorize"+            `withQuery` [scopeParam " " scopes]+      , oauth2TokenEndpoint =+          "https://id.twitch.tv/oauth2/token"+            `withQuery` [ ("client_id", T.encodeUtf8 clientId)+                        , ("client_secret", T.encodeUtf8 clientSecret)+                        ]+      , oauth2RedirectUri = Nothing+      }
src/Yesod/Auth/OAuth2/Upcase.hs view
@@ -1,11 +1,11 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- -- OAuth2 plugin for http://upcase.com -- -- * Authenticates against upcase -- * Uses upcase user id as credentials identifier--- module Yesod.Auth.OAuth2.Upcase   ( oauth2Upcase   ) where@@ -27,22 +27,25 @@ oauth2Upcase :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2Upcase clientId clientSecret =   authOAuth2 pluginName oauth2 $ \manager token -> do-    (User userId, userResponse) <- authGetProfile-      pluginName-      manager-      token-      "http://upcase.com/api/v1/me.json"+    (User userId, userResponse) <-+      authGetProfile+        pluginName+        manager+        token+        "http://upcase.com/api/v1/me.json" -    pure Creds-      { credsPlugin = pluginName-      , credsIdent = T.pack $ show userId-      , credsExtra = setExtra token userResponse-      }+    pure+      Creds+        { credsPlugin = pluginName+        , credsIdent = T.pack $ show userId+        , credsExtra = setExtra token userResponse+        }  where-  oauth2 = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint = "http://upcase.com/oauth/authorize"-    , oauth2TokenEndpoint = "http://upcase.com/oauth/token"-    , oauth2RedirectUri = Nothing-    }+  oauth2 =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint = "http://upcase.com/oauth/authorize"+      , oauth2TokenEndpoint = "http://upcase.com/oauth/token"+      , oauth2RedirectUri = Nothing+      }
src/Yesod/Auth/OAuth2/WordPressDotCom.hs view
@@ -16,31 +16,35 @@   parseJSON = withObject "WpUser" $ \o -> WpUser <$> o .: "ID"  oauth2WordPressDotCom-  :: (YesodAuth m)-  => Text -- ^ Client Id-  -> Text -- ^ Client Secret+  :: YesodAuth m+  => Text+  -- ^ Client Id+  -> Text+  -- ^ Client Secret   -> AuthPlugin m oauth2WordPressDotCom clientId clientSecret =   authOAuth2 pluginName oauth2 $ \manager token -> do-    (WpUser userId, userResponse) <- authGetProfile-      pluginName-      manager-      token-      "https://public-api.wordpress.com/rest/v1/me/"--    pure Creds-      { credsPlugin = pluginName-      , credsIdent = T.pack $ show userId-      , credsExtra = setExtra token userResponse-      }+    (WpUser userId, userResponse) <-+      authGetProfile+        pluginName+        manager+        token+        "https://public-api.wordpress.com/rest/v1/me/" +    pure+      Creds+        { credsPlugin = pluginName+        , credsIdent = T.pack $ show userId+        , credsExtra = setExtra token userResponse+        }  where-  oauth2 = OAuth2-    { oauth2ClientId = clientId-    , oauth2ClientSecret = Just clientSecret-    , oauth2AuthorizeEndpoint =-      "https://public-api.wordpress.com/oauth2/authorize"-        `withQuery` [scopeParam "," ["auth"]]-    , oauth2TokenEndpoint = "https://public-api.wordpress.com/oauth2/token"-    , oauth2RedirectUri = Nothing-    }+  oauth2 =+    OAuth2+      { oauth2ClientId = clientId+      , oauth2ClientSecret = Just clientSecret+      , oauth2AuthorizeEndpoint =+          "https://public-api.wordpress.com/oauth2/authorize"+            `withQuery` [scopeParam "," ["auth"]]+      , oauth2TokenEndpoint = "https://public-api.wordpress.com/oauth2/token"+      , oauth2RedirectUri = Nothing+      }
test/URI/ByteString/ExtensionSpec.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}+ module URI.ByteString.ExtensionSpec   ( spec   ) where@@ -61,9 +62,8 @@       uriWithQuery `shouldBe` [uri|http://example.com?foo=bar|]      it "handles a URI with an existing query" $ do-      let-        uriWithQuery =-          [uri|http://example.com?foo=bar|] `withQuery` [("baz", "bat")]+      let uriWithQuery =+            [uri|http://example.com?foo=bar|] `withQuery` [("baz", "bat")]        uriWithQuery `shouldBe` [uri|http://example.com?foo=bar&baz=bat|] @@ -71,9 +71,8 @@     -- it's worthwhile to show that you don't (and can't) pre-sanitize when     -- using this function.     it "handles santization of the query" $ do-      let-        uriWithQuery =-          [uri|http://example.com|] `withQuery` [("foo", "bar baz")]+      let uriWithQuery =+            [uri|http://example.com|] `withQuery` [("foo", "bar baz")]        toText uriWithQuery `shouldBe` "http://example.com?foo=bar%20baz" 
yesod-auth-oauth2.cabal view
@@ -1,6 +1,6 @@ cabal-version:   1.18 name:            yesod-auth-oauth2-version:         0.7.1.0+version:         0.7.1.1 license:         MIT license-file:    LICENSE maintainer:      engineering@freckle.com