From 2c0c484839890c3b12433935c1c3a20a7db13a3e Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 6 Dec 2022 13:32:11 +0000 Subject: [PATCH 01/27] added jose package --- services/brig/brig.cabal | 1 + 1 file changed, 1 insertion(+) diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 45f4b73565..226001a4ef 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -238,6 +238,7 @@ library , insert-ordered-containers , iproute >=1.5 , iso639 >=0.1 + , jose , jwt-tools , lens >=3.8 , lens-aeson >=1.0 From 0766d79d7453f7865decf2ed5c63c222904db126 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 6 Dec 2022 15:31:37 +0000 Subject: [PATCH 02/27] basic jwt functions --- services/brig/src/Brig/API/OAuth.hs | 80 ++++++++++++++++++--- services/brig/test/integration/API/OAuth.hs | 6 +- 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index d12ee41ad6..44a4de9128 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -19,17 +19,20 @@ module Brig.API.OAuth where import Brig.API.Error (throwStd) import Brig.API.Handler (Handler) -import Brig.App (Env, wrapClient) +import Brig.App (Env, liftSem, wrapClient) import Brig.Password (Password, mkSafePassword) import Cassandra hiding (Set) import qualified Cassandra as C -import Control.Lens ((.~)) +import Control.Lens ((.~), (?~), (^?)) import Control.Monad.Except +import Crypto.JWT hiding (params, uri) import qualified Data.Aeson as A +import qualified Data.Aeson.KeyMap as M import qualified Data.Aeson.Types as A import Data.ByteString.Conversion import Data.ByteString.Lazy (toStrict) -import Data.Id (OAuthClientId, UserId, randomId) +import Data.Domain +import Data.Id (OAuthClientId, UserId, idToText, randomId) import Data.Misc (PlainTextPassword (PlainTextPassword)) import Data.Range import Data.Schema @@ -40,14 +43,18 @@ import qualified Data.Text as T import Data.Text.Ascii import qualified Data.Text.Encoding as TE import Data.Text.Encoding.Error as TErr -import Imports +import Data.Time (NominalDiffTime, addUTCTime) +import Imports hiding (exp) import OpenSSL.Random (randBytes) +import Polysemy (Member) import Servant hiding (Handler, Tagged) import URI.ByteString import Wire.API.Error import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named (Named (..)) import Wire.API.Routes.Public (ZUser) +import Wire.Sem.Now (Now) +import qualified Wire.Sem.Now as Now -------------------------------------------------------------------------------- -- Types @@ -166,6 +173,13 @@ instance FromByteString OAuthScope where "conversation-code:create" -> pure ConversationCodeCreate _ -> fail "invalid scope" +newtype OAuthScopes = OAuthScopes {unOAuthScopes :: Set OAuthScope} + deriving stock (Eq, Show, Generic) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema OAuthScopes) + +instance ToSchema OAuthScopes where + schema = OAuthScopes <$> (oauthScopesToText . unOAuthScopes) .= withParser schema oauthScopeParser + oauthScopesToText :: Set OAuthScope -> Text oauthScopesToText = T.intercalate " " . fmap (cs . toByteString') . Set.toList @@ -176,7 +190,7 @@ oauthScopeParser scope = data NewOAuthAuthCode = NewOAuthAuthCode { noacClientId :: OAuthClientId, - noacScope :: Set OAuthScope, + noacScope :: OAuthScopes, noacResponseType :: OAuthResponseType, noacRedirectUri :: RedirectUrl, noacState :: Text @@ -189,7 +203,7 @@ instance ToSchema NewOAuthAuthCode where object "NewOAuthAuthCode" $ NewOAuthAuthCode <$> noacClientId .= field "clientId" schema - <*> (oauthScopesToText . noacScope) .= field "scope" (withParser schema oauthScopeParser) + <*> noacScope .= field "scope" schema <*> noacResponseType .= field "responseType" schema <*> noacRedirectUri .= field "redirectUri" schema <*> noacState .= field "state" schema @@ -228,6 +242,7 @@ data OAuthError = OAuthClientNotFound | RedirectUrlMissMatch | UnsupportedResponseType + | JwtError type instance MapError 'OAuthClientNotFound = 'StaticError 404 "not-found" "OAuth client not found" @@ -235,6 +250,8 @@ type instance MapError 'RedirectUrlMissMatch = 'StaticError 400 "redirect-url-mi type instance MapError 'UnsupportedResponseType = 'StaticError 400 "unsupported-response-type" "Unsupported response type" +type instance MapError 'JwtError = 'StaticError 500 "jwt-error" "Internal error while creating JWT" + type OAuthAPI = Named "get-oauth-client" @@ -304,6 +321,53 @@ createNewOAuthAuthCode uid (NewOAuthAuthCode cid scope responseType redirectUrl rand32Bytes :: MonadIO m => m AsciiBase64Url rand32Bytes = liftIO . fmap encodeBase64Url $ randBytes 32 +createAccessToken :: IO () +createAccessToken = undefined + +data OAuthClaimSet = OAuthClaimSet {jwtClaims :: ClaimsSet, scope :: OAuthScopes} + deriving stock (Eq, Show, Generic) + +instance HasClaimsSet OAuthClaimSet where + claimsSet f s = fmap (\a' -> s {jwtClaims = a'}) (f (jwtClaims s)) + +instance A.FromJSON OAuthClaimSet where + parseJSON = A.withObject "OAuthClaimSet" $ \o -> + OAuthClaimSet + <$> A.parseJSON (A.Object o) + <*> o A..: "scope" + +instance A.ToJSON OAuthClaimSet where + toJSON s = + ins "scope" (scope s) (A.toJSON (jwtClaims s)) + where + ins k v (A.Object o) = A.Object $ M.insert k (A.toJSON v) o + ins _ _ a = a + +mkClaims :: (Member Now r) => UserId -> Domain -> OAuthScopes -> NominalDiffTime -> (Handler r) OAuthClaimSet +mkClaims uid domain scopes ttl = do + iat <- lift (liftSem Now.get) + uri <- maybe (throwStd $ errorToWai @'JwtError) pure $ domainText domain ^? stringOrUri + sub <- maybe (throwStd $ errorToWai @'JwtError) pure $ idToText uid ^? stringOrUri + let exp = addUTCTime ttl iat + let claimSet = + emptyClaimsSet + & claimIss ?~ uri + & claimAud ?~ Audience [uri] + & claimIat ?~ NumericDate iat + & claimSub ?~ sub + & claimExp ?~ NumericDate exp + pure $ OAuthClaimSet claimSet scopes + +doJwtSign :: JWK -> OAuthClaimSet -> (Handler r) SignedJWT +doJwtSign key claims = do + jwtOrError <- liftIO $ doSignClaims + either (const $ throwStd $ errorToWai @'JwtError) pure jwtOrError + where + doSignClaims :: IO (Either JWTError SignedJWT) + doSignClaims = runJOSE $ do + algo <- bestJWSAlg key + signJWT key (newJWSHeader ((), algo)) claims + -------------------------------------------------------------------------------- -- DB @@ -321,9 +385,9 @@ lookupOauthClient cid = do q :: PrepQuery R (Identity OAuthClientId) (OAuthApplicationName, RedirectUrl) q = "SELECT name, redirect_uri FROM oauth_client WHERE id = ?" -insertOAuthAuthCode :: (MonadClient m, MonadReader Env m) => OAuthAuthCode -> OAuthClientId -> UserId -> Set OAuthScope -> RedirectUrl -> m () +insertOAuthAuthCode :: (MonadClient m, MonadReader Env m) => OAuthAuthCode -> OAuthClientId -> UserId -> OAuthScopes -> RedirectUrl -> m () insertOAuthAuthCode code cid uid scope uri = do - let cqlScope = C.Set (Set.toList scope) + let cqlScope = C.Set (Set.toList (unOAuthScopes scope)) retry x5 . write q $ params LocalQuorum (code, cid, uid, cqlScope, uri) where q :: PrepQuery W (OAuthAuthCode, OAuthClientId, UserId, C.Set OAuthScope, RedirectUrl) () diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 909e29ce8b..ea5a2dce25 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -59,7 +59,7 @@ testCreateOAuthCodeSuccess brig = do let newOAuthClient@(NewOAuthClient _ redirectUrl) = newOAuthClientRequestBody "E Corp" "https://example.com" cid <- occClientId <$> registerNewOAuthClient brig newOAuthClient uid <- userId <$> randomUser brig - let scope = Set.fromList [ConversationCreate, ConversationCodeCreate] + let scope = OAuthScopes $ Set.fromList [ConversationCreate, ConversationCodeCreate] let state = "foobar" createOAuthCode brig uid (NewOAuthAuthCode cid scope OAuthResponseTypeCode redirectUrl state) !!! do const 302 === statusCode @@ -84,7 +84,7 @@ testCreateOAuthCodeRedirectUrlMismatch brig = do cid <- occClientId <$> registerNewOAuthClient brig (newOAuthClientRequestBody "E Corp" "https://example.com") uid <- userId <$> randomUser brig let differentUrl = fromMaybe (error "invalid url") $ fromByteString' "https://wire.com" - createOAuthCode brig uid (NewOAuthAuthCode cid Set.empty OAuthResponseTypeCode differentUrl "") !!! do + createOAuthCode brig uid (NewOAuthAuthCode cid (OAuthScopes Set.empty) OAuthResponseTypeCode differentUrl "") !!! do const 400 === statusCode const (Just "redirect-url-miss-match") === fmap Error.label . responseJsonMaybe @@ -93,7 +93,7 @@ testCreateOAuthCodeClientNotFound brig = do cid <- randomId uid <- userId <$> randomUser brig let redirectUrl = fromMaybe (error "invalid url") $ fromByteString' "https://example.com" - createOAuthCode brig uid (NewOAuthAuthCode cid Set.empty OAuthResponseTypeCode redirectUrl "") !!! do + createOAuthCode brig uid (NewOAuthAuthCode cid (OAuthScopes Set.empty) OAuthResponseTypeCode redirectUrl "") !!! do const 404 === statusCode const (Just "not-found") === fmap Error.label . responseJsonMaybe From 25451a5d3285d7d247c4ab864ab248618d83aee3 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 7 Dec 2022 14:28:37 +0000 Subject: [PATCH 03/27] create access token endpoint with fake jwk --- services/brig/brig.cabal | 2 + services/brig/src/Brig/API/OAuth.hs | 304 ++++++++++++++---- services/brig/src/Brig/API/Public.hs | 4 +- .../brig/src/Brig/CanonicalInterpreter.hs | 5 +- services/brig/src/Brig/Effects/Jwk.hs | 27 ++ 5 files changed, 274 insertions(+), 68 deletions(-) create mode 100644 services/brig/src/Brig/Effects/Jwk.hs diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 226001a4ef..ec48b8ea09 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -67,6 +67,7 @@ library Brig.Effects.Delay Brig.Effects.GalleyProvider Brig.Effects.GalleyProvider.RPC + Brig.Effects.Jwk Brig.Effects.JwtTools Brig.Effects.PasswordResetStore Brig.Effects.PasswordResetStore.CodeStore @@ -230,6 +231,7 @@ library , HsOpenSSL >=0.10 , HsOpenSSL-x509-system >=0.1 , html-entities >=1.1 + , http-api-data , http-client >=0.5 , http-client-openssl >=0.2 , http-media diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index 44a4de9128..4fec2bde8e 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE DeriveGeneric #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -19,11 +21,14 @@ module Brig.API.OAuth where import Brig.API.Error (throwStd) import Brig.API.Handler (Handler) -import Brig.App (Env, liftSem, wrapClient) +import Brig.App +import Brig.Effects.Jwk +import qualified Brig.Effects.Jwk as Jwk +import qualified Brig.Options as Opt import Brig.Password (Password, mkSafePassword) import Cassandra hiding (Set) import qualified Cassandra as C -import Control.Lens ((.~), (?~), (^?)) +import Control.Lens (view, (.~), (?~), (^?)) import Control.Monad.Except import Crypto.JWT hiding (params, uri) import qualified Data.Aeson as A @@ -32,6 +37,7 @@ import qualified Data.Aeson.Types as A import Data.ByteString.Conversion import Data.ByteString.Lazy (toStrict) import Data.Domain +import qualified Data.HashMap.Strict as HM import Data.Id (OAuthClientId, UserId, idToText, randomId) import Data.Misc (PlainTextPassword (PlainTextPassword)) import Data.Range @@ -49,6 +55,7 @@ import OpenSSL.Random (randBytes) import Polysemy (Member) import Servant hiding (Handler, Tagged) import URI.ByteString +import Web.FormUrlEncoded (Form (..), FromForm (..), ToForm (..), parseUnique) import Wire.API.Error import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named (Named (..)) @@ -60,7 +67,7 @@ import qualified Wire.Sem.Now as Now -- Types newtype RedirectUrl = RedirectUrl {unRedirectUrl :: URIRef Absolute} - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema RedirectUrl) instance ToByteString RedirectUrl where @@ -83,7 +90,7 @@ instance FromHttpApiData RedirectUrl where parseHeader = bimap (T.pack . show) RedirectUrl . parseURI strictURIParserOptions newtype OAuthApplicationName = OAuthApplicationName {unOAuthApplicationName :: Range 1 256 Text} - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema OAuthApplicationName) instance ToSchema OAuthApplicationName where @@ -93,7 +100,7 @@ data NewOAuthClient = NewOAuthClient { nocApplicationName :: OAuthApplicationName, nocRedirectUrl :: RedirectUrl } - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema NewOAuthClient) instance ToSchema NewOAuthClient where @@ -104,7 +111,7 @@ instance ToSchema NewOAuthClient where <*> nocRedirectUrl .= field "redirectUrl" schema newtype OAuthClientPlainTextSecret = OAuthClientPlainTextSecret {unOAuthClientPlainTextSecret :: AsciiBase16} - deriving stock (Eq, Generic) + deriving (Eq, Generic) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema OAuthClientPlainTextSecret) instance Show OAuthClientPlainTextSecret where @@ -113,11 +120,17 @@ instance Show OAuthClientPlainTextSecret where instance ToSchema OAuthClientPlainTextSecret where schema = (toText . unOAuthClientPlainTextSecret) .= parsedText "OAuthClientPlainTextSecret" (fmap OAuthClientPlainTextSecret . validateBase16) +instance FromHttpApiData OAuthClientPlainTextSecret where + parseQueryParam = bimap cs OAuthClientPlainTextSecret . validateBase16 . cs + +instance ToHttpApiData OAuthClientPlainTextSecret where + toQueryParam = toText . unOAuthClientPlainTextSecret + data OAuthClientCredentials = OAuthClientCredentials { occClientId :: OAuthClientId, occClientSecret :: OAuthClientPlainTextSecret } - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema OAuthClientCredentials) instance ToSchema OAuthClientCredentials where @@ -132,7 +145,7 @@ data OAuthClient = OAuthClient ocName :: OAuthApplicationName, ocRedirectUrl :: RedirectUrl } - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema OAuthClient) instance ToSchema OAuthClient where @@ -144,7 +157,7 @@ instance ToSchema OAuthClient where <*> ocRedirectUrl .= field "redirectUrl" schema data OAuthResponseType = OAuthResponseTypeCode - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema OAuthResponseType) instance ToSchema OAuthResponseType where @@ -158,7 +171,7 @@ instance ToSchema OAuthResponseType where data OAuthScope = ConversationCreate | ConversationCodeCreate - deriving stock (Eq, Show, Generic, Ord) + deriving (Eq, Show, Generic, Ord) instance ToByteString OAuthScope where builder = \case @@ -167,14 +180,14 @@ instance ToByteString OAuthScope where instance FromByteString OAuthScope where parser = do - s <- parser @Text + s <- parser case s & T.toLower of "conversation:create" -> pure ConversationCreate "conversation-code:create" -> pure ConversationCodeCreate _ -> fail "invalid scope" newtype OAuthScopes = OAuthScopes {unOAuthScopes :: Set OAuthScope} - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema OAuthScopes) instance ToSchema OAuthScopes where @@ -195,7 +208,7 @@ data NewOAuthAuthCode = NewOAuthAuthCode noacRedirectUri :: RedirectUrl, noacState :: Text } - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema NewOAuthAuthCode) instance ToSchema NewOAuthAuthCode where @@ -208,8 +221,11 @@ instance ToSchema NewOAuthAuthCode where <*> noacRedirectUri .= field "redirectUri" schema <*> noacState .= field "state" schema -newtype OAuthAuthCode = OAuthAuthCode {unOAuthAuthCode :: AsciiBase64Url} - deriving stock (Show, Eq, Generic) +newtype OAuthAuthCode = OAuthAuthCode {unOAuthAuthCode :: AsciiBase16} + deriving (Show, Eq, Generic) + +instance ToSchema OAuthAuthCode where + schema = (toText . unOAuthAuthCode) .= parsedText "OAuthAuthCode" (fmap OAuthAuthCode . validateBase16) instance ToByteString OAuthAuthCode where builder = builder . unOAuthAuthCode @@ -217,6 +233,135 @@ instance ToByteString OAuthAuthCode where instance FromByteString OAuthAuthCode where parser = OAuthAuthCode <$> parser +instance FromHttpApiData OAuthAuthCode where + parseQueryParam = bimap cs OAuthAuthCode . validateBase16 . cs + +instance ToHttpApiData OAuthAuthCode where + toQueryParam = toText . unOAuthAuthCode + +data OAuthGrantType = OAuthGrantTypeAuthorizationCode + deriving (Eq, Show, Generic) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema OAuthGrantType) + +instance ToSchema OAuthGrantType where + schema = + enum @Text "OAuthGrantType" $ + mconcat + [ element "authorization_code" OAuthGrantTypeAuthorizationCode + ] + +instance FromByteString OAuthGrantType where + parser = do + s <- parser + case s & T.toLower of + "authorization_code" -> pure OAuthGrantTypeAuthorizationCode + _ -> fail "invalid OAuthGrantType" + +instance ToByteString OAuthGrantType where + builder = \case + OAuthGrantTypeAuthorizationCode -> "authorization" + +instance FromHttpApiData OAuthGrantType where + parseQueryParam = maybe (Left "invalid OAuthGrantType") pure . fromByteString . cs + +instance ToHttpApiData OAuthGrantType where + toQueryParam = cs . toByteString + +data OAuthAccessTokenRequest = OAuthAccessTokenRequest + { oatGrantType :: OAuthGrantType, + oatClientId :: OAuthClientId, + oatClientSecret :: OAuthClientPlainTextSecret, + oatCode :: OAuthAuthCode, + oatRedirectUri :: RedirectUrl + } + deriving (Eq, Show, Generic) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema OAuthAccessTokenRequest) + +instance ToSchema OAuthAccessTokenRequest where + schema = + object "OAuthAccessTokenRequest" $ + OAuthAccessTokenRequest + <$> oatGrantType .= field "grantType" schema + <*> oatClientId .= field "clientId" schema + <*> oatClientSecret .= field "clientSecret" schema + <*> oatCode .= field "code" schema + <*> oatRedirectUri .= field "redirectUri" schema + +instance FromForm OAuthAccessTokenRequest where + fromForm f = + OAuthAccessTokenRequest + <$> parseUnique "grant_type" f + <*> parseUnique "client_id" f + <*> parseUnique "client_secret" f + <*> parseUnique "code" f + <*> parseUnique "redirect_uri" f + +instance ToForm OAuthAccessTokenRequest where + toForm req = + Form $ + mempty + & HM.insert "grant_type" [toQueryParam (oatGrantType req)] + & HM.insert "client_id" [toQueryParam (oatClientId req)] + & HM.insert "client_secret" [toQueryParam (oatClientSecret req)] + & HM.insert "code" [toQueryParam (oatCode req)] + & HM.insert "redirect_uri" [toQueryParam (oatRedirectUri req)] + +data OAuthAccessTokenType = OAuthAccessTokenTypeBearer + deriving (Eq, Show, Generic) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema OAuthAccessTokenType) + +instance ToSchema OAuthAccessTokenType where + schema = + enum @Text "OAuthAccessTokenType" $ + mconcat + [ element "Bearer" OAuthAccessTokenTypeBearer + ] + +newtype OauthAccessToken = OauthAccessToken {unOauthAccessToken :: ByteString} + deriving (Show, Eq, Generic) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema OauthAccessToken + +instance ToSchema OauthAccessToken where + schema = (TE.decodeUtf8 . unOauthAccessToken) .= fmap (OauthAccessToken . TE.encodeUtf8) schema + +data OAuthAccessTokenResponse = OAuthAccessTokenResponse + { oatAccessToken :: OauthAccessToken, + oatTokenType :: OAuthAccessTokenType, + oatExpiresIn :: NominalDiffTime + } + deriving (Eq, Show, Generic) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema OAuthAccessTokenResponse) + +instance ToSchema OAuthAccessTokenResponse where + schema = + object "OAuthAccessTokenResponse" $ + OAuthAccessTokenResponse + <$> oatAccessToken .= field "accessToken" schema + <*> oatTokenType .= field "tokenType" schema + <*> oatExpiresIn .= field "expiresIn" (fromIntegral <$> roundDiffTime .= schema) + where + roundDiffTime :: NominalDiffTime -> Int32 + roundDiffTime = round + +data OAuthClaimSet = OAuthClaimSet {jwtClaims :: ClaimsSet, scope :: OAuthScopes} + deriving (Eq, Show, Generic) + +instance HasClaimsSet OAuthClaimSet where + claimsSet f s = fmap (\a' -> s {jwtClaims = a'}) (f (jwtClaims s)) + +instance A.FromJSON OAuthClaimSet where + parseJSON = A.withObject "OAuthClaimSet" $ \o -> + OAuthClaimSet + <$> A.parseJSON (A.Object o) + <*> o A..: "scope" + +instance A.ToJSON OAuthClaimSet where + toJSON s = + ins "scope" (scope s) (A.toJSON (jwtClaims s)) + where + ins k v (A.Object o) = A.Object $ M.insert k (A.toJSON v) o + ins _ _ a = a + -------------------------------------------------------------------------------- -- API Internal @@ -243,6 +388,7 @@ data OAuthError | RedirectUrlMissMatch | UnsupportedResponseType | JwtError + | OAuthAuthCodeNotFound type instance MapError 'OAuthClientNotFound = 'StaticError 404 "not-found" "OAuth client not found" @@ -252,6 +398,8 @@ type instance MapError 'UnsupportedResponseType = 'StaticError 400 "unsupported- type instance MapError 'JwtError = 'StaticError 500 "jwt-error" "Internal error while creating JWT" +type instance MapError 'OAuthAuthCodeNotFound = 'StaticError 404 "not-found" "OAuth authorization code not found" + type OAuthAPI = Named "get-oauth-client" @@ -282,11 +430,20 @@ type OAuthAPI = '[WithHeaders '[Header "Location" RedirectUrl] RedirectUrl (RespondEmpty 302 "Found")] RedirectUrl ) + :<|> Named + "create-oauth-access-token" + ( Summary "Create an OAuth access token" + :> "oauth" + :> "token" + :> ReqBody '[FormUrlEncoded] OAuthAccessTokenRequest + :> Post '[JSON] OAuthAccessTokenResponse + ) -oauthAPI :: ServerT OAuthAPI (Handler r) +oauthAPI :: (Member Now r, Member Jwk r) => ServerT OAuthAPI (Handler r) oauthAPI = Named @"get-oauth-client" getOAuthClient :<|> Named @"create-oauth-auth-code" createNewOAuthAuthCode + :<|> Named @"create-oauth-access-token" createAccessToken -------------------------------------------------------------------------------- -- Handlers @@ -299,7 +456,7 @@ createNewOAuthClient (NewOAuthClient name uri) = do pure credentials where createSecret :: MonadIO m => m OAuthClientPlainTextSecret - createSecret = OAuthClientPlainTextSecret <$> (liftIO . fmap encodeBase16 $ randBytes 32) + createSecret = OAuthClientPlainTextSecret <$> rand32Bytes hashClientSecret :: MonadIO m => OAuthClientPlainTextSecret -> m Password hashClientSecret = mkSafePassword . PlainTextPassword . toText . unOAuthClientPlainTextSecret @@ -318,55 +475,53 @@ createNewOAuthAuthCode uid (NewOAuthAuthCode cid scope responseType redirectUrl returnedRedirectUrl = redirectUrl & unRedirectUrl & (queryL . queryPairsL) .~ queryParams & RedirectUrl pure returnedRedirectUrl -rand32Bytes :: MonadIO m => m AsciiBase64Url -rand32Bytes = liftIO . fmap encodeBase64Url $ randBytes 32 - -createAccessToken :: IO () -createAccessToken = undefined - -data OAuthClaimSet = OAuthClaimSet {jwtClaims :: ClaimsSet, scope :: OAuthScopes} - deriving stock (Eq, Show, Generic) - -instance HasClaimsSet OAuthClaimSet where - claimsSet f s = fmap (\a' -> s {jwtClaims = a'}) (f (jwtClaims s)) - -instance A.FromJSON OAuthClaimSet where - parseJSON = A.withObject "OAuthClaimSet" $ \o -> - OAuthClaimSet - <$> A.parseJSON (A.Object o) - <*> o A..: "scope" - -instance A.ToJSON OAuthClaimSet where - toJSON s = - ins "scope" (scope s) (A.toJSON (jwtClaims s)) - where - ins k v (A.Object o) = A.Object $ M.insert k (A.toJSON v) o - ins _ _ a = a - -mkClaims :: (Member Now r) => UserId -> Domain -> OAuthScopes -> NominalDiffTime -> (Handler r) OAuthClaimSet -mkClaims uid domain scopes ttl = do - iat <- lift (liftSem Now.get) - uri <- maybe (throwStd $ errorToWai @'JwtError) pure $ domainText domain ^? stringOrUri - sub <- maybe (throwStd $ errorToWai @'JwtError) pure $ idToText uid ^? stringOrUri - let exp = addUTCTime ttl iat - let claimSet = - emptyClaimsSet - & claimIss ?~ uri - & claimAud ?~ Audience [uri] - & claimIat ?~ NumericDate iat - & claimSub ?~ sub - & claimExp ?~ NumericDate exp - pure $ OAuthClaimSet claimSet scopes - -doJwtSign :: JWK -> OAuthClaimSet -> (Handler r) SignedJWT -doJwtSign key claims = do - jwtOrError <- liftIO $ doSignClaims - either (const $ throwStd $ errorToWai @'JwtError) pure jwtOrError +createAccessToken :: (Member Now r, Member Jwk r) => OAuthAccessTokenRequest -> (Handler r) OAuthAccessTokenResponse +createAccessToken req = do + let exp :: NominalDiffTime = 60 * 60 * 24 * 7 * 3 -- (3 weeks) TODO: make configurable + let jwkFp :: FilePath = "" -- TODO: make configurable + (authCodeCid, authCodeUserId, authCodeScopes, authCodeRedirectUrl) <- + lift (wrapClient $ lookupAndDeleteOAuthAuthCode (oatCode req)) + >>= maybe (throwStd $ errorToWai @'OAuthAuthCodeNotFound) pure + oauthClient <- getOAuthClient authCodeUserId (oatClientId req) >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure + + -- validate request + unless (ocRedirectUrl oauthClient == oatRedirectUri req) $ throwStd $ errorToWai @'OAuthAuthCodeNotFound + unless (authCodeCid == oatClientId req) $ throwStd $ errorToWai @'OAuthAuthCodeNotFound + unless (authCodeRedirectUrl == oatRedirectUri req) $ throwStd $ errorToWai @'OAuthAuthCodeNotFound + + domain <- Opt.setFederationDomain <$> view settings + claims <- mkClaims authCodeUserId domain authCodeScopes exp + key <- lift (liftSem $ Jwk.get jwkFp) >>= maybe (throwStd $ errorToWai @'JwtError) pure + token <- OauthAccessToken . cs . encodeCompact <$> doJwtSign key claims + pure $ OAuthAccessTokenResponse token OAuthAccessTokenTypeBearer exp where - doSignClaims :: IO (Either JWTError SignedJWT) - doSignClaims = runJOSE $ do - algo <- bestJWSAlg key - signJWT key (newJWSHeader ((), algo)) claims + mkClaims :: (Member Now r) => UserId -> Domain -> OAuthScopes -> NominalDiffTime -> (Handler r) OAuthClaimSet + mkClaims u domain scopes ttl = do + iat <- lift (liftSem Now.get) + uri <- maybe (throwStd $ errorToWai @'JwtError) pure $ domainText domain ^? stringOrUri + sub <- maybe (throwStd $ errorToWai @'JwtError) pure $ idToText u ^? stringOrUri + let exp = addUTCTime ttl iat + let claimSet = + emptyClaimsSet + & claimIss ?~ uri + & claimAud ?~ Audience [uri] + & claimIat ?~ NumericDate iat + & claimSub ?~ sub + & claimExp ?~ NumericDate exp + pure $ OAuthClaimSet claimSet scopes + + doJwtSign :: JWK -> OAuthClaimSet -> (Handler r) SignedJWT + doJwtSign key claims = do + jwtOrError <- liftIO $ doSignClaims + either (const $ throwStd $ errorToWai @'JwtError) pure jwtOrError + where + doSignClaims :: IO (Either JWTError SignedJWT) + doSignClaims = runJOSE $ do + algo <- bestJWSAlg key + signJWT key (newJWSHeader ((), algo)) claims + +rand32Bytes :: MonadIO m => m AsciiBase16 +rand32Bytes = liftIO . fmap encodeBase16 $ randBytes 32 -------------------------------------------------------------------------------- -- DB @@ -393,6 +548,23 @@ insertOAuthAuthCode code cid uid scope uri = do q :: PrepQuery W (OAuthAuthCode, OAuthClientId, UserId, C.Set OAuthScope, RedirectUrl) () q = "INSERT INTO oauth_auth_code (code, client, user, scope, redirect_uri) VALUES (?, ?, ?, ?, ?) USING TTL 300" +lookupOAuthAuthCode :: (MonadClient m, MonadReader Env m) => OAuthAuthCode -> m (Maybe (OAuthClientId, UserId, OAuthScopes, RedirectUrl)) +lookupOAuthAuthCode code = do + mTuple <- retry x5 . query1 q $ params LocalQuorum (Identity code) + pure $ mTuple <&> \(cid, uid, C.Set scope, uri) -> (cid, uid, OAuthScopes (Set.fromList scope), uri) + where + q :: PrepQuery R (Identity OAuthAuthCode) (OAuthClientId, UserId, C.Set OAuthScope, RedirectUrl) + q = "SELECT client, user, scope, redirect_uri FROM oauth_auth_code WHERE code = ?" + +deleteOAuthAuthCode :: (MonadClient m, MonadReader Env m) => OAuthAuthCode -> m () +deleteOAuthAuthCode code = retry x5 . write q $ params LocalQuorum (Identity code) + where + q :: PrepQuery W (Identity OAuthAuthCode) () + q = "DELETE FROM oauth_auth_code WHERE code = ?" + +lookupAndDeleteOAuthAuthCode :: (MonadClient m, MonadReader Env m) => OAuthAuthCode -> m (Maybe (OAuthClientId, UserId, OAuthScopes, RedirectUrl)) +lookupAndDeleteOAuthAuthCode code = lookupOAuthAuthCode code <* deleteOAuthAuthCode code + -------------------------------------------------------------------------------- -- CQL instances @@ -411,7 +583,7 @@ instance Cql RedirectUrl where instance Cql OAuthAuthCode where ctype = Tagged AsciiColumn toCql = CqlAscii . toText . unOAuthAuthCode - fromCql (CqlAscii t) = OAuthAuthCode <$> validateBase64Url t + fromCql (CqlAscii t) = OAuthAuthCode <$> validateBase16 t fromCql _ = Left "OAuthAuthCode: Ascii expected" instance Cql OAuthScope where diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index c1f0376505..206efae192 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -52,6 +52,7 @@ import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.CodeStore (CodeStore) import Brig.Effects.GalleyProvider (GalleyProvider) import qualified Brig.Effects.GalleyProvider as GalleyProvider +import Brig.Effects.Jwk (Jwk) import Brig.Effects.JwtTools (JwtTools) import Brig.Effects.PasswordResetStore (PasswordResetStore) import Brig.Effects.PublicKeyBundle (PublicKeyBundle) @@ -180,7 +181,8 @@ servantSitemap :: Now, PasswordResetStore, PublicKeyBundle, - UserPendingActivationStore p + UserPendingActivationStore p, + Jwk ] r => ServerT (BrigAPI :<|> OAuthAPI) (Handler r) diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index d6076ce31a..9843017b07 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -9,6 +9,7 @@ import Brig.Effects.CodeStore (CodeStore) import Brig.Effects.CodeStore.Cassandra (codeStoreToCassandra, interpretClientToIO) import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.Effects.GalleyProvider.RPC (interpretGalleyProviderToRPC) +import Brig.Effects.Jwk import Brig.Effects.JwtTools import Brig.Effects.PasswordResetStore (PasswordResetStore) import Brig.Effects.PasswordResetStore.CodeStore (passwordResetStoreToCodeStore) @@ -35,7 +36,8 @@ import Wire.Sem.Now.IO (nowToIOAction) import Wire.Sem.Paging.Cassandra (InternalPaging) type BrigCanonicalEffects = - '[ PublicKeyBundle, + '[ Jwk, + PublicKeyBundle, JwtTools, BlacklistPhonePrefixStore, BlacklistStore, @@ -76,6 +78,7 @@ runBrigToIO e (AppT ma) = do . interpretBlacklistPhonePrefixStoreToCassandra @Cas.Client . interpretJwtTools . interpretPublicKeyBundle + . interpretFakeJwk ) ) $ runReaderT ma e diff --git a/services/brig/src/Brig/Effects/Jwk.hs b/services/brig/src/Brig/Effects/Jwk.hs new file mode 100644 index 0000000000..6b434d6fc1 --- /dev/null +++ b/services/brig/src/Brig/Effects/Jwk.hs @@ -0,0 +1,27 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Brig.Effects.Jwk where + +import Control.Exception +import Crypto.JOSE.JWK +import Data.Aeson +import qualified Data.ByteString as BS +import Data.String.Conversions (cs) +import Imports +import Polysemy + +data Jwk m a where + Get :: FilePath -> Jwk m (Maybe JWK) + +makeSem ''Jwk + +interpretJwk :: Members '[Embed IO] r => Sem (Jwk ': r) a -> Sem r a +interpretJwk = interpret $ \(Get fp) -> do + contents :: Either IOException ByteString <- liftIO $ try $ BS.readFile fp + pure $ either (const Nothing) (decode . cs) contents + +interpretFakeJwk :: Members '[Embed IO] r => Sem (Jwk ': r) a -> Sem r a +interpretFakeJwk = interpret $ \(Get _) -> pure $ fakeJwk + +fakeJwk :: Maybe JWK +fakeJwk = decode "{\"p\":\"8U9gI_GHo8ca1g-6Miv9f7E7zTvY90mPZO9Hhygf3ZxhFk_TNH7y3dMyUZebnYLbXf1wUltLve-nND9AO2omtz6WgPEjPomo6nLeIO1swzkBkTqrQhvPioo7rXIAlv4O5vEPPnLFJceSVRmDSGovkjcyklAhZiVRfzAv-_GdnG8\",\"kty\":\"RSA\",\"q\":\"43dqCXHtiIYJ2bvVvVW8Ch4yhJmPA9VUQSTN2aVlLZoqMOLh8rn6fl0UiBLCQJdoPI3Hc2QuS6_GohVrKI-WgpHEGMssZnH9cbfGuNUX7QK6glhsGSPoxSRpFJMgVDxG5jCgUSOt0BJdY_hgo7wwO_bx2VTdSJPgIt761TUNjiU\",\"d\":\"ixZk7xvHUYzY8Eu0NwAF5LoGG7xJSqSuvy7lg4Ag8Pb8imlvwvyo1G2aYpNXGWi9lTv8h_tnVaSBfb7KddS9KLpoC-EuBk6tn8EUyeevKdRD6c-WLZX6QehET_B_LWL_EQQRB2cqfiThkDghN5HWZn18-QskyYgS0vWS_EYquM8LS3s0emQhXkz77ZX4fQrhZGRCAmDeI-iZDt90uRTF6OceKKoC5eTimx9YimQ0Z-qOlKiCpIjplSgHL8QgIZrp6mLdCTVy3vxaegwSNKwGqCCwcewtdBky9nb9pX6sEc6CA3WfyYZMbthMYIYTi0KJ2kaaF2QdR3l7VLTdPBwK-Q\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"QVapB_JRK66AMCYKN7LHsl4DlkICTNYB6ExJrtmva88\",\"qi\":\"sYHbPsyiiRIBKbN-chbXYTLsd03jL_kiT2VdyMsP_tNjOz87WcC8Td-lBUIViw_aMq1VOjJEyhB6yE9-Po4YmlAFbPSf1rmypAdjTKRQznkuTGDue6yd4z1t4SgZJOSlpJSGBtkEiYaIlI9-fviPxtIpDh0JfAbE7XgPItu5vT8\",\"dp\":\"LFmnVNPWVx6H6yHW2dPF8osTb6P5O3HUnftwTQHt4WAVY_Fl4vcWEfj3_ZD8s9VSFhM3apuG7zC1rV-WoZTf6rvhE2--R4creXOFKc4ZOEQo6pU61pqf6VmAoF8chqPI_178q0CSxV_JoERhIZUKizgD3mpFyS1ArjygBREiI5c\",\"alg\":\"RS256\",\"dq\":\"UXTY7yryQhql-mfugc_q98CanKuU8G17r5xrbw5wriTwCPOmsFJzZr8UdCorkA_oTw8CpsrwXUBEJJUA-9R3tKhYjfxsGP-nIAsMyfHdkI2SlsztYQ9f8wE39Bs_z4qKZTJyprvdKY76DKvMh7YDm-Wx8_8VA0GZWUN6ldFa2lU\",\"n\":\"1mnyGVTdwCi4umlGMvT6y7aTmibMQuBvySoT5eZcwKti4_sfnEJhyWXfsE6tasUC8ce7YGzFiq886S9-iZc6hYW0ReIPQYCLZ5hn-fUCxefEZdX03e-uJww7OwX_kNQifgdYwzM__QJsy1_nbgEn7olTzplJfJmUSk9dkfFJ-3o5L3AJ1YMbFPgXae0OLmkxjVKfT_6093a0RLSpUCzcti4OhtNa-OEUMX4rYl0jQyzZyO281IH-MatecA1O_o6JbCI1wH46jWNY8of43cemzFwQaS2SnSyppOmJzGf9fpm5j5dozYrAdTdPYdZEA62CimvdLvJqEt4OjyQuRTAuCw\"}" From ded5152d76150c3383af26915607b6a35160bbd3 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 7 Dec 2022 15:59:41 +0000 Subject: [PATCH 04/27] integration test --- services/brig/brig.cabal | 1 + services/brig/src/Brig/API/OAuth.hs | 9 +++- services/brig/test/integration/API/OAuth.hs | 49 ++++++++++++++++++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index ec48b8ea09..c3a03b5b4e 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -558,6 +558,7 @@ executable brig-integration , http-reverse-proxy , http-types , imports + , jose , lens >=3.9 , lens-aeson , metrics-wai diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index 4fec2bde8e..21baf2538c 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -36,6 +36,7 @@ import qualified Data.Aeson.KeyMap as M import qualified Data.Aeson.Types as A import Data.ByteString.Conversion import Data.ByteString.Lazy (toStrict) +import qualified Data.ByteString.Lazy as BL import Data.Domain import qualified Data.HashMap.Strict as HM import Data.Id (OAuthClientId, UserId, idToText, randomId) @@ -259,7 +260,7 @@ instance FromByteString OAuthGrantType where instance ToByteString OAuthGrantType where builder = \case - OAuthGrantTypeAuthorizationCode -> "authorization" + OAuthGrantTypeAuthorizationCode -> "authorization_code" instance FromHttpApiData OAuthGrantType where parseQueryParam = maybe (Left "invalid OAuthGrantType") pure . fromByteString . cs @@ -523,6 +524,12 @@ createAccessToken req = do rand32Bytes :: MonadIO m => m AsciiBase16 rand32Bytes = liftIO . fmap encodeBase16 $ randBytes 32 +verify :: JWK -> BL.ByteString -> IO (Either JWTError OAuthClaimSet) +verify k s = runJOSE $ do + let audCheck = const True -- should be a proper audience check + jwt <- decodeCompact s -- decode JWT + verifyJWT (defaultJWTValidationSettings audCheck) k jwt + -------------------------------------------------------------------------------- -- DB diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index ea5a2dce25..2070a52bf2 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -20,19 +20,23 @@ module API.OAuth where import Bilge import Bilge.Assert import Brig.API.OAuth +import Brig.Effects.Jwk (fakeJwk) import Brig.Options import Control.Lens +import Crypto.JWT (Audience (Audience), NumericDate (NumericDate), claimAud, claimExp, claimIat, claimIss, claimSub, stringOrUri) import Data.ByteString.Conversion (fromByteString, fromByteString', toByteString') -import Data.Id (OAuthClientId, UserId, randomId) +import Data.Id (OAuthClientId, UserId, idToText, randomId) import Data.Range (unsafeRange) import Data.Set as Set import Data.String.Conversions (cs) +import Data.Time import Imports import qualified Network.Wai.Utilities as Error import Test.Tasty import Test.Tasty.HUnit import URI.ByteString import Util +import Web.FormUrlEncoded import Wire.API.User tests :: Manager -> Brig -> Opts -> TestTree @@ -41,7 +45,8 @@ tests m b _opts = do [ test m "register new OAuth client" $ testRegisterNewOAuthClient b, test m "create oauth code - success" $ testCreateOAuthCodeSuccess b, test m "create oauth code - oauth client not found" $ testCreateOAuthCodeClientNotFound b, - test m "create oauth code - redirect url mismatch" $ testCreateOAuthCodeRedirectUrlMismatch b + test m "create oauth code - redirect url mismatch" $ testCreateOAuthCodeRedirectUrlMismatch b, + test m "create access token - success" $ testCreateAccessTokenSuccess b ] testRegisterNewOAuthClient :: Brig -> Http () @@ -97,6 +102,25 @@ testCreateOAuthCodeClientNotFound brig = do const 404 === statusCode const (Just "not-found") === fmap Error.label . responseJsonMaybe +testCreateAccessTokenSuccess :: Brig -> Http () +testCreateAccessTokenSuccess brig = do + now <- liftIO getCurrentTime + uid <- userId <$> randomUser brig + let redirectUrl = fromMaybe (error "invalid url") $ fromByteString' "https://example.com" + let scopes = OAuthScopes $ Set.fromList [ConversationCreate, ConversationCodeCreate] + (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + accessToken <- createOAuthAccessToken brig accessTokenRequest + result <- liftIO $ verify (fromMaybe (error "invalid key") fakeJwk) (cs $ unOauthAccessToken $ oatAccessToken accessToken) + liftIO $ do + isRight result @?= True + scope <$> result @?= Right scopes + view claimIss <$> result @?= Right ("example.com" ^? stringOrUri @Text) + view claimAud <$> result @?= Right (Audience . (: []) <$> "example.com" ^? stringOrUri @Text) + view claimSub <$> result @?= Right (idToText uid ^? stringOrUri) + (\(NumericDate expTime) -> diffUTCTime expTime now > 0) . fromMaybe (error "exp claim missing") . view claimExp <$> result @?= Right True + (\(NumericDate issuingTime) -> diffUTCTime issuingTime now < 0) . fromMaybe (error "iat claim missing") . view claimIat <$> result @?= Right True + ------------------------------------------------------------------------------- -- Util @@ -116,3 +140,24 @@ getOAuthClientInfo brig uid cid = createOAuthCode :: HasCallStack => Brig -> UserId -> NewOAuthAuthCode -> Http ResponseLBS createOAuthCode brig uid reqBody = post (brig . paths ["oauth", "authorization", "codes"] . zUser uid . json reqBody . noRedirect) + +createOAuthAccessToken :: HasCallStack => Brig -> OAuthAccessTokenRequest -> Http OAuthAccessTokenResponse +createOAuthAccessToken brig reqBody = do + r <- post (brig . paths ["oauth", "token"] . content "application/x-www-form-urlencoded" . body (RequestBodyLBS $ urlEncodeAsForm reqBody)) + responseJsonError r + +generateOAuthClientAndAuthCode :: Brig -> UserId -> OAuthScopes -> RedirectUrl -> Http (OAuthClientId, OAuthClientPlainTextSecret, OAuthAuthCode) +generateOAuthClientAndAuthCode brig uid scope url = do + let newOAuthClient = NewOAuthClient (OAuthApplicationName (unsafeRange "E Corp")) url + OAuthClientCredentials cid secret <- registerNewOAuthClient brig newOAuthClient + let state = "foobar" + response <- + createOAuthCode brig uid (NewOAuthAuthCode cid scope OAuthResponseTypeCode url state) => fromByteString >=> getQueryParamValue "code" >=> fromByteString) response + where + getQueryParams :: RedirectUrl -> [(ByteString, ByteString)] + getQueryParams (RedirectUrl uri) = uri ^. (queryL . queryPairsL) + + getQueryParamValue :: ByteString -> RedirectUrl -> Maybe ByteString + getQueryParamValue key uri = snd <$> find ((== key) . fst) (getQueryParams uri) From a7daee507e09ccb9b04298c38ef0fda9f35fae24 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 7 Dec 2022 16:05:08 +0000 Subject: [PATCH 05/27] comment --- services/brig/src/Brig/API/OAuth.hs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index 21baf2538c..5b2ae72825 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -433,7 +433,8 @@ type OAuthAPI = ) :<|> Named "create-oauth-access-token" - ( Summary "Create an OAuth access token" + ( -- TODO: add error responses and corresponding tests + Summary "Create an OAuth access token" :> "oauth" :> "token" :> ReqBody '[FormUrlEncoded] OAuthAccessTokenRequest @@ -526,8 +527,8 @@ rand32Bytes = liftIO . fmap encodeBase16 $ randBytes 32 verify :: JWK -> BL.ByteString -> IO (Either JWTError OAuthClaimSet) verify k s = runJOSE $ do - let audCheck = const True -- should be a proper audience check - jwt <- decodeCompact s -- decode JWT + let audCheck = const True + jwt <- decodeCompact s verifyJWT (defaultJWTValidationSettings audCheck) k jwt -------------------------------------------------------------------------------- @@ -553,7 +554,7 @@ insertOAuthAuthCode code cid uid scope uri = do retry x5 . write q $ params LocalQuorum (code, cid, uid, cqlScope, uri) where q :: PrepQuery W (OAuthAuthCode, OAuthClientId, UserId, C.Set OAuthScope, RedirectUrl) () - q = "INSERT INTO oauth_auth_code (code, client, user, scope, redirect_uri) VALUES (?, ?, ?, ?, ?) USING TTL 300" + q = "INSERT INTO oauth_auth_code (code, client, user, scope, redirect_uri) VALUES (?, ?, ?, ?, ?) USING TTL 300" -- TODO: make configurable lookupOAuthAuthCode :: (MonadClient m, MonadReader Env m) => OAuthAuthCode -> m (Maybe (OAuthClientId, UserId, OAuthScopes, RedirectUrl)) lookupOAuthAuthCode code = do From a6d4f3c6bfadb96f16b3dbe078f9e696a361c5e4 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 8 Dec 2022 09:26:12 +0000 Subject: [PATCH 06/27] clean up --- services/brig/src/Brig/API/OAuth.hs | 12 +++++------- services/brig/test/integration/API/OAuth.hs | 19 +++++++++++-------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index 5b2ae72825..e077973932 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -36,7 +36,6 @@ import qualified Data.Aeson.KeyMap as M import qualified Data.Aeson.Types as A import Data.ByteString.Conversion import Data.ByteString.Lazy (toStrict) -import qualified Data.ByteString.Lazy as BL import Data.Domain import qualified Data.HashMap.Strict as HM import Data.Id (OAuthClientId, UserId, idToText, randomId) @@ -486,7 +485,6 @@ createAccessToken req = do >>= maybe (throwStd $ errorToWai @'OAuthAuthCodeNotFound) pure oauthClient <- getOAuthClient authCodeUserId (oatClientId req) >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure - -- validate request unless (ocRedirectUrl oauthClient == oatRedirectUri req) $ throwStd $ errorToWai @'OAuthAuthCodeNotFound unless (authCodeCid == oatClientId req) $ throwStd $ errorToWai @'OAuthAuthCodeNotFound unless (authCodeRedirectUrl == oatRedirectUri req) $ throwStd $ errorToWai @'OAuthAuthCodeNotFound @@ -494,7 +492,7 @@ createAccessToken req = do domain <- Opt.setFederationDomain <$> view settings claims <- mkClaims authCodeUserId domain authCodeScopes exp key <- lift (liftSem $ Jwk.get jwkFp) >>= maybe (throwStd $ errorToWai @'JwtError) pure - token <- OauthAccessToken . cs . encodeCompact <$> doJwtSign key claims + token <- OauthAccessToken . cs . encodeCompact <$> signJwtToken key claims pure $ OAuthAccessTokenResponse token OAuthAccessTokenTypeBearer exp where mkClaims :: (Member Now r) => UserId -> Domain -> OAuthScopes -> NominalDiffTime -> (Handler r) OAuthClaimSet @@ -512,8 +510,8 @@ createAccessToken req = do & claimExp ?~ NumericDate exp pure $ OAuthClaimSet claimSet scopes - doJwtSign :: JWK -> OAuthClaimSet -> (Handler r) SignedJWT - doJwtSign key claims = do + signJwtToken :: JWK -> OAuthClaimSet -> (Handler r) SignedJWT + signJwtToken key claims = do jwtOrError <- liftIO $ doSignClaims either (const $ throwStd $ errorToWai @'JwtError) pure jwtOrError where @@ -525,10 +523,10 @@ createAccessToken req = do rand32Bytes :: MonadIO m => m AsciiBase16 rand32Bytes = liftIO . fmap encodeBase16 $ randBytes 32 -verify :: JWK -> BL.ByteString -> IO (Either JWTError OAuthClaimSet) +verify :: JWK -> ByteString -> IO (Either JWTError OAuthClaimSet) verify k s = runJOSE $ do let audCheck = const True - jwt <- decodeCompact s + jwt <- decodeCompact (cs s) verifyJWT (defaultJWTValidationSettings audCheck) k jwt -------------------------------------------------------------------------------- diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 2070a52bf2..215272d221 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -111,15 +111,18 @@ testCreateAccessTokenSuccess brig = do (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest - result <- liftIO $ verify (fromMaybe (error "invalid key") fakeJwk) (cs $ unOauthAccessToken $ oatAccessToken accessToken) + verifiedOrError <- liftIO $ verify (fromMaybe (error "invalid key") fakeJwk) (cs $ unOauthAccessToken $ oatAccessToken accessToken) liftIO $ do - isRight result @?= True - scope <$> result @?= Right scopes - view claimIss <$> result @?= Right ("example.com" ^? stringOrUri @Text) - view claimAud <$> result @?= Right (Audience . (: []) <$> "example.com" ^? stringOrUri @Text) - view claimSub <$> result @?= Right (idToText uid ^? stringOrUri) - (\(NumericDate expTime) -> diffUTCTime expTime now > 0) . fromMaybe (error "exp claim missing") . view claimExp <$> result @?= Right True - (\(NumericDate issuingTime) -> diffUTCTime issuingTime now < 0) . fromMaybe (error "iat claim missing") . view claimIat <$> result @?= Right True + isRight verifiedOrError @?= True + let claims = either (error "invalid token") id verifiedOrError + scope claims @?= scopes + (view claimIss $ claims) @?= ("example.com" ^? stringOrUri @Text) + (view claimAud $ claims) @?= (Audience . (: []) <$> "example.com" ^? stringOrUri @Text) + (view claimSub $ claims) @?= (idToText uid ^? stringOrUri) + let expTime = (\(NumericDate x) -> x) . fromMaybe (error "exp claim missing") . view claimExp $ claims + diffUTCTime expTime now > 0 @?= True + let issuingTime = (\(NumericDate x) -> x) . fromMaybe (error "iat claim missing") . view claimIat $ claims + diffUTCTime issuingTime now < 0 @?= True ------------------------------------------------------------------------------- -- Util From d506bd1dbda2e42e980d982bed5e7c8507157b6a Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 8 Dec 2022 09:35:59 +0000 Subject: [PATCH 07/27] test wrong key fail --- services/brig/test/integration/API/OAuth.hs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 215272d221..1a5b562cc5 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -23,7 +23,9 @@ import Brig.API.OAuth import Brig.Effects.Jwk (fakeJwk) import Brig.Options import Control.Lens +import Crypto.JOSE (JWK) import Crypto.JWT (Audience (Audience), NumericDate (NumericDate), claimAud, claimExp, claimIat, claimIss, claimSub, stringOrUri) +import qualified Data.Aeson as A import Data.ByteString.Conversion (fromByteString, fromByteString', toByteString') import Data.Id (OAuthClientId, UserId, idToText, randomId) import Data.Range (unsafeRange) @@ -112,8 +114,10 @@ testCreateAccessTokenSuccess brig = do let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest verifiedOrError <- liftIO $ verify (fromMaybe (error "invalid key") fakeJwk) (cs $ unOauthAccessToken $ oatAccessToken accessToken) + verifiedOrErrorWithWrongKey <- liftIO $ verify wrongKey (cs $ unOauthAccessToken $ oatAccessToken accessToken) liftIO $ do isRight verifiedOrError @?= True + isLeft verifiedOrErrorWithWrongKey @?= True let claims = either (error "invalid token") id verifiedOrError scope claims @?= scopes (view claimIss $ claims) @?= ("example.com" ^? stringOrUri @Text) @@ -164,3 +168,6 @@ generateOAuthClientAndAuthCode brig uid scope url = do getQueryParamValue :: ByteString -> RedirectUrl -> Maybe ByteString getQueryParamValue key uri = snd <$> find ((== key) . fst) (getQueryParams uri) + +wrongKey :: JWK +wrongKey = fromMaybe (error "invalid jwk") $ A.decode "{\"p\":\"-Ahl1aNMOqXLUtJHVO1OLGt92EOrjzcNlwB5AL9hp8-GykJIK6BIfDvCCJgDUX-8ZZ-1R485XFVtUiI5W72MKbJ-qicTB7Smzd7St_zO6PZUbkgQoJiosAOMjP_8DBs9CbMl9FqUfE1pNo4O0gYHslUoCKwS5IsAB9HjuHGEQ38\",\"kty\":\"RSA\",\"q\":\"qRih0wBK2xg2wyJcBN6dDpUHTBxNEt8jxmvy33oMU-_Vx0hFLVeAqDYK-awlHGtJQJKp1mXdURXocKXKPukVitnfEH8nvl6vQIr4-uXyENe3yLgADi8VRDZCbWuDVWYAlYlFgdNODZ_A_fIqCmGAw27bwXyZZ3IRusnipyFN6iM\",\"d\":\"L0uBKJrI4I-_X9KPQawrLDEnPT7msevOH5Rf264CPZgwe8B9M0mbGmhIzYFIThNSaEzGoEtyJdTf27zoawh3O3KQO0aJr2HKSCTMZUh7fpqIjYlu5jA_dT3k7yHHMIR4lRLQV0vb936Mu09kTkRqMZ0jSo46dJ5iw0wnuSF0dAiqVG0rSJK-gVBdIbzZYxhSBW4ZF3n4CqtFb6lc1stfZHcnzWHyF6Cofzup6pJumeFe7xXF9-aGU-3UcTSzTnMa21NVP-vT2CXkH8dSfwLI-PuJwlW6tcpBwT2PXrCGyAGqQ3h5cdAmwcgfbla8wqrzj1A08SlkKHvTDixVvnnzpQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"0makAydOdX3vNv4YTToO45ccQUCOoLisvAFVyhiKA4c\",\"qi\":\"phNbA_tiDLQq1omVgM1dHtOe6Dd7J_ZoRdz1Rmc4uaSQyJe-yn88DxXlX10DJkM9uqyzcojOtD5awBUXgYSzmasZvcZ0e2XNi7iXmSwsggTux3lUVVqKWV8HreaSywJ-HqitxjitooWSWOyD9o8yq9RS4r2QdXyuCfthwnEZdpc\",\"dp\":\"q0IJJmjZYolFiYsdq5sq5erWerPGyl0l6gRuiECcqiTVmeQINu81_Wm5gPuNFwHO0JBkt-NBpOprUFHHLvwCwmu3n77ZGfH3VqCq-FT7fMlQ5NCngmvF1bqtmlHJ84X_MCpdY4oDioxcwEl4HDYDrHO17774UItVWxDmXl0rCPs\",\"alg\":\"PS512\",\"dq\":\"NznQQDVsPTofSIPEQeLisIyDoZvsoCk4ael_nPUjaZZ-32L_FNvrLQTZeMl8JVf0yJ4d0ePa8EyTaZb8AqflXT_i1mRw-n-6BP5earMG5_FMGMXfXsKJ04lVEJ94eT-jGTOH--qjJ1fxk_6vNEy73RgrtXmYMGzU1Yhx-duqsrk\",\"n\":\"o9VozUwUc1mQMrAH2fEna_ihmNa3CVRzK7MUgDHEbfY0T71wREpK4f4fOkDysKIqnmMdxRzJhsXTDpxX8_8AlKcimPgR8Qb2z7GwDsnDZOdgAYrZ7l7gj0nX02IX35MBk7a7tWr0nILFLV9SxEu6UFcZo0bL2Rhck81TRqLbomJpIzAq8VCS8uMQeg6hEMarl9tGvSKyFuMdTCV3JE9dSv_NErAWx7uBIgkai3Imjs4ufatvRsi9ZHaUV5V3NtrFbYDulg-GOH1eXZwnO6UrKgcAdB3nS1WKL-vcxqupceAHeFHRjARm6AV07hJyXVOVHxdffv6BFX5GihFPFvpQXQ\"}" From 32fdfe2a6bd83279a8f744fd75d6eaf352233101 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 8 Dec 2022 11:32:00 +0000 Subject: [PATCH 08/27] verify client secret, tests --- services/brig/src/Brig/API/OAuth.hs | 28 +++++++- services/brig/test/integration/API/OAuth.hs | 79 ++++++++++++++++++--- 2 files changed, 96 insertions(+), 11 deletions(-) diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index e077973932..94acbdc847 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -25,7 +25,7 @@ import Brig.App import Brig.Effects.Jwk import qualified Brig.Effects.Jwk as Jwk import qualified Brig.Options as Opt -import Brig.Password (Password, mkSafePassword) +import Brig.Password (Password, mkSafePassword, verifyPassword) import Cassandra hiding (Set) import qualified Cassandra as C import Control.Lens (view, (.~), (?~), (^?)) @@ -419,6 +419,9 @@ type OAuthAPI = :<|> Named "create-oauth-auth-code" ( Summary "" + :> CanThrow 'UnsupportedResponseType + :> CanThrow 'RedirectUrlMissMatch + :> CanThrow 'OAuthClientNotFound :> ZUser :> "oauth" :> "authorization" @@ -432,8 +435,10 @@ type OAuthAPI = ) :<|> Named "create-oauth-access-token" - ( -- TODO: add error responses and corresponding tests - Summary "Create an OAuth access token" + ( Summary "Create an OAuth access token" + :> CanThrow 'JwtError + :> CanThrow 'OAuthAuthCodeNotFound + :> CanThrow 'OAuthClientNotFound :> "oauth" :> "token" :> ReqBody '[FormUrlEncoded] OAuthAccessTokenRequest @@ -485,6 +490,7 @@ createAccessToken req = do >>= maybe (throwStd $ errorToWai @'OAuthAuthCodeNotFound) pure oauthClient <- getOAuthClient authCodeUserId (oatClientId req) >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure + unlessM (verifyClientSecret (oatClientSecret req) (ocId oauthClient)) $ throwStd $ errorToWai @'OAuthClientNotFound unless (ocRedirectUrl oauthClient == oatRedirectUri req) $ throwStd $ errorToWai @'OAuthAuthCodeNotFound unless (authCodeCid == oatClientId req) $ throwStd $ errorToWai @'OAuthAuthCodeNotFound unless (authCodeRedirectUrl == oatRedirectUri req) $ throwStd $ errorToWai @'OAuthAuthCodeNotFound @@ -520,6 +526,15 @@ createAccessToken req = do algo <- bestJWSAlg key signJWT key (newJWSHeader ((), algo)) claims + verifyClientSecret :: OAuthClientPlainTextSecret -> OAuthClientId -> (Handler r) Bool + verifyClientSecret secret cid = do + let plainTextPw = PlainTextPassword $ toText $ unOAuthClientPlainTextSecret secret + lift $ + wrapClient $ + lookupOAuthClientSecret cid <&> \case + Nothing -> False + Just pw -> verifyPassword plainTextPw pw + rand32Bytes :: MonadIO m => m AsciiBase16 rand32Bytes = liftIO . fmap encodeBase16 $ randBytes 32 @@ -546,6 +561,13 @@ lookupOauthClient cid = do q :: PrepQuery R (Identity OAuthClientId) (OAuthApplicationName, RedirectUrl) q = "SELECT name, redirect_uri FROM oauth_client WHERE id = ?" +lookupOAuthClientSecret :: (MonadClient m, MonadReader Env m) => OAuthClientId -> m (Maybe Password) +lookupOAuthClientSecret cid = do + runIdentity <$$> retry x5 (query1 q (params LocalQuorum (Identity cid))) + where + q :: PrepQuery R (Identity OAuthClientId) (Identity Password) + q = "SELECT secret FROM oauth_client WHERE id = ?" + insertOAuthAuthCode :: (MonadClient m, MonadReader Env m) => OAuthAuthCode -> OAuthClientId -> UserId -> OAuthScopes -> RedirectUrl -> m () insertOAuthAuthCode code cid uid scope uri = do let cqlScope = C.Set (Set.toList (unOAuthScopes scope)) diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 1a5b562cc5..f6e3d64c32 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -31,6 +31,7 @@ import Data.Id (OAuthClientId, UserId, idToText, randomId) import Data.Range (unsafeRange) import Data.Set as Set import Data.String.Conversions (cs) +import Data.Text.Ascii (encodeBase16) import Data.Time import Imports import qualified Network.Wai.Utilities as Error @@ -45,10 +46,18 @@ tests :: Manager -> Brig -> Opts -> TestTree tests m b _opts = do testGroup "oauth" $ [ test m "register new OAuth client" $ testRegisterNewOAuthClient b, - test m "create oauth code - success" $ testCreateOAuthCodeSuccess b, - test m "create oauth code - oauth client not found" $ testCreateOAuthCodeClientNotFound b, - test m "create oauth code - redirect url mismatch" $ testCreateOAuthCodeRedirectUrlMismatch b, - test m "create access token - success" $ testCreateAccessTokenSuccess b + testGroup "create oauth code" $ + [ test m "success" $ testCreateOAuthCodeSuccess b, + test m "oauth client not found" $ testCreateOAuthCodeClientNotFound b, + test m "redirect url mismatch" $ testCreateOAuthCodeRedirectUrlMismatch b + ], + testGroup "create access token" $ + [ test m "success" $ testCreateAccessTokenSuccess b, + test m "wrong client id fail" $ testCreateAccessTokenWrongClientId b, + test m "wrong client secret fail" $ testCreateAccessTokenWrongClientSecret b, + test m "wrong code fail" $ testCreateAccessTokenWrongAuthCode b, + test m "wrong redirect url fail" $ testCreateAccessTokenWrongUrl b + ] ] testRegisterNewOAuthClient :: Brig -> Http () @@ -113,6 +122,10 @@ testCreateAccessTokenSuccess brig = do (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest + -- authorization code should be deleted and can only be used once + createOAuthAccessToken' brig accessTokenRequest !!! do + const 404 === statusCode + const (Just "not-found") === fmap Error.label . responseJsonMaybe verifiedOrError <- liftIO $ verify (fromMaybe (error "invalid key") fakeJwk) (cs $ unOauthAccessToken $ oatAccessToken accessToken) verifiedOrErrorWithWrongKey <- liftIO $ verify wrongKey (cs $ unOauthAccessToken $ oatAccessToken accessToken) liftIO $ do @@ -126,7 +139,55 @@ testCreateAccessTokenSuccess brig = do let expTime = (\(NumericDate x) -> x) . fromMaybe (error "exp claim missing") . view claimExp $ claims diffUTCTime expTime now > 0 @?= True let issuingTime = (\(NumericDate x) -> x) . fromMaybe (error "iat claim missing") . view claimIat $ claims - diffUTCTime issuingTime now < 0 @?= True + abs (diffUTCTime issuingTime now) < 5 @?= True -- allow for some generous clock skew + +testCreateAccessTokenWrongClientId :: Brig -> Http () +testCreateAccessTokenWrongClientId brig = do + uid <- userId <$> randomUser brig + let redirectUrl = fromMaybe (error "invalid url") $ fromByteString' "https://example.com" + let scopes = OAuthScopes $ Set.fromList [ConversationCreate, ConversationCodeCreate] + (_, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl + cid <- randomId + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + createOAuthAccessToken' brig accessTokenRequest !!! do + const 404 === statusCode + const (Just "not-found") === fmap Error.label . responseJsonMaybe + +testCreateAccessTokenWrongClientSecret :: Brig -> Http () +testCreateAccessTokenWrongClientSecret brig = do + uid <- userId <$> randomUser brig + let redirectUrl = fromMaybe (error "invalid url") $ fromByteString' "https://example.com" + let scopes = OAuthScopes $ Set.fromList [ConversationCreate, ConversationCodeCreate] + (cid, _, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl + let secret = OAuthClientPlainTextSecret $ encodeBase16 "ee2316e304f5c318e4607d86748018eb9c66dc4f391c31bcccd9291d24b4c7e" + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + createOAuthAccessToken' brig accessTokenRequest !!! do + const 404 === statusCode + const (Just "not-found") === fmap Error.label . responseJsonMaybe + +testCreateAccessTokenWrongAuthCode :: Brig -> Http () +testCreateAccessTokenWrongAuthCode brig = do + uid <- userId <$> randomUser brig + let redirectUrl = fromMaybe (error "invalid url") $ fromByteString' "https://example.com" + let scopes = OAuthScopes $ Set.fromList [ConversationCreate, ConversationCodeCreate] + (cid, secret, _) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl + let code = OAuthAuthCode $ encodeBase16 "eb32eb9e2aa36c081c89067dddf81bce83c1c57e0b74cfb14c9f026f145f2b1f" + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + createOAuthAccessToken' brig accessTokenRequest !!! do + const 404 === statusCode + const (Just "not-found") === fmap Error.label . responseJsonMaybe + +testCreateAccessTokenWrongUrl :: Brig -> Http () +testCreateAccessTokenWrongUrl brig = do + uid <- userId <$> randomUser brig + let redirectUrl = fromMaybe (error "invalid url") $ fromByteString' "https://wire.com" + let scopes = OAuthScopes $ Set.fromList [ConversationCreate, ConversationCodeCreate] + (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl + let wrongUrl = fromMaybe (error "invalid url") $ fromByteString' "https://example.com" + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code wrongUrl + createOAuthAccessToken' brig accessTokenRequest !!! do + const 404 === statusCode + const (Just "not-found") === fmap Error.label . responseJsonMaybe ------------------------------------------------------------------------------- -- Util @@ -149,9 +210,11 @@ createOAuthCode :: HasCallStack => Brig -> UserId -> NewOAuthAuthCode -> Http Re createOAuthCode brig uid reqBody = post (brig . paths ["oauth", "authorization", "codes"] . zUser uid . json reqBody . noRedirect) createOAuthAccessToken :: HasCallStack => Brig -> OAuthAccessTokenRequest -> Http OAuthAccessTokenResponse -createOAuthAccessToken brig reqBody = do - r <- post (brig . paths ["oauth", "token"] . content "application/x-www-form-urlencoded" . body (RequestBodyLBS $ urlEncodeAsForm reqBody)) - responseJsonError r +createOAuthAccessToken brig reqBody = responseJsonError =<< createOAuthAccessToken' brig reqBody + +createOAuthAccessToken' :: HasCallStack => Brig -> OAuthAccessTokenRequest -> Http ResponseLBS +createOAuthAccessToken' brig reqBody = do + post (brig . paths ["oauth", "token"] . content "application/x-www-form-urlencoded" . body (RequestBodyLBS $ urlEncodeAsForm reqBody)) generateOAuthClientAndAuthCode :: Brig -> UserId -> OAuthScopes -> RedirectUrl -> Http (OAuthClientId, OAuthClientPlainTextSecret, OAuthAuthCode) generateOAuthClientAndAuthCode brig uid scope url = do From 6f8a8365ae81e0d5f9da8e1f1bf913bfad860b13 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 8 Dec 2022 11:45:17 +0000 Subject: [PATCH 09/27] nginz and helm --- charts/nginz/values.yaml | 4 ++++ services/nginz/integration-test/conf/nginz/nginx.conf | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index 4c7695da0c..5824e5d16c 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -388,6 +388,10 @@ nginx_conf: - path: /oauth/authorization/codes envs: - all + - path: /oauth/token + envs: + - all + disable_zauth: true galley: - path: /conversations/code-check disable_zauth: true diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index 1112a63c1a..30214299a9 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -301,6 +301,11 @@ http { proxy_pass http://brig; } + location /oauth/token { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + # Cargohold Endpoints rewrite ^/api-docs/assets /assets/api-docs?base_url=http://127.0.0.1:8080/ break; From f0d26b4aff7da0bd89bda84af7453c4025be171c Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 8 Dec 2022 11:47:44 +0000 Subject: [PATCH 10/27] changelog --- changelog.d/2-features/pr-2882 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/2-features/pr-2882 b/changelog.d/2-features/pr-2882 index ef558943b7..f482dd66f1 100644 --- a/changelog.d/2-features/pr-2882 +++ b/changelog.d/2-features/pr-2882 @@ -1 +1 @@ -New OAuth endpoints for registering an OAuth app and for retrieving an authorization code (#2882, #2901) +New OAuth endpoints for registering an OAuth app, for retrieving an authorization code, and for retrieving an access token (#2882, #2901, #2907) From 05b2fc16785e16edef5406a5b10a596633960908 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 8 Dec 2022 12:36:45 +0000 Subject: [PATCH 11/27] read jwk from path --- services/brig/brig.integration.yaml | 1 + services/brig/src/Brig/API/OAuth.hs | 4 ++-- services/brig/src/Brig/CanonicalInterpreter.hs | 2 +- services/brig/src/Brig/Effects/Jwk.hs | 11 +++-------- services/brig/src/Brig/Options.hs | 4 +++- services/brig/test/integration/API/OAuth.hs | 14 ++++++++------ services/brig/test/resources/oauth/jwk.json | 1 + 7 files changed, 19 insertions(+), 18 deletions(-) create mode 100644 services/brig/test/resources/oauth/jwk.json diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index e990d9be5f..fbb72e684c 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -193,6 +193,7 @@ optSettings: setDpopTokenExpirationTimeSecs: 300 # 5 minutes setPublicKeyBundle: test/resources/jwt/ed25519_bundle.pem setEnableMLS: true + setOAuthJwkKeyPair: test/resources/oauth/jwk.json logLevel: Warn # ^ NOTE: We log too much in brig, if we set this to Info like other services, running tests diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index 94acbdc847..a037e023a4 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -484,7 +484,6 @@ createNewOAuthAuthCode uid (NewOAuthAuthCode cid scope responseType redirectUrl createAccessToken :: (Member Now r, Member Jwk r) => OAuthAccessTokenRequest -> (Handler r) OAuthAccessTokenResponse createAccessToken req = do let exp :: NominalDiffTime = 60 * 60 * 24 * 7 * 3 -- (3 weeks) TODO: make configurable - let jwkFp :: FilePath = "" -- TODO: make configurable (authCodeCid, authCodeUserId, authCodeScopes, authCodeRedirectUrl) <- lift (wrapClient $ lookupAndDeleteOAuthAuthCode (oatCode req)) >>= maybe (throwStd $ errorToWai @'OAuthAuthCodeNotFound) pure @@ -497,7 +496,8 @@ createAccessToken req = do domain <- Opt.setFederationDomain <$> view settings claims <- mkClaims authCodeUserId domain authCodeScopes exp - key <- lift (liftSem $ Jwk.get jwkFp) >>= maybe (throwStd $ errorToWai @'JwtError) pure + fp <- view settings >>= maybe (throwStd $ errorToWai @'JwtError) pure . Opt.setOAuthJwkKeyPair + key <- lift (liftSem $ Jwk.get fp) >>= maybe (throwStd $ errorToWai @'JwtError) pure token <- OauthAccessToken . cs . encodeCompact <$> signJwtToken key claims pure $ OAuthAccessTokenResponse token OAuthAccessTokenTypeBearer exp where diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 9843017b07..3cea580063 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -78,7 +78,7 @@ runBrigToIO e (AppT ma) = do . interpretBlacklistPhonePrefixStoreToCassandra @Cas.Client . interpretJwtTools . interpretPublicKeyBundle - . interpretFakeJwk + . interpretJwk ) ) $ runReaderT ma e diff --git a/services/brig/src/Brig/Effects/Jwk.hs b/services/brig/src/Brig/Effects/Jwk.hs index 6b434d6fc1..fc6581c60f 100644 --- a/services/brig/src/Brig/Effects/Jwk.hs +++ b/services/brig/src/Brig/Effects/Jwk.hs @@ -16,12 +16,7 @@ data Jwk m a where makeSem ''Jwk interpretJwk :: Members '[Embed IO] r => Sem (Jwk ': r) a -> Sem r a -interpretJwk = interpret $ \(Get fp) -> do - contents :: Either IOException ByteString <- liftIO $ try $ BS.readFile fp - pure $ either (const Nothing) (decode . cs) contents +interpretJwk = interpret $ \(Get fp) -> liftIO $ readJwk fp -interpretFakeJwk :: Members '[Embed IO] r => Sem (Jwk ': r) a -> Sem r a -interpretFakeJwk = interpret $ \(Get _) -> pure $ fakeJwk - -fakeJwk :: Maybe JWK -fakeJwk = decode "{\"p\":\"8U9gI_GHo8ca1g-6Miv9f7E7zTvY90mPZO9Hhygf3ZxhFk_TNH7y3dMyUZebnYLbXf1wUltLve-nND9AO2omtz6WgPEjPomo6nLeIO1swzkBkTqrQhvPioo7rXIAlv4O5vEPPnLFJceSVRmDSGovkjcyklAhZiVRfzAv-_GdnG8\",\"kty\":\"RSA\",\"q\":\"43dqCXHtiIYJ2bvVvVW8Ch4yhJmPA9VUQSTN2aVlLZoqMOLh8rn6fl0UiBLCQJdoPI3Hc2QuS6_GohVrKI-WgpHEGMssZnH9cbfGuNUX7QK6glhsGSPoxSRpFJMgVDxG5jCgUSOt0BJdY_hgo7wwO_bx2VTdSJPgIt761TUNjiU\",\"d\":\"ixZk7xvHUYzY8Eu0NwAF5LoGG7xJSqSuvy7lg4Ag8Pb8imlvwvyo1G2aYpNXGWi9lTv8h_tnVaSBfb7KddS9KLpoC-EuBk6tn8EUyeevKdRD6c-WLZX6QehET_B_LWL_EQQRB2cqfiThkDghN5HWZn18-QskyYgS0vWS_EYquM8LS3s0emQhXkz77ZX4fQrhZGRCAmDeI-iZDt90uRTF6OceKKoC5eTimx9YimQ0Z-qOlKiCpIjplSgHL8QgIZrp6mLdCTVy3vxaegwSNKwGqCCwcewtdBky9nb9pX6sEc6CA3WfyYZMbthMYIYTi0KJ2kaaF2QdR3l7VLTdPBwK-Q\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"QVapB_JRK66AMCYKN7LHsl4DlkICTNYB6ExJrtmva88\",\"qi\":\"sYHbPsyiiRIBKbN-chbXYTLsd03jL_kiT2VdyMsP_tNjOz87WcC8Td-lBUIViw_aMq1VOjJEyhB6yE9-Po4YmlAFbPSf1rmypAdjTKRQznkuTGDue6yd4z1t4SgZJOSlpJSGBtkEiYaIlI9-fviPxtIpDh0JfAbE7XgPItu5vT8\",\"dp\":\"LFmnVNPWVx6H6yHW2dPF8osTb6P5O3HUnftwTQHt4WAVY_Fl4vcWEfj3_ZD8s9VSFhM3apuG7zC1rV-WoZTf6rvhE2--R4creXOFKc4ZOEQo6pU61pqf6VmAoF8chqPI_178q0CSxV_JoERhIZUKizgD3mpFyS1ArjygBREiI5c\",\"alg\":\"RS256\",\"dq\":\"UXTY7yryQhql-mfugc_q98CanKuU8G17r5xrbw5wriTwCPOmsFJzZr8UdCorkA_oTw8CpsrwXUBEJJUA-9R3tKhYjfxsGP-nIAsMyfHdkI2SlsztYQ9f8wE39Bs_z4qKZTJyprvdKY76DKvMh7YDm-Wx8_8VA0GZWUN6ldFa2lU\",\"n\":\"1mnyGVTdwCi4umlGMvT6y7aTmibMQuBvySoT5eZcwKti4_sfnEJhyWXfsE6tasUC8ce7YGzFiq886S9-iZc6hYW0ReIPQYCLZ5hn-fUCxefEZdX03e-uJww7OwX_kNQifgdYwzM__QJsy1_nbgEn7olTzplJfJmUSk9dkfFJ-3o5L3AJ1YMbFPgXae0OLmkxjVKfT_6093a0RLSpUCzcti4OhtNa-OEUMX4rYl0jQyzZyO281IH-MatecA1O_o6JbCI1wH46jWNY8of43cemzFwQaS2SnSyppOmJzGf9fpm5j5dozYrAdTdPYdZEA62CimvdLvJqEt4OjyQuRTAuCw\"}" +readJwk :: FilePath -> IO (Maybe JWK) +readJwk fp = try @IOException (BS.readFile fp) <&> either (const Nothing) (decode . cs) diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 464dc3d62f..221f518027 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -603,7 +603,9 @@ data Settings = Settings setDpopTokenExpirationTimeSecsInternal :: !(Maybe Word64), -- | Path to a .pem file containing the server's public key and private key -- e.g. to sign JWT tokens - setPublicKeyBundle :: !(Maybe FilePath) + setPublicKeyBundle :: !(Maybe FilePath), + -- | Path to the public and private JSON web key pair used to sign OAuth access tokens + setOAuthJwkKeyPair :: !(Maybe FilePath) } deriving (Show, Generic) diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index f6e3d64c32..47755e0e6b 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -20,8 +20,9 @@ module API.OAuth where import Bilge import Bilge.Assert import Brig.API.OAuth -import Brig.Effects.Jwk (fakeJwk) +import Brig.Effects.Jwk (readJwk) import Brig.Options +import qualified Brig.Options as Opt import Control.Lens import Crypto.JOSE (JWK) import Crypto.JWT (Audience (Audience), NumericDate (NumericDate), claimAud, claimExp, claimIat, claimIss, claimSub, stringOrUri) @@ -43,7 +44,7 @@ import Web.FormUrlEncoded import Wire.API.User tests :: Manager -> Brig -> Opts -> TestTree -tests m b _opts = do +tests m b opts = do testGroup "oauth" $ [ test m "register new OAuth client" $ testRegisterNewOAuthClient b, testGroup "create oauth code" $ @@ -52,7 +53,7 @@ tests m b _opts = do test m "redirect url mismatch" $ testCreateOAuthCodeRedirectUrlMismatch b ], testGroup "create access token" $ - [ test m "success" $ testCreateAccessTokenSuccess b, + [ test m "success" $ testCreateAccessTokenSuccess opts b, test m "wrong client id fail" $ testCreateAccessTokenWrongClientId b, test m "wrong client secret fail" $ testCreateAccessTokenWrongClientSecret b, test m "wrong code fail" $ testCreateAccessTokenWrongAuthCode b, @@ -113,8 +114,8 @@ testCreateOAuthCodeClientNotFound brig = do const 404 === statusCode const (Just "not-found") === fmap Error.label . responseJsonMaybe -testCreateAccessTokenSuccess :: Brig -> Http () -testCreateAccessTokenSuccess brig = do +testCreateAccessTokenSuccess :: Opt.Opts -> Brig -> Http () +testCreateAccessTokenSuccess opts brig = do now <- liftIO getCurrentTime uid <- userId <$> randomUser brig let redirectUrl = fromMaybe (error "invalid url") $ fromByteString' "https://example.com" @@ -126,7 +127,8 @@ testCreateAccessTokenSuccess brig = do createOAuthAccessToken' brig accessTokenRequest !!! do const 404 === statusCode const (Just "not-found") === fmap Error.label . responseJsonMaybe - verifiedOrError <- liftIO $ verify (fromMaybe (error "invalid key") fakeJwk) (cs $ unOauthAccessToken $ oatAccessToken accessToken) + k <- liftIO $ readJwk (fromMaybe "" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") + verifiedOrError <- liftIO $ verify k (cs $ unOauthAccessToken $ oatAccessToken accessToken) verifiedOrErrorWithWrongKey <- liftIO $ verify wrongKey (cs $ unOauthAccessToken $ oatAccessToken accessToken) liftIO $ do isRight verifiedOrError @?= True diff --git a/services/brig/test/resources/oauth/jwk.json b/services/brig/test/resources/oauth/jwk.json new file mode 100644 index 0000000000..1af478a608 --- /dev/null +++ b/services/brig/test/resources/oauth/jwk.json @@ -0,0 +1 @@ +{"p":"8U9gI_GHo8ca1g-6Miv9f7E7zTvY90mPZO9Hhygf3ZxhFk_TNH7y3dMyUZebnYLbXf1wUltLve-nND9AO2omtz6WgPEjPomo6nLeIO1swzkBkTqrQhvPioo7rXIAlv4O5vEPPnLFJceSVRmDSGovkjcyklAhZiVRfzAv-_GdnG8","kty":"RSA","q":"43dqCXHtiIYJ2bvVvVW8Ch4yhJmPA9VUQSTN2aVlLZoqMOLh8rn6fl0UiBLCQJdoPI3Hc2QuS6_GohVrKI-WgpHEGMssZnH9cbfGuNUX7QK6glhsGSPoxSRpFJMgVDxG5jCgUSOt0BJdY_hgo7wwO_bx2VTdSJPgIt761TUNjiU","d":"ixZk7xvHUYzY8Eu0NwAF5LoGG7xJSqSuvy7lg4Ag8Pb8imlvwvyo1G2aYpNXGWi9lTv8h_tnVaSBfb7KddS9KLpoC-EuBk6tn8EUyeevKdRD6c-WLZX6QehET_B_LWL_EQQRB2cqfiThkDghN5HWZn18-QskyYgS0vWS_EYquM8LS3s0emQhXkz77ZX4fQrhZGRCAmDeI-iZDt90uRTF6OceKKoC5eTimx9YimQ0Z-qOlKiCpIjplSgHL8QgIZrp6mLdCTVy3vxaegwSNKwGqCCwcewtdBky9nb9pX6sEc6CA3WfyYZMbthMYIYTi0KJ2kaaF2QdR3l7VLTdPBwK-Q","e":"AQAB","use":"sig","kid":"QVapB_JRK66AMCYKN7LHsl4DlkICTNYB6ExJrtmva88","qi":"sYHbPsyiiRIBKbN-chbXYTLsd03jL_kiT2VdyMsP_tNjOz87WcC8Td-lBUIViw_aMq1VOjJEyhB6yE9-Po4YmlAFbPSf1rmypAdjTKRQznkuTGDue6yd4z1t4SgZJOSlpJSGBtkEiYaIlI9-fviPxtIpDh0JfAbE7XgPItu5vT8","dp":"LFmnVNPWVx6H6yHW2dPF8osTb6P5O3HUnftwTQHt4WAVY_Fl4vcWEfj3_ZD8s9VSFhM3apuG7zC1rV-WoZTf6rvhE2--R4creXOFKc4ZOEQo6pU61pqf6VmAoF8chqPI_178q0CSxV_JoERhIZUKizgD3mpFyS1ArjygBREiI5c","alg":"RS256","dq":"UXTY7yryQhql-mfugc_q98CanKuU8G17r5xrbw5wriTwCPOmsFJzZr8UdCorkA_oTw8CpsrwXUBEJJUA-9R3tKhYjfxsGP-nIAsMyfHdkI2SlsztYQ9f8wE39Bs_z4qKZTJyprvdKY76DKvMh7YDm-Wx8_8VA0GZWUN6ldFa2lU","n":"1mnyGVTdwCi4umlGMvT6y7aTmibMQuBvySoT5eZcwKti4_sfnEJhyWXfsE6tasUC8ce7YGzFiq886S9-iZc6hYW0ReIPQYCLZ5hn-fUCxefEZdX03e-uJww7OwX_kNQifgdYwzM__QJsy1_nbgEn7olTzplJfJmUSk9dkfFJ-3o5L3AJ1YMbFPgXae0OLmkxjVKfT_6093a0RLSpUCzcti4OhtNa-OEUMX4rYl0jQyzZyO281IH-MatecA1O_o6JbCI1wH46jWNY8of43cemzFwQaS2SnSyppOmJzGf9fpm5j5dozYrAdTdPYdZEA62CimvdLvJqEt4OjyQuRTAuCw"} From e33904b9480c861129f181191297c5861098bbd0 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 8 Dec 2022 12:46:30 +0000 Subject: [PATCH 12/27] add jwk key to server secrets --- charts/brig/templates/configmap.yaml | 3 +++ charts/brig/templates/secret.yaml | 3 +++ hack/helm_vars/wire-server/values.yaml.gotmpl | 15 +++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/charts/brig/templates/configmap.yaml b/charts/brig/templates/configmap.yaml index df36b2331b..84d98bf957 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/brig/templates/configmap.yaml @@ -299,5 +299,8 @@ data: {{- if .setEnableMLS }} setEnableMLS: {{ .setEnableMLS }} {{- end }} + {{- if $.Values.secrets.oauthJwkKeyPair }} + setOAuthJwkKeyPair: /etc/wire/brig/secrets/jwk_oauth.json + {{- end }} {{- end }} {{- end }} diff --git a/charts/brig/templates/secret.yaml b/charts/brig/templates/secret.yaml index dd967b4a67..c2c0866613 100644 --- a/charts/brig/templates/secret.yaml +++ b/charts/brig/templates/secret.yaml @@ -28,5 +28,8 @@ data: {{- if .dpopSigKeyBundle }} dpop_sig_key_bundle.pem: {{ .dpopSigKeyBundle | b64enc | quote }} {{- end }} + {{- if .oauthJwkKeyPair }} + jwk_oath.json: {{ .oauthJwkKeyPair | b64enc | quote }} + {{- end }} {{- end }} diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 789cdf16b9..77995e42b0 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -121,6 +121,21 @@ brig: -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEACPvhIdimF20tOPjbb+fXJrwS2RKDp7686T90AZ0+Th8= -----END PUBLIC KEY----- + oauthJwkKeyPair: | + { + "p":"8U9gI_GHo8ca1g-6Miv9f7E7zTvY90mPZO9Hhygf3ZxhFk_TNH7y3dMyUZebnYLbXf1wUltLve-nND9AO2omtz6WgPEjPomo6nLeIO1swzkBkTqrQhvPioo7rXIAlv4O5vEPPnLFJceSVRmDSGovkjcyklAhZiVRfzAv-_GdnG8", + "kty":"RSA", + "q":"43dqCXHtiIYJ2bvVvVW8Ch4yhJmPA9VUQSTN2aVlLZoqMOLh8rn6fl0UiBLCQJdoPI3Hc2QuS6_GohVrKI-WgpHEGMssZnH9cbfGuNUX7QK6glhsGSPoxSRpFJMgVDxG5jCgUSOt0BJdY_hgo7wwO_bx2VTdSJPgIt761TUNjiU", + "d":"ixZk7xvHUYzY8Eu0NwAF5LoGG7xJSqSuvy7lg4Ag8Pb8imlvwvyo1G2aYpNXGWi9lTv8h_tnVaSBfb7KddS9KLpoC-EuBk6tn8EUyeevKdRD6c-WLZX6QehET_B_LWL_EQQRB2cqfiThkDghN5HWZn18-QskyYgS0vWS_EYquM8LS3s0emQhXkz77ZX4fQrhZGRCAmDeI-iZDt90uRTF6OceKKoC5eTimx9YimQ0Z-qOlKiCpIjplSgHL8QgIZrp6mLdCTVy3vxaegwSNKwGqCCwcewtdBky9nb9pX6sEc6CA3WfyYZMbthMYIYTi0KJ2kaaF2QdR3l7VLTdPBwK-Q", + "e":"AQAB", + "use":"sig", + "kid":"QVapB_JRK66AMCYKN7LHsl4DlkICTNYB6ExJrtmva88", + "qi":"sYHbPsyiiRIBKbN-chbXYTLsd03jL_kiT2VdyMsP_tNjOz87WcC8Td-lBUIViw_aMq1VOjJEyhB6yE9-Po4YmlAFbPSf1rmypAdjTKRQznkuTGDue6yd4z1t4SgZJOSlpJSGBtkEiYaIlI9-fviPxtIpDh0JfAbE7XgPItu5vT8", + "dp":"LFmnVNPWVx6H6yHW2dPF8osTb6P5O3HUnftwTQHt4WAVY_Fl4vcWEfj3_ZD8s9VSFhM3apuG7zC1rV-WoZTf6rvhE2--R4creXOFKc4ZOEQo6pU61pqf6VmAoF8chqPI_178q0CSxV_JoERhIZUKizgD3mpFyS1ArjygBREiI5c", + "alg":"RS256", + "dq":"UXTY7yryQhql-mfugc_q98CanKuU8G17r5xrbw5wriTwCPOmsFJzZr8UdCorkA_oTw8CpsrwXUBEJJUA-9R3tKhYjfxsGP-nIAsMyfHdkI2SlsztYQ9f8wE39Bs_z4qKZTJyprvdKY76DKvMh7YDm-Wx8_8VA0GZWUN6ldFa2lU", + "n":"1mnyGVTdwCi4umlGMvT6y7aTmibMQuBvySoT5eZcwKti4_sfnEJhyWXfsE6tasUC8ce7YGzFiq886S9-iZc6hYW0ReIPQYCLZ5hn-fUCxefEZdX03e-uJww7OwX_kNQifgdYwzM__QJsy1_nbgEn7olTzplJfJmUSk9dkfFJ-3o5L3AJ1YMbFPgXae0OLmkxjVKfT_6093a0RLSpUCzcti4OhtNa-OEUMX4rYl0jQyzZyO281IH-MatecA1O_o6JbCI1wH46jWNY8of43cemzFwQaS2SnSyppOmJzGf9fpm5j5dozYrAdTdPYdZEA62CimvdLvJqEt4OjyQuRTAuCw" + } tests: enableFederationTests: true cannon: From b4c05380403dcbbdb659df9e35282b8ecea2d0f8 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 8 Dec 2022 13:27:54 +0000 Subject: [PATCH 13/27] docs for setting up the jwk secret --- docs/src/how-to/install/index.rst | 1 + docs/src/how-to/install/oauth.md | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 docs/src/how-to/install/oauth.md diff --git a/docs/src/how-to/install/index.rst b/docs/src/how-to/install/index.rst index 03802f43c7..8af395dbb8 100644 --- a/docs/src/how-to/install/index.rst +++ b/docs/src/how-to/install/index.rst @@ -19,6 +19,7 @@ Installing wire-server (production) Other configuration options Server and team feature settings Messaging Layer Security (MLS) + OAuth Web app settings sft restund diff --git a/docs/src/how-to/install/oauth.md b/docs/src/how-to/install/oauth.md new file mode 100644 index 0000000000..0b710ae5e1 --- /dev/null +++ b/docs/src/how-to/install/oauth.md @@ -0,0 +1,30 @@ +# OAuth + +To use the OAuth functionality, you will need to set up a public and private JSON web key pair (JWK) in the wire-server helm chart. This key pair will be used to sign and verify OAuth access tokens. + +To configure the JWK, go to `brig.secrets.oauthJwkKeyPair` in the wire-server helm chart and provide the JWK information, as shown in the example below: + +```yaml +# values.yaml or secrets.yaml +brig: + secrets: + oauthJwkKeyPair: | + { + "p":"8U9gI_...", + "kty":"RSA", + "q":"43dqC...", + "d":"ixZk7x...", + "e":"AQAB", + "use":"sig", + "kid":"QVapB_J...", + "qi":"sYHbPsy...", + "dp":"LFmnVNPW...", + "alg":"RS256", + "dq":"UXTY7...", + "n":"1mnyGVT..." + } +``` + +Note that the JWK is a sensitive configuration value, so it is recommended to use helm's support for managing secrets instead of including it in a plaintext values.yaml file. + +Please keep in mind that OAuth is currently under development and may not be available for use yet. Once it is ready, you will be able to use the OAuth functionality by setting up the JWK as described above. From c9db3432c5a1d3b567bbd52c3769a4baa80c98bb Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 8 Dec 2022 13:39:04 +0000 Subject: [PATCH 14/27] dev docs config options --- docs/src/developer/reference/config-options.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 4fe558e4ad..e948c1861e 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -604,3 +604,14 @@ If there is no configuration for a domain, it's defaulted to `no_search`. #### `setEnableDevelopmentVersions` This options determines whether development versions should be enabled. If set to `False`, all development versions are removed from the `supported` field of the `/api-version` endpoint. Note that they are still listed in the `development` field, and continue to work normally. + +### OAuth + +Configure the JWK to sign and verify OAuth access tokens for local testing as follows: + +```yaml +# [brig.yaml] +optSettings: + # ... + setOAuthJwkKeyPair: test/resources/oauth/jwk.json +``` From c4445bc9e71ac784563d3c24beb23afa27dcbd69 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 8 Dec 2022 14:13:53 +0000 Subject: [PATCH 15/27] config for code and token expiration --- charts/brig/templates/configmap.yaml | 6 +++++ charts/brig/values.yaml | 2 ++ .../src/developer/reference/config-options.md | 12 +++++++++- docs/src/how-to/install/oauth.md | 17 +++++++++++++- hack/helm_vars/wire-server/values.yaml.gotmpl | 2 ++ services/brig/brig.integration.yaml | 2 ++ services/brig/src/Brig/API/OAuth.hs | 11 +++++----- services/brig/src/Brig/Options.hs | 22 ++++++++++++++++++- 8 files changed, 66 insertions(+), 8 deletions(-) diff --git a/charts/brig/templates/configmap.yaml b/charts/brig/templates/configmap.yaml index 84d98bf957..9af77032cb 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/brig/templates/configmap.yaml @@ -302,5 +302,11 @@ data: {{- if $.Values.secrets.oauthJwkKeyPair }} setOAuthJwkKeyPair: /etc/wire/brig/secrets/jwk_oauth.json {{- end }} + {{- if .setOAuthAuthCodeExpirationTimeSecs }} + setOAuthAuthCodeExpirationTimeSecs: {{ .setOAuthAuthCodeExpirationTimeSecs }} + {{- end }} + {{- if .setOAuthAccessTokenExpirationTimeSecs }} + setOAuthAccessTokenExpirationTimeSecs: {{ .setOAuthAccessTokenExpirationTimeSecs }} + {{- end }} {{- end }} {{- end }} diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml index 99418a4c6f..dc458d3763 100644 --- a/charts/brig/values.yaml +++ b/charts/brig/values.yaml @@ -87,6 +87,8 @@ config: setNonceTtlSecs: 300 # 5 minutes setDpopMaxSkewSecs: 1 setDpopTokenExpirationTimeSecs: 300 # 5 minutes + setOAuthAuthCodeExpirationTimeSecs: 300 # 5 minutes + setOAuthAccessTokenExpirationTimeSecs: 1814400 # 3 weeks smtp: passwordFile: /etc/wire/brig/secrets/smtp-password.txt proxy: {} diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index e948c1861e..b5138099fb 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -607,7 +607,7 @@ This options determines whether development versions should be enabled. If set t ### OAuth -Configure the JWK to sign and verify OAuth access tokens for local testing as follows: +Optionally, configure the JWK to sign and verify OAuth access tokens for local testing as follows: ```yaml # [brig.yaml] @@ -615,3 +615,13 @@ optSettings: # ... setOAuthJwkKeyPair: test/resources/oauth/jwk.json ``` + +Optionally, configure the OAuth authorization code and access token expiration time in seconds as follows: + +```yaml +# [brig.yaml] +optSettings: + # ... + setOAuthAuthCodeExpirationTimeSecs: 300 # 5 minutes + setOAuthAccessTokenExpirationTimeSecs: 1814400 # 3 weeks +``` diff --git a/docs/src/how-to/install/oauth.md b/docs/src/how-to/install/oauth.md index 0b710ae5e1..1b5e04a6e1 100644 --- a/docs/src/how-to/install/oauth.md +++ b/docs/src/how-to/install/oauth.md @@ -25,6 +25,21 @@ brig: } ``` -Note that the JWK is a sensitive configuration value, so it is recommended to use helm's support for managing secrets instead of including it in a plaintext values.yaml file. +Note that the JWK is a sensitive configuration value, so it is recommended to use Helm's support for managing secrets instead of including it in a plaintext values.yaml file. Please keep in mind that OAuth is currently under development and may not be available for use yet. Once it is ready, you will be able to use the OAuth functionality by setting up the JWK as described above. + +### OAuth authorization code and access token expiration + +The the OAuth authorization code expiration (default 5 minutes) and access token expiration (default 3 weeks) can be overridden in the Helm file as follows: + +```yaml +brig: + # ... + config: + # ... + optSettings: + # ... + setOAuthAuthCodeExpirationTimeSecs: 300 # 5 minutes + setOAuthAccessTokenExpirationTimeSecs: 1814400 # 3 weeks +``` diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 77995e42b0..bf4e437e40 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -86,6 +86,8 @@ brig: setDpopMaxSkewSecs: 1 setDpopTokenExpirationTimeSecs: 300 setEnableMLS: true + setOAuthAuthCodeExpirationTimeSecs: 300 # 5 minutes + setOAuthAccessTokenExpirationTimeSecs: 1814400 # 3 weeks aws: sesEndpoint: http://fake-aws-ses:4569 sqsEndpoint: http://fake-aws-sqs:4568 diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index fbb72e684c..11359c0170 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -194,6 +194,8 @@ optSettings: setPublicKeyBundle: test/resources/jwt/ed25519_bundle.pem setEnableMLS: true setOAuthJwkKeyPair: test/resources/oauth/jwk.json + setOAuthAuthCodeExpirationTimeSecs: 300 # 5 minutes + setOAuthAccessTokenExpirationTimeSecs: 1814400 # 3 weeks logLevel: Warn # ^ NOTE: We log too much in brig, if we set this to Info like other services, running tests diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index a037e023a4..b878ca1f24 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -476,14 +476,14 @@ createNewOAuthAuthCode uid (NewOAuthAuthCode cid scope responseType redirectUrl OAuthClient _ _ uri <- getOAuthClient uid cid >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure unless (uri == redirectUrl) $ throwStd $ errorToWai @'RedirectUrlMissMatch oauthCode <- OAuthAuthCode <$> rand32Bytes - lift $ wrapClient $ insertOAuthAuthCode oauthCode cid uid scope redirectUrl + ttl <- Opt.setOAuthAuthCodeExpirationTimeSecs <$> view settings + lift $ wrapClient $ insertOAuthAuthCode ttl oauthCode cid uid scope redirectUrl let queryParams = [("code", toByteString' oauthCode), ("state", cs state)] returnedRedirectUrl = redirectUrl & unRedirectUrl & (queryL . queryPairsL) .~ queryParams & RedirectUrl pure returnedRedirectUrl createAccessToken :: (Member Now r, Member Jwk r) => OAuthAccessTokenRequest -> (Handler r) OAuthAccessTokenResponse createAccessToken req = do - let exp :: NominalDiffTime = 60 * 60 * 24 * 7 * 3 -- (3 weeks) TODO: make configurable (authCodeCid, authCodeUserId, authCodeScopes, authCodeRedirectUrl) <- lift (wrapClient $ lookupAndDeleteOAuthAuthCode (oatCode req)) >>= maybe (throwStd $ errorToWai @'OAuthAuthCodeNotFound) pure @@ -495,6 +495,7 @@ createAccessToken req = do unless (authCodeRedirectUrl == oatRedirectUri req) $ throwStd $ errorToWai @'OAuthAuthCodeNotFound domain <- Opt.setFederationDomain <$> view settings + exp <- fromIntegral . Opt.setOAuthAccessTokenExpirationTimeSecs <$> view settings claims <- mkClaims authCodeUserId domain authCodeScopes exp fp <- view settings >>= maybe (throwStd $ errorToWai @'JwtError) pure . Opt.setOAuthJwkKeyPair key <- lift (liftSem $ Jwk.get fp) >>= maybe (throwStd $ errorToWai @'JwtError) pure @@ -568,13 +569,13 @@ lookupOAuthClientSecret cid = do q :: PrepQuery R (Identity OAuthClientId) (Identity Password) q = "SELECT secret FROM oauth_client WHERE id = ?" -insertOAuthAuthCode :: (MonadClient m, MonadReader Env m) => OAuthAuthCode -> OAuthClientId -> UserId -> OAuthScopes -> RedirectUrl -> m () -insertOAuthAuthCode code cid uid scope uri = do +insertOAuthAuthCode :: (MonadClient m, MonadReader Env m) => Word64 -> OAuthAuthCode -> OAuthClientId -> UserId -> OAuthScopes -> RedirectUrl -> m () +insertOAuthAuthCode ttl code cid uid scope uri = do let cqlScope = C.Set (Set.toList (unOAuthScopes scope)) retry x5 . write q $ params LocalQuorum (code, cid, uid, cqlScope, uri) where q :: PrepQuery W (OAuthAuthCode, OAuthClientId, UserId, C.Set OAuthScope, RedirectUrl) () - q = "INSERT INTO oauth_auth_code (code, client, user, scope, redirect_uri) VALUES (?, ?, ?, ?, ?) USING TTL 300" -- TODO: make configurable + q = fromString $ "INSERT INTO oauth_auth_code (code, client, user, scope, redirect_uri) VALUES (?, ?, ?, ?, ?) USING TTL " <> show ttl lookupOAuthAuthCode :: (MonadClient m, MonadReader Env m) => OAuthAuthCode -> m (Maybe (OAuthClientId, UserId, OAuthScopes, RedirectUrl)) lookupOAuthAuthCode code = do diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 221f518027..a751ca05d1 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -605,7 +605,13 @@ data Settings = Settings -- e.g. to sign JWT tokens setPublicKeyBundle :: !(Maybe FilePath), -- | Path to the public and private JSON web key pair used to sign OAuth access tokens - setOAuthJwkKeyPair :: !(Maybe FilePath) + setOAuthJwkKeyPair :: !(Maybe FilePath), + -- | The expiration time of an OAuth access token in seconds. + -- use `setOAuthAccessTokenExpirationTimeSecs` as the getter function which always provides a default value + setOAuthAccessTokenExpirationTimeSecsInternal :: !(Maybe Word64), + -- | The expiration time of an OAuth refresh token in seconds. + -- use `setOAuthAuthCodeExpirationTimeSecs` as the getter function which always provides a default value + setOAuthAuthCodeExpirationTimeSecsInternal :: !(Maybe Word64) } deriving (Show, Generic) @@ -651,6 +657,18 @@ defaultDpopTokenExpirationTimeSecs = 30 setDpopTokenExpirationTimeSecs :: Settings -> Word64 setDpopTokenExpirationTimeSecs = fromMaybe defaultDpopTokenExpirationTimeSecs . setDpopTokenExpirationTimeSecsInternal +defaultOAuthAccessTokenExpirationTimeSecs :: Word64 +defaultOAuthAccessTokenExpirationTimeSecs = 60 * 60 * 24 * 7 * 3 -- 3 weeks + +setOAuthAccessTokenExpirationTimeSecs :: Settings -> Word64 +setOAuthAccessTokenExpirationTimeSecs = fromMaybe defaultOAuthAccessTokenExpirationTimeSecs . setOAuthAccessTokenExpirationTimeSecsInternal + +defaultOAuthAuthCodeExpirationTimeSecs :: Word64 +defaultOAuthAuthCodeExpirationTimeSecs = 300 -- 5 minutes + +setOAuthAuthCodeExpirationTimeSecs :: Settings -> Word64 +setOAuthAuthCodeExpirationTimeSecs = fromMaybe defaultOAuthAuthCodeExpirationTimeSecs . setOAuthAuthCodeExpirationTimeSecsInternal + -- | The analog to `GT.FeatureFlags`. This type tracks only the things that we need to -- express our current cloud business logic. -- @@ -834,6 +852,8 @@ instance FromJSON Settings where "setNonceTtlSecsInternal" -> "setNonceTtlSecs" "setDpopMaxSkewSecsInternal" -> "setDpopMaxSkewSecs" "setDpopTokenExpirationTimeSecsInternal" -> "setDpopTokenExpirationTimeSecs" + "setOAuthAuthCodeExpirationTimeSecsInternal" -> "setOAuthAuthCodeExpirationTimeSecs" + "setOAuthAccessTokenExpirationTimeSecsInternal" -> "setOAuthAccessTokenExpirationTimeSecs" other -> other } From 031b967d7c3f1756be9c6f75e7d95abacfe1f14f Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 8 Dec 2022 14:42:14 +0000 Subject: [PATCH 16/27] fix nix packages --- nix/haskell-pins.nix | 24 ++++++++++++++++++++---- services/brig/default.nix | 4 ++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 0082da9262..b10d85c249 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -3,12 +3,12 @@ # 1. If your target git repository has only package with the cabal file at the # root, add it like this under 'gitPins': # = { -# src = fetchgit = { -# url = "" +# src = fetchgit { +# url = ""; # rev = ""; # sha256 = ""; -# } -# } +# }; +# }; # # 2. If your target git repsitory has many packages, add it like this under 'gitPins': # @@ -227,6 +227,22 @@ let sha256 = "sha256-8XeCeJWbkdqrUf6tERFMoGM8xRI5l/nKNqI810kzMs0="; }; }; + tasty-hedgehog = { + src = fetchgit { + url = "https://github.com/qfpl/tasty-hedgehog"; + rev = "729617f82699be189954825920d6f30985e1cfa7"; + sha256 = "sha256-O81wlQbzwCOWLueDLiqf/K2g9XWvSNWgHv7IbYmLsgI="; + }; + }; + jose = { + src = fetchgit { + url = "https://github.com/frasertweedale/hs-jose"; + rev = "a7f919b19f667dfbb4d5c989ce620d3e75af8247"; + sha256 = "sha256-SKEE9ZqhjBxHYUKQaoB4IpN4/Ui3tS4S98FgZqj7WlY="; + }; + }; + }; + hackagePins = { kind-generics = { src = fetchgit { url = "https://gitlab.com/trupill/kind-generics.git"; diff --git a/services/brig/default.nix b/services/brig/default.nix index b67ec42e53..ca9e36c9e7 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -68,6 +68,7 @@ , insert-ordered-containers , iproute , iso639 +, jose , jwt-tools , lens , lens-aeson @@ -213,6 +214,7 @@ mkDerivation { HsOpenSSL HsOpenSSL-x509-system html-entities + http-api-data http-client http-client-openssl http-media @@ -221,6 +223,7 @@ mkDerivation { insert-ordered-containers iproute iso639 + jose jwt-tools lens lens-aeson @@ -332,6 +335,7 @@ mkDerivation { http-reverse-proxy http-types imports + jose lens lens-aeson metrics-wai From a6866e801741dbb391e08d876c98215559249efb Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 9 Dec 2022 08:28:32 +0000 Subject: [PATCH 17/27] test auth code expires --- hack/helm_vars/wire-server/values.yaml.gotmpl | 2 +- services/brig/brig.integration.yaml | 2 +- services/brig/test/integration/API/OAuth.hs | 16 +++++++++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index bf4e437e40..f6255cc33e 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -86,7 +86,7 @@ brig: setDpopMaxSkewSecs: 1 setDpopTokenExpirationTimeSecs: 300 setEnableMLS: true - setOAuthAuthCodeExpirationTimeSecs: 300 # 5 minutes + setOAuthAuthCodeExpirationTimeSecs: 3 # 3 secs setOAuthAccessTokenExpirationTimeSecs: 1814400 # 3 weeks aws: sesEndpoint: http://fake-aws-ses:4569 diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index 11359c0170..5c3f70bfca 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -194,7 +194,7 @@ optSettings: setPublicKeyBundle: test/resources/jwt/ed25519_bundle.pem setEnableMLS: true setOAuthJwkKeyPair: test/resources/oauth/jwk.json - setOAuthAuthCodeExpirationTimeSecs: 300 # 5 minutes + setOAuthAuthCodeExpirationTimeSecs: 3 # 3 secs setOAuthAccessTokenExpirationTimeSecs: 1814400 # 3 weeks logLevel: Warn diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 47755e0e6b..945dc12ae6 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -57,7 +57,8 @@ tests m b opts = do test m "wrong client id fail" $ testCreateAccessTokenWrongClientId b, test m "wrong client secret fail" $ testCreateAccessTokenWrongClientSecret b, test m "wrong code fail" $ testCreateAccessTokenWrongAuthCode b, - test m "wrong redirect url fail" $ testCreateAccessTokenWrongUrl b + test m "wrong redirect url fail" $ testCreateAccessTokenWrongUrl b, + test m "expired code fail" $ testCreateAccessTokenExpiredCode b ] ] @@ -191,6 +192,19 @@ testCreateAccessTokenWrongUrl brig = do const 404 === statusCode const (Just "not-found") === fmap Error.label . responseJsonMaybe +testCreateAccessTokenExpiredCode :: Brig -> Http () +testCreateAccessTokenExpiredCode brig = do + uid <- userId <$> randomUser brig + let redirectUrl = fromMaybe (error "invalid url") $ fromByteString' "https://example.com" + let scopes = OAuthScopes $ Set.fromList [ConversationCreate, ConversationCodeCreate] + (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl + -- assuming that the code is valid for 3 seconds + liftIO $ threadDelay (4 * 1000 * 1000) + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + createOAuthAccessToken' brig accessTokenRequest !!! do + const 404 === statusCode + const (Just "not-found") === fmap Error.label . responseJsonMaybe + ------------------------------------------------------------------------------- -- Util From 9c497bb3d1110d25fcd88c1a415d217368e96ed5 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 9 Dec 2022 12:12:31 +0000 Subject: [PATCH 18/27] test for status code as well --- services/brig/test/integration/API/OAuth.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 945dc12ae6..c70473e085 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -226,7 +226,7 @@ createOAuthCode :: HasCallStack => Brig -> UserId -> NewOAuthAuthCode -> Http Re createOAuthCode brig uid reqBody = post (brig . paths ["oauth", "authorization", "codes"] . zUser uid . json reqBody . noRedirect) createOAuthAccessToken :: HasCallStack => Brig -> OAuthAccessTokenRequest -> Http OAuthAccessTokenResponse -createOAuthAccessToken brig reqBody = responseJsonError =<< createOAuthAccessToken' brig reqBody +createOAuthAccessToken brig reqBody = responseJsonError =<< createOAuthAccessToken' brig reqBody Brig -> OAuthAccessTokenRequest -> Http ResponseLBS createOAuthAccessToken' brig reqBody = do From 8bf014933be0c5753c704db57f5002a68c53e2ae Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 9 Dec 2022 15:16:44 +0000 Subject: [PATCH 19/27] attempt to fix json in yaml file --- charts/brig/templates/secret.yaml | 2 +- hack/helm_vars/wire-server/values.yaml.gotmpl | 16 +--------------- services/brig/src/Brig/API/OAuth.hs | 2 -- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/charts/brig/templates/secret.yaml b/charts/brig/templates/secret.yaml index c2c0866613..4661df44e9 100644 --- a/charts/brig/templates/secret.yaml +++ b/charts/brig/templates/secret.yaml @@ -29,7 +29,7 @@ data: dpop_sig_key_bundle.pem: {{ .dpopSigKeyBundle | b64enc | quote }} {{- end }} {{- if .oauthJwkKeyPair }} - jwk_oath.json: {{ .oauthJwkKeyPair | b64enc | quote }} + jwk_oath.json: {{ .oauthJwkKeyPair | json | quote }} {{- end }} {{- end }} diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index f6255cc33e..5e8bd026b4 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -123,21 +123,7 @@ brig: -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEACPvhIdimF20tOPjbb+fXJrwS2RKDp7686T90AZ0+Th8= -----END PUBLIC KEY----- - oauthJwkKeyPair: | - { - "p":"8U9gI_GHo8ca1g-6Miv9f7E7zTvY90mPZO9Hhygf3ZxhFk_TNH7y3dMyUZebnYLbXf1wUltLve-nND9AO2omtz6WgPEjPomo6nLeIO1swzkBkTqrQhvPioo7rXIAlv4O5vEPPnLFJceSVRmDSGovkjcyklAhZiVRfzAv-_GdnG8", - "kty":"RSA", - "q":"43dqCXHtiIYJ2bvVvVW8Ch4yhJmPA9VUQSTN2aVlLZoqMOLh8rn6fl0UiBLCQJdoPI3Hc2QuS6_GohVrKI-WgpHEGMssZnH9cbfGuNUX7QK6glhsGSPoxSRpFJMgVDxG5jCgUSOt0BJdY_hgo7wwO_bx2VTdSJPgIt761TUNjiU", - "d":"ixZk7xvHUYzY8Eu0NwAF5LoGG7xJSqSuvy7lg4Ag8Pb8imlvwvyo1G2aYpNXGWi9lTv8h_tnVaSBfb7KddS9KLpoC-EuBk6tn8EUyeevKdRD6c-WLZX6QehET_B_LWL_EQQRB2cqfiThkDghN5HWZn18-QskyYgS0vWS_EYquM8LS3s0emQhXkz77ZX4fQrhZGRCAmDeI-iZDt90uRTF6OceKKoC5eTimx9YimQ0Z-qOlKiCpIjplSgHL8QgIZrp6mLdCTVy3vxaegwSNKwGqCCwcewtdBky9nb9pX6sEc6CA3WfyYZMbthMYIYTi0KJ2kaaF2QdR3l7VLTdPBwK-Q", - "e":"AQAB", - "use":"sig", - "kid":"QVapB_JRK66AMCYKN7LHsl4DlkICTNYB6ExJrtmva88", - "qi":"sYHbPsyiiRIBKbN-chbXYTLsd03jL_kiT2VdyMsP_tNjOz87WcC8Td-lBUIViw_aMq1VOjJEyhB6yE9-Po4YmlAFbPSf1rmypAdjTKRQznkuTGDue6yd4z1t4SgZJOSlpJSGBtkEiYaIlI9-fviPxtIpDh0JfAbE7XgPItu5vT8", - "dp":"LFmnVNPWVx6H6yHW2dPF8osTb6P5O3HUnftwTQHt4WAVY_Fl4vcWEfj3_ZD8s9VSFhM3apuG7zC1rV-WoZTf6rvhE2--R4creXOFKc4ZOEQo6pU61pqf6VmAoF8chqPI_178q0CSxV_JoERhIZUKizgD3mpFyS1ArjygBREiI5c", - "alg":"RS256", - "dq":"UXTY7yryQhql-mfugc_q98CanKuU8G17r5xrbw5wriTwCPOmsFJzZr8UdCorkA_oTw8CpsrwXUBEJJUA-9R3tKhYjfxsGP-nIAsMyfHdkI2SlsztYQ9f8wE39Bs_z4qKZTJyprvdKY76DKvMh7YDm-Wx8_8VA0GZWUN6ldFa2lU", - "n":"1mnyGVTdwCi4umlGMvT6y7aTmibMQuBvySoT5eZcwKti4_sfnEJhyWXfsE6tasUC8ce7YGzFiq886S9-iZc6hYW0ReIPQYCLZ5hn-fUCxefEZdX03e-uJww7OwX_kNQifgdYwzM__QJsy1_nbgEn7olTzplJfJmUSk9dkfFJ-3o5L3AJ1YMbFPgXae0OLmkxjVKfT_6093a0RLSpUCzcti4OhtNa-OEUMX4rYl0jQyzZyO281IH-MatecA1O_o6JbCI1wH46jWNY8of43cemzFwQaS2SnSyppOmJzGf9fpm5j5dozYrAdTdPYdZEA62CimvdLvJqEt4OjyQuRTAuCw" - } + oauthJwkKeyPair: "{\"p\":\"8U9gI_GHo8ca1g-6Miv9f7E7zTvY90mPZO9Hhygf3ZxhFk_TNH7y3dMyUZebnYLbXf1wUltLve-nND9AO2omtz6WgPEjPomo6nLeIO1swzkBkTqrQhvPioo7rXIAlv4O5vEPPnLFJceSVRmDSGovkjcyklAhZiVRfzAv-_GdnG8\",\"kty\":\"RSA\",\"q\":\"43dqCXHtiIYJ2bvVvVW8Ch4yhJmPA9VUQSTN2aVlLZoqMOLh8rn6fl0UiBLCQJdoPI3Hc2QuS6_GohVrKI-WgpHEGMssZnH9cbfGuNUX7QK6glhsGSPoxSRpFJMgVDxG5jCgUSOt0BJdY_hgo7wwO_bx2VTdSJPgIt761TUNjiU\",\"d\":\"ixZk7xvHUYzY8Eu0NwAF5LoGG7xJSqSuvy7lg4Ag8Pb8imlvwvyo1G2aYpNXGWi9lTv8h_tnVaSBfb7KddS9KLpoC-EuBk6tn8EUyeevKdRD6c-WLZX6QehET_B_LWL_EQQRB2cqfiThkDghN5HWZn18-QskyYgS0vWS_EYquM8LS3s0emQhXkz77ZX4fQrhZGRCAmDeI-iZDt90uRTF6OceKKoC5eTimx9YimQ0Z-qOlKiCpIjplSgHL8QgIZrp6mLdCTVy3vxaegwSNKwGqCCwcewtdBky9nb9pX6sEc6CA3WfyYZMbthMYIYTi0KJ2kaaF2QdR3l7VLTdPBwK-Q\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"QVapB_JRK66AMCYKN7LHsl4DlkICTNYB6ExJrtmva88\",\"qi\":\"sYHbPsyiiRIBKbN-chbXYTLsd03jL_kiT2VdyMsP_tNjOz87WcC8Td-lBUIViw_aMq1VOjJEyhB6yE9-Po4YmlAFbPSf1rmypAdjTKRQznkuTGDue6yd4z1t4SgZJOSlpJSGBtkEiYaIlI9-fviPxtIpDh0JfAbE7XgPItu5vT8\",\"dp\":\"LFmnVNPWVx6H6yHW2dPF8osTb6P5O3HUnftwTQHt4WAVY_Fl4vcWEfj3_ZD8s9VSFhM3apuG7zC1rV-WoZTf6rvhE2--R4creXOFKc4ZOEQo6pU61pqf6VmAoF8chqPI_178q0CSxV_JoERhIZUKizgD3mpFyS1ArjygBREiI5c\",\"alg\":\"RS256\",\"dq\":\"UXTY7yryQhql-mfugc_q98CanKuU8G17r5xrbw5wriTwCPOmsFJzZr8UdCorkA_oTw8CpsrwXUBEJJUA-9R3tKhYjfxsGP-nIAsMyfHdkI2SlsztYQ9f8wE39Bs_z4qKZTJyprvdKY76DKvMh7YDm-Wx8_8VA0GZWUN6ldFa2lU\",\"n\":\"1mnyGVTdwCi4umlGMvT6y7aTmibMQuBvySoT5eZcwKti4_sfnEJhyWXfsE6tasUC8ce7YGzFiq886S9-iZc6hYW0ReIPQYCLZ5hn-fUCxefEZdX03e-uJww7OwX_kNQifgdYwzM__QJsy1_nbgEn7olTzplJfJmUSk9dkfFJ-3o5L3AJ1YMbFPgXae0OLmkxjVKfT_6093a0RLSpUCzcti4OhtNa-OEUMX4rYl0jQyzZyO281IH-MatecA1O_o6JbCI1wH46jWNY8of43cemzFwQaS2SnSyppOmJzGf9fpm5j5dozYrAdTdPYdZEA62CimvdLvJqEt4OjyQuRTAuCw\"}" tests: enableFederationTests: true cannon: diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index b878ca1f24..9585bac79a 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE DeriveGeneric #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH From 25c22faa8181f73048cdc3b6a8541b1802daf691 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 9 Dec 2022 15:43:15 +0000 Subject: [PATCH 20/27] another attempt to fix json in yaml --- charts/brig/templates/secret.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/brig/templates/secret.yaml b/charts/brig/templates/secret.yaml index 4661df44e9..c2c0866613 100644 --- a/charts/brig/templates/secret.yaml +++ b/charts/brig/templates/secret.yaml @@ -29,7 +29,7 @@ data: dpop_sig_key_bundle.pem: {{ .dpopSigKeyBundle | b64enc | quote }} {{- end }} {{- if .oauthJwkKeyPair }} - jwk_oath.json: {{ .oauthJwkKeyPair | json | quote }} + jwk_oath.json: {{ .oauthJwkKeyPair | b64enc | quote }} {{- end }} {{- end }} From f0cf11c3f5a2291ac227d04aaf92bb6a9d7fc737 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 14 Dec 2022 09:31:06 +0000 Subject: [PATCH 21/27] jwk in integration tests another attempt --- charts/brig/templates/secret.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/brig/templates/secret.yaml b/charts/brig/templates/secret.yaml index c2c0866613..ea9079de42 100644 --- a/charts/brig/templates/secret.yaml +++ b/charts/brig/templates/secret.yaml @@ -29,7 +29,7 @@ data: dpop_sig_key_bundle.pem: {{ .dpopSigKeyBundle | b64enc | quote }} {{- end }} {{- if .oauthJwkKeyPair }} - jwk_oath.json: {{ .oauthJwkKeyPair | b64enc | quote }} + jwk_oath.json: {{ .oauthJwkKeyPair }} {{- end }} {{- end }} From cdee5479913fd80424c657671904348cbdd2e3a0 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 14 Dec 2022 11:30:44 +0000 Subject: [PATCH 22/27] next attempt --- charts/brig/templates/secret.yaml | 2 +- services/brig/src/Brig/Effects/Jwk.hs | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/charts/brig/templates/secret.yaml b/charts/brig/templates/secret.yaml index ea9079de42..ba083da0b3 100644 --- a/charts/brig/templates/secret.yaml +++ b/charts/brig/templates/secret.yaml @@ -29,7 +29,7 @@ data: dpop_sig_key_bundle.pem: {{ .dpopSigKeyBundle | b64enc | quote }} {{- end }} {{- if .oauthJwkKeyPair }} - jwk_oath.json: {{ .oauthJwkKeyPair }} + jwk_oath.pem: {{ .oauthJwkKeyPair | b64enc | quote }} {{- end }} {{- end }} diff --git a/services/brig/src/Brig/Effects/Jwk.hs b/services/brig/src/Brig/Effects/Jwk.hs index fc6581c60f..5451d68577 100644 --- a/services/brig/src/Brig/Effects/Jwk.hs +++ b/services/brig/src/Brig/Effects/Jwk.hs @@ -9,6 +9,7 @@ import qualified Data.ByteString as BS import Data.String.Conversions (cs) import Imports import Polysemy +import Debug.Trace (traceM) data Jwk m a where Get :: FilePath -> Jwk m (Maybe JWK) @@ -19,4 +20,12 @@ interpretJwk :: Members '[Embed IO] r => Sem (Jwk ': r) a -> Sem r a interpretJwk = interpret $ \(Get fp) -> liftIO $ readJwk fp readJwk :: FilePath -> IO (Maybe JWK) -readJwk fp = try @IOException (BS.readFile fp) <&> either (const Nothing) (decode . cs) +readJwk fp = do -- try @IOException (BS.readFile fp) <&> either (const Nothing) (decode . cs) + bsOrError <- try @IOException $ BS.readFile fp + case bsOrError of + Left err -> traceM ("Failed to read file because: " <> show err) $> Nothing + Right bs -> do + traceM $ "File contents:\n" <> show bs + case eitherDecode (cs bs) of + Left err -> traceM ("Failed to decode file because: " <> show err) $> Nothing + Right jwk -> pure $ Just jwk From 4d902f8c6301cadab00075a0ac416c5a3e435fdd Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 14 Dec 2022 11:38:29 +0000 Subject: [PATCH 23/27] formatting --- services/brig/src/Brig/Effects/Jwk.hs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/brig/src/Brig/Effects/Jwk.hs b/services/brig/src/Brig/Effects/Jwk.hs index 5451d68577..86dfa6c3d4 100644 --- a/services/brig/src/Brig/Effects/Jwk.hs +++ b/services/brig/src/Brig/Effects/Jwk.hs @@ -7,9 +7,9 @@ import Crypto.JOSE.JWK import Data.Aeson import qualified Data.ByteString as BS import Data.String.Conversions (cs) +import Debug.Trace (traceM) import Imports import Polysemy -import Debug.Trace (traceM) data Jwk m a where Get :: FilePath -> Jwk m (Maybe JWK) @@ -20,7 +20,8 @@ interpretJwk :: Members '[Embed IO] r => Sem (Jwk ': r) a -> Sem r a interpretJwk = interpret $ \(Get fp) -> liftIO $ readJwk fp readJwk :: FilePath -> IO (Maybe JWK) -readJwk fp = do -- try @IOException (BS.readFile fp) <&> either (const Nothing) (decode . cs) +readJwk fp = do + -- try @IOException (BS.readFile fp) <&> either (const Nothing) (decode . cs) bsOrError <- try @IOException $ BS.readFile fp case bsOrError of Left err -> traceM ("Failed to read file because: " <> show err) $> Nothing @@ -28,4 +29,4 @@ readJwk fp = do -- try @IOException (BS.readFile fp) <&> either (const Nothing) traceM $ "File contents:\n" <> show bs case eitherDecode (cs bs) of Left err -> traceM ("Failed to decode file because: " <> show err) $> Nothing - Right jwk -> pure $ Just jwk + Right jwk -> pure $ Just jwk From c67809b5f5a0ad0c1d0bb19b9bd0cd0f9014ed91 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 14 Dec 2022 12:22:08 +0000 Subject: [PATCH 24/27] next attempt - hopefully works - fixed typo --- charts/brig/templates/secret.yaml | 2 +- docs/src/developer/reference/config-options.md | 4 ++++ hack/helm_vars/wire-server/values.yaml.gotmpl | 16 +++++++++++++++- services/brig/src/Brig/Effects/Jwk.hs | 12 +----------- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/charts/brig/templates/secret.yaml b/charts/brig/templates/secret.yaml index ba083da0b3..979568d0d5 100644 --- a/charts/brig/templates/secret.yaml +++ b/charts/brig/templates/secret.yaml @@ -29,7 +29,7 @@ data: dpop_sig_key_bundle.pem: {{ .dpopSigKeyBundle | b64enc | quote }} {{- end }} {{- if .oauthJwkKeyPair }} - jwk_oath.pem: {{ .oauthJwkKeyPair | b64enc | quote }} + jwk_oauth.json: {{ .oauthJwkKeyPair | b64enc | quote }} {{- end }} {{- end }} diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index b5138099fb..bf3c1470c6 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -607,6 +607,8 @@ This options determines whether development versions should be enabled. If set t ### OAuth +#### JWK + Optionally, configure the JWK to sign and verify OAuth access tokens for local testing as follows: ```yaml @@ -616,6 +618,8 @@ optSettings: setOAuthJwkKeyPair: test/resources/oauth/jwk.json ``` +#### Expiration time + Optionally, configure the OAuth authorization code and access token expiration time in seconds as follows: ```yaml diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 5e8bd026b4..2a4b3e0aef 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -123,7 +123,21 @@ brig: -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEACPvhIdimF20tOPjbb+fXJrwS2RKDp7686T90AZ0+Th8= -----END PUBLIC KEY----- - oauthJwkKeyPair: "{\"p\":\"8U9gI_GHo8ca1g-6Miv9f7E7zTvY90mPZO9Hhygf3ZxhFk_TNH7y3dMyUZebnYLbXf1wUltLve-nND9AO2omtz6WgPEjPomo6nLeIO1swzkBkTqrQhvPioo7rXIAlv4O5vEPPnLFJceSVRmDSGovkjcyklAhZiVRfzAv-_GdnG8\",\"kty\":\"RSA\",\"q\":\"43dqCXHtiIYJ2bvVvVW8Ch4yhJmPA9VUQSTN2aVlLZoqMOLh8rn6fl0UiBLCQJdoPI3Hc2QuS6_GohVrKI-WgpHEGMssZnH9cbfGuNUX7QK6glhsGSPoxSRpFJMgVDxG5jCgUSOt0BJdY_hgo7wwO_bx2VTdSJPgIt761TUNjiU\",\"d\":\"ixZk7xvHUYzY8Eu0NwAF5LoGG7xJSqSuvy7lg4Ag8Pb8imlvwvyo1G2aYpNXGWi9lTv8h_tnVaSBfb7KddS9KLpoC-EuBk6tn8EUyeevKdRD6c-WLZX6QehET_B_LWL_EQQRB2cqfiThkDghN5HWZn18-QskyYgS0vWS_EYquM8LS3s0emQhXkz77ZX4fQrhZGRCAmDeI-iZDt90uRTF6OceKKoC5eTimx9YimQ0Z-qOlKiCpIjplSgHL8QgIZrp6mLdCTVy3vxaegwSNKwGqCCwcewtdBky9nb9pX6sEc6CA3WfyYZMbthMYIYTi0KJ2kaaF2QdR3l7VLTdPBwK-Q\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"QVapB_JRK66AMCYKN7LHsl4DlkICTNYB6ExJrtmva88\",\"qi\":\"sYHbPsyiiRIBKbN-chbXYTLsd03jL_kiT2VdyMsP_tNjOz87WcC8Td-lBUIViw_aMq1VOjJEyhB6yE9-Po4YmlAFbPSf1rmypAdjTKRQznkuTGDue6yd4z1t4SgZJOSlpJSGBtkEiYaIlI9-fviPxtIpDh0JfAbE7XgPItu5vT8\",\"dp\":\"LFmnVNPWVx6H6yHW2dPF8osTb6P5O3HUnftwTQHt4WAVY_Fl4vcWEfj3_ZD8s9VSFhM3apuG7zC1rV-WoZTf6rvhE2--R4creXOFKc4ZOEQo6pU61pqf6VmAoF8chqPI_178q0CSxV_JoERhIZUKizgD3mpFyS1ArjygBREiI5c\",\"alg\":\"RS256\",\"dq\":\"UXTY7yryQhql-mfugc_q98CanKuU8G17r5xrbw5wriTwCPOmsFJzZr8UdCorkA_oTw8CpsrwXUBEJJUA-9R3tKhYjfxsGP-nIAsMyfHdkI2SlsztYQ9f8wE39Bs_z4qKZTJyprvdKY76DKvMh7YDm-Wx8_8VA0GZWUN6ldFa2lU\",\"n\":\"1mnyGVTdwCi4umlGMvT6y7aTmibMQuBvySoT5eZcwKti4_sfnEJhyWXfsE6tasUC8ce7YGzFiq886S9-iZc6hYW0ReIPQYCLZ5hn-fUCxefEZdX03e-uJww7OwX_kNQifgdYwzM__QJsy1_nbgEn7olTzplJfJmUSk9dkfFJ-3o5L3AJ1YMbFPgXae0OLmkxjVKfT_6093a0RLSpUCzcti4OhtNa-OEUMX4rYl0jQyzZyO281IH-MatecA1O_o6JbCI1wH46jWNY8of43cemzFwQaS2SnSyppOmJzGf9fpm5j5dozYrAdTdPYdZEA62CimvdLvJqEt4OjyQuRTAuCw\"}" + oauthJwkKeyPair: | + { + "p":"8U9gI_GHo8ca1g-6Miv9f7E7zTvY90mPZO9Hhygf3ZxhFk_TNH7y3dMyUZebnYLbXf1wUltLve-nND9AO2omtz6WgPEjPomo6nLeIO1swzkBkTqrQhvPioo7rXIAlv4O5vEPPnLFJceSVRmDSGovkjcyklAhZiVRfzAv-_GdnG8", + "kty":"RSA", + "q":"43dqCXHtiIYJ2bvVvVW8Ch4yhJmPA9VUQSTN2aVlLZoqMOLh8rn6fl0UiBLCQJdoPI3Hc2QuS6_GohVrKI-WgpHEGMssZnH9cbfGuNUX7QK6glhsGSPoxSRpFJMgVDxG5jCgUSOt0BJdY_hgo7wwO_bx2VTdSJPgIt761TUNjiU", + "d":"ixZk7xvHUYzY8Eu0NwAF5LoGG7xJSqSuvy7lg4Ag8Pb8imlvwvyo1G2aYpNXGWi9lTv8h_tnVaSBfb7KddS9KLpoC-EuBk6tn8EUyeevKdRD6c-WLZX6QehET_B_LWL_EQQRB2cqfiThkDghN5HWZn18-QskyYgS0vWS_EYquM8LS3s0emQhXkz77ZX4fQrhZGRCAmDeI-iZDt90uRTF6OceKKoC5eTimx9YimQ0Z-qOlKiCpIjplSgHL8QgIZrp6mLdCTVy3vxaegwSNKwGqCCwcewtdBky9nb9pX6sEc6CA3WfyYZMbthMYIYTi0KJ2kaaF2QdR3l7VLTdPBwK-Q", + "e":"AQAB", + "use":"sig", + "kid":"QVapB_JRK66AMCYKN7LHsl4DlkICTNYB6ExJrtmva88", + "qi":"sYHbPsyiiRIBKbN-chbXYTLsd03jL_kiT2VdyMsP_tNjOz87WcC8Td-lBUIViw_aMq1VOjJEyhB6yE9-Po4YmlAFbPSf1rmypAdjTKRQznkuTGDue6yd4z1t4SgZJOSlpJSGBtkEiYaIlI9-fviPxtIpDh0JfAbE7XgPItu5vT8", + "dp":"LFmnVNPWVx6H6yHW2dPF8osTb6P5O3HUnftwTQHt4WAVY_Fl4vcWEfj3_ZD8s9VSFhM3apuG7zC1rV-WoZTf6rvhE2--R4creXOFKc4ZOEQo6pU61pqf6VmAoF8chqPI_178q0CSxV_JoERhIZUKizgD3mpFyS1ArjygBREiI5c", + "alg":"RS256", + "dq":"UXTY7yryQhql-mfugc_q98CanKuU8G17r5xrbw5wriTwCPOmsFJzZr8UdCorkA_oTw8CpsrwXUBEJJUA-9R3tKhYjfxsGP-nIAsMyfHdkI2SlsztYQ9f8wE39Bs_z4qKZTJyprvdKY76DKvMh7YDm-Wx8_8VA0GZWUN6ldFa2lU", + "n":"1mnyGVTdwCi4umlGMvT6y7aTmibMQuBvySoT5eZcwKti4_sfnEJhyWXfsE6tasUC8ce7YGzFiq886S9-iZc6hYW0ReIPQYCLZ5hn-fUCxefEZdX03e-uJww7OwX_kNQifgdYwzM__QJsy1_nbgEn7olTzplJfJmUSk9dkfFJ-3o5L3AJ1YMbFPgXae0OLmkxjVKfT_6093a0RLSpUCzcti4OhtNa-OEUMX4rYl0jQyzZyO281IH-MatecA1O_o6JbCI1wH46jWNY8of43cemzFwQaS2SnSyppOmJzGf9fpm5j5dozYrAdTdPYdZEA62CimvdLvJqEt4OjyQuRTAuCw" + } tests: enableFederationTests: true cannon: diff --git a/services/brig/src/Brig/Effects/Jwk.hs b/services/brig/src/Brig/Effects/Jwk.hs index 86dfa6c3d4..fc6581c60f 100644 --- a/services/brig/src/Brig/Effects/Jwk.hs +++ b/services/brig/src/Brig/Effects/Jwk.hs @@ -7,7 +7,6 @@ import Crypto.JOSE.JWK import Data.Aeson import qualified Data.ByteString as BS import Data.String.Conversions (cs) -import Debug.Trace (traceM) import Imports import Polysemy @@ -20,13 +19,4 @@ interpretJwk :: Members '[Embed IO] r => Sem (Jwk ': r) a -> Sem r a interpretJwk = interpret $ \(Get fp) -> liftIO $ readJwk fp readJwk :: FilePath -> IO (Maybe JWK) -readJwk fp = do - -- try @IOException (BS.readFile fp) <&> either (const Nothing) (decode . cs) - bsOrError <- try @IOException $ BS.readFile fp - case bsOrError of - Left err -> traceM ("Failed to read file because: " <> show err) $> Nothing - Right bs -> do - traceM $ "File contents:\n" <> show bs - case eitherDecode (cs bs) of - Left err -> traceM ("Failed to decode file because: " <> show err) $> Nothing - Right jwk -> pure $ Just jwk +readJwk fp = try @IOException (BS.readFile fp) <&> either (const Nothing) (decode . cs) From cc931be84404fdc8120d9fd0d2edc06e488c2871 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 14 Dec 2022 13:06:41 +0000 Subject: [PATCH 25/27] fix integration test --- services/brig/test/integration/API/OAuth.hs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index c70473e085..07764518b1 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -28,6 +28,7 @@ import Crypto.JOSE (JWK) import Crypto.JWT (Audience (Audience), NumericDate (NumericDate), claimAud, claimExp, claimIat, claimIss, claimSub, stringOrUri) import qualified Data.Aeson as A import Data.ByteString.Conversion (fromByteString, fromByteString', toByteString') +import Data.Domain (domainText) import Data.Id (OAuthClientId, UserId, idToText, randomId) import Data.Range (unsafeRange) import Data.Set as Set @@ -131,13 +132,14 @@ testCreateAccessTokenSuccess opts brig = do k <- liftIO $ readJwk (fromMaybe "" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") verifiedOrError <- liftIO $ verify k (cs $ unOauthAccessToken $ oatAccessToken accessToken) verifiedOrErrorWithWrongKey <- liftIO $ verify wrongKey (cs $ unOauthAccessToken $ oatAccessToken accessToken) + let expectedDomain = domainText $ Opt.setFederationDomain $ Opt.optSettings opts liftIO $ do isRight verifiedOrError @?= True isLeft verifiedOrErrorWithWrongKey @?= True let claims = either (error "invalid token") id verifiedOrError scope claims @?= scopes - (view claimIss $ claims) @?= ("example.com" ^? stringOrUri @Text) - (view claimAud $ claims) @?= (Audience . (: []) <$> "example.com" ^? stringOrUri @Text) + (view claimIss $ claims) @?= (expectedDomain ^? stringOrUri @Text) + (view claimAud $ claims) @?= (Audience . (: []) <$> expectedDomain ^? stringOrUri @Text) (view claimSub $ claims) @?= (idToText uid ^? stringOrUri) let expTime = (\(NumericDate x) -> x) . fromMaybe (error "exp claim missing") . view claimExp $ claims diffUTCTime expTime now > 0 @?= True From 6322e113e411bb5b85f8ca090c13bc7764b077ed Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 23 Dec 2022 11:09:14 +0000 Subject: [PATCH 26/27] fix makefile --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index f035d080cc..067ec68296 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,6 @@ endif .PHONY: clean clean: cabal clean - $(MAKE) -C services/nginz clean -rm -rf dist .PHONY: clean-hint From 3f0015244a742c5b09b02ed1162eff40d43fa32a Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 23 Dec 2022 11:10:14 +0000 Subject: [PATCH 27/27] fix haskell-pins.nix file --- nix/haskell-pins.nix | 2 -- 1 file changed, 2 deletions(-) diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index b10d85c249..93f2aea5d1 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -241,8 +241,6 @@ let sha256 = "sha256-SKEE9ZqhjBxHYUKQaoB4IpN4/Ui3tS4S98FgZqj7WlY="; }; }; - }; - hackagePins = { kind-generics = { src = fetchgit { url = "https://gitlab.com/trupill/kind-generics.git";