From 06cda975355486054045c32ec05c120ef5044d47 Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Wed, 24 May 2017 11:56:15 +0200 Subject: [PATCH 1/8] New JSON Web Token (JWT) library. --- library/security/jwt/README.md | 26 +++ library/security/jwt/jwt.ecf | 14 ++ library/security/jwt/package.iron | 16 ++ library/security/jwt/src/jwt.e | 265 +++++++++++++++++++++++ library/security/jwt/testing/test_jwt.e | 79 +++++++ library/security/jwt/testing/testing.ecf | 15 ++ 6 files changed, 415 insertions(+) create mode 100644 library/security/jwt/README.md create mode 100644 library/security/jwt/jwt.ecf create mode 100644 library/security/jwt/package.iron create mode 100644 library/security/jwt/src/jwt.e create mode 100644 library/security/jwt/testing/test_jwt.e create mode 100644 library/security/jwt/testing/testing.ecf diff --git a/library/security/jwt/README.md b/library/security/jwt/README.md new file mode 100644 index 00000000..c5bfd597 --- /dev/null +++ b/library/security/jwt/README.md @@ -0,0 +1,26 @@ +JSON Web Token (JWT) + +http://jwt.io/ + +Note: supporting only HS256 and none algorithm for signature. + +# How to use +```eiffel + local + jwt: JWT + do + create jwt + tok := jwt.encoded_string ("[ + {"iss":"joe", "exp":1200819380,"http://example.com/is_root":true} + ]", "secret", "HS256") + if + attached jwt.decoded_string (tok, "secret", Void) as l_tok_payload and + not jwt.has_error + then + check verified: not jwt.has_unverified_token_error end + check no_error: not jwt.has_error end + print (l_tok_payload) + end + end +``` + diff --git a/library/security/jwt/jwt.ecf b/library/security/jwt/jwt.ecf new file mode 100644 index 00000000..66c4fe63 --- /dev/null +++ b/library/security/jwt/jwt.ecf @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/library/security/jwt/package.iron b/library/security/jwt/package.iron new file mode 100644 index 00000000..ef42b57a --- /dev/null +++ b/library/security/jwt/package.iron @@ -0,0 +1,16 @@ +package jwt + +project + jwt = "jwt.ecf" + +note + title: JSON Web Token + description: JSON Web Token + tags:jwt,web,jws,jwe,token + copyright: 2011-2016, Jocelyn Fiat, Eiffel Software and others + license: Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt) + link[license]: http://www.eiffel.com/licensing/forum.txt + link[source]: "github" https://github.com/EiffelWebFramework/EWF/tree/master/library/security/jwt + link[doc]: "Documentation" https://github.com/EiffelWebFramework/EWF/tree/master/library/security/jwt/README.md + +end diff --git a/library/security/jwt/src/jwt.e b/library/security/jwt/src/jwt.e new file mode 100644 index 00000000..147160c8 --- /dev/null +++ b/library/security/jwt/src/jwt.e @@ -0,0 +1,265 @@ +note + description: "JSON Web Token" + date: "$Date$" + revision: "$Revision$" + +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) + 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 -- Status report + + 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 := alg.is_case_insensitive_equal (alg_hs256) or + alg.is_case_insensitive_equal (alg_none) + end + + 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 -- 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 + + 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 + +end diff --git a/library/security/jwt/testing/test_jwt.e b/library/security/jwt/testing/test_jwt.e new file mode 100644 index 00000000..2b269762 --- /dev/null +++ b/library/security/jwt/testing/test_jwt.e @@ -0,0 +1,79 @@ +note + description: "Summary description for {TEST_JWT}." + date: "$Date$" + revision: "$Revision$" + +class + TEST_JWT + +inherit + EQA_TEST_SET + +feature -- Test + + test_jwt_io + local + jwt: JWT + header: STRING + payload: STRING + do + payload := "[ + {"sub":"1234567890","name":"John Doe","admin":true} + ]" + payload.adjust + payload.replace_substring_all ("%N", "%R%N") + + create jwt + + 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")) + end + + test_jwt + local + jwt: JWT + payload: STRING + tok: STRING + do + payload := "[ + {"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") + + 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)) + end + end + + test_unsecured_jwt + local + jwt: JWT + payload: STRING + tok: STRING + do + payload := "[ + {"iss":"joe", + "exp":1300819380, + "http://example.com/is_root":true} + ]" + + create jwt + tok := jwt.encoded_string (payload, "secret", "none") + + 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)) + end + end + +end diff --git a/library/security/jwt/testing/testing.ecf b/library/security/jwt/testing/testing.ecf new file mode 100644 index 00000000..3358cbe3 --- /dev/null +++ b/library/security/jwt/testing/testing.ecf @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + From d4b9301a571e4a495cae24b057c1b013d69153c2 Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Fri, 2 Jun 2017 17:40:53 +0200 Subject: [PATCH 2/8] Reverted to previous `TYPE.name` to be compilable with version prior to 17.05. (TYPE.name_32 exists since 17.05). --- library/server/wsf/src/request/value/wsf_any.e | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/server/wsf/src/request/value/wsf_any.e b/library/server/wsf/src/request/value/wsf_any.e index 3064dbfc..5ef0b824 100644 --- a/library/server/wsf/src/request/value/wsf_any.e +++ b/library/server/wsf/src/request/value/wsf_any.e @@ -57,7 +57,8 @@ feature -- Query -- if possible do if attached value as v then - Result := generating_type.name_32 + -- FIXME: in the future, use the new `{TYPE}.name_32` + Result := generating_type.name.to_string_32 else Result := {STRING_32} "Void" end From 7e54825b8465896c13a70a3e9eed6a5d96d050ed Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Wed, 7 Jun 2017 23:24:46 +0200 Subject: [PATCH 3/8] 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 @@ - + - - - - - + + + - + + From 0783049fb4cc9f0c58cd1f0b28be9fa978ec16d6 Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Thu, 8 Jun 2017 11:25:26 +0200 Subject: [PATCH 4/8] updated package.iron --- library/security/jwt/package.iron | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/security/jwt/package.iron b/library/security/jwt/package.iron index ef42b57a..fd132cd6 100644 --- a/library/security/jwt/package.iron +++ b/library/security/jwt/package.iron @@ -6,8 +6,8 @@ project note title: JSON Web Token description: JSON Web Token - tags:jwt,web,jws,jwe,token - copyright: 2011-2016, Jocelyn Fiat, Eiffel Software and others + tags:jwt,web,jws,jwe,token,jose + copyright: 2011-2017, Jocelyn Fiat, Eiffel Software and others license: Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt) link[license]: http://www.eiffel.com/licensing/forum.txt link[source]: "github" https://github.com/EiffelWebFramework/EWF/tree/master/library/security/jwt From 1ec3b8e7a44e35f1f31582d3a042fce6b1029cb2 Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Wed, 14 Jun 2017 16:19:43 +0200 Subject: [PATCH 5/8] Added support for multiple file in form data. Made clear what is the meaning of upload_filename, upload_data and form_data. --- .../src/http_client_request_context.e | 53 +- .../http_client_request_file_parameter.e | 155 ++++++ .../http_client_request_form_parameters.e | 34 ++ .../http_client_request_parameter.e | 71 +++ .../http_client_request_parameters.e | 62 +++ .../http_client_request_query_parameters.e | 26 + .../http_client_request_string_parameter.e | 68 +++ .../libcurl/libcurl_http_client_request.e | 106 ++-- .../src/spec/net/net_http_client_request.e | 148 ++---- .../tests/{test-safe.ecf => test.ecf} | 7 +- .../http_client/tests/test_libcurl_with_web.e | 5 + .../http_client/tests/test_net_with_web.e | 5 + .../http_client/tests/test_with_web_i.e | 453 ++++++++---------- 13 files changed, 764 insertions(+), 429 deletions(-) create mode 100644 library/network/http_client/src/implementation/http_client_request_file_parameter.e create mode 100644 library/network/http_client/src/implementation/http_client_request_form_parameters.e create mode 100644 library/network/http_client/src/implementation/http_client_request_parameter.e create mode 100644 library/network/http_client/src/implementation/http_client_request_parameters.e create mode 100644 library/network/http_client/src/implementation/http_client_request_query_parameters.e create mode 100644 library/network/http_client/src/implementation/http_client_request_string_parameter.e rename library/network/http_client/tests/{test-safe.ecf => test.ecf} (87%) diff --git a/library/network/http_client/src/http_client_request_context.e b/library/network/http_client/src/http_client_request_context.e index d8e7fe60..c2f069c3 100644 --- a/library/network/http_client/src/http_client_request_context.e +++ b/library/network/http_client/src/http_client_request_context.e @@ -58,11 +58,11 @@ feature -- Access -- Specific headers to use in addition to the one set in the related HTTP_CLIENT_SESSION --| note: the value from Current context override the one from the session in case of conflict - query_parameters: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32] + query_parameters: HTTP_CLIENT_REQUEST_QUERY_PARAMETERS -- Query parameters to be appended to the url --| note: if the url already contains a query_string, the `query_parameters' will be appended to the url - form_parameters: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32] + form_parameters: HTTP_CLIENT_REQUEST_FORM_PARAMETERS -- Form parameters upload_data: detachable READABLE_STRING_8 @@ -145,13 +145,25 @@ feature -- Element change add_query_parameter (k: READABLE_STRING_GENERAL; v: READABLE_STRING_GENERAL) -- Add a query parameter `k=v'. do - query_parameters.force (v.to_string_32, k.to_string_32) + query_parameters.force (create {HTTP_CLIENT_REQUEST_STRING_PARAMETER}.make (k, v)) end add_form_parameter (k: READABLE_STRING_GENERAL; v: READABLE_STRING_GENERAL) -- Add a form parameter `k'= `v'. do - form_parameters.force (v.to_string_32, k.to_string_32) + form_parameters.force (create {HTTP_CLIENT_REQUEST_STRING_PARAMETER}.make (k, v)) + end + + add_file_form_parameter (k: READABLE_STRING_GENERAL; a_location: READABLE_STRING_GENERAL; a_content_type: detachable READABLE_STRING_8) + -- Add a form file parameter named `k`, located at `a_location`, with optional content type `a_content_type`. + require + has_no_upload_data_or_filename: not has_upload_data and not has_upload_filename + local + param: HTTP_CLIENT_REQUEST_FILE_PARAMETER + do + create param.make_with_path (k, create {PATH}.make_from_string (a_location)) + param.set_content_type (a_content_type) + form_parameters.force (param) end set_credentials_required (b: BOOLEAN) @@ -164,7 +176,8 @@ feature -- Element change -- Set `upload_data' to `a_data' --| note: the Current context can have upload_data XOR upload_filename, but not both. require - has_upload_filename: (a_data /= Void and then not a_data.is_empty) implies not has_upload_filename + has_no_upload_filename: (a_data /= Void and then not a_data.is_empty) implies not has_upload_filename + has_no_form_data: (a_data /= Void and then not a_data.is_empty) implies not has_form_data do if a_data = Void or else a_data.is_empty then upload_data := Void @@ -180,6 +193,7 @@ feature -- Element change --| note: the Current context can have upload_data XOR upload_filename, but not both. require has_no_upload_data: (a_fn /= Void and then not a_fn.is_empty) implies not has_upload_data + has_no_form_data: (a_fn /= Void and then not a_fn.is_empty) implies not has_form_data do if a_fn = Void or else a_fn.is_empty then upload_filename := Void @@ -266,9 +280,9 @@ feature -- URL helpers a_url.append_character ('&') end l_first_param := False - uri_percent_encoder.append_query_name_encoded_string_to (ic.key, a_url) + uri_percent_encoder.append_query_name_encoded_string_to (ic.item.name, a_url) a_url.append_character ('=') - uri_percent_encoder.append_query_value_encoded_string_to (ic.item, a_url) + ic.item.append_query_value_encoded_to (a_url) end end end @@ -315,38 +329,35 @@ feature {NONE} -- Implementation end end - parameters_to_uri_percent_encoded_string (ht: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32]): STRING_8 - -- Build query urlencoded string using parameters from `ht'. + parameters_to_uri_percent_encoded_string (a_params: HTTP_CLIENT_REQUEST_PARAMETERS [HTTP_CLIENT_REQUEST_PARAMETER]): STRING_8 + -- Build query urlencoded string using parameters from `a_params'. do create Result.make (64) across - ht as ic + a_params as ic loop if not Result.is_empty then Result.append_character ('&') end - uri_percent_encoder.append_query_name_encoded_string_to (ic.key, Result) + uri_percent_encoder.append_query_name_encoded_string_to (ic.item.name, Result) Result.append_character ('=') - uri_percent_encoder.append_query_value_encoded_string_to (ic.item, Result) + ic.item.append_query_value_encoded_to (Result) end end - parameters_to_x_www_form_urlencoded_string (ht: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32]): STRING_8 - -- Build x-www-form-urlencoded string using parameters from `ht'. + parameters_to_x_www_form_urlencoded_string (a_params: HTTP_CLIENT_REQUEST_PARAMETERS [HTTP_CLIENT_REQUEST_PARAMETER]): STRING_8 + -- Build x-www-form-urlencoded string using parameters from `a_params'. do create Result.make (64) - from - ht.start - until - ht.after + across + a_params as ic loop if not Result.is_empty then Result.append_character ('&') end - Result.append (x_www_form_url_encoder.encoded_string (ht.key_for_iteration)) + x_www_form_url_encoder.append_percent_encoded_string_to (ic.item.name, Result) Result.append_character ('=') - Result.append (x_www_form_url_encoder.encoded_string (ht.item_for_iteration)) - ht.forth + ic.item.append_form_url_encoded_to (Result) end end diff --git a/library/network/http_client/src/implementation/http_client_request_file_parameter.e b/library/network/http_client/src/implementation/http_client_request_file_parameter.e new file mode 100644 index 00000000..15776039 --- /dev/null +++ b/library/network/http_client/src/implementation/http_client_request_file_parameter.e @@ -0,0 +1,155 @@ +note + description: "Summary description for {HTTP_CLIENT_REQUEST_FILE_PARAMETER}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + HTTP_CLIENT_REQUEST_FILE_PARAMETER + +inherit + HTTP_CLIENT_REQUEST_PARAMETER + +create + make_with_path + +feature {NONE} -- Initialization + + make_with_path (a_name: READABLE_STRING_GENERAL; a_path: PATH) + do + set_name (a_name) + location := a_path + if attached a_path.entry as e then + file_name := e.name + end + set_content_type ("application/octet-stream") -- Default + end + +feature -- Access + + count: INTEGER + local + f: RAW_FILE + do + create f.make_with_path (location) + if f.exists and then f.is_access_readable then + Result := f.count + end + end + + location: PATH + + file_name: detachable READABLE_STRING_32 + +feature -- Element change + + set_file_name (fn: detachable READABLE_STRING_GENERAL) + do + if fn = Void then + file_name := Void + else + file_name := fn.to_string_32 + end + end + +feature -- Status report + + exists: BOOLEAN + local + fut: FILE_UTILITIES + do + Result := fut.file_path_exists (location) + end + +feature {NONE} -- Data + + file_content: detachable STRING_8 + require + exists: exists + local + f: RAW_FILE + do + create f.make_with_path (location) + if f.exists and then f.is_access_readable then + create Result.make (f.count) + f.open_read + from + until + f.exhausted or f.end_of_file + loop + f.read_stream_thread_aware (2_048) + Result.append (f.last_string) + end + f.close + end + end + +feature -- Data + + append_file_content_to (a_output: STRING) + -- Append content of file located at `location`to `a_output'. + require + exists: exists + local + f: RAW_FILE + l_buffer_size: INTEGER + do + create f.make_with_path (location) + if f.exists and then f.is_access_readable then + f.open_read + from + l_buffer_size := 2_048 + until + f.exhausted or f.end_of_file + loop + f.read_stream_thread_aware (l_buffer_size) + a_output.append (f.last_string) + end + f.close + end + end + +feature -- Conversion + + append_form_url_encoded_to (a_output: STRING_8) + -- Append as form url encoded string to `a_output`. + do + if exists and then attached file_content as s then + x_www_form_url_encoder.append_percent_encoded_string_to (s, a_output) + else + check exists: False end + end + end + + append_query_value_encoded_to (a_output: STRING_8) + do + if exists and then attached file_content as s then + uri_percent_encoder.append_query_value_encoded_string_to (s, a_output) + else + check exists: False end + end + end + + append_as_mime_encoded_to (a_output: STRING_8) + -- Encoded unicode string for mime value. + -- For instance uploaded filename, or form data key or values. + do + -- FIXME: find the proper encoding! + if exists then + append_file_content_to (a_output) + else + check exists: False end + end + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" + source: "[ + Eiffel Software + 5949 Hollister Ave., Goleta, CA 93117 USA + Telephone 805-685-1006, Fax 805-685-6869 + Website http://www.eiffel.com + Customer support http://support.eiffel.com + ]" +end diff --git a/library/network/http_client/src/implementation/http_client_request_form_parameters.e b/library/network/http_client/src/implementation/http_client_request_form_parameters.e new file mode 100644 index 00000000..be047fd0 --- /dev/null +++ b/library/network/http_client/src/implementation/http_client_request_form_parameters.e @@ -0,0 +1,34 @@ +note + description: "Summary description for {HTTP_CLIENT_REQUEST_FORM_PARAMETERS}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + HTTP_CLIENT_REQUEST_FORM_PARAMETERS + +inherit + HTTP_CLIENT_REQUEST_PARAMETERS [HTTP_CLIENT_REQUEST_PARAMETER] + +create + make + +feature -- Status report + + has_file_parameter: BOOLEAN + -- Has any file parameter? + do + Result := across items as ic some attached {HTTP_CLIENT_REQUEST_FILE_PARAMETER} ic.item end + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" + source: "[ + Eiffel Software + 5949 Hollister Ave., Goleta, CA 93117 USA + Telephone 805-685-1006, Fax 805-685-6869 + Website http://www.eiffel.com + Customer support http://support.eiffel.com + ]" +end diff --git a/library/network/http_client/src/implementation/http_client_request_parameter.e b/library/network/http_client/src/implementation/http_client_request_parameter.e new file mode 100644 index 00000000..fcaf13da --- /dev/null +++ b/library/network/http_client/src/implementation/http_client_request_parameter.e @@ -0,0 +1,71 @@ +note + description: "Summary description for {HTTP_CLIENT_REQUEST_PARAMETER}." + author: "" + date: "$Date$" + revision: "$Revision$" + +deferred class + HTTP_CLIENT_REQUEST_PARAMETER + +feature -- Access + + name: READABLE_STRING_32 + + content_type: detachable READABLE_STRING_8 + + count: INTEGER + -- Integer representing the length of source value. + deferred + end + +feature -- Conversion + + append_form_url_encoded_to (a_output: STRING_8) + -- Append as form url encoded string to `a_output`. + deferred + end + + append_query_value_encoded_to (a_output: STRING_8) + deferred + end + + append_as_mime_encoded_to (a_output: STRING_8) + deferred + end + +feature -- Element change + + set_name (a_name: READABLE_STRING_GENERAL) + do + name := a_name.as_string_32 + end + + set_content_type (ct: detachable READABLE_STRING_8) + do + content_type := ct + end + +feature {NONE} -- Implementation + + x_www_form_url_encoder: X_WWW_FORM_URL_ENCODER + -- Shared x-www-form-urlencoded encoder. + once + create Result + end + + uri_percent_encoder: URI_PERCENT_ENCODER + once + create Result + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" + source: "[ + Eiffel Software + 5949 Hollister Ave., Goleta, CA 93117 USA + Telephone 805-685-1006, Fax 805-685-6869 + Website http://www.eiffel.com + Customer support http://support.eiffel.com + ]" +end diff --git a/library/network/http_client/src/implementation/http_client_request_parameters.e b/library/network/http_client/src/implementation/http_client_request_parameters.e new file mode 100644 index 00000000..bb6a7a9b --- /dev/null +++ b/library/network/http_client/src/implementation/http_client_request_parameters.e @@ -0,0 +1,62 @@ +note + description: "Summary description for {HTTP_CLIENT_REQUEST_PARAMETERS}." + author: "" + date: "$Date$" + revision: "$Revision$" + +deferred class + HTTP_CLIENT_REQUEST_PARAMETERS [G -> HTTP_CLIENT_REQUEST_PARAMETER] + +inherit + ITERABLE [G] + +feature {NONE} -- Initialization + + make (nb: INTEGER) + do + create items.make (nb) + end + +feature -- Access + + is_empty: BOOLEAN + do + Result := items.is_empty + end + + count: INTEGER + do + Result := items.count + end + +feature -- Element change + + extend, force (i: G) + do + items.force (i) + end + +feature -- Iteration + + new_cursor: ARRAYED_LIST_ITERATION_CURSOR [G] + -- + do + Result := items.new_cursor + end + +feature {NONE} -- Implementation + + items: ARRAYED_LIST [G] + +invariant +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" + source: "[ + Eiffel Software + 5949 Hollister Ave., Goleta, CA 93117 USA + Telephone 805-685-1006, Fax 805-685-6869 + Website http://www.eiffel.com + Customer support http://support.eiffel.com + ]" +end diff --git a/library/network/http_client/src/implementation/http_client_request_query_parameters.e b/library/network/http_client/src/implementation/http_client_request_query_parameters.e new file mode 100644 index 00000000..8c541da0 --- /dev/null +++ b/library/network/http_client/src/implementation/http_client_request_query_parameters.e @@ -0,0 +1,26 @@ +note + description: "Summary description for {HTTP_CLIENT_REQUEST_QUERY_PARAMETERS}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + HTTP_CLIENT_REQUEST_QUERY_PARAMETERS + +inherit + HTTP_CLIENT_REQUEST_PARAMETERS [HTTP_CLIENT_REQUEST_STRING_PARAMETER] + +create + make + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" + source: "[ + Eiffel Software + 5949 Hollister Ave., Goleta, CA 93117 USA + Telephone 805-685-1006, Fax 805-685-6869 + Website http://www.eiffel.com + Customer support http://support.eiffel.com + ]" +end diff --git a/library/network/http_client/src/implementation/http_client_request_string_parameter.e b/library/network/http_client/src/implementation/http_client_request_string_parameter.e new file mode 100644 index 00000000..4a71e853 --- /dev/null +++ b/library/network/http_client/src/implementation/http_client_request_string_parameter.e @@ -0,0 +1,68 @@ +note + description: "Summary description for {HTTP_CLIENT_REQUEST_STRING_PARAMETER}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + HTTP_CLIENT_REQUEST_STRING_PARAMETER + +inherit + HTTP_CLIENT_REQUEST_PARAMETER + +create + make + +feature {NONE} -- Initialization + + make (a_name, a_value: READABLE_STRING_GENERAL) + do + set_name (a_name) + value := a_value.as_string_32 + end + +feature -- Access + + value: READABLE_STRING_32 + + count: INTEGER + do + Result := value.count + end + +feature -- Conversion + + append_form_url_encoded_to (a_output: STRING_8) + -- Append as form url encoded string to `a_output`. + do + x_www_form_url_encoder.append_percent_encoded_string_to (value, a_output) + end + + append_query_value_encoded_to (a_output: STRING_8) + do + uri_percent_encoder.append_query_value_encoded_string_to (value, a_output) + end + + append_as_mime_encoded_to (a_output: STRING_8) + -- Encoded unicode string for mime value. + -- For instance uploaded filename, or form data key or values. + local + utf: UTF_CONVERTER + do + -- FIXME: find the proper encoding! + utf.utf_32_string_into_utf_8_string_8 (value, a_output) + end + +invariant + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" + source: "[ + Eiffel Software + 5949 Hollister Ave., Goleta, CA 93117 USA + Telephone 805-685-1006, Fax 805-685-6869 + Website http://www.eiffel.com + Customer support http://support.eiffel.com + ]" +end diff --git a/library/network/http_client/src/spec/libcurl/libcurl_http_client_request.e b/library/network/http_client/src/spec/libcurl/libcurl_http_client_request.e index 1ad66b1d..13e44bbb 100644 --- a/library/network/http_client/src/spec/libcurl/libcurl_http_client_request.e +++ b/library/network/http_client/src/spec/libcurl/libcurl_http_client_request.e @@ -58,7 +58,6 @@ feature -- Execution ctx: like context p_slist: POINTER retried: BOOLEAN - l_form_data: detachable HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32] l_upload_data: detachable READABLE_STRING_8 l_upload_filename: detachable READABLE_STRING_GENERAL l_headers: like headers @@ -151,70 +150,19 @@ feature -- Execution --| Credentials not provided ... end end - if ctx.has_upload_data then l_upload_data := ctx.upload_data end if ctx.has_upload_filename then l_upload_filename := ctx.upload_filename end - if ctx.has_form_data then - l_form_data := ctx.form_parameters - check non_empty_form_data: not l_form_data.is_empty end - if l_upload_data = Void and l_upload_filename = Void then - -- Send as form-urlencoded - if - attached l_headers.item ("Content-Type") as l_ct - then - if l_ct.starts_with ("application/x-www-form-urlencoded") then - -- Content-Type is already application/x-www-form-urlencoded - l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string - elseif l_ct.starts_with ("multipart/form-data") then - l_use_curl_form := True - else - -- Not supported, use libcurl form. - l_use_curl_form := True - end - else - l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string - end - else - l_use_curl_form := True - end - if l_use_curl_form then - create l_form.make - create l_last.make - from - l_form_data.start - until - l_form_data.after - loop - curl.formadd_string_string (l_form, l_last, - {CURL_FORM_CONSTANTS}.curlform_copyname, l_form_data.key_for_iteration, - {CURL_FORM_CONSTANTS}.curlform_copycontents, l_form_data.item_for_iteration, - {CURL_FORM_CONSTANTS}.curlform_end - ) - l_form_data.forth - end - if l_upload_filename /= Void then - curl.formadd_string_string (l_form, l_last, - {CURL_FORM_CONSTANTS}.curlform_copyname, "file", - {CURL_FORM_CONSTANTS}.curlform_file, l_upload_filename, - {CURL_FORM_CONSTANTS}.curlform_end - ) - l_upload_filename := Void - end - l_last.release_item - curl_easy.setopt_form (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httppost, l_form) - end - end - if l_upload_data /= Void then check post_or_put_request_method: request_method.is_case_insensitive_equal ("POST") or request_method.is_case_insensitive_equal ("PUT") or request_method.is_case_insensitive_equal ("PATCH") end + check no_form_data: not ctx.has_form_data end curl_easy.setopt_string (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_postfields, l_upload_data) curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_postfieldsize, l_upload_data.count) @@ -224,6 +172,7 @@ feature -- Execution or request_method.is_case_insensitive_equal ("PUT") or request_method.is_case_insensitive_equal ("PATCH") end + check no_form_data: not ctx.has_form_data end create l_upload_file.make_with_name (l_upload_filename) if l_upload_file.exists and then l_upload_file.is_readable then @@ -238,12 +187,59 @@ feature -- Execution l_upload_file.open_read curl_easy.set_curl_function (l_custom_function) end + elseif + ctx.has_form_data and + attached ctx.form_parameters as l_form_data + then + check non_empty_form_data: not l_form_data.is_empty end + -- Send as form-urlencoded + if + attached l_headers.item ("Content-Type") as l_ct + then + if l_ct.starts_with ("application/x-www-form-urlencoded") then + -- Content-Type is already application/x-www-form-urlencoded + l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string + elseif l_ct.starts_with ("multipart/form-data") or l_form_data.has_file_parameter then + l_use_curl_form := True + else + -- Not supported, use libcurl form. + l_use_curl_form := True + end + else + l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string + end + if l_use_curl_form then + create l_form.make + create l_last.make + across + l_form_data as ic + loop + if attached {HTTP_CLIENT_REQUEST_STRING_PARAMETER} ic.item as strparam then + curl.formadd_string_string (l_form, l_last, + {CURL_FORM_CONSTANTS}.curlform_copyname, strparam.name, + {CURL_FORM_CONSTANTS}.curlform_copycontents, strparam.value, + {CURL_FORM_CONSTANTS}.curlform_end + ) + elseif attached {HTTP_CLIENT_REQUEST_FILE_PARAMETER} ic.item as fileparam then + curl.formadd_string_string (l_form, l_last, + {CURL_FORM_CONSTANTS}.curlform_copyname, "file", + {CURL_FORM_CONSTANTS}.curlform_file, fileparam.location.name, + {CURL_FORM_CONSTANTS}.curlform_end + ) + else + check supported_parameter_type: False end + end + end + l_last.release_item + curl_easy.setopt_form (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httppost, l_form) + end else - check no_upload_data: l_upload_data = Void and l_upload_filename = Void end + -- No form, or upload data to send! + check no_data: not (ctx.has_upload_data or ctx.has_upload_filename or ctx.has_form_data) end end end -- ctx /= Void - --| Header + --| Header across l_headers as curs loop diff --git a/library/network/http_client/src/spec/net/net_http_client_request.e b/library/network/http_client/src/spec/net/net_http_client_request.e index 5808b031..3c00f138 100644 --- a/library/network/http_client/src/spec/net/net_http_client_request.e +++ b/library/network/http_client/src/spec/net/net_http_client_request.e @@ -91,8 +91,8 @@ feature -- Access l_authorization: HTTP_AUTHORIZATION l_platform: STRING l_upload_data: detachable READABLE_STRING_8 - l_form_data: detachable HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32] ctx: like context + l_ct: detachable READABLE_STRING_8 l_upload_file: detachable RAW_FILE l_upload_filename: detachable READABLE_STRING_GENERAL l_form_string: STRING @@ -149,7 +149,7 @@ feature -- Access then create l_authorization.make_basic_auth (u_name, u_pass) if attached l_authorization.http_authorization as auth then - headers.extend (auth, "Authorization") + headers.force (auth, "Authorization") end check headers.has_key ("Authorization") end end @@ -176,7 +176,7 @@ feature -- Access else l_platform := "Unknown" end - headers.extend ("eiffelhttpclient/" + net_http_client_version + " (" + l_platform + ")", "User-Agent") + headers.force ("eiffelhttpclient/" + net_http_client_version + " (" + l_platform + ")", "User-Agent") end -- handle sending data @@ -191,67 +191,52 @@ feature -- Access l_upload_data := ctx.upload_data end - if ctx.has_form_data then - l_form_data := ctx.form_parameters - if l_upload_data = Void and l_upload_filename = Void then - if - attached headers.item ("Content-Type") as l_ct - then - if l_ct.starts_with ("application/x-www-form-urlencoded") then - l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string - elseif l_ct.starts_with ("multipart/form-data") then - -- create form using multipart/form-data encoding - l_boundary := new_mime_boundary (l_form_data) - headers.extend ("multipart/form-data; boundary=" + l_boundary, "Content-Type") - l_upload_data := form_date_and_uploaded_files_to_mime_string (l_form_data, l_upload_filename, l_boundary) - else - -- not supported ! - -- Send as form-urlencoded - headers.extend ("application/x-www-form-urlencoded", "Content-Type") - l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string - end - else - -- Send as form-urlencoded - headers.extend ("application/x-www-form-urlencoded", "Content-Type") - l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string - end - headers.extend (l_upload_data.count.out, "Content-Length") - if l_is_chunked_transfer_encoding then - -- Discard chunked transfer encoding - headers.remove ("Transfer-Encoding") - l_is_chunked_transfer_encoding := False - end - elseif l_form_data /= Void then - check l_upload_data = Void end - - -- create form using multipart/form-data encoding - l_boundary := new_mime_boundary (l_form_data) - headers.extend ("multipart/form-data; boundary=" + l_boundary, "Content-Type") - l_upload_data := form_date_and_uploaded_files_to_mime_string (l_form_data, l_upload_filename, l_boundary) - headers.extend (l_upload_data.count.out, "Content-Length") - if l_is_chunked_transfer_encoding then - -- Discard chunked transfer encoding - headers.remove ("Transfer-Encoding") - l_is_chunked_transfer_encoding := False - end - end - elseif l_upload_data /= Void then + if l_upload_data /= Void then check ctx.has_upload_data end + check no_form_data: not ctx.has_form_data end if not headers.has ("Content-Type") then - headers.extend ("application/x-www-form-urlencoded", "Content-Type") + headers.force ("application/x-www-form-urlencoded", "Content-Type") end if not l_is_chunked_transfer_encoding then - headers.extend (l_upload_data.count.out, "Content-Length") + headers.force (l_upload_data.count.out, "Content-Length") end elseif l_upload_filename /= Void then check ctx.has_upload_filename end + check no_form_data: not ctx.has_form_data end create l_upload_file.make_with_name (l_upload_filename) if l_upload_file.exists and then l_upload_file.is_access_readable then if not l_is_chunked_transfer_encoding then - headers.extend (l_upload_file.count.out, "Content-Length") + headers.force (l_upload_file.count.out, "Content-Length") end end check l_upload_file /= Void end + elseif + ctx.has_form_data and + attached ctx.form_parameters as l_form_data + then + l_ct := headers.item ("Content-Type") + if l_ct /= Void and then l_ct.starts_with ("application/x-www-form-urlencoded") then + l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string + elseif + (l_ct /= Void and then l_ct.starts_with ("multipart/form-data")) + or l_form_data.has_file_parameter + then + -- create form using multipart/form-data encoding + l_boundary := new_mime_boundary (l_form_data) + headers.force ("multipart/form-data; boundary=" + l_boundary, "Content-Type") + l_upload_data := form_date_and_uploaded_files_to_mime_string (l_form_data, l_boundary) + else + -- not supported ! + -- Send as form-urlencoded + headers.force ("application/x-www-form-urlencoded", "Content-Type") + l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string + end + headers.force (l_upload_data.count.out, "Content-Length") + if l_is_chunked_transfer_encoding then + -- Discard chunked transfer encoding + headers.remove ("Transfer-Encoding") + l_is_chunked_transfer_encoding := False + end end end @@ -482,14 +467,9 @@ feature {NONE} -- Helpers Result := a_status >= 300 and a_status < 400 end - form_date_and_uploaded_files_to_mime_string (a_form_parameters: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32]; a_upload_filename: detachable READABLE_STRING_GENERAL; a_mime_boundary: READABLE_STRING_8): STRING + form_date_and_uploaded_files_to_mime_string (a_form_parameters: ITERABLE [HTTP_CLIENT_REQUEST_PARAMETER]; a_mime_boundary: READABLE_STRING_8): STRING -- Form data and uploaded files converted to mime string. -- TODO: design a proper MIME... component. - local - l_path: PATH - l_mime_type: READABLE_STRING_8 - l_upload_file: detachable RAW_FILE - l_mime_type_mapping: HTTP_FILE_EXTENSION_MIME_MAPPING do create Result.make (100) across @@ -500,48 +480,26 @@ feature {NONE} -- Helpers Result.append (http_end_of_header_line) Result.append ("Content-Disposition: form-data; name=") Result.append_character ('%"') - Result.append (string_to_mime_encoded_string (ic.key)) + Result.append (string_to_mime_encoded_string (ic.item.name)) Result.append_character ('%"') - Result.append (http_end_of_header_line) - Result.append (http_end_of_header_line) - Result.append (string_to_mime_encoded_string (ic.item)) - Result.append (http_end_of_header_line) - end - - if a_upload_filename /= Void then - -- get file extension, otherwise set default - create l_mime_type_mapping.make_default - create l_path.make_from_string (a_upload_filename) if - attached l_path.extension as ext and then - attached l_mime_type_mapping.mime_type (ext) as l_mt + attached {HTTP_CLIENT_REQUEST_FILE_PARAMETER} ic.item as fileparam and then + attached fileparam.file_name as fn then - l_mime_type := l_mt - else - l_mime_type := "application/octet-stream" + Result.append ("; filename=") + Result.append_character ('%"') + Result.append (string_to_mime_encoded_string (fn)) + Result.append_character ('%"') end - Result.append ("--") - Result.append (a_mime_boundary) - Result.append (http_end_of_header_line) - Result.append ("Content-Disposition: form-data; name=%"") - Result.append (string_to_mime_encoded_string (a_upload_filename)) - Result.append_character ('%"') - Result.append ("; filename=%"") - Result.append (string_to_mime_encoded_string (a_upload_filename)) - Result.append_character ('%"') - Result.append (http_end_of_header_line) - Result.append ("Content-Type: ") - Result.append (l_mime_type) - Result.append (http_end_of_header_line) - Result.append (http_end_of_header_line) - - create l_upload_file.make_with_path (l_path) - if l_upload_file.exists and then l_upload_file.is_access_readable then - append_file_content_to (l_upload_file, l_upload_file.count, Result) - -- Reset l_upload_file to Void, since the related content is already processed. - l_upload_file := Void + if attached ic.item.content_type as ct then + Result.append (http_end_of_header_line) + Result.append ("Content-Type: ") + Result.append (ct) end Result.append (http_end_of_header_line) + Result.append (http_end_of_header_line) + ic.item.append_as_mime_encoded_to (Result) + Result.append (http_end_of_header_line) end Result.append ("--") Result.append (a_mime_boundary) @@ -893,7 +851,7 @@ feature {NONE} -- Helpers end end - new_mime_boundary (a_data: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32]): STRING + new_mime_boundary (a_data: ITERABLE [HTTP_CLIENT_REQUEST_PARAMETER]): STRING -- New MIME boundary. local s: STRING @@ -904,7 +862,7 @@ feature {NONE} -- Helpers across a_data as ic loop - i := i + ic.item.count + ic.key.count + i := i + ic.item.count + ic.item.name.count end create ran.set_seed (i) -- FIXME: use a real random seed. ran.start diff --git a/library/network/http_client/tests/test-safe.ecf b/library/network/http_client/tests/test.ecf similarity index 87% rename from library/network/http_client/tests/test-safe.ecf rename to library/network/http_client/tests/test.ecf index 2c0cdb86..49622c2a 100644 --- a/library/network/http_client/tests/test-safe.ecf +++ b/library/network/http_client/tests/test.ecf @@ -1,6 +1,6 @@ - - + + /.git$ @@ -10,7 +10,8 @@ - + + diff --git a/library/network/http_client/tests/test_libcurl_with_web.e b/library/network/http_client/tests/test_libcurl_with_web.e index 69ad2337..842f78d0 100644 --- a/library/network/http_client/tests/test_libcurl_with_web.e +++ b/library/network/http_client/tests/test_libcurl_with_web.e @@ -59,6 +59,11 @@ feature -- Tests test_post_with_file_and_form_data end + libcurl_test_post_with_multiple_file_and_form_data + do + test_post_with_multiple_file_and_form_data + end + libcurl_test_get_with_redirection do test_get_with_redirection diff --git a/library/network/http_client/tests/test_net_with_web.e b/library/network/http_client/tests/test_net_with_web.e index 6a1a112c..14488c99 100644 --- a/library/network/http_client/tests/test_net_with_web.e +++ b/library/network/http_client/tests/test_net_with_web.e @@ -59,6 +59,11 @@ feature -- Tests test_post_with_file_and_form_data end + net_test_post_with_multiple_file_and_form_data + do + test_post_with_multiple_file_and_form_data + end + net_test_get_with_redirection do test_get_with_redirection diff --git a/library/network/http_client/tests/test_with_web_i.e b/library/network/http_client/tests/test_with_web_i.e index 56a1604a..e1539ea4 100644 --- a/library/network/http_client/tests/test_with_web_i.e +++ b/library/network/http_client/tests/test_with_web_i.e @@ -21,8 +21,7 @@ feature -- Initialization on_prepare do Precursor - global_requestbin_path := "/s0jkhhs0" - if global_requestbin_path = Void then + if is_using_requestbin and global_requestbin_path = Void then global_requestbin_path := new_requestbin_path end end @@ -33,7 +32,13 @@ feature -- Factory deferred end -feature -- Requestbin +feature -- Requestbin + + is_using_requestbin: BOOLEAN = False + is_using_mockbincom: BOOLEAN + do + Result := not is_using_requestbin + end global_requestbin_path: detachable READABLE_STRING_8 @@ -42,7 +47,7 @@ feature -- Requestbin i,j: INTEGER do if - attached new_session ("http://requestb.in") as sess and then + attached new_session ("https://requestb.in") as sess and then attached sess.post ("/api/v1/bins", Void, Void) as resp then if resp.error_occurred then @@ -67,13 +72,30 @@ feature -- Requestbin if not Result.starts_with ("/") then Result.prepend_character ('/') end - print ("new_requestbin_path => http://requestb.in" + Result + "?inspect%N") + print ("new_requestbin_path => " + sess.base_url + Result + "?inspect%N") end end end end end + new_web_session: like new_session + do + if is_using_mockbincom then + Result := new_session ("http://mockbin.com/request") + end + if Result = Void and is_using_requestbin then + if attached global_requestbin_path as l_path then + Result := new_session ("https://requestb.in" + l_path) + else + assert ("Has requestbin path", False) + end + end + if Result = Void then + Result := new_session ("http://mockbin.com/request") -- Default + end + end + feature -- Factory test_post_url_encoded @@ -81,288 +103,200 @@ feature -- Factory sess: HTTP_CLIENT_SESSION h: STRING_8 do - if attached global_requestbin_path as requestbin_path then - -- URL ENCODED POST REQUEST - -- check requestbin to ensure the "Hello World" has been received in the raw body - -- also check that User-Agent was sent - create h.make_empty - sess := new_session ("http://requestb.in") - if - attached sess.post (requestbin_path, Void, "Hello World") as res and then - attached res.headers as hds - then - across - hds as c - loop - h.append (c.item.name + ": " + c.item.value + "%R%N") - end - end - print (h) - else - assert ("Has requestbin path", False) + -- URL ENCODED POST REQUEST + -- check requestbin to ensure the "Hello World" has been received in the raw body + -- also check that User-Agent was sent + create h.make_empty + sess := new_web_session + if + attached sess.post ("", Void, "Hello World") as res + then + check_response (res) end end test_post_with_form_data local sess: HTTP_CLIENT_SESSION - h: STRING_8 l_ctx: HTTP_CLIENT_REQUEST_CONTEXT do - if attached global_requestbin_path as requestbin_path then - - -- POST REQUEST WITH FORM DATA - -- check requestbin to ensure the form parameters are correctly received - sess := new_session ("http://requestb.in") - create l_ctx.make - l_ctx.add_form_parameter ("First Key", "First Value") - l_ctx.add_form_parameter ("Second Key", "Second Value") - l_ctx.add_form_parameter ("unicode", {STRING_32} "Hello / 你好 !") - l_ctx.add_form_parameter ({STRING_32} "Field 你好 !", "How are you?") - create h.make_empty - if - attached sess.post (requestbin_path, l_ctx, "") as res and then - attached res.headers as hds - then - across - hds as c - loop - h.append (c.item.name + ": " + c.item.value + "%R%N") - end - end - print (h) - else - assert ("Has requestbin path", False) + -- POST REQUEST WITH FORM DATA + -- check requestbin to ensure the form parameters are correctly received + sess := new_web_session + create l_ctx.make + l_ctx.add_form_parameter ("First Key", "First Value") + l_ctx.add_form_parameter ("Second Key", "Second Value") + l_ctx.add_form_parameter ("unicode", {STRING_32} "Hello / 你好 !") + l_ctx.add_form_parameter ({STRING_32} "Field 你好 !", "How are you?") + if + attached sess.post ("", l_ctx, "") as res + then + check_response (res) end end test_post_with_uncommon_form_data local sess: HTTP_CLIENT_SESSION - h: STRING_8 l_ctx: HTTP_CLIENT_REQUEST_CONTEXT do - if attached global_requestbin_path as requestbin_path then + -- POST REQUEST WITH FORM DATA + -- check requestbin to ensure the form parameters are correctly received + sess := new_web_session + create l_ctx.make - -- POST REQUEST WITH FORM DATA - -- check requestbin to ensure the form parameters are correctly received - sess := new_session ("http://requestb.in") - create l_ctx.make + l_ctx.add_form_parameter ("title", "Eiffel World!") -- space and ! + l_ctx.add_form_parameter ("path", "foo/bar") -- slash + l_ctx.add_form_parameter ("unreserved", ":!@[]{}()*") -- ... + l_ctx.add_form_parameter ("reserved", "+=?&_#_") -- ... + l_ctx.add_form_parameter ("a=b", "a=b") -- equal sign + l_ctx.add_form_parameter ("test", "!$&'()*") -- + l_ctx.add_form_parameter ("lst[a][b]", "[123][456]") -- brackets + l_ctx.add_form_parameter ("pos{1,2}", "loc{a,b}") -- curly brackets + l_ctx.add_form_parameter ("?foo", "?bar") -- question mark + l_ctx.add_form_parameter ("?", "?") -- question mark + l_ctx.add_form_parameter ("&bar", "&bar") -- ampersand + l_ctx.add_form_parameter ("&", "&") -- ampersand - l_ctx.add_form_parameter ("title", "Eiffel World!") -- space and ! - l_ctx.add_form_parameter ("path", "foo/bar") -- slash - l_ctx.add_form_parameter ("unreserved", ":!@[]{}()*") -- ... - l_ctx.add_form_parameter ("reserved", "+=?&_#_") -- ... - l_ctx.add_form_parameter ("a=b", "a=b") -- equal sign - l_ctx.add_form_parameter ("test", "!$&'()*") -- - l_ctx.add_form_parameter ("lst[a][b]", "[123][456]") -- brackets - l_ctx.add_form_parameter ("pos{1,2}", "loc{a,b}") -- curly brackets - l_ctx.add_form_parameter ("?foo", "?bar") -- question mark - l_ctx.add_form_parameter ("?", "?") -- question mark - l_ctx.add_form_parameter ("&bar", "&bar") -- ampersand - l_ctx.add_form_parameter ("&", "&") -- ampersand + assert ("form data well generated", l_ctx.form_parameters_to_x_www_form_url_encoded_string.same_string ("title=Eiffel+World!&path=foo%%2Fbar&unreserved=%%3A!%%40%%5B%%5D%%7B%%7D()*&reserved=%%2B%%3D%%3F%%26_%%23_&a%%3Db=a%%3Db&test=!%%24%%26'()*&lst%%5Ba%%5D%%5Bb%%5D=%%5B123%%5D%%5B456%%5D&pos%%7B1%%2C2%%7D=loc%%7Ba%%2Cb%%7D&%%3Ffoo=%%3Fbar&%%3F=%%3F&%%26bar=%%26bar&%%26=%%26")) - assert ("form data well generated", l_ctx.form_parameters_to_x_www_form_url_encoded_string.same_string ("title=Eiffel+World!&path=foo%%2Fbar&unreserved=%%3A!%%40%%5B%%5D%%7B%%7D()*&reserved=%%2B%%3D%%3F%%26_%%23_&a%%3Db=a%%3Db&test=!%%24%%26'()*&lst%%5Ba%%5D%%5Bb%%5D=%%5B123%%5D%%5B456%%5D&pos%%7B1%%2C2%%7D=loc%%7Ba%%2Cb%%7D&%%3Ffoo=%%3Fbar&%%3F=%%3F&%%26bar=%%26bar&%%26=%%26")) - - create h.make_empty - if - attached sess.post (requestbin_path, l_ctx, "") as res and then - attached res.headers as hds - then - across - hds as c - loop - h.append (c.item.name + ": " + c.item.value + "%R%N") - end - end - print (h) - else - assert ("Has requestbin path", False) + if + attached sess.post ("", l_ctx, "") as res + then + check_response (res) end end test_post_with_file local sess: HTTP_CLIENT_SESSION - h: STRING_8 l_ctx: HTTP_CLIENT_REQUEST_CONTEXT do - if attached global_requestbin_path as requestbin_path then - - -- POST REQUEST WITH A FILE - -- check requestbin to ensure the form parameters are correctly received - -- set filename to a local file - sess := new_session ("http://requestb.in") - create l_ctx.make - l_ctx.set_upload_filename ("test.txt") - create h.make_empty - if - attached sess.post (requestbin_path, l_ctx, Void) as res and then - attached res.headers as hds - then - across - hds as c - loop - h.append (c.item.name + ": " + c.item.value + "%R%N") - end - end - print (h) - else - assert ("Has requestbin path", False) + -- POST REQUEST WITH A FILE + -- check requestbin to ensure the form parameters are correctly received + -- set filename to a local file + sess := new_web_session + create l_ctx.make + l_ctx.set_upload_filename ("test.txt") + if + attached sess.post ("", l_ctx, Void) as res + then + check_response (res) end end test_put_with_file local sess: HTTP_CLIENT_SESSION - h: STRING_8 l_ctx: HTTP_CLIENT_REQUEST_CONTEXT do - if attached global_requestbin_path as requestbin_path then - - -- PUT REQUEST WITH A FILE - -- check requestbin to ensure the file is correctly received - -- set filename to a local file - sess := new_session ("http://requestb.in") - create l_ctx.make - l_ctx.set_upload_filename ("test.txt") - create h.make_empty - if - attached sess.put (requestbin_path, l_ctx, Void) as res and then - attached res.headers as hds - then - across - hds as c - loop - h.append (c.item.name + ": " + c.item.value + "%R%N") - end - end - print (h) - else - assert ("Has requestbin path", False) + -- PUT REQUEST WITH A FILE + -- check requestbin to ensure the file is correctly received + -- set filename to a local file + sess := new_web_session + create l_ctx.make + l_ctx.set_upload_filename ("test.txt") + if + attached sess.put ("", l_ctx, Void) as res + then + check_response (res) end end test_put_with_data local sess: HTTP_CLIENT_SESSION - h: STRING_8 l_ctx: HTTP_CLIENT_REQUEST_CONTEXT do - if attached global_requestbin_path as requestbin_path then - - -- PUT REQUEST WITH A FILE - -- check requestbin to ensure the file is correctly received - -- set filename to a local file - sess := new_session ("http://requestb.in") - create l_ctx.make - l_ctx.set_upload_data ("name=This is a test for http client.%N") - create h.make_empty - if - attached sess.put (requestbin_path, l_ctx, Void) as res and then - attached res.headers as hds - then - across - hds as c - loop - h.append (c.item.name + ": " + c.item.value + "%R%N") - end - end - print (h) - else - assert ("Has requestbin path", False) + -- PUT REQUEST WITH A FILE + -- check requestbin to ensure the file is correctly received + -- set filename to a local file + sess := new_web_session + create l_ctx.make + l_ctx.set_upload_data ("name=This is a test for http client.%N") + if + attached sess.put ("", l_ctx, Void) as res + then + check_response (res) end + end test_post_with_file_and_form_data local sess: HTTP_CLIENT_SESSION - h: STRING_8 l_ctx: HTTP_CLIENT_REQUEST_CONTEXT do - if attached global_requestbin_path as requestbin_path then + -- POST REQUEST WITH A FILE AND FORM DATA + -- check requestbin to ensure the file and form parameters are correctly received + -- set filename to a local file + sess := new_web_session + create l_ctx.make +-- l_ctx.add_file_form_parameter ("image", "test.txt", "image/jpeg") + l_ctx.add_file_form_parameter ("text", "test.txt", "plain/text") + l_ctx.add_form_parameter ("First", "Value") + l_ctx.add_form_parameter ("Second", "and last value") + if + attached sess.post ("", l_ctx, Void) as res + then + check_response (res) + end + end - -- POST REQUEST WITH A FILE AND FORM DATA - -- check requestbin to ensure the file and form parameters are correctly received - -- set filename to a local file - sess := new_session ("http://requestb.in") --- sess := new_session ("http://localhost:9090") - create l_ctx.make --- l_ctx.set_upload_filename ("logo.jpg") - l_ctx.set_upload_filename ("test.txt") - l_ctx.add_form_parameter ("First", "Value") - l_ctx.add_form_parameter ("Second", "and last value") - create h.make_empty - if - attached sess.post (requestbin_path, l_ctx, Void) as res and then - attached res.headers as hds - then - across - hds as c - loop - h.append (c.item.name + ": " + c.item.value + "%R%N") - end - end - print (h) - else - assert ("Has requestbin path", False) + test_post_with_multiple_file_and_form_data + local + sess: HTTP_CLIENT_SESSION + l_ctx: HTTP_CLIENT_REQUEST_CONTEXT + do + -- POST REQUEST WITH A FILE AND FORM DATA + -- check requestbin to ensure the file and form parameters are correctly received + -- set filename to a local file + sess := new_web_session + create l_ctx.make + l_ctx.add_header ("Content-Type", "multipart/form-data") + + l_ctx.add_file_form_parameter ("first_file", "test.txt", "plain/text") + l_ctx.add_file_form_parameter ("image", "logo.jpg", "image/jpeg") + l_ctx.add_form_parameter ("First", "Value") + l_ctx.add_form_parameter ("Second", "and last value") + l_ctx.add_file_form_parameter ("last_file", "test.txt", Void) + + if + attached sess.post ("", l_ctx, Void) as res + then + check_response (res) end end test_post_with_file_using_chunked_transfer_encoding local sess: HTTP_CLIENT_SESSION - h: STRING_8 l_ctx: HTTP_CLIENT_REQUEST_CONTEXT do - if attached global_requestbin_path as requestbin_path then - - -- POST REQUEST WITH A FILE AND FORM DATA - -- check requestbin to ensure the file and form parameters are correctly received - -- set filename to a local file - sess := new_session ("http://requestb.in") - create l_ctx.make - l_ctx.add_header ("Transfer-Encoding", "chunked") - l_ctx.set_upload_filename ("logo.jpg") - create h.make_empty - if - attached sess.post (requestbin_path, l_ctx, Void) as res and then - attached res.headers as hds - then - across - hds as c - loop - h.append (c.item.name + ": " + c.item.value + "%R%N") - end - end - print (h) - else - assert ("Has requestbin path", False) + -- POST REQUEST WITH A FILE AND FORM DATA + -- check requestbin to ensure the file and form parameters are correctly received + -- set filename to a local file + sess := new_web_session + create l_ctx.make + l_ctx.add_header ("Transfer-Encoding", "chunked") + l_ctx.set_upload_filename ("logo.jpg") + if + attached sess.post ("", l_ctx, Void) as res + then + check_response (res) end end test_get_with_redirection local sess: HTTP_CLIENT_SESSION - h: STRING_8 do - if attached global_requestbin_path as requestbin_path then - - -- GET REQUEST, Forwarding (google's first answer is a forward) - -- check headers received (printed in console) - sess := new_session ("http://google.com") - create h.make_empty - if attached sess.get ("/", Void) as res and then attached res.headers as hds then - assert("was redirected", res.redirections_count > 0) - across - hds as c - loop - h.append (c.item.name + ": " + c.item.value + "%R%N") - end - end - print (h) - else - assert ("Has requestbin path", False) + -- GET REQUEST, Forwarding (google's first answer is a forward) + -- check headers received (printed in console) + sess := new_session ("http://google.com") + if attached sess.get ("/", Void) as res then + check_response (res) + assert("was redirected", res.redirections_count > 0) end end @@ -377,6 +311,7 @@ feature -- Factory sess.set_credentials ("test", "test") create ctx.make_with_credentials_required if attached sess.get ("/password-ok.php", ctx) as res then + check_response (res) if attached {READABLE_STRING_8} res.body as l_body then assert ("Fetch all body, including closing html tag", l_body.has_substring ("")) else @@ -388,50 +323,58 @@ feature -- Factory test_get_with_query_parameters local sess: HTTP_CLIENT_SESSION - h: STRING_8 l_ctx: HTTP_CLIENT_REQUEST_CONTEXT q: STRING do - if attached global_requestbin_path as requestbin_path then - - -- GET REQUEST WITH A FILE AND FORM DATA - -- check requestbin to ensure the file and form parameters are correctly received - -- set filename to a local file - sess := new_session ("http://requestb.in") - create l_ctx.make - l_ctx.add_query_parameter ("?", "?first&arg") - l_ctx.add_query_parameter ("title", "Eiffel World!") - l_ctx.add_query_parameter ("path", "foo/bar") - l_ctx.add_query_parameter ("reserved", "+=&?") - l_ctx.add_query_parameter ("unreserved", ":!@'()*") - l_ctx.add_query_parameter ("unsafe", "%"[]{}") - l_ctx.add_query_parameter ("test", "!$&'()*") - l_ctx.add_query_parameter ("a&b", "a&b") - l_ctx.add_query_parameter ("lst[a][b]", "[abc][123]") - l_ctx.add_query_parameter ("foo(a,b)", "bar(1,2)*pi") - create q.make_empty - l_ctx.append_query_parameters_to_url (q) - assert("query", q.same_string ("??=?first%%26arg&title=Eiffel+World!&path=foo/bar&reserved=%%2B=%%26?&unreserved=:!@'()*&unsafe=%%22%%5B%%5D%%7B%%7D&test=!$%%26'()*&a%%26b=a%%26b&lst%%5Ba%%5D%%5Bb%%5D=%%5Babc%%5D%%5B123%%5D&foo(a,b)=bar(1,2)*pi")) + -- GET REQUEST WITH A FILE AND FORM DATA + -- check requestbin to ensure the file and form parameters are correctly received + -- set filename to a local file + sess := new_web_session + create l_ctx.make + l_ctx.add_query_parameter ("?", "?first&arg") + l_ctx.add_query_parameter ("title", "Eiffel World!") + l_ctx.add_query_parameter ("path", "foo/bar") + l_ctx.add_query_parameter ("reserved", "+=&?") + l_ctx.add_query_parameter ("unreserved", ":!@'()*") + l_ctx.add_query_parameter ("unsafe", "%"[]{}") + l_ctx.add_query_parameter ("test", "!$&'()*") + l_ctx.add_query_parameter ("a&b", "a&b") + l_ctx.add_query_parameter ("lst[a][b]", "[abc][123]") + l_ctx.add_query_parameter ("foo(a,b)", "bar(1,2)*pi") + create q.make_empty + l_ctx.append_query_parameters_to_url (q) + assert("query", q.same_string ("??=?first%%26arg&title=Eiffel+World!&path=foo/bar&reserved=%%2B=%%26?&unreserved=:!@'()*&unsafe=%%22%%5B%%5D%%7B%%7D&test=!$%%26'()*&a%%26b=a%%26b&lst%%5Ba%%5D%%5Bb%%5D=%%5Babc%%5D%%5B123%%5D&foo(a,b)=bar(1,2)*pi")) - create h.make_empty - if - attached sess.get (requestbin_path, l_ctx) as res and then - attached res.headers as hds - then - across - hds as c - loop - h.append (c.item.name + ": " + c.item.value + "%R%N") - end - end - print (h) - else - assert ("Has requestbin path", False) + if + attached sess.get ("", l_ctx) as res + then + check_response (res) end end +feature {NONE} -- Implementation + check_response (res: HTTP_CLIENT_RESPONSE) + local + h: STRING + do + assert ("ok", not res.error_occurred) + create h.make_empty + if + attached res.headers as hds + then + across + hds as c + loop + h.append (c.item.name + ": " + c.item.value + "%R%N") + end + end + print (h) + if attached res.body as b then + print (b) + end + end end From 10a83c6ad8415807e6b64fa94614f713ec58c612 Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Wed, 14 Jun 2017 16:26:31 +0200 Subject: [PATCH 6/8] Added possibility to create JWS object with specific algo hs256 or none easily. --- library/security/jwt/src/jws.e | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/library/security/jwt/src/jws.e b/library/security/jwt/src/jws.e index 37067915..82a7578a 100644 --- a/library/security/jwt/src/jws.e +++ b/library/security/jwt/src/jws.e @@ -1,6 +1,5 @@ note description: "Summary description for {JWS}." - author: "" date: "$Date$" revision: "$Revision$" @@ -9,19 +8,35 @@ class inherit JWT + redefine + default_create + end JWT_UTILITIES - undefine + redefine default_create end create default_create, + make_with_algorithm, make_with_claims, make_with_json_payload feature {NONE} -- Initialization + default_create + do + Precursor {JWT} + set_algorithm_to_hs256 + end + + make_with_algorithm (alg: like algorithm) + do + default_create + set_algorithm (alg) + end + make_with_claims (tb: STRING_TABLE [READABLE_STRING_GENERAL]) do default_create @@ -77,4 +92,14 @@ feature -- Element change header.set_algorithm (alg) end + set_algorithm_to_hs256 + do + set_algorithm (alg_hs256) + end + + set_algorithm_to_none + do + set_algorithm (alg_none) + end + end From 9e5e8bb1bf19b8e615d2d4fd302fbf92101575f8 Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Wed, 14 Jun 2017 16:27:24 +0200 Subject: [PATCH 7/8] Added simple way to set the issued_at claim value to current UTC date time. --- library/security/jwt/src/jwt_claimset.e | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/security/jwt/src/jwt_claimset.e b/library/security/jwt/src/jwt_claimset.e index 962a0729..6e45bb71 100644 --- a/library/security/jwt/src/jwt_claimset.e +++ b/library/security/jwt/src/jwt_claimset.e @@ -259,6 +259,11 @@ feature -- Element change end end + set_issued_at_now_utc + do + set_issued_at (create {DATE_TIME}.make_now_utc) + 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 From a6806c676a717293606174d740955a648032bb66 Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Tue, 20 Jun 2017 09:48:10 +0200 Subject: [PATCH 8/8] Updated ecf files to version 1-16-0 . Minor cosmetic changes. --- draft/src/gewf/src/gewf_generator.e | 5 ++--- library/security/jwt/jwt-safe.ecf | 16 ++-------------- library/security/jwt/jwt.ecf | 9 ++++++--- library/security/jwt/testing/testing.ecf | 13 ++++++++----- library/security/openid/package.iron | 1 - 5 files changed, 18 insertions(+), 26 deletions(-) diff --git a/draft/src/gewf/src/gewf_generator.e b/draft/src/gewf/src/gewf_generator.e index 389c606e..df81398d 100644 --- a/draft/src/gewf/src/gewf_generator.e +++ b/draft/src/gewf/src/gewf_generator.e @@ -113,14 +113,13 @@ feature -- Implementation across l_values as c loop - s.replace_substring_all ({STRING_32} "${" + c.key.as_string_32 + "}", c.item) + s.replace_substring_all ({STRING_32} "${" + c.key.to_string_32 + "}", c.item.to_string_32) end end end - note - copyright: "2011-2013, Jocelyn Fiat, Javier Velilla, Olivier Ligot, Eiffel Software and others" + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Olivier Ligot, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" source: "[ Eiffel Software diff --git a/library/security/jwt/jwt-safe.ecf b/library/security/jwt/jwt-safe.ecf index f2d8c544..075633dd 100644 --- a/library/security/jwt/jwt-safe.ecf +++ b/library/security/jwt/jwt-safe.ecf @@ -1,15 +1,3 @@ - - - - - - - - - - - - - + + diff --git a/library/security/jwt/jwt.ecf b/library/security/jwt/jwt.ecf index 8deb9ee8..7a370628 100644 --- a/library/security/jwt/jwt.ecf +++ b/library/security/jwt/jwt.ecf @@ -1,10 +1,13 @@ - + - - + + + + diff --git a/library/security/jwt/testing/testing.ecf b/library/security/jwt/testing/testing.ecf index 7bad43cb..09486123 100644 --- a/library/security/jwt/testing/testing.ecf +++ b/library/security/jwt/testing/testing.ecf @@ -1,13 +1,16 @@ - + - - - + + + + + - + diff --git a/library/security/openid/package.iron b/library/security/openid/package.iron index 80c50f34..55db2019 100644 --- a/library/security/openid/package.iron +++ b/library/security/openid/package.iron @@ -8,7 +8,6 @@ note title: Eiffel OpenID description: OpenID consumer library tags: openid,security,web,authentication,sso - license: Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt) copyright: 2011-2016, Jocelyn Fiat, Eiffel Software and others license: Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt) link[license]: http://www.eiffel.com/licensing/forum.txt