diff --git a/library/security/jwt/src/errors/jwt_mismatched_alg_error.e b/library/security/jwt/src/errors/jwt_mismatched_alg_error.e new file mode 100644 index 00000000..4e9ba1c3 --- /dev/null +++ b/library/security/jwt/src/errors/jwt_mismatched_alg_error.e @@ -0,0 +1,36 @@ +note + description: "Summary description for {JWT_MISMATCHED_ALG_ERROR}." + date: "$Date$" + revision: "$Revision$" + +class + JWT_MISMATCHED_ALG_ERROR + +inherit + JWT_ERROR + +create + make + +feature {NONE} -- Initialization + + make (a_alg, a_header_alg: READABLE_STRING_8) + do + alg := a_alg + header_alg := a_header_alg + end + +feature -- Access + + alg: READABLE_STRING_8 + + header_alg: READABLE_STRING_8 + + id: STRING = "ALG_MISMATCH" + + message: READABLE_STRING_8 + do + Result := "Header alg [" + header_alg + "] does not match given alg [" + alg + "]!" + end + +end diff --git a/library/security/jwt/src/jwt.e b/library/security/jwt/src/jwt.e index 381afc9f..f23091fb 100644 --- a/library/security/jwt/src/jwt.e +++ b/library/security/jwt/src/jwt.e @@ -118,6 +118,11 @@ feature {JWT_UTILITIES} -- Error reporting l_errors.extend (err) end + report_mismatched_alg_error (alg, a_header_alg: READABLE_STRING_8) + do + report_error (create {JWT_MISMATCHED_ALG_ERROR}.make (alg, a_header_alg)) + end + report_unsupported_alg_error (alg: READABLE_STRING_8) do report_error (create {JWT_UNSUPPORTED_ALG_ERROR}.make (alg)) diff --git a/library/security/jwt/src/jwt_loader.e b/library/security/jwt/src/jwt_loader.e index 69df2beb..e6466585 100644 --- a/library/security/jwt/src/jwt_loader.e +++ b/library/security/jwt/src/jwt_loader.e @@ -1,8 +1,8 @@ note - description: "Summary description for {JWT_LOADER}." - author: "" + description: "Loader and verifier to JWT token." date: "$Date$" revision: "$Revision$" + EIS: "name=Known Critical vulnerabilities in JWT libs", "protocol=URI", "src=https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/" class JWT_LOADER @@ -12,9 +12,13 @@ inherit feature -- Access - token (a_token_input: READABLE_STRING_8; a_secret: READABLE_STRING_8; ctx: detachable JWT_CONTEXT): detachable JWT - -- Decoded token from `a_token_input` given the secret `a_secret`, and optional context `ctx` + token (a_token_input: READABLE_STRING_8; a_alg: detachable READABLE_STRING_8; a_verification_key: READABLE_STRING_8; ctx: detachable JWT_CONTEXT): detachable JWT + -- Decoded token from `a_token_input` given the verification key `a_verification_key` and optional (but recommended) signature algorithm `a_alg`, and optional context `ctx` -- used to specify eventual issuer and various parameters. + -- WARNING: passing Void for `a_alg` is not safe, as the server should know which alg he used for tokens, + -- leaving the possibility to use the header alg is dangerous as client may use "none" and then bypass verification! + require + a_valid_alg: a_alg /= Void implies is_supporting_signature_algorithm (a_alg) local jws: JWS i,j,n: INTEGER @@ -29,20 +33,27 @@ feature -- Access l_enc_payload := a_token_input.substring (i + 1, j - 1) l_signature := a_token_input.substring (j + 1, n) create jws.make_with_json_payload (base64url_decode (l_enc_payload)) - alg := signature_algorithm_from_encoded_header (l_enc_header) - jws.set_algorithm (alg) - if alg = Void then - -- Use default - alg := alg_hs256 + if a_alg /= Void then + if alg /= Void and then not alg.is_case_insensitive_equal_general (a_alg) then + jws.report_mismatched_alg_error (a_alg, alg) + else + alg := a_alg + end + else + if alg = Void then + -- Use default + alg := alg_hs256 + end end + jws.set_algorithm (alg) check alg_set: alg /= Void end if ctx = Void or else not ctx.validation_ignored then if not is_supporting_signature_algorithm (alg) then jws.report_unsupported_alg_error (alg) alg := alg_hs256 end - if not l_signature.same_string (signature (l_enc_header, l_enc_payload, a_secret, alg)) then + if not l_signature.same_string (signature (l_enc_header, l_enc_payload, a_verification_key, alg)) then jws.report_unverified_token_error end if diff --git a/library/security/jwt/testing/test_jwt.e b/library/security/jwt/testing/test_jwt.e index 4642458b..9e4dcac8 100644 --- a/library/security/jwt/testing/test_jwt.e +++ b/library/security/jwt/testing/test_jwt.e @@ -54,7 +54,14 @@ feature -- Test create jwt_loader - if attached jwt_loader.token (tok, "secret", Void) as l_tok then + -- Use header alg! + if attached jwt_loader.token (tok, Void, "secret", Void) as l_tok then + assert ("no error", not l_tok.has_error) + assert ("same payload", l_tok.claimset.string.same_string (payload)) + end + + -- Use given alg! + if attached jwt_loader.token (tok, jwt.algorithm, "secret", Void) as l_tok then assert ("no error", not l_tok.has_error) assert ("same payload", l_tok.claimset.string.same_string (payload)) end @@ -96,21 +103,21 @@ feature -- Test create jwt_loader -- Test with validation + exp - if attached jwt_loader.token (tok, "secret", Void) as l_tok then + if attached jwt_loader.token (tok, jwt.algorithm, "secret", Void) as l_tok then assert ("no error", not l_tok.has_error) assert ("same payload", l_tok.claimset.string.same_string (payload)) end create ctx ctx.set_time (now) - if attached jwt_loader.token (tok, "secret", ctx) as l_tok then + if attached jwt_loader.token (tok, jwt.algorithm, "secret", ctx) as l_tok then assert ("no error", not l_tok.has_error) end dt := duplicated_time (now) dt.hour_add (5) ctx.set_time (dt) - if attached jwt_loader.token (tok, "secret", ctx) as l_tok then + if attached jwt_loader.token (tok, jwt.algorithm, "secret", ctx) as l_tok then assert ("exp error", l_tok.has_error) end @@ -122,7 +129,7 @@ feature -- Test tok := jwt.encoded_string ("secret") ctx.set_time (now) - if attached jwt_loader.token (tok, "secret", ctx) as l_tok then + if attached jwt_loader.token (tok, jwt.algorithm, "secret", ctx) as l_tok then assert ("has nbf error", l_tok.has_error) end @@ -130,7 +137,7 @@ feature -- Test dt.second_add (15) ctx.set_time (dt) - if attached jwt_loader.token (tok, "secret", ctx) as l_tok then + if attached jwt_loader.token (tok, jwt.algorithm, "secret", ctx) as l_tok then assert ("has nbf error", l_tok.has_error) end @@ -138,31 +145,51 @@ feature -- Test dt.minute_add (45) ctx.set_time (dt) - if attached jwt_loader.token (tok, "secret", ctx) as l_tok then + if attached jwt_loader.token (tok, jwt.algorithm, "secret", ctx) as l_tok then assert ("no error", not l_tok.has_error) end -- Test Issuer ctx.set_issuer ("urn:foobar") - if attached jwt_loader.token (tok, "secret", ctx) as l_tok then + if attached jwt_loader.token (tok, jwt.algorithm, "secret", ctx) as l_tok then assert ("has iss error", l_tok.has_error) end ctx.set_issuer ("urn:foo") - if attached jwt_loader.token (tok, "secret", ctx) as l_tok then + if attached jwt_loader.token (tok, jwt.algorithm, "secret", ctx) as l_tok then assert ("no error", not l_tok.has_error) end -- Test Audience ctx.set_audience ("urn:foobar") - if attached jwt_loader.token (tok, "secret", ctx) as l_tok then + if attached jwt_loader.token (tok, jwt.algorithm, "secret", ctx) as l_tok then assert ("has aud error", l_tok.has_error) end ctx.set_audience ("urn:foo") - if attached jwt_loader.token (tok, "secret", ctx) as l_tok then + if attached jwt_loader.token (tok, jwt.algorithm, "secret", ctx) as l_tok then assert ("no error", not l_tok.has_error) end end + test_mismatched_alg_jwt + local + jwt: JWS + payload: STRING + tok: STRING + do + payload := "[ + {"iss":"joe","exp":1300819380,"http://example.com/is_root":true} + ]" + + create jwt.make_with_json_payload (payload) + jwt.set_algorithm ("none") + tok := jwt.encoded_string ("secret") + + if attached (create {JWT_LOADER}).token (tok, "HS256", "secret", Void) as l_tok then + assert ("no error", not jwt.has_error) + assert ("same payload", l_tok.claimset.string.same_string (payload)) + end + end + test_unsecured_jwt local jwt: JWS @@ -177,7 +204,11 @@ feature -- Test jwt.set_algorithm ("none") tok := jwt.encoded_string ("secret") - if attached (create {JWT_LOADER}).token (tok, "secret", Void) as l_tok then + if attached (create {JWT_LOADER}).token (tok, "none", "secret", Void) as l_tok then + assert ("no error", not jwt.has_error) + assert ("same payload", l_tok.claimset.string.same_string (payload)) + end + if attached (create {JWT_LOADER}).token (tok, Void, "secret", Void) as l_tok then assert ("no error", not jwt.has_error) assert ("same payload", l_tok.claimset.string.same_string (payload)) end