diff options
author | Jay Doane <jay.s.doane@gmail.com> | 2017-06-08 13:41:12 -0700 |
---|---|---|
committer | Jay Doane <jay.s.doane@gmail.com> | 2017-06-08 13:41:12 -0700 |
commit | d9a718b8cbb68259b3611b44e1eeac9f4b15e0e1 (patch) | |
tree | 410d1624cb02c9fa84575c1b9ba57be3bf6d91f7 | |
parent | b396a1d1bc818c5138d78e74668ac94be1ef8dd1 (diff) | |
download | couchdb-d9a718b8cbb68259b3611b44e1eeac9f4b15e0e1.tar.gz |
Support JWT encoding
Implement jwtf:encode/3 for encoding JSON Web Tokens. Test encode/decode
round trip for each supported alg.
-rw-r--r-- | src/jwtf.erl | 159 |
1 files changed, 117 insertions, 42 deletions
diff --git a/src/jwtf.erl b/src/jwtf.erl index 78b36a9c3..a461da98d 100644 --- a/src/jwtf.erl +++ b/src/jwtf.erl @@ -17,7 +17,53 @@ -module(jwtf). --export([decode/3]). +-export([ + encode/3, + decode/3 +]). + +-define(ALGS, [ + {<<"RS256">>, {public_key, sha256}}, % RSA PKCS#1 signature with SHA-256 + {<<"RS384">>, {public_key, sha384}}, + {<<"RS512">>, {public_key, sha512}}, + {<<"ES256">>, {public_key, sha256}}, + {<<"ES384">>, {public_key, sha384}}, + {<<"ES512">>, {public_key, sha512}}, + {<<"HS256">>, {hmac, sha256}}, + {<<"HS384">>, {hmac, sha384}}, + {<<"HS512">>, {hmac, sha512}}]). + +-define(VALID_ALGS, proplists:get_keys(?ALGS)). + + +% @doc encode +% Encode the JSON Header and Claims using Key and Alg obtained from Header +-spec encode(term(), term(), term()) -> + {ok, binary()} | no_return(). +encode(Header = {HeaderProps}, Claims, Key) -> + try + Alg = case prop(<<"alg">>, HeaderProps) of + undefined -> + throw(missing_alg); + Val -> + Val + end, + EncodedHeader = b64url:encode(jiffy:encode(Header)), + EncodedClaims = b64url:encode(jiffy:encode(Claims)), + Message = <<EncodedHeader/binary, $., EncodedClaims/binary>>, + SignatureOrMac = case verification_algorithm(Alg) of + {public_key, Algorithm} -> + public_key:sign(Message, Algorithm, Key); + {hmac, Algorithm} -> + crypto:hmac(Algorithm, Key, Message) + end, + EncodedSignatureOrMac = b64url:encode(SignatureOrMac), + {ok, <<Message/binary, $., EncodedSignatureOrMac/binary>>} + catch + throw:Error -> + {error, Error} + end. + % @doc decode % Decodes the supplied encoded token, checking @@ -35,6 +81,19 @@ decode(EncodedToken, Checks, KS) -> end. +% @doc verification_algorithm +% Return {VerificationMethod, Algorithm} tuple for the specified Alg +-spec verification_algorithm(binary()) -> + {atom(), atom()} | no_return(). +verification_algorithm(Alg) -> + case lists:keyfind(Alg, 1, ?ALGS) of + {Alg, Val} -> + Val; + false -> + throw(invalid_alg) + end. + + validate(Header0, Payload0, Signature, Checks, KS) -> Header1 = props(decode_json(Header0)), validate_header(Header1, Checks), @@ -70,26 +129,13 @@ validate_typ(Props, Checks) -> validate_alg(Props, Checks) -> Required = prop(alg, Checks), Alg = prop(<<"alg">>, Props), - Valid = [ - <<"RS256">>, - <<"RS384">>, - <<"RS512">>, - - <<"HS256">>, - <<"HS384">>, - <<"HS512">>, - - <<"ES384">>, - <<"ES512">>, - <<"ES512">> - ], case {Required, Alg} of {undefined, _} -> ok; {true, undefined} -> throw({error, missing_alg}); {true, Alg} -> - case lists:member(Alg, Valid) of + case lists:member(Alg, ?VALID_ALGS) of true -> ok; false -> @@ -179,35 +225,20 @@ key(Props, Checks, KS) -> end. -verify(Alg, Header, Payload, Signature0, Key) -> +verify(Alg, Header, Payload, SignatureOrMac0, Key) -> Message = <<Header/binary, $., Payload/binary>>, - Signature1 = b64url:decode(Signature0), - case Alg of - <<"RS256">> -> - public_key_verify(sha256, Message, Signature1, Key); - <<"RS384">> -> - public_key_verify(sha384, Message, Signature1, Key); - <<"RS512">> -> - public_key_verify(sha512, Message, Signature1, Key); - - <<"ES256">> -> - public_key_verify(sha256, Message, Signature1, Key); - <<"ES384">> -> - public_key_verify(sha384, Message, Signature1, Key); - <<"ES512">> -> - public_key_verify(sha512, Message, Signature1, Key); - - <<"HS256">> -> - hmac_verify(sha256, Message, Signature1, Key); - <<"HS384">> -> - hmac_verify(sha384, Message, Signature1, Key); - <<"HS512">> -> - hmac_verify(sha512, Message, Signature1, Key) + SignatureOrMac1 = b64url:decode(SignatureOrMac0), + {VerificationMethod, Algorithm} = verification_algorithm(Alg), + case VerificationMethod of + public_key -> + public_key_verify(Algorithm, Message, SignatureOrMac1, Key); + hmac -> + hmac_verify(Algorithm, Message, SignatureOrMac1, Key) end. -public_key_verify(Alg, Message, Signature, PublicKey) -> - case public_key:verify(Message, Alg, Signature, PublicKey) of +public_key_verify(Algorithm, Message, Signature, PublicKey) -> + case public_key:verify(Message, Algorithm, Signature, PublicKey) of true -> ok; false -> @@ -215,8 +246,8 @@ public_key_verify(Alg, Message, Signature, PublicKey) -> end. -hmac_verify(Alg, Message, HMAC, SecretKey) -> - case crypto:hmac(Alg, SecretKey, Message) of +hmac_verify(Algorithm, Message, HMAC, SecretKey) -> + case crypto:hmac(Algorithm, SecretKey, Message) of HMAC -> ok; _ -> @@ -443,4 +474,48 @@ rs256_test() -> ?assertMatch({ok, ExpectedPayload}, decode(EncodedToken, Checks, KS)). +encode_missing_alg_test() -> + ?assertEqual({error, missing_alg}, + encode({[]}, {[]}, <<"foo">>)). + + +encode_invalid_alg_test() -> + ?assertEqual({error, invalid_alg}, + encode({[{<<"alg">>, <<"BOGUS">>}]}, {[]}, <<"foo">>)). + + +encode_decode_test_() -> + [{Alg, encode_decode(Alg)} || Alg <- ?VALID_ALGS]. + + +encode_decode(Alg) -> + {EncodeKey, DecodeKey} = case verification_algorithm(Alg) of + {public_key, Algorithm} -> + jwtf_test_util:create_keypair(); + {hmac, Algorithm} -> + Key = <<"a-super-secret-key">>, + {Key, Key} + end, + Claims = claims(), + {ok, Encoded} = encode(header(Alg), Claims, EncodeKey), + KS = fun(_, _) -> DecodeKey end, + {ok, Decoded} = decode(Encoded, [], KS), + ?_assertMatch(Claims, Decoded). + + +header(Alg) -> + {[ + {<<"typ">>, <<"JWT">>}, + {<<"alg">>, Alg}, + {<<"kid">>, <<"20170520-00:00:00">>} + ]}. + + +claims() -> + EpochSeconds = 1496205841, + {[ + {<<"iat">>, EpochSeconds}, + {<<"exp">>, EpochSeconds + 3600} + ]}. + -endif. |