From 7e54825b8465896c13a70a3e9eed6a5d96d050ed Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Wed, 7 Jun 2017 23:24:46 +0200 Subject: [PATCH] Updated JWT library, add supports for claim exp, iat, nbf, iss, aud . --- library/security/jwt/jwt-safe.ecf | 15 + library/security/jwt/jwt.ecf | 9 +- .../src/errors/jwt_claim_validation_error.e | 33 ++ .../security/jwt/src/errors/jwt_dev_error.e | 29 ++ .../jwt/src/errors/jwt_invalid_token_error.e | 21 ++ .../src/errors/jwt_unsupported_alg_error.e | 33 ++ .../src/errors/jwt_unverified_token_error.e | 21 ++ library/security/jwt/src/jws.e | 80 +++++ library/security/jwt/src/jwt.e | 290 +++++------------- library/security/jwt/src/jwt_claimset.e | 287 +++++++++++++++++ library/security/jwt/src/jwt_context.e | 50 +++ library/security/jwt/src/jwt_encoder.e | 276 +++++++++++++++++ library/security/jwt/src/jwt_error.e | 19 ++ library/security/jwt/src/jwt_header.e | 117 +++++++ library/security/jwt/src/jwt_loader.e | 97 ++++++ library/security/jwt/src/jwt_utilities.e | 112 +++++++ library/security/jwt/testing/test_jwt.e | 172 +++++++++-- library/security/jwt/testing/testing.ecf | 14 +- 18 files changed, 1429 insertions(+), 246 deletions(-) create mode 100644 library/security/jwt/jwt-safe.ecf create mode 100644 library/security/jwt/src/errors/jwt_claim_validation_error.e create mode 100644 library/security/jwt/src/errors/jwt_dev_error.e create mode 100644 library/security/jwt/src/errors/jwt_invalid_token_error.e create mode 100644 library/security/jwt/src/errors/jwt_unsupported_alg_error.e create mode 100644 library/security/jwt/src/errors/jwt_unverified_token_error.e create mode 100644 library/security/jwt/src/jws.e create mode 100644 library/security/jwt/src/jwt_claimset.e create mode 100644 library/security/jwt/src/jwt_context.e create mode 100644 library/security/jwt/src/jwt_encoder.e create mode 100644 library/security/jwt/src/jwt_error.e create mode 100644 library/security/jwt/src/jwt_header.e create mode 100644 library/security/jwt/src/jwt_loader.e create mode 100644 library/security/jwt/src/jwt_utilities.e diff --git a/library/security/jwt/jwt-safe.ecf b/library/security/jwt/jwt-safe.ecf new file mode 100644 index 00000000..f2d8c544 --- /dev/null +++ b/library/security/jwt/jwt-safe.ecf @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/library/security/jwt/jwt.ecf b/library/security/jwt/jwt.ecf index 66c4fe63..8deb9ee8 100644 --- a/library/security/jwt/jwt.ecf +++ b/library/security/jwt/jwt.ecf @@ -1,14 +1,15 @@ - + - - - + + + diff --git a/library/security/jwt/src/errors/jwt_claim_validation_error.e b/library/security/jwt/src/errors/jwt_claim_validation_error.e new file mode 100644 index 00000000..0b929be5 --- /dev/null +++ b/library/security/jwt/src/errors/jwt_claim_validation_error.e @@ -0,0 +1,33 @@ +note + description: "Summary description for {JWT_CLAIM_VALIDATION_ERROR}." + date: "$Date$" + revision: "$Revision$" + +class + JWT_CLAIM_VALIDATION_ERROR + +inherit + JWT_ERROR + +create + make + +feature {NONE} -- Initialization + + make (a_claim: READABLE_STRING_8) + do + claim_name := a_claim + end + +feature -- Access + + claim_name: READABLE_STRING_8 + + id: STRING = "CLAIM" + + message: READABLE_STRING_8 + do + Result := "Claim [" + claim_name + "] not validated!" + end + +end diff --git a/library/security/jwt/src/errors/jwt_dev_error.e b/library/security/jwt/src/errors/jwt_dev_error.e new file mode 100644 index 00000000..06878b85 --- /dev/null +++ b/library/security/jwt/src/errors/jwt_dev_error.e @@ -0,0 +1,29 @@ +note + description: "Summary description for {JWT_DEV_ERROR}." + date: "$Date$" + revision: "$Revision$" + +class + JWT_DEV_ERROR + +inherit + JWT_ERROR + +create + make + +feature {NONE} -- Initialization + + make (a_id: READABLE_STRING_8; msg: READABLE_STRING_8) + do + id := a_id + message := msg + end + +feature -- Access + + id: STRING + + message: READABLE_STRING_8 + +end diff --git a/library/security/jwt/src/errors/jwt_invalid_token_error.e b/library/security/jwt/src/errors/jwt_invalid_token_error.e new file mode 100644 index 00000000..5c5dcfec --- /dev/null +++ b/library/security/jwt/src/errors/jwt_invalid_token_error.e @@ -0,0 +1,21 @@ +note + description: "Summary description for {JWT_INVALID_TOKEN_ERROR}." + date: "$Date$" + revision: "$Revision$" + +class + JWT_INVALID_TOKEN_ERROR + +inherit + JWT_ERROR + +feature -- Access + + id: STRING = "INVALID" + + message: READABLE_STRING_8 + do + Result := "Invalid token" + end + +end diff --git a/library/security/jwt/src/errors/jwt_unsupported_alg_error.e b/library/security/jwt/src/errors/jwt_unsupported_alg_error.e new file mode 100644 index 00000000..147d727c --- /dev/null +++ b/library/security/jwt/src/errors/jwt_unsupported_alg_error.e @@ -0,0 +1,33 @@ +note + description: "Summary description for {JWT_UNSUPPORTED_ALG_ERROR}." + date: "$Date$" + revision: "$Revision$" + +class + JWT_UNSUPPORTED_ALG_ERROR + +inherit + JWT_ERROR + +create + make + +feature {NONE} -- Initialization + + make (a_alg: READABLE_STRING_8) + do + alg := a_alg + end + +feature -- Access + + alg: READABLE_STRING_8 + + id: STRING = "ALG" + + message: READABLE_STRING_8 + do + Result := "Unsupported alg [" + alg + "]" + end + +end diff --git a/library/security/jwt/src/errors/jwt_unverified_token_error.e b/library/security/jwt/src/errors/jwt_unverified_token_error.e new file mode 100644 index 00000000..364faa34 --- /dev/null +++ b/library/security/jwt/src/errors/jwt_unverified_token_error.e @@ -0,0 +1,21 @@ +note + description: "Summary description for {JWT_UNVERIFIED_TOKEN_ERROR}." + date: "$Date$" + revision: "$Revision$" + +class + JWT_UNVERIFIED_TOKEN_ERROR + +inherit + JWT_ERROR + +feature -- Access + + id: STRING = "UNVERIFIED" + + message: READABLE_STRING_8 + do + Result := "Unverified token" + end + +end diff --git a/library/security/jwt/src/jws.e b/library/security/jwt/src/jws.e new file mode 100644 index 00000000..37067915 --- /dev/null +++ b/library/security/jwt/src/jws.e @@ -0,0 +1,80 @@ +note + description: "Summary description for {JWS}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + JWS + +inherit + JWT + + JWT_UTILITIES + undefine + default_create + end + +create + default_create, + make_with_claims, + make_with_json_payload + +feature {NONE} -- Initialization + + make_with_claims (tb: STRING_TABLE [READABLE_STRING_GENERAL]) + do + default_create + across + tb as ic + loop + claimset.set_claim (ic.key, ic.item) + end + end + + make_with_json_payload (a_json: READABLE_STRING_8) + do + default_create + claimset.import_json (a_json) + end + +feature -- Access + + algorithm: READABLE_STRING_8 + do + Result := header.algorithm + end + +feature -- Conversion + + encoded_string (a_secret: READABLE_STRING_8): STRING + local + alg, sign: READABLE_STRING_8 + 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 ... + end + l_enc_header := base64url_encode (header.string) + l_enc_payload := base64url_encode (claimset.string) + sign := signature (l_enc_header, l_enc_payload, a_secret, alg) + + create Result.make (l_enc_header.count + 1 + l_enc_payload.count + 1 + sign.count) + Result.append (l_enc_header) + Result.append_character ('.') + Result.append (l_enc_payload) + Result.append_character ('.') + Result.append (sign) + end + +feature -- Element change + + set_algorithm (alg: detachable READABLE_STRING_8) + do + header.set_algorithm (alg) + end + +end diff --git a/library/security/jwt/src/jwt.e b/library/security/jwt/src/jwt.e index 147160c8..381afc9f 100644 --- a/library/security/jwt/src/jwt.e +++ b/library/security/jwt/src/jwt.e @@ -3,263 +3,141 @@ note date: "$Date$" revision: "$Revision$" -class +deferred class JWT -feature -- Initialization - - encoded_string (a_payload: READABLE_STRING_8; a_secret: READABLE_STRING_8; a_algo: READABLE_STRING_8): STRING - local - alg, sign: STRING_8 - l_enc_payload, l_enc_header: READABLE_STRING_8 - do - reset_error - if a_algo.is_case_insensitive_equal_general (alg_hs256) then - alg := alg_hs256 - elseif a_algo.is_case_insensitive_equal_general (alg_none) then - alg := alg_none - else - report_unsupported_alg_error (a_algo) - alg := alg_hs256 -- Default ... - end - l_enc_header := base64url_encode (header ("JWT", alg)) - l_enc_payload := base64url_encode (a_payload) - sign := signature (l_enc_header, l_enc_payload, a_secret, alg) - create Result.make (l_enc_header.count + 1 + l_enc_payload.count + 1 + sign.count) - Result.append (l_enc_header) - Result.append_character ('.') - Result.append (l_enc_payload) - Result.append_character ('.') - Result.append (sign) +inherit + ANY + redefine + default_create end - decoded_string (a_token: READABLE_STRING_8; a_secret: READABLE_STRING_8; a_algo: detachable READABLE_STRING_8): detachable STRING - local - i,j,n: INTEGER - alg, l_enc_payload, l_enc_header, l_signature: READABLE_STRING_8 - do - reset_error - n := a_token.count - i := a_token.index_of ('.', 1) - if i > 0 then - j := a_token.index_of ('.', i + 1) - if j > 0 then - l_enc_header := a_token.substring (1, i - 1) - l_enc_payload := a_token.substring (i + 1, j - 1) - l_signature := a_token.substring (j + 1, n) - Result := base64url_decode (l_enc_payload) - alg := a_algo - if alg = Void then - alg := signature_algorithm_from_encoded_header (l_enc_header) - if alg = Void then - -- Use default - alg := alg_hs256 - end - end - check alg_set: alg /= Void end - if alg.is_case_insensitive_equal (alg_hs256) then - alg := alg_hs256 - elseif alg.is_case_insensitive_equal (alg_none) then - alg := alg_none - else - alg := alg_hs256 - report_unsupported_alg_error (alg) - end +feature {NONE} -- Initialization - if not l_signature.same_string (signature (l_enc_header, l_enc_payload, a_secret, alg)) then - report_unverified_token_error - end - else - report_invalid_token - end - else - report_invalid_token - end + default_create + do + create header + create claimset end +feature -- Access + + header: JWT_HEADER + + claimset: JWT_CLAIMSET + feature -- Status report - supported_signature_algorithms: LIST [READABLE_STRING_8] - -- Supported signature algorithm `alg`? + is_expired (dt: detachable DATE_TIME): BOOLEAN + -- Is Current token expired? + -- See "exp" claim. do - create {ARRAYED_LIST [READABLE_STRING_8]} Result.make (2) - Result.extend (alg_hs256) - Result.extend (alg_none) + if attached claimset.expiration_time as l_exp_time then + if dt /= Void then + Result := dt > l_exp_time + else + Result := (create {DATE_TIME}.make_now_utc) > l_exp_time + end + end end - is_supporting_signature_algorithm (alg: READABLE_STRING_8): BOOLEAN - -- Is supporting signature algorithm `alg`? + is_nbf_validated (dt: detachable DATE_TIME): BOOLEAN + -- Does `dt` or now verify the "nbf" claim? + -- See "nbf" claim. do - Result := alg.is_case_insensitive_equal (alg_hs256) or - alg.is_case_insensitive_equal (alg_none) + Result := True + if attached claimset.not_before_time as l_time then + if dt /= Void then + Result := dt >= l_time + else + Result := (create {DATE_TIME}.make_now_utc) >= l_time + end + end end - error_code: INTEGER - -- Last error, if any. + is_iss_validated (a_issuer: detachable READABLE_STRING_8): BOOLEAN + do + if attached claimset.issuer as iss then + Result := a_issuer = Void or else a_issuer.same_string (iss) + end + end + + is_aud_validated (a_audience: detachable READABLE_STRING_8): BOOLEAN + do + if attached claimset.audience as aud then + Result := a_audience = Void or else a_audience.same_string (aud) + end + end + +feature -- Conversion + + encoded_string (a_secret: READABLE_STRING_8): STRING + deferred + end + +feature -- status report has_error: BOOLEAN - -- Last `encoded_string` reported an error? do - Result := error_code /= 0 + Result := attached errors as errs and then not errs.is_empty end has_unsupported_alg_error: BOOLEAN do - Result := error_code = unsupported_alg_error + Result := attached errors as errs and then across errs as ic some attached {JWT_UNSUPPORTED_ALG_ERROR} ic.item end end has_unverified_token_error: BOOLEAN do - Result := error_code = unverified_token_error + Result := attached errors as errs and then across errs as ic some attached {JWT_UNVERIFIED_TOKEN_ERROR} ic.item end end has_invalid_token_error: BOOLEAN do - Result := error_code = invalid_token_error + Result := attached errors as errs and then across errs as ic some attached {JWT_INVALID_TOKEN_ERROR} ic.item end end -feature -- Error reporting + errors: detachable ARRAYED_LIST [JWT_ERROR] + +feature {JWT_UTILITIES} -- Error reporting reset_error do - error_code := 0 + errors := Void + end + + report_error (err: JWT_ERROR) + local + l_errors: like errors + do + l_errors := errors + if l_errors = Void then + create l_errors.make (1) + errors := l_errors + end + l_errors.extend (err) end report_unsupported_alg_error (alg: READABLE_STRING_8) do - error_code := unsupported_alg_error + report_error (create {JWT_UNSUPPORTED_ALG_ERROR}.make (alg)) end report_unverified_token_error do - error_code := unverified_token_error + report_error (create {JWT_UNVERIFIED_TOKEN_ERROR}) end report_invalid_token do - error_code := invalid_token_error + report_error (create {JWT_INVALID_TOKEN_ERROR}) end -feature {NONE} -- Constants - - unsupported_alg_error: INTEGER = -2 - - unverified_token_error: INTEGER = -4 - - invalid_token_error: INTEGER = -8 - - alg_hs256: STRING = "HS256" - -- HMAC SHA256. - - alg_none: STRING = "none" - -- for unsecured token. - -feature -- Conversion - - header (a_type: detachable READABLE_STRING_8; alg: READABLE_STRING_8): STRING + report_claim_validation_error (a_claimname: READABLE_STRING_8) do - create Result.make_empty - Result.append ("{%"typ%":%"") - if a_type /= Void then - Result.append (a_type) - else - Result.append ("JWT") - end - Result.append ("%",%"alg%":%"") - Result.append (alg) - Result.append ("%"}") + report_error (create {JWT_CLAIM_VALIDATION_ERROR}.make (a_claimname)) end - signature_algorithm_from_encoded_header (a_enc_header: READABLE_STRING_8): detachable STRING_8 - local - jp: JSON_PARSER - do - create jp.make_with_string (base64url_decode (a_enc_header)) - jp.parse_content - if - attached jp.parsed_json_object as jo and then - attached {JSON_STRING} jo.item ("alg") as j_alg - then - Result := j_alg.unescaped_string_8 - end - end - -feature -- Implementation - - base64url_encode (s: READABLE_STRING_8): STRING_8 - local - urlencoder: URL_ENCODER - base64: BASE64 - do - create urlencoder - create base64 - Result := urlsafe_encode (base64.encoded_string (s)) - end - -feature {NONE} -- Implementation - - signature (a_enc_header, a_enc_payload: READABLE_STRING_8; a_secret: READABLE_STRING_8; alg: READABLE_STRING_8): STRING_8 - local - s: STRING - do - if alg = alg_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 = alg_hs256 then - Result := base64_hmacsha256 (s, a_secret) - else - Result := base64_hmacsha256 (s, a_secret) - end - Result := urlsafe_encode (Result) - end - end - - base64url_decode (s: READABLE_STRING_8): STRING_8 - local - urlencoder: URL_ENCODER - base64: BASE64 - do - create urlencoder - create base64 - Result := base64.decoded_string (urlsafe_decode (s)) - end - - urlsafe_encode (s: READABLE_STRING_8): STRING_8 - do - create Result.make_from_string (s) - Result.replace_substring_all ("=", "") - Result.replace_substring_all ("+", "-") - Result.replace_substring_all ("/", "_") - end - - urlsafe_decode (s: READABLE_STRING_8): STRING_8 - local - i: INTEGER - do - create Result.make_from_string (s) - Result.replace_substring_all ("-", "+") - Result.replace_substring_all ("_", "/") - from - i := Result.count \\ 4 - until - i = 0 - loop - i := i - 1 - Result.extend ('=') - 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) - Result := hs256.base64_digest --lowercase_hexadecimal_string_digest - end +invariant end diff --git a/library/security/jwt/src/jwt_claimset.e b/library/security/jwt/src/jwt_claimset.e new file mode 100644 index 00000000..962a0729 --- /dev/null +++ b/library/security/jwt/src/jwt_claimset.e @@ -0,0 +1,287 @@ +note + description: "Summary description for {JWT_CLAIMSET}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + JWT_CLAIMSET + +inherit + ANY + redefine + default_create + end + +create + default_create + +convert + string: {READABLE_STRING_8, STRING_8} + +feature {NONE} -- Initialization + + default_create + do + create json.make_empty + end + +feature -- Element change + + import_json (j: READABLE_STRING_8) + local + jp: JSON_PARSER + do + create jp.make_with_string (j) + jp.parse_content + if jp.is_valid and then attached jp.parsed_json_object as jo then + across + jo as ic + loop + json.put (ic.item, ic.key) + end + end + end + +feature -- Access + + claim alias "[]" (a_name: READABLE_STRING_GENERAL): detachable ANY + do + if attached json.item (a_name) as jv then + if attached {JSON_STRING} jv as js then + Result := js.unescaped_string_32 + elseif attached {JSON_BOOLEAN} jv as jb then + Result := jb.item + elseif attached {JSON_NUMBER} jv as jnum then + if jnum.is_integer then + Result := jnum.integer_64_item + elseif jnum.is_natural then + Result := jnum.natural_64_item + elseif jnum.is_real then + Result := jnum.real_64_item + else + Result := jnum.item + end + end + end + end + + string_32_claim (a_name: READABLE_STRING_GENERAL): detachable READABLE_STRING_32 + do + if attached json.item (a_name) as jv then + if attached {JSON_STRING} jv as js then + Result := js.unescaped_string_32 + elseif attached {JSON_BOOLEAN} jv as jb then + Result := jb.item.out + elseif attached {JSON_NUMBER} jv as jnum then + Result := jnum.item + end + end + end + + string_8_claim (a_name: READABLE_STRING_GENERAL): detachable READABLE_STRING_8 + do + if attached json.item (a_name) as jv then + if attached {JSON_STRING} jv as js then + Result := js.unescaped_string_8 + elseif attached {JSON_BOOLEAN} jv as jb then + Result := jb.item.out + elseif attached {JSON_NUMBER} jv as jnum then + Result := jnum.item + end + end + end + + issuer: detachable READABLE_STRING_8 assign set_issuer + do + Result := string_8_claim ("iss") + end + + subjet: detachable READABLE_STRING_32 assign set_subject + do + Result := string_32_claim ("sub") + end + + audience: detachable READABLE_STRING_8 assign set_audience + do + Result := string_8_claim ("aud") + end + + expiration_time: detachable DATE_TIME assign set_expiration_time + do + if attached {INTEGER_64} claim ("exp") as i64 then + Result := numeric_date_value_to_datetime (i64) + end + end + + not_before_time: detachable DATE_TIME assign set_not_before_time + do + if attached {INTEGER_64} claim ("nbf") as i64 then + Result := numeric_date_value_to_datetime (i64) + end + end + + issued_at: detachable DATE_TIME assign set_issued_at + do + if attached {INTEGER_64} claim ("iat") as i then + Result := numeric_date_value_to_datetime (i) + end + end + + jwd_id: detachable READABLE_STRING_8 assign set_jwt_id + do + Result := string_8_claim ("jti") + end + +feature -- Conversion + + json: JSON_OBJECT + + string: STRING + do + Result := json.representation + end + +feature -- Element change + + set_claim (a_name: READABLE_STRING_GENERAL; a_val: detachable ANY) + do + if a_val = Void then + json.remove (a_name) + elseif attached {READABLE_STRING_GENERAL} a_val as str then + json.put_string (str, a_name) + elseif attached {BOOLEAN} a_val as b then + json.put_boolean (b, a_name) + elseif attached {DATE_TIME} a_val as dt then + json.put_integer (datetime_to_numeric_date_value (dt), a_name) + elseif attached {DATE} a_val as d then + json.put_integer (datetime_to_numeric_date_value (create {DATE_TIME}.make_by_date (d)), a_name) + elseif attached {NUMERIC} a_val as num then + if attached {INTEGER_64} num as i64 then + json.put_integer (i64, a_name) + elseif attached {INTEGER_32} num as i32 then + json.put_integer (i32.to_integer_64, a_name) + elseif attached {NATURAL_64} num as n64 then + json.put_natural (n64, a_name) + elseif attached {INTEGER_32} num as n32 then + json.put_natural (n32.to_natural_64, a_name) + elseif attached {REAL_64} num as r64 then + json.put_real (r64, a_name) + elseif attached {REAL_32} num as r32 then + json.put_real (r32, a_name) + else + json.put_string (a_val.out, a_name) + end + else + json.put_string (a_val.out, a_name) + end + end + + set_issuer (iss: detachable READABLE_STRING_8) + -- The "iss" (issuer) claim identifies the principal that issued the + -- JWT. The processing of this claim is generally application specific. + -- The "iss" value is a case-sensitive string containing a StringOrURI + -- value. Use of this claim is OPTIONAL. + do + set_claim ("iss", iss) + end + + set_subject (sub: detachable READABLE_STRING_32) + -- The "sub" (subject) claim identifies the principal that is the + -- subject of the JWT. The claims in a JWT are normally statements + -- about the subject. The subject value MUST either be scoped to be + -- locally unique in the context of the issuer or be globally unique. + -- The processing of this claim is generally application specific. The + -- "sub" value is a case-sensitive string containing a StringOrURI + -- value. Use of this claim is OPTIONAL. + do + set_claim ("sub", sub) + end + + set_audience (aud: detachable READABLE_STRING_8) + -- The "aud" (audience) claim identifies the recipients that the JWT is + -- intended for. Each principal intended to process the JWT MUST + -- identify itself with a value in the audience claim. If the principal + -- processing the claim does not identify itself with a value in the + -- "aud" claim when this claim is present, then the JWT MUST be + -- rejected. In the general case, the "aud" value is an array of case- + -- sensitive strings, each containing a StringOrURI value. In the + -- special case when the JWT has one audience, the "aud" value MAY be a + -- single case-sensitive string containing a StringOrURI value. The + -- interpretation of audience values is generally application specific. + -- Use of this claim is OPTIONAL. + do + set_claim ("aud", aud) + end + + set_expiration_time (exp: detachable DATE_TIME) + -- The "exp" (expiration time) claim identifies the expiration time on + -- or after which the JWT MUST NOT be accepted for processing. The + -- processing of the "exp" claim requires that the current date/time + -- MUST be before the expiration date/time listed in the "exp" claim. + -- Implementers MAY provide for some small leeway, usually no more than + -- a few minutes, to account for clock skew. Its value MUST be a number + -- containing a NumericDate value. Use of this claim is OPTIONAL. + do + if exp = Void then + set_claim ("exp", Void) + else + set_claim ("exp", datetime_to_numeric_date_value (exp)) + end + end + + set_not_before_time (nbf: detachable DATE_TIME) + -- The "nbf" (not before) claim identifies the time before which the JWT + -- MUST NOT be accepted for processing. The processing of the "nbf" + -- claim requires that the current date/time MUST be after or equal to + -- the not-before date/time listed in the "nbf" claim. Implementers MAY + -- provide for some small leeway, usually no more than a few minutes, to + -- account for clock skew. Its value MUST be a number containing a + -- NumericDate value. Use of this claim is OPTIONAL. + do + if nbf = Void then + set_claim ("nbf", Void) + else + set_claim ("nbf", datetime_to_numeric_date_value (nbf)) + end + end + + set_issued_at (iat: detachable DATE_TIME) + -- The "iat" (issued at) claim identifies the time at which the JWT was + -- issued. This claim can be used to determine the age of the JWT. Its + -- value MUST be a number containing a NumericDate value. Use of this + -- claim is OPTIONAL. + do + if iat = Void then + set_claim ("iat", Void) + else + set_claim ("iat", datetime_to_numeric_date_value (iat)) + end + end + + set_jwt_id (jti: detachable READABLE_STRING_8) + -- The "jti" (JWT ID) claim provides a unique identifier for the JWT. + -- The identifier value MUST be assigned in a manner that ensures that + -- there is a negligible probability that the same value will be + -- accidentally assigned to a different data object; if the application + -- uses multiple issuers, collisions MUST be prevented among values + -- produced by different issuers as well. The "jti" claim can be used + -- to prevent the JWT from being replayed. The "jti" value is a case- + -- sensitive string. Use of this claim is OPTIONAL. + do + set_claim ("jti", jti) + end + +feature {NONE} -- Implementation + + numeric_date_value_to_datetime (v: INTEGER_64): DATE_TIME + do + create Result.make_from_epoch (v.to_integer_32) + end + + datetime_to_numeric_date_value (dt: DATE_TIME): INTEGER_64 + do + Result := dt.definite_duration (create {DATE_TIME}.make_from_epoch (0)).seconds_count + end + +end diff --git a/library/security/jwt/src/jwt_context.e b/library/security/jwt/src/jwt_context.e new file mode 100644 index 00000000..625f223b --- /dev/null +++ b/library/security/jwt/src/jwt_context.e @@ -0,0 +1,50 @@ +note + description: "Summary description for {JWT_CONTEXT}." + date: "$Date$" + revision: "$Revision$" + +class + JWT_CONTEXT + +feature -- Access + + time: detachable DATE_TIME + -- Date time to use for validation, if Void, use current date time. + + validation_ignored: BOOLEAN + -- Read claimset of JWT without performing validation of the signature + -- or any of the regustered claim names. + -- Warning: - Use this setting with care, only if you clearly understand + -- what you are doing. + -- - Without digital signature information, the integrity or authenticity + -- of the claimset cannot be trusted. + + issuer: detachable READABLE_STRING_8 + + audience: detachable READABLE_STRING_8 + +feature -- Element change + + ignore_validation (b: BOOLEAN) + -- If `b` then ignore validations. + do + validation_ignored := b + end + + set_time (dt: detachable DATE_TIME) + do + time := dt + end + + set_issuer (iss: like issuer) + do + issuer := iss + end + + set_audience (aud: like audience) + do + audience := aud + end + + +end diff --git a/library/security/jwt/src/jwt_encoder.e b/library/security/jwt/src/jwt_encoder.e new file mode 100644 index 00000000..0dffce81 --- /dev/null +++ b/library/security/jwt/src/jwt_encoder.e @@ -0,0 +1,276 @@ +note + description: "JSON Web Token encoder" + date: "$Date$" + revision: "$Revision$" + +class + JWT_ENCODER + +feature -- Basic operations + + encoded_values (a_values: STRING_TABLE [READABLE_STRING_GENERAL]; a_secret: READABLE_STRING_8; a_algo: READABLE_STRING_8): STRING + local + j: JSON_OBJECT + do + create j.make_with_capacity (a_values.count) + across + a_values as ic + loop + j.put_string (ic.item, ic.key) + end + Result := encoded_json (j, a_secret, a_algo) + end + + encoded_json (a_json: JSON_OBJECT; a_secret: READABLE_STRING_8; a_algo: READABLE_STRING_8): STRING + local + vis: JSON_PRETTY_STRING_VISITOR + s: STRING + do + create s.make_empty + create vis.make (s) + vis.visit_json_object (a_json) + Result := encoded_string (s, a_secret, a_algo) + end + + encoded_string (a_payload: READABLE_STRING_8; a_secret: READABLE_STRING_8; a_algo: READABLE_STRING_8): STRING + local + alg, sign: STRING_8 + l_enc_payload, l_enc_header: READABLE_STRING_8 + do + reset_error + if a_algo.is_case_insensitive_equal_general (alg_hs256) then + alg := alg_hs256 + elseif a_algo.is_case_insensitive_equal_general (alg_none) then + alg := alg_none + else + report_unsupported_alg_error (a_algo) + alg := alg_hs256 -- Default ... + end + l_enc_header := base64url_encode (header ("JWT", alg)) + l_enc_payload := base64url_encode (a_payload) + sign := signature (l_enc_header, l_enc_payload, a_secret, alg) + create Result.make (l_enc_header.count + 1 + l_enc_payload.count + 1 + sign.count) + Result.append (l_enc_header) + Result.append_character ('.') + Result.append (l_enc_payload) + Result.append_character ('.') + Result.append (sign) + end + + decoded_string (a_token: READABLE_STRING_8; a_secret: READABLE_STRING_8; a_algo: detachable READABLE_STRING_8): detachable STRING + local + i,j,n: INTEGER + alg, l_enc_payload, l_enc_header, l_signature: READABLE_STRING_8 + do + reset_error + n := a_token.count + i := a_token.index_of ('.', 1) + if i > 0 then + j := a_token.index_of ('.', i + 1) + if j > 0 then + l_enc_header := a_token.substring (1, i - 1) + l_enc_payload := a_token.substring (i + 1, j - 1) + l_signature := a_token.substring (j + 1, n) + Result := base64url_decode (l_enc_payload) + alg := a_algo + if alg = Void then + alg := signature_algorithm_from_encoded_header (l_enc_header) + if alg = Void then + -- Use default + alg := alg_hs256 + end + end + check alg_set: alg /= Void end + if alg.is_case_insensitive_equal (alg_hs256) then + alg := alg_hs256 + elseif alg.is_case_insensitive_equal (alg_none) then + alg := alg_none + else + alg := alg_hs256 + report_unsupported_alg_error (alg) + end + + if not l_signature.same_string (signature (l_enc_header, l_enc_payload, a_secret, alg)) then + report_unverified_token_error + end + else + report_invalid_token + end + else + report_invalid_token + end + end + +feature -- Error status + + error_code: INTEGER + -- Last error, if any. + + has_error: BOOLEAN + -- Last `encoded_string` reported an error? + do + Result := error_code /= 0 + end + + has_unsupported_alg_error: BOOLEAN + do + Result := error_code = unsupported_alg_error + end + + has_unverified_token_error: BOOLEAN + do + Result := error_code = unverified_token_error + end + + has_invalid_token_error: BOOLEAN + do + Result := error_code = invalid_token_error + end + +feature {NONE} -- Error reporting + + reset_error + do + error_code := 0 + end + + report_unsupported_alg_error (alg: READABLE_STRING_8) + do + error_code := unsupported_alg_error + end + + report_unverified_token_error + do + error_code := unverified_token_error + end + + report_invalid_token + do + error_code := invalid_token_error + end + +feature {NONE} -- Constants + + unsupported_alg_error: INTEGER = -2 + + unverified_token_error: INTEGER = -4 + + invalid_token_error: INTEGER = -8 + + alg_hs256: STRING = "HS256" + -- HMAC SHA256. + + alg_none: STRING = "none" + -- for unsecured token. + +feature -- Conversion + + header (a_type: detachable READABLE_STRING_8; alg: READABLE_STRING_8): STRING + do + create Result.make_empty + Result.append ("{%"typ%":%"") + if a_type /= Void then + Result.append (a_type) + else + Result.append ("JWT") + end + Result.append ("%",%"alg%":%"") + Result.append (alg) + Result.append ("%"}") + end + +feature {NONE} -- Conversion + + signature_algorithm_from_encoded_header (a_enc_header: READABLE_STRING_8): detachable STRING_8 + local + jp: JSON_PARSER + do + create jp.make_with_string (base64url_decode (a_enc_header)) + jp.parse_content + if + attached jp.parsed_json_object as jo and then + attached {JSON_STRING} jo.item ("alg") as j_alg + then + Result := j_alg.unescaped_string_8 + end + end + +feature -- Base64 + + base64url_encode (s: READABLE_STRING_8): STRING_8 + local + urlencoder: URL_ENCODER + base64: BASE64 + do + create urlencoder + create base64 + Result := urlsafe_encode (base64.encoded_string (s)) + end + +feature {NONE} -- Implementation + + signature (a_enc_header, a_enc_payload: READABLE_STRING_8; a_secret: READABLE_STRING_8; alg: READABLE_STRING_8): STRING_8 + local + s: STRING + do + if alg = alg_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 = alg_hs256 then + Result := base64_hmacsha256 (s, a_secret) + else + Result := base64_hmacsha256 (s, a_secret) + end + Result := urlsafe_encode (Result) + end + end + + base64url_decode (s: READABLE_STRING_8): STRING_8 + local + urlencoder: URL_ENCODER + base64: BASE64 + do + create urlencoder + create base64 + Result := base64.decoded_string (urlsafe_decode (s)) + end + + urlsafe_encode (s: READABLE_STRING_8): STRING_8 + do + create Result.make_from_string (s) + Result.replace_substring_all ("=", "") + Result.replace_substring_all ("+", "-") + Result.replace_substring_all ("/", "_") + end + + urlsafe_decode (s: READABLE_STRING_8): STRING_8 + local + i: INTEGER + do + create Result.make_from_string (s) + Result.replace_substring_all ("-", "+") + Result.replace_substring_all ("_", "/") + from + i := Result.count \\ 4 + until + i = 0 + loop + i := i - 1 + Result.extend ('=') + 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) + Result := hs256.base64_digest --lowercase_hexadecimal_string_digest + end + +end diff --git a/library/security/jwt/src/jwt_error.e b/library/security/jwt/src/jwt_error.e new file mode 100644 index 00000000..28f0ddbe --- /dev/null +++ b/library/security/jwt/src/jwt_error.e @@ -0,0 +1,19 @@ +note + description: "Summary description for {JWT_ERROR}." + date: "$Date$" + revision: "$Revision$" + +deferred class + JWT_ERROR + +feature -- Access + + id: STRING + deferred + end + + message: READABLE_STRING_8 + deferred + end + +end diff --git a/library/security/jwt/src/jwt_header.e b/library/security/jwt/src/jwt_header.e new file mode 100644 index 00000000..0d85990a --- /dev/null +++ b/library/security/jwt/src/jwt_header.e @@ -0,0 +1,117 @@ +note + description: "[ + JOSE Header + + See https://tools.ietf.org/html/rfc7515 + ]" + date: "$Date$" + revision: "$Revision$" + +class + JWT_HEADER + +inherit + ANY + redefine + default_create + end + +create + default_create, + make_from_json + +convert + string: {READABLE_STRING_8, STRING_8} + +feature {NONE} -- Initialization + + default_create + do + type := "JWT" + algorithm := "HS256" + end + + make_from_json (a_json: READABLE_STRING_8) + do + default_create + import_json (a_json) + end + +feature -- Access + + type: READABLE_STRING_8 + -- Token type (typ) - If present, it is recommended to set this to "JWT". + + content_type: detachable READABLE_STRING_8 + -- Content type (cty) + -- If nested signing or encryption is employed, it is recommended to set this to JWT, + -- otherwise omit this field. + + algorithm: READABLE_STRING_8 + -- Message authentication code algorithm (alg) + -- The issuer can freely set an algorithm to verify the signature on the token. + -- However, some supported algorithms are insecure. + +feature -- Conversion + + string: STRING + do + create Result.make_empty + Result.append ("{%"typ%":%"") + Result.append (type) + Result.append ("%"") + if attached content_type as cty then + Result.append (",%"cty%":%"") + Result.append (cty) + Result.append ("%"") + end + Result.append (",%"alg%":%"") + Result.append (algorithm) + Result.append ("%"}") + end + +feature -- Element change + + set_type (typ: READABLE_STRING_8) + do + type := typ + end + + set_content_type (cty: detachable READABLE_STRING_8) + do + content_type := cty + end + + set_algorithm (alg: detachable READABLE_STRING_8) + do + if alg = Void then + algorithm := "none" + else + algorithm := alg + end + end + +feature -- Element change + + import_json (a_json: READABLE_STRING_8) + local + jp: JSON_PARSER + do + create jp.make_with_string (a_json) + jp.parse_content + if + attached jp.parsed_json_object as jo + then + if attached {JSON_STRING} jo.item ("typ") as j_typ then + set_type (j_typ.unescaped_string_8) + end + if attached {JSON_STRING} jo.item ("cty") as j_cty then + set_content_type (j_cty.unescaped_string_8) + end + if attached {JSON_STRING} jo.item ("alg") as j_alg then + set_algorithm (j_alg.unescaped_string_8) + end + end + end + +end diff --git a/library/security/jwt/src/jwt_loader.e b/library/security/jwt/src/jwt_loader.e new file mode 100644 index 00000000..69df2beb --- /dev/null +++ b/library/security/jwt/src/jwt_loader.e @@ -0,0 +1,97 @@ +note + description: "Summary description for {JWT_LOADER}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + JWT_LOADER + +inherit + JWT_UTILITIES + +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` + -- used to specify eventual issuer and various parameters. + local + jws: JWS + i,j,n: INTEGER + alg, l_enc_payload, l_enc_header, l_signature: READABLE_STRING_8 + do + n := a_token_input.count + i := a_token_input.index_of ('.', 1) + if i > 0 then + j := a_token_input.index_of ('.', i + 1) + if j > 0 then + l_enc_header := a_token_input.substring (1, i - 1) + 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 + end + 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 + jws.report_unverified_token_error + end + if + not jws.has_error and then + ctx /= Void + then + check not ctx.validation_ignored end + if jws.is_expired (ctx.time) then + jws.report_claim_validation_error ("exp") + end + if not jws.is_nbf_validated (ctx.time) then + jws.report_claim_validation_error ("nbf") + end + if + not jws.is_iss_validated (ctx.issuer) + then + jws.report_claim_validation_error ("iss") + end + if + not jws.is_aud_validated (ctx.audience) + then + jws.report_claim_validation_error ("aud") + end + + end + end + else + -- jws.report_invalid_token + end + else + -- jws.report_invalid_token + end + Result := jws + end + +feature {NONE} -- Implementation + + signature_algorithm_from_encoded_header (a_enc_header: READABLE_STRING_8): detachable STRING_8 + local + jp: JSON_PARSER + do + create jp.make_with_string (base64url_decode (a_enc_header)) + jp.parse_content + if + attached jp.parsed_json_object as jo and then + attached {JSON_STRING} jo.item ("alg") as j_alg + then + Result := j_alg.unescaped_string_8 + end + end + +end diff --git a/library/security/jwt/src/jwt_utilities.e b/library/security/jwt/src/jwt_utilities.e new file mode 100644 index 00000000..e7bf4547 --- /dev/null +++ b/library/security/jwt/src/jwt_utilities.e @@ -0,0 +1,112 @@ +note + description: "Summary description for {JWT_UTILITIES}." + author: "" + date: "$Date$" + revision: "$Revision$" + +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 + local + urlencoder: URL_ENCODER + base64: BASE64 + do + create urlencoder + create base64 + Result := urlsafe_encode (base64.encoded_string (s)) + end + + urlsafe_encode (s: READABLE_STRING_8): STRING_8 + do + create Result.make_from_string (s) + Result.replace_substring_all ("=", "") + Result.replace_substring_all ("+", "-") + 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 + local + s: STRING + do + if alg.is_case_insensitive_equal (alg_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) + 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) + Result := hs256.base64_digest --lowercase_hexadecimal_string_digest + end + +feature -- Decoding + + base64url_decode (s: READABLE_STRING_8): STRING_8 + local + urlencoder: URL_ENCODER + base64: BASE64 + do + create urlencoder + create base64 + Result := base64.decoded_string (urlsafe_decode (s)) + end + + urlsafe_decode (s: READABLE_STRING_8): STRING_8 + local + i: INTEGER + do + create Result.make_from_string (s) + Result.replace_substring_all ("-", "+") + Result.replace_substring_all ("_", "/") + from + i := Result.count \\ 4 + until + i = 0 + loop + i := i - 1 + Result.extend ('=') + 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/test_jwt.e b/library/security/jwt/testing/test_jwt.e index 2b269762..4642458b 100644 --- a/library/security/jwt/testing/test_jwt.e +++ b/library/security/jwt/testing/test_jwt.e @@ -9,71 +9,185 @@ class inherit EQA_TEST_SET + SHARED_EXECUTION_ENVIRONMENT + undefine + default_create + end + feature -- Test test_jwt_io local - jwt: JWT - header: STRING - payload: STRING + jwt: JWS + ut: JWT_UTILITIES do - payload := "[ - {"sub":"1234567890","name":"John Doe","admin":true} - ]" - payload.adjust - payload.replace_substring_all ("%N", "%R%N") - create jwt + jwt.set_algorithm ("HS256") + jwt.claimset.set_subject ("1234567890") + jwt.claimset.set_claim ("name", "John Doe") + jwt.claimset.set_claim ("admin", True) + create ut - assert ("header", jwt.base64url_encode (jwt.header (Void, "HS256")).same_string ("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9")) - assert ("payload", jwt.base64url_encode (payload).same_string ("eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9")) - assert ("signature", jwt.encoded_string (payload, "secret", "HS256").same_string ("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.pcHcZspUvuiqIPVB_i_qmcvCJv63KLUgIAKIlXI1gY8")) + assert ("header", ut.base64url_encode (jwt.header.string).same_string ("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9")) + assert ("payload", ut.base64url_encode (jwt.claimset.string).same_string ("eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9")) + assert ("signature", jwt.encoded_string ("secret").same_string ("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.pcHcZspUvuiqIPVB_i_qmcvCJv63KLUgIAKIlXI1gY8")) end test_jwt local - jwt: JWT + jwt: JWS + jwt_loader: JWT_LOADER payload: STRING tok: STRING do payload := "[ - {"iss":"joe", - "exp":1300819380, - "http://example.com/is_root":true} + {"iss":"joe","exp":1300819380,"http://example.com/is_root":true} ]" -- payload := "[ -- {"sub":"1234567890","name":"John Doe","admin":true} -- ]" - create jwt - tok := jwt.encoded_string (payload, "secret", "HS256") + create jwt.make_with_json_payload (payload) + jwt.set_algorithm ("HS256") + tok := jwt.encoded_string ("secret") - if attached jwt.decoded_string (tok, "secret", Void) as l_tok_payload then - assert ("no error", not jwt.has_error) - assert ("same payload", l_tok_payload.same_string (payload)) + create jwt_loader + + if attached jwt_loader.token (tok, "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 + end + + test_jwt_with_claimset + local + jwt: JWS + jwt_loader: JWT_LOADER + payload: STRING + tok: STRING + now, dt: DATE_TIME + ctx: JWT_CONTEXT + do +-- payload := "[ +-- {"iss":"joe","exp":1300819380,"http://example.com/is_root":true} +-- ]" + + payload := "[ + {"sub":"1234567890","name":"John Doe","admin":true} + ]" + + create jwt.make_with_json_payload (payload) + jwt.set_algorithm ("HS256") + create now.make_now_utc + jwt.claimset.set_issued_at (now) + + dt := duplicated_time (now) + dt.minute_add (60) + jwt.claimset.set_expiration_time (dt) + + jwt.claimset.set_issuer ("urn:foo") + jwt.claimset.set_audience ("urn:foo") + + tok := jwt.encoded_string ("secret") + + payload := jwt.claimset.string + + create jwt_loader + + -- Test with validation + exp + if attached jwt_loader.token (tok, "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 + 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 + assert ("exp error", l_tok.has_error) + end + + -- Test with validation + not before + + dt := duplicated_time (now) + dt.second_add (30) + jwt.claimset.set_not_before_time (dt) + tok := jwt.encoded_string ("secret") + + ctx.set_time (now) + if attached jwt_loader.token (tok, "secret", ctx) as l_tok then + assert ("has nbf error", l_tok.has_error) + end + + dt := duplicated_time (now) + dt.second_add (15) + ctx.set_time (dt) + + if attached jwt_loader.token (tok, "secret", ctx) as l_tok then + assert ("has nbf error", l_tok.has_error) + end + + dt := duplicated_time (now) + dt.minute_add (45) + ctx.set_time (dt) + + if attached jwt_loader.token (tok, "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 + 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 + 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 + 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 + assert ("no error", not l_tok.has_error) end end test_unsecured_jwt local - jwt: JWT + jwt: JWS payload: STRING tok: STRING do payload := "[ - {"iss":"joe", - "exp":1300819380, - "http://example.com/is_root":true} + {"iss":"joe","exp":1300819380,"http://example.com/is_root":true} ]" - create jwt - tok := jwt.encoded_string (payload, "secret", "none") + create jwt.make_with_json_payload (payload) + jwt.set_algorithm ("none") + tok := jwt.encoded_string ("secret") - if attached jwt.decoded_string (tok, "secret", Void) as l_tok_payload then + if attached (create {JWT_LOADER}).token (tok, "secret", Void) as l_tok then assert ("no error", not jwt.has_error) - assert ("same payload", l_tok_payload.same_string (payload)) + assert ("same payload", l_tok.claimset.string.same_string (payload)) end end +feature -- Implementation + + duplicated_time (dt: DATE_TIME): DATE_TIME + do + Result := dt.deep_twin + end + end diff --git a/library/security/jwt/testing/testing.ecf b/library/security/jwt/testing/testing.ecf index 3358cbe3..7bad43cb 100644 --- a/library/security/jwt/testing/testing.ecf +++ b/library/security/jwt/testing/testing.ecf @@ -1,15 +1,15 @@ - + - - - - - + + + - + +