New JSON Web Token (JWT) library.
This commit is contained in:
26
library/security/jwt/README.md
Normal file
26
library/security/jwt/README.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
|
|
||||||
14
library/security/jwt/jwt.ecf
Normal file
14
library/security/jwt/jwt.ecf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||||
|
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-16-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-16-0 http://www.eiffel.com/developers/xml/configuration-1-16-0.xsd" name="jwt" uuid="A75C2D84-D543-4708-BAF3-254C308376CC" library_target="jwt">
|
||||||
|
<target name="jwt">
|
||||||
|
<root all_classes="true"/>
|
||||||
|
<capability>
|
||||||
|
<void_safety support="all"/>
|
||||||
|
</capability>
|
||||||
|
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
|
||||||
|
<library name="crypto" location="$ISE_LIBRARY\unstable\library\text\encryption\crypto\crypto.ecf"/>
|
||||||
|
<library name="encoder" location="$ISE_LIBRARY\contrib\library\web\framework\ewf\text\encoder\encoder.ecf"/>
|
||||||
|
<library name="json" location="$ISE_LIBRARY\contrib\library\text\parser\json\library\json.ecf"/>
|
||||||
|
<cluster name="src" location="src\" recursive="true"/>
|
||||||
|
</target>
|
||||||
|
</system>
|
||||||
16
library/security/jwt/package.iron
Normal file
16
library/security/jwt/package.iron
Normal file
@@ -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
|
||||||
265
library/security/jwt/src/jwt.e
Normal file
265
library/security/jwt/src/jwt.e
Normal file
@@ -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
|
||||||
79
library/security/jwt/testing/test_jwt.e
Normal file
79
library/security/jwt/testing/test_jwt.e
Normal file
@@ -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
|
||||||
15
library/security/jwt/testing/testing.ecf
Normal file
15
library/security/jwt/testing/testing.ecf
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||||
|
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-16-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-16-0 http://www.eiffel.com/developers/xml/configuration-1-16-0.xsd" name="testing" uuid="DB49E98A-0048-414A-A469-EE9B5B903BF3">
|
||||||
|
<target name="testing">
|
||||||
|
<root class="ANY" feature="default_create"/>
|
||||||
|
<setting name="console_application" value="false"/>
|
||||||
|
<capability>
|
||||||
|
<concurrency support="none"/>
|
||||||
|
<void_safety support="all"/>
|
||||||
|
</capability>
|
||||||
|
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
|
||||||
|
<library name="jwt" location="..\jwt.ecf" readonly="false"/>
|
||||||
|
<library name="testing" location="$ISE_LIBRARY\library\testing\testing.ecf"/>
|
||||||
|
<tests name="src" location=".\" recursive="true"/>
|
||||||
|
</target>
|
||||||
|
</system>
|
||||||
Reference in New Issue
Block a user