From fbe0e37d281e19bcdbb926eb9a128c69186dd596 Mon Sep 17 00:00:00 2001 From: David Mosbach Date: Tue, 5 Mar 2024 23:57:10 +0000 Subject: [PATCH] feat(auth): oidc based sso for auth protected routes --- config/settings.yml | 4 +++- src/Auth/OAuth2.hs | 22 ++++++++++++++-------- src/Foundation/Instances.hs | 21 ++++++++++++++------- src/Settings.hs | 5 ++++- 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/config/settings.yml b/config/settings.yml index 602c9c0e2..28858440b 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 Gregor Kleen ,Sarah Vaupel ,Steffen Jost ,Wolfgang Witt +# SPDX-FileCopyrightText: 2022-2024 Gregor Kleen ,Sarah Vaupel ,Steffen Jost ,Wolfgang Witt ,David Mosbach # # SPDX-License-Identifier: AGPL-3.0-or-later @@ -131,6 +131,8 @@ database: auto-db-migrate: '_env:AUTO_DB_MIGRATE:true' +single-sign-on: "_env:OIDC_SSO:true" + ldap: - host: "_env:LDAPHOST:" tls: "_env:LDAPTLS:" diff --git a/src/Auth/OAuth2.hs b/src/Auth/OAuth2.hs index fab04ca16..613a1ddd5 100644 --- a/src/Auth/OAuth2.hs +++ b/src/Auth/OAuth2.hs @@ -8,7 +8,7 @@ module Auth.OAuth2 ( AzureUserException(..) , azurePluginName , oauth2MockServer -, mockPluginName +, mockPluginName , queryOAuth2User , UserDataException ) where @@ -36,9 +36,9 @@ instance Exception AzureUserException azurePluginName :: Text azurePluginName = "azureadv2" ----------------------------------------- ----- OAuth2 development auth plugin ---- ----------------------------------------- +----------------------------------------------- +---- OAuth2 + OIDC development auth plugin ---- +----------------------------------------------- mockPluginName :: Text mockPluginName = "dev-oauth2-mock" @@ -53,7 +53,11 @@ oauth2MockServer port = let oa = OAuth2 { oauth2ClientId = "42" , oauth2ClientSecret = Just "shhh" - , oauth2AuthorizeEndpoint = (fromString $ mockServerURL <> "/auth") `withQuery` [scopeParam " " ["ID", "Profile"]] + , oauth2AuthorizeEndpoint = (fromString $ mockServerURL <> "/auth") + `withQuery` [ scopeParam " " ["openid", "profile", "email", "offline_access"] -- TODO read scopes from config + , ("response_type", "code id_token") + , ("nonce", "Foo") -- TODO generate meaningful value + ] , oauth2TokenEndpoint = fromString $ mockServerURL <> "/token" , oauth2RedirectUri = Nothing } @@ -94,7 +98,8 @@ queryOAuth2User userID = runExceptT $ do setSessionJson SessionOAuth2Token (Just $ accessToken newTokens, refreshToken newTokens) eResult <- lift $ getResponseBody <$> httpJSONEither @m @j (req { secure = secure - , requestHeaders = [("Authorization", encodeUtf8 . ("Bearer " <>) . atoken $ accessToken newTokens)] }) + , requestHeaders = [("Authorization", encodeUtf8 . ("Bearer " <>) . atoken $ accessToken newTokens)] + }) case eResult of Left x -> throwE $ UserDataJSONException x Right x -> return x @@ -130,8 +135,8 @@ refreshOAuth2Token (_, rToken) url secure body' <- if secure then do clientID <- liftIO $ fromJust <$> lookupEnv "CLIENT_ID" clientSecret <- liftIO $ fromJust <$> lookupEnv "CLIENT_SECRET" - return $ body ++ [("client_id", fromString clientID), ("client_secret", fromString clientSecret), ("scope", "openid profile")] - else return $ ("scope", "ID Profile") : body + return $ body ++ [("client_id", fromString clientID), ("client_secret", fromString clientSecret), ("scope", "openid profile offline_access")] -- TODO read from config + else return $ ("scope", "openid profile offline_access") : body -- TODO read from config $logErrorS "\27[31mAdmin Handler\27[0m" $ tshow (requestBody $ urlEncodedBody body' req{ secure = secure }) eResult <- lift $ getResponseBody <$> httpJSONEither @m @OAuth2Token (urlEncodedBody body' req{ secure = secure }) case eResult of @@ -142,3 +147,4 @@ refreshOAuth2Token (_, rToken) url secure instance Show RequestBody where show (RequestBodyLBS x) = show x show _ = error ":(" + diff --git a/src/Foundation/Instances.hs b/src/Foundation/Instances.hs index 49b6b5de9..ca9dc9ad3 100644 --- a/src/Foundation/Instances.hs +++ b/src/Foundation/Instances.hs @@ -11,11 +11,14 @@ module Foundation.Instances , unsafeHandler ) where +import qualified Prelude as P + import Import.NoFoundation import qualified Data.Text as Text import Data.List (inits) +import Yesod.Auth.OAuth2 import qualified Yesod.Core.Unsafe as Unsafe import qualified Yesod.Auth.Message as Auth @@ -23,6 +26,7 @@ import Utils.Form import Auth.LDAP import Auth.PWHash import Auth.Dummy +import Auth.OAuth2 import qualified Foundation.Yesod.Session as UniWorX import qualified Foundation.Yesod.Middleware as UniWorX @@ -133,17 +137,20 @@ instance YesodAuth UniWorX where redirectToReferer _ = True loginHandler = do + plugins <- getsYesod authPlugins + AppSettings{..} <- getsYesod appSettings' + + when appSingleSignOn $ do + let plugin = P.head $ P.filter ((`elem` [mockPluginName, azurePluginName]) . apName) plugins + pieces = case oauth2Url (apName plugin) of + PluginR _ p -> p + _ -> error "Unexpected OAuth2 AuthRoute" + void $ apDispatch plugin "GET" pieces + toParent <- getRouteToParent liftHandler . defaultLayout $ do - plugins <- getsYesod authPlugins $logDebugS "Auth" $ "Enabled plugins: " <> Text.intercalate ", " (map apName plugins) - -#ifdef DEVELOPMENT mPort <- liftIO $ lookupEnv "OAUTH2_SERVER_PORT" -#else - let mPort = Nothing -#endif - setTitleI MsgLoginTitle $(widgetFile "login") diff --git a/src/Settings.hs b/src/Settings.hs index e3fcc6105..5dadb7646 100644 --- a/src/Settings.hs +++ b/src/Settings.hs @@ -1,4 +1,4 @@ --- SPDX-FileCopyrightText: 2022 Gregor Kleen ,Sarah Vaupel ,Steffen Jost +-- SPDX-FileCopyrightText: 2022-2024 Gregor Kleen ,Sarah Vaupel ,Steffen Jost ,David Mosbach -- -- SPDX-License-Identifier: AGPL-3.0-or-later @@ -96,6 +96,8 @@ data AppSettings = AppSettings , appDatabaseConf :: PostgresConf -- ^ Configuration settings for accessing the database. , appAutoDbMigrate :: Bool + , appSingleSignOn :: Bool + -- ^ Enable OIDC single sign-on , appLdapConf :: Maybe (PointedList LdapConf) -- ^ Configuration settings for CSV export/import to LMS (= Learn Management System) , appLmsConf :: LmsConf @@ -627,6 +629,7 @@ instance FromJSON AppSettings where appWebpackEntrypoints <- o .: "webpack-manifest" appDatabaseConf <- o .: "database" appAutoDbMigrate <- o .: "auto-db-migrate" + appSingleSignOn <- o .: "single-sign-on" let nonEmptyHost LdapConf{..} = case ldapHost of Ldap.Tls host _ -> not $ null host Ldap.Plain host -> not $ null host