feat(auth): oidc based sso for auth protected routes

This commit is contained in:
David Mosbach 2024-03-05 23:57:10 +00:00
parent 956464659e
commit fbe0e37d28
4 changed files with 35 additions and 17 deletions

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de> # SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>,David Mosbach <david.mosbach@uniworx.de>
# #
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
@ -131,6 +131,8 @@ database:
auto-db-migrate: '_env:AUTO_DB_MIGRATE:true' auto-db-migrate: '_env:AUTO_DB_MIGRATE:true'
single-sign-on: "_env:OIDC_SSO:true"
ldap: ldap:
- host: "_env:LDAPHOST:" - host: "_env:LDAPHOST:"
tls: "_env:LDAPTLS:" tls: "_env:LDAPTLS:"

View File

@ -8,7 +8,7 @@ module Auth.OAuth2
( AzureUserException(..) ( AzureUserException(..)
, azurePluginName , azurePluginName
, oauth2MockServer , oauth2MockServer
, mockPluginName , mockPluginName
, queryOAuth2User , queryOAuth2User
, UserDataException , UserDataException
) where ) where
@ -36,9 +36,9 @@ instance Exception AzureUserException
azurePluginName :: Text azurePluginName :: Text
azurePluginName = "azureadv2" azurePluginName = "azureadv2"
---------------------------------------- -----------------------------------------------
---- OAuth2 development auth plugin ---- ---- OAuth2 + OIDC development auth plugin ----
---------------------------------------- -----------------------------------------------
mockPluginName :: Text mockPluginName :: Text
mockPluginName = "dev-oauth2-mock" mockPluginName = "dev-oauth2-mock"
@ -53,7 +53,11 @@ oauth2MockServer port =
let oa = OAuth2 let oa = OAuth2
{ oauth2ClientId = "42" { oauth2ClientId = "42"
, oauth2ClientSecret = Just "shhh" , 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" , oauth2TokenEndpoint = fromString $ mockServerURL <> "/token"
, oauth2RedirectUri = Nothing , oauth2RedirectUri = Nothing
} }
@ -94,7 +98,8 @@ queryOAuth2User userID = runExceptT $ do
setSessionJson SessionOAuth2Token (Just $ accessToken newTokens, refreshToken newTokens) setSessionJson SessionOAuth2Token (Just $ accessToken newTokens, refreshToken newTokens)
eResult <- lift $ getResponseBody <$> httpJSONEither @m @j (req eResult <- lift $ getResponseBody <$> httpJSONEither @m @j (req
{ secure = secure { secure = secure
, requestHeaders = [("Authorization", encodeUtf8 . ("Bearer " <>) . atoken $ accessToken newTokens)] }) , requestHeaders = [("Authorization", encodeUtf8 . ("Bearer " <>) . atoken $ accessToken newTokens)]
})
case eResult of case eResult of
Left x -> throwE $ UserDataJSONException x Left x -> throwE $ UserDataJSONException x
Right x -> return x Right x -> return x
@ -130,8 +135,8 @@ refreshOAuth2Token (_, rToken) url secure
body' <- if secure then do body' <- if secure then do
clientID <- liftIO $ fromJust <$> lookupEnv "CLIENT_ID" clientID <- liftIO $ fromJust <$> lookupEnv "CLIENT_ID"
clientSecret <- liftIO $ fromJust <$> lookupEnv "CLIENT_SECRET" clientSecret <- liftIO $ fromJust <$> lookupEnv "CLIENT_SECRET"
return $ body ++ [("client_id", fromString clientID), ("client_secret", fromString clientSecret), ("scope", "openid profile")] return $ body ++ [("client_id", fromString clientID), ("client_secret", fromString clientSecret), ("scope", "openid profile offline_access")] -- TODO read from config
else return $ ("scope", "ID Profile") : body 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 }) $logErrorS "\27[31mAdmin Handler\27[0m" $ tshow (requestBody $ urlEncodedBody body' req{ secure = secure })
eResult <- lift $ getResponseBody <$> httpJSONEither @m @OAuth2Token (urlEncodedBody body' req{ secure = secure }) eResult <- lift $ getResponseBody <$> httpJSONEither @m @OAuth2Token (urlEncodedBody body' req{ secure = secure })
case eResult of case eResult of
@ -142,3 +147,4 @@ refreshOAuth2Token (_, rToken) url secure
instance Show RequestBody where instance Show RequestBody where
show (RequestBodyLBS x) = show x show (RequestBodyLBS x) = show x
show _ = error ":(" show _ = error ":("

View File

@ -11,11 +11,14 @@ module Foundation.Instances
, unsafeHandler , unsafeHandler
) where ) where
import qualified Prelude as P
import Import.NoFoundation import Import.NoFoundation
import qualified Data.Text as Text import qualified Data.Text as Text
import Data.List (inits) import Data.List (inits)
import Yesod.Auth.OAuth2
import qualified Yesod.Core.Unsafe as Unsafe import qualified Yesod.Core.Unsafe as Unsafe
import qualified Yesod.Auth.Message as Auth import qualified Yesod.Auth.Message as Auth
@ -23,6 +26,7 @@ import Utils.Form
import Auth.LDAP import Auth.LDAP
import Auth.PWHash import Auth.PWHash
import Auth.Dummy import Auth.Dummy
import Auth.OAuth2
import qualified Foundation.Yesod.Session as UniWorX import qualified Foundation.Yesod.Session as UniWorX
import qualified Foundation.Yesod.Middleware as UniWorX import qualified Foundation.Yesod.Middleware as UniWorX
@ -133,17 +137,20 @@ instance YesodAuth UniWorX where
redirectToReferer _ = True redirectToReferer _ = True
loginHandler = do 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 toParent <- getRouteToParent
liftHandler . defaultLayout $ do liftHandler . defaultLayout $ do
plugins <- getsYesod authPlugins
$logDebugS "Auth" $ "Enabled plugins: " <> Text.intercalate ", " (map apName plugins) $logDebugS "Auth" $ "Enabled plugins: " <> Text.intercalate ", " (map apName plugins)
#ifdef DEVELOPMENT
mPort <- liftIO $ lookupEnv "OAUTH2_SERVER_PORT" mPort <- liftIO $ lookupEnv "OAUTH2_SERVER_PORT"
#else
let mPort = Nothing
#endif
setTitleI MsgLoginTitle setTitleI MsgLoginTitle
$(widgetFile "login") $(widgetFile "login")

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de> -- SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,David Mosbach <david.mosbach@uniworx.de>
-- --
-- SPDX-License-Identifier: AGPL-3.0-or-later -- SPDX-License-Identifier: AGPL-3.0-or-later
@ -96,6 +96,8 @@ data AppSettings = AppSettings
, appDatabaseConf :: PostgresConf , appDatabaseConf :: PostgresConf
-- ^ Configuration settings for accessing the database. -- ^ Configuration settings for accessing the database.
, appAutoDbMigrate :: Bool , appAutoDbMigrate :: Bool
, appSingleSignOn :: Bool
-- ^ Enable OIDC single sign-on
, appLdapConf :: Maybe (PointedList LdapConf) , appLdapConf :: Maybe (PointedList LdapConf)
-- ^ Configuration settings for CSV export/import to LMS (= Learn Management System) -- ^ Configuration settings for CSV export/import to LMS (= Learn Management System)
, appLmsConf :: LmsConf , appLmsConf :: LmsConf
@ -627,6 +629,7 @@ instance FromJSON AppSettings where
appWebpackEntrypoints <- o .: "webpack-manifest" appWebpackEntrypoints <- o .: "webpack-manifest"
appDatabaseConf <- o .: "database" appDatabaseConf <- o .: "database"
appAutoDbMigrate <- o .: "auto-db-migrate" appAutoDbMigrate <- o .: "auto-db-migrate"
appSingleSignOn <- o .: "single-sign-on"
let nonEmptyHost LdapConf{..} = case ldapHost of let nonEmptyHost LdapConf{..} = case ldapHost of
Ldap.Tls host _ -> not $ null host Ldap.Tls host _ -> not $ null host
Ldap.Plain host -> not $ null host Ldap.Plain host -> not $ null host