Test that the JWT's iss field meets our expectations

The JWT should match "accounts.google.com" or "https://accounts.google.com". If
it doesn't, we produce a validation error.

TL;DR:
- Group all failed stringOrURI function calls as StringOrURIParseFailure errors
This commit is contained in:
William Carroll 2020-08-08 14:08:11 +01:00
parent 526728eb89
commit f1883b2790
3 changed files with 42 additions and 9 deletions

View file

@ -16,6 +16,7 @@ import qualified TestUtils
data JWTFields = JWTFields data JWTFields = JWTFields
{ overwriteSigner :: Signer { overwriteSigner :: Signer
, overwriteAuds :: [StringOrURI] , overwriteAuds :: [StringOrURI]
, overwriteIss :: StringOrURI
} }
defaultJWTFields :: JWTFields defaultJWTFields :: JWTFields
@ -23,6 +24,7 @@ defaultJWTFields = JWTFields
{ overwriteSigner = hmacSecret "secret" { overwriteSigner = hmacSecret "secret"
, overwriteAuds = ["771151720060-buofllhed98fgt0j22locma05e7rpngl.apps.googleusercontent.com"] , overwriteAuds = ["771151720060-buofllhed98fgt0j22locma05e7rpngl.apps.googleusercontent.com"]
|> fmap TestUtils.unsafeStringOrURI |> fmap TestUtils.unsafeStringOrURI
, overwriteIss = TestUtils.unsafeStringOrURI "accounts.google.com"
} }
googleJWT :: JWTFields -> GoogleSignIn.EncodedJWT googleJWT :: JWTFields -> GoogleSignIn.EncodedJWT
@ -43,7 +45,7 @@ googleJWT JWTFields{..} =
claimSet :: JWTClaimsSet claimSet :: JWTClaimsSet
claimSet = JWTClaimsSet claimSet = JWTClaimsSet
{ iss = stringOrURI "accounts.google.com" { iss = Just overwriteIss
, sub = stringOrURI "114079822315085727057" , sub = stringOrURI "114079822315085727057"
, aud = overwriteAuds |> Right |> Just , aud = overwriteAuds |> Right |> Just
-- TODO: Replace date creation with a human-readable date constructor. -- TODO: Replace date creation with a human-readable date constructor.

View file

@ -8,6 +8,7 @@ import Web.JWT
import Utils import Utils
import qualified Network.HTTP.Simple as HTTP import qualified Network.HTTP.Simple as HTTP
import qualified Data.Text as Text
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
newtype EncodedJWT = EncodedJWT Text newtype EncodedJWT = EncodedJWT Text
@ -18,7 +19,9 @@ data ValidationResult
| DecodeError | DecodeError
| GoogleSaysInvalid Text | GoogleSaysInvalid Text
| NoMatchingClientIDs [StringOrURI] | NoMatchingClientIDs [StringOrURI]
| ClientIDParseFailure Text | WrongIssuer StringOrURI
| StringOrURIParseFailure Text
| MissingIssuer
deriving (Eq, Show) deriving (Eq, Show)
-- | Returns True when the supplied `jwt` meets the following criteria: -- | Returns True when the supplied `jwt` meets the following criteria:
@ -49,15 +52,31 @@ jwtIsValid skipHTTP (EncodedJWT encodedJWT) = do
where where
continue :: JWT UnverifiedJWT -> IO ValidationResult continue :: JWT UnverifiedJWT -> IO ValidationResult
continue jwt = do continue jwt = do
let audValues = jwt |> claims |> auds let audValues :: [StringOrURI]
mClientID = stringOrURI "771151720060-buofllhed98fgt0j22locma05e7rpngl.apps.googleusercontent.com" audValues = jwt |> claims |> auds
case mClientID of expectedClientID :: Text
Nothing -> expectedClientID = "771151720060-buofllhed98fgt0j22locma05e7rpngl.apps.googleusercontent.com"
pure $ ClientIDParseFailure "771151720060-buofllhed98fgt0j22locma05e7rpngl.apps.googleusercontent.com" expectedIssuers :: [Text]
Just clientID -> expectedIssuers = [ "accounts.google.com"
, "https://accounts.google.com"
]
mExpectedClientID :: Maybe StringOrURI
mExpectedClientID = stringOrURI expectedClientID
mExpectedIssuers :: Maybe [StringOrURI]
mExpectedIssuers = expectedIssuers |> traverse stringOrURI
case (mExpectedClientID, mExpectedIssuers) of
(Nothing, _) -> pure $ StringOrURIParseFailure expectedClientID
(_, Nothing) -> pure $ StringOrURIParseFailure (Text.unwords expectedIssuers)
(Just clientID, Just parsedIssuers) ->
-- TODO: Prefer reading clientID from a config. I'm thinking of the -- TODO: Prefer reading clientID from a config. I'm thinking of the
-- AppContext type having my Configuration -- AppContext type having my Configuration
if not $ clientID `elem` audValues then if not $ clientID `elem` audValues then
pure $ NoMatchingClientIDs audValues pure $ NoMatchingClientIDs audValues
else else
pure Valid case jwt |> claims |> iss of
Nothing -> pure MissingIssuer
Just jwtIssuer ->
if not $ jwtIssuer `elem` parsedIssuers then
pure $ WrongIssuer jwtIssuer
else
pure Valid

View file

@ -32,3 +32,15 @@ main = hspec $ do
encodedJWT = F.defaultJWTFields { F.overwriteAuds = auds } encodedJWT = F.defaultJWTFields { F.overwriteAuds = auds }
|> F.googleJWT |> F.googleJWT
jwtIsValid' encodedJWT `shouldReturn` Valid jwtIsValid' encodedJWT `shouldReturn` Valid
it "returns validation error when one of the iss field doesn't match accounts.google.com or https://accounts.google.com" $ do
let erroneousIssuer = TestUtils.unsafeStringOrURI "not-accounts.google.com"
encodedJWT = F.defaultJWTFields { F.overwriteIss = erroneousIssuer }
|> F.googleJWT
jwtIsValid' encodedJWT `shouldReturn` WrongIssuer erroneousIssuer
it "returns validation success when the iss field matches accounts.google.com or https://accounts.google.com" $ do
let erroneousIssuer = TestUtils.unsafeStringOrURI "https://accounts.google.com"
encodedJWT = F.defaultJWTFields { F.overwriteIss = erroneousIssuer }
|> F.googleJWT
jwtIsValid' encodedJWT `shouldReturn` Valid