feat(auth): oidc based sso for auth protected routes
This commit is contained in:
parent
956464659e
commit
fbe0e37d28
@ -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:"
|
||||||
|
|||||||
@ -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 ":("
|
||||||
|
|
||||||
|
|||||||
@ -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")
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user