feat(auth): tokens can be stored & refreshed

This commit is contained in:
David Mosbach 2024-01-30 05:06:06 +00:00
parent 5a023a9e32
commit c8fa509ace
3 changed files with 58 additions and 19 deletions

View File

@ -62,7 +62,7 @@ import Jobs
import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding as Text
import qualified Data.Text as Text import qualified Data.Text as Text
import Yesod.Auth.OAuth2.AzureADv2 (oauth2AzureADv2) import Yesod.Auth.OAuth2.AzureADv2 (oauth2AzureADv2Scoped)
import Yesod.Auth.Util.PasswordStore import Yesod.Auth.Util.PasswordStore
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
@ -338,7 +338,7 @@ makeFoundation appSettings''@AppSettings{..} = do
appAuthPlugins <- liftIO $ sequence [ appAuthPlugins <- liftIO $ sequence [
(oauth2MockServer . fromJust) <$> lookupEnv "OAUTH2_SERVER_PORT" (oauth2MockServer . fromJust) <$> lookupEnv "OAUTH2_SERVER_PORT"
, loadPlugin (oauth2AzureADv2 tenantID) "AZURE_ADV2" , loadPlugin (oauth2AzureADv2Scoped ["openid", "profile", "offline_access"] tenantID) "AZURE_ADV2"
] ]

View File

@ -9,9 +9,10 @@ module Auth.OAuth2
, azurePluginName , azurePluginName
, oauth2MockServer , oauth2MockServer
, mockPluginName , mockPluginName
, queryOauth2User , queryOAuth2User
) where ) where
import Data.Maybe (fromJust)
import Data.Text import Data.Text
import Import.NoFoundation hiding (unpack) import Import.NoFoundation hiding (unpack)
@ -21,7 +22,7 @@ import Network.HTTP.Simple (httpJSONEither, getResponseBody, JSONException)
import System.Environment (lookupEnv) import System.Environment (lookupEnv)
import Yesod.Auth.OAuth2 import Yesod.Auth.OAuth2
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude hiding (encodeUtf8)
data AzureUserException = AzureUserError data AzureUserException = AzureUserError
@ -73,21 +74,58 @@ data UserData = UD
instance FromJSON UserData where instance FromJSON UserData where
parseJSON _ = pure UD parseJSON _ = pure UD
queryOauth2User :: forall m . (MonadIO m, MonadThrow m) queryOAuth2User :: forall m . (MonadIO m, MonadThrow m, MonadHandler m, MonadFail m)
=> Text => Text
-> Text -> Text
-> m (Either JSONException UserData) -> m (Either JSONException UserData)
queryOauth2User authPlugin userID = do queryOAuth2User authPlugin userID = do
baseUrl <- liftIO mkBaseUrl (queryUrl, tokenUrl) <- liftIO $ mkBaseUrls authPlugin
req <- parseRequest $ "GET " ++ baseUrl ++ unpack userID req <- parseRequest $ "GET " ++ queryUrl ++ unpack userID
-- TODO get new token & put token in auth header mTokens <- lookupSessionJson SessionOAuth2Token
getResponseBody <$> httpJSONEither @m @UserData req unless (isJust mTokens) . fail $ "Tried to load sesion Oauth2 tokens, but there are none"
where eNewToken <- refreshOAuth2Token @m (fromJust mTokens) tokenUrl (authPlugin == azurePluginName)
mkBaseUrl :: IO String case eNewToken of
mkBaseUrl Left e -> return $ Left e
| authPlugin == azurePluginName = return "https://graph.microsoft.com/v1.0/users/" Right newTokens -> do
| authPlugin == mockPluginName = do setSessionJson SessionOAuth2Token newTokens
Just port <- lookupEnv "OAUTH2_SERVER_PORT" getResponseBody <$> httpJSONEither @m @UserData (req
return $ "http://localhost:" ++ port ++ "/users/query?id=" { secure = authPlugin == azurePluginName
| otherwise = fail $ unpack authPlugin , requestHeaders = [("Authorization", encodeUtf8 . atoken . fromJust $ fst newTokens)] })
mkBaseUrls :: Text -> IO (String, String)
mkBaseUrls authPlugin
| authPlugin == azurePluginName = do
Just tenantID <- lookupEnv "AZURE_TENANT_ID"
return ( "https://graph.microsoft.com/v1.0/users/"
, "https://login.microsoftonline.com/" ++ tenantID ++ "/oauth2/v2.0" )
| authPlugin == mockPluginName = do
Just port <- lookupEnv "OAUTH2_SERVER_PORT"
let base = "http://localhost:" ++ port
return ( base ++ "/users/query?id="
, base ++ "/token" )
| otherwise = fail $ "Unsupported auth plugin: " ++ unpack authPlugin
refreshOAuth2Token :: forall m x. (MonadIO m, MonadThrow m, MonadHandler m, MonadFail m, x ~ (Maybe AccessToken, Maybe RefreshToken))
=> x
-> String
-> Bool
-> m (Either JSONException x)
refreshOAuth2Token (_, refreshToken) url secure
| isJust refreshToken = do
req <- parseRequest $ "POST " ++ url
let
body =
[ ("grant_type", "refresh_token")
, ("refresh_token", encodeUtf8 . rtoken $ fromJust refreshToken)
, ("scope", "") -- TODO must be subset of previously requested scopes. space separated list
]
body' <- if secure then do
Just clientID <- liftIO $ lookupEnv "CLIENT_ID"
Just clientSecret <- liftIO $ lookupEnv "CLIENT_SECRET"
return $ body ++ [("client_id", fromString clientID), ("client_secret", fromString clientSecret)]
else return body
getResponseBody <$> httpJSONEither @m @x (urlEncodedBody body' req{ secure = secure })
| otherwise = fail "Could not refresh access token. Refresh token is missing."

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de> -- SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@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
@ -20,6 +20,7 @@ data SessionKey = SessionActiveAuthTags | SessionInactiveAuthTags
| SessionLang | SessionLang
| SessionError | SessionError
| SessionFiles | SessionFiles
| SessionOAuth2Token
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic) deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
deriving anyclass (Universe, Finite) deriving anyclass (Universe, Finite)