Skip to content

Commit 60c8a98

Browse files
committed
fix: Validate aud is a valid URI even if jwt-aud is not configured
* Introduced StringOrURI newtype and its validating FromJSON implementation in Auth.JWT * Changed JwtError constructor AudClaimNotStringOrArray to AudClaimNotStringOrURIOrArray and modified error message for it * Added test in AuthSpec * Modified tests to verify new error message Left validation of URI in jwt-aud as is.
1 parent 6616110 commit 60c8a98

File tree

4 files changed

+41
-18
lines changed

4 files changed

+41
-18
lines changed

src/PostgREST/Auth/Jwt.hs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,13 @@ import PostgREST.Auth.Types (AuthResult (..))
3737
import PostgREST.Config (AppConfig (..), FilterExp (..), JSPath,
3838
JSPathExp (..), audMatchesCfg)
3939
import PostgREST.Error (Error (..),
40-
JwtClaimsError (AudClaimNotStringOrArray, ExpClaimNotNumber, IatClaimNotNumber, JWTExpired, JWTIssuedAtFuture, JWTNotInAudience, JWTNotYetValid, NbfClaimNotNumber, ParsingClaimsFailed),
40+
JwtClaimsError (AudClaimNotStringOrURIOrArray, ExpClaimNotNumber, IatClaimNotNumber, JWTExpired, JWTIssuedAtFuture, JWTNotInAudience, JWTNotYetValid, NbfClaimNotNumber, ParsingClaimsFailed),
4141
JwtDecodeError (..), JwtError (..))
4242

4343
import Data.Aeson ((.:?))
4444
import Data.Aeson.Types (parseMaybe)
4545
import Jose.Jwk (JwkSet)
46+
import Network.URI (isURI)
4647
import Protolude hiding (first)
4748

4849
parseAndDecodeClaims :: (MonadError Error m, MonadIO m) => JwkSet -> ByteString -> m JSON.Object
@@ -55,7 +56,13 @@ decodeClaims _ = throwError $ JwtErr $ JwtDecodeErr UnsupportedTokenType
5556
validateClaims :: MonadError Error m => UTCTime -> (Text -> Bool) -> JSON.Object -> m ()
5657
validateClaims time audMatches claims = liftEither $ maybeToLeft () (fmap JwtErr . getAlt $ JwtClaimsErr <$> checkForErrors time audMatches claims)
5758

58-
data ValidAud = VAString Text | VAArray [Text] deriving Generic
59+
newtype StringOrURI = StringOrURI { unStringOrURI :: Text }
60+
instance JSON.FromJSON StringOrURI where
61+
parseJSON = fmap StringOrURI . mfilter isValidURI . JSON.parseJSON
62+
where
63+
isValidURI = (||) <$> not . T.isInfixOf ":" <*> isURI . T.unpack
64+
65+
data ValidAud = VAString StringOrURI | VAArray [StringOrURI] deriving Generic
5966
instance JSON.FromJSON ValidAud where
6067
parseJSON = JSON.genericParseJSON JSON.defaultOptions { JSON.sumEncoding = JSON.UntaggedValue }
6168

@@ -65,7 +72,7 @@ checkForErrors time audMatches = mconcat
6572
claim "exp" ExpClaimNotNumber $ inThePast JWTExpired
6673
, claim "nbf" NbfClaimNotNumber $ inTheFuture JWTNotYetValid
6774
, claim "iat" IatClaimNotNumber $ inTheFuture JWTIssuedAtFuture
68-
, claim "aud" AudClaimNotStringOrArray $ checkValue (not . validAud) JWTNotInAudience
75+
, claim "aud" AudClaimNotStringOrURIOrArray $ checkValue (not . validAud) JWTNotInAudience
6976
]
7077
where
7178
allowedSkewSeconds = 30 :: Int64
@@ -79,8 +86,9 @@ checkForErrors time audMatches = mconcat
7986
checkTime cond = checkValue (cond. sciToInt)
8087

8188
validAud = \case
82-
(VAString aud) -> audMatches aud
83-
(VAArray auds) -> null auds || any audMatches auds
89+
(VAString aud) -> validAudString aud
90+
(VAArray auds) -> null auds || any validAudString auds
91+
validAudString = audMatches . unStringOrURI
8492

8593
checkValue invalid msg val =
8694
if invalid val then

src/PostgREST/Error.hs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@ data JwtClaimsError
675675
| ExpClaimNotNumber
676676
| NbfClaimNotNumber
677677
| IatClaimNotNumber
678-
| AudClaimNotStringOrArray
678+
| AudClaimNotStringOrURIOrArray
679679
deriving Show
680680

681681
instance PgrstError Error where
@@ -752,15 +752,15 @@ instance ErrorBody JwtError where
752752
UnreachableDecodeError -> "JWT couldn't be decoded"
753753
message JwtTokenRequired = "Anonymous access is disabled"
754754
message (JwtClaimsErr e) = case e of
755-
JWTExpired -> "JWT expired"
756-
JWTNotYetValid -> "JWT not yet valid"
757-
JWTIssuedAtFuture -> "JWT issued at future"
758-
JWTNotInAudience -> "JWT not in audience"
759-
ParsingClaimsFailed -> "Parsing claims failed"
760-
ExpClaimNotNumber -> "The JWT 'exp' claim must be a number"
761-
NbfClaimNotNumber -> "The JWT 'nbf' claim must be a number"
762-
IatClaimNotNumber -> "The JWT 'iat' claim must be a number"
763-
AudClaimNotStringOrArray -> "The JWT 'aud' claim must be a string or an array of strings"
755+
JWTExpired -> "JWT expired"
756+
JWTNotYetValid -> "JWT not yet valid"
757+
JWTIssuedAtFuture -> "JWT issued at future"
758+
JWTNotInAudience -> "JWT not in audience"
759+
ParsingClaimsFailed -> "Parsing claims failed"
760+
ExpClaimNotNumber -> "The JWT 'exp' claim must be a number"
761+
NbfClaimNotNumber -> "The JWT 'nbf' claim must be a number"
762+
IatClaimNotNumber -> "The JWT 'iat' claim must be a number"
763+
AudClaimNotStringOrURIOrArray -> "The JWT 'aud' claim must be a string, URI or an array of mixed strings or URIs"
764764

765765
details (JwtDecodeErr jde) = case jde of
766766
KeyError dets -> Just $ JSON.String dets

test/spec/Feature/Auth/AudienceJwtSecretSpec.hs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,21 @@ disabledSpec = describe "test handling of aud claims in JWT when the jwt-aud con
175175
request methodGet "/authors_only" [auth] ""
176176
`shouldRespondWith` 200
177177

178+
it "fails when the audience claim is invalid URI" $ do
179+
let jwtPayload = [json|
180+
{
181+
"exp": 9999999999,
182+
"role": "postgrest_test_author",
183+
"id": "jdoe",
184+
"aud": "http://%%"
185+
}|]
186+
auth = authHeaderJWT $ generateJWT jwtPayload
187+
request methodGet "/authors_only" [auth] ""
188+
`shouldRespondWith`
189+
[json|{"code":"PGRST303","details":null,"hint":null,"message":"The JWT 'aud' claim must be a string, URI or an array of mixed strings or URIs"}|]
190+
{ matchStatus = 401 }
191+
192+
178193
context "when the audience is an array of strings" $ do
179194
it "ignores the audience claim and suceeds when it has 1 element" $ do
180195
let jwtPayload = [json|

test/spec/Feature/Auth/AuthSpec.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ spec = describe "authorization" $ do
173173
[json|{"code":"PGRST303","details":null,"hint":null,"message":"The JWT 'iat' claim must be a number"}|]
174174
{ matchStatus = 401 }
175175

176-
it "fails when the aud claim has a single value and it's not a string" $ do
176+
it "fails when the aud claim has a single value and it's an object" $ do
177177
let jwtPayload = [json|
178178
{
179179
"aud": {"invalid": "value"},
@@ -182,7 +182,7 @@ spec = describe "authorization" $ do
182182
auth = authHeaderJWT $ generateJWT jwtPayload
183183
request methodGet "/authors_only" [auth] ""
184184
`shouldRespondWith`
185-
[json|{"code":"PGRST303","details":null,"hint":null,"message":"The JWT 'aud' claim must be a string or an array of strings"}|]
185+
[json|{"code":"PGRST303","details":null,"hint":null,"message":"The JWT 'aud' claim must be a string, URI or an array of mixed strings or URIs"}|]
186186
{ matchStatus = 401 }
187187

188188
it "fails when the aud claim is an array but it has non-string elements" $ do
@@ -194,7 +194,7 @@ spec = describe "authorization" $ do
194194
auth = authHeaderJWT $ generateJWT jwtPayload
195195
request methodGet "/authors_only" [auth] ""
196196
`shouldRespondWith`
197-
[json|{"code":"PGRST303","details":null,"hint":null,"message":"The JWT 'aud' claim must be a string or an array of strings"}|]
197+
[json|{"code":"PGRST303","details":null,"hint":null,"message":"The JWT 'aud' claim must be a string, URI or an array of mixed strings or URIs"}|]
198198
{ matchStatus = 401 }
199199

200200
describe "custom pre-request proc acting on id claim" $ do

0 commit comments

Comments
 (0)