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