diff --git a/cassandra-schema.cql b/cassandra-schema.cql index b6a72d7ea3..b997068703 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -1508,10 +1508,11 @@ CREATE TABLE brig_test.prekeys ( CREATE TABLE brig_test.oauth_auth_code ( code ascii PRIMARY KEY, client uuid, + code_challenge blob, redirect_uri blob, scope set, user uuid -) WITH bloom_filter_fp_chance = 0.1 +) WITH bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} @@ -1648,7 +1649,7 @@ CREATE TABLE brig_test.password_reset ( retries int, timeout timestamp, user uuid -) WITH bloom_filter_fp_chance = 0.01 +) WITH bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} diff --git a/changelog.d/2-features/pr-3165 b/changelog.d/2-features/pr-3165 new file mode 100644 index 0000000000..c455e13b5c --- /dev/null +++ b/changelog.d/2-features/pr-3165 @@ -0,0 +1 @@ +Authorization Code Flow with PKCE support diff --git a/docs/src/developer/reference/oauth.md b/docs/src/developer/reference/oauth.md index 25e459595b..2babf4a766 100644 --- a/docs/src/developer/reference/oauth.md +++ b/docs/src/developer/reference/oauth.md @@ -38,7 +38,7 @@ The authorization server does the authentication of the user and establishes whe ### Supported OAuth flow -`wire-server` currently only supports the [authorization code flow](https://www.rfc-editor.org/rfc/rfc6749#section-4.1) which is optimized for confidential clients such as Outlook Calendar Extension. +`wire-server` currently only supports the [Authorization Code Flow with Proof Key for Code Exchange (PKCE)](https://www.rfc-editor.org/rfc/rfc7636) which is optimized for public clients such as Outlook Calendar Extension. ```{image} oauth.svg ``` @@ -76,7 +76,7 @@ Client credentials will be generated and returned by wire-server: These credentials have to be stored in a safe place and cannot be recovered if they are lost. -### Get authorization code +### Authorization request When the user wants to use the 3rd party app for the first time, they need to authorize it to access Wire resources on their behalf. @@ -86,6 +86,8 @@ If the user is already logged in the authentication will be skipped and they are On the consent page, the user is asked to authorize the client's access request. They can either grant or deny the request and the corresponding scope, a list of permissions to give to the 3rd party app, (4. in diagram above). +The client needs to create a unique `code_verifier` as described in [RFC 7636 section 4.1](https://www.rfc-editor.org/rfc/rfc7636#section-4.1) and send a `code_challenge`, which is the unpadded base64url-encoded SHA256 hash of the code verifier as described in [RFC 7636 section 4.2](https://www.rfc-editor.org/rfc/rfc7636#section-4.2). The `code_challenge` must be included in the request. The `S256` code challenge method is mandatory. The `code_verifier` must not be included in the request. + Example request: ```http @@ -94,18 +96,22 @@ GET /authorize? response_type=code& client_id=b9e65569-aa61-462d-915d-94c8d6ef17a7& redirect_uri=https%3A%2F%2Fclient.example.com& - state=foobar HTTP/1.1 + state=foobar& + code_challenge=qVrqDTN8ivyWEEw6wyfUc3bwhCA2RE4V2fbiC4mC7ofqAF4t& + code_challenge_method=S256 HTTP/1.1 ``` Url encoded query parameters: -| Parameter | Description | -| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `scope` | Required. The scope of the access request. | -| `response_type` | Required. Value MUST be set to `code`. | -| `client_id` | Required. The client identifier. | -| `redirect_url` | Required. MUST match the URL that was provided during client registration | -| `state` | Required. An opaque value used by the client to maintain state between the request and callback.
The authorization server includes this value when redirecting the user-agent back to the client.
The parameter is used for preventing cross-site request forgery. | +| Parameter | Description | +| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `scope` | Required. The scope of the access request. | +| `response_type` | Required. Value MUST be set to `code`. | +| `client_id` | Required. The client identifier. | +| `redirect_url` | Required. MUST match the URL that was provided during client registration | +| `state` | Required. An opaque value used by the client to maintain state between the request and callback.
The authorization server includes this value when redirecting the user-agent back to the client.
The parameter is used for preventing cross-site request forgery. | +| `code_challenge` | Required. Generated by the client from the `code_verifier` | +| `code_challenge_method` | Required. It MUST be set to `S256` | Once the user consents, the browser will be redirected back to the 3rd party app, using the redirect URI provided during client registration, with an authorization code and the state value as query parameters (5. in diagram above). The authorization code can now be used by the 3rd party app to retrieve an access token and a refresh token and is good for one use. @@ -127,7 +133,7 @@ The 3rd party app sends the authorization code together with the client credenti ```shell curl -s -X POST server.example.com/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ - -d 'code=1395a1a44b72e0b81ec8fe6c791d2d3f22bc1c4df96857a88c3e2914bb687b7b&client_id=b9e65569-aa61-462d-915d-94c8d6ef17a7&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fclient.example.com&client_secret=3f6fbd62835859b2bac411b2a2a2a54699ec56504ee32099748de3a762d41a2d' + -d 'code=1395a1a44b72e0b81ec8fe6c791d2d3f22bc1c4df96857a88c3e2914bb687b7b&client_id=b9e65569-aa61-462d-915d-94c8d6ef17a7&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fclient.example.com&code_verifier=2dae11ce5e162e2c01180ae4f8b55103b8297408b8aab12f99f63df3c2415234' ``` Parameters: @@ -138,7 +144,7 @@ Parameters: | `client_id` | Required. The client identifier. | | `grant_type` | Required. Value MUST be set to `authorization_code`. | | `redirect_uri` | Required. The value MUST be identical to the one provided in the authorization request | -| `client_secret` | Required. The client's secret. | +| `code_verifier` | Required. The code verifier as described above. | Example response: @@ -151,7 +157,7 @@ Example response: } ``` -The expiration time the response (`expires_in`) refers to the expiration time of the access token. +The expiration time in the response (`expires_in`) refers to the expiration time of the access token. ### Accessing a resource @@ -205,8 +211,8 @@ curl -i -s -X POST localhost:8080/oauth/revoke \ Parameters: -| Parameter | Description | -| -------------- | ------------------------------------------------- | +| Parameter | Description | +| --------------- | ------------------------------------------------- | | `client_id` | Required. The client identifier. | | `refresh_token` | Required. The refresh token issued to the client. | | `client_secret` | Required. The client's secret. | diff --git a/docs/src/developer/reference/oauth.mmd b/docs/src/developer/reference/oauth.mmd new file mode 100644 index 0000000000..e1f4dfe7f2 --- /dev/null +++ b/docs/src/developer/reference/oauth.mmd @@ -0,0 +1,18 @@ +sequenceDiagram + autonumber + actor U as User + participant C as Outlook Calendar Extension + participant A as Authorization Server (wire-server) + participant R as Resource Server (wire-server) + + U->>C: Click login + C->>A: Authorization code request + code challenge /authorize + A->>U: Redirect to login/authorization prompt + U->>A: Authenticate and consent + A->>C: Authorization code + C->>A: Authorization code + code verifier + A->>A: Validate authorization code + code verifier + A->>C: Access token + C->>R: Request a resource with access token (e.g. POST /conversations) + R->>R: Validate access token with public key from auth server + R->>C: Response diff --git a/docs/src/developer/reference/oauth.svg b/docs/src/developer/reference/oauth.svg index 0597fc1d5e..ebd4f56c36 100644 --- a/docs/src/developer/reference/oauth.svg +++ b/docs/src/developer/reference/oauth.svg @@ -1 +1 @@ -UserOutlook Calendar ExtensionAuthorization Server (wire-server)Resource Server (wire-server)Click login1Authorization code request /authorize2Redirect to login/authorization prompt3Authenticate and consent4Authorization code5Authorization code + client credentials6Validate authorization code + client credentials7Access token8Request a resource with access token (e.g. POST /conversations)9Validate access token with public key from auth server10Response11UserOutlook Calendar ExtensionAuthorization Server (wire-server)Resource Server (wire-server) +UserOutlook Calendar ExtensionAuthorization Server (wire-server)Resource Server (wire-server)Click login1Authorization code request + code challenge /authorize2Redirect to login/authorization prompt3Authenticate and consent4Authorization code5Authorization code + code verifier6Validate authorization code + code verifier7Access token8Request a resource with access token (e.g. POST /conversations)9Validate access token with public key from auth server10Response11UserOutlook Calendar ExtensionAuthorization Server (wire-server)Resource Server (wire-server) diff --git a/libs/types-common/src/Data/Text/Ascii.hs b/libs/types-common/src/Data/Text/Ascii.hs index 428d7dc12b..1b602acdb2 100644 --- a/libs/types-common/src/Data/Text/Ascii.hs +++ b/libs/types-common/src/Data/Text/Ascii.hs @@ -57,6 +57,7 @@ module Data.Text.Ascii AsciiBase64Url, validateBase64Url, encodeBase64Url, + encodeBase64UrlUnpadded, decodeBase64Url, -- * Base16 (Hex) Characters @@ -310,6 +311,12 @@ validateBase64Url = validate encodeBase64Url :: ByteString -> AsciiBase64Url encodeBase64Url = unsafeFromByteString . B64Url.encode +-- | Encode a bytestring into a text containing only url-safe +-- base-64 characters. The resulting text is always a valid +-- encoding in unpadded form. +encodeBase64UrlUnpadded :: ByteString -> AsciiBase64Url +encodeBase64UrlUnpadded = unsafeFromByteString . B64Url.encodeUnpadded + -- | Decode a text containing only url-safe base-64 characters. -- Decoding only succeeds if the text is a valid encoding and -- a multiple of 4 bytes in length. diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index 12fed5439b..b1abe69bf2 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -23,11 +23,14 @@ module Wire.API.OAuth where import Cassandra hiding (Set) import Control.Lens (preview, view, (%~), (?~)) import Control.Monad.Except +import Crypto.Hash as Crypto import Crypto.JWT hiding (Context, params, uri, verify) import qualified Data.Aeson.KeyMap as M import qualified Data.Aeson.Types as A +import Data.ByteArray (convert) import Data.ByteString.Conversion import Data.ByteString.Lazy (toStrict) +import Data.Either.Combinators (mapLeft) import qualified Data.HashMap.Strict as HM import Data.Id as Id import Data.Range @@ -243,12 +246,70 @@ instance ToSchema OAuthScopes where oauthScopeParser scope = pure $ (not . T.null) `filter` T.splitOn " " scope & maybe Set.empty Set.fromList . mapM (fromByteString' . cs) +data CodeChallengeMethod = S256 + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform CodeChallengeMethod) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema CodeChallengeMethod) + +instance ToSchema CodeChallengeMethod where + schema :: ValueSchema NamedSwaggerDoc CodeChallengeMethod + schema = + enum @Text "CodeChallengeMethod" $ + mconcat + [ element "S256" S256 + ] + +newtype OAuthCodeVerifier = OAuthCodeVerifier {unOAuthCodeVerifier :: Range 43 128 Text} + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform OAuthCodeVerifier) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema OAuthCodeVerifier) + +instance ToSchema OAuthCodeVerifier where + schema :: ValueSchema NamedSwaggerDoc OAuthCodeVerifier + schema = OAuthCodeVerifier <$> unOAuthCodeVerifier .= schema + +instance FromHttpApiData OAuthCodeVerifier where + parseQueryParam = fmap OAuthCodeVerifier . mapLeft cs . checkedEither + +instance ToHttpApiData OAuthCodeVerifier where + toQueryParam = fromRange . unOAuthCodeVerifier + +newtype OAuthCodeChallenge = OAuthCodeChallenge {unOAuthCodeChallenge :: Text} + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform OAuthCodeChallenge) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema OAuthCodeChallenge) + +instance ToSchema OAuthCodeChallenge where + schema = named "OAuthCodeChallenge" $ unOAuthCodeChallenge .= fmap OAuthCodeChallenge (unnamed schema) + +instance ToByteString OAuthCodeChallenge where + builder = builder . unOAuthCodeChallenge + +instance FromByteString OAuthCodeChallenge where + parser = OAuthCodeChallenge <$> parser + +verifyCodeChallenge :: OAuthCodeVerifier -> OAuthCodeChallenge -> Bool +verifyCodeChallenge verifier challenge = challenge == mkChallenge verifier + +mkChallenge :: OAuthCodeVerifier -> OAuthCodeChallenge +mkChallenge = + OAuthCodeChallenge + . toText + . encodeBase64UrlUnpadded + . convert + . Crypto.hash @ByteString @Crypto.SHA256 + . cs + . fromRange + . unOAuthCodeVerifier + data CreateOAuthAuthorizationCodeRequest = CreateOAuthAuthorizationCodeRequest { clientId :: OAuthClientId, scope :: OAuthScopes, responseType :: OAuthResponseType, - redirectUrl :: RedirectUrl, - state :: Text + redirectUri :: RedirectUrl, + state :: Text, + codeChallengeMethod :: CodeChallengeMethod, + codeChallenge :: OAuthCodeChallenge } deriving (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform CreateOAuthAuthorizationCodeRequest) @@ -260,15 +321,19 @@ instance ToSchema CreateOAuthAuthorizationCodeRequest where CreateOAuthAuthorizationCodeRequest <$> (.clientId) .= fieldWithDocModifier "client_id" clientIdDescription schema <*> (.scope) .= fieldWithDocModifier "scope" scopeDescription schema - <*> responseType .= fieldWithDocModifier "response_type" responseTypeDescription schema - <*> (.redirectUrl) .= fieldWithDocModifier "redirect_uri" redirectUriDescription schema - <*> state .= fieldWithDocModifier "state" stateDescription schema + <*> (.responseType) .= fieldWithDocModifier "response_type" responseTypeDescription schema + <*> (.redirectUri) .= fieldWithDocModifier "redirect_uri" redirectUriDescription schema + <*> (.state) .= fieldWithDocModifier "state" stateDescription schema + <*> (.codeChallengeMethod) .= fieldWithDocModifier "code_challenge_method" codeChallengeMethodDescription schema + <*> (.codeChallenge) .= fieldWithDocModifier "code_challenge" codeChallengeDescription schema where clientIdDescription = description ?~ "The ID of the OAuth client" scopeDescription = description ?~ "The scopes which are requested to get authorization for, separated by a space" responseTypeDescription = description ?~ "Indicates which authorization flow to use. Use `code` for authorization code flow." redirectUriDescription = description ?~ "The URL to which to redirect the browser after authorization has been granted by the user." stateDescription = description ?~ "An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery" + codeChallengeMethodDescription = description ?~ "The method used to encode the code challenge. Only `S256` is supported." + codeChallengeDescription = description ?~ "Generated by the client from the code verifier (unpadded base64url-encoded SHA256 hash of the code verifier)" newtype OAuthAuthorizationCode = OAuthAuthorizationCode {unOAuthAuthorizationCode :: AsciiBase16} deriving (Eq, Generic, Arbitrary) @@ -326,7 +391,7 @@ instance ToHttpApiData OAuthGrantType where data OAuthAccessTokenRequest = OAuthAccessTokenRequest { grantType :: OAuthGrantType, clientId :: OAuthClientId, - clientSecret :: OAuthClientPlainTextSecret, + codeVerifier :: OAuthCodeVerifier, code :: OAuthAuthorizationCode, redirectUri :: RedirectUrl } @@ -340,22 +405,22 @@ instance ToSchema OAuthAccessTokenRequest where OAuthAccessTokenRequest <$> (.grantType) .= fieldWithDocModifier "grant_type" grantTypeDescription schema <*> (.clientId) .= fieldWithDocModifier "client_id" clientIdDescription schema - <*> (.clientSecret) .= fieldWithDocModifier "client_secret" clientSecretDescription schema - <*> code .= fieldWithDocModifier "code" codeDescription schema - <*> redirectUri .= fieldWithDocModifier "redirect_uri" redirectUriDescription schema + <*> (.codeVerifier) .= fieldWithDocModifier "code_verifier" codeVerifierDescription schema + <*> (.code) .= fieldWithDocModifier "code" codeDescription schema + <*> (.redirectUri) .= fieldWithDocModifier "redirect_uri" redirectUrlDescription schema where grantTypeDescription = description ?~ "Indicates which authorization flow to use. Use `authorization_code` for authorization code flow." clientIdDescription = description ?~ "The ID of the OAuth client" - clientSecretDescription = description ?~ "The secret of the OAuth client" + codeVerifierDescription = description ?~ "The code verifier to complete the code challenge" codeDescription = description ?~ "The authorization code" - redirectUriDescription = description ?~ "The URL must match the URL that was used to generate the authorization code." + redirectUrlDescription = description ?~ "The URL must match the URL that was used to generate the authorization code." instance FromForm OAuthAccessTokenRequest where fromForm f = OAuthAccessTokenRequest <$> parseUnique "grant_type" f <*> parseUnique "client_id" f - <*> parseUnique "client_secret" f + <*> parseUnique "code_verifier" f <*> parseUnique "code" f <*> parseUnique "redirect_uri" f @@ -365,7 +430,7 @@ instance ToForm OAuthAccessTokenRequest where mempty & HM.insert "grant_type" [toQueryParam (req.grantType)] & HM.insert "client_id" [toQueryParam (req.clientId)] - & HM.insert "client_secret" [toQueryParam (req.clientSecret)] + & HM.insert "code_verifier" [toQueryParam (req.codeVerifier)] & HM.insert "code" [toQueryParam (req.code)] & HM.insert "redirect_uri" [toQueryParam (req.redirectUri)] @@ -486,7 +551,6 @@ data OAuthRefreshTokenInfo = OAuthRefreshTokenInfo data OAuthRefreshAccessTokenRequest = OAuthRefreshAccessTokenRequest { grantType :: OAuthGrantType, clientId :: OAuthClientId, - clientSecret :: OAuthClientPlainTextSecret, refreshToken :: OAuthRefreshToken } deriving (Eq, Show, Generic) @@ -499,12 +563,10 @@ instance ToSchema OAuthRefreshAccessTokenRequest where OAuthRefreshAccessTokenRequest <$> (.grantType) .= fieldWithDocModifier "grant_type" grantTypeDescription schema <*> (.clientId) .= fieldWithDocModifier "client_id" clientIdDescription schema - <*> (.clientSecret) .= fieldWithDocModifier "client_secret" clientSecretDescription schema <*> (.refreshToken) .= fieldWithDocModifier "refresh_token" refreshTokenDescription schema where grantTypeDescription = description ?~ "The grant type. Must be `refresh_token`" clientIdDescription = description ?~ "The OAuth client's ID" - clientSecretDescription = description ?~ "The OAuth client's secret" refreshTokenDescription = description ?~ "The refresh token" instance FromForm OAuthRefreshAccessTokenRequest where @@ -513,7 +575,6 @@ instance FromForm OAuthRefreshAccessTokenRequest where OAuthRefreshAccessTokenRequest <$> parseUnique "grant_type" f <*> parseUnique "client_id" f - <*> parseUnique "client_secret" f <*> parseUnique "refresh_token" f instance ToForm OAuthRefreshAccessTokenRequest where @@ -522,7 +583,6 @@ instance ToForm OAuthRefreshAccessTokenRequest where mempty & HM.insert "grant_type" [toQueryParam (req.grantType)] & HM.insert "client_id" [toQueryParam (req.clientId)] - & HM.insert "client_secret" [toQueryParam (req.clientSecret)] & HM.insert "refresh_token" [toQueryParam (req.refreshToken)] instance FromForm (Either OAuthAccessTokenRequest OAuthRefreshAccessTokenRequest) where @@ -536,7 +596,6 @@ instance FromForm (Either OAuthAccessTokenRequest OAuthRefreshAccessTokenRequest data OAuthRevokeRefreshTokenRequest = OAuthRevokeRefreshTokenRequest { clientId :: OAuthClientId, - clientSecret :: OAuthClientPlainTextSecret, refreshToken :: OAuthRefreshToken } deriving (Eq, Show, Generic) @@ -547,11 +606,9 @@ instance ToSchema OAuthRevokeRefreshTokenRequest where object "OAuthRevokeRefreshTokenRequest" $ OAuthRevokeRefreshTokenRequest <$> (.clientId) .= fieldWithDocModifier "client_id" clientIdDescription schema - <*> (.clientSecret) .= fieldWithDocModifier "client_secret" clientSecretDescription schema <*> (.refreshToken) .= fieldWithDocModifier "refresh_token" refreshTokenDescription schema where clientIdDescription = description ?~ "The OAuth client's ID" - clientSecretDescription = description ?~ "The OAuth client's secret" refreshTokenDescription = description ?~ "The refresh token" data OAuthApplication = OAuthApplication @@ -585,6 +642,7 @@ data OAuthError | OAuthInvalidClientCredentials | OAuthInvalidGrantType | OAuthInvalidRefreshToken + | OAuthInvalidGrant instance KnownError (MapError e) => IsSwaggerError (e :: OAuthError) where addToSwagger = addStaticErrorToSwagger @(MapError e) @@ -607,6 +665,8 @@ type instance MapError 'OAuthInvalidGrantType = 'StaticError 403 "forbidden" "In type instance MapError 'OAuthInvalidRefreshToken = 'StaticError 403 "forbidden" "Invalid refresh token" +type instance MapError 'OAuthInvalidGrant = 'StaticError 403 "invalid_grant" "Invalid grant" + -------------------------------------------------------------------------------- -- CQL instances @@ -633,3 +693,9 @@ instance Cql OAuthScope where toCql = CqlText . cs . toByteString' fromCql (CqlText t) = maybe (Left "invalid oauth scope") Right $ fromByteString' (cs t) fromCql _ = Left "OAuthScope: Text expected" + +instance Cql OAuthCodeChallenge where + ctype = Tagged BlobColumn + toCql = CqlBlob . toByteString + fromCql (CqlBlob t) = runParser parser (toStrict t) + fromCql _ = Left "OAuthCodeChallenge: Blob expected" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs index 65101229c2..2a37bd71c6 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs @@ -75,6 +75,7 @@ type OAuthAPI = :> CanThrow 'OAuthInvalidRefreshToken :> CanThrow 'OAuthInvalidGrantType :> CanThrow 'OAuthInvalidClientCredentials + :> CanThrow 'OAuthInvalidGrant :> "oauth" :> "token" :> ReqBody '[FormUrlEncoded] (Either OAuthAccessTokenRequest OAuthRefreshAccessTokenRequest) @@ -87,7 +88,6 @@ type OAuthAPI = :> CanThrow 'OAuthJwtError :> CanThrow 'OAuthInvalidRefreshToken :> CanThrow 'OAuthClientNotFound - :> CanThrow 'OAuthInvalidClientCredentials :> "oauth" :> "revoke" :> ReqBody '[JSON] OAuthRevokeRefreshTokenRequest diff --git a/libs/wire-api/test/unit/Main.hs b/libs/wire-api/test/unit/Main.hs index 3a0b5eb11a..f0e2368876 100644 --- a/libs/wire-api/test/unit/Main.hs +++ b/libs/wire-api/test/unit/Main.hs @@ -26,6 +26,7 @@ import Test.Tasty import qualified Test.Wire.API.Call.Config as Call.Config import qualified Test.Wire.API.Conversation as Conversation import qualified Test.Wire.API.MLS as MLS +import qualified Test.Wire.API.OAuth as OAuth import qualified Test.Wire.API.RawJson as RawJson import qualified Test.Wire.API.Roundtrip.Aeson as Roundtrip.Aeson import qualified Test.Wire.API.Roundtrip.ByteString as Roundtrip.ByteString @@ -66,5 +67,6 @@ main = MLS.tests, Routes.Version.tests, unsafePerformIO Routes.Version.Wai.tests, - RawJson.tests + RawJson.tests, + OAuth.tests ] diff --git a/libs/wire-api/test/unit/Test/Wire/API/OAuth.hs b/libs/wire-api/test/unit/Test/Wire/API/OAuth.hs new file mode 100644 index 0000000000..a7775f8af1 --- /dev/null +++ b/libs/wire-api/test/unit/Test/Wire/API/OAuth.hs @@ -0,0 +1,44 @@ +{-# LANGUAGE ScopedTypeVariables #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.OAuth where + +import Data.Aeson +import Imports +import Test.Tasty +import Test.Tasty.HUnit +import Wire.API.OAuth + +tests :: TestTree +tests = + testGroup "Oauth" $ + [ testGroup "code challenge verification should succeed" $ + [ testCase "should" testCodeChallengeVerification + ] + ] + +testCodeChallengeVerification :: Assertion +testCodeChallengeVerification = do + mkChallenge codeVerifier @?= codeChallenge + where + codeChallenge :: OAuthCodeChallenge + codeChallenge = either (\e -> error $ "invalid code challenge " <> show e) id $ eitherDecode "\"G7CWLBqYDT8doT_oEIN3un_QwZWYKHmOqG91nwNzITc\"" + + codeVerifier :: OAuthCodeVerifier + codeVerifier = either (\e -> error $ "invalid code verifier " <> show e) id $ eitherDecode "\"nE3k3zykOmYki~kriKzAmeFiGT7cWugcuToFwo1YPgrZ1cFvaQqLa.dXY9MnDj3umAmG-8lSNIYIl31Cs_.fV5r2psa4WWZcB.Nlc3A-t3p67NDZaOJjIiH~8PvUH_hR\"" diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index a6da3bc148..47ed51063e 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -656,6 +656,7 @@ test-suite wire-api-tests Test.Wire.API.Call.Config Test.Wire.API.Conversation Test.Wire.API.MLS + Test.Wire.API.OAuth Test.Wire.API.RawJson Test.Wire.API.Roundtrip.Aeson Test.Wire.API.Roundtrip.ByteString diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 34babd7b7f..9fcb28c673 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -657,6 +657,7 @@ executable brig-schema V72_AddNonceTable V73_ReplaceNonceTable V74_AddOAuthTables + V75_AddOAuthCodeChallenge V_FUTUREWORK hs-source-dirs: schema/src diff --git a/services/brig/schema/src/Main.hs b/services/brig/schema/src/Main.hs index 10ff490894..f1f35ccd37 100644 --- a/services/brig/schema/src/Main.hs +++ b/services/brig/schema/src/Main.hs @@ -54,6 +54,7 @@ import qualified V71_AddTableVCodesThrottle import qualified V72_AddNonceTable import qualified V73_ReplaceNonceTable import qualified V74_AddOAuthTables +import qualified V75_AddOAuthCodeChallenge main :: IO () main = do @@ -95,7 +96,8 @@ main = do V71_AddTableVCodesThrottle.migration, V72_AddNonceTable.migration, V73_ReplaceNonceTable.migration, - V74_AddOAuthTables.migration + V74_AddOAuthTables.migration, + V75_AddOAuthCodeChallenge.migration -- When adding migrations here, don't forget to update -- 'schemaVersion' in Brig.App diff --git a/services/brig/schema/src/V75_AddOAuthCodeChallenge.hs b/services/brig/schema/src/V75_AddOAuthCodeChallenge.hs new file mode 100644 index 0000000000..ebead11e8c --- /dev/null +++ b/services/brig/schema/src/V75_AddOAuthCodeChallenge.hs @@ -0,0 +1,36 @@ +{-# LANGUAGE QuasiQuotes #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module V75_AddOAuthCodeChallenge + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 75 "Add PKCE code_challenge to oauth_auth_code table" $ + schema' + [r| ALTER TABLE oauth_auth_code ADD ( + code_challenge blob + ) + |] diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index 380aecc383..090ec00e7a 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DuplicateRecordFields #-} -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -28,7 +29,7 @@ import Brig.API.Error (throwStd) import Brig.API.Handler (Handler) import Brig.App import qualified Brig.Options as Opt -import Brig.Password (Password, mkSafePassword, verifyPassword) +import Brig.Password (Password, mkSafePassword) import Cassandra hiding (Set) import qualified Cassandra as C import Control.Error (assertMay, failWith, failWithM) @@ -107,13 +108,13 @@ createNewOAuthAuthorizationCode :: UserId -> CreateOAuthAuthorizationCodeRequest createNewOAuthAuthorizationCode uid code = do runExceptT (validateAndCreateAuthorizationCode uid code) >>= \case Right oauthCode -> - pure $ CreateOAuthCodeSuccess $ code.redirectUrl & addParams [("code", toByteString' oauthCode), ("state", cs code.state)] + pure $ CreateOAuthCodeSuccess $ code.redirectUri & addParams [("code", toByteString' oauthCode), ("state", cs code.state)] Left CreateNewOAuthCodeErrorFeatureDisabled -> - pure $ CreateOAuthCodeFeatureDisabled $ code.redirectUrl & addParams [("error", "access_denied"), ("error_description", "OAuth is not enabled"), ("state", cs code.state)] + pure $ CreateOAuthCodeFeatureDisabled $ code.redirectUri & addParams [("error", "access_denied"), ("error_description", "OAuth is not enabled"), ("state", cs code.state)] Left CreateNewOAuthCodeErrorClientNotFound -> - pure $ CreateOAuthCodeClientNotFound $ code.redirectUrl & addParams [("error", "access_denied"), ("error_description", "The client ID was not found"), ("state", cs code.state)] + pure $ CreateOAuthCodeClientNotFound $ code.redirectUri & addParams [("error", "access_denied"), ("error_description", "The client ID was not found"), ("state", cs code.state)] Left CreateNewOAuthCodeErrorUnsupportedResponseType -> - pure $ CreateOAuthCodeUnsupportedResponseType $ code.redirectUrl & addParams [("error", "access_denied"), ("error_description", "The client ID was not found"), ("state", cs code.state)] + pure $ CreateOAuthCodeUnsupportedResponseType $ code.redirectUri & addParams [("error", "access_denied"), ("error_description", "The client ID was not found"), ("state", cs code.state)] Left CreateNewOAuthCodeErrorRedirectUrlMissMatch -> pure CreateOAuthCodeRedirectUrlMissMatch @@ -124,7 +125,7 @@ data CreateNewOAuthCodeError | CreateNewOAuthCodeErrorRedirectUrlMissMatch validateAndCreateAuthorizationCode :: UserId -> CreateOAuthAuthorizationCodeRequest -> ExceptT CreateNewOAuthCodeError (Handler r) OAuthAuthorizationCode -validateAndCreateAuthorizationCode uid (CreateOAuthAuthorizationCodeRequest cid scope responseType redirectUrl _) = do +validateAndCreateAuthorizationCode uid (CreateOAuthAuthorizationCodeRequest cid scope responseType redirectUrl _state _ chal) = do failWithM CreateNewOAuthCodeErrorFeatureDisabled (assertMay . Opt.setOAuthEnabled <$> view settings) failWith CreateNewOAuthCodeErrorUnsupportedResponseType (assertMay $ responseType == OAuthResponseTypeCode) client <- failWithM CreateNewOAuthCodeErrorClientNotFound $ getOAuthClient uid cid @@ -135,7 +136,7 @@ validateAndCreateAuthorizationCode uid (CreateOAuthAuthorizationCodeRequest cid mkAuthorizationCode = do oauthCode <- OAuthAuthorizationCode <$> rand32Bytes ttl <- Opt.setOAuthAuthorizationCodeExpirationTimeSecs <$> view settings - lift $ wrapClient $ insertOAuthAuthorizationCode ttl oauthCode cid uid scope redirectUrl + lift $ wrapClient $ insertOAuthAuthorizationCode ttl oauthCode cid uid scope redirectUrl chal pure oauthCode -------------------------------------------------------------------------------- @@ -153,10 +154,7 @@ createAccessTokenWithRefreshToken req = do key <- signingKey (OAuthRefreshTokenInfo _ cid uid scope _) <- lookupVerifyAndDeleteToken key req.refreshToken void $ getOAuthClient uid cid >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure - unless (cid == req.clientId) $ throwStd $ errorToWai @'OAuthInvalidClientCredentials - unlessM (verifyClientSecret req.clientSecret cid) $ throwStd $ errorToWai @'OAuthInvalidClientCredentials - createAccessToken key uid cid scope lookupVerifyAndDeleteToken :: JWK -> OAuthRefreshToken -> (Handler r) OAuthRefreshTokenInfo @@ -175,14 +173,14 @@ verifyRefreshToken key rt = do createAccessTokenWithAuthorizationCode :: (Member Now r, Member Jwk r) => OAuthAccessTokenRequest -> (Handler r) OAuthAccessTokenResponse createAccessTokenWithAuthorizationCode req = do unless (req.grantType == OAuthGrantTypeAuthorizationCode) $ throwStd $ errorToWai @'OAuthInvalidGrantType - (cid, uid, scope, uri) <- + (cid, uid, scope, uri, mChal) <- lift (wrapClient $ lookupAndDeleteByOAuthAuthorizationCode req.code) >>= maybe (throwStd $ errorToWai @'OAuthAuthorizationCodeNotFound) pure oauthClient <- getOAuthClient uid req.clientId >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure unless (uri == req.redirectUri) $ throwStd $ errorToWai @'OAuthRedirectUrlMissMatch unless (oauthClient.redirectUrl == req.redirectUri) $ throwStd $ errorToWai @'OAuthRedirectUrlMissMatch - unlessM (verifyClientSecret req.clientSecret oauthClient.clientId) $ throwStd $ errorToWai @'OAuthInvalidClientCredentials + unless (maybe False (verifyCodeChallenge req.codeVerifier) mChal) $ throwStd $ errorToWai @'OAuthInvalidGrant key <- signingKey createAccessToken key uid cid scope @@ -253,17 +251,6 @@ createAccessToken key uid cid scope = do algo <- bestJWSAlg key signClaims key (newJWSHeader ((), algo)) claims -verifyClientSecret :: OAuthClientPlainTextSecret -> OAuthClientId -> (Handler r) Bool -verifyClientSecret secret cid = - case plainTextPassword6 $ toText $ unOAuthClientPlainTextSecret secret of - Nothing -> pure False - Just plainTextPw -> - lift $ - wrapClient $ - lookupOAuthClientSecret cid <&> \case - Nothing -> False - Just pw -> verifyPassword plainTextPw pw - -------------------------------------------------------------------------------- revokeRefreshToken :: Member Jwk r => OAuthRevokeRefreshTokenRequest -> (Handler r) () @@ -271,7 +258,6 @@ revokeRefreshToken req = do key <- signingKey info <- lookupAndVerifyToken key req.refreshToken void $ getOAuthClient info.userId info.clientId >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure - unlessM (verifyClientSecret req.clientSecret info.clientId) $ throwStd $ errorToWai @'OAuthInvalidClientCredentials lift $ wrapClient $ deleteOAuthRefreshToken info lookupAndVerifyToken :: JWK -> OAuthRefreshToken -> (Handler r) OAuthRefreshTokenInfo @@ -314,31 +300,24 @@ lookupOauthClient cid = do q :: PrepQuery R (Identity OAuthClientId) (OAuthApplicationName, RedirectUrl) q = "SELECT name, redirect_uri FROM oauth_client WHERE id = ?" -lookupOAuthClientSecret :: (MonadClient 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 = ?" - -insertOAuthAuthorizationCode :: (MonadClient m) => Word64 -> OAuthAuthorizationCode -> OAuthClientId -> UserId -> OAuthScopes -> RedirectUrl -> m () -insertOAuthAuthorizationCode ttl code cid uid scope uri = do +insertOAuthAuthorizationCode :: (MonadClient m) => Word64 -> OAuthAuthorizationCode -> OAuthClientId -> UserId -> OAuthScopes -> RedirectUrl -> OAuthCodeChallenge -> m () +insertOAuthAuthorizationCode ttl code cid uid scope uri chal = do let cqlScope = C.Set (Set.toList (unOAuthScopes scope)) - retry x5 . write q $ params LocalQuorum (code, cid, uid, cqlScope, uri) + retry x5 . write q $ params LocalQuorum (code, cid, uid, cqlScope, uri, chal, fromIntegral ttl) where - q :: PrepQuery W (OAuthAuthorizationCode, OAuthClientId, UserId, C.Set OAuthScope, RedirectUrl) () - q = fromString $ "INSERT INTO oauth_auth_code (code, client, user, scope, redirect_uri) VALUES (?, ?, ?, ?, ?) USING TTL " <> show ttl + q :: PrepQuery W (OAuthAuthorizationCode, OAuthClientId, UserId, C.Set OAuthScope, RedirectUrl, OAuthCodeChallenge, Int32) () + q = fromString $ "INSERT INTO oauth_auth_code (code, client, user, scope, redirect_uri, code_challenge) VALUES (?, ?, ?, ?, ?, ?) USING TTL ?" -lookupAndDeleteByOAuthAuthorizationCode :: (MonadClient m) => OAuthAuthorizationCode -> m (Maybe (OAuthClientId, UserId, OAuthScopes, RedirectUrl)) +lookupAndDeleteByOAuthAuthorizationCode :: (MonadClient m) => OAuthAuthorizationCode -> m (Maybe (OAuthClientId, UserId, OAuthScopes, RedirectUrl, Maybe OAuthCodeChallenge)) lookupAndDeleteByOAuthAuthorizationCode code = lookupOAuthAuthorizationCode <* deleteOAuthAuthorizationCode where - lookupOAuthAuthorizationCode :: (MonadClient m) => m (Maybe (OAuthClientId, UserId, OAuthScopes, RedirectUrl)) + lookupOAuthAuthorizationCode :: (MonadClient m) => m (Maybe (OAuthClientId, UserId, OAuthScopes, RedirectUrl, Maybe OAuthCodeChallenge)) lookupOAuthAuthorizationCode = 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) + pure $ mTuple <&> \(cid, uid, C.Set scope, uri, mChal) -> (cid, uid, OAuthScopes (Set.fromList scope), uri, mChal) where - q :: PrepQuery R (Identity OAuthAuthorizationCode) (OAuthClientId, UserId, C.Set OAuthScope, RedirectUrl) - q = "SELECT client, user, scope, redirect_uri FROM oauth_auth_code WHERE code = ?" + q :: PrepQuery R (Identity OAuthAuthorizationCode) (OAuthClientId, UserId, C.Set OAuthScope, RedirectUrl, Maybe OAuthCodeChallenge) + q = "SELECT client, user, scope, redirect_uri, code_challenge FROM oauth_auth_code WHERE code = ?" deleteOAuthAuthorizationCode :: (MonadClient m) => m () deleteOAuthAuthorizationCode = retry x5 . write q $ params LocalQuorum (Identity code) @@ -348,17 +327,17 @@ lookupAndDeleteByOAuthAuthorizationCode code = lookupOAuthAuthorizationCode <* d insertOAuthRefreshToken :: (MonadClient m) => Word32 -> Word64 -> OAuthRefreshTokenInfo -> m () insertOAuthRefreshToken maxActiveTokens ttl info = do - let rid = refreshTokenId info + let rid = info.refreshTokenId oldTokes <- determineOldestTokensToBeDeleted <$> lookupOAuthRefreshTokens info.userId for_ oldTokes deleteOAuthRefreshToken - retry x5 . write qInsertId $ params LocalQuorum (info.userId, rid) - retry x5 . write qInsertInfo $ params LocalQuorum (rid, info.clientId, info.userId, C.Set (Set.toList (unOAuthScopes (scopes info))), info.createdAt) + retry x5 . write qInsertId $ params LocalQuorum (info.userId, rid, fromIntegral ttl) + retry x5 . write qInsertInfo $ params LocalQuorum (rid, info.clientId, info.userId, C.Set (Set.toList (unOAuthScopes info.scopes)), info.createdAt, fromIntegral ttl) where - qInsertInfo :: PrepQuery W (OAuthRefreshTokenId, OAuthClientId, UserId, C.Set OAuthScope, UTCTime) () - qInsertInfo = fromString $ "INSERT INTO oauth_refresh_token (id, client, user, scope, created_at) VALUES (?, ?, ?, ?, ?) USING TTL " <> show ttl + qInsertInfo :: PrepQuery W (OAuthRefreshTokenId, OAuthClientId, UserId, C.Set OAuthScope, UTCTime, Int32) () + qInsertInfo = fromString $ "INSERT INTO oauth_refresh_token (id, client, user, scope, created_at) VALUES (?, ?, ?, ?, ?) USING TTL ?" - qInsertId :: PrepQuery W (UserId, OAuthRefreshTokenId) () - qInsertId = fromString $ "INSERT INTO oauth_user_refresh_token (user, token_id) VALUES (?, ?) USING TTL " <> show ttl + qInsertId :: PrepQuery W (UserId, OAuthRefreshTokenId, Int32) () + qInsertId = fromString $ "INSERT INTO oauth_user_refresh_token (user, token_id) VALUES (?, ?) USING TTL ?" determineOldestTokensToBeDeleted :: [OAuthRefreshTokenInfo] -> [OAuthRefreshTokenInfo] determineOldestTokensToBeDeleted tokens = @@ -384,8 +363,8 @@ lookupOAuthRefreshTokenInfo rid = do deleteOAuthRefreshToken :: (MonadClient m) => OAuthRefreshTokenInfo -> m () deleteOAuthRefreshToken info = do - let rid = refreshTokenId info - retry x5 . write qDeleteId $ params LocalQuorum (userId info, rid) + let rid = info.refreshTokenId + retry x5 . write qDeleteId $ params LocalQuorum (info.userId, rid) retry x5 . write qDeleteInfo $ params LocalQuorum (Identity rid) where qDeleteId :: PrepQuery W (UserId, OAuthRefreshTokenId) () diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index bffe40c61f..cb24915bd4 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -150,7 +150,7 @@ import Wire.API.User.Identity (Email) import Wire.API.User.Profile (Locale) schemaVersion :: Int32 -schemaVersion = 74 +schemaVersion = 75 ------------------------------------------------------------------------------- -- Environment diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index d07684a42b..1a08b525ee 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -79,11 +79,12 @@ tests m db b n o = do "create access token" [ test m "success" $ testCreateAccessTokenSuccess o b, test m "wrong client id fail" $ testCreateAccessTokenWrongClientId b, - test m "wrong client secret fail" $ testCreateAccessTokenWrongClientSecret b, test m "wrong code fail" $ testCreateAccessTokenWrongAuthorizationCode b, test m "wrong redirect url fail" $ testCreateAccessTokenWrongUrl b, test m "expired code fail" $ testCreateAccessTokenExpiredCode o b, - test m "wrong grant type fail" $ testCreateAccessTokenWrongGrantType b + test m "wrong grant type fail" $ testCreateAccessTokenWrongGrantType b, + test m "wrong code challenge fail" $ testCreateAccessTokenWrongCodeChallenge b, + test m "wrong code verifier fail" $ testCreateAccessTokenWrongCodeVerifier b ], testGroup "access denied when disabled" @@ -116,7 +117,6 @@ tests m db b n o = do test m "no token id - fail" $ testRefreshTokenNoTokenId o b, test m "non-existing id - fail" $ testRefreshTokenNonExistingId o b, test m "wrong client id - fail" $ testRefreshTokenWrongClientId b, - test m "wrong client secret - fail" $ testRefreshTokenWrongClientSecret b, test m "wrong grant type - fail" $ testRefreshTokenWrongGrantType b, test m "expired token - fail" $ testRefreshTokenExpiredToken o b, test m "revoked token - fail" $ testRefreshTokenRevokedToken b @@ -145,7 +145,8 @@ testCreateOAuthCodeSuccess brig = do uid <- randomId let scope = OAuthScopes $ Set.fromList [WriteConversations, WriteConversationsCode] state <- UUID.toText <$> liftIO nextRandom - createOAuthCode brig uid (CreateOAuthAuthorizationCodeRequest c.clientId scope OAuthResponseTypeCode redirectUrl state) !!! do + + createOAuthCode brig uid (CreateOAuthAuthorizationCodeRequest c.clientId scope OAuthResponseTypeCode redirectUrl state S256 challenge) !!! do const 201 === statusCode const (Just $ unRedirectUrl redirectUrl ^. pathL) === (fmap getPath . getLocation) const (Just $ ["code", "state"]) === (fmap (fmap fst . getQueryParams) . getLocation) @@ -162,7 +163,7 @@ testCreateOAuthCodeRedirectUrlMismatch brig = do uid <- randomId state <- UUID.toText <$> liftIO nextRandom let differentUrl = mkUrl "https://wire.com" - createOAuthCode brig uid (CreateOAuthAuthorizationCodeRequest c.clientId mempty OAuthResponseTypeCode differentUrl state) !!! do + createOAuthCode brig uid (CreateOAuthAuthorizationCodeRequest c.clientId mempty OAuthResponseTypeCode differentUrl state S256 challenge) !!! do const 400 === statusCode const Nothing === (fmap getPath . getLocation) const (Just "redirect-url-miss-match") === fmap Error.label . responseJsonMaybe @@ -173,7 +174,7 @@ testCreateOAuthCodeClientNotFound brig = do uid <- randomId let redirectUrl = mkUrl "https://example.com" state <- UUID.toText <$> liftIO nextRandom - createOAuthCode brig uid (CreateOAuthAuthorizationCodeRequest cid mempty OAuthResponseTypeCode redirectUrl state) !!! do + createOAuthCode brig uid (CreateOAuthAuthorizationCodeRequest cid mempty OAuthResponseTypeCode redirectUrl state S256 challenge) !!! do const 404 === statusCode const (Just $ "access_denied") === (getLocation >=> getQueryParamValue "error") const (Just $ cs state) === (getLocation >=> getQueryParamValue "state") @@ -186,8 +187,8 @@ testCreateAccessTokenSuccess opts brig = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.singleton ReadSelf - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest -- authorization code should be deleted and can only be used once createOAuthAccessToken' brig accessTokenRequest !!! do @@ -215,33 +216,21 @@ testCreateAccessTokenWrongClientId brig = do uid <- randomId let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [WriteConversations, WriteConversationsCode] - (_, secret, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl + (_, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl cid <- randomId - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl createOAuthAccessToken' brig accessTokenRequest !!! do const 404 === statusCode const (Just "not-found") === fmap Error.label . responseJsonMaybe -testCreateAccessTokenWrongClientSecret :: Brig -> Http () -testCreateAccessTokenWrongClientSecret brig = do - uid <- randomId - let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [WriteConversations, WriteConversationsCode] - (cid, _, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl - let secret = OAuthClientPlainTextSecret $ encodeBase16 "ee2316e304f5c318e4607d86748018eb9c66dc4f391c31bcccd9291d24b4c7e" - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl - createOAuthAccessToken' brig accessTokenRequest !!! do - const 403 === statusCode - const (Just "forbidden") === fmap Error.label . responseJsonMaybe - testCreateAccessTokenWrongAuthorizationCode :: Brig -> Http () testCreateAccessTokenWrongAuthorizationCode brig = do uid <- randomId let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [WriteConversations, WriteConversationsCode] - (cid, secret, _) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl + (cid, _) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl let code = OAuthAuthorizationCode $ encodeBase16 "eb32eb9e2aa36c081c89067dddf81bce83c1c57e0b74cfb14c9f026f145f2b1f" - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl createOAuthAccessToken' brig accessTokenRequest !!! do const 404 === statusCode const (Just "not-found") === fmap Error.label . responseJsonMaybe @@ -251,9 +240,9 @@ testCreateAccessTokenWrongUrl brig = do uid <- randomId let redirectUrl = mkUrl "https://wire.com" let scopes = OAuthScopes $ Set.fromList [WriteConversations, WriteConversationsCode] - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl let wrongUrl = mkUrl "https://example.com" - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code wrongUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code wrongUrl createOAuthAccessToken' brig accessTokenRequest !!! do const 400 === statusCode const (Just "redirect-url-miss-match") === fmap Error.label . responseJsonMaybe @@ -264,9 +253,9 @@ testCreateAccessTokenExpiredCode opts brig = uid <- randomId let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [WriteConversations, WriteConversationsCode] - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl liftIO $ threadDelay (1 * 1200 * 1000) - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl createOAuthAccessToken' brig accessTokenRequest !!! do const 404 === statusCode const (Just "not-found") === fmap Error.label . responseJsonMaybe @@ -276,10 +265,38 @@ testCreateAccessTokenWrongGrantType brig = do uid <- randomId let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [WriteConversations, WriteConversationsCode] - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeRefreshToken cid secret code redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeRefreshToken cid verifier code redirectUrl createOAuthAccessToken' brig accessTokenRequest !!! assertAccessDenied +testCreateAccessTokenWrongCodeChallenge :: Brig -> Http () +testCreateAccessTokenWrongCodeChallenge brig = do + uid <- randomId + let redirectUrl = mkUrl "https://example.com" + let scopes = OAuthScopes $ Set.fromList [WriteConversations, WriteConversationsCode] + (cid, code) <- generateOAuthClientAndAuthorizationCode' wrongCodeChallenge brig uid scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl + createOAuthAccessToken' brig accessTokenRequest !!! do + const 403 === statusCode + const (Just "invalid_grant") === fmap Error.label . responseJsonMaybe + where + wrongCodeChallenge :: OAuthCodeChallenge + wrongCodeChallenge = fromMaybe (error $ "invalid code challenge") $ A.decode "\"kw8DtStRIz2MTWyG59pd9h2Kyfhoa8SM4aU8CUWM1DU\"" + +testCreateAccessTokenWrongCodeVerifier :: Brig -> Http () +testCreateAccessTokenWrongCodeVerifier brig = do + uid <- randomId + let redirectUrl = mkUrl "https://example.com" + let scopes = OAuthScopes $ Set.fromList [WriteConversations, WriteConversationsCode] + (cid, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid wrongCodeVerifier code redirectUrl + createOAuthAccessToken' brig accessTokenRequest !!! do + const 403 === statusCode + const (Just "invalid_grant") === fmap Error.label . responseJsonMaybe + where + wrongCodeVerifier :: OAuthCodeVerifier + wrongCodeVerifier = fromMaybe (error "invalid code verifier") $ A.decode "\"x9xpNj_TNfXY5h-CggZozno7ldzPmbEh8al~HJQmfiZtvvx0uxlDa~mNCZrH37XZnClD71Vx_Edx8FU1XU2mt38.o49Wnca~at75RBoxHn..F-_n5kveOSCpc_Oemyap\"" + testGetOAuthClientInfoAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () testGetOAuthClientInfoAccessDeniedWhenDisabled opts brig = withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthEnabledInternal ?~ False) $ do @@ -294,7 +311,7 @@ testCreateCodeOAuthClientAccessDeniedWhenDisabled opts brig = uid <- randomId state <- UUID.toText <$> liftIO nextRandom let redirectUrl = mkUrl "https://example.com" - createOAuthCode brig uid (CreateOAuthAuthorizationCodeRequest cid mempty OAuthResponseTypeCode redirectUrl state) !!! do + createOAuthCode brig uid (CreateOAuthAuthorizationCodeRequest cid mempty OAuthResponseTypeCode redirectUrl state S256 challenge) !!! do const 403 === statusCode const (Just $ "access_denied") === (getLocation >=> getQueryParamValue "error") const (Just $ cs state) === (getLocation >=> getQueryParamValue "state") @@ -305,10 +322,9 @@ testCreateAccessTokenAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () testCreateAccessTokenAccessDeniedWhenDisabled opts brig = withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthEnabledInternal ?~ False) $ do cid <- randomId - let secret = OAuthClientPlainTextSecret $ encodeBase16 "ee2316e304f5c318e4607d86748018eb9c66dc4f391c31bcccd9291d24b4c7e" let code = OAuthAuthorizationCode $ encodeBase16 "eb32eb9e2aa36c081c89067dddf81bce83c1c57e0b74cfb14c9f026f145f2b1f" let url = mkUrl "https://example.com" - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code url + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code url createOAuthAccessToken' brig accessTokenRequest !!! assertAccessDenied testRefreshAccessTokenAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () @@ -316,11 +332,11 @@ testRefreshAccessTokenAccessDeniedWhenDisabled opts brig = do uid <- randomId let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthEnabledInternal ?~ False) $ do - let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid secret resp.refreshToken + let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid resp.refreshToken refreshOAuthAccessToken' brig refreshAccessTokenRequest !!! assertAccessDenied testRegisterOAuthClientAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () @@ -347,10 +363,10 @@ testAccessResourceSuccessNginz brig nginz = do -- with Authorization header containing an OAuth bearer token let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl - oauthToken <- accessToken <$> createOAuthAccessToken brig accessTokenRequest - self' <- responseJsonError =<< get (nginz . paths ["self"] . authHeader oauthToken) Nginz -> Http () @@ -358,8 +374,8 @@ testAccessResourceInsufficientScope brig nginz = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [WriteConversations] - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest get (nginz . paths ["self"] . authHeader resp.accessToken) !!! do const 403 === statusCode @@ -370,8 +386,8 @@ testAccessResourceExpiredToken brig nginz = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest liftIO $ threadDelay (5 * 1000 * 1000) get (nginz . paths ["self"] . authHeader resp.accessToken) !!! do @@ -395,8 +411,8 @@ testAccessResourceInvalidSignature opts brig nginz = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") claimSet <- fromRight (error "token invalid") <$> liftIO (verify key (unOAuthToken $ resp.accessToken)) @@ -417,10 +433,10 @@ testRefreshTokenMaxActiveTokens opts db brig = -- this is due to the interpreter of the `Now` effect which auto-updates every second -- FUTUREWORK: once the interpreter of the `Now` effect is changed to use a monotonic clock, we can remove this delay threadDelay $ 1000 * 1000 - (rid1, cid, secret) <- do + (rid1, cid, _) <- do let testMsg = "0 active refresh tokens - 1st requested token will be active" - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest rid <- extractRefreshTokenId jwk resp.refreshToken tokens <- C.runClient db (lookupOAuthRefreshTokens uid) @@ -430,7 +446,7 @@ testRefreshTokenMaxActiveTokens opts db brig = rid2 <- do let testMsg = "1 active refresh token - 2nd requested token will added to active tokens" code <- generateOAuthAuthorizationCode brig uid cid scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest rid <- extractRefreshTokenId jwk resp.refreshToken tokens <- C.runClient db (lookupOAuthRefreshTokens uid) @@ -440,7 +456,7 @@ testRefreshTokenMaxActiveTokens opts db brig = rid3 <- do let testMsg = "2 active refresh tokens - 3rd token requested replaces the 1st one" code <- generateOAuthAuthorizationCode brig uid cid scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest rid <- extractRefreshTokenId jwk resp.refreshToken tokens <- C.runClient db (lookupOAuthRefreshTokens uid) @@ -450,7 +466,7 @@ testRefreshTokenMaxActiveTokens opts db brig = do let testMsg = "2 active refresh tokens - 4th token requests replaces the 2nd one" code <- generateOAuthAuthorizationCode brig uid cid scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest rid <- extractRefreshTokenId jwk resp.refreshToken tokens <- C.runClient db (lookupOAuthRefreshTokens uid) @@ -468,13 +484,13 @@ testRefreshTokenRetrieveAccessToken brig nginz = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest get (nginz . paths ["self"] . authHeader (resp.accessToken)) !!! const 200 === statusCode threadDelay $ 5 * 1000 * 1000 -- wait 5 seconds for access token to expire get (nginz . paths ["self"] . authHeader (resp.accessToken)) !!! const 401 === statusCode - let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid secret resp.refreshToken + let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid resp.refreshToken resp' <- refreshOAuthAccessToken brig refreshAccessTokenRequest get (nginz . paths ["self"] . authHeader resp'.accessToken) !!! const 200 === statusCode @@ -483,14 +499,14 @@ testRefreshTokenWrongSignature opts brig = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") badRefreshToken <- liftIO $ do claims <- verifyRefreshToken key (unOAuthToken $ resp.refreshToken) OAuthToken <$> signRefreshToken badKey claims - let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid secret badRefreshToken + let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid badRefreshToken refreshOAuthAccessToken' brig refreshAccessTokenRequest !!! do const 403 === statusCode const "Forbidden" === statusMessage @@ -500,10 +516,10 @@ testRefreshTokenNoTokenId opts brig = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, secret, _) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + (cid, _) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") badRefreshToken <- liftIO $ OAuthToken <$> signRefreshToken key emptyClaimsSet - let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid secret badRefreshToken + let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid badRefreshToken refreshOAuthAccessToken' brig refreshAccessTokenRequest !!! do const 403 === statusCode const "Forbidden" === statusMessage @@ -513,8 +529,8 @@ testRefreshTokenNonExistingId opts brig = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") badRefreshToken <- @@ -525,7 +541,7 @@ testRefreshTokenNonExistingId opts brig = do sub <- maybe (error "creating sub claim failed") pure $ idToText rid ^? stringOrUri let invalidClaims = claims & claimSub ?~ sub signRefreshToken key invalidClaims - let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid secret badRefreshToken + let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid badRefreshToken refreshOAuthAccessToken' brig refreshAccessTokenRequest !!! do const 403 === statusCode const "Forbidden" === statusMessage @@ -535,25 +551,11 @@ testRefreshTokenWrongClientId brig = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest badCid <- randomId - let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken badCid secret resp.refreshToken - refreshOAuthAccessToken' brig refreshAccessTokenRequest !!! do - const 403 === statusCode - const "Forbidden" === statusMessage - -testRefreshTokenWrongClientSecret :: Brig -> Http () -testRefreshTokenWrongClientSecret brig = do - user <- createUser "alice" brig - let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl - resp <- createOAuthAccessToken brig accessTokenRequest - let badSecret = OAuthClientPlainTextSecret $ encodeBase16 "ee2316e304f5c318e4607d86748018eb9c66dc4f391c31bcccd9291d24b4c7e" - let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid badSecret resp.refreshToken + let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken badCid resp.refreshToken refreshOAuthAccessToken' brig refreshAccessTokenRequest !!! do const 403 === statusCode const "Forbidden" === statusMessage @@ -563,10 +565,10 @@ testRefreshTokenWrongGrantType brig = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest - let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret resp.refreshToken + let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeAuthorizationCode cid resp.refreshToken refreshOAuthAccessToken' brig refreshAccessTokenRequest !!! do const 403 === statusCode const "Forbidden" === statusMessage @@ -578,10 +580,10 @@ testRefreshTokenExpiredToken opts brig = user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest - let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid secret resp.refreshToken + let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid resp.refreshToken threadDelay $ 2 * 1010 * 1000 -- wait for 2 seconds for the token to expire refreshOAuthAccessToken' brig refreshAccessTokenRequest !!! do const 403 === statusCode @@ -592,11 +594,11 @@ testRefreshTokenRevokedToken brig = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest - let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid secret resp.refreshToken - revokeOAuthRefreshToken brig (OAuthRevokeRefreshTokenRequest cid secret resp.refreshToken) !!! const 200 === statusCode + let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid resp.refreshToken + revokeOAuthRefreshToken brig (OAuthRevokeRefreshTokenRequest cid resp.refreshToken) !!! const 200 === statusCode refreshOAuthAccessToken' brig refreshAccessTokenRequest !!! do const 403 === statusCode const "Forbidden" === statusMessage @@ -686,8 +688,8 @@ postConvCode svc mkHeader token c = do getAccessTokenForScope :: Brig -> UserId -> [OAuthScope] -> Http OAuthAccessTokenResponse getAccessTokenForScope brig uid scopes = do let redirectUrl = mkUrl "https://example.com" - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig uid (OAuthScopes $ Set.fromList scopes) redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig uid (OAuthScopes $ Set.fromList scopes) redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl createOAuthAccessToken brig accessTokenRequest createTeamConv :: @@ -720,8 +722,8 @@ getFeatureConfigs svc mkHeader token = do createOAuthApplicationWithAccountAccess :: Brig -> UserId -> Http OAuthAccessTokenResponse createOAuthApplicationWithAccountAccess brig uid = do let redirectUrl = mkUrl "https://example.com" - (cid, secret, code) <- generateOAuthClientAndAuthorizationCode brig uid (OAuthScopes $ mempty) redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig uid (OAuthScopes $ mempty) redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl createOAuthAccessToken brig accessTokenRequest verifyRefreshToken :: JWK -> SignedJWT -> IO ClaimsSet @@ -791,17 +793,23 @@ revokeOAuthApplicationAccess :: (MonadIO m, MonadHttp m, MonadCatch m, HasCallSt revokeOAuthApplicationAccess brig uid cid = void $ revokeOAuthApplicationAccess' brig uid cid Brig -> UserId -> OAuthScopes -> RedirectUrl -> m (OAuthClientId, OAuthClientPlainTextSecret, OAuthAuthorizationCode) -generateOAuthClientAndAuthorizationCode brig uid scope url = do +generateOAuthClientAndAuthorizationCode :: (MonadIO m, MonadHttp m, MonadCatch m, HasCallStack) => Brig -> UserId -> OAuthScopes -> RedirectUrl -> m (OAuthClientId, OAuthAuthorizationCode) +generateOAuthClientAndAuthorizationCode = generateOAuthClientAndAuthorizationCode' challenge + +generateOAuthClientAndAuthorizationCode' :: (MonadIO m, MonadHttp m, MonadCatch m, HasCallStack) => OAuthCodeChallenge -> Brig -> UserId -> OAuthScopes -> RedirectUrl -> m (OAuthClientId, OAuthAuthorizationCode) +generateOAuthClientAndAuthorizationCode' chal brig uid scope url = do let newOAuthClient = RegisterOAuthClientRequest (OAuthApplicationName (unsafeRange "E Corp")) url - OAuthClientCredentials cid secret <- registerNewOAuthClient brig newOAuthClient - (cid,secret,) <$> generateOAuthAuthorizationCode brig uid cid scope url + OAuthClientCredentials cid _ <- registerNewOAuthClient brig newOAuthClient + (cid,) <$> generateOAuthAuthorizationCode' chal brig uid cid scope url generateOAuthAuthorizationCode :: (MonadIO m, MonadHttp m, MonadCatch m, HasCallStack) => Brig -> UserId -> OAuthClientId -> OAuthScopes -> RedirectUrl -> m OAuthAuthorizationCode -generateOAuthAuthorizationCode brig uid cid scope url = do +generateOAuthAuthorizationCode = generateOAuthAuthorizationCode' challenge + +generateOAuthAuthorizationCode' :: (MonadIO m, MonadHttp m, MonadCatch m, HasCallStack) => OAuthCodeChallenge -> Brig -> UserId -> OAuthClientId -> OAuthScopes -> RedirectUrl -> m OAuthAuthorizationCode +generateOAuthAuthorizationCode' chal brig uid cid scope url = do state <- UUID.toText <$> liftIO nextRandom response <- - createOAuthCode brig uid (CreateOAuthAuthorizationCodeRequest cid scope OAuthResponseTypeCode url state) => fromByteString >=> getQueryParamValue "code" >=> fromByteString) response @@ -851,3 +859,9 @@ getQueryParams (RedirectUrl uri) = uri ^. (queryL . queryPairsL) getQueryParamValue :: ByteString -> RedirectUrl -> Maybe ByteString getQueryParamValue key uri = snd <$> find ((== key) . fst) (getQueryParams uri) + +challenge :: OAuthCodeChallenge +challenge = either (\e -> error $ "invalid code challenge " <> show e) id $ A.eitherDecode "\"G7CWLBqYDT8doT_oEIN3un_QwZWYKHmOqG91nwNzITc\"" + +verifier :: OAuthCodeVerifier +verifier = either (\e -> error $ "invalid code verifier " <> show e) id $ A.eitherDecode "\"nE3k3zykOmYki~kriKzAmeFiGT7cWugcuToFwo1YPgrZ1cFvaQqLa.dXY9MnDj3umAmG-8lSNIYIl31Cs_.fV5r2psa4WWZcB.Nlc3A-t3p67NDZaOJjIiH~8PvUH_hR\""