generate encrypted tokens

This commit is contained in:
David Mosbach 2024-01-09 03:57:03 +01:00
parent 23a5a93509
commit 751f3a463a
4 changed files with 63 additions and 15 deletions

View File

@ -35,6 +35,7 @@ library
, bytestring , bytestring
, containers , containers
, http-client , http-client
, jose-jwt
, servant , servant
, servant-client , servant-client
, servant-server , servant-server
@ -60,6 +61,7 @@ executable oauth2-mock-server-exe
, bytestring , bytestring
, containers , containers
, http-client , http-client
, jose-jwt
, oauth2-mock-server , oauth2-mock-server
, servant , servant
, servant-client , servant-client
@ -87,6 +89,7 @@ test-suite oauth2-mock-server-test
, bytestring , bytestring
, containers , containers
, http-client , http-client
, jose-jwt
, oauth2-mock-server , oauth2-mock-server
, servant , servant
, servant-client , servant-client

View File

@ -33,6 +33,7 @@ dependencies:
- time - time
- transformers - transformers
- bytestring - bytestring
- jose-jwt
ghc-options: ghc-options:
- -Wall - -Wall

View File

@ -18,10 +18,16 @@ import Control.Concurrent.STM.TVar
import Control.Monad (void, (>=>)) import Control.Monad (void, (>=>))
import Control.Monad.STM import Control.Monad.STM
import Jose.Jwk (Jwk(..))
newtype State = State { activeCodes :: Map String (String, UTCTime) } deriving Show -- ^ maps auth codes to (client ID, expiration time)
data State = State
{ activeCodes :: Map String (String, UTCTime) -- ^ maps auth codes to (client ID, expiration time)
, publicKey :: Jwk
, privateKey :: Jwk
} deriving Show
type AuthState = TVar State type AuthState = TVar State

View File

@ -1,4 +1,4 @@
{-# LANGUAGE DataKinds, TypeOperators, OverloadedStrings, ScopedTypeVariables, TypeApplications #-} {-# LANGUAGE DataKinds, TypeOperators, OverloadedStrings, ScopedTypeVariables, TypeApplications, RecordWildCards #-}
module Server module Server
( insecureOAuthMock' ( insecureOAuthMock'
@ -11,23 +11,30 @@ import User
import Control.Applicative ((<|>)) import Control.Applicative ((<|>))
import Control.Concurrent import Control.Concurrent
import Control.Concurrent.STM.TVar (newTVarIO) import Control.Concurrent.STM (atomically)
import Control.Concurrent.STM.TVar (newTVarIO, readTVar)
import Control.Exception (bracket) import Control.Exception (bracket)
import Control.Monad (unless) import Control.Monad (unless)
import Control.Monad.IO.Class import Control.Monad.IO.Class
import Control.Monad.Trans.Reader import Control.Monad.Trans.Reader
import Data.Aeson import Data.Aeson
import Data.ByteString (ByteString (..)) import Data.ByteString (ByteString (..), toStrict)
import Data.List (find, elemIndex) import Data.List (find, elemIndex)
import Data.Maybe (fromMaybe, isJust) import Data.Maybe (fromMaybe, isJust)
import Data.String (IsString (..)) import Data.String (IsString (..))
import Data.Text hiding (elem, find, head, length, map, null, splitAt, tail, words) import Data.Text hiding (elem, find, head, length, map, null, splitAt, tail, words)
import Data.Text.Encoding (decodeUtf8) import Data.Text.Encoding (decodeUtf8)
import Data.Time.Clock (NominalDiffTime (..), nominalDay) import Data.Time.Clock (NominalDiffTime (..), nominalDay, UTCTime(..), getCurrentTime, addUTCTime)
import qualified Data.ByteString.Char8 as BS
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import Jose.Jwa
import Jose.Jwe
import Jose.Jwk (generateRsaKeyPair, generateSymmetricKey, KeyUse(Enc), KeyId)
import Jose.Jwt hiding (decode, encode)
import Network.HTTP.Client (newManager, defaultManagerSettings) import Network.HTTP.Client (newManager, defaultManagerSettings)
import Network.Wai.Handler.Warp import Network.Wai.Handler.Warp
@ -148,17 +155,23 @@ frontend ba = client authAPI ba "[ID]" "42" "code" ""
runMockServer :: Int -> IO () runMockServer :: Int -> IO ()
runMockServer port = do runMockServer port = do
state <- newTVarIO $ State { activeCodes = Map.empty } state <- mkState
run port $ insecureOAuthMock' testUsers state run port $ insecureOAuthMock' testUsers state
runMockServer' :: Int -> IO () runMockServer' :: Int -> IO ()
runMockServer' port = do runMockServer' port = do
mgr <- newManager defaultManagerSettings mgr <- newManager defaultManagerSettings
state <- newTVarIO $ State { activeCodes = Map.empty } state <- mkState
bracket (forkIO . run port $ insecureOAuthMock' testUsers state) killThread $ \_ -> bracket (forkIO . run port $ insecureOAuthMock' testUsers state) killThread $ \_ ->
runClientM (frontend $ BasicAuthData "foo@bar.com" "0000") (mkClientEnv mgr (BaseUrl Http "localhost" port "")) runClientM (frontend $ BasicAuthData "foo@bar.com" "0000") (mkClientEnv mgr (BaseUrl Http "localhost" port ""))
>>= print >>= print
mkState :: IO AuthState
mkState = do
(publicKey, privateKey) <- generateRsaKeyPair 256 (KeyId "Oauth2MockKey") Enc Nothing
let activeCodes = Map.empty
newTVarIO State{..}
------ ------
------ Token ------ Token
@ -200,26 +213,51 @@ instance FromJSON GrantType where
| otherwise = error $ show s ++ " grant type not supported yet" | otherwise = error $ show s ++ " grant type not supported yet"
data JWT = JWT data JWT = JWT
{ token :: Text -- TODO should be JWT { issuer :: Text
, tokenType :: Text -- TODO enum , expiration :: UTCTime
, expiration :: NominalDiffTime } deriving (Show, Eq)
}
instance ToJSON JWT where
toJSON (JWT i e) = object ["iss" .= i, "exp" .= e]
data JWTWrapper = JWTW
{ token :: String
, expiresIn :: NominalDiffTime
} deriving (Show)
instance ToJSON JWTWrapper where
toJSON (JWTW t e) = object ["access_token" .= t, "token_type" .= ("JWT" :: Text), "expires_in" .= e]
type Token = "token" type Token = "token"
:> ReqBody '[JSON] ClientData :> ReqBody '[JSON] ClientData
:> Post '[JSON] JWT :> Post '[JSON] JWTWrapper
tokenEndpoint :: AuthServer Token tokenEndpoint :: AuthServer Token
tokenEndpoint = provideToken tokenEndpoint = provideToken
where where
provideToken :: ClientData -> AuthHandler JWT provideToken :: ClientData -> AuthHandler JWTWrapper
provideToken client = case (grantType client) of provideToken client = case (grantType client) of
AuthCode -> do AuthCode -> do
--TODO validate everything
unless (Client (pack $ clientID client) (pack $ clientSecret client) `elem` trustedClients) . unless (Client (pack $ clientID client) (pack $ clientSecret client) `elem` trustedClients) .
throwError $ err500 { errBody = "Invalid client" } throwError $ err500 { errBody = "Invalid client" }
valid <- asks (verify (grant client) (clientID client)) >>= liftIO valid <- asks (verify (grant client) (clientID client)) >>= liftIO
unless valid . throwError $ err500 { errBody = "Invalid authorisation code" } unless valid . throwError $ err500 { errBody = "Invalid authorisation code" }
return JWT {token = "", tokenType = "JWT", expiration = 0.25 * nominalDay} -- return JWT {token = "", tokenType = "JWT", expiration = 0.25 * nominalDay}
token <- asks mkToken >>= liftIO
return token
x -> error $ show x ++ " not supported yet" x -> error $ show x ++ " not supported yet"
mkToken :: AuthState -> IO JWTWrapper
mkToken state = do
privateKey <- atomically $ readTVar state >>= return . privateKey
now <- getCurrentTime
let
lifetime = nominalDay / 4 -- TODO make configurable
jwt = JWT "Oauth2MockServer" (lifetime `addUTCTime` now)
encoded <- jwkEncode RSA_OAEP_256 A128GCM privateKey (Nested . Jwt . toStrict $ encode jwt)
case encoded of
Right (Jwt token) -> return $ JWTW (BS.unpack token) lifetime
Left e -> error $ show e