277 lines
6.5 KiB
Plaintext
277 lines
6.5 KiB
Plaintext
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
|