diff --git a/library/security/jwt/src/jws.e b/library/security/jwt/src/jws.e index 82a7578a..c527909a 100644 --- a/library/security/jwt/src/jws.e +++ b/library/security/jwt/src/jws.e @@ -64,14 +64,16 @@ feature -- Conversion encoded_string (a_secret: READABLE_STRING_8): STRING local - alg, sign: READABLE_STRING_8 + sign, alg_name: READABLE_STRING_8 + alg: JWT_ALG l_enc_payload, l_enc_header: READABLE_STRING_8 do reset_error - alg := header.algorithm - if not is_supporting_signature_algorithm (alg) then - report_unsupported_alg_error (alg) - alg := alg_hs256 -- Default ... + alg_name := header.algorithm + alg := algorithms [alg_name] + if alg = Void then + report_unsupported_alg_error (alg_name) + alg := algorithms.hs256 -- Default ... end l_enc_header := base64url_encode (header.string) l_enc_payload := base64url_encode (claimset.string) @@ -94,12 +96,12 @@ feature -- Element change set_algorithm_to_hs256 do - set_algorithm (alg_hs256) + set_algorithm (algorithms.hs256.name) end set_algorithm_to_none do - set_algorithm (alg_none) + set_algorithm (algorithms.none.name) end end diff --git a/library/security/jwt/src/jwt.e b/library/security/jwt/src/jwt.e index acf1e3e0..2053a6af 100644 --- a/library/security/jwt/src/jwt.e +++ b/library/security/jwt/src/jwt.e @@ -16,12 +16,15 @@ feature {NONE} -- Initialization default_create do + create algorithms create header create claimset end feature -- Access + algorithms: JWT_ALGORITHMS + header: JWT_HEADER claimset: JWT_CLAIMSET @@ -60,7 +63,7 @@ feature -- Status report if attached claimset.issuer as iss then Result := a_issuer = Void or else a_issuer.same_string (iss) else - Result := a_issuer = Void + Result := a_issuer = Void end end @@ -69,7 +72,7 @@ feature -- Status report if attached claimset.audience as aud then Result := a_audience = Void or else a_audience.same_string (aud) else - Result := a_audience = Void + Result := a_audience = Void end end @@ -91,6 +94,11 @@ feature -- status report Result := attached errors as errs and then across errs as ic some attached {JWT_UNSUPPORTED_ALG_ERROR} ic.item end end + has_mismatched_alg_error: BOOLEAN + do + Result := attached errors as errs and then across errs as ic some attached {JWT_MISMATCHED_ALG_ERROR} ic.item end + end + has_unverified_token_error: BOOLEAN do Result := attached errors as errs and then across errs as ic some attached {JWT_UNVERIFIED_TOKEN_ERROR} ic.item end diff --git a/library/security/jwt/src/jwt_alg.e b/library/security/jwt/src/jwt_alg.e new file mode 100644 index 00000000..30a97a6e --- /dev/null +++ b/library/security/jwt/src/jwt_alg.e @@ -0,0 +1,27 @@ +note + description: "Summary description for {JWT_ALG}." + author: "" + date: "$Date$" + revision: "$Revision$" + +deferred class + JWT_ALG + +feature -- Access + + name: READABLE_STRING_8 + deferred + end + + encoded_string (a_message: READABLE_STRING_8; a_secret: READABLE_STRING_8): STRING + deferred + end + +feature -- Status report + + is_none: BOOLEAN + -- Is Current algorithm is "none" ? + do + end + +end diff --git a/library/security/jwt/src/jwt_alg_hs256.e b/library/security/jwt/src/jwt_alg_hs256.e new file mode 100644 index 00000000..5b5394b2 --- /dev/null +++ b/library/security/jwt/src/jwt_alg_hs256.e @@ -0,0 +1,57 @@ +note + description: "Summary description for {JWT_ALG_HS256}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + JWT_ALG_HS256 + +inherit + JWT_ALG + +feature -- Access + + name: STRING = "hs256" + + encoded_string (a_message: READABLE_STRING_8; a_secret: READABLE_STRING_8): STRING + do + Result := base64_hmacsha256 (a_message, a_secret) + end + +feature {NONE} -- Implementation + + base64_hmacsha256 (s: READABLE_STRING_8; a_secret: READABLE_STRING_8): STRING_8 + local + hs256: HMAC_SHA256 + do + create hs256.make_ascii_key (a_secret) + hs256.update_from_string (s) + -- if Version >= EiffelStudio 18.01 then + -- Result := hs256.base64_digest --lowercase_hexadecimal_string_digest + -- else + Result := base64_bytes_encoded_string (hs256.digest) + -- end + end + + base64_bytes_encoded_string (a_bytes: SPECIAL [NATURAL_8]): STRING_8 + -- Base64 string from `a_bytes`. + --| Note: to be removed when 18.01 is not latest release anymore. + local + s: STRING + i,n: INTEGER + do + from + i := 1 + n := a_bytes.count + create s.make (n) + until + i > n + loop + s.append_code (a_bytes[i - 1]) + i := i + 1 + end + Result := (create {BASE64}).encoded_string (s) + end + +end diff --git a/library/security/jwt/src/jwt_alg_none.e b/library/security/jwt/src/jwt_alg_none.e new file mode 100644 index 00000000..04358452 --- /dev/null +++ b/library/security/jwt/src/jwt_alg_none.e @@ -0,0 +1,30 @@ +note + description: "Summary description for {JWT_ALG_NONE}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + JWT_ALG_NONE + +inherit + JWT_ALG + redefine + is_none + end + +feature -- Access + + name: STRING = "none" + + encoded_string (a_message: READABLE_STRING_8; a_secret: READABLE_STRING_8): STRING + do + create Result.make_empty + end + +feature -- Status report + + is_none: BOOLEAN = True + -- Is Current algorithm is "none" ? + +end diff --git a/library/security/jwt/src/jwt_algorithms.e b/library/security/jwt/src/jwt_algorithms.e new file mode 100644 index 00000000..6a41964f --- /dev/null +++ b/library/security/jwt/src/jwt_algorithms.e @@ -0,0 +1,98 @@ +note + description: "Summary description for {JWT_ALGORITHMS}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + JWT_ALGORITHMS + +inherit + ANY + redefine + default_create + end + +create + default_create + +feature {NONE} -- Initialization + + default_create + do + create items.make_caseless (2) + register_algorithm (hs256) + register_algorithm (none) + -- TODO: check if this is acceptable default. + set_default_algorithm ({JWT_ALG_HS256}.name) + end + +feature -- Access + + hs256: JWT_ALG_HS256 + do + create Result + end + + none: JWT_ALG_NONE + do + create Result + end + +feature -- Access + + default_algorithm: JWT_ALG + do + if attached internal_default_alg_name as l_alg_name then + Result := algorithm (l_alg_name) + end + if Result = Void then + Result := none + end + end + + algorithm alias "[]" (a_name: READABLE_STRING_GENERAL): detachable JWT_ALG + do + Result := items [a_name] + end + +feature -- Element change + + register_algorithm (alg: attached like algorithm) + do + items [alg.name] := alg + end + + unregister_algorithm (a_alg_name: READABLE_STRING_GENERAL) + do + items.remove (a_alg_name) + end + + set_default_algorithm (a_alg_name: detachable READABLE_STRING_GENERAL) + do + if + a_alg_name = Void or else + not is_supported_algorithm (a_alg_name) + then + internal_default_alg_name := Void + else + internal_default_alg_name := a_alg_name + end + end + +feature -- Status report + + is_supported_algorithm (a_name: READABLE_STRING_GENERAL): BOOLEAN + do + Result := items.has (a_name) + end + +feature {NONE} -- Implementation + + items: STRING_TABLE [attached like algorithm] + + internal_default_alg_name: detachable READABLE_STRING_GENERAL + +invariant + +end diff --git a/library/security/jwt/src/jwt_loader.e b/library/security/jwt/src/jwt_loader.e index e6466585..7efaae25 100644 --- a/library/security/jwt/src/jwt_loader.e +++ b/library/security/jwt/src/jwt_loader.e @@ -9,6 +9,20 @@ class inherit JWT_UTILITIES + redefine + default_create + end + +feature {NONE} -- Initialization + + default_create + do + create algorithms + end + +feature -- Settings + + algorithms: JWT_ALGORITHMS feature -- Access @@ -18,11 +32,12 @@ feature -- Access -- 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) + a_valid_alg: a_alg /= Void implies algorithms.is_supported_algorithm (a_alg) local jws: JWS i,j,n: INTEGER alg, l_enc_payload, l_enc_header, l_signature: READABLE_STRING_8 + alg_encoder: JWT_ALG do n := a_token_input.count i := a_token_input.index_of ('.', 1) @@ -43,17 +58,18 @@ feature -- Access else if alg = Void then -- Use default - alg := alg_hs256 + alg := algorithms.default_algorithm.name 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 + alg_encoder := algorithms [alg] + if alg_encoder = Void then jws.report_unsupported_alg_error (alg) - alg := alg_hs256 + alg_encoder := algorithms.default_algorithm end - if not l_signature.same_string (signature (l_enc_header, l_enc_payload, a_verification_key, alg)) then + if not l_signature.same_string (signature (l_enc_header, l_enc_payload, a_verification_key, alg_encoder)) then jws.report_unverified_token_error end if diff --git a/library/security/jwt/src/jwt_utilities.e b/library/security/jwt/src/jwt_utilities.e index 24eb5cc9..58d99204 100644 --- a/library/security/jwt/src/jwt_utilities.e +++ b/library/security/jwt/src/jwt_utilities.e @@ -7,14 +7,6 @@ note class JWT_UTILITIES -feature -- Constants - - alg_hs256: STRING = "HS256" - -- HMAC SHA256. - - alg_none: STRING = "none" - -- for unsecured token. - feature -- Encoding base64url_encode (s: READABLE_STRING_8): STRING_8 @@ -35,61 +27,21 @@ feature -- Encoding Result.replace_substring_all ("/", "_") end - signature (a_enc_header, a_enc_payload: READABLE_STRING_8; a_secret: READABLE_STRING_8; alg: READABLE_STRING_8): STRING_8 + signature (a_enc_header, a_enc_payload: READABLE_STRING_8; a_secret: READABLE_STRING_8; alg: JWT_ALG): STRING_8 local s: STRING do - if alg.is_case_insensitive_equal (alg_none) then + if alg.is_none then create Result.make_empty else create s.make (a_enc_header.count + 1 + a_enc_payload.count) s.append (a_enc_header) s.append_character ('.') s.append (a_enc_payload) - if alg.is_case_insensitive_equal (alg_hs256) then - Result := base64_hmacsha256 (s, a_secret) - else - Result := base64_hmacsha256 (s, a_secret) - end - Result := urlsafe_encode (Result) + Result := urlsafe_encode (alg.encoded_string (s, a_secret)) end end - base64_hmacsha256 (s: READABLE_STRING_8; a_secret: READABLE_STRING_8): STRING_8 - local - hs256: HMAC_SHA256 - do - create hs256.make_ascii_key (a_secret) - hs256.update_from_string (s) - -- if Version >= EiffelStudio 17.11 then - -- Result := hs256.base64_digest --lowercase_hexadecimal_string_digest - -- else - Result := base64_bytes_encoded_string (hs256.digest) - -- end - end - -feature {NONE} -- Implementation - - base64_bytes_encoded_string (a_bytes: SPECIAL [NATURAL_8]): STRING_8 - -- Base64 string from `a_bytes`. - --| Note: to be removed when 17.11 is not latest release anymore. - local - s: STRING - i,n: INTEGER - do - from - i := 1 - n := a_bytes.count - create s.make (n) - until - i > n - loop - s.append_code (a_bytes[i - 1]) - i := i + 1 - end - Result := (create {BASE64}).encoded_string (s) - end - feature -- Decoding base64url_decode (s: READABLE_STRING_8): STRING_8 @@ -119,20 +71,4 @@ feature -- Decoding end end -feature -- Signature - - supported_signature_algorithms: LIST [READABLE_STRING_8] - -- Supported signature algorithm `alg`? - do - create {ARRAYED_LIST [READABLE_STRING_8]} Result.make (2) - Result.extend (alg_hs256) - Result.extend (alg_none) - end - - is_supporting_signature_algorithm (alg: READABLE_STRING_8): BOOLEAN - -- Is supporting signature algorithm `alg`? - do - Result := across supported_signature_algorithms as ic some alg.is_case_insensitive_equal (ic.item) end - end - end diff --git a/library/security/jwt/testing/jwt_alg_test.e b/library/security/jwt/testing/jwt_alg_test.e new file mode 100644 index 00000000..a84249ca --- /dev/null +++ b/library/security/jwt/testing/jwt_alg_test.e @@ -0,0 +1,22 @@ +note + description: "Summary description for {JWT_ALG_TEST}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + JWT_ALG_TEST + +inherit + JWT_ALG + +feature -- Access + + name: STRING = "test" + + encoded_string (a_message: READABLE_STRING_8; a_secret: READABLE_STRING_8): STRING + do + Result := "TEST<<"+ a_message + ">>" + end + +end diff --git a/library/security/jwt/testing/test_jwt.e b/library/security/jwt/testing/test_jwt.e index 9e4dcac8..ca4c2068 100644 --- a/library/security/jwt/testing/test_jwt.e +++ b/library/security/jwt/testing/test_jwt.e @@ -16,6 +16,30 @@ inherit feature -- Test + example + local + jwt: JWS + l_loader: JWT_LOADER + tok: STRING + do + create jwt.make_with_json_payload ("[ + {"iss":"joe", "exp":1200819380,"http://example.com/is_root":true} + ]") + jwt.set_algorithm_to_hs256 + tok := jwt.encoded_string ("my-secret") + + create l_loader + if + attached l_loader.token (tok, Void, "my-secret", Void) as l_tok and then + not l_tok.has_error + then + print (l_tok.claimset.string) + check verified: not l_tok.has_unverified_token_error end + check no_error: not l_tok.has_error end + end + end + + test_jwt_io local jwt: JWS @@ -185,7 +209,8 @@ feature -- Test 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 ("error", l_tok.has_error) + assert ("has_mismatched_alg_error", l_tok.has_mismatched_alg_error) assert ("same payload", l_tok.claimset.string.same_string (payload)) end end @@ -205,15 +230,50 @@ feature -- Test tok := jwt.encoded_string ("secret") if attached (create {JWT_LOADER}).token (tok, "none", "secret", Void) as l_tok then - assert ("no error", not jwt.has_error) + assert ("no error", not l_tok.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 ("no error", not l_tok.has_error) assert ("same payload", l_tok.claimset.string.same_string (payload)) end end + test_additional_alg + local + jwt: JWS + payload: STRING + tok: STRING + l_loader: JWT_LOADER + do + payload := "[ + {"iss":"joe","exp":1300819380,"http://example.com/is_root":true} + ]" + + create jwt.make_with_json_payload (payload) + jwt.algorithms.register_algorithm (create {JWT_ALG_TEST}) + jwt.set_algorithm ({JWT_ALG_TEST}.name) + tok := jwt.encoded_string ("secret") + + create l_loader + l_loader.algorithms.register_algorithm (create {JWT_ALG_TEST}) + if attached l_loader.token (tok, "test", "secret", Void) as l_tok then + assert ("no error", not l_tok.has_error) + assert ("not has_unsupported_alg_error", not l_tok.has_unsupported_alg_error) + assert ("same payload", l_tok.claimset.string.same_string (payload)) + end + if attached l_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 + + create l_loader + if attached l_loader.token (tok, "test", "secret", Void) as l_tok then + assert ("has error", l_tok.has_error) + assert ("has_unsupported_alg_error", l_tok.has_unsupported_alg_error) + end + end + feature -- Implementation duplicated_time (dt: DATE_TIME): DATE_TIME