mirror of
https://github.com/valitydev/token-keeper.git
synced 2024-11-06 02:15:21 +00:00
ED-175: Ephemeral token issuing (#11)
This commit is contained in:
parent
81fb238b29
commit
8500ffb9bd
@ -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`
|
||||
|
@ -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"}
|
||||
|
@ -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 = #{}) ->
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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.
|
||||
|
@ -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)}.
|
||||
|
@ -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) ->
|
@ -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}}.
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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) ->
|
||||
|
@ -4,7 +4,10 @@
|
||||
{get_by_token,
|
||||
started
|
||||
| succeeded
|
||||
| {failed, _Reason}}.
|
||||
| {failed, _Reason}}
|
||||
| {create_ephemeral,
|
||||
started
|
||||
| succeeded}.
|
||||
|
||||
-type metadata() :: #{
|
||||
token => tk_token_jwt:t(),
|
||||
|
70
src/tk_storage.erl
Normal file
70
src/tk_storage.erl
Normal file
@ -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, #{}}.
|
133
src/tk_storage_claim.erl
Normal file
133
src/tk_storage_claim.erl
Normal file
@ -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">>
|
||||
].
|
43
src/tk_storage_machinegun.erl
Normal file
43
src/tk_storage_machinegun.erl
Normal file
@ -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}.
|
@ -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
|
||||
|
@ -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.
|
@ -9,6 +9,7 @@
|
||||
genlib,
|
||||
woody,
|
||||
jose,
|
||||
snowflake,
|
||||
token_keeper_proto,
|
||||
bouncer_client,
|
||||
how_are_you,
|
||||
|
@ -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}}
|
||||
}
|
||||
].
|
||||
|
||||
|
@ -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#{
|
||||
|
@ -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 = #{
|
||||
|
Loading…
Reference in New Issue
Block a user