summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Newson <rnewson@apache.org>2020-03-12 11:56:37 +0000
committerRobert Newson <rnewson@apache.org>2020-03-12 11:56:37 +0000
commit087f7271ba9bf00ad7630c8617c471c6a5cc2160 (patch)
tree3ee207a4bf29717359c656242e5ecc911f4f6040
parentaf2eb048cb8f8ebf4b529795f984697d0ed760c5 (diff)
parent588eadea7925c634e62ec38d595850378f95d2a1 (diff)
downloadcouchdb-087f7271ba9bf00ad7630c8617c471c6a5cc2160.tar.gz
merged jwtf into src/jwtf
-rw-r--r--src/jwtf/.gitignore4
-rw-r--r--src/jwtf/LICENSE176
-rw-r--r--src/jwtf/README.md18
-rw-r--r--src/jwtf/rebar.config2
-rw-r--r--src/jwtf/src/jwtf.app.src30
-rw-r--r--src/jwtf/src/jwtf.erl316
-rw-r--r--src/jwtf/test/jwtf_tests.erl305
7 files changed, 851 insertions, 0 deletions
diff --git a/src/jwtf/.gitignore b/src/jwtf/.gitignore
new file mode 100644
index 000000000..5eadeac89
--- /dev/null
+++ b/src/jwtf/.gitignore
@@ -0,0 +1,4 @@
+*~
+_build/
+doc/
+rebar.lock
diff --git a/src/jwtf/LICENSE b/src/jwtf/LICENSE
new file mode 100644
index 000000000..d9a10c0d8
--- /dev/null
+++ b/src/jwtf/LICENSE
@@ -0,0 +1,176 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/src/jwtf/README.md b/src/jwtf/README.md
new file mode 100644
index 000000000..e6038fbc0
--- /dev/null
+++ b/src/jwtf/README.md
@@ -0,0 +1,18 @@
+# jwtf
+
+JSON Web Token Functions
+
+This library provides JWT parsing and validation functions
+
+Supports;
+
+* Verify
+* RS256
+* RS384
+* RS512
+* HS256
+* HS384
+* HS512
+* ES256
+* ES384
+* ES512
diff --git a/src/jwtf/rebar.config b/src/jwtf/rebar.config
new file mode 100644
index 000000000..e0d18443b
--- /dev/null
+++ b/src/jwtf/rebar.config
@@ -0,0 +1,2 @@
+{cover_enabled, true}.
+{cover_print_enabled, true}.
diff --git a/src/jwtf/src/jwtf.app.src b/src/jwtf/src/jwtf.app.src
new file mode 100644
index 000000000..304bb9e0a
--- /dev/null
+++ b/src/jwtf/src/jwtf.app.src
@@ -0,0 +1,30 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+{application, jwtf, [
+ {description, "JSON Web Token Functions"},
+ {vsn, git},
+ {registered, []},
+ {applications, [
+ kernel,
+ stdlib,
+ b64url,
+ crypto,
+ jiffy,
+ public_key
+ ]},
+ {env,[]},
+ {modules, []},
+ {maintainers, []},
+ {licenses, []},
+ {links, []}
+]}.
diff --git a/src/jwtf/src/jwtf.erl b/src/jwtf/src/jwtf.erl
new file mode 100644
index 000000000..8e58e0897
--- /dev/null
+++ b/src/jwtf/src/jwtf.erl
@@ -0,0 +1,316 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+% @doc
+% This module decodes and validates JWT tokens. Almost all property
+% checks are optional. If not checked, the presence or validity of the
+% field is not verified. Signature check is mandatory, though.
+
+-module(jwtf).
+
+-export([
+ encode/3,
+ decode/3,
+ valid_algorithms/0,
+ verification_algorithm/1
+]).
+
+-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}}]).
+
+
+% @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({bad_request, <<"Missing alg header parameter">>});
+ 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
+% for the attributes defined in Checks and calling
+% the key store function to retrieve the key needed
+% to verify the signature
+decode(EncodedToken, Checks, KS) ->
+ try
+ [Header, Payload, Signature] = split(EncodedToken),
+ validate(Header, Payload, Signature, Checks, KS),
+ {ok, decode_b64url_json(Payload)}
+ catch
+ throw:Error ->
+ {error, Error}
+ end.
+
+
+% @doc valid_algorithms
+% Return a list of supported algorithms
+-spec valid_algorithms() -> [binary()].
+valid_algorithms() ->
+ proplists:get_keys(?ALGS).
+
+
+% @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({bad_request, <<"Invalid alg header parameter">>})
+ end.
+
+
+validate(Header0, Payload0, Signature, Checks, KS) ->
+ Header1 = props(decode_b64url_json(Header0)),
+ validate_header(Header1, Checks),
+
+ Payload1 = props(decode_b64url_json(Payload0)),
+ validate_payload(Payload1, Checks),
+
+ Alg = prop(<<"alg">>, Header1),
+ Key = key(Header1, Checks, KS),
+ verify(Alg, Header0, Payload0, Signature, Key).
+
+
+validate_header(Props, Checks) ->
+ validate_typ(Props, Checks),
+ validate_alg(Props, Checks).
+
+
+validate_typ(Props, Checks) ->
+ Required = prop(typ, Checks),
+ TYP = prop(<<"typ">>, Props),
+ case {Required, TYP} of
+ {undefined, _} ->
+ ok;
+ {true, undefined} ->
+ throw({bad_request, <<"Missing typ header parameter">>});
+ {true, <<"JWT">>} ->
+ ok;
+ {true, _} ->
+ throw({bad_request, <<"Invalid typ header parameter">>})
+ end.
+
+
+validate_alg(Props, Checks) ->
+ Required = prop(alg, Checks),
+ Alg = prop(<<"alg">>, Props),
+ case {Required, Alg} of
+ {undefined, _} ->
+ ok;
+ {true, undefined} ->
+ throw({bad_request, <<"Missing alg header parameter">>});
+ {true, Alg} ->
+ case lists:member(Alg, valid_algorithms()) of
+ true ->
+ ok;
+ false ->
+ throw({bad_request, <<"Invalid alg header parameter">>})
+ end
+ end.
+
+
+%% Not all these fields have to be present, but if they _are_ present
+%% they must be valid.
+validate_payload(Props, Checks) ->
+ validate_iss(Props, Checks),
+ validate_iat(Props, Checks),
+ validate_nbf(Props, Checks),
+ validate_exp(Props, Checks).
+
+
+validate_iss(Props, Checks) ->
+ ExpectedISS = prop(iss, Checks),
+ ActualISS = prop(<<"iss">>, Props),
+
+ case {ExpectedISS, ActualISS} of
+ {undefined, _} ->
+ ok;
+ {_ISS, undefined} ->
+ throw({bad_request, <<"Missing iss claim">>});
+ {ISS, ISS} ->
+ ok;
+ {_, _} ->
+ throw({bad_request, <<"Invalid iss claim">>})
+ end.
+
+
+validate_iat(Props, Checks) ->
+ Required = prop(iat, Checks),
+ IAT = prop(<<"iat">>, Props),
+
+ case {Required, IAT} of
+ {undefined, _} ->
+ ok;
+ {true, undefined} ->
+ throw({bad_request, <<"Missing iat claim">>});
+ {true, IAT} when is_integer(IAT) ->
+ ok;
+ {true, _} ->
+ throw({bad_request, <<"Invalid iat claim">>})
+ end.
+
+
+validate_nbf(Props, Checks) ->
+ Required = prop(nbf, Checks),
+ NBF = prop(<<"nbf">>, Props),
+
+ case {Required, NBF} of
+ {undefined, _} ->
+ ok;
+ {true, undefined} ->
+ throw({bad_request, <<"Missing nbf claim">>});
+ {true, IAT} ->
+ assert_past(<<"nbf">>, IAT)
+ end.
+
+
+validate_exp(Props, Checks) ->
+ Required = prop(exp, Checks),
+ EXP = prop(<<"exp">>, Props),
+
+ case {Required, EXP} of
+ {undefined, _} ->
+ ok;
+ {true, undefined} ->
+ throw({bad_request, <<"Missing exp claim">>});
+ {true, EXP} ->
+ assert_future(<<"exp">>, EXP)
+ end.
+
+
+key(Props, Checks, KS) ->
+ Alg = prop(<<"alg">>, Props),
+ Required = prop(kid, Checks),
+ KID = prop(<<"kid">>, Props),
+ case {Required, KID} of
+ {true, undefined} ->
+ throw({bad_request, <<"Missing kid claim">>});
+ {_, KID} ->
+ KS(Alg, KID)
+ end.
+
+
+verify(Alg, Header, Payload, SignatureOrMac0, Key) ->
+ Message = <<Header/binary, $., Payload/binary>>,
+ 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(Algorithm, Message, Signature, PublicKey) ->
+ case public_key:verify(Message, Algorithm, Signature, PublicKey) of
+ true ->
+ ok;
+ false ->
+ throw({bad_request, <<"Bad signature">>})
+ end.
+
+
+hmac_verify(Algorithm, Message, HMAC, SecretKey) ->
+ case crypto:hmac(Algorithm, SecretKey, Message) of
+ HMAC ->
+ ok;
+ _ ->
+ throw({bad_request, <<"Bad HMAC">>})
+ end.
+
+
+split(EncodedToken) ->
+ case binary:split(EncodedToken, <<$.>>, [global]) of
+ [_, _, _] = Split -> Split;
+ _ -> throw({bad_request, <<"Malformed token">>})
+ end.
+
+
+decode_b64url_json(B64UrlEncoded) ->
+ try
+ case b64url:decode(B64UrlEncoded) of
+ {error, Reason} ->
+ throw({bad_request, Reason});
+ JsonEncoded ->
+ jiffy:decode(JsonEncoded)
+ end
+ catch
+ error:Error ->
+ throw({bad_request, Error})
+ end.
+
+
+props({Props}) ->
+ Props;
+
+props(_) ->
+ throw({bad_request, <<"Not an object">>}).
+
+
+assert_past(Name, Time) ->
+ case Time < now_seconds() of
+ true ->
+ ok;
+ false ->
+ throw({unauthorized, <<Name/binary, " not in past">>})
+ end.
+
+assert_future(Name, Time) ->
+ case Time > now_seconds() of
+ true ->
+ ok;
+ false ->
+ throw({unauthorized, <<Name/binary, " not in future">>})
+ end.
+
+
+now_seconds() ->
+ {MegaSecs, Secs, _MicroSecs} = os:timestamp(),
+ MegaSecs * 1000000 + Secs.
+
+
+prop(Prop, Props) ->
+ proplists:get_value(Prop, Props).
diff --git a/src/jwtf/test/jwtf_tests.erl b/src/jwtf/test/jwtf_tests.erl
new file mode 100644
index 000000000..dcebe5f40
--- /dev/null
+++ b/src/jwtf/test/jwtf_tests.erl
@@ -0,0 +1,305 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(jwtf_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("public_key/include/public_key.hrl").
+
+encode(Header0, Payload0) ->
+ Header1 = b64url:encode(jiffy:encode(Header0)),
+ Payload1 = b64url:encode(jiffy:encode(Payload0)),
+ Sig = b64url:encode(<<"bad">>),
+ <<Header1/binary, $., Payload1/binary, $., Sig/binary>>.
+
+valid_header() ->
+ {[{<<"typ">>, <<"JWT">>}, {<<"alg">>, <<"RS256">>}]}.
+
+jwt_io_pubkey() ->
+ PublicKeyPEM = <<"-----BEGIN PUBLIC KEY-----\n"
+ "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGH"
+ "FHYLugdUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6"
+ "dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkl"
+ "e+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB\n"
+ "-----END PUBLIC KEY-----\n">>,
+ [PEMEntry] = public_key:pem_decode(PublicKeyPEM),
+ public_key:pem_entry_decode(PEMEntry).
+
+
+b64_badarg_test() ->
+ Encoded = <<"0.0.0">>,
+ ?assertEqual({error, {bad_request,badarg}},
+ jwtf:decode(Encoded, [], nil)).
+
+
+b64_bad_block_test() ->
+ Encoded = <<" aGVsbG8. aGVsbG8. aGVsbG8">>,
+ ?assertEqual({error, {bad_request,{bad_block,0}}},
+ jwtf:decode(Encoded, [], nil)).
+
+
+invalid_json_test() ->
+ Encoded = <<"fQ.fQ.fQ">>,
+ ?assertEqual({error, {bad_request,{1,invalid_json}}},
+ jwtf:decode(Encoded, [], nil)).
+
+
+truncated_json_test() ->
+ Encoded = <<"ew.ew.ew">>,
+ ?assertEqual({error, {bad_request,{2,truncated_json}}},
+ jwtf:decode(Encoded, [], nil)).
+
+
+missing_typ_test() ->
+ Encoded = encode({[]}, []),
+ ?assertEqual({error, {bad_request,<<"Missing typ header parameter">>}},
+ jwtf:decode(Encoded, [typ], nil)).
+
+
+invalid_typ_test() ->
+ Encoded = encode({[{<<"typ">>, <<"NOPE">>}]}, []),
+ ?assertEqual({error, {bad_request,<<"Invalid typ header parameter">>}},
+ jwtf:decode(Encoded, [typ], nil)).
+
+
+missing_alg_test() ->
+ Encoded = encode({[]}, []),
+ ?assertEqual({error, {bad_request,<<"Missing alg header parameter">>}},
+ jwtf:decode(Encoded, [alg], nil)).
+
+
+invalid_alg_test() ->
+ Encoded = encode({[{<<"alg">>, <<"NOPE">>}]}, []),
+ ?assertEqual({error, {bad_request,<<"Invalid alg header parameter">>}},
+ jwtf:decode(Encoded, [alg], nil)).
+
+
+missing_iss_test() ->
+ Encoded = encode(valid_header(), {[]}),
+ ?assertEqual({error, {bad_request,<<"Missing iss claim">>}},
+ jwtf:decode(Encoded, [{iss, right}], nil)).
+
+
+invalid_iss_test() ->
+ Encoded = encode(valid_header(), {[{<<"iss">>, <<"wrong">>}]}),
+ ?assertEqual({error, {bad_request,<<"Invalid iss claim">>}},
+ jwtf:decode(Encoded, [{iss, right}], nil)).
+
+
+missing_iat_test() ->
+ Encoded = encode(valid_header(), {[]}),
+ ?assertEqual({error, {bad_request,<<"Missing iat claim">>}},
+ jwtf:decode(Encoded, [iat], nil)).
+
+
+invalid_iat_test() ->
+ Encoded = encode(valid_header(), {[{<<"iat">>, <<"hello">>}]}),
+ ?assertEqual({error, {bad_request,<<"Invalid iat claim">>}},
+ jwtf:decode(Encoded, [iat], nil)).
+
+
+missing_nbf_test() ->
+ Encoded = encode(valid_header(), {[]}),
+ ?assertEqual({error, {bad_request,<<"Missing nbf claim">>}},
+ jwtf:decode(Encoded, [nbf], nil)).
+
+
+invalid_nbf_test() ->
+ Encoded = encode(valid_header(), {[{<<"nbf">>, 2 * now_seconds()}]}),
+ ?assertEqual({error, {unauthorized, <<"nbf not in past">>}},
+ jwtf:decode(Encoded, [nbf], nil)).
+
+
+missing_exp_test() ->
+ Encoded = encode(valid_header(), {[]}),
+ ?assertEqual({error, {bad_request, <<"Missing exp claim">>}},
+ jwtf:decode(Encoded, [exp], nil)).
+
+
+invalid_exp_test() ->
+ Encoded = encode(valid_header(), {[{<<"exp">>, 0}]}),
+ ?assertEqual({error, {unauthorized, <<"exp not in future">>}},
+ jwtf:decode(Encoded, [exp], nil)).
+
+
+missing_kid_test() ->
+ Encoded = encode({[]}, {[]}),
+ ?assertEqual({error, {bad_request, <<"Missing kid claim">>}},
+ jwtf:decode(Encoded, [kid], nil)).
+
+
+public_key_not_found_test() ->
+ Encoded = encode(
+ {[{<<"alg">>, <<"RS256">>}, {<<"kid">>, <<"1">>}]},
+ {[]}),
+ KS = fun(_, _) -> throw(not_found) end,
+ Expected = {error, not_found},
+ ?assertEqual(Expected, jwtf:decode(Encoded, [], KS)).
+
+
+bad_rs256_sig_test() ->
+ Encoded = encode(
+ {[{<<"typ">>, <<"JWT">>}, {<<"alg">>, <<"RS256">>}]},
+ {[]}),
+ KS = fun(<<"RS256">>, undefined) -> jwt_io_pubkey() end,
+ ?assertEqual({error, {bad_request, <<"Bad signature">>}},
+ jwtf:decode(Encoded, [], KS)).
+
+
+bad_hs256_sig_test() ->
+ Encoded = encode(
+ {[{<<"typ">>, <<"JWT">>}, {<<"alg">>, <<"HS256">>}]},
+ {[]}),
+ KS = fun(<<"HS256">>, undefined) -> <<"bad">> end,
+ ?assertEqual({error, {bad_request, <<"Bad HMAC">>}},
+ jwtf:decode(Encoded, [], KS)).
+
+
+malformed_token_test() ->
+ ?assertEqual({error, {bad_request, <<"Malformed token">>}},
+ jwtf:decode(<<"a.b.c.d">>, [], nil)).
+
+
+%% jwt.io generated
+hs256_test() ->
+ EncodedToken = <<"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMzQ1Ni"
+ "J9.eyJpc3MiOiJodHRwczovL2Zvby5jb20iLCJpYXQiOjAsImV4cCI"
+ "6MTAwMDAwMDAwMDAwMDAsImtpZCI6ImJhciJ9.iS8AH11QHHlczkBn"
+ "Hl9X119BYLOZyZPllOVhSBZ4RZs">>,
+ KS = fun(<<"HS256">>, <<"123456">>) -> <<"secret">> end,
+ Checks = [{iss, <<"https://foo.com">>}, iat, exp, typ, alg, kid],
+ ?assertMatch({ok, _}, catch jwtf:decode(EncodedToken, Checks, KS)).
+
+
+%% pip install PyJWT
+%% > import jwt
+%% > jwt.encode({'foo':'bar'}, 'secret', algorithm='HS384')
+hs384_test() ->
+ EncodedToken = <<"eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIif"
+ "Q.2quwghs6I56GM3j7ZQbn-ASZ53xdBqzPzTDHm_CtVec32LUy-Ezy"
+ "L3JjIe7WjL93">>,
+ KS = fun(<<"HS384">>, _) -> <<"secret">> end,
+ ?assertMatch({ok, {[{<<"foo">>,<<"bar">>}]}},
+ catch jwtf:decode(EncodedToken, [], KS)).
+
+
+%% pip install PyJWT
+%% > import jwt
+%% > jwt.encode({'foo':'bar'}, 'secret', algorithm='HS512')
+hs512_test() ->
+ EncodedToken = <<"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYX"
+ "IifQ.WePl7achkd0oGNB8XRF_LJwxlyiPZqpdNgdKpDboAjSTsW"
+ "q-aOGNynTp8TOv8KjonFym8vwFwppXOLoLXbkIaQ">>,
+ KS = fun(<<"HS512">>, _) -> <<"secret">> end,
+ ?assertMatch({ok, {[{<<"foo">>,<<"bar">>}]}},
+ catch jwtf:decode(EncodedToken, [], KS)).
+
+
+%% jwt.io generated
+rs256_test() ->
+ EncodedToken = <<"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0N"
+ "TY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.Ek"
+ "N-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8j"
+ "O19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF"
+ "39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn"
+ "5-HIirE">>,
+
+ Checks = [sig, alg],
+ KS = fun(<<"RS256">>, undefined) -> jwt_io_pubkey() end,
+
+ ExpectedPayload = {[
+ {<<"sub">>, <<"1234567890">>},
+ {<<"name">>, <<"John Doe">>},
+ {<<"admin">>, true}
+ ]},
+
+ ?assertMatch({ok, ExpectedPayload}, jwtf:decode(EncodedToken, Checks, KS)).
+
+
+encode_missing_alg_test() ->
+ ?assertEqual({error, {bad_request, <<"Missing alg header parameter">>}},
+ jwtf:encode({[]}, {[]}, <<"foo">>)).
+
+
+encode_invalid_alg_test() ->
+ ?assertEqual({error, {bad_request, <<"Invalid alg header parameter">>}},
+ jwtf:encode({[{<<"alg">>, <<"BOGUS">>}]}, {[]}, <<"foo">>)).
+
+
+encode_decode_test_() ->
+ [{Alg, encode_decode(Alg)} || Alg <- jwtf:valid_algorithms()].
+
+
+encode_decode(Alg) ->
+ {EncodeKey, DecodeKey} = case jwtf:verification_algorithm(Alg) of
+ {public_key, _Algorithm} ->
+ create_keypair();
+ {hmac, _Algorithm} ->
+ Key = <<"a-super-secret-key">>,
+ {Key, Key}
+ end,
+ Claims = claims(),
+ {ok, Encoded} = jwtf:encode(header(Alg), Claims, EncodeKey),
+ KS = fun(_, _) -> DecodeKey end,
+ {ok, Decoded} = jwtf: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}
+ ]}.
+
+create_keypair() ->
+ %% https://tools.ietf.org/html/rfc7517#appendix-C
+ N = decode(<<"t6Q8PWSi1dkJj9hTP8hNYFlvadM7DflW9mWepOJhJ66w7nyoK1gPNqFMSQRy"
+ "O125Gp-TEkodhWr0iujjHVx7BcV0llS4w5ACGgPrcAd6ZcSR0-Iqom-QFcNP"
+ "8Sjg086MwoqQU_LYywlAGZ21WSdS_PERyGFiNnj3QQlO8Yns5jCtLCRwLHL0"
+ "Pb1fEv45AuRIuUfVcPySBWYnDyGxvjYGDSM-AqWS9zIQ2ZilgT-GqUmipg0X"
+ "OC0Cc20rgLe2ymLHjpHciCKVAbY5-L32-lSeZO-Os6U15_aXrk9Gw8cPUaX1"
+ "_I8sLGuSiVdt3C_Fn2PZ3Z8i744FPFGGcG1qs2Wz-Q">>),
+ E = decode(<<"AQAB">>),
+ D = decode(<<"GRtbIQmhOZtyszfgKdg4u_N-R_mZGU_9k7JQ_jn1DnfTuMdSNprTeaSTyWfS"
+ "NkuaAwnOEbIQVy1IQbWVV25NY3ybc_IhUJtfri7bAXYEReWaCl3hdlPKXy9U"
+ "vqPYGR0kIXTQRqns-dVJ7jahlI7LyckrpTmrM8dWBo4_PMaenNnPiQgO0xnu"
+ "ToxutRZJfJvG4Ox4ka3GORQd9CsCZ2vsUDmsXOfUENOyMqADC6p1M3h33tsu"
+ "rY15k9qMSpG9OX_IJAXmxzAh_tWiZOwk2K4yxH9tS3Lq1yX8C1EWmeRDkK2a"
+ "hecG85-oLKQt5VEpWHKmjOi_gJSdSgqcN96X52esAQ">>),
+ RSAPrivateKey = #'RSAPrivateKey'{
+ modulus = N,
+ publicExponent = E,
+ privateExponent = D
+ },
+ RSAPublicKey = #'RSAPublicKey'{
+ modulus = N,
+ publicExponent = E
+ },
+ {RSAPrivateKey, RSAPublicKey}.
+
+
+decode(Goop) ->
+ crypto:bytes_to_integer(b64url:decode(Goop)).
+
+
+now_seconds() ->
+ {MegaSecs, Secs, _MicroSecs} = os:timestamp(),
+ MegaSecs * 1000000 + Secs.