From 8500ffb9bdf16f5c56142a48b00551edd0c60526 Mon Sep 17 00:00:00 2001 From: Alexey Date: Fri, 30 Jul 2021 12:17:08 +0300 Subject: [PATCH] ED-175: Ephemeral token issuing (#11) --- config/sys.config | 28 +++- rebar.config | 1 + src/tk_audit_log.erl | 29 +++- src/tk_authdata_source.erl | 15 +- src/tk_authdata_source_extractor.erl | 18 ++- src/tk_authdata_source_storage.erl | 24 +++- src/tk_authority.erl | 42 +++++- ...context_extractor.erl => tk_extractor.erl} | 16 +-- src/tk_extractor_claim.erl | 91 ------------ src/tk_extractor_detect_token.erl | 6 +- src/tk_extractor_invoice_tpl_token.erl | 4 +- src/tk_extractor_phony_api_key.erl | 4 +- src/tk_extractor_user_session_token.erl | 6 +- src/tk_pulse.erl | 5 +- src/tk_storage.erl | 70 +++++++++ src/tk_storage_claim.erl | 133 ++++++++++++++++++ src/tk_storage_machinegun.erl | 43 ++++++ src/tk_token_jwt.erl | 81 +++++------ src/{tk_handler.erl => tk_woody_handler.erl} | 66 +++++++-- src/token_keeper.app.src | 1 + src/token_keeper.erl | 2 +- test/tk_tests_SUITE.erl | 114 ++++++++++++--- test/tk_token_jwt_tests_SUITE.erl | 11 +- 23 files changed, 576 insertions(+), 234 deletions(-) rename src/{tk_context_extractor.erl => tk_extractor.erl} (69%) delete mode 100644 src/tk_extractor_claim.erl create mode 100644 src/tk_storage.erl create mode 100644 src/tk_storage_claim.erl create mode 100644 src/tk_storage_machinegun.erl rename src/{tk_handler.erl => tk_woody_handler.erl} (58%) diff --git a/config/sys.config b/config/sys.config index 7e11af1..294225d 100644 --- a/config/sys.config +++ b/config/sys.config @@ -50,23 +50,37 @@ } }}, + %% Token issuing configuration + {issuing, #{ + authority => keycloak + }}, + %% Authority configuration {authorities, #{ keycloak => #{ %% What to use as authority id string id => <<"test.rbkmoney.keycloak">>, + %% Which jwt key to use for signing for this authority + %% You can only use keys that have the same authority configured for them + signer => test, %% Where to fetch authdata for tokens issued by this authority authdata_sources => [ - %% Fetch from service storage (currently NOT IMPLEMENTED) - storage, - %% Extract bouncer context from the token itself and make up the rest + %% Fetch from storage + {storage, + %% Use token claims as storage + {claim, #{ + %% Enable compatability with legacy issued tokens when getting from storage + compatability => {true, <<"test.rbkmoney.apikeymgmt">>} + }} + }, + {storage, + %% Use machinegun as storage (NOT IMPLEMENTED) + machinegun + }, + %% Create a new bouncer context using token data {extract, #{ %% Configuration for how to extract said context methods => [ - %% Extract bouncer context from claim - {claim, #{ - metadata_ns => <<"test.rbkmoney.apikeymgmt">> - }}, %% Create bouncer context from various legacy data the token has to offer %% Avaiable options are: `phony_api_key`, `user_session_token`, %% `invoice_template_access_token`, `detect_token` diff --git a/rebar.config b/rebar.config index 048c3d6..42b278d 100644 --- a/rebar.config +++ b/rebar.config @@ -39,6 +39,7 @@ {branch, "master"} } }, + {snowflake, {git, "https://github.com/rbkmoney/snowflake.git", {branch, "master"}}}, {woody, {git, "https://github.com/rbkmoney/woody_erlang.git", {branch, "master"} diff --git a/src/tk_audit_log.erl b/src/tk_audit_log.erl index 05bac31..b73a12e 100644 --- a/src/tk_audit_log.erl +++ b/src/tk_audit_log.erl @@ -234,11 +234,14 @@ log_allowed(Level) -> %% get_level({get_by_token, started}, _Level) -> log_allowed(debug); +get_level({create_ephemeral, started}, _Level) -> log_allowed(debug); get_level(_, Level) -> Level. get_message({get_by_token, started}) -> <<"get_by_token started">>; get_message({get_by_token, succeeded}) -> <<"get_by_token succeeded">>; -get_message({get_by_token, {failed, _}}) -> <<"get_by_token failed">>. +get_message({get_by_token, {failed, _}}) -> <<"get_by_token failed">>; +get_message({create_ephemeral, started}) -> <<"create_ephemeral started">>; +get_message({create_ephemeral, succeeded}) -> <<"create_ephemeral succeeded">>. get_beat_metadata({get_by_token, Event}) -> #{ @@ -258,6 +261,20 @@ get_beat_metadata({get_by_token, Event}) -> error => encode_error(Error) } end + }; +get_beat_metadata({create_ephemeral, Event}) -> + #{ + create_ephemeral => + case Event of + started -> + #{ + event => started + }; + succeeded -> + #{ + event => succeeded + } + end }. encode_error({Class, Details}) when is_atom(Class) -> @@ -278,12 +295,12 @@ extract_opt_meta(K, Metadata, EncodeFun, Acc) -> error -> Acc end. -encode_token({JTI, Claims, Authority, TokenMetadata}) -> +encode_token(TokenInfo) -> #{ - jti => JTI, - claims => Claims, - authority => Authority, - metadata => TokenMetadata + jti => tk_token_jwt:get_token_id(TokenInfo), + claims => tk_token_jwt:get_claims(TokenInfo), + authority => tk_token_jwt:get_authority(TokenInfo), + metadata => tk_token_jwt:get_metadata(TokenInfo) }. encode_token_source(TokenSourceContext = #{}) -> diff --git a/src/tk_authdata_source.erl b/src/tk_authdata_source.erl index d77dec3..0e10723 100644 --- a/src/tk_authdata_source.erl +++ b/src/tk_authdata_source.erl @@ -2,7 +2,7 @@ %% Behaviour --callback get_authdata(tk_token_jwt:t(), source_opts()) -> stored_authdata() | undefined. +-callback get_authdata(tk_token_jwt:t(), source_opts()) -> sourced_authdata() | undefined. %% API functions @@ -11,21 +11,22 @@ %% API Types -type authdata_source() :: storage_source() | extractor_source(). - --type stored_authdata() :: #{ - id => tk_authority:id(), +-type sourced_authdata() :: #{ + id => tk_authority:authdata_id(), status := tk_authority:status(), context := tk_authority:encoded_context_fragment(), + authority => tk_authority:autority_id(), metadata => tk_authority:metadata() }. -export_type([authdata_source/0]). --export_type([stored_authdata/0]). +-export_type([sourced_authdata/0]). %% Internal types --type storage_source() :: maybe_opts(storage, tk_authdata_source_storage:source_opts()). +-type storage_source() :: {storage, tk_authdata_source_storage:source_opts()}. -type extractor_source() :: maybe_opts(extractor, tk_authdata_source_extractor:source_opts()). + -type maybe_opts(Source, Opts) :: Source | {Source, Opts}. -type source_opts() :: @@ -34,7 +35,7 @@ %% API functions --spec get_authdata(authdata_source(), tk_token_jwt:t()) -> stored_authdata() | undefined. +-spec get_authdata(authdata_source(), tk_token_jwt:t()) -> sourced_authdata() | undefined. get_authdata(AuthDataSource, Token) -> {Source, Opts} = get_source_opts(AuthDataSource), Hander = get_source_handler(Source), diff --git a/src/tk_authdata_source_extractor.erl b/src/tk_authdata_source_extractor.erl index 75e16f3..f230fcd 100644 --- a/src/tk_authdata_source_extractor.erl +++ b/src/tk_authdata_source_extractor.erl @@ -9,14 +9,22 @@ %% --type source_opts() :: #{ - methods => tk_context_extractor:methods() +-type extracted_authdata() :: #{ + status := tk_authority:status(), + context := tk_authority:encoded_context_fragment(), + metadata => tk_authority:metadata() }. + +-type source_opts() :: #{ + methods => tk_extractor:methods() +}. + +-export_type([extracted_authdata/0]). -export_type([source_opts/0]). %% Behaviour functions --spec get_authdata(tk_token_jwt:t(), source_opts()) -> tk_authdata_source:stored_authdata() | undefined. +-spec get_authdata(tk_token_jwt:t(), source_opts()) -> extracted_authdata() | undefined. get_authdata(Token, Opts) -> Methods = get_extractor_methods(Opts), case extract_context_with(Methods, Token) of @@ -35,7 +43,7 @@ extract_context_with([], _Token) -> undefined; extract_context_with([MethodOpts | Rest], Token) -> {Method, Opts} = get_method_opts(MethodOpts), - case tk_context_extractor:get_context(Method, Token, Opts) of + case tk_extractor:get_context(Method, Token, Opts) of AuthData when AuthData =/= undefined -> AuthData; undefined -> @@ -49,8 +57,6 @@ make_auth_data(ContextFragment, Metadata) -> metadata => Metadata }). -encode_context_fragment({encoded_context_fragment, ContextFragment}) -> - ContextFragment; encode_context_fragment(ContextFragment) -> #bctx_ContextFragment{ type = v1_thrift_binary, diff --git a/src/tk_authdata_source_storage.erl b/src/tk_authdata_source_storage.erl index 894bc13..979fd5a 100644 --- a/src/tk_authdata_source_storage.erl +++ b/src/tk_authdata_source_storage.erl @@ -7,12 +7,26 @@ %% --type source_opts() :: #{}. +-type stored_authdata() :: tk_storage:storable_authdata(). +-type source_opts() :: claim_storage(). + +-export_type([stored_authdata/0]). -export_type([source_opts/0]). +%% + +-type claim_storage() :: maybe_opts(claim, tk_storage_claim:storage_opts()). +-type maybe_opts(Source, Opts) :: Source | {Source, Opts}. + %% Behaviour functions --spec get_authdata(tk_token_jwt:t(), source_opts()) -> undefined. -get_authdata(_Token, _Opts) -> - %@TODO: This is for when we actually have storage for authdata - undefined. +-spec get_authdata(tk_token_jwt:t(), source_opts()) -> stored_authdata() | undefined. +get_authdata(Token, StorageOpts) -> + Claims = tk_token_jwt:get_claims(Token), + case tk_storage:get_by_claims(Claims, StorageOpts) of + {ok, AuthData} -> + AuthData; + {error, Reason} -> + _ = logger:warning("Failed storage get: ~p", [Reason]), + undefined + end. diff --git a/src/tk_authority.erl b/src/tk_authority.erl index 34d2a1f..3e4a190 100644 --- a/src/tk_authority.erl +++ b/src/tk_authority.erl @@ -5,34 +5,39 @@ %% API functions +-export([get_id/1]). +-export([get_signer/1]). +-export([create_authdata/4]). -export([get_authdata_by_token/2]). %% API Types -type authority() :: #{ id := autority_id(), + signer => tk_token_jwt:keyname(), authdata_sources := [tk_authdata_source:authdata_source()] }. +-type autority_id() :: binary(). + -type authdata() :: #{ - id => id(), + id => authdata_id(), status := status(), context := encoded_context_fragment(), authority := autority_id(), metadata => metadata() }. --type id() :: binary(). +-type authdata_id() :: binary(). -type status() :: active | revoked. -type encoded_context_fragment() :: tk_context_thrift:'ContextFragment'(). -type metadata() :: #{metadata_ns() => #{binary() => binary()}}. -type metadata_ns() :: binary(). --type autority_id() :: binary(). -export_type([authority/0]). -export_type([authdata/0]). --export_type([id/0]). +-export_type([authdata_id/0]). -export_type([status/0]). -export_type([encoded_context_fragment/0]). -export_type([metadata/0]). @@ -41,13 +46,30 @@ %% API Functions +-spec get_id(authority()) -> autority_id(). +get_id(Authority) -> + maps:get(id, Authority). + +-spec get_signer(authority()) -> tk_token_jwt:keyname(). +get_signer(Authority) -> + maps:get(signer, Authority). + +-spec create_authdata(authdata_id() | undefined, encoded_context_fragment(), metadata(), authority()) -> authdata(). +create_authdata(ID, ContextFragment, Metadata, Authority) -> + AuthData = #{ + status => active, + context => ContextFragment, + metadata => Metadata + }, + add_authority_id(add_id(AuthData, ID), Authority). + -spec get_authdata_by_token(tk_token_jwt:t(), authority()) -> {ok, authdata()} | {error, {authdata_not_found, _Sources}}. get_authdata_by_token(Token, Authority) -> AuthDataSources = get_auth_data_sources(Authority), case get_authdata_from_sources(AuthDataSources, Token) of AuthData when AuthData =/= undefined -> - {ok, add_authority_id(AuthData, Authority)}; + {ok, maybe_add_authority_id(AuthData, Authority)}; undefined -> {error, {authdata_not_found, AuthDataSources}} end. @@ -72,5 +94,15 @@ get_authdata_from_sources([SourceOpts | Rest], Token) -> get_authdata_from_sources(Rest, Token) end. +maybe_add_authority_id(AuthData = #{authority := _}, _Authority) -> + AuthData; +maybe_add_authority_id(AuthData, Authority) -> + add_authority_id(AuthData, Authority). + +add_id(AuthData, undefined) -> + AuthData; +add_id(AuthData, ID) -> + AuthData#{id => ID}. + add_authority_id(AuthData, Authority) -> AuthData#{authority => maps:get(id, Authority)}. diff --git a/src/tk_context_extractor.erl b/src/tk_extractor.erl similarity index 69% rename from src/tk_context_extractor.erl rename to src/tk_extractor.erl index cdd2141..bd06c82 100644 --- a/src/tk_context_extractor.erl +++ b/src/tk_extractor.erl @@ -1,4 +1,4 @@ --module(tk_context_extractor). +-module(tk_extractor). %% Behaviour @@ -11,27 +11,21 @@ %% API Types -type methods() :: [{method(), extractor_opts()} | method()]. --type method() :: claim | detect_token | api_key_token | user_session_token | invoice_template_access_token. +-type method() :: detect_token | api_key_token | user_session_token | invoice_template_access_token. -type extractor_opts() :: - tk_extractor_claim:extractor_opts() - | tk_extractor_detect_token:extractor_opts() + tk_extractor_detect_token:extractor_opts() | tk_extractor_phony_api_key:extractor_opts() | tk_extractor_user_session_token:extractor_opts() | tk_extractor_invoice_tpl_token:extractor_opts(). -type extracted_context() :: {context_fragment(), tk_authority:metadata() | undefined}. - --type encoded_context_fragment() :: tk_context_thrift:'ContextFragment'(). --type context_fragment() :: - {encoded_context_fragment, encoded_context_fragment()} - | bouncer_context_helpers:context_fragment(). +-type context_fragment() :: bouncer_context_helpers:context_fragment(). -export_type([methods/0]). -export_type([method/0]). -export_type([extractor_opts/0]). -export_type([extracted_context/0]). --export_type([encoded_context_fragment/0]). -export_type([context_fragment/0]). %% API functions @@ -43,8 +37,6 @@ get_context(Method, Token, Opts) -> %% -get_extractor_handler(claim) -> - tk_extractor_claim; get_extractor_handler(detect_token) -> tk_extractor_detect_token; get_extractor_handler(phony_api_key) -> diff --git a/src/tk_extractor_claim.erl b/src/tk_extractor_claim.erl deleted file mode 100644 index 226691b..0000000 --- a/src/tk_extractor_claim.erl +++ /dev/null @@ -1,91 +0,0 @@ --module(tk_extractor_claim). --behaviour(tk_context_extractor). - --include_lib("token_keeper_proto/include/tk_context_thrift.hrl"). - --export([get_context/2]). - -%% - --type extractor_opts() :: #{ - metadata_ns := binary() -}. - --export_type([extractor_opts/0]). - -%% API functions - --spec get_context(tk_token_jwt:t(), extractor_opts()) -> tk_context_extractor:extracted_context() | undefined. -get_context(Token, ExtractorOpts) -> - % TODO - % We deliberately do not handle decoding errors here since we extract claims from verified - % tokens only, hence they must be well-formed here. - case get_bouncer_claim(Token) of - {ok, ClaimFragment} -> - {ClaimFragment, wrap_metadata(get_metadata(Token), ExtractorOpts)}; - undefined -> - undefined - end. - -%% Internal functions - -get_metadata(Token) -> - Metadata = maps:with(get_passthrough_claim_names(), tk_token_jwt:get_claims(Token)), - %% @TEMP: This is a temporary hack. - %% When some external services will stop requiring woody user identity to be present it must be removed too - genlib_map:compact(Metadata#{ - <<"party_id">> => tk_token_jwt:get_subject_id(Token) - }). - -wrap_metadata(Metadata, _ExtractorOpts) when map_size(Metadata) =:= 0 -> - undefined; -wrap_metadata(Metadata, ExtractorOpts) -> - MetadataNS = maps:get(metadata_ns, ExtractorOpts), - #{MetadataNS => Metadata}. - -get_passthrough_claim_names() -> - [ - %% token consumer - <<"cons">> - ]. - --define(CLAIM_BOUNCER_CTX, <<"bouncer_ctx">>). --define(CLAIM_CTX_TYPE, <<"ty">>). --define(CLAIM_CTX_CONTEXT, <<"ct">>). - --define(CLAIM_CTX_TYPE_V1_THRIFT_BINARY, <<"v1_thrift_binary">>). - --type claim() :: tk_token_jwt:claim(). - --spec get_bouncer_claim(tk_token_jwt:t()) -> - {ok, tk_context_extractor:context_fragment()} - | {error, {unsupported, claim()} | {malformed, binary()}} - | undefined. -get_bouncer_claim(Token) -> - case tk_token_jwt:get_claim(?CLAIM_BOUNCER_CTX, Token, undefined) of - Claim when Claim /= undefined -> - decode_bouncer_claim(Claim); - undefined -> - undefined - end. - --spec decode_bouncer_claim(claim()) -> - {ok, tk_context_extractor:context_fragment()} | {error, {unsupported, claim()} | {malformed, binary()}}. -decode_bouncer_claim(#{ - ?CLAIM_CTX_TYPE := ?CLAIM_CTX_TYPE_V1_THRIFT_BINARY, - ?CLAIM_CTX_CONTEXT := Content -}) -> - try - {ok, - {encoded_context_fragment, #bctx_ContextFragment{ - type = v1_thrift_binary, - content = base64:decode(Content) - }}} - catch - % NOTE - % The `base64:decode/1` fails in unpredictable ways. - error:_ -> - {error, {malformed, Content}} - end; -decode_bouncer_claim(Ctx) -> - {error, {unsupported, Ctx}}. diff --git a/src/tk_extractor_detect_token.erl b/src/tk_extractor_detect_token.erl index 118e875..78465a3 100644 --- a/src/tk_extractor_detect_token.erl +++ b/src/tk_extractor_detect_token.erl @@ -1,5 +1,5 @@ -module(tk_extractor_detect_token). --behaviour(tk_context_extractor). +-behaviour(tk_extractor). %% Behaviour @@ -22,11 +22,11 @@ %% Behaviour --spec get_context(tk_token_jwt:t(), extractor_opts()) -> tk_context_extractor:extracted_context() | undefined. +-spec get_context(tk_token_jwt:t(), extractor_opts()) -> tk_extractor:extracted_context() | undefined. get_context(Token, Opts = #{user_session_token_origins := UserTokenOrigins}) -> TokenSourceContext = tk_token_jwt:get_source_context(Token), TokenType = determine_token_type(TokenSourceContext, UserTokenOrigins), - tk_context_extractor:get_context(TokenType, Token, get_opts(TokenType, Opts)). + tk_extractor:get_context(TokenType, Token, get_opts(TokenType, Opts)). %% Internal functions diff --git a/src/tk_extractor_invoice_tpl_token.erl b/src/tk_extractor_invoice_tpl_token.erl index 55897b1..33deea9 100644 --- a/src/tk_extractor_invoice_tpl_token.erl +++ b/src/tk_extractor_invoice_tpl_token.erl @@ -5,7 +5,7 @@ %% therefore a lot of them do not have a standart bouncer context claim built-in. %% It is advisable to get rid of this exctractor when this issue will be solved. --behaviour(tk_context_extractor). +-behaviour(tk_extractor). -export([get_context/2]). @@ -20,7 +20,7 @@ %% API functions --spec get_context(tk_token_jwt:t(), extractor_opts()) -> tk_context_extractor:extracted_context(). +-spec get_context(tk_token_jwt:t(), extractor_opts()) -> tk_extractor:extracted_context(). get_context(Token, ExtractorOpts) -> UserID = tk_token_jwt:get_subject_id(Token), case extract_invoice_template_rights(Token, ExtractorOpts) of diff --git a/src/tk_extractor_phony_api_key.erl b/src/tk_extractor_phony_api_key.erl index 986a32b..254086d 100644 --- a/src/tk_extractor_phony_api_key.erl +++ b/src/tk_extractor_phony_api_key.erl @@ -1,5 +1,5 @@ -module(tk_extractor_phony_api_key). --behaviour(tk_context_extractor). +-behaviour(tk_extractor). -export([get_context/2]). @@ -13,7 +13,7 @@ %% API functions --spec get_context(tk_token_jwt:t(), extractor_opts()) -> tk_context_extractor:extracted_context(). +-spec get_context(tk_token_jwt:t(), extractor_opts()) -> tk_extractor:extracted_context(). get_context(Token, ExtractorOpts) -> UserID = tk_token_jwt:get_subject_id(Token), Acc0 = bouncer_context_helpers:empty(), diff --git a/src/tk_extractor_user_session_token.erl b/src/tk_extractor_user_session_token.erl index d8e1aad..9c054e7 100644 --- a/src/tk_extractor_user_session_token.erl +++ b/src/tk_extractor_user_session_token.erl @@ -1,5 +1,5 @@ -module(tk_extractor_user_session_token). --behaviour(tk_context_extractor). +-behaviour(tk_extractor). -export([get_context/2]). @@ -14,7 +14,7 @@ %% API functions --spec get_context(tk_token_jwt:t(), extractor_opts()) -> tk_context_extractor:extracted_context(). +-spec get_context(tk_token_jwt:t(), extractor_opts()) -> tk_extractor:extracted_context(). get_context(Token, ExtractorOpts) -> UserID = tk_token_jwt:get_subject_id(Token), Email = tk_token_jwt:get_subject_email(Token), @@ -51,7 +51,7 @@ get_context(Token, ExtractorOpts) -> make_auth_expiration(Timestamp) when is_integer(Timestamp) -> genlib_rfc3339:format(Timestamp, second); -make_auth_expiration(unlimited) -> +make_auth_expiration(Expiration) when Expiration =:= unlimited; Expiration =:= undefined -> undefined. wrap_metadata(Metadata, ExtractorOpts) -> diff --git a/src/tk_pulse.erl b/src/tk_pulse.erl index e82ca03..dc73b82 100644 --- a/src/tk_pulse.erl +++ b/src/tk_pulse.erl @@ -4,7 +4,10 @@ {get_by_token, started | succeeded - | {failed, _Reason}}. + | {failed, _Reason}} + | {create_ephemeral, + started + | succeeded}. -type metadata() :: #{ token => tk_token_jwt:t(), diff --git a/src/tk_storage.erl b/src/tk_storage.erl new file mode 100644 index 0000000..630930e --- /dev/null +++ b/src/tk_storage.erl @@ -0,0 +1,70 @@ +-module(tk_storage). + +-export([get/2]). +-export([get_by_claims/2]). +-export([store/2]). +-export([revoke/2]). + +%% + +-callback get(authdata_id(), opts()) -> {ok, tk_storage:storable_authdata()} | {error, _Reason}. +-callback get_by_claims(claims(), opts()) -> {ok, tk_storage:storable_authdata()} | {error, _Reason}. +-callback store(tk_storage:storable_authdata(), opts()) -> {ok, claims()} | {error, _Reason}. +-callback revoke(authdata_id(), opts()) -> ok | {error, _Reason}. + +%% + +-type storable_authdata() :: #{ + id => tk_authority:authdata_id(), + status := tk_authority:status(), + context := tk_authority:encoded_context_fragment(), + authority => tk_authority:autority_id(), + metadata => tk_authority:metadata() +}. + +-export_type([storable_authdata/0]). + +%% + +-type authdata_id() :: tk_authority:authdata_id(). +-type claims() :: tk_token_jwt:claims(). + +-type storage_opts() :: {storage(), opts()} | storage(). +-type storage() :: claim. +-type opts() :: tk_storage_claim:storage_opts(). + +%% + +-spec get(authdata_id(), storage_opts()) -> {ok, storable_authdata()} | {error, _Reason}. +get(DataID, StorageOpts) -> + {Storage, Opts} = get_storage_opts(StorageOpts), + Handler = get_storage_handler(Storage), + Handler:get(DataID, Opts). + +-spec get_by_claims(claims(), storage_opts()) -> {ok, storable_authdata()} | {error, _Reason}. +get_by_claims(Claims, StorageOpts) -> + {Storage, Opts} = get_storage_opts(StorageOpts), + Handler = get_storage_handler(Storage), + Handler:get_by_claims(Claims, Opts). + +-spec store(storable_authdata(), storage_opts()) -> {ok, claims()} | {error, _Reason}. +store(AuthData, StorageOpts) -> + {Storage, Opts} = get_storage_opts(StorageOpts), + Handler = get_storage_handler(Storage), + Handler:store(AuthData, Opts). + +-spec revoke(authdata_id(), storage_opts()) -> ok | {error, _Reason}. +revoke(DataID, StorageOpts) -> + {Storage, Opts} = get_storage_opts(StorageOpts), + Handler = get_storage_handler(Storage), + Handler:revoke(DataID, Opts). + +%% + +get_storage_handler(claim) -> + tk_storage_claim. + +get_storage_opts({_Storage, _Opts} = StorageOpts) -> + StorageOpts; +get_storage_opts(Storage) when is_atom(Storage) -> + {Storage, #{}}. diff --git a/src/tk_storage_claim.erl b/src/tk_storage_claim.erl new file mode 100644 index 0000000..13591e1 --- /dev/null +++ b/src/tk_storage_claim.erl @@ -0,0 +1,133 @@ +-module(tk_storage_claim). + +-include_lib("token_keeper_proto/include/tk_context_thrift.hrl"). + +-behaviour(tk_storage). +-export([get/2]). +-export([get_by_claims/2]). +-export([store/2]). +-export([revoke/2]). + +-type storage_opts() :: #{ + compatability => {true, MetadataNS :: binary()} | false +}. + +-export_type([storage_opts/0]). + +%% + +-type storable_authdata() :: tk_storage:storable_authdata(). +-type authdata_id() :: tk_authority:authdata_id(). +-type claim() :: tk_token_jwt:claim(). +-type claims() :: tk_token_jwt:claims(). + +-define(CLAIM_BOUNCER_CTX, <<"bouncer_ctx">>). +-define(CLAIM_TK_METADATA, <<"tk_metadata">>). + +-define(CLAIM_CTX_TYPE, <<"ty">>). +-define(CLAIM_CTX_CONTEXT, <<"ct">>). +-define(CLAIM_CTX_TYPE_V1_THRIFT_BINARY, <<"v1_thrift_binary">>). + +%% + +-spec get(authdata_id(), storage_opts()) -> {error, not_found}. +get(_DataID, _Opts) -> + {error, not_found}. + +-spec get_by_claims(claims(), storage_opts()) -> + {ok, storable_authdata()} + | {error, not_found | {claim_decode_error, {unsupported, claim()} | {malformed, binary()}}}. +get_by_claims(#{?CLAIM_BOUNCER_CTX := BouncerClaim} = Claims, Opts) -> + case decode_bouncer_claim(BouncerClaim) of + {ok, ContextFragment} -> + case get_metadata(Claims, Opts) of + {ok, Metadata} -> + {ok, create_authdata(ContextFragment, Metadata)}; + {error, no_metadata_claim} -> + {error, not_found} + end; + {error, Reason} -> + {error, {claim_decode_error, Reason}} + end; +get_by_claims(_Claims, _Opts) -> + {error, not_found}. + +-spec store(storable_authdata(), storage_opts()) -> {ok, claims()}. +store(#{context := ContextFragment} = AuthData, _Opts) -> + {ok, #{ + ?CLAIM_BOUNCER_CTX => encode_bouncer_claim(ContextFragment), + ?CLAIM_TK_METADATA => encode_metadata(AuthData) + }}. + +-spec revoke(authdata_id(), storage_opts()) -> {error, storage_immutable}. +revoke(_DataID, _Opts) -> + {error, storage_immutable}. + +%% Internal functions + +decode_bouncer_claim(#{ + ?CLAIM_CTX_TYPE := ?CLAIM_CTX_TYPE_V1_THRIFT_BINARY, + ?CLAIM_CTX_CONTEXT := Content +}) -> + try + {ok, #bctx_ContextFragment{ + type = v1_thrift_binary, + content = base64:decode(Content) + }} + catch + % NOTE + % The `base64:decode/1` fails in unpredictable ways. + error:_ -> + {error, {malformed, Content}} + end; +decode_bouncer_claim(Ctx) -> + {error, {unsupported, Ctx}}. + +encode_bouncer_claim( + #bctx_ContextFragment{ + type = v1_thrift_binary, + content = Content + } +) -> + #{ + ?CLAIM_CTX_TYPE => ?CLAIM_CTX_TYPE_V1_THRIFT_BINARY, + ?CLAIM_CTX_CONTEXT => base64:encode(Content) + }. + +encode_metadata(#{metadata := Metadata}) -> + Metadata; +encode_metadata(#{}) -> + #{}. + +get_metadata(#{?CLAIM_TK_METADATA := Metadata}, _Opts) -> + {ok, Metadata}; +get_metadata(Claims, #{compatability := {true, MetadataNS}}) -> + {ok, wrap_metadata(create_metadata(Claims), MetadataNS)}; +get_metadata(_Claims, _Opts) -> + {error, no_metadata_claim}. + +create_authdata(ContextFragment, Metadata) -> + genlib_map:compact(#{ + status => active, + context => ContextFragment, + metadata => Metadata + }). + +create_metadata(Claims) -> + Metadata = maps:with(get_passthrough_claim_names(), Claims), + %% TODO: This is a temporary hack. + %% When some external services will stop requiring woody user identity to be present it must be removed too + genlib_map:compact(Metadata#{ + <<"party_id">> => maps:get(<<"sub">>, Claims, undefined) + }). + +wrap_metadata(Metadata, _MetadataNS) when map_size(Metadata) =:= 0 -> + undefined; +wrap_metadata(Metadata, MetadataNS) -> + #{MetadataNS => Metadata}. + +get_passthrough_claim_names() -> + [ + %% token consumer + <<"cons">> + ]. diff --git a/src/tk_storage_machinegun.erl b/src/tk_storage_machinegun.erl new file mode 100644 index 0000000..08dc259 --- /dev/null +++ b/src/tk_storage_machinegun.erl @@ -0,0 +1,43 @@ +-module(tk_storage_machinegun). + +%% NOTE: This storage is not yet implemented + +-include_lib("token_keeper_proto/include/tk_context_thrift.hrl"). + +-behaviour(tk_storage). +-export([get/2]). +-export([get_by_claims/2]). +-export([store/2]). +-export([revoke/2]). + +-type storage_opts() :: #{}. +-export_type([storage_opts/0]). + +%% + +-type storable_authdata() :: tk_storage:storable_authdata(). +-type authdata_id() :: tk_authority:authdata_id(). +-type claims() :: tk_token_jwt:claims(). + +%% + +-spec get(authdata_id(), storage_opts()) -> {ok, storable_authdata()} | {error, not_found}. +get(_DataID, _Opts) -> + %% Collapse history and return the auth data? + {error, not_found}. + +-spec get_by_claims(claims(), storage_opts()) -> {ok, storable_authdata()} | {error, not_found}. +get_by_claims(_Claims, _Opts) -> + %% Extract id from claims, collapse history and return the auth data? + {error, not_found}. + +-spec store(storable_authdata(), storage_opts()) -> {ok, claims()} | {error, _Reason}. +store(_AuthData, _Opts) -> + %% Start a new machine, post event, make claims with id + %% Consider ways to generate authdata ids? + {error, not_implemented}. + +-spec revoke(authdata_id(), storage_opts()) -> ok | {error, _Reason}. +revoke(_DataID, _Opts) -> + %% Post a revocation event? + {error, not_implemented}. diff --git a/src/tk_token_jwt.erl b/src/tk_token_jwt.erl index c1cc2a5..7a1c0dc 100644 --- a/src/tk_token_jwt.erl +++ b/src/tk_token_jwt.erl @@ -5,7 +5,7 @@ %% API --export([issue/3]). +-export([issue/2]). -export([verify/2]). -export([get_token_id/1]). @@ -22,6 +22,8 @@ -export([create_claims/2]). -export([set_subject_email/2]). +-export([get_key_authority/1]). + %% Supervisor callbacks -export([init/1]). @@ -29,7 +31,7 @@ %% API types --type t() :: {token_id(), claims(), authority(), metadata()}. +-type t() :: {claims(), authority(), metadata()}. -type claim() :: expiration() | term(). -type claims() :: #{binary() => claim()}. -type token() :: binary(). @@ -44,6 +46,8 @@ source_context => source_context() }. +-type keyname() :: term(). + -export_type([t/0]). -export_type([claim/0]). -export_type([claims/0]). @@ -51,10 +55,10 @@ -export_type([expiration/0]). -export_type([metadata/0]). -export_type([options/0]). +-export_type([keyname/0]). %% Internal types --type keyname() :: term(). -type kid() :: binary(). -type key() :: #jose_jwk{}. @@ -89,9 +93,9 @@ %% API functions %% --spec get_token_id(t()) -> token_id(). -get_token_id({TokenId, _Claims, _Authority, _Metadata}) -> - TokenId. +-spec get_token_id(t()) -> token_id() | undefined. +get_token_id(T) -> + get_claim(?CLAIM_TOKEN_ID, T, undefined). -spec get_subject_id(t()) -> subject_id() | undefined. get_subject_id(T) -> @@ -101,35 +105,35 @@ get_subject_id(T) -> get_subject_email(T) -> get_claim(?CLAIM_SUBJECT_EMAIL, T, undefined). --spec get_expires_at(t()) -> expiration(). -get_expires_at({_TokenId, Claims, _Authority, _Metadata}) -> - case maps:get(?CLAIM_EXPIRES_AT, Claims) of +-spec get_expires_at(t()) -> expiration() | undefined. +get_expires_at(T) -> + case get_claim(?CLAIM_EXPIRES_AT, T, undefined) of 0 -> unlimited; V -> V end. -spec get_claims(t()) -> claims(). -get_claims({_TokenId, Claims, _Authority, _Metadata}) -> +get_claims({Claims, _Authority, _Metadata}) -> Claims. -spec get_claim(binary(), t()) -> claim(). -get_claim(ClaimName, {_TokenId, Claims, _Authority, _Metadata}) -> +get_claim(ClaimName, {Claims, _Authority, _Metadata}) -> maps:get(ClaimName, Claims). -spec get_claim(binary(), t(), claim()) -> claim(). -get_claim(ClaimName, {_TokenId, Claims, _Authority, _Metadata}, Default) -> +get_claim(ClaimName, {Claims, _Authority, _Metadata}, Default) -> maps:get(ClaimName, Claims, Default). -spec get_authority(t()) -> authority(). -get_authority({_TokenId, _Claims, Authority, _Metadata}) -> +get_authority({_Claims, Authority, _Metadata}) -> Authority. -spec get_metadata(t()) -> metadata(). -get_metadata({_TokenId, _Claims, _Authority, Metadata}) -> +get_metadata({_Claims, _Authority, Metadata}) -> Metadata. -spec get_source_context(t()) -> source_context(). -get_source_context({_TokenId, _Claims, _Authority, Metadata}) -> +get_source_context({_Claims, _Authority, Metadata}) -> maps:get(source_context, Metadata). -spec create_claims(claims(), expiration()) -> claims(). @@ -143,14 +147,14 @@ set_subject_email(SubjectEmail, Claims) -> %% --spec issue(token_id(), claims(), keyname()) -> +-spec issue(claims(), keyname()) -> {ok, token()} | {error, nonexistent_key} | {error, {invalid_signee, Reason :: atom()}}. -issue(JTI, Claims, Signer) -> +issue(Claims, Signer) -> case try_get_key_for_sign(Signer) of {ok, Key} -> - FinalClaims = construct_final_claims(Claims, JTI), + FinalClaims = construct_final_claims(Claims), sign(Key, FinalClaims); {error, Error} -> {error, Error} @@ -166,10 +170,8 @@ try_get_key_for_sign(Keyname) -> {error, nonexistent_key} end. -construct_final_claims(Claims, JTI) -> - Token0 = #{?CLAIM_TOKEN_ID => JTI}, - EncodedClaims = maps:map(fun encode_claim/2, Claims), - maps:merge(EncodedClaims, Token0). +construct_final_claims(Claims) -> + maps:map(fun encode_claim/2, Claims). encode_claim(?CLAIM_EXPIRES_AT, Expiration) -> mk_expires_at(Expiration); @@ -235,24 +237,11 @@ make_metadata(SourceContext) -> verify_with_key(JWK, ExpandedToken, Authority, Metadata) -> case jose_jwt:verify(JWK, ExpandedToken) of {true, #jose_jwt{fields = Claims}, _JWS} -> - _ = validate_claims(Claims), - get_result(Claims, Authority, Metadata); + {ok, {Claims, Authority, Metadata}}; {false, _JWT, _JWS} -> {error, invalid_signature} end. -validate_claims(Claims) -> - validate_claims(Claims, get_validators()). - -validate_claims(Claims, [{Name, Claim, Validator} | Rest]) -> - _ = Validator(Name, maps:get(Claim, Claims, undefined)), - validate_claims(Claims, Rest); -validate_claims(Claims, []) -> - Claims. - -get_result(#{?CLAIM_TOKEN_ID := TokenID} = Claims, Authority, Metadata) -> - {ok, {TokenID, maps:without([?CLAIM_TOKEN_ID], Claims), Authority, Metadata}}. - get_kid(#{<<"kid">> := KID}) when is_binary(KID) -> KID; get_kid(#{}) -> @@ -263,18 +252,16 @@ get_alg(#{<<"alg">> := Alg}) when is_binary(Alg) -> get_alg(#{}) -> throw({invalid_token, {missing, alg}}). -get_validators() -> - [ - {token_id, ?CLAIM_TOKEN_ID, fun check_presence/2}, - {expires_at, ?CLAIM_EXPIRES_AT, fun check_presence/2} - ]. +%% -check_presence(_, V) when is_binary(V) -> - V; -check_presence(_, V) when is_integer(V) -> - V; -check_presence(C, undefined) -> - throw({invalid_token, {missing, C}}). +-spec get_key_authority(keyname()) -> {ok, authority()} | {error, {nonexistent_key, keyname()}}. +get_key_authority(KeyName) -> + case get_key_by_name(KeyName) of + #{authority := Authority} -> + {ok, Authority}; + undefined -> + {error, {nonexistent_key, KeyName}} + end. %% %% Supervisor callbacks diff --git a/src/tk_handler.erl b/src/tk_woody_handler.erl similarity index 58% rename from src/tk_handler.erl rename to src/tk_woody_handler.erl index 03439b7..704bd54 100644 --- a/src/tk_handler.erl +++ b/src/tk_woody_handler.erl @@ -1,4 +1,4 @@ --module(tk_handler). +-module(tk_woody_handler). -include_lib("token_keeper_proto/include/tk_context_thrift.hrl"). -include_lib("token_keeper_proto/include/tk_token_keeper_thrift.hrl"). @@ -25,21 +25,27 @@ -spec handle_function(woody:func(), woody:args(), woody_context:ctx(), opts()) -> {ok, woody:result()}. handle_function(Op, Args, WoodyCtx, Opts) -> State = make_state(WoodyCtx, Opts), - do_handle_function(Op, Args, State). + handle_function_(Op, Args, State). -do_handle_function('Create', _, _State) -> +handle_function_('Create', {_ID, _ContextFragment, _Metadata}, _State) -> + %% TODO: Change protocol to include authdata id erlang:error(not_implemented); -do_handle_function('CreateEphemeral', _, _State) -> +handle_function_('CreateEphemeral' = Op, {ContextFragment, Metadata}, State) -> + _ = handle_beat(Op, started, State), + StorageType = claim, + AuthorityID = get_issuing_authority(), + AuthData = issue_token(ContextFragment, Metadata, AuthorityID, StorageType), + _ = handle_beat(Op, succeeded, State), + {ok, AuthData}; +handle_function_('AddExistingToken', _, _State) -> erlang:error(not_implemented); -do_handle_function('AddExistingToken', _, _State) -> - erlang:error(not_implemented); -do_handle_function('GetByToken' = Op, {Token, TokenSourceContext}, State) -> +handle_function_('GetByToken' = Op, {Token, TokenSourceContext}, State) -> _ = handle_beat(Op, started, State), TokenSourceContextDecoded = decode_source_context(TokenSourceContext), case tk_token_jwt:verify(Token, TokenSourceContextDecoded) of {ok, TokenInfo} -> State1 = save_pulse_metadata(#{token => TokenInfo}, State), - Authority = get_token_authority(TokenInfo), + Authority = get_autority_config(get_token_authority(TokenInfo)), case tk_authority:get_authdata_by_token(TokenInfo, Authority) of {ok, AuthDataPrototype} -> EncodedAuthData = encode_auth_data(AuthDataPrototype#{token => Token}), @@ -53,13 +59,23 @@ do_handle_function('GetByToken' = Op, {Token, TokenSourceContext}, State) -> _ = handle_beat(Op, {failed, {token_verification, Reason}}, State), woody_error:raise(business, #token_keeper_InvalidToken{}) end; -do_handle_function('Get', _, _State) -> +handle_function_('Get', _, _State) -> erlang:error(not_implemented); -do_handle_function('Revoke', _, _State) -> +handle_function_('Revoke', _, _State) -> erlang:error(not_implemented). %% Internal functions +issue_token(ContextFragment, Metadata, AuthorityID, StorageType) -> + issue_token(undefined, ContextFragment, Metadata, AuthorityID, StorageType). + +issue_token(ID, ContextFragment, Metadata, AuthorityID, StorageType) -> + Authority = get_autority_config(AuthorityID), + AuthDataPrototype = tk_authority:create_authdata(ID, ContextFragment, Metadata, Authority), + {ok, StorageClaims} = tk_storage:store(AuthDataPrototype, StorageType), + {ok, Token} = tk_token_jwt:issue(StorageClaims, get_signer(AuthorityID, Authority)), + encode_auth_data(AuthDataPrototype#{token => Token}). + make_state(WoodyCtx, Opts) -> #state{ woody_context = WoodyCtx, @@ -86,7 +102,9 @@ decode_source_context(TokenSourceContext) -> %% get_token_authority(TokenInfo) -> - AuthorityID = tk_token_jwt:get_authority(TokenInfo), + tk_token_jwt:get_authority(TokenInfo). + +get_autority_config(AuthorityID) -> Authorities = application:get_env(token_keeper, authorities, #{}), case maps:get(AuthorityID, Authorities, undefined) of Config when Config =/= undefined -> @@ -95,6 +113,30 @@ get_token_authority(TokenInfo) -> throw({misconfiguration, {no_such_authority, AuthorityID}}) end. +get_issuing_authority() -> + maps:get(authority, get_issuing_config()). + +get_issuing_config() -> + case application:get_env(token_keeper, issuing, undefined) of + Config when Config =/= undefined -> + Config; + undefined -> + error({misconfiguration, {issuing, not_configured}}) + end. + +%% + +get_signer(AuthorityID, Authority) -> + SignerKID = tk_authority:get_signer(Authority), + case tk_token_jwt:get_key_authority(SignerKID) of + {ok, AuthorityID} -> + SignerKID; + {ok, OtherAuthorityID} -> + error({misconfiguration, {issuing, {key_ownership_error, {AuthorityID, OtherAuthorityID}}}}); + _ -> + error({misconfiguration, {issuing, {no_key, SignerKID}}}) + end. + %% handle_beat(Op, Event, State) -> @@ -103,5 +145,7 @@ handle_beat(Op, Event, State) -> save_pulse_metadata(Metadata, State = #state{pulse_metadata = PulseMetadata}) -> State#state{pulse_metadata = maps:merge(Metadata, PulseMetadata)}. +encode_pulse_op('CreateEphemeral') -> + create_ephemeral; encode_pulse_op('GetByToken') -> get_by_token. diff --git a/src/token_keeper.app.src b/src/token_keeper.app.src index 5f7b9dd..b014520 100644 --- a/src/token_keeper.app.src +++ b/src/token_keeper.app.src @@ -9,6 +9,7 @@ genlib, woody, jose, + snowflake, token_keeper_proto, bouncer_client, how_are_you, diff --git a/src/token_keeper.erl b/src/token_keeper.erl index 71c8c75..d04e467 100644 --- a/src/token_keeper.erl +++ b/src/token_keeper.erl @@ -110,7 +110,7 @@ get_handler_specs(ServiceOpts, AuditPulse) -> [ { maps:get(path, TokenKeeperService, <<"/v1/token-keeper">>), - {{tk_token_keeper_thrift, 'TokenKeeper'}, {tk_handler, TokenKeeperOpts}} + {{tk_token_keeper_thrift, 'TokenKeeper'}, {tk_woody_handler, TokenKeeperOpts}} } ]. diff --git a/test/tk_tests_SUITE.erl b/test/tk_tests_SUITE.erl index 6cb31e9..569a1a6 100644 --- a/test/tk_tests_SUITE.erl +++ b/test/tk_tests_SUITE.erl @@ -27,6 +27,7 @@ -export([invoice_template_access_token_ok_test/1]). -export([invoice_template_access_token_no_access_test/1]). -export([invoice_template_access_token_invalid_access_test/1]). +-export([basic_issuing_test/1]). -type config() :: ct_helper:config(). -type group_name() :: atom(). @@ -63,7 +64,8 @@ all() -> [ {group, detect_token_type}, {group, claim_only}, - {group, invoice_template_access_token} + {group, invoice_template_access_token}, + {group, issuing} ]. -spec groups() -> [{group_name(), list(), [test_case_name()]}]. @@ -83,6 +85,9 @@ groups() -> invoice_template_access_token_ok_test, invoice_template_access_token_no_access_test, invoice_template_access_token_invalid_access_test + ]}, + {issuing, [parallel], [ + basic_issuing_test ]} ]. @@ -115,7 +120,6 @@ init_per_group(detect_token_type = Name, C) -> keycloak => #{ id => ?TK_AUTHORITY_KEYCLOAK, authdata_sources => [ - storage, {extract, #{ methods => [ {detect_token, #{ @@ -149,13 +153,10 @@ init_per_group(claim_only = Name, C) -> claim_only => #{ id => ?TK_AUTHORITY_CAPI, authdata_sources => [ - {extract, #{ - methods => [ - {claim, #{ - metadata_ns => ?TK_META_NS_APIKEYMGMT - }} - ] - }} + {storage, + {claim, #{ + compatability => {true, ?TK_META_NS_APIKEYMGMT} + }}} ] } }} @@ -175,11 +176,12 @@ init_per_group(invoice_template_access_token = Name, C) -> invoice_tpl_authority => #{ id => ?TK_AUTHORITY_CAPI, authdata_sources => [ + {storage, + {claim, #{ + compatability => {true, ?TK_META_NS_APIKEYMGMT} + }}}, {extract, #{ methods => [ - {claim, #{ - metadata_ns => ?TK_META_NS_APIKEYMGMT - }}, {invoice_template_access_token, #{ domain => ?TK_RESOURCE_DOMAIN, metadata_ns => ?TK_META_NS_APIKEYMGMT @@ -191,6 +193,30 @@ init_per_group(invoice_template_access_token = Name, C) -> }} ]) ++ [{groupname, Name} | C]; +init_per_group(issuing = Name, C) -> + start_keeper([ + {jwt, #{ + keyset => #{ + test => #{ + source => {pem_file, get_keysource("keys/local/private.pem", C)}, + authority => issuing_authority + } + } + }}, + {issuing, #{ + authority => issuing_authority + }}, + {authorities, #{ + issuing_authority => #{ + id => ?TK_AUTHORITY_CAPI, + signer => test, + authdata_sources => [ + {storage, claim} + ] + } + }} + ]) ++ + [{groupname, Name} | C]; init_per_group(Name, C) -> [{groupname, Name} | C]. @@ -382,6 +408,27 @@ invoice_template_access_token_invalid_access_test(C) -> #token_keeper_AuthDataNotFound{} = (catch call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(), Client)). +-spec basic_issuing_test(config()) -> ok. +basic_issuing_test(C) -> + Client = mk_client(C), + JTI = unique_id(), + BinaryContextFragment = create_bouncer_context(JTI), + Context = #bctx_ContextFragment{ + type = v1_thrift_binary, + content = BinaryContextFragment + }, + Metadata = #{<<"ns">> => #{<<"my">> => <<"metadata">>}}, + #token_keeper_AuthData{ + id = undefined, + token = Token, + status = active, + context = Context, + metadata = Metadata, + authority = ?TK_AUTHORITY_CAPI + } = AuthData = call_create_ephemeral(Context, Metadata, Client), + ok = verify_token(Token, BinaryContextFragment, Metadata, JTI), + AuthData = call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(), Client). + %% mk_client(C) -> @@ -392,6 +439,9 @@ mk_client(C) -> call_get_by_token(Token, TokenSourceContext, Client) -> call_token_keeper('GetByToken', {Token, TokenSourceContext}, Client). +call_create_ephemeral(ContextFragment, Metadata, Client) -> + call_token_keeper('CreateEphemeral', {ContextFragment, Metadata}, Client). + call_token_keeper(Operation, Args, Client) -> call(token_keeper, Operation, Args, Client). @@ -455,6 +505,25 @@ assert_user({user_session_token, _JTI, SubjectID, SubjectEmail, _Exp}, User) -> %% +verify_token(Token, BinaryContextFragment, Metadata, _JTI) -> + EncodedContextFragment = base64:encode(BinaryContextFragment), + case tk_token_jwt:verify(Token, #{}) of + {ok, TokenInfo} -> + #{ + %<<"jti">> := JTI, %% FIXME this will never match + <<"bouncer_ctx">> := #{ + <<"ty">> := <<"v1_thrift_binary">>, + <<"ct">> := EncodedContextFragment + }, + <<"tk_metadata">> := Metadata + } = tk_token_jwt:get_claims(TokenInfo), + ok; + Error -> + Error + end. + +%% + make_auth_expiration(Timestamp) when is_integer(Timestamp) -> genlib_rfc3339:format(Timestamp, second); make_auth_expiration(unlimited) -> @@ -462,14 +531,7 @@ make_auth_expiration(unlimited) -> %% -issue_token(JTI, Claims0, Expiration) -> - Claims = tk_token_jwt:create_claims(Claims0, Expiration), - tk_token_jwt:issue(JTI, Claims, test). - -issue_token_with_context(JTI, SubjectID) -> - issue_token_with_context(JTI, SubjectID, #{}). - -issue_token_with_context(JTI, SubjectID, AdditionalClaims) -> +create_bouncer_context(JTI) -> Acc0 = bouncer_context_helpers:empty(), Acc1 = bouncer_context_helpers:add_auth( #{ @@ -478,7 +540,17 @@ issue_token_with_context(JTI, SubjectID, AdditionalClaims) -> }, Acc0 ), - FragmentContent = encode_context_fragment_content(Acc1), + encode_context_fragment_content(Acc1). + +issue_token(JTI, Claims0, Expiration) -> + Claims1 = tk_token_jwt:create_claims(Claims0, Expiration), + tk_token_jwt:issue(Claims1#{<<"jti">> => JTI}, test). + +issue_token_with_context(JTI, SubjectID) -> + issue_token_with_context(JTI, SubjectID, #{}). + +issue_token_with_context(JTI, SubjectID, AdditionalClaims) -> + FragmentContent = create_bouncer_context(JTI), issue_token( JTI, AdditionalClaims#{ diff --git a/test/tk_token_jwt_tests_SUITE.erl b/test/tk_token_jwt_tests_SUITE.erl index 87ff1c1..b19957f 100644 --- a/test/tk_token_jwt_tests_SUITE.erl +++ b/test/tk_token_jwt_tests_SUITE.erl @@ -65,7 +65,10 @@ verify_test(_) -> JTI = unique_id(), PartyID = <<"TEST">>, {ok, Token} = issue_token(JTI, #{<<"sub">> => PartyID, <<"TEST">> => <<"TEST">>}, unlimited), - {ok, {JTI, #{<<"sub">> := PartyID, <<"TEST">> := <<"TEST">>}, test, #{}}} = tk_token_jwt:verify(Token, #{}). + {ok, {#{<<"jti">> := JTI, <<"sub">> := PartyID, <<"TEST">> := <<"TEST">>}, test, #{}}} = tk_token_jwt:verify( + Token, + #{} + ). -spec bad_token_test(config()) -> _. bad_token_test(Config) -> @@ -76,13 +79,13 @@ bad_token_test(Config) -> bad_signee_test(_) -> Claims = tk_token_jwt:create_claims(#{}, unlimited), {error, nonexistent_key} = - tk_token_jwt:issue(unique_id(), Claims, random). + tk_token_jwt:issue(Claims, random). %% issue_token(JTI, Claims0, Expiration) -> - Claims = tk_token_jwt:create_claims(Claims0, Expiration), - tk_token_jwt:issue(JTI, Claims, test). + Claims1 = tk_token_jwt:create_claims(Claims0, Expiration), + tk_token_jwt:issue(Claims1#{<<"jti">> => JTI}, test). issue_dummy_token(Config) -> Claims = #{