blob: df3866f2341e04429c34f9aeaf97eb3f2c082cd8 [file] [log] [blame]
% 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)).
unknown_check_test() ->
?assertError({unknown_checks, [bar, foo]},
jwtf:decode(<<"a.b.c">>, [exp, foo, iss, bar, exp], 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.