Updated JWT library, add supports for claim exp, iat, nbf, iss, aud .

This commit is contained in:
Jocelyn Fiat
2017-06-07 23:24:46 +02:00
parent 40cbe7dfc9
commit 7e54825b84
18 changed files with 1429 additions and 246 deletions

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-15-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-15-0 http://www.eiffel.com/developers/xml/configuration-1-15-0.xsd" name="jwt" uuid="A75C2D84-D543-4708-BAF3-254C308376CC" library_target="jwt">
<target name="jwt">
<root all_classes="true"/>
<option warning="true" void_safety="all">
</option>
<setting name="concurrency" value="scoop"/>
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
<library name="crypto" location="$ISE_LIBRARY\unstable\library\text\encryption\crypto\crypto-safe.ecf"/>
<library name="encoder" location="$ISE_LIBRARY\contrib\library\web\framework\ewf\text\encoder\encoder-safe.ecf"/>
<library name="json" location="$ISE_LIBRARY\contrib\library\text\parser\json\library\json-safe.ecf"/>
<library name="time" location="$ISE_LIBRARY\library\time\time-safe.ecf"/>
<cluster name="src" location="src\" recursive="true"/>
</target>
</system>

View File

@@ -1,14 +1,15 @@
<?xml version="1.0" encoding="ISO-8859-1"?> <?xml version="1.0" encoding="ISO-8859-1"?>
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-16-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-16-0 http://www.eiffel.com/developers/xml/configuration-1-16-0.xsd" name="jwt" uuid="A75C2D84-D543-4708-BAF3-254C308376CC" library_target="jwt"> <system xmlns="http://www.eiffel.com/developers/xml/configuration-1-15-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-15-0 http://www.eiffel.com/developers/xml/configuration-1-15-0.xsd" name="jwt" uuid="A75C2D84-D543-4708-BAF3-254C308376CC" library_target="jwt">
<target name="jwt"> <target name="jwt">
<root all_classes="true"/> <root all_classes="true"/>
<capability> <option warning="true" void_safety="none">
<void_safety support="all"/> </option>
</capability> <setting name="concurrency" value="scoop"/>
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/> <library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
<library name="crypto" location="$ISE_LIBRARY\unstable\library\text\encryption\crypto\crypto.ecf"/> <library name="crypto" location="$ISE_LIBRARY\unstable\library\text\encryption\crypto\crypto.ecf"/>
<library name="encoder" location="$ISE_LIBRARY\contrib\library\web\framework\ewf\text\encoder\encoder.ecf"/> <library name="encoder" location="$ISE_LIBRARY\contrib\library\web\framework\ewf\text\encoder\encoder.ecf"/>
<library name="json" location="$ISE_LIBRARY\contrib\library\text\parser\json\library\json.ecf"/> <library name="json" location="$ISE_LIBRARY\contrib\library\text\parser\json\library\json.ecf"/>
<library name="time" location="$ISE_LIBRARY\library\time\time.ecf"/>
<cluster name="src" location="src\" recursive="true"/> <cluster name="src" location="src\" recursive="true"/>
</target> </target>
</system> </system>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,263 +3,141 @@ note
date: "$Date$" date: "$Date$"
revision: "$Revision$" revision: "$Revision$"
class deferred class
JWT JWT
feature -- Initialization inherit
ANY
encoded_string (a_payload: READABLE_STRING_8; a_secret: READABLE_STRING_8; a_algo: READABLE_STRING_8): STRING redefine
local default_create
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 end
decoded_string (a_token: READABLE_STRING_8; a_secret: READABLE_STRING_8; a_algo: detachable READABLE_STRING_8): detachable STRING feature {NONE} -- Initialization
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 default_create
report_unverified_token_error do
end create header
else create claimset
report_invalid_token
end
else
report_invalid_token
end
end end
feature -- Access
header: JWT_HEADER
claimset: JWT_CLAIMSET
feature -- Status report feature -- Status report
supported_signature_algorithms: LIST [READABLE_STRING_8] is_expired (dt: detachable DATE_TIME): BOOLEAN
-- Supported signature algorithm `alg`? -- Is Current token expired?
-- See "exp" claim.
do do
create {ARRAYED_LIST [READABLE_STRING_8]} Result.make (2) if attached claimset.expiration_time as l_exp_time then
Result.extend (alg_hs256) if dt /= Void then
Result.extend (alg_none) Result := dt > l_exp_time
else
Result := (create {DATE_TIME}.make_now_utc) > l_exp_time
end
end
end end
is_supporting_signature_algorithm (alg: READABLE_STRING_8): BOOLEAN is_nbf_validated (dt: detachable DATE_TIME): BOOLEAN
-- Is supporting signature algorithm `alg`? -- Does `dt` or now verify the "nbf" claim?
-- See "nbf" claim.
do do
Result := alg.is_case_insensitive_equal (alg_hs256) or Result := True
alg.is_case_insensitive_equal (alg_none) 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 end
error_code: INTEGER is_iss_validated (a_issuer: detachable READABLE_STRING_8): BOOLEAN
-- Last error, if any. 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 has_error: BOOLEAN
-- Last `encoded_string` reported an error?
do do
Result := error_code /= 0 Result := attached errors as errs and then not errs.is_empty
end end
has_unsupported_alg_error: BOOLEAN has_unsupported_alg_error: BOOLEAN
do 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 end
has_unverified_token_error: BOOLEAN has_unverified_token_error: BOOLEAN
do 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 end
has_invalid_token_error: BOOLEAN has_invalid_token_error: BOOLEAN
do 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 end
feature -- Error reporting errors: detachable ARRAYED_LIST [JWT_ERROR]
feature {JWT_UTILITIES} -- Error reporting
reset_error reset_error
do 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 end
report_unsupported_alg_error (alg: READABLE_STRING_8) report_unsupported_alg_error (alg: READABLE_STRING_8)
do do
error_code := unsupported_alg_error report_error (create {JWT_UNSUPPORTED_ALG_ERROR}.make (alg))
end end
report_unverified_token_error report_unverified_token_error
do do
error_code := unverified_token_error report_error (create {JWT_UNVERIFIED_TOKEN_ERROR})
end end
report_invalid_token report_invalid_token
do do
error_code := invalid_token_error report_error (create {JWT_INVALID_TOKEN_ERROR})
end end
feature {NONE} -- Constants report_claim_validation_error (a_claimname: READABLE_STRING_8)
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 do
create Result.make_empty report_error (create {JWT_CLAIM_VALIDATION_ERROR}.make (a_claimname))
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 end
signature_algorithm_from_encoded_header (a_enc_header: READABLE_STRING_8): detachable STRING_8 invariant
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
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,71 +9,185 @@ class
inherit inherit
EQA_TEST_SET EQA_TEST_SET
SHARED_EXECUTION_ENVIRONMENT
undefine
default_create
end
feature -- Test feature -- Test
test_jwt_io test_jwt_io
local local
jwt: JWT jwt: JWS
header: STRING ut: JWT_UTILITIES
payload: STRING
do do
payload := "[
{"sub":"1234567890","name":"John Doe","admin":true}
]"
payload.adjust
payload.replace_substring_all ("%N", "%R%N")
create jwt 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 ("header", ut.base64url_encode (jwt.header.string).same_string ("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"))
assert ("payload", jwt.base64url_encode (payload).same_string ("eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9")) assert ("payload", ut.base64url_encode (jwt.claimset.string).same_string ("eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9"))
assert ("signature", jwt.encoded_string (payload, "secret", "HS256").same_string ("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.pcHcZspUvuiqIPVB_i_qmcvCJv63KLUgIAKIlXI1gY8")) assert ("signature", jwt.encoded_string ("secret").same_string ("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.pcHcZspUvuiqIPVB_i_qmcvCJv63KLUgIAKIlXI1gY8"))
end end
test_jwt test_jwt
local local
jwt: JWT jwt: JWS
jwt_loader: JWT_LOADER
payload: STRING payload: STRING
tok: STRING tok: STRING
do do
payload := "[ payload := "[
{"iss":"joe", {"iss":"joe","exp":1300819380,"http://example.com/is_root":true}
"exp":1300819380,
"http://example.com/is_root":true}
]" ]"
-- payload := "[ -- payload := "[
-- {"sub":"1234567890","name":"John Doe","admin":true} -- {"sub":"1234567890","name":"John Doe","admin":true}
-- ]" -- ]"
create jwt create jwt.make_with_json_payload (payload)
tok := jwt.encoded_string (payload, "secret", "HS256") jwt.set_algorithm ("HS256")
tok := jwt.encoded_string ("secret")
if attached jwt.decoded_string (tok, "secret", Void) as l_tok_payload then create jwt_loader
assert ("no error", not jwt.has_error)
assert ("same payload", l_tok_payload.same_string (payload)) 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
end end
test_unsecured_jwt test_unsecured_jwt
local local
jwt: JWT jwt: JWS
payload: STRING payload: STRING
tok: STRING tok: STRING
do do
payload := "[ payload := "[
{"iss":"joe", {"iss":"joe","exp":1300819380,"http://example.com/is_root":true}
"exp":1300819380,
"http://example.com/is_root":true}
]" ]"
create jwt create jwt.make_with_json_payload (payload)
tok := jwt.encoded_string (payload, "secret", "none") 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 ("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
end end
feature -- Implementation
duplicated_time (dt: DATE_TIME): DATE_TIME
do
Result := dt.deep_twin
end
end end

View File

@@ -1,15 +1,15 @@
<?xml version="1.0" encoding="ISO-8859-1"?> <?xml version="1.0" encoding="ISO-8859-1"?>
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-16-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-16-0 http://www.eiffel.com/developers/xml/configuration-1-16-0.xsd" name="testing" uuid="DB49E98A-0048-414A-A469-EE9B5B903BF3"> <system xmlns="http://www.eiffel.com/developers/xml/configuration-1-15-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-15-0 http://www.eiffel.com/developers/xml/configuration-1-15-0.xsd" name="testing" uuid="DB49E98A-0048-414A-A469-EE9B5B903BF3">
<target name="testing"> <target name="testing">
<root class="ANY" feature="default_create"/> <root class="ANY" feature="default_create"/>
<setting name="console_application" value="false"/> <setting name="console_application" value="true"/>
<capability> <option warning="true" void_safety="all">
<concurrency support="none"/> </option>
<void_safety support="all"/> <setting name="concurrency" value="none"/>
</capability>
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/> <library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
<library name="jwt" location="..\jwt.ecf" readonly="false"/> <library name="jwt" location="..\jwt-safe.ecf" readonly="false"/>
<library name="testing" location="$ISE_LIBRARY\library\testing\testing.ecf"/> <library name="testing" location="$ISE_LIBRARY\library\testing\testing.ecf"/>
<library name="time" location="$ISE_LIBRARY\library\time\time.ecf"/>
<tests name="src" location=".\" recursive="true"/> <tests name="src" location=".\" recursive="true"/>
</target> </target>
</system> </system>