-module(jwerl). -export([sign/1, sign/2, sign/3, verify/1, verify/2, verify/3, verify/4, verify/5, header/1]). -on_load(conveniece_keys/0). -define(DEFAULT_ALG, <<"HS256">>). -define(DEFAULT_HEADER, #{typ => <<"JWT">>, alg => ?DEFAULT_ALG}). -type algorithm() :: hs256 | hs384 | hs512 | rs256 | rs384 | rs512 | es256 | es384 | es512 | none. % @equiv sign(Data, hs256, <<"">>) -spec sign(Data :: map()) -> binary(). sign(Data) -> sign(Data, hs256, <<"">>). % @equiv sign(Data, Algorithm, <<"">>) -spec sign(Data :: map(), Algorithm :: algorithm()) -> binary(). sign(Data, Algorithm) -> sign(Data, Algorithm, <<"">>). % @doc % Sign Data with the given Algorithm and KeyOrPem. % % Supported algorithms : %
% Token = jwerl:sign(#{key => <<"Hello World">>}, hs256, <<"s3cr3t k3y">>).
%
% @end
-spec sign(Data :: map() | list(), Algorithm :: algorithm(), KeyOrPem :: binary()) -> binary().
sign(Data, Algorithm, KeyOrPem) when (is_map(Data) orelse is_list(Data)), is_atom(Algorithm), is_binary(KeyOrPem) ->
encode(jsx:encode(Data), config_headers(#{alg => algorithm_to_binary(Algorithm)}), KeyOrPem).
% @equiv verify(Data, <<"">>, hs256, #{}, #{})
verify(Data) ->
verify(Data, hs256, <<"">>, #{}, #{}).
% @equiv verify(Data, Algorithm, <<"">>, #{}, #{})
verify(Data, Algorithm) ->
verify(Data, Algorithm, <<"">>, #{}, #{}).
% @equiv verify(Data, Algorithm, KeyOrPem, #{}, #{})
verify(Data, Algorithm, KeyOrPem) ->
verify(Data, Algorithm, KeyOrPem, #{}, #{}).
% @equiv verify(Data, Algorithm, KeyOrPem, #{}, #{})
verify(Data, Algorithm, KeyOrPem, Claims) ->
verify(Data, Algorithm, KeyOrPem, Claims, #{}).
% @doc
% Verify a JWToken according to the given Algorithm, KeyOrPem and Claims.
% This verifycation can ignore (CheckClaims =:= false) claims.
%
% This function support ext, nbt, iat, iss, sub, aud and jti.
%
% Options:
%
%
% jwerl:verify(Token, hs256, <<"s3cr3t k3y">>, #{sub => <<"hello">>,
% aud => [<<"world">>, <<"aliens">>]}).
%
% @end
-spec verify(Data :: binary(), Algorithm :: algorithm(), KeyOrPem :: binary(), CheckClaims :: map() | list() | false, Opts :: map() | list()) ->
{ok, map()} | {error, term()}.
verify(Data, Algorithm, KeyOrPem, Claims, Opts) ->
case decode(Data, KeyOrPem, Algorithm) of
{ok, TokenData} when is_map(Claims) orelse is_list(Claims) ->
case check_claims(TokenData, Claims, Opts) of
ok ->
{ok, TokenData};
Error ->
Error
end;
Result ->
Result
end.
% @doc
% Return the header for a given JWToken.
%
% Example:
%
% % jwerl:header(Token). %% @end -spec header(Data :: binary()) -> map(). header(Data) -> decode_header(Data). check_claims(TokenData, Claims, Opts) when is_map(Opts) -> check_claims(TokenData, Claims, maps:to_list(Opts)); check_claims(TokenData, Claims, Opts) when is_list(Opts) -> Now = os:system_time(seconds), claims_errors( [ check_claim(TokenData, exp, false, fun(ExpireTime) -> ExpLeeway = proplists:get_value(exp_leeway, Opts, 0), Now < ExpireTime + ExpLeeway end, exp), check_claim(TokenData, iat, false, fun(IssuedAt) -> IatLeeway = proplists:get_value(iat_leeway, Opts, 0), IssuedAt - IatLeeway =< Now end, iat), check_claim(TokenData, nbf, false, fun(NotBefore) -> NotBefore =< Now end, nbf) | [ check_claim( TokenData, Claim, true, fun(Value) -> claim_match(Expected, Value) end, Claim) || {Claim, Expected} <- get_claims(Claims) ] ], []); check_claims(TokenData, Claims, _Opts) -> check_claims(TokenData, Claims, []). claims_errors([], []) -> ok; claims_errors([], List) -> {error, List}; claims_errors([ok|Rest], Acc) -> claims_errors(Rest, Acc); claims_errors([{error, Error}|Rest], Acc) -> claims_errors(Rest, [Error|Acc]). claim_match(Expected, Value) -> case is_string_or_uri(Value) of true -> case Expected of Value -> true; List when is_list(List) -> lists:member(Value, List); _Other -> false end; false -> false end. check_claim(TokenData, Key, Required, F, FailReason) -> case get_claim(Key, TokenData) of error when Required =:= false -> %% Ignore if missing. If it has been correctly signed, %% this was intended. ok; error -> {error, FailReason}; {ok, Value} -> %% Call back if found for custom checking logic case F(Value) of true -> ok; false -> {error, FailReason} end end. get_claim(Claim, Map) when is_map(Map) -> maps:find(Claim, Map); get_claim(Claim, List) when is_list(List) -> case lists:keyfind(Claim, 1, List) of {Claim, Value} -> {ok, Value}; false -> error end. get_claims(Map) when is_map(Map) -> maps:to_list(Map); get_claims(List) when is_list(List) -> List. encode(Data, #{alg := <<"none">>} = Options, _) -> encode_input(Data, Options); encode(Data, Options, Key) -> Input = encode_input(Data, Options), <>. decode(Data, KeyOrPem, Algorithm) -> Header = decode_header(Data), case algorithm_to_atom(maps:get(alg, Header)) of Algorithm -> payload(Data, Algorithm, KeyOrPem); Algorithm1 -> {error, {invalid_algorithm, Algorithm1, Algorithm}} end. base64_encode(Data) -> Data1 = base64_encode_strip(lists:reverse(base64:encode_to_string(Data))), << << (urlencode_digit(D)) >> || <