diff options
author | Calvin Morrison <calvin@pobox.com> | 2025-09-03 21:15:36 -0400 |
---|---|---|
committer | Calvin Morrison <calvin@pobox.com> | 2025-09-03 21:15:36 -0400 |
commit | 49fa5aa2a127bdf8924d02bf77e5086b39c7a447 (patch) | |
tree | 61d86a7705dacc9fddccc29fa79d075d83ab8059 /server/_build/default/lib/jwt |
Diffstat (limited to 'server/_build/default/lib/jwt')
-rw-r--r-- | server/_build/default/lib/jwt/LICENSE | 20 | ||||
-rw-r--r-- | server/_build/default/lib/jwt/README.md | 101 | ||||
-rw-r--r-- | server/_build/default/lib/jwt/ebin/jwk.beam | bin | 0 -> 10844 bytes | |||
-rw-r--r-- | server/_build/default/lib/jwt/ebin/jwt.app | 9 | ||||
-rw-r--r-- | server/_build/default/lib/jwt/ebin/jwt.beam | bin | 0 -> 12536 bytes | |||
-rw-r--r-- | server/_build/default/lib/jwt/ebin/jwt_ecdsa.beam | bin | 0 -> 13516 bytes | |||
-rw-r--r-- | server/_build/default/lib/jwt/hex_metadata.config | 21 | ||||
-rw-r--r-- | server/_build/default/lib/jwt/include/jwt_ecdsa.hrl | 49 | ||||
-rw-r--r-- | server/_build/default/lib/jwt/rebar.config | 22 | ||||
-rw-r--r-- | server/_build/default/lib/jwt/rebar.config.script | 35 | ||||
-rw-r--r-- | server/_build/default/lib/jwt/rebar.lock | 11 | ||||
-rw-r--r-- | server/_build/default/lib/jwt/src/jwk.erl | 69 | ||||
-rw-r--r-- | server/_build/default/lib/jwt/src/jwt.app.src | 8 | ||||
-rw-r--r-- | server/_build/default/lib/jwt/src/jwt.erl | 340 | ||||
-rw-r--r-- | server/_build/default/lib/jwt/src/jwt_ecdsa.erl | 76 |
15 files changed, 761 insertions, 0 deletions
diff --git a/server/_build/default/lib/jwt/LICENSE b/server/_build/default/lib/jwt/LICENSE new file mode 100644 index 0000000..0e00cef --- /dev/null +++ b/server/_build/default/lib/jwt/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Katō + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/server/_build/default/lib/jwt/README.md b/server/_build/default/lib/jwt/README.md new file mode 100644 index 0000000..09e56f6 --- /dev/null +++ b/server/_build/default/lib/jwt/README.md @@ -0,0 +1,101 @@ + + +# jwt — Erlang JWT Library # +--------- + +[ +](https://travis-ci.org/artemeff/jwt) +[ +](https://coveralls.io/github/artemeff/jwt?branch=master) +[ +](https://hex.pm/packages/jwt) + +--------- + +JWT is a simple authorization token [format](https://jwt.io/) based on JSON. + + +#### <a name="Installation">Installation</a> #### + +If you use rebar (supports both 2 and 3 versions) or mix (Elixir): + +```erlang + +% in rebar.config for rebar3 +{deps, [{jwt}]}. + +% or for rebar2 +{deps, [{jwt, ".*", {git, "https://github.com/artemeff/jwt", {tag, "0.1.0"}}}]} + +``` + +```elixir + +% mix.exs +def deps do + [{:jwt, "~> 0.1"}] +end + +``` + +Or use it as git dependency. + + +#### <a name="Usage_example">Usage example</a> #### + +```erlang + +%% Create JWT token +> application:ensure_all_started(jwt). +> Key = <<"supas3cri7">>. +> Claims = [ + {user_id, 42}, + {user_name, <<"Bob">>} + ]. +> {ok, Token} = jwt:encode(<<"HS256">>, Claims, Key). +%% or with expiration +> ExpirationSeconds = 86400. +> {ok, Token} = jwt:encode(<<"HS256">>, Claims, ExpirationSeconds, Key). + +%% Parse JWT token +> {ok, Claims} = jwt:decode(Token, Key). +%% Issuer specific keys workflow + +%% The encoder just knows about itself +> Issuer = <<"iss1">>. +> IssuerKey = <<"Issuer-1-Key">>. +> Claims2 = [ + {iss, Issuer}, + {user_id, 42}, + {user_name, <<"Bob">>} + ]. +> {ok, Token2} = jwt:encode(<<"HS256">>, Claims, ExpirationSeconds, IssuerKey). + +%% Decoder Workflow +%% The decoder knows about all encoder keys (issuer specific) +> IssuerKeyMapping = #{ Issuer => IssuerKey, + <<"iss2">> => <<"Issuer2Key">>}. +> {ok, Claims} = jwt:decode(Token, <<"default-key">>, IssuerKeyMapping). + +``` +--------- + + +### <a name="Contributing">Contributing</a> ### +* Fork it +* Create your feature branch (`git checkout -b my-new-feature`) +* Commit your changes (`git commit -am 'add some feature'`) +* Push to the branch (`git push origin my-new-feature`) +* Create new Pull Request + + + + +## Modules ## + + +<table width="100%" border="0" summary="list of modules"> +<tr><td><a href="http://github.com/artemeff/jwt/blob/master/doc/jwk.md" class="module">jwk</a></td></tr> +<tr><td><a href="http://github.com/artemeff/jwt/blob/master/doc/jwt.md" class="module">jwt</a></td></tr> +<tr><td><a href="http://github.com/artemeff/jwt/blob/master/doc/jwt_ecdsa.md" class="module">jwt_ecdsa</a></td></tr></table> + diff --git a/server/_build/default/lib/jwt/ebin/jwk.beam b/server/_build/default/lib/jwt/ebin/jwk.beam Binary files differnew file mode 100644 index 0000000..c583f9c --- /dev/null +++ b/server/_build/default/lib/jwt/ebin/jwk.beam diff --git a/server/_build/default/lib/jwt/ebin/jwt.app b/server/_build/default/lib/jwt/ebin/jwt.app new file mode 100644 index 0000000..c792f7d --- /dev/null +++ b/server/_build/default/lib/jwt/ebin/jwt.app @@ -0,0 +1,9 @@ +{application,jwt, + [{description,"Erlang JWT library"}, + {vsn,"0.1.11"}, + {registered,[]}, + {applications,[kernel,stdlib,crypto,public_key,jsx,base64url]}, + {env,[]}, + {licenses,["MIT"]}, + {links,[{"GitHub","https://github.com/artemeff/jwt"}]}, + {modules,[jwk,jwt,jwt_ecdsa]}]}. diff --git a/server/_build/default/lib/jwt/ebin/jwt.beam b/server/_build/default/lib/jwt/ebin/jwt.beam Binary files differnew file mode 100644 index 0000000..fe6332a --- /dev/null +++ b/server/_build/default/lib/jwt/ebin/jwt.beam diff --git a/server/_build/default/lib/jwt/ebin/jwt_ecdsa.beam b/server/_build/default/lib/jwt/ebin/jwt_ecdsa.beam Binary files differnew file mode 100644 index 0000000..cb8ffde --- /dev/null +++ b/server/_build/default/lib/jwt/ebin/jwt_ecdsa.beam diff --git a/server/_build/default/lib/jwt/hex_metadata.config b/server/_build/default/lib/jwt/hex_metadata.config new file mode 100644 index 0000000..6de7750 --- /dev/null +++ b/server/_build/default/lib/jwt/hex_metadata.config @@ -0,0 +1,21 @@ +{<<"app">>,<<"jwt">>}. +{<<"build_tools">>,[<<"rebar3">>]}. +{<<"description">>,<<"Erlang JWT library">>}. +{<<"files">>, + [<<"LICENSE">>,<<"README.md">>,<<"include/jwt_ecdsa.hrl">>, + <<"rebar.config">>,<<"rebar.config.script">>,<<"rebar.lock">>, + <<"src/jwk.erl">>,<<"src/jwt.app.src">>,<<"src/jwt.erl">>, + <<"src/jwt_ecdsa.erl">>]}. +{<<"licenses">>,[<<"MIT">>]}. +{<<"links">>,[{<<"GitHub">>,<<"https://github.com/artemeff/jwt">>}]}. +{<<"name">>,<<"jwt">>}. +{<<"requirements">>, + [{<<"base64url">>, + [{<<"app">>,<<"base64url">>}, + {<<"optional">>,false}, + {<<"requirement">>,<<"~>0.0.1">>}]}, + {<<"jsx">>, + [{<<"app">>,<<"jsx">>}, + {<<"optional">>,false}, + {<<"requirement">>,<<"~>2.8.0">>}]}]}. +{<<"version">>,<<"0.1.11">>}. diff --git a/server/_build/default/lib/jwt/include/jwt_ecdsa.hrl b/server/_build/default/lib/jwt/include/jwt_ecdsa.hrl new file mode 100644 index 0000000..7f58843 --- /dev/null +++ b/server/_build/default/lib/jwt/include/jwt_ecdsa.hrl @@ -0,0 +1,49 @@ +-define(EC_GROUP_DEGREE, #{ + sect571r1 => 571, + sect571k1 => 571, + sect409r1 => 409, + sect409k1 => 409, + secp521r1 => 521, + secp384r1 => 384, + secp224r1 => 224, + secp224k1 => 224, + secp192k1 => 192, + secp160r2 => 160, + secp128r2 => 128, + secp128r1 => 128, + sect233r1 => 233, + sect233k1 => 233, + sect193r2 => 193, + sect193r1 => 193, + sect131r2 => 131, + sect131r1 => 131, + sect283r1 => 283, + sect283k1 => 283, + sect163r2 => 163, + secp256k1 => 256, + secp160k1 => 160, + secp160r1 => 160, + secp112r2 => 112, + secp112r1 => 112, + sect113r2 => 113, + sect113r1 => 113, + sect239k1 => 239, + sect163r1 => 163, + sect163k1 => 163, + secp256r1 => 256, + secp192r1 => 192, + brainpoolP160r1 => 160, + brainpoolP160t1 => 160, + brainpoolP192r1 => 192, + brainpoolP192t1 => 192, + brainpoolP224r1 => 224, + brainpoolP224t1 => 224, + brainpoolP256r1 => 256, + brainpoolP256t1 => 256, + brainpoolP320r1 => 320, + brainpoolP320t1 => 320, + brainpoolP384r1 => 384, + brainpoolP384t1 => 384, + brainpoolP512r1 => 512, + brainpoolP512t1 => 512 +}). diff --git a/server/_build/default/lib/jwt/rebar.config b/server/_build/default/lib/jwt/rebar.config new file mode 100644 index 0000000..3d00da9 --- /dev/null +++ b/server/_build/default/lib/jwt/rebar.config @@ -0,0 +1,22 @@ +{erl_opts, [warnings_as_errors]}. +{deps, [base64url, jsx]}. +{plugins, [coveralls]}. +{eunit_compile_opts, [export_all]}. + +{profiles, [ + {edown, [ + {deps, [ + {edown, "0.8.1"} + ]}, + {edoc_opts, [ + {doclet, edown_doclet}, + {top_level_readme, + {"./README.md", "http://github.com/artemeff/jwt", "master"}} + ]} + ]} +]}. + +{cover_enabled, true}. +{cover_export_enabled, true}. +{coveralls_coverdata, "_build/test/cover/eunit.coverdata"}. +{coveralls_service_name, "travis-ci"}. diff --git a/server/_build/default/lib/jwt/rebar.config.script b/server/_build/default/lib/jwt/rebar.config.script new file mode 100644 index 0000000..45cbf92 --- /dev/null +++ b/server/_build/default/lib/jwt/rebar.config.script @@ -0,0 +1,35 @@ +IsRebar3OrMix = case application:get_env(rebar, vsn) of + {ok, VSN} -> + [Major|_] = string:tokens(VSN, "."), + (list_to_integer(Major) >= 3); + + undefined -> + %% mix is used? + lists:keymember(mix, 1, application:loaded_applications()) + end, + +GitDeps = + [ {base64url, ".*", {git, "https://github.com/dvv/base64url", {tag, "v1.0"}}} + , {jsx, ".*", {git, "https://github.com/talentdeficit/jsx", {tag, "2.8.0"}}} + ], + +Config = case IsRebar3OrMix of + true -> CONFIG; + _ -> lists:keyreplace(deps, 1, CONFIG, {deps, GitDeps}) +end, + +ConfigCI = case os:getenv("CI") of + "true" -> + Plugins = [rebar3_lint | proplists:get_value(plugins, Config, [])], + lists:keystore(plugins, 1, Config, {plugins, Plugins}); + _ -> + Config +end, + +case os:getenv("TRAVIS") of + "true" -> + JobId = os:getenv("TRAVIS_JOB_ID"), + lists:keystore(coveralls_service_job_id, 1, ConfigCI, {coveralls_service_job_id, JobId}); + _ -> + ConfigCI +end. diff --git a/server/_build/default/lib/jwt/rebar.lock b/server/_build/default/lib/jwt/rebar.lock new file mode 100644 index 0000000..8e1c439 --- /dev/null +++ b/server/_build/default/lib/jwt/rebar.lock @@ -0,0 +1,11 @@ +{"1.2.0", +[{<<"base64url">>,{pkg,<<"base64url">>,<<"0.0.1">>},0}, + {<<"jsx">>,{pkg,<<"jsx">>,<<"2.8.0">>},0}]}. +[ +{pkg_hash,[ + {<<"base64url">>, <<"36A90125F5948E3AFD7BE97662A1504B934DD5DAC78451CA6E9ABF85A10286BE">>}, + {<<"jsx">>, <<"749BEC6D205C694AE1786D62CEA6CC45A390437E24835FD16D12D74F07097727">>}]}, +{pkg_hash_ext,[ + {<<"base64url">>, <<"FAB09B20E3F5DB886725544CBCF875B8E73EC93363954EB8A1A9ED834AA8C1F9">>}, + {<<"jsx">>, <<"A8BA15D5BAC2C48B2BE1224A0542AD794538D79E2CC16841A4E24CA75F0F8378">>}]} +]. diff --git a/server/_build/default/lib/jwt/src/jwk.erl b/server/_build/default/lib/jwt/src/jwk.erl new file mode 100644 index 0000000..9226aae --- /dev/null +++ b/server/_build/default/lib/jwt/src/jwk.erl @@ -0,0 +1,69 @@ +%% @doc RFC 7517: JSON Web Key (JWK) + +-module(jwk). +-include_lib("public_key/include/OTP-PUB-KEY.hrl"). + +-export([encode/2, decode/2]). + +-type id() :: binary(). +-type public_key() :: #'RSAPublicKey'{} | pem(). +-type pem() :: binary(). +-type json() :: binary(). + +-spec encode(id(), public_key()) -> {ok, json()} | {error, _}. +%% @doc encode Erlang/OTP Key to JWK +encode(Id, #'RSAPublicKey'{modulus = N, publicExponent = E}) -> + {ok, jsx:encode( + #{ + keys => + [ + #{ + kid => Id, + kty => <<"RSA">>, + n => encode_int(N), + e => encode_int(E) + } + ] + } + )}; +encode(Id, PEM) when is_binary(PEM) -> + [RSAEntry] = public_key:pem_decode(PEM), + encode(Id, public_key:pem_entry_decode(RSAEntry)); +encode(_, _) -> + {error, not_supported}. + +-spec decode(id(), json()) -> {ok, public_key()} | {error, _}. +%% @doc decode JWK to Erlang/OTP Key +decode(Id, #{<<"keys">> := JWTs}) -> + decode( + lists:dropwhile( + fun(X) -> + maps:get(<<"kid">>, X, undefined) /= Id + end, + JWTs + ) + ); +decode(Id, Json) when is_binary(Json) -> + decode(Id, jsx:decode(Json, [return_maps])). + +%% @private +decode([#{<<"kty">> := <<"RSA">>, <<"n">> := N, <<"e">> := E} | _]) -> + {ok, + #'RSAPublicKey'{ + modulus = decode_int(N), + publicExponent = decode_int(E) + } + }; +decode([]) -> + {error, not_found}; +decode(_) -> + {error, not_supported}. + + +%% @private +encode_int(X) -> + base64url:encode(binary:encode_unsigned(X)). + +%% @private +decode_int(X) -> + binary:decode_unsigned(base64url:decode(X)). diff --git a/server/_build/default/lib/jwt/src/jwt.app.src b/server/_build/default/lib/jwt/src/jwt.app.src new file mode 100644 index 0000000..b39bcb6 --- /dev/null +++ b/server/_build/default/lib/jwt/src/jwt.app.src @@ -0,0 +1,8 @@ +{application,jwt, + [{description,"Erlang JWT library"}, + {vsn,"0.1.11"}, + {registered,[]}, + {applications,[kernel,stdlib,crypto,public_key,jsx,base64url]}, + {env,[]}, + {licenses,["MIT"]}, + {links,[{"GitHub","https://github.com/artemeff/jwt"}]}]}. diff --git a/server/_build/default/lib/jwt/src/jwt.erl b/server/_build/default/lib/jwt/src/jwt.erl new file mode 100644 index 0000000..b17b679 --- /dev/null +++ b/server/_build/default/lib/jwt/src/jwt.erl @@ -0,0 +1,340 @@ +%% @doc JWT Library for Erlang. +%% +%% Written by Peter Hizalev at Kato (http://kato.im) +%% +%% Rewritten by Yuri Artemev (http://artemff.com) +%% +%% @end +-module(jwt). + +-export([decode/2, decode/3]). +-export([encode/3, encode/4]). + +-define(HOUR, 3600). +-define(DAY, (?HOUR * 24)). + +%% Handle version compatibility for crypto +-ifdef(OTP_RELEASE). + -if(?OTP_RELEASE >= 23). + -define(HMAC(Type, Key, Data), crypto:mac(hmac, Type, Key, Data)). + -else. + -define(HMAC(Type, Key, Data), crypto:hmac(Type, Key, Data)). + -endif. +-else. + -define(HMAC(Type, Key, Data), crypto:hmac(Type, Key, Data)). +-endif. + +-type expiration() :: {hourly, non_neg_integer()} | {daily, non_neg_integer()} | non_neg_integer(). +-type context() :: map(). + +%% +%% API +%% +-spec encode( + Alg :: binary(), + ClaimsSet :: map() | list(), + Key :: binary() | public_key:private_key() +) -> {ok, Token :: binary()} | {error, any()}. +%% @doc Creates a token from given data and signs it with a given secret +%% +%% Parameters are +%% <ul> +%% <li> +%% `Alg' is a binary one of +%% +%% [HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512, PS256, PS384, PS512] +%% +%% But only [HS256, HS384, HS512, RS256] are supported +%% </li> +%% <li>`ClaimsSet' the payload of the token. Can be both map and proplist</li> +%% <li>`Key' binary in case of hmac encryption and private key if rsa</li> +%% </ul> +%% +%% @end +encode(Alg, ClaimsSet, Key) -> + Claims = base64url:encode(jsx:encode(ClaimsSet)), + Header = base64url:encode(jsx:encode(jwt_header(Alg))), + Payload = <<Header/binary, ".", Claims/binary>>, + case jwt_sign(Alg, Payload, Key) of + undefined -> {error, algorithm_not_supported}; + Signature -> {ok, <<Payload/binary, ".", Signature/binary>>} + end. + +-spec encode( + Alg :: binary(), + ClaimsSet :: map() | list(), + Expiration :: expiration(), + Key :: binary() | public_key:private_key() +) -> {ok, Token :: binary()} | {error, any()}. +%% @doc Creates a token from given data and signs it with a given secret +%% +%% and also adds `exp' claim to payload +%% +%% `Expiration' can be one of the tuples: +%% `{hourly, SecondsAfterBeginningOfCurrentHour}', +%% `{daily, SecondsAfterBeginningOfCurrentDay}' +%% or can be just an integer representing the amount of seconds +%% the token will live +%% +%% @end +encode(Alg, ClaimsSet, Expiration, Key) -> + Claims = jwt_add_exp(ClaimsSet, Expiration), + encode(Alg, Claims, Key). + +-spec decode( + Token :: binary(), + Key :: binary() | public_key:public_key() | public_key:private_key() +) -> {ok, Claims :: map()} | {error, any()}. +%% @doc Decodes a token, checks the signature and returns the content of the token +%% +%% <ul> +%% <li>`Token' is a JWT itself</li> +%% <li>`Key' is a secret phrase or public/private key depend on encryption algorithm</li> +%% </ul> +%% +%% @end +decode(Token, Key) -> + decode(Token, Key, #{}). + +% When there are multiple issuers and keys are on a per issuer bases +% then apply those keys instead +-spec decode( + Token :: binary(), + DefaultKey :: binary() | public_key:public_key() | public_key:private_key(), + IssuerKeyMapping :: map() +) -> {ok, Claims :: map()} | {error, any()}. +%% @doc Decode with an issuer key mapping +%% +%% Receives the issuer key mapping as the last parameter +%% +%% @end +decode(Token, DefaultKey, IssuerKeyMapping) -> + result(reduce_while(fun(F, Acc) -> apply(F, [Acc]) end, #{token => Token}, [ + fun split_token/1, + fun decode_jwt/1, + fun (Context) -> + get_key(Context, DefaultKey, IssuerKeyMapping) + end, + fun check_signature/1, + fun check_expired/1 + ])). + +result(#{claims_json := ClaimsJSON}) -> + {ok, ClaimsJSON}; +result({error, _} = Error) -> + Error. + +reduce_while(_Fun, Acc, []) -> + Acc; +reduce_while(Fun, Acc, [Item|Rest]) -> + case Fun(Item, Acc) of + {cont, NewAcc} -> + reduce_while(Fun, NewAcc, Rest); + {halt, Result} -> + Result + end. + +-spec split_token(Context :: context()) -> + {cont, context()} | {halt, {error, invalid_token}}. +%% @private +split_token(#{token := Token} = Context) -> + case binary:split(Token, <<".">>, [global]) of + [Header, Claims, Signature] -> + {cont, maps:merge(Context, #{ + header => Header, + claims => Claims, + signature => Signature + })}; + _ -> + {halt, {error, invalid_token}} + end. + +-spec decode_jwt(context()) -> {cont, context()} | {halt, {error, invalid_token}}. +%% @private +decode_jwt(#{header := Header, claims := Claims} = Context) -> + try + [HeaderJSON, ClaimsJSON] = + Decoded = [jsx_decode_safe(base64url:decode(X)) || X <- [Header, Claims]], + case lists:any(fun(E) -> E =:= invalid end, Decoded) of + false -> + {cont, maps:merge(Context, #{ + header_json => HeaderJSON, + claims_json => ClaimsJSON + })}; + true -> + {halt, {error, invalid_token}} + end + catch _:_ -> + {halt, {error, invalid_token}} + end. + +%% @private +get_key(#{claims_json := Claims} = Context, DefaultKey, IssuerKeyMapping) -> + Issuer = maps:get(<<"iss">>, Claims, undefined), + Key = maps:get(Issuer, IssuerKeyMapping, DefaultKey), + {cont, maps:merge(Context, #{key => Key})}. + +%% @private +check_signature(#{ + key := Key, + header := Header, + claims := Claims, + signature := Signature, + header_json := #{<<"alg">> := Alg} +} = Context) -> + case jwt_check_sig(Alg, Header, Claims, Signature, Key) of + true -> + {cont, Context}; + false -> + {halt, {error, invalid_signature}} + end. + +%% @private +check_expired(#{claims_json := ClaimsJSON} = Context) -> + case jwt_is_expired(ClaimsJSON) of + true -> + {halt, {error, expired}}; + false -> + {cont, Context} + end. + +%% +%% Decoding helpers +%% +-spec jsx_decode_safe(binary()) -> map() | invalid. +%% @private +jsx_decode_safe(Bin) -> + try + jsx:decode(Bin, [return_maps]) + catch _ -> + invalid + end. + +-spec jwt_is_expired(map()) -> boolean(). +%% @private +jwt_is_expired(#{<<"exp">> := Exp} = _ClaimsJSON) -> + case (Exp - epoch()) of + DeltaSecs when DeltaSecs > 0 -> false; + _ -> true + end; +jwt_is_expired(_) -> + false. + +-spec jwt_check_sig( + Alg :: binary(), + Header :: binary(), + Claims :: binary(), + Signature :: binary(), + Key :: binary() | public_key:public_key() | public_key:private_key() +) -> boolean(). +%% @private +jwt_check_sig(Alg, Header, Claims, Signature, Key) -> + jwt_check_sig(algorithm_to_crypto(Alg), <<Header/binary, ".", Claims/binary>>, Signature, Key). + +-spec jwt_check_sig( + {atom(), atom()}, + Payload :: binary(), + Signature :: binary(), + Key :: binary() | public_key:public_key() | public_key:private_key() +) -> boolean(). +%% @private +jwt_check_sig({hmac, _} = Alg, Payload, Signature, Key) -> + jwt_sign_with_crypto(Alg, Payload, Key) =:= Signature; + +jwt_check_sig({Algo, Crypto}, Payload, Signature, Pem) + when (Algo =:= rsa orelse Algo =:= ecdsa) andalso is_binary(Pem) -> + jwt_check_sig({Algo, Crypto}, Payload, Signature, pem_to_key(Pem)); + +jwt_check_sig({rsa, Crypto}, Payload, Signature, Key) -> + public_key:verify(Payload, Crypto, base64url:decode(Signature), Key); + +jwt_check_sig({ecdsa, Crypto}, Payload, Signature, Key) -> + public_key:verify(Payload, Crypto, jwt_ecdsa:signature(Signature), Key); + +jwt_check_sig(_, _, _, _) -> + false. + +%% +%% Encoding helpers +%% +-spec jwt_add_exp(ClaimsSet :: map() | list(), Expiration :: expiration()) -> map() | list(). +%% @private +jwt_add_exp(ClaimsSet, Expiration) -> + Exp = expiration_to_epoch(Expiration), + append_claim(ClaimsSet, <<"exp">>, Exp). + +-spec jwt_header(Alg :: binary()) -> list(). +jwt_header(Alg) -> + [ {<<"alg">>, Alg} + , {<<"typ">>, <<"JWT">>} + ]. + +%% +%% Helpers +%% +-spec jwt_sign( + Alg :: binary(), + Payload :: binary(), + Key :: binary() | public_key:private_key() +) -> binary() | undefined. +%% @private +jwt_sign(Alg, Payload, Key) -> + jwt_sign_with_crypto(algorithm_to_crypto(Alg), Payload, Key). + +jwt_sign_with_crypto({hmac, Crypto}, Payload, Key) -> + base64url:encode(?HMAC(Crypto, Key, Payload)); + +jwt_sign_with_crypto({Algo, Crypto}, Payload, Pem) + when (Algo =:= rsa orelse Algo =:= ecdsa) andalso is_binary(Pem) -> + jwt_sign_with_crypto({Algo, Crypto}, Payload, pem_to_key(Pem)); + +jwt_sign_with_crypto({rsa, Crypto}, Payload, Key) -> + base64url:encode(public_key:sign(Payload, Crypto, Key)); + +jwt_sign_with_crypto({ecdsa, Crypto}, Payload, Key) -> + base64url:encode(jwt_ecdsa:signature(Payload, Crypto, Key)); + +jwt_sign_with_crypto(_, _Payload, _Key) -> + undefined. + +-spec algorithm_to_crypto(binary()) -> {atom(), atom()} | undefined. +%% @private +algorithm_to_crypto(<<"HS256">>) -> {hmac, sha256}; +algorithm_to_crypto(<<"HS384">>) -> {hmac, sha384}; +algorithm_to_crypto(<<"HS512">>) -> {hmac, sha512}; +algorithm_to_crypto(<<"RS256">>) -> {rsa, sha256}; +algorithm_to_crypto(<<"RS384">>) -> {rsa, sha384}; +algorithm_to_crypto(<<"RS512">>) -> {rsa, sha512}; +algorithm_to_crypto(<<"ES256">>) -> {ecdsa, sha256}; +algorithm_to_crypto(_) -> undefined. + +-spec epoch() -> non_neg_integer(). +%% @private +epoch() -> erlang:system_time(seconds). + +-spec expiration_to_epoch(Expiration :: expiration()) -> neg_integer(). +%% @private +expiration_to_epoch(Expiration) -> + expiration_to_epoch(Expiration, epoch()). + +expiration_to_epoch(Expiration, Ts) -> + case Expiration of + {hourly, Expiration0} -> (Ts - (Ts rem ?HOUR)) + Expiration0; + {daily, Expiration0} -> (Ts - (Ts rem ?DAY)) + Expiration0; + _ -> epoch() + Expiration + end. + +-spec append_claim(ClaimsSet :: map() | list(), binary(), any()) -> map() | list(). +%% @private +append_claim(ClaimsSet, Key, Val) when is_map(ClaimsSet) -> + ClaimsSet#{ Key => Val }; +append_claim(ClaimsSet, Key, Val) -> [{ Key, Val } | ClaimsSet]. + +pem_to_key(Pem) -> + Decoded = case public_key:pem_decode(Pem) of + [_, Key] -> + Key; + [Key] -> + Key + end, + public_key:pem_entry_decode(Decoded). diff --git a/server/_build/default/lib/jwt/src/jwt_ecdsa.erl b/server/_build/default/lib/jwt/src/jwt_ecdsa.erl new file mode 100644 index 0000000..7596e90 --- /dev/null +++ b/server/_build/default/lib/jwt/src/jwt_ecdsa.erl @@ -0,0 +1,76 @@ +%% @doc Eliptic curve digital signature algorithm +%% +%% Helper functions for encoding/decoding ECDSA signature +%% +%% @end +-module(jwt_ecdsa). + +-include_lib("jwt_ecdsa.hrl"). +-include_lib("public_key/include/public_key.hrl"). + +-export([ + signature/1, + signature/3 +]). + +%% @doc Signature for JWT verification +%% +%% Transcode the ECDSA Base64-encoded signature into ASN.1/DER format +%% +%% @end +signature(Base64Sig) -> + Signature = base64url:decode(Base64Sig), + SignatureLen = byte_size(Signature), + {RBin, SBin} = split_binary(Signature, (SignatureLen div 2)), + R = crypto:bytes_to_integer(RBin), + S = crypto:bytes_to_integer(SBin), + public_key:der_encode('ECDSA-Sig-Value', #'ECDSA-Sig-Value'{ r = R, s = S }). + +%% @doc Signature to sign JWT +%% +%% Transcodes the JCA ASN.1/DER-encoded signature into the concatenated R + S format +%% a.k.a <em>raw</em> format +%% +%% @end +signature(Payload, Crypto, Key) -> + Der = public_key:sign(Payload, Crypto, Key), + raw(Der, Key). + +raw(Der, #'ECPrivateKey'{parameters = {namedCurve, NamedCurve}}) -> + #'ECDSA-Sig-Value'{ r = R, s = S } = public_key:der_decode('ECDSA-Sig-Value', Der), + CurveName = pubkey_cert_records:namedCurves(NamedCurve), + GroupDegree = group_degree(CurveName), + Size = (GroupDegree + 7) div 8, + RBin = int_to_bin(R), + SBin = int_to_bin(S), + RPad = pad(RBin, Size), + SPad = pad(SBin, Size), + <<RPad/binary, SPad/binary>>. + +%% @private +int_to_bin(X) when X < 0 -> + int_to_bin_neg(X, []); +int_to_bin(X) -> + int_to_bin_pos(X, []). + +%% @private +int_to_bin_pos(0, Ds = [_|_]) -> + list_to_binary(Ds); +int_to_bin_pos(X, Ds) -> + int_to_bin_pos(X bsr 8, [(X band 255)|Ds]). + +%% @private +int_to_bin_neg(-1, Ds = [MSB|_]) when MSB >= 16#80 -> + list_to_binary(Ds); +int_to_bin_neg(X, Ds) -> + int_to_bin_neg(X bsr 8, [(X band 255)|Ds]). + +%% @private +pad(Bin, Size) when byte_size(Bin) =:= Size -> + Bin; +pad(Bin, Size) when byte_size(Bin) < Size -> + pad(<<0, Bin/binary>>, Size). + +%% See the OpenSSL documentation for EC_GROUP_get_degree() +group_degree(CurveName) -> + maps:get(CurveName, ?EC_GROUP_DEGREE). |