Last active
August 24, 2017 17:10
-
-
Save potatosalad/88e6c10eaad3cbd1d6650b2f9fa32358 to your computer and use it in GitHub Desktop.
Snippets from unreleased IAM erlang library
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| %% -*- mode: erlang; tab-width: 4; indent-tabs-mode: 1; st-rulers: [70] -*- | |
| %% vim: ts=4 sw=4 ft=erlang noet | |
| %%%------------------------------------------------------------------- | |
| %%% @author Andrew Bennett <andrew@pixid.com> | |
| %%% @copyright 2017, Andrew Bennett | |
| %%% @doc | |
| %%% | |
| %%% @end | |
| %%% Created : 24 Apr 2017 by Andrew Bennett <andrew@pixid.com> | |
| %%%------------------------------------------------------------------- | |
| -module(iam_assert). | |
| -include("iam_error.hrl"). | |
| -include_lib("jose/include/jose.hrl"). | |
| %% Types | |
| -type claim_check() :: | |
| fun((any()) -> boolean()). | |
| -export_type([claim_check/0]). | |
| -type t() :: #{ | |
| '__struct__' := ?MODULE, | |
| loaded := boolean(), | |
| validated := boolean(), | |
| verified := boolean(), | |
| assertion := nil | binary(), | |
| audience := nil | binary() | claim_check(), | |
| authorized_party := nil | binary() | claim_check(), | |
| checks := #{ | |
| binary() => term() | claim_check() | |
| }, | |
| claims := nil | map(), | |
| issuer := nil | binary() | claim_check(), | |
| jwt_id := nil | binary() | {hash, atom()} | claim_check(), | |
| max_age := nil | non_neg_integer(), | |
| not_before := nil | non_neg_integer() | claim_check(), | |
| now := nil | non_neg_integer(), | |
| protected := nil | map(), | |
| public_key := nil | map(), | |
| required := #{ | |
| expiration_time := boolean(), | |
| issued_at := boolean(), | |
| not_before := boolean(), | |
| atom() => boolean() | |
| }, | |
| subject := nil | binary() | claim_check(), | |
| window := non_neg_integer(), | |
| atom() => any() | |
| }. | |
| -export_type([t/0]). | |
| %% Elixir API | |
| -export(['__struct__'/0]). | |
| -export(['__struct__'/1]). | |
| %% API | |
| -export([new/0]). | |
| -export([new/1]). | |
| -export([load/2]). | |
| -export([check/2]). | |
| -export([check/3]). | |
| -export([not_before/1]). | |
| -export([not_before/2]). | |
| -export([require/2]). | |
| %% JWT API | |
| -export([authenticate/3]). | |
| -export([authenticate/4]). | |
| -export([decrypt/4]). | |
| -export(['info!'/1]). | |
| -export([info/1]). | |
| -export(['info!'/2]). | |
| -export([info/2]). | |
| -export([type/1]). | |
| -export([validate/1]). | |
| -export([validate/2]). | |
| -export([verify/3]). | |
| -export([verify/4]). | |
| %%%=================================================================== | |
| %%% Elixir API functions | |
| %%%=================================================================== | |
| '__struct__'() -> | |
| #{ | |
| '__struct__' => ?MODULE, | |
| loaded => false, | |
| validated => false, | |
| verified => false, | |
| assertion => nil, | |
| audience => nil, | |
| authorized_party => nil, | |
| checks => #{}, | |
| claims => nil, | |
| issuer => nil, | |
| jwt_id => nil, | |
| max_age => nil, | |
| not_before => nil, | |
| now => nil, | |
| protected => nil, | |
| public_key => nil, | |
| required => #{ | |
| expiration_time => true, | |
| issued_at => true, | |
| not_before => true | |
| }, | |
| subject => nil, | |
| window => 5 | |
| }. | |
| '__struct__'(List) when is_list(List) -> | |
| '__struct__'(maps:from_list(List)); | |
| '__struct__'(BaseMap0) when is_map(BaseMap0) -> | |
| {BaseReq, BaseMap} = | |
| case maps:take(required, BaseMap0) of | |
| {BR, BM} when is_map(BR) -> | |
| {BR, BM}; | |
| {BR, BM} when is_list(BR) -> | |
| {maps:from_list(BR), BM}; | |
| error -> | |
| {#{}, BaseMap0} | |
| end, | |
| S0 = #{ required := R0 } = '__struct__'(), | |
| R1 = maps:merge(R0, BaseReq), | |
| S1 = S0#{ required := R1 }, | |
| maps:fold(fun maps:update/3, S1, BaseMap). | |
| %%%=================================================================== | |
| %%% API functions | |
| %%%=================================================================== | |
| new() -> | |
| new(#{}). | |
| new(BaseMap) when is_map(BaseMap) -> | |
| '__struct__'(BaseMap); | |
| new(List) when is_list(List) -> | |
| new(maps:from_list(List)). | |
| load(Assert0=#{ | |
| '__struct__' := ?MODULE, | |
| loaded := false, | |
| assertion := nil, | |
| claims := nil | |
| }, Assertion) -> | |
| try jose_jwt:to_map(jose_jwt:peek_payload(Assertion)) of | |
| {_, Claims} when is_map(Claims) -> | |
| Assert = Assert0#{ loaded := true, assertion := Assertion, claims := Claims }, | |
| Assert; | |
| _ -> | |
| ?iam_throw(iam, invalid_assertion, Assert0) | |
| catch | |
| _:_ -> | |
| ?iam_throw(iam, invalid_assertion, Assert0) | |
| end. | |
| check(A=#{ '__struct__' := ?MODULE }, [{Key, Check} | Checks]) -> | |
| check(check(A, Key, Check), Checks); | |
| check(A=#{ '__struct__' := ?MODULE }, []) -> | |
| A. | |
| check(A=#{ '__struct__' := ?MODULE }, K, Check) when is_atom(K) -> | |
| Key = | |
| case K of | |
| audience -> <<"aud">>; | |
| authorized_party -> <<"azp">>; | |
| expiration_time -> <<"exp">>; | |
| issued_at -> <<"iat">>; | |
| issuer -> <<"iss">>; | |
| jwt_id -> <<"jti">>; | |
| not_before -> <<"nbf">>; | |
| subject -> <<"sub">>; | |
| _ -> erlang:atom_to_binary(K, unicode) | |
| end, | |
| check(A, Key, Check); | |
| check(A=#{ checks := C }, K, V) when is_binary(K) -> | |
| A#{ checks := maps:put(K, V, C) }. | |
| not_before(Assert=#{ '__struct__' := ?MODULE, not_before := nil }) -> | |
| not_before(Assert, os:system_time(second)). | |
| not_before(Assert=#{ '__struct__' := ?MODULE, not_before := nil }, NotBefore) when is_integer(NotBefore) andalso NotBefore >= 0 -> | |
| Assert#{ not_before := NotBefore }; | |
| not_before(Assert=#{ '__struct__' := ?MODULE, not_before := nil }, nil) -> | |
| not_before(Assert, 0). | |
| require(A=#{ required := R }, K) when K == audience orelse K == <<"aud">> -> | |
| A#{ required := maps:put(audience, true, R) }; | |
| require(A=#{ required := R }, K) when K == authorized_party orelse K == <<"azp">> -> | |
| A#{ required := maps:put(authorized_party, true, R) }; | |
| require(A=#{ required := R }, K) when K == expiration_time orelse K == <<"exp">> -> | |
| A#{ required := maps:put(expiration_time, true, R) }; | |
| require(A=#{ required := R }, K) when K == issued_at orelse K == <<"iat">> -> | |
| A#{ required := maps:put(issued_at, true, R) }; | |
| require(A=#{ required := R }, K) when K == issuer orelse K == <<"iss">> -> | |
| A#{ required := maps:put(issuer, true, R) }; | |
| require(A=#{ required := R }, K) when K == jwt_id orelse K == <<"jti">> -> | |
| A#{ required := maps:put(jwt_id, true, R) }; | |
| require(A=#{ required := R }, K) when K == not_before orelse K == <<"nbf">> -> | |
| A#{ required := maps:put(not_before, true, R) }; | |
| require(A=#{ required := R }, K) when K == subject orelse K == <<"sub">> -> | |
| A#{ required := maps:put(subject, true, R) }; | |
| require(A=#{ '__struct__' := ?MODULE }, K) when is_atom(K) -> | |
| require(A, erlang:atom_to_binary(K, unicode)); | |
| require(A=#{ required := R }, K) when is_binary(K) -> | |
| A#{ required := maps:put(K, true, R) }; | |
| require(A=#{ '__struct__' := ?MODULE }, [H | T]) -> | |
| require(require(A, H), T); | |
| require(A=#{ '__struct__' := ?MODULE }, []) -> | |
| A. | |
| %%%=================================================================== | |
| %%% JWT API functions | |
| %%%=================================================================== | |
| authenticate(Assert0=#{ | |
| '__struct__' := ?MODULE, | |
| loaded := true, | |
| verified := false, | |
| validated := false | |
| }, AllowedSigningAlgs, JWK) -> | |
| Assert1 = validate(Assert0), | |
| Assert2 = verify(Assert1, AllowedSigningAlgs, JWK), | |
| Assert2. | |
| authenticate(Assert=#{ | |
| '__struct__' := ?MODULE, | |
| loaded := false | |
| }, Assertion, Allowed, JWK) -> | |
| try binary:split(Assertion, << $. >>, [global]) of | |
| [_, _, _, _, _] -> | |
| validate(decrypt(Assert, Assertion, Allowed, JWK)); | |
| [_, _, _] -> | |
| authenticate(load(Assert, Assertion), Allowed, JWK); | |
| _ -> | |
| ?iam_throw(iam, invalid_assertion, Assert) | |
| catch | |
| _:_ -> | |
| ?iam_throw(iam, invalid_assertion, Assert) | |
| end. | |
| decrypt(Assert0=#{ | |
| '__struct__' := ?MODULE, | |
| loaded := false, | |
| verified := false, | |
| validated := false, | |
| assertion := nil, | |
| claims := nil | |
| }, Assertion, AllowedEncryption0, JWK0) when is_binary(Assertion) -> | |
| AllowedEncryption = [element(2, jose_jwe:to_map(iam_util:cast_jwe(AE))) || AE <- AllowedEncryption0], | |
| ValidEncryption = | |
| try jose_jwe:expand(Assertion) of | |
| {_, #{ <<"protected">> := EncodedProtected }} -> | |
| try iam_json:decode(iam_base64url:'decode!'(EncodedProtected)) of | |
| JWEMap when is_map(JWEMap) -> | |
| valid_encryption(JWEMap, AllowedEncryption); | |
| _ -> | |
| ?iam_throw(iam, invalid_assertion, Assert0) | |
| catch | |
| _:_ -> | |
| ?iam_raise(throw, iam, invalid_assertion, Assert0) | |
| end; | |
| _ -> | |
| ?iam_throw(iam, invalid_assertion, Assert0) | |
| catch | |
| _:_ -> | |
| ?iam_raise(throw, iam, invalid_assertion, Assert0) | |
| end, | |
| case ValidEncryption of | |
| true -> | |
| JWK = iam_util:cast_jwk(JWK0), | |
| try jose_jwe:block_decrypt(JWK, Assertion) of | |
| {JWTPlainText, JWE=#jose_jwe{}} when is_binary(JWTPlainText) -> | |
| try iam_json:decode(JWTPlainText) of | |
| Claims when is_map(Claims) -> | |
| {_, Protected} = jose_jwe:to_map(JWE), | |
| PublicKey = maps:get(<<"epk">>, Protected, nil), | |
| Assert1 = Assert0#{ | |
| loaded := true, | |
| verified := true, | |
| assertion := Assertion, | |
| claims := Claims, | |
| protected := Protected, | |
| public_key := PublicKey | |
| }, | |
| Assert1; | |
| _ -> | |
| ?iam_throw(iam, invalid_assertion, Assert0) | |
| catch | |
| _:_ -> | |
| ?iam_raise(throw, iam, invalid_assertion, Assert0) | |
| end; | |
| _ -> | |
| ?iam_throw(iam, invalid_assertion, Assert0) | |
| catch | |
| _:_ -> | |
| ?iam_raise(throw, iam, invalid_assertion, Assert0) | |
| end; | |
| false -> | |
| ?iam_throw(iam, invalid_assertion, Assert0) | |
| end. | |
| info(Assertion) -> | |
| Result = | |
| try jose_jws:expand(Assertion) of | |
| {_, #{ | |
| <<"payload">> := JWSPayload, | |
| <<"protected">> := JWSProtected, | |
| <<"signature">> := JWSSignature | |
| }} -> | |
| try | |
| Signed = #{ | |
| payload => iam_json:decode(iam_base64url:'decode!'(JWSPayload)), | |
| protected => iam_json:decode(iam_base64url:'decode!'(JWSProtected)), | |
| signature => iam_base64url:'decode!'(JWSSignature) | |
| }, | |
| {ok, Signed} | |
| catch | |
| _:_ -> | |
| error | |
| end; | |
| _ -> | |
| error | |
| catch | |
| _:_ -> | |
| error | |
| end, | |
| case Result of | |
| {ok, _} -> | |
| Result; | |
| error -> | |
| try jose_jwe:expand(Assertion) of | |
| {_, #{ | |
| <<"ciphertext">> := JWEECiphertext, | |
| <<"encrypted_key">> := JWEEncryptedKey, | |
| <<"iv">> := JWEIV, | |
| <<"protected">> := JWEProtected, | |
| <<"tag">> := JWETag | |
| }} -> | |
| Encrypted = #{ | |
| ciphertext => iam_base64url:'decode!'(JWEECiphertext), | |
| encrypted_key => iam_base64url:'decode!'(JWEEncryptedKey), | |
| iv => iam_base64url:'decode!'(JWEIV), | |
| protected => iam_json:decode(iam_base64url:'decode!'(JWEProtected)), | |
| tag => iam_base64url:'decode!'(JWETag) | |
| }, | |
| {ok, Encrypted}; | |
| _ -> | |
| error | |
| catch | |
| _:_ -> | |
| error | |
| end | |
| end. | |
| 'info!'(Assertion) -> | |
| case info(Assertion) of | |
| {ok, Info} -> | |
| Info; | |
| error -> | |
| ?iam_raise(throw, iam, invalid_assertion, nil) | |
| end. | |
| info(Assertion, Key) -> | |
| case info(Assertion) of | |
| {ok, Info} -> | |
| maps:find(Key, Info); | |
| error -> | |
| error | |
| end. | |
| 'info!'(Assertion, Key) -> | |
| case info(Assertion, Key) of | |
| {ok, Info} -> | |
| Info; | |
| error -> | |
| ?iam_raise(throw, iam, invalid_assertion, nil) | |
| end. | |
| type(Assertion) -> | |
| try binary:split(Assertion, << $. >>, [global]) of | |
| [_, _, _, _, _] -> | |
| {ok, enc}; | |
| [_, _, _] -> | |
| {ok, sig}; | |
| _ -> | |
| error | |
| catch | |
| _:_ -> | |
| error | |
| end. | |
| validate(Assert0=#{ | |
| '__struct__' := ?MODULE, | |
| loaded := true, | |
| validated := false, | |
| claims := Claims | |
| }) when is_map(Claims) -> | |
| Now = | |
| case Assert0 of | |
| #{ now := nil } -> | |
| os:system_time(second); | |
| #{ now := Now0 } -> | |
| Now0 | |
| end, | |
| Assert1 = Assert0#{ now := Now }, | |
| Validated = | |
| validate(Assert1, Claims, [ | |
| required, | |
| audience, | |
| expiration_time, | |
| issued_at, | |
| issuer, | |
| max_age, | |
| jwt_id, | |
| not_before, | |
| subject, | |
| checks | |
| ]), | |
| case Validated of | |
| true -> | |
| Assert = Assert1#{ validated := true }, | |
| Assert; | |
| false -> | |
| ?iam_throw(iam, invalid_assertion, Assert1) | |
| end. | |
| validate(Assert=#{ | |
| '__struct__' := ?MODULE, | |
| loaded := false | |
| }, Assertion) -> | |
| validate(load(Assert, Assertion)). | |
| verify(Assert0=#{ | |
| '__struct__' := ?MODULE, | |
| loaded := true, | |
| verified := false, | |
| assertion := Assertion, | |
| claims := Claims | |
| }, AllowedSigningAlgs, JWK0) when is_binary(Assertion) andalso is_map(Claims) -> | |
| JWK = iam_util:cast_jwk(JWK0), | |
| try jose_jwt:verify_strict(JWK, AllowedSigningAlgs, Assertion) of | |
| {true, JWT, JWS} -> | |
| {_, Claims} = jose_jwt:to_map(JWT), | |
| {_, Protected} = jose_jws:to_map(JWS), | |
| Assert = Assert0#{ verified := true, claims := Claims, protected := Protected }, | |
| Assert; | |
| _ -> | |
| ?iam_throw(iam, invalid_assertion, Assert0) | |
| catch | |
| _:_ -> | |
| ?iam_raise(throw, iam, invalid_assertion, Assert0) | |
| end. | |
| verify(Assert=#{ | |
| '__struct__' := ?MODULE, | |
| loaded := false | |
| }, Assertion, AllowedSigningAlgs, JWK) -> | |
| verify(load(Assert, Assertion), AllowedSigningAlgs, JWK). | |
| %%%------------------------------------------------------------------- | |
| %%% Internal functions | |
| %%%------------------------------------------------------------------- | |
| %% @private | |
| valid_encryption(JWE, [H | T]) -> | |
| case maps:with(maps:keys(H), JWE) of | |
| H -> | |
| true; | |
| _ -> | |
| valid_encryption(JWE, T) | |
| end; | |
| valid_encryption(_, []) -> | |
| false. | |
| %% @private | |
| % audience | |
| validate(Assert=#{ audience := Audience }, Claims, [audience | Spec]) -> | |
| Valid = | |
| case maps:find(<<"aud">>, Claims) of | |
| {ok, Audience} when is_binary(Audience) -> | |
| true; | |
| {ok, Challenge} when is_function(Audience, 1) -> | |
| Audience(Challenge); | |
| _ when Audience == nil -> | |
| true; | |
| {ok, _} -> | |
| false; | |
| error -> | |
| false | |
| end, | |
| case Valid of | |
| true -> | |
| validate(Assert, Claims, Spec); | |
| false -> | |
| ?iam_throw(iam, invalid_assertion, Assert) | |
| end; | |
| % checks | |
| validate(Assert=#{ checks := Checks }, Claims, [checks | Spec]) -> | |
| case validate_checks(Claims, maps:to_list(Checks)) of | |
| true -> | |
| validate(Assert, Claims, Spec); | |
| false -> | |
| ?iam_throw(iam, invalid_assertion, Assert) | |
| end; | |
| % expiration_time | |
| validate(Assert=#{ now := Now, window := Window }, Claims, [expiration_time | Spec]) -> | |
| case maps:find(<<"exp">>, Claims) of | |
| {ok, ExpirationTime} when is_integer(ExpirationTime) andalso (ExpirationTime + Window) >= Now -> | |
| validate(Assert, Claims, Spec); | |
| {ok, _} -> | |
| ?iam_throw(iam, invalid_assertion, Assert); | |
| error -> | |
| validate(Assert, Claims, Spec) | |
| end; | |
| % issued_at | |
| validate(Assert=#{ now := Now, window := Window }, Claims, [issued_at | Spec]) -> | |
| case maps:find(<<"iat">>, Claims) of | |
| {ok, IssuedAt} when is_integer(IssuedAt) andalso IssuedAt =< (Now + Window) -> | |
| validate(Assert, Claims, Spec); | |
| {ok, _} -> | |
| ?iam_throw(iam, invalid_assertion, Assert); | |
| error -> | |
| validate(Assert, Claims, Spec) | |
| end; | |
| % issuer | |
| validate(Assert=#{ issuer := Issuer }, Claims, [issuer | Spec]) -> | |
| Valid = | |
| case maps:find(<<"iss">>, Claims) of | |
| {ok, Issuer} when is_binary(Issuer) -> | |
| true; | |
| {ok, Challenge} when is_function(Issuer, 1) -> | |
| Issuer(Challenge); | |
| _ when Issuer == nil -> | |
| true; | |
| {ok, _} -> | |
| false; | |
| error -> | |
| false | |
| end, | |
| case Valid of | |
| true -> | |
| validate(Assert, Claims, Spec); | |
| false -> | |
| ?iam_throw(iam, invalid_assertion, Assert) | |
| end; | |
| % jwt_id | |
| validate(Assert=#{ jwt_id := JWTID }, Claims, [jwt_id | Spec]) -> | |
| Valid = | |
| case maps:find(<<"jti">>, Claims) of | |
| {ok, JWTID} when is_binary(JWTID) -> | |
| validate(Assert, Claims, Spec); | |
| {ok, Challenge} when is_binary(Challenge) andalso is_tuple(JWTID) andalso element(1, JWTID) == hash andalso is_atom(element(2, JWTID)) -> | |
| {hash, Hash} = JWTID, | |
| Message = iam_json:encode(maps:remove(<<"jti">>, Claims)), | |
| Digest = crypto:hash(Hash, Message), | |
| JTI = iam_base64url:encode(Digest, #{ padding => false }), | |
| iam_crypto:constant_time_compare(JTI, Challenge); | |
| {ok, Challenge} when is_function(JWTID, 1) -> | |
| JWTID(Challenge); | |
| _ when JWTID == nil -> | |
| true; | |
| {ok, _} -> | |
| false; | |
| error -> | |
| false | |
| end, | |
| case Valid of | |
| true -> | |
| validate(Assert, Claims, Spec); | |
| false -> | |
| ?iam_throw(iam, invalid_assertion, Assert) | |
| end; | |
| % max_age | |
| validate(Assert=#{ max_age := nil }, Claims, [max_age | Spec]) -> | |
| validate(Assert, Claims, Spec); | |
| validate(Assert=#{ max_age := MaxAge, now := Now, window := Window }, Claims, [max_age | Spec]) -> | |
| Valid = | |
| case Claims of | |
| #{ <<"iat">> := IssuedAt, <<"exp">> := ExpirationTime } -> | |
| is_integer(IssuedAt) andalso is_integer(ExpirationTime) andalso (ExpirationTime - IssuedAt) =< (MaxAge + Window); | |
| #{ <<"exp">> := ExpirationTime } -> | |
| is_integer(ExpirationTime) andalso (ExpirationTime - Now) =< (MaxAge + Window); | |
| _ -> | |
| true | |
| end, | |
| case Valid of | |
| true -> | |
| validate(Assert, Claims, Spec); | |
| false -> | |
| ?iam_throw(iam, invalid_assertion, Assert) | |
| end; | |
| % not_before | |
| validate(Assert=#{ not_before := NotBefore, now := Now, window := Window }, Claims, [not_before | Spec]) -> | |
| NotBeforeValid = | |
| case maps:find(<<"nbf">>, Claims) of | |
| {ok, Challenge} when is_integer(NotBefore) andalso is_integer(Challenge) andalso Challenge =< (Now + Window) andalso (Challenge + Window) >= NotBefore -> | |
| true; | |
| {ok, Challenge} when is_function(NotBefore, 1) -> | |
| NotBefore(Challenge); | |
| {ok, Challenge} when NotBefore == nil andalso is_integer(Challenge) andalso Challenge =< (Now + Window) -> | |
| true; | |
| {ok, _} -> | |
| false; | |
| error when is_function(NotBefore, 1) -> | |
| NotBefore(nil); | |
| error -> | |
| true | |
| end, | |
| IssuedAtValid = | |
| case maps:find(<<"iat">>, Claims) of | |
| {ok, IssuedAt} when is_integer(NotBefore) andalso is_integer(IssuedAt) andalso IssuedAt =< (Now + Window) andalso (IssuedAt + Window) >= NotBefore -> | |
| true; | |
| {ok, IssuedAt} when is_function(NotBefore, 1) -> | |
| NotBefore(IssuedAt); | |
| _ when NotBefore == nil -> | |
| true; | |
| {ok, _} -> | |
| false; | |
| error -> | |
| false | |
| end, | |
| case NotBeforeValid andalso IssuedAtValid of | |
| true -> | |
| validate(Assert, Claims, Spec); | |
| false -> | |
| ?iam_throw(iam, invalid_assertion, Assert) | |
| end; | |
| % required | |
| validate(Assert=#{ required := Required }, Claims, [required | Spec]) -> | |
| case validate_required(Claims, maps:to_list(Required)) of | |
| true -> | |
| validate(Assert, Claims, Spec); | |
| false -> | |
| ?iam_throw(iam, invalid_assertion, Assert) | |
| end; | |
| % subject | |
| validate(Assert=#{ subject := Subject }, Claims, [subject | Spec]) -> | |
| Valid = | |
| case maps:find(<<"sub">>, Claims) of | |
| {ok, Subject} when is_binary(Subject) -> | |
| true; | |
| {ok, Challenge} when is_function(Subject, 1) -> | |
| Subject(Challenge); | |
| _ when Subject == nil -> | |
| true; | |
| {ok, _} -> | |
| false; | |
| error -> | |
| false | |
| end, | |
| case Valid of | |
| true -> | |
| validate(Assert, Claims, Spec); | |
| false -> | |
| ?iam_throw(iam, invalid_assertion, Assert) | |
| end; | |
| % validation success | |
| validate(_Assert, _Claims, []) -> | |
| true; | |
| % validation failure | |
| validate(_Assert, _Claims, _Spec) -> | |
| false. | |
| %% @private | |
| validate_checks(Claims, [{Key, Check} | Checks]) when is_function(Check, 1) -> | |
| case maps:find(Key, Claims) of | |
| {ok, Challenge} -> | |
| case Check(Challenge) of | |
| true -> | |
| validate_checks(Claims, Checks); | |
| false -> | |
| false | |
| end; | |
| error -> | |
| false | |
| end; | |
| validate_checks(Claims, [{Key, Check} | Checks]) -> | |
| case maps:find(Key, Claims) of | |
| {ok, Check} -> | |
| validate_checks(Claims, Checks); | |
| {ok, _} -> | |
| false; | |
| error -> | |
| false | |
| end; | |
| validate_checks(_Claims, []) -> | |
| true. | |
| %% @private | |
| validate_required(Claims=#{ <<"aud">> := Audience }, [{audience, true} | Required]) when is_binary(Audience) -> | |
| validate_required(Claims, Required); | |
| validate_required(Claims=#{ <<"azp">> := AuthorizedParty }, [{authorized_party, true} | Required]) when is_binary(AuthorizedParty) -> | |
| validate_required(Claims, Required); | |
| validate_required(Claims=#{ <<"exp">> := ExpirationTime }, [{expiration_time, true} | Required]) when is_integer(ExpirationTime) -> | |
| validate_required(Claims, Required); | |
| validate_required(Claims=#{ <<"iat">> := IssuedAt }, [{issued_at, true} | Required]) when is_integer(IssuedAt) -> | |
| validate_required(Claims, Required); | |
| validate_required(Claims=#{ <<"iss">> := Issuer }, [{issuer, true} | Required]) when is_binary(Issuer) -> | |
| validate_required(Claims, Required); | |
| validate_required(Claims=#{ <<"jti">> := JWTID }, [{jwt_id, true} | Required]) when is_binary(JWTID) -> | |
| validate_required(Claims, Required); | |
| validate_required(Claims=#{ <<"nbf">> := NotBefore }, [{not_before, true} | Required]) when is_integer(NotBefore) -> | |
| validate_required(Claims, Required); | |
| validate_required(Claims=#{ <<"sub">> := Subject }, [{subject, true} | Required]) when is_binary(Subject) -> | |
| validate_required(Claims, Required); | |
| validate_required(Claims, [{_, false} | Required]) -> | |
| validate_required(Claims, Required); | |
| validate_required(Claims, [{Key, true} | Required]) when is_binary(Key) -> | |
| case maps:is_key(Key, Claims) of | |
| true -> | |
| validate_required(Claims, Required); | |
| false -> | |
| false | |
| end; | |
| validate_required(_Claims, []) -> | |
| true; | |
| validate_required(_Claims, _Required) -> | |
| false. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| %% -*- mode: erlang; tab-width: 4; indent-tabs-mode: 1; st-rulers: [70] -*- | |
| %% vim: ts=4 sw=4 ft=erlang noet | |
| %%%------------------------------------------------------------------- | |
| %%% @author Andrew Bennett <andrew@pixid.com> | |
| %%% @copyright 2017, Andrew Bennett | |
| %%% @doc | |
| %%% | |
| %%% @end | |
| %%% Created : 26 Apr 2017 by Andrew Bennett <andrew@pixid.com> | |
| %%%------------------------------------------------------------------- | |
| -module(iam_claims). | |
| -include_lib("jose/include/jose.hrl"). | |
| %% Types | |
| -type t() :: #{ | |
| '__struct__' := ?MODULE, | |
| audience := nil | binary(), | |
| authorized_party := nil | binary(), | |
| claims := map(), | |
| expiration_time := nil | non_neg_integer(), | |
| issued_at := nil | non_neg_integer(), | |
| issuer := nil | binary(), | |
| jwt_id := nil | binary() | {hash, atom()}, | |
| key_id := boolean() | binary(), | |
| not_before := nil | non_neg_integer(), | |
| subject := nil | binary(), | |
| atom() => any() | |
| }. | |
| -export_type([t/0]). | |
| %% Elixir API | |
| -export(['__struct__'/0]). | |
| -export(['__struct__'/1]). | |
| %% API | |
| -export([new/0]). | |
| -export([new/1]). | |
| -export([expire_after/2]). | |
| -export([issue/1]). | |
| -export([issue/2]). | |
| -export([merge/2]). | |
| -export([not_before/1]). | |
| -export([not_before/2]). | |
| -export([payload/1]). | |
| -export([put/3]). | |
| %% JWT API | |
| -export([encrypt/3]). | |
| -export([seal/3]). | |
| -export([seal/4]). | |
| -export([sign/3]). | |
| %%%=================================================================== | |
| %%% Elixir API functions | |
| %%%=================================================================== | |
| '__struct__'() -> | |
| #{ | |
| '__struct__' => ?MODULE, | |
| audience => nil, | |
| authorized_party => nil, | |
| claims => #{}, | |
| expiration_time => nil, | |
| issued_at => nil, | |
| issuer => nil, | |
| jwt_id => nil, | |
| key_id => true, | |
| not_before => nil, | |
| subject => nil | |
| }. | |
| '__struct__'(List) when is_list(List) -> | |
| '__struct__'(maps:from_list(List)); | |
| '__struct__'(Map) when is_map(Map) -> | |
| maps:fold(fun maps:update/3, '__struct__'(), Map). | |
| %%%=================================================================== | |
| %%% API functions | |
| %%%=================================================================== | |
| new() -> | |
| new(#{}). | |
| new(BaseMap0) when is_map(BaseMap0) -> | |
| case maps:take(claims, BaseMap0) of | |
| {Claims, BaseMap} -> | |
| maps:fold(fun fold_put/3, '__struct__'(BaseMap), Claims); | |
| error -> | |
| '__struct__'(BaseMap0) | |
| end; | |
| new(List) when is_list(List) -> | |
| new(maps:from_list(List)). | |
| expire_after(Claims=#{ '__struct__' := ?MODULE, expiration_time := nil }, ExpireAfter) when is_integer(ExpireAfter) andalso ExpireAfter >= 0 -> | |
| Now = | |
| case Claims of | |
| #{ issued_at := nil } -> | |
| os:system_time(second); | |
| #{ issued_at := IssuedAt } -> | |
| IssuedAt | |
| end, | |
| ExpirationTime = Now + ExpireAfter, | |
| Claims#{ expiration_time := ExpirationTime }. | |
| issue(Claims=#{ '__struct__' := ?MODULE, issued_at := nil }) -> | |
| issue(Claims, os:system_time(second)). | |
| issue(Claims=#{ '__struct__' := ?MODULE, issued_at := nil }, IssuedAt) when is_integer(IssuedAt) andalso IssuedAt >= 0 -> | |
| Claims#{ issued_at := IssuedAt }; | |
| issue(Claims=#{ '__struct__' := ?MODULE, issued_at := nil }, nil) -> | |
| issue(Claims). | |
| merge(L=#{ '__struct__' := ?MODULE, claims := LClaims }, R=#{ '__struct__' := ?MODULE, claims := RClaims }) -> | |
| Claims = maps:merge(LClaims, RClaims), | |
| M = maps:merge(L, R), | |
| maps:put(claims, Claims, M); | |
| merge(L=#{ '__struct__' := ?MODULE }, Enumerable) -> | |
| R = ?MODULE:new(Enumerable), | |
| merge(L, R). | |
| not_before(Claims=#{ '__struct__' := ?MODULE, not_before := nil }) -> | |
| not_before(Claims, os:system_time(second)). | |
| not_before(Claims=#{ '__struct__' := ?MODULE, not_before := nil }, NotBefore) when is_integer(NotBefore) andalso NotBefore >= 0 -> | |
| Claims#{ not_before := NotBefore }; | |
| not_before(Claims=#{ '__struct__' := ?MODULE, not_before := nil }, nil) -> | |
| not_before(Claims). | |
| payload(#{ | |
| '__struct__' := ?MODULE, | |
| audience := Audience, | |
| authorized_party := AuthorizedParty, | |
| claims := Claims, | |
| expiration_time := ExpirationTime, | |
| issued_at := IssuedAt, | |
| issuer := Issuer, | |
| jwt_id := JWTID, | |
| not_before := NotBefore, | |
| subject := Subject | |
| }) -> | |
| payload(#{}, [ | |
| {audience, Audience}, | |
| {authorized_party, AuthorizedParty}, | |
| {expiration_time, ExpirationTime}, | |
| {issued_at, IssuedAt}, | |
| {issuer, Issuer}, | |
| {not_before, NotBefore}, | |
| {subject, Subject}, | |
| {claims, Claims}, | |
| {jwt_id, JWTID} | |
| ]). | |
| put(Claims=#{ '__struct__' := ?MODULE }, K, V) when K == audience orelse K == <<"aud">> -> | |
| Claims#{ audience := V }; | |
| put(Claims=#{ '__struct__' := ?MODULE }, K, V) when K == authorized_party orelse K == <<"azp">> -> | |
| Claims#{ authorized_party := V }; | |
| put(Claims=#{ '__struct__' := ?MODULE }, K, V) when K == claims -> | |
| Claims#{ claims := V }; | |
| put(Claims=#{ '__struct__' := ?MODULE }, K, V) when K == expiration_time orelse K == <<"exp">> -> | |
| Claims#{ expiration_time := V }; | |
| put(Claims=#{ '__struct__' := ?MODULE }, K, V) when K == issued_at orelse K == <<"iat">> -> | |
| Claims#{ issued_at := V }; | |
| put(Claims=#{ '__struct__' := ?MODULE }, K, V) when K == issuer orelse K == <<"iss">> -> | |
| Claims#{ issuer := V }; | |
| put(Claims=#{ '__struct__' := ?MODULE }, K, V) when K == jwt_id orelse K == <<"jti">> -> | |
| Claims#{ jwt_id := V }; | |
| put(Claims=#{ '__struct__' := ?MODULE }, K, V) when K == not_before orelse K == <<"nbf">> -> | |
| Claims#{ not_before := V }; | |
| put(Claims=#{ '__struct__' := ?MODULE }, K, V) when K == subject orelse K == <<"sub">> -> | |
| Claims#{ subject := V }; | |
| put(Claims=#{ '__struct__' := ?MODULE }, Key, Value) when is_atom(Key) -> | |
| put(Claims, erlang:atom_to_binary(Key, unicode), Value); | |
| put(Claims=#{ '__struct__' := ?MODULE, claims := C0 }, Key, Value) when is_binary(Key) -> | |
| C1 = maps:put(Key, Value, C0), | |
| Claims#{ claims := C1 }. | |
| %%%=================================================================== | |
| %%% JWT API functions | |
| %%%=================================================================== | |
| encrypt(Claims=#{ '__struct__' := ?MODULE }, Encryption, JWK0) -> | |
| JWT0 = payload(Claims), | |
| JWT = iam_util:cast_jwt(JWT0), | |
| JWK = iam_util:cast_jwk(JWK0), | |
| JWE0 = iam_util:cast_jwe(Encryption), | |
| JWE = maybe_add_kid(Claims, JWE0, JWK), | |
| {_, Assertion} = jose_jwe:compact(jose_jwt:encrypt(JWK, JWE, JWT)), | |
| Assertion. | |
| seal(Claims=#{ '__struct__' := ?MODULE }, Encryption, BPK0) -> | |
| BPK = jose_jwk:to_public(iam_util:cast_jwk(BPK0)), | |
| ASK = jose_jwk:generate_key(BPK), | |
| Assertion = seal(Claims, Encryption, BPK, ASK), | |
| {ok, Assertion, ASK}. | |
| seal(Claims=#{ '__struct__' := ?MODULE }, Encryption, BPK0, ASK0) -> | |
| BPK = iam_util:cast_jwk(BPK0), | |
| ASK = iam_util:cast_jwk(ASK0), | |
| JWT0 = payload(Claims), | |
| JWT = iam_util:cast_jwt(JWT0), | |
| JWE = iam_util:cast_jwe(Encryption), | |
| {_, JWTPlainText} = jose_jwt:to_binary(JWT), | |
| {_, Assertion} = jose_jwe:compact(jose_jwe:block_encrypt({BPK, ASK}, JWTPlainText, JWE)), | |
| Assertion. | |
| sign(Claims=#{ '__struct__' := ?MODULE }, SigningAlg, JWK0) -> | |
| JWT0 = payload(Claims), | |
| JWT = iam_util:cast_jwt(JWT0), | |
| JWK = iam_util:cast_jwk(JWK0), | |
| JWS0 = iam_util:cast_jws(SigningAlg), | |
| JWS = maybe_add_kid(Claims, JWS0, JWK), | |
| {_, Assertion} = jose_jws:compact(jose_jwt:sign(JWK, JWS, JWT)), | |
| Assertion. | |
| %%%------------------------------------------------------------------- | |
| %%% Internal functions | |
| %%%------------------------------------------------------------------- | |
| %% @private | |
| fold_put(Key, Value, Claims) -> | |
| ?MODULE:put(Claims, Key, Value). | |
| %% @private | |
| maybe_add_kid(#{ key_id := false }, JWE=#jose_jwe{}, #jose_jwk{}) -> | |
| JWE; | |
| maybe_add_kid(#{ key_id := false }, JWS=#jose_jws{}, #jose_jwk{}) -> | |
| JWS; | |
| maybe_add_kid(#{ key_id := true }, JWE=#jose_jwe{}, JWK=#jose_jwk{}) -> | |
| case jose_jwe:to_map(JWE) of | |
| {_, #{ <<"kid">> := _ }} -> | |
| JWE; | |
| {_, JWEMap} -> | |
| KID = | |
| case jose_jwk:to_map(JWK) of | |
| {_, #{ <<"kid">> := KID0 }} -> | |
| KID0; | |
| _ -> | |
| jose_jwk:thumbprint(JWK) | |
| end, | |
| iam_util:cast_jwe(maps:put(<<"kid">>, KID, JWEMap)) | |
| end; | |
| maybe_add_kid(#{ key_id := true }, JWS=#jose_jws{}, JWK=#jose_jwk{}) -> | |
| case jose_jws:to_map(JWS) of | |
| {_, #{ <<"kid">> := _ }} -> | |
| JWS; | |
| {_, JWSMap} -> | |
| KID = | |
| case jose_jwk:to_map(JWK) of | |
| {_, #{ <<"kid">> := KID0 }} -> | |
| KID0; | |
| _ -> | |
| jose_jwk:thumbprint(JWK) | |
| end, | |
| iam_util:cast_jws(maps:put(<<"kid">>, KID, JWSMap)) | |
| end; | |
| maybe_add_kid(#{ key_id := KID }, JWE=#jose_jwe{}, #jose_jwk{}) when is_binary(KID) -> | |
| case jose_jwe:to_map(JWE) of | |
| {_, #{ <<"kid">> := _ }} -> | |
| JWE; | |
| {_, JWEMap} -> | |
| iam_util:cast_jwe(maps:put(<<"kid">>, KID, JWEMap)) | |
| end; | |
| maybe_add_kid(#{ key_id := KID }, JWS=#jose_jws{}, #jose_jwk{}) when is_binary(KID) -> | |
| case jose_jws:to_map(JWS) of | |
| {_, #{ <<"kid">> := _ }} -> | |
| JWS; | |
| {_, JWSMap} -> | |
| iam_util:cast_jws(maps:put(<<"kid">>, KID, JWSMap)) | |
| end. | |
| %% @private | |
| payload(JWT, [{_, nil} | Rest]) -> | |
| payload(JWT, Rest); | |
| payload(JWT, [{audience, Audience} | Rest]) when is_binary(Audience) -> | |
| payload(maps:put(<<"aud">>, Audience, JWT), Rest); | |
| payload(JWT, [{authorized_party, AuthorizedParty} | Rest]) when is_binary(AuthorizedParty) -> | |
| payload(maps:put(<<"azp">>, AuthorizedParty, JWT), Rest); | |
| payload(JWT, [{claims, Claims} | Rest]) when is_map(Claims) -> | |
| payload(maps:merge(JWT, Claims), Rest); | |
| payload(JWT, [{expiration_time, ExpirationTime} | Rest]) when is_integer(ExpirationTime) -> | |
| payload(maps:put(<<"exp">>, ExpirationTime, JWT), Rest); | |
| payload(JWT, [{issued_at, IssuedAt} | Rest]) when is_integer(IssuedAt) -> | |
| payload(maps:put(<<"iat">>, IssuedAt, JWT), Rest); | |
| payload(JWT, [{issuer, Issuer} | Rest]) when is_binary(Issuer) -> | |
| payload(maps:put(<<"iss">>, Issuer, JWT), Rest); | |
| payload(JWT, [{jwt_id, JWTID} | Rest]) when is_binary(JWTID) -> | |
| payload(maps:put(<<"jti">>, JWTID, JWT), Rest); | |
| payload(JWT, [{jwt_id, {hash, Hash}} | Rest]) when is_atom(Hash) -> | |
| Message = iam_json:encode(JWT), | |
| Digest = crypto:hash(Hash, Message), | |
| JTI = iam_base64url:encode(Digest, #{ padding => false }), | |
| payload(maps:put(<<"jti">>, JTI, JWT), Rest); | |
| payload(JWT, [{not_before, NotBefore} | Rest]) when is_integer(NotBefore) -> | |
| payload(maps:put(<<"nbf">>, NotBefore, JWT), Rest); | |
| payload(JWT, [{subject, Subject} | Rest]) when is_binary(Subject) -> | |
| payload(maps:put(<<"sub">>, Subject, JWT), Rest); | |
| payload(JWT, []) -> | |
| JWT. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment