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 @@
-
+
-
-
-
-
-
+
+
+
-
+
+