You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by rn...@apache.org on 2020/04/01 14:31:08 UTC

[couchdb] 01/08: Merge pull request #2658 from apache/import-jwtf

This is an automated email from the ASF dual-hosted git repository.

rnewson pushed a commit to branch backport-jwt-3.x
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 9c99366f94fd9d3dd19a34e44afac0faec2fafcd
Author: Robert Newson <rn...@apache.org>
AuthorDate: Thu Mar 12 14:10:11 2020 +0000

    Merge pull request #2658 from apache/import-jwtf
    
    Import jwtf
---
 rebar.config.script          |   1 +
 rel/reltool.config           |   2 +
 src/jwtf/.gitignore          |   4 +
 src/jwtf/LICENSE             | 176 ++++++++++++++++++++++++
 src/jwtf/README.md           |  18 +++
 src/jwtf/rebar.config        |   2 +
 src/jwtf/src/jwtf.app.src    |  30 ++++
 src/jwtf/src/jwtf.erl        | 316 +++++++++++++++++++++++++++++++++++++++++++
 src/jwtf/test/jwtf_tests.erl | 305 +++++++++++++++++++++++++++++++++++++++++
 9 files changed, 854 insertions(+)

diff --git a/rebar.config.script b/rebar.config.script
index 1dcad56..408ad3d 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -132,6 +132,7 @@ SubDirs = [
     "src/fabric",
     "src/global_changes",
     "src/ioq",
+    "src/jwtf",
     "src/ken",
     "src/mango",
     "src/rexi",
diff --git a/rel/reltool.config b/rel/reltool.config
index 5285504..7960192 100644
--- a/rel/reltool.config
+++ b/rel/reltool.config
@@ -51,6 +51,7 @@
         ibrowse,
         ioq,
         jiffy,
+        jwtf,
         ken,
         khash,
         mango,
@@ -110,6 +111,7 @@
     {app, ibrowse, [{incl_cond, include}]},
     {app, ioq, [{incl_cond, include}]},
     {app, jiffy, [{incl_cond, include}]},
+    {app, jwtf, [{incl_cond, include}]},
     {app, ken, [{incl_cond, include}]},
     {app, khash, [{incl_cond, include}]},
     {app, mango, [{incl_cond, include}]},
diff --git a/src/jwtf/.gitignore b/src/jwtf/.gitignore
new file mode 100644
index 0000000..5eadeac
--- /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 0000000..d9a10c0
--- /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 0000000..e6038fb
--- /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 0000000..e0d1844
--- /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 0000000..304bb9e
--- /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 0000000..8e58e08
--- /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 0000000..dcebe5f
--- /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.