mirror of
https://github.com/valitydev/token-keeper.git
synced 2024-11-06 02:15:21 +00:00
ED-46: Refactor authdata aquisition and creation code (#7)
This commit is contained in:
parent
40ddb4ef26
commit
18286e30be
@ -40,23 +40,48 @@
|
||||
% service => {erl_health, service , [<<"bouncer">>]}
|
||||
}},
|
||||
|
||||
{tokens, #{
|
||||
jwt => #{
|
||||
{jwt, #{
|
||||
keyset => #{
|
||||
test => #{
|
||||
source => {pem_file, "keys/local/private.pem"},
|
||||
metadata => #{
|
||||
authority => <<"test.rbkmoney.keycloak">>,
|
||||
metadata_ns => <<"test.rbkmoney.token-keeper">>,
|
||||
auth_method => detect,
|
||||
user_realm => <<"external">>
|
||||
}
|
||||
}
|
||||
%% Who is the authority for this token
|
||||
authority => keycloak
|
||||
}
|
||||
}
|
||||
}},
|
||||
|
||||
{user_session_token_origins, [<<"http://localhost">>]}
|
||||
%% Authority configuration
|
||||
{authorities, #{
|
||||
keycloak => #{
|
||||
%% What to use as authority id string
|
||||
id => <<"test.rbkmoney.keycloak">>,
|
||||
%% Where to fetch authdata for tokens issued by this authority
|
||||
authdata_sources => [
|
||||
%% Fetch from service storage
|
||||
storage,
|
||||
%% Extract bouncer context from the token itself and make up the rest
|
||||
{extract, #{
|
||||
%% Configuration for how to extract said context
|
||||
methods => [
|
||||
%% Extract bouncer context from claim
|
||||
claim,
|
||||
%% Create bouncer context from various legacy data the token has to offer
|
||||
%% Avaiable options are: `phony_api_key`, `user_session_token`, `detect_token`
|
||||
%% `user_session_token` requires the `user_realm` option to be set
|
||||
%% `detect_token` tries to determine wether the token is an
|
||||
%% `phony_api_key` or `user_session_token` based on token's source context and
|
||||
%% `user_session_token_origins` option
|
||||
{detect_token, #{
|
||||
user_session_token_origins => [<<"http://localhost">>],
|
||||
user_realm => <<"external">>
|
||||
}}
|
||||
],
|
||||
%% What to use as metadata namespace
|
||||
metadata_ns => <<"test.rbkmoney.token-keeper">>
|
||||
}}
|
||||
]
|
||||
}
|
||||
}}
|
||||
|
||||
]},
|
||||
|
||||
|
@ -240,10 +240,11 @@ extract_opt_meta(K, Metadata, EncodeFun, Acc) ->
|
||||
error -> Acc
|
||||
end.
|
||||
|
||||
encode_token({JTI, Claims, TokenMetadata}) ->
|
||||
encode_token({JTI, Claims, Authority, TokenMetadata}) ->
|
||||
#{
|
||||
jti => JTI,
|
||||
claims => Claims,
|
||||
authority => Authority,
|
||||
metadata => TokenMetadata
|
||||
}.
|
||||
|
||||
|
46
src/tk_authdata_source.erl
Normal file
46
src/tk_authdata_source.erl
Normal file
@ -0,0 +1,46 @@
|
||||
-module(tk_authdata_source).
|
||||
|
||||
%% Behaviour
|
||||
|
||||
-callback get_authdata(tk_token_jwt:t(), source_opts()) -> stored_authdata() | undefined.
|
||||
|
||||
%% API functions
|
||||
|
||||
-export([get_authdata/3]).
|
||||
|
||||
%% API Types
|
||||
|
||||
-type authdata_source() :: token_source() | {token_source(), source_opts()}.
|
||||
|
||||
-type token_source() :: storage | extractor.
|
||||
|
||||
-type source_opts() :: #{
|
||||
methods => tk_context_extractor:methods(),
|
||||
metadata_ns => binary()
|
||||
}.
|
||||
|
||||
-type stored_authdata() :: #{
|
||||
id => tk_authority:id(),
|
||||
status := tk_authority:status(),
|
||||
context := tk_authority:encoded_context_fragment(),
|
||||
metadata => tk_authority:metadata()
|
||||
}.
|
||||
|
||||
-export_type([authdata_source/0]).
|
||||
-export_type([token_source/0]).
|
||||
-export_type([source_opts/0]).
|
||||
-export_type([stored_authdata/0]).
|
||||
|
||||
%% API functions
|
||||
|
||||
-spec get_authdata(token_source(), tk_token_jwt:t(), source_opts()) -> stored_authdata() | undefined.
|
||||
get_authdata(Source, Token, Opts) ->
|
||||
Hander = get_source_handler(Source),
|
||||
Hander:get_authdata(Token, Opts).
|
||||
|
||||
%%
|
||||
|
||||
get_source_handler(storage) ->
|
||||
tk_authdata_source_storage;
|
||||
get_source_handler(extract) ->
|
||||
tk_authdata_source_extractor.
|
71
src/tk_authdata_source_extractor.erl
Normal file
71
src/tk_authdata_source_extractor.erl
Normal file
@ -0,0 +1,71 @@
|
||||
-module(tk_authdata_source_extractor).
|
||||
-behaviour(tk_authdata_source).
|
||||
|
||||
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
|
||||
|
||||
%% Behaviour
|
||||
|
||||
-export([get_authdata/2]).
|
||||
|
||||
%% Behaviour functions
|
||||
|
||||
-spec get_authdata(tk_token_jwt:t(), tk_authdata_source:source_opts()) ->
|
||||
tk_authdata_source:stored_authdata() | undefined.
|
||||
get_authdata(Token, Opts) ->
|
||||
Methods = get_extractor_methods(Opts),
|
||||
case extract_context_with(Methods, Token) of
|
||||
{Context, Metadata} ->
|
||||
make_auth_data(Context, Metadata, Opts);
|
||||
undefined ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
%%
|
||||
|
||||
get_extractor_methods(Opts) ->
|
||||
maps:get(methods, Opts).
|
||||
|
||||
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
|
||||
AuthData when AuthData =/= undefined ->
|
||||
AuthData;
|
||||
undefined ->
|
||||
extract_context_with(Rest, Token)
|
||||
end.
|
||||
|
||||
make_auth_data(ContextFragment, Metadata, SourceOpts) ->
|
||||
genlib_map:compact(#{
|
||||
status => active,
|
||||
context => encode_context_fragment(ContextFragment),
|
||||
metadata => wrap_metadata(Metadata, SourceOpts)
|
||||
}).
|
||||
|
||||
wrap_metadata(undefined, _SourceOpts) ->
|
||||
undefined;
|
||||
wrap_metadata(Metadata, SourceOpts) ->
|
||||
MetadataNS = maps:get(metadata_ns, SourceOpts),
|
||||
#{MetadataNS => Metadata}.
|
||||
|
||||
encode_context_fragment({encoded_context_fragment, ContextFragment}) ->
|
||||
ContextFragment;
|
||||
encode_context_fragment(ContextFragment) ->
|
||||
#bctx_ContextFragment{
|
||||
type = v1_thrift_binary,
|
||||
content = encode_context_fragment_content(ContextFragment)
|
||||
}.
|
||||
|
||||
encode_context_fragment_content(ContextFragment) ->
|
||||
Type = {struct, struct, {bouncer_context_v1_thrift, 'ContextFragment'}},
|
||||
Codec = thrift_strict_binary_codec:new(),
|
||||
case thrift_strict_binary_codec:write(Codec, Type, ContextFragment) of
|
||||
{ok, Codec1} ->
|
||||
thrift_strict_binary_codec:close(Codec1)
|
||||
end.
|
||||
|
||||
get_method_opts({_Method, _Opts} = MethodOpts) ->
|
||||
MethodOpts;
|
||||
get_method_opts(Method) when is_atom(Method) ->
|
||||
{Method, #{}}.
|
13
src/tk_authdata_source_storage.erl
Normal file
13
src/tk_authdata_source_storage.erl
Normal file
@ -0,0 +1,13 @@
|
||||
-module(tk_authdata_source_storage).
|
||||
-behaviour(tk_authdata_source).
|
||||
|
||||
%% Behaviour
|
||||
|
||||
-export([get_authdata/2]).
|
||||
|
||||
%% Behaviour functions
|
||||
|
||||
-spec get_authdata(tk_token_jwt:t(), tk_authdata_source:source_opts()) -> undefined.
|
||||
get_authdata(_Token, _Opts) ->
|
||||
%@TODO: This is for when we actually have storage for authdata
|
||||
undefined.
|
82
src/tk_authority.erl
Normal file
82
src/tk_authority.erl
Normal file
@ -0,0 +1,82 @@
|
||||
-module(tk_authority).
|
||||
|
||||
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
|
||||
-include_lib("token_keeper_proto/include/tk_token_keeper_thrift.hrl").
|
||||
|
||||
%% API functions
|
||||
|
||||
-export([get_authdata_by_token/2]).
|
||||
|
||||
%% API Types
|
||||
|
||||
-type authority() :: #{
|
||||
id := autority_id(),
|
||||
authdata_sources := [tk_authdata_source:authdata_source()]
|
||||
}.
|
||||
|
||||
-type authdata() :: #{
|
||||
id => id(),
|
||||
status := status(),
|
||||
context := encoded_context_fragment(),
|
||||
authority := autority_id(),
|
||||
metadata => metadata()
|
||||
}.
|
||||
|
||||
-type 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([status/0]).
|
||||
-export_type([encoded_context_fragment/0]).
|
||||
-export_type([metadata/0]).
|
||||
-export_type([metadata_ns/0]).
|
||||
-export_type([autority_id/0]).
|
||||
|
||||
%% API Functions
|
||||
|
||||
-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)};
|
||||
undefined ->
|
||||
{error, {authdata_not_found, AuthDataSources}}
|
||||
end.
|
||||
|
||||
%%
|
||||
|
||||
get_auth_data_sources(Authority) ->
|
||||
case maps:get(authdata_sources, Authority, undefined) of
|
||||
Sources when Sources =/= undefined ->
|
||||
Sources;
|
||||
undefined ->
|
||||
throw({misconfiguration, {no_authdata_sources, Authority}})
|
||||
end.
|
||||
|
||||
get_authdata_from_sources([], _Token) ->
|
||||
undefined;
|
||||
get_authdata_from_sources([SourceOpts | Rest], Token) ->
|
||||
{Source, Opts} = get_source_opts(SourceOpts),
|
||||
case tk_authdata_source:get_authdata(Source, Token, Opts) of
|
||||
AuthData when AuthData =/= undefined ->
|
||||
AuthData;
|
||||
undefined ->
|
||||
get_authdata_from_sources(Rest, Token)
|
||||
end.
|
||||
|
||||
add_authority_id(AuthData, Authority) ->
|
||||
AuthData#{authority => maps:get(id, Authority)}.
|
||||
|
||||
get_source_opts({_Source, _Opts} = SourceOpts) ->
|
||||
SourceOpts;
|
||||
get_source_opts(Source) when is_atom(Source) ->
|
||||
{Source, #{}}.
|
@ -1,151 +0,0 @@
|
||||
-module(tk_bouncer_context).
|
||||
|
||||
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
|
||||
|
||||
-export([extract_context_fragment/2]).
|
||||
|
||||
-type encoded_context_fragment() :: tk_context_thrift:'ContextFragment'().
|
||||
|
||||
%%
|
||||
|
||||
-spec extract_context_fragment(tk_token_jwt:t(), token_keeper:token_type()) -> encoded_context_fragment() | undefined.
|
||||
extract_context_fragment(TokenInfo, TokenType) ->
|
||||
extract_context_fragment([claim, metadata], TokenInfo, TokenType).
|
||||
|
||||
extract_context_fragment([Method | Rest], TokenInfo, TokenType) ->
|
||||
case extract_context_fragment_by(Method, TokenInfo, TokenType) of
|
||||
Fragment when Fragment =/= undefined ->
|
||||
Fragment;
|
||||
undefined ->
|
||||
extract_context_fragment(Rest, TokenInfo, TokenType)
|
||||
end;
|
||||
extract_context_fragment([], _, _) ->
|
||||
undefined.
|
||||
|
||||
%%
|
||||
|
||||
extract_context_fragment_by(claim, TokenInfo, _TokenType) ->
|
||||
% TODO
|
||||
% We deliberately do not handle decoding errors here since we extract claims from verified
|
||||
% tokens only, hence they must be well-formed here.
|
||||
Claims = tk_token_jwt:get_claims(TokenInfo),
|
||||
case get_claim(Claims) of
|
||||
{ok, ClaimFragment} ->
|
||||
ClaimFragment;
|
||||
undefined ->
|
||||
undefined
|
||||
end;
|
||||
extract_context_fragment_by(metadata, TokenInfo, TokenType) ->
|
||||
case tk_token_jwt:get_metadata(TokenInfo) of
|
||||
#{auth_method := detect} ->
|
||||
AuthMethod = get_auth_method(TokenType),
|
||||
build_auth_context_fragment(AuthMethod, TokenInfo);
|
||||
#{auth_method := AuthMethod} ->
|
||||
build_auth_context_fragment(AuthMethod, TokenInfo);
|
||||
#{} ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
get_auth_method(TokenType) ->
|
||||
TokenType.
|
||||
|
||||
-spec build_auth_context_fragment(
|
||||
tk_token_jwt:auth_method(),
|
||||
tk_token_jwt:t()
|
||||
) -> encoded_context_fragment().
|
||||
build_auth_context_fragment(api_key_token, TokenInfo) ->
|
||||
UserID = tk_token_jwt:get_subject_id(TokenInfo),
|
||||
Acc0 = bouncer_context_helpers:empty(),
|
||||
Acc1 = bouncer_context_helpers:add_auth(
|
||||
#{
|
||||
method => <<"ApiKeyToken">>,
|
||||
token => #{id => tk_token_jwt:get_token_id(TokenInfo)},
|
||||
scope => [#{party => #{id => UserID}}]
|
||||
},
|
||||
Acc0
|
||||
),
|
||||
encode_context_fragment(Acc1);
|
||||
build_auth_context_fragment(user_session_token, TokenInfo) ->
|
||||
Metadata = tk_token_jwt:get_metadata(TokenInfo),
|
||||
UserID = tk_token_jwt:get_subject_id(TokenInfo),
|
||||
Expiration = tk_token_jwt:get_expires_at(TokenInfo),
|
||||
Acc0 = bouncer_context_helpers:empty(),
|
||||
Acc1 = bouncer_context_helpers:add_user(
|
||||
#{
|
||||
id => UserID,
|
||||
email => tk_token_jwt:get_subject_email(TokenInfo),
|
||||
realm => #{id => maps:get(user_realm, Metadata, undefined)}
|
||||
},
|
||||
Acc0
|
||||
),
|
||||
Acc2 = bouncer_context_helpers:add_auth(
|
||||
#{
|
||||
method => <<"SessionToken">>,
|
||||
expiration => make_auth_expiration(Expiration),
|
||||
token => #{id => tk_token_jwt:get_token_id(TokenInfo)}
|
||||
},
|
||||
Acc1
|
||||
),
|
||||
encode_context_fragment(Acc2).
|
||||
|
||||
make_auth_expiration(Timestamp) when is_integer(Timestamp) ->
|
||||
genlib_rfc3339:format(Timestamp, second);
|
||||
make_auth_expiration(unlimited) ->
|
||||
undefined.
|
||||
|
||||
%%
|
||||
|
||||
-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().
|
||||
-type claims() :: tk_token_jwt:claims().
|
||||
|
||||
-spec get_claim(claims()) ->
|
||||
{ok, encoded_context_fragment()} | {error, {unsupported, claim()} | {malformed, binary()}} | undefined.
|
||||
get_claim(Claims) ->
|
||||
case maps:get(?CLAIM_BOUNCER_CTX, Claims, undefined) of
|
||||
Claim when Claim /= undefined ->
|
||||
decode_claim(Claim);
|
||||
undefined ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
-spec decode_claim(claim()) ->
|
||||
{ok, encoded_context_fragment()} | {error, {unsupported, claim()} | {malformed, binary()}}.
|
||||
decode_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_claim(Ctx) ->
|
||||
{error, {unsupported, Ctx}}.
|
||||
|
||||
%%
|
||||
|
||||
encode_context_fragment(ContextFragment) ->
|
||||
#bctx_ContextFragment{
|
||||
type = v1_thrift_binary,
|
||||
content = encode_context_fragment_content(ContextFragment)
|
||||
}.
|
||||
|
||||
encode_context_fragment_content(ContextFragment) ->
|
||||
Type = {struct, struct, {bouncer_context_v1_thrift, 'ContextFragment'}},
|
||||
Codec = thrift_strict_binary_codec:new(),
|
||||
case thrift_strict_binary_codec:write(Codec, Type, ContextFragment) of
|
||||
{ok, Codec1} ->
|
||||
thrift_strict_binary_codec:close(Codec1)
|
||||
end.
|
51
src/tk_context_extractor.erl
Normal file
51
src/tk_context_extractor.erl
Normal file
@ -0,0 +1,51 @@
|
||||
-module(tk_context_extractor).
|
||||
|
||||
%% Behaviour
|
||||
|
||||
-callback get_context(tk_token_jwt:t(), extractor_opts()) -> extracted_context() | undefined.
|
||||
|
||||
%% API functions
|
||||
|
||||
-export([get_context/3]).
|
||||
|
||||
%% API Types
|
||||
|
||||
-type methods() :: [{method(), extractor_opts()} | method()].
|
||||
-type method() :: claim | detect_token | api_key_token | user_session_token.
|
||||
|
||||
-type extractor_opts() :: #{
|
||||
user_session_token_origins => list(binary()),
|
||||
user_realm => binary()
|
||||
}.
|
||||
|
||||
-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().
|
||||
|
||||
-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
|
||||
|
||||
-spec get_context(method(), tk_token_jwt:t(), extractor_opts()) -> extracted_context() | undefined.
|
||||
get_context(Method, Token, Opts) ->
|
||||
Hander = get_extractor_handler(Method),
|
||||
Hander:get_context(Token, Opts).
|
||||
|
||||
%%
|
||||
|
||||
get_extractor_handler(claim) ->
|
||||
tk_extractor_claim;
|
||||
get_extractor_handler(detect_token) ->
|
||||
tk_extractor_detect_token;
|
||||
get_extractor_handler(phony_api_key) ->
|
||||
tk_extractor_phony_api_key;
|
||||
get_extractor_handler(user_session_token) ->
|
||||
tk_extractor_user_session_token.
|
66
src/tk_extractor_claim.erl
Normal file
66
src/tk_extractor_claim.erl
Normal file
@ -0,0 +1,66 @@
|
||||
-module(tk_extractor_claim).
|
||||
-behaviour(tk_context_extractor).
|
||||
|
||||
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
|
||||
|
||||
-export([get_context/2]).
|
||||
|
||||
%% API functions
|
||||
|
||||
-spec get_context(tk_token_jwt:t(), tk_context_extractor: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.
|
||||
Claims = tk_token_jwt:get_claims(Token),
|
||||
case get_claim(Claims) of
|
||||
{ok, ClaimFragment} ->
|
||||
{ClaimFragment, undefined};
|
||||
undefined ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
%% Internal functions
|
||||
|
||||
-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().
|
||||
-type claims() :: tk_token_jwt:claims().
|
||||
|
||||
-spec get_claim(claims()) ->
|
||||
{ok, tk_context_extractor:context_fragment()}
|
||||
| {error, {unsupported, claim()} | {malformed, binary()}}
|
||||
| undefined.
|
||||
get_claim(Claims) ->
|
||||
case maps:get(?CLAIM_BOUNCER_CTX, Claims, undefined) of
|
||||
Claim when Claim /= undefined ->
|
||||
decode_claim(Claim);
|
||||
undefined ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
-spec decode_claim(claim()) ->
|
||||
{ok, tk_context_extractor:context_fragment()} | {error, {unsupported, claim()} | {malformed, binary()}}.
|
||||
decode_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_claim(Ctx) ->
|
||||
{error, {unsupported, Ctx}}.
|
39
src/tk_extractor_detect_token.erl
Normal file
39
src/tk_extractor_detect_token.erl
Normal file
@ -0,0 +1,39 @@
|
||||
-module(tk_extractor_detect_token).
|
||||
-behaviour(tk_context_extractor).
|
||||
|
||||
%% Behaviour
|
||||
|
||||
-export([get_context/2]).
|
||||
|
||||
%% API Types
|
||||
|
||||
-type token_source() :: #{
|
||||
request_origin => binary()
|
||||
}.
|
||||
|
||||
-export_type([token_source/0]).
|
||||
|
||||
%% Behaviour
|
||||
|
||||
-spec get_context(tk_token_jwt:t(), tk_context_extractor:extractor_opts()) ->
|
||||
tk_context_extractor:extracted_context() | undefined.
|
||||
get_context(Token, Opts = #{user_session_token_origins := UserTokenOrigins}) ->
|
||||
TokenSourceContext = tk_token_jwt:get_source_context(Token),
|
||||
Opts1 = maps:without([user_session_token_origins], Opts),
|
||||
tk_context_extractor:get_context(
|
||||
determine_token_type(TokenSourceContext, UserTokenOrigins),
|
||||
Token,
|
||||
Opts1
|
||||
).
|
||||
|
||||
%% Internal functions
|
||||
|
||||
determine_token_type(#{request_origin := Origin}, UserTokenOrigins) ->
|
||||
case lists:member(Origin, UserTokenOrigins) of
|
||||
true ->
|
||||
user_session_token;
|
||||
false ->
|
||||
phony_api_key
|
||||
end;
|
||||
determine_token_type(#{}, _UserTokenOrigins) ->
|
||||
phony_api_key.
|
20
src/tk_extractor_phony_api_key.erl
Normal file
20
src/tk_extractor_phony_api_key.erl
Normal file
@ -0,0 +1,20 @@
|
||||
-module(tk_extractor_phony_api_key).
|
||||
-behaviour(tk_context_extractor).
|
||||
|
||||
-export([get_context/2]).
|
||||
|
||||
%% API functions
|
||||
|
||||
-spec get_context(tk_token_jwt:t(), tk_context_extractor:extractor_opts()) -> tk_context_extractor:extracted_context().
|
||||
get_context(Token, _ExtractorOpts) ->
|
||||
UserID = tk_token_jwt:get_subject_id(Token),
|
||||
Acc0 = bouncer_context_helpers:empty(),
|
||||
Acc1 = bouncer_context_helpers:add_auth(
|
||||
#{
|
||||
method => <<"ApiKeyToken">>,
|
||||
token => #{id => tk_token_jwt:get_token_id(Token)},
|
||||
scope => [#{party => #{id => UserID}}]
|
||||
},
|
||||
Acc0
|
||||
),
|
||||
{Acc1, #{<<"party_id">> => UserID}}.
|
41
src/tk_extractor_user_session_token.erl
Normal file
41
src/tk_extractor_user_session_token.erl
Normal file
@ -0,0 +1,41 @@
|
||||
-module(tk_extractor_user_session_token).
|
||||
-behaviour(tk_context_extractor).
|
||||
|
||||
-export([get_context/2]).
|
||||
|
||||
%% API functions
|
||||
|
||||
-spec get_context(tk_token_jwt:t(), tk_context_extractor:extractor_opts()) -> tk_context_extractor:extracted_context().
|
||||
get_context(Token, ExtractorOpts) ->
|
||||
UserID = tk_token_jwt:get_subject_id(Token),
|
||||
Email = tk_token_jwt:get_subject_email(Token),
|
||||
Expiration = tk_token_jwt:get_expires_at(Token),
|
||||
Acc0 = bouncer_context_helpers:empty(),
|
||||
Acc1 = bouncer_context_helpers:add_user(
|
||||
#{
|
||||
id => UserID,
|
||||
email => Email,
|
||||
realm => #{id => maps:get(user_realm, ExtractorOpts, undefined)}
|
||||
},
|
||||
Acc0
|
||||
),
|
||||
Acc2 = bouncer_context_helpers:add_auth(
|
||||
#{
|
||||
method => <<"SessionToken">>,
|
||||
expiration => make_auth_expiration(Expiration),
|
||||
token => #{id => tk_token_jwt:get_token_id(Token)}
|
||||
},
|
||||
Acc1
|
||||
),
|
||||
{Acc2,
|
||||
genlib_map:compact(#{
|
||||
<<"user_id">> => UserID,
|
||||
<<"user_email">> => Email
|
||||
})}.
|
||||
|
||||
%% Internal functions
|
||||
|
||||
make_auth_expiration(Timestamp) when is_integer(Timestamp) ->
|
||||
genlib_rfc3339:format(Timestamp, second);
|
||||
make_auth_expiration(unlimited) ->
|
||||
undefined.
|
@ -35,18 +35,19 @@ do_handle_function('AddExistingToken', _, _State) ->
|
||||
erlang:error(not_implemented);
|
||||
do_handle_function('GetByToken' = Op, {Token, TokenSourceContext}, State) ->
|
||||
_ = handle_beat(Op, started, State),
|
||||
case tk_token_jwt:verify(Token) of
|
||||
{ok, TokenInfo} ->
|
||||
TokenSourceContextDecoded = decode_source_context(TokenSourceContext),
|
||||
State1 = save_pulse_metadata(#{token => TokenInfo, source => TokenSourceContextDecoded}, State),
|
||||
case extract_auth_data(TokenInfo, TokenSourceContextDecoded) of
|
||||
case tk_token_jwt:verify(Token, TokenSourceContextDecoded) of
|
||||
{ok, TokenInfo} ->
|
||||
State1 = save_pulse_metadata(#{token => TokenInfo}, State),
|
||||
Authority = get_token_authority(TokenInfo),
|
||||
case tk_authority:get_authdata_by_token(TokenInfo, Authority) of
|
||||
{ok, AuthDataPrototype} ->
|
||||
EncodedAuthData = encode_auth_data(AuthDataPrototype#{token => Token}),
|
||||
_ = handle_beat(Op, succeeded, State1),
|
||||
{ok, EncodedAuthData};
|
||||
{error, Reason} ->
|
||||
_ = handle_beat(Op, {failed, {context_creaton, Reason}}, State1),
|
||||
woody_error:raise(business, #token_keeper_ContextCreationFailed{})
|
||||
_ = handle_beat(Op, {failed, {not_found, Reason}}, State1),
|
||||
woody_error:raise(business, #token_keeper_AuthDataNotFound{})
|
||||
end;
|
||||
{error, Reason} ->
|
||||
_ = handle_beat(Op, {failed, {token_verification, Reason}}, State),
|
||||
@ -66,56 +67,12 @@ make_state(WoodyCtx, Opts) ->
|
||||
pulse_metadata = #{woody_ctx => WoodyCtx}
|
||||
}.
|
||||
|
||||
extract_auth_data(TokenInfo, TokenSourceContext) ->
|
||||
TokenType = determine_token_type(TokenSourceContext),
|
||||
case tk_bouncer_context:extract_context_fragment(TokenInfo, TokenType) of
|
||||
ContextFragment when ContextFragment =/= undefined ->
|
||||
AuthDataPrototype = genlib_map:compact(#{
|
||||
context => ContextFragment,
|
||||
metadata => extract_token_metadata(TokenType, TokenInfo),
|
||||
authority => get_authority(TokenInfo)
|
||||
}),
|
||||
{ok, AuthDataPrototype};
|
||||
undefined ->
|
||||
{error, unable_to_infer_auth_data}
|
||||
end.
|
||||
|
||||
determine_token_type(#{request_origin := Origin}) ->
|
||||
UserTokenOrigins = application:get_env(token_keeper, user_session_token_origins, []),
|
||||
case lists:member(Origin, UserTokenOrigins) of
|
||||
true ->
|
||||
user_session_token;
|
||||
false ->
|
||||
api_key_token
|
||||
end;
|
||||
determine_token_type(#{}) ->
|
||||
api_key_token.
|
||||
|
||||
get_authority(TokenInfo) ->
|
||||
Metadata = tk_token_jwt:get_metadata(TokenInfo),
|
||||
maps:get(authority, Metadata).
|
||||
|
||||
extract_token_metadata(api_key_token, TokenInfo) ->
|
||||
case tk_token_jwt:get_subject_id(TokenInfo) of
|
||||
PartyID when PartyID =/= undefined ->
|
||||
wrap_metadata(#{<<"party_id">> => PartyID}, TokenInfo);
|
||||
_ ->
|
||||
undefined
|
||||
end;
|
||||
extract_token_metadata(user_session_token, _TokenInfo) ->
|
||||
undefined.
|
||||
|
||||
wrap_metadata(Metadata, TokenInfo) ->
|
||||
TokenMetadata = tk_token_jwt:get_metadata(TokenInfo),
|
||||
MetadataNS = maps:get(metadata_ns, TokenMetadata),
|
||||
#{MetadataNS => Metadata}.
|
||||
|
||||
encode_auth_data(AuthData) ->
|
||||
#token_keeper_AuthData{
|
||||
id = maps:get(id, AuthData, undefined),
|
||||
token = maps:get(token, AuthData),
|
||||
%% Assume active?
|
||||
status = maps:get(status, AuthData, active),
|
||||
status = maps:get(status, AuthData),
|
||||
context = maps:get(context, AuthData),
|
||||
metadata = maps:get(metadata, AuthData, #{}),
|
||||
authority = maps:get(authority, AuthData)
|
||||
@ -128,6 +85,18 @@ decode_source_context(TokenSourceContext) ->
|
||||
|
||||
%%
|
||||
|
||||
get_token_authority(TokenInfo) ->
|
||||
AuthorityID = tk_token_jwt:get_authority(TokenInfo),
|
||||
Authorities = application:get_env(token_keeper, authorities, #{}),
|
||||
case maps:get(AuthorityID, Authorities, undefined) of
|
||||
Config when Config =/= undefined ->
|
||||
Config;
|
||||
undefined ->
|
||||
throw({misconfiguration, {no_such_authority, AuthorityID}})
|
||||
end.
|
||||
|
||||
%%
|
||||
|
||||
handle_beat(Op, Event, State) ->
|
||||
tk_pulse:handle_beat({encode_pulse_op(Op), Event}, State#state.pulse_metadata, State#state.pulse).
|
||||
|
||||
|
@ -8,7 +8,6 @@
|
||||
|
||||
-type metadata() :: #{
|
||||
token => tk_token_jwt:t(),
|
||||
source => token_keeper:token_source(),
|
||||
woody_ctx => woody_context:ctx()
|
||||
}.
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
%% API
|
||||
|
||||
-export([issue/3]).
|
||||
-export([verify/1]).
|
||||
-export([verify/2]).
|
||||
|
||||
-export([get_token_id/1]).
|
||||
-export([get_subject_id/1]).
|
||||
@ -15,7 +15,9 @@
|
||||
-export([get_claims/1]).
|
||||
-export([get_claim/2]).
|
||||
-export([get_claim/3]).
|
||||
-export([get_authority/1]).
|
||||
-export([get_metadata/1]).
|
||||
-export([get_source_context/1]).
|
||||
|
||||
-export([create_claims/2]).
|
||||
-export([set_subject_email/2]).
|
||||
@ -27,7 +29,7 @@
|
||||
|
||||
%% API types
|
||||
|
||||
-type t() :: {token_id(), claims(), metadata()}.
|
||||
-type t() :: {token_id(), claims(), authority(), metadata()}.
|
||||
-type claim() :: expiration() | term().
|
||||
-type claims() :: #{binary() => claim()}.
|
||||
-type token() :: binary().
|
||||
@ -38,14 +40,8 @@
|
||||
keyset => keyset()
|
||||
}.
|
||||
|
||||
%@TODO Separate classification parameters from jwt decoder logic
|
||||
-type auth_method() ::
|
||||
user_session_token | api_key_token | detect.
|
||||
-type metadata() :: #{
|
||||
authority := binary(),
|
||||
metadata_ns => binary(),
|
||||
auth_method => auth_method(),
|
||||
user_realm => realm()
|
||||
source_context => source_context()
|
||||
}.
|
||||
|
||||
-export_type([t/0]).
|
||||
@ -54,7 +50,6 @@
|
||||
-export_type([token/0]).
|
||||
-export_type([expiration/0]).
|
||||
-export_type([metadata/0]).
|
||||
-export_type([auth_method/0]).
|
||||
-export_type([options/0]).
|
||||
|
||||
%% Internal types
|
||||
@ -66,20 +61,23 @@
|
||||
-type subject_id() :: binary().
|
||||
-type token_id() :: binary().
|
||||
|
||||
-type authority() :: atom().
|
||||
|
||||
%??
|
||||
-type source_context() :: tk_extractor_detect_token:token_source().
|
||||
|
||||
-type keyset() :: #{
|
||||
keyname() => key_opts()
|
||||
}.
|
||||
|
||||
-type key_opts() :: #{
|
||||
source := keysource(),
|
||||
metadata => metadata()
|
||||
authority := authority()
|
||||
}.
|
||||
|
||||
-type keysource() ::
|
||||
{pem_file, file:filename()}.
|
||||
|
||||
-type realm() :: binary().
|
||||
|
||||
%%
|
||||
|
||||
-define(CLAIM_TOKEN_ID, <<"jti">>).
|
||||
@ -92,7 +90,7 @@
|
||||
%%
|
||||
|
||||
-spec get_token_id(t()) -> token_id().
|
||||
get_token_id({TokenId, _Claims, _Metadata}) ->
|
||||
get_token_id({TokenId, _Claims, _Authority, _Metadata}) ->
|
||||
TokenId.
|
||||
|
||||
-spec get_subject_id(t()) -> subject_id() | undefined.
|
||||
@ -104,28 +102,36 @@ get_subject_email(T) ->
|
||||
get_claim(?CLAIM_SUBJECT_EMAIL, T, undefined).
|
||||
|
||||
-spec get_expires_at(t()) -> expiration().
|
||||
get_expires_at({_TokenId, Claims, _Metadata}) ->
|
||||
get_expires_at({_TokenId, Claims, _Authority, _Metadata}) ->
|
||||
case maps:get(?CLAIM_EXPIRES_AT, Claims) of
|
||||
0 -> unlimited;
|
||||
V -> V
|
||||
end.
|
||||
|
||||
-spec get_claims(t()) -> claims().
|
||||
get_claims({_TokenId, Claims, _Metadata}) ->
|
||||
get_claims({_TokenId, Claims, _Authority, _Metadata}) ->
|
||||
Claims.
|
||||
|
||||
-spec get_claim(binary(), t()) -> claim().
|
||||
get_claim(ClaimName, {_TokenId, Claims, _Metadata}) ->
|
||||
get_claim(ClaimName, {_TokenId, Claims, _Authority, _Metadata}) ->
|
||||
maps:get(ClaimName, Claims).
|
||||
|
||||
-spec get_claim(binary(), t(), claim()) -> claim().
|
||||
get_claim(ClaimName, {_TokenId, Claims, _Metadata}, Default) ->
|
||||
get_claim(ClaimName, {_TokenId, Claims, _Authority, _Metadata}, Default) ->
|
||||
maps:get(ClaimName, Claims, Default).
|
||||
|
||||
-spec get_authority(t()) -> authority().
|
||||
get_authority({_TokenId, _Claims, Authority, _Metadata}) ->
|
||||
Authority.
|
||||
|
||||
-spec get_metadata(t()) -> metadata().
|
||||
get_metadata({_TokenId, _Claims, Metadata}) ->
|
||||
get_metadata({_TokenId, _Claims, _Authority, Metadata}) ->
|
||||
Metadata.
|
||||
|
||||
-spec get_source_context(t()) -> source_context().
|
||||
get_source_context({_TokenId, _Claims, _Authority, Metadata}) ->
|
||||
maps:get(source_context, Metadata).
|
||||
|
||||
-spec create_claims(claims(), expiration()) -> claims().
|
||||
create_claims(Claims, Expiration) ->
|
||||
Claims#{?CLAIM_EXPIRES_AT => Expiration}.
|
||||
@ -182,7 +188,7 @@ sign(#{kid := KID, jwk := JWK, signer := #{} = JWS}, Claims) ->
|
||||
|
||||
%%
|
||||
|
||||
-spec verify(token()) ->
|
||||
-spec verify(token(), source_context()) ->
|
||||
{ok, t()}
|
||||
| {error,
|
||||
{invalid_token,
|
||||
@ -193,14 +199,14 @@ sign(#{kid := KID, jwk := JWK, signer := #{} = JWS}, Claims) ->
|
||||
| {invalid_operation, term()}
|
||||
| invalid_signature}.
|
||||
|
||||
verify(Token) ->
|
||||
verify(Token, SourceContext) ->
|
||||
try
|
||||
{_, ExpandedToken} = jose_jws:expand(Token),
|
||||
#{<<"protected">> := ProtectedHeader} = ExpandedToken,
|
||||
Header = base64url_to_map(ProtectedHeader),
|
||||
Alg = get_alg(Header),
|
||||
KID = get_kid(Header),
|
||||
verify(KID, Alg, ExpandedToken)
|
||||
verify(KID, Alg, ExpandedToken, SourceContext)
|
||||
catch
|
||||
%% from get_alg and get_kid
|
||||
throw:Reason ->
|
||||
@ -214,20 +220,23 @@ base64url_to_map(Base64) when is_binary(Base64) ->
|
||||
{ok, Json} = jose_base64url:decode(Base64),
|
||||
jsx:decode(Json, [return_maps]).
|
||||
|
||||
verify(KID, Alg, ExpandedToken) ->
|
||||
verify(KID, Alg, ExpandedToken, SourceContext) ->
|
||||
case get_key_by_kid(KID) of
|
||||
#{jwk := JWK, verifier := Algs, metadata := Metadata} ->
|
||||
#{jwk := JWK, verifier := Algs, authority := Authority} ->
|
||||
_ = lists:member(Alg, Algs) orelse throw({invalid_operation, Alg}),
|
||||
verify_with_key(JWK, ExpandedToken, Metadata);
|
||||
verify_with_key(JWK, ExpandedToken, Authority, make_metadata(SourceContext));
|
||||
undefined ->
|
||||
{error, {nonexistent_key, KID}}
|
||||
end.
|
||||
|
||||
verify_with_key(JWK, ExpandedToken, Metadata) ->
|
||||
make_metadata(SourceContext) ->
|
||||
#{source_context => 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, Metadata);
|
||||
get_result(Claims, Authority, Metadata);
|
||||
{false, _JWT, _JWS} ->
|
||||
{error, invalid_signature}
|
||||
end.
|
||||
@ -241,8 +250,8 @@ validate_claims(Claims, [{Name, Claim, Validator} | Rest]) ->
|
||||
validate_claims(Claims, []) ->
|
||||
Claims.
|
||||
|
||||
get_result(#{?CLAIM_TOKEN_ID := TokenID} = Claims, Metadata) ->
|
||||
{ok, {TokenID, maps:without([?CLAIM_TOKEN_ID], Claims), Metadata}}.
|
||||
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;
|
||||
@ -284,26 +293,13 @@ parse_options(Options) ->
|
||||
_ = is_map(Keyset) orelse exit({invalid_option, keyset, Keyset}),
|
||||
_ = genlib_map:foreach(
|
||||
fun(KeyName, KeyOpts = #{source := Source}) ->
|
||||
Metadata = maps:get(metadata, KeyOpts),
|
||||
Authority = maps:get(authority, Metadata),
|
||||
AuthMethod = maps:get(auth_method, Metadata, undefined),
|
||||
UserRealm = maps:get(user_realm, Metadata, <<>>),
|
||||
MetadataNS = maps:get(metadata_ns, Metadata, <<>>),
|
||||
Authority = maps:get(authority, KeyOpts),
|
||||
_ =
|
||||
is_keysource(Source) orelse
|
||||
exit({invalid_source, KeyName, Source}),
|
||||
_ =
|
||||
is_auth_method(AuthMethod) orelse
|
||||
exit({invalid_auth_method, KeyName, AuthMethod}),
|
||||
_ =
|
||||
is_binary(UserRealm) orelse
|
||||
exit({invalid_user_realm, KeyName, AuthMethod}),
|
||||
_ =
|
||||
is_binary(Authority) orelse
|
||||
exit({invalid_authority, KeyName, AuthMethod}),
|
||||
_ =
|
||||
is_binary(MetadataNS) orelse
|
||||
exit({invalid_metadata_ns, KeyName, MetadataNS})
|
||||
is_atom(Authority) orelse
|
||||
exit({invalid_authority, KeyName, Authority})
|
||||
end,
|
||||
Keyset
|
||||
),
|
||||
@ -314,17 +310,6 @@ is_keysource({pem_file, Fn}) ->
|
||||
is_keysource(_) ->
|
||||
false.
|
||||
|
||||
is_auth_method(user_session_token) ->
|
||||
true;
|
||||
is_auth_method(api_key_token) ->
|
||||
true;
|
||||
is_auth_method(detect) ->
|
||||
true;
|
||||
is_auth_method(undefined) ->
|
||||
true;
|
||||
is_auth_method(_) ->
|
||||
false.
|
||||
|
||||
%%
|
||||
|
||||
-spec init(keyset()) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
|
||||
@ -335,17 +320,17 @@ init(Keyset) ->
|
||||
|
||||
ensure_store_key(KeyName, KeyOpts) ->
|
||||
Source = maps:get(source, KeyOpts),
|
||||
Metadata = maps:get(metadata, KeyOpts, #{}),
|
||||
case store_key(KeyName, Source, Metadata) of
|
||||
Authority = maps:get(authority, KeyOpts),
|
||||
case store_key(KeyName, Source, Authority) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
exit({import_error, KeyName, Source, Reason})
|
||||
end.
|
||||
|
||||
-spec store_key(keyname(), {pem_file, file:filename()}, metadata()) -> ok | {error, file:posix() | {unknown_key, _}}.
|
||||
store_key(Keyname, {pem_file, Filename}, Metadata) ->
|
||||
store_key(Keyname, {pem_file, Filename}, Metadata, #{
|
||||
-spec store_key(keyname(), {pem_file, file:filename()}, authority()) -> ok | {error, file:posix() | {unknown_key, _}}.
|
||||
store_key(Keyname, {pem_file, Filename}, Authority) ->
|
||||
store_key(Keyname, {pem_file, Filename}, Authority, #{
|
||||
kid => fun derive_kid_from_public_key_pem_entry/1
|
||||
}).
|
||||
|
||||
@ -359,13 +344,13 @@ derive_kid_from_public_key_pem_entry(JWK) ->
|
||||
kid => fun((key()) -> kid())
|
||||
}.
|
||||
|
||||
-spec store_key(keyname(), {pem_file, file:filename()}, metadata(), store_opts()) ->
|
||||
-spec store_key(keyname(), {pem_file, file:filename()}, authority(), store_opts()) ->
|
||||
ok | {error, file:posix() | {unknown_key, _}}.
|
||||
store_key(Keyname, {pem_file, Filename}, Metadata, Opts) ->
|
||||
store_key(Keyname, {pem_file, Filename}, Authority, Opts) ->
|
||||
case jose_jwk:from_pem_file(Filename) of
|
||||
JWK = #jose_jwk{} ->
|
||||
Key = construct_key(derive_kid(JWK, Opts), JWK),
|
||||
ok = insert_key(Keyname, Key#{metadata => Metadata});
|
||||
ok = insert_key(Keyname, Key#{authority => Authority});
|
||||
Error = {error, _} ->
|
||||
Error
|
||||
end.
|
||||
|
@ -76,12 +76,12 @@ init([]) ->
|
||||
additional_routes => [erl_health_handle:get_route(Healthcheck)]
|
||||
}
|
||||
),
|
||||
TokensOpts = genlib_app:env(?MODULE, tokens, #{}),
|
||||
TokensChildSpecs = get_tokens_specs(TokensOpts),
|
||||
TokensOpts = genlib_app:env(?MODULE, jwt, #{}),
|
||||
TokensChildSpec = tk_token_jwt:child_spec(TokensOpts),
|
||||
{ok,
|
||||
{
|
||||
#{strategy => one_for_all, intensity => 6, period => 30},
|
||||
[HandlerChildSpec | TokensChildSpecs]
|
||||
[HandlerChildSpec, TokensChildSpec]
|
||||
}}.
|
||||
|
||||
-spec get_ip_address() -> inet:ip_address().
|
||||
@ -133,17 +133,3 @@ enable_health_logging(Check) ->
|
||||
fun(_, Runner) -> #{runner => Runner, event_handler => EvHandler} end,
|
||||
Check
|
||||
).
|
||||
|
||||
%%
|
||||
|
||||
get_tokens_specs(TokensOpts) ->
|
||||
maps:fold(
|
||||
fun(K, V, Acc) ->
|
||||
[get_token_spec(K, V) | Acc]
|
||||
end,
|
||||
[],
|
||||
TokensOpts
|
||||
).
|
||||
|
||||
get_token_spec(jwt, JWTOptions) ->
|
||||
tk_token_jwt:child_spec(JWTOptions).
|
||||
|
@ -21,7 +21,7 @@
|
||||
-export([detect_api_key_test/1]).
|
||||
-export([detect_user_session_token_test/1]).
|
||||
-export([detect_dummy_token_test/1]).
|
||||
-export([no_token_metadata_test/1]).
|
||||
-export([no_token_claim_test/1]).
|
||||
-export([bouncer_context_from_claims_test/1]).
|
||||
|
||||
-type config() :: ct_helper:config().
|
||||
@ -33,7 +33,11 @@
|
||||
-define(TK_AUTHORITY_TOKEN_KEEPER, <<"com.rbkmoney.token-keeper">>).
|
||||
-define(TK_AUTHORITY_KEYCLOAK, <<"com.rbkmoney.keycloak">>).
|
||||
|
||||
-define(PARTY_METADATA(Authority, SubjectID), #{Authority := #{<<"party_id">> := SubjectID}}).
|
||||
-define(METADATA(Authority, Metadata), #{Authority := Metadata}).
|
||||
-define(PARTY_METADATA(Authority, SubjectID), ?METADATA(Authority, #{<<"party_id">> := SubjectID})).
|
||||
-define(USER_METADATA(Authority, SubjectID, Email),
|
||||
?METADATA(Authority, #{<<"user_id">> := SubjectID, <<"user_email">> := Email})
|
||||
).
|
||||
|
||||
-define(TOKEN_SOURCE_CONTEXT(), ?TOKEN_SOURCE_CONTEXT(<<"http://spanish.inquisition">>)).
|
||||
-define(TOKEN_SOURCE_CONTEXT(SourceURL), #token_keeper_TokenSourceContext{request_origin = SourceURL}).
|
||||
@ -49,7 +53,7 @@
|
||||
all() ->
|
||||
[
|
||||
{group, detect_token_type},
|
||||
{group, no_token_metadata}
|
||||
{group, claim_only}
|
||||
].
|
||||
|
||||
-spec groups() -> [{group_name(), list(), [test_case_name()]}].
|
||||
@ -60,8 +64,8 @@ groups() ->
|
||||
detect_user_session_token_test,
|
||||
detect_dummy_token_test
|
||||
]},
|
||||
{no_token_metadata, [parallel], [
|
||||
no_token_metadata_test,
|
||||
{claim_only, [parallel], [
|
||||
no_token_claim_test,
|
||||
bouncer_context_from_claims_test
|
||||
]}
|
||||
].
|
||||
@ -83,37 +87,52 @@ end_per_suite(C) ->
|
||||
-spec init_per_group(group_name(), config()) -> config().
|
||||
init_per_group(detect_token_type = Name, C) ->
|
||||
start_keeper([
|
||||
{tokens, #{
|
||||
jwt => #{
|
||||
{jwt, #{
|
||||
keyset => #{
|
||||
test => #{
|
||||
source => {pem_file, get_keysource("keys/local/private.pem", C)},
|
||||
metadata => #{
|
||||
authority => ?TK_AUTHORITY_KEYCLOAK,
|
||||
metadata_ns => ?TK_AUTHORITY_TOKEN_KEEPER,
|
||||
auth_method => detect,
|
||||
user_realm => <<"external">>
|
||||
}
|
||||
}
|
||||
authority => keycloak
|
||||
}
|
||||
}
|
||||
}},
|
||||
{user_session_token_origins, [?USER_TOKEN_SOURCE]}
|
||||
{authorities, #{
|
||||
keycloak => #{
|
||||
id => ?TK_AUTHORITY_KEYCLOAK,
|
||||
authdata_sources => [
|
||||
storage,
|
||||
{extract, #{
|
||||
methods => [
|
||||
claim,
|
||||
{detect_token, #{
|
||||
user_session_token_origins => [?USER_TOKEN_SOURCE],
|
||||
user_realm => <<"external">>
|
||||
}}
|
||||
],
|
||||
metadata_ns => ?TK_AUTHORITY_TOKEN_KEEPER
|
||||
}}
|
||||
]
|
||||
}
|
||||
}}
|
||||
]) ++
|
||||
[{groupname, Name} | C];
|
||||
init_per_group(no_token_metadata = Name, C) ->
|
||||
init_per_group(claim_only = Name, C) ->
|
||||
start_keeper([
|
||||
{tokens, #{
|
||||
jwt => #{
|
||||
{jwt, #{
|
||||
keyset => #{
|
||||
test => #{
|
||||
source => {pem_file, get_keysource("keys/local/private.pem", C)},
|
||||
metadata => #{
|
||||
authority => ?TK_AUTHORITY_KEYCLOAK,
|
||||
metadata_ns => ?TK_AUTHORITY_TOKEN_KEEPER
|
||||
}
|
||||
authority => claim_only
|
||||
}
|
||||
}
|
||||
}},
|
||||
{authorities, #{
|
||||
claim_only => #{
|
||||
id => ?TK_AUTHORITY_KEYCLOAK,
|
||||
authdata_sources => [
|
||||
{extract, #{
|
||||
methods => [claim]
|
||||
}}
|
||||
]
|
||||
}
|
||||
}}
|
||||
]) ++
|
||||
@ -124,7 +143,7 @@ init_per_group(Name, C) ->
|
||||
-spec end_per_group(group_name(), config()) -> _.
|
||||
end_per_group(GroupName, C) when
|
||||
GroupName =:= detect_token_type;
|
||||
GroupName =:= no_token_metadata
|
||||
GroupName =:= claim_only
|
||||
->
|
||||
ok = stop_keeper(C),
|
||||
ok;
|
||||
@ -201,7 +220,10 @@ detect_user_session_token_test(C) ->
|
||||
AuthData#token_keeper_AuthData.context
|
||||
)
|
||||
),
|
||||
?assertMatch(#{}, AuthData#token_keeper_AuthData.metadata),
|
||||
?assertMatch(
|
||||
?USER_METADATA(?TK_AUTHORITY_TOKEN_KEEPER, SubjectID, SubjectEmail),
|
||||
AuthData#token_keeper_AuthData.metadata
|
||||
),
|
||||
?assertEqual(?TK_AUTHORITY_KEYCLOAK, AuthData#token_keeper_AuthData.authority).
|
||||
|
||||
-spec detect_dummy_token_test(config()) -> ok.
|
||||
@ -211,13 +233,13 @@ detect_dummy_token_test(C) ->
|
||||
#token_keeper_InvalidToken{} =
|
||||
(catch call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(), Client)).
|
||||
|
||||
-spec no_token_metadata_test(config()) -> ok.
|
||||
no_token_metadata_test(C) ->
|
||||
-spec no_token_claim_test(config()) -> ok.
|
||||
no_token_claim_test(C) ->
|
||||
Client = mk_client(C),
|
||||
JTI = unique_id(),
|
||||
SubjectID = <<"TEST">>,
|
||||
{ok, Token} = issue_token(JTI, #{<<"sub">> => SubjectID}, unlimited),
|
||||
#token_keeper_ContextCreationFailed{} =
|
||||
#token_keeper_AuthDataNotFound{} =
|
||||
(catch call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(), Client)).
|
||||
|
||||
-spec bouncer_context_from_claims_test(config()) -> ok.
|
||||
@ -230,8 +252,8 @@ bouncer_context_from_claims_test(C) ->
|
||||
?assertEqual(undefined, AuthData#token_keeper_AuthData.id),
|
||||
?assertEqual(Token, AuthData#token_keeper_AuthData.token),
|
||||
?assertEqual(active, AuthData#token_keeper_AuthData.status),
|
||||
?assert(assert_context({api_key_token, JTI, SubjectID}, AuthData#token_keeper_AuthData.context)),
|
||||
?assertMatch(?PARTY_METADATA(?TK_AUTHORITY_TOKEN_KEEPER, SubjectID), AuthData#token_keeper_AuthData.metadata),
|
||||
?assert(assert_context({claim_token, JTI}, AuthData#token_keeper_AuthData.context)),
|
||||
?assertEqual(#{}, AuthData#token_keeper_AuthData.metadata),
|
||||
?assertEqual(?TK_AUTHORITY_KEYCLOAK, AuthData#token_keeper_AuthData.authority).
|
||||
|
||||
%%
|
||||
@ -271,6 +293,10 @@ assert_context(TokenInfo, EncodedContextFragment) ->
|
||||
?assert(assert_user(TokenInfo, User)),
|
||||
true.
|
||||
|
||||
assert_auth({claim_token, JTI}, Auth) ->
|
||||
?assertEqual(<<"ClaimToken">>, Auth#bctx_v1_Auth.method),
|
||||
?assertMatch(#bctx_v1_Token{id = JTI}, Auth#bctx_v1_Auth.token),
|
||||
true;
|
||||
assert_auth({api_key_token, JTI, SubjectID}, Auth) ->
|
||||
?assertEqual(<<"ApiKeyToken">>, Auth#bctx_v1_Auth.method),
|
||||
?assertMatch(#bctx_v1_Token{id = JTI}, Auth#bctx_v1_Auth.token),
|
||||
@ -282,6 +308,8 @@ assert_auth({user_session_token, JTI, _SubjectID, _SubjectEmail, Exp}, Auth) ->
|
||||
?assertEqual(make_auth_expiration(Exp), Auth#bctx_v1_Auth.expiration),
|
||||
true.
|
||||
|
||||
assert_user({claim_token, _}, undefined) ->
|
||||
true;
|
||||
assert_user({api_key_token, _, _}, undefined) ->
|
||||
true;
|
||||
assert_user({user_session_token, _JTI, SubjectID, SubjectEmail, _Exp}, User) ->
|
||||
@ -307,9 +335,8 @@ issue_token_with_context(JTI, SubjectID) ->
|
||||
Acc0 = bouncer_context_helpers:empty(),
|
||||
Acc1 = bouncer_context_helpers:add_auth(
|
||||
#{
|
||||
method => <<"ApiKeyToken">>,
|
||||
token => #{id => JTI},
|
||||
scope => [#{party => #{id => SubjectID}}]
|
||||
method => <<"ClaimToken">>,
|
||||
token => #{id => JTI}
|
||||
},
|
||||
Acc0
|
||||
),
|
||||
|
@ -42,17 +42,11 @@ init_per_suite(Config) ->
|
||||
path => <<"/v1/token-keeper">>
|
||||
}
|
||||
}},
|
||||
{tokens, #{
|
||||
jwt => #{
|
||||
{jwt, #{
|
||||
keyset => #{
|
||||
test => #{
|
||||
source => {pem_file, get_keysource("keys/local/private.pem", Config)},
|
||||
metadata => #{
|
||||
authority => <<"TEST">>,
|
||||
auth_method => user_session_token,
|
||||
user_realm => <<"external">>
|
||||
}
|
||||
}
|
||||
authority => test
|
||||
}
|
||||
}
|
||||
}}
|
||||
@ -71,12 +65,12 @@ verify_test(_) ->
|
||||
JTI = unique_id(),
|
||||
PartyID = <<"TEST">>,
|
||||
{ok, Token} = issue_token(JTI, #{<<"sub">> => PartyID, <<"TEST">> => <<"TEST">>}, unlimited),
|
||||
{ok, {JTI, #{<<"sub">> := PartyID, <<"TEST">> := <<"TEST">>}, #{}}} = tk_token_jwt:verify(Token).
|
||||
{ok, {JTI, #{<<"sub">> := PartyID, <<"TEST">> := <<"TEST">>}, test, #{}}} = tk_token_jwt:verify(Token, #{}).
|
||||
|
||||
-spec bad_token_test(config()) -> _.
|
||||
bad_token_test(Config) ->
|
||||
{ok, Token} = issue_dummy_token(Config),
|
||||
{error, invalid_signature} = tk_token_jwt:verify(Token).
|
||||
{error, invalid_signature} = tk_token_jwt:verify(Token, #{}).
|
||||
|
||||
-spec bad_signee_test(config()) -> _.
|
||||
bad_signee_test(_) ->
|
||||
|
Loading…
Reference in New Issue
Block a user