Packages
oidcc
3.1.0
3.7.2
3.7.1
3.7.0
3.6.0
3.5.2
3.5.1
3.5.0
retired
3.4.0
3.3.0
3.2.6
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.2.0-beta.3
3.2.0-beta.2
retired
3.2.0-beta.1
retired
3.1.2
3.1.2-beta.1
retired
3.1.1
retired
3.1.0
retired
3.1.0-beta.2
retired
3.1.0-beta.1
retired
3.0.2
3.0.1
retired
3.0.0
retired
3.0.0-rc.6
retired
3.0.0-rc.5
retired
3.0.0-rc.4
retired
3.0.0-rc.3
retired
3.0.0-rc.2
retired
3.0.0-rc.1
retired
3.0.0-alpha.5
retired
3.0.0-alpha.4
retired
3.0.0-alpha.3
retired
3.0.0-alpha.2
retired
3.0.0-alpha.1
retired
2.0.0-alpha.2
retired
2.0.0-alpha.1
retired
1.8.1
retired
1.8.0
retired
1.7.0
retired
1.6.0
retired
1.5.1
retired
1.5.0
retired
1.4.0
retired
1.3.0
retired
1.2.1
retired
1.2.0
retired
1.1.0
retired
1.0.1
retired
1.0.0
retired
OpenID Connect client library for the BEAM.
Retired package: Security issue - DoS Vulnerability, see https://github.com/erlef/oidcc/security/advisories/GHSA-mj35-2rgf-cv8p
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
src/oidcc_provider_configuration_worker.erl
%%%-------------------------------------------------------------------
%% @doc OIDC Config Provider Worker
%%
%% Loads and continuously refreshes the OIDC configuration and JWKs
%%
%% The worker supports reading values concurrently via an ets table. To use
%% this performance improvement, the worker has to be registered with a
%% `{local, Name}'. No name / `{global, Name}' and `{via, RegModule, ViaName}'
%% are not supported.
%% @end
%% @since 3.0.0
%%%-------------------------------------------------------------------
-module(oidcc_provider_configuration_worker).
-feature(maybe_expr, enable).
-behaviour(gen_server).
-include("oidcc_provider_configuration.hrl").
-include_lib("jose/include/jose_jwk.hrl").
-export([get_jwks/1]).
-export([get_provider_configuration/1]).
-export([handle_call/3]).
-export([handle_cast/2]).
-export([handle_continue/2]).
-export([handle_info/2]).
-export([init/1]).
-export([refresh_configuration/1]).
-export([refresh_jwks/1]).
-export([refresh_jwks_for_unknown_kid/2]).
-export([start_link/1]).
-export_type([opts/0]).
%% Configuration Options
%%
%% <ul>
%% <li>`name' - The gen_server name of the provider.</li>
%% <li>`issuer' - The issuer URI.</li>
%% <li>`provider_configuration_opts' - Options for the provider configuration fetching.</li>
%% </ul>
-type opts() :: #{
name => gen_server:server_name(),
issuer := uri_string:uri_string(),
provider_configuration_opts => oidcc_provider_configuration:opts()
}.
-record(state, {
provider_configuration = undefined :: #oidcc_provider_configuration{} | undefined,
jwks = undefined :: jose_jwk:key() | undefined,
issuer :: uri_string:uri_string(),
provider_configuration_opts :: oidcc_provider_configuration:opts(),
configuration_refresh_timer = undefined :: timer:tref() | undefined,
jwks_refresh_timer = undefined :: timer:tref() | undefined,
ets_table = undefined :: ets:table() | undefined
}).
%% @doc Start Configuration Provider
%%
%% <h2>Examples</h2>
%%
%% ```
%% {ok, Pid} =
%% oidcc_provider_configuration_worker:start_link(#{
%% issuer => <<"https://accounts.google.com">>,
%% name => {local, google_config_provider}
%% }).
%% '''
%%
%% ```
%% %% ...
%%
%% -behaviour(supervisor).
%%
%% %% ...
%%
%% init(_opts) ->
%% SupFlags = #{strategy => one_for_one, intensity => 1, period => 5},
%% ChildSpecs = [#{id => google_config_provider,
%% start => {oidcc_provider_configuration_worker,
%% start_link,
%% [
%% #{issuer => <<"https://accounts.google.com">>}
%% ]},
%% restart => permanent,
%% type => worker,
%% modules => [oidcc_provider_configuration_worker]}],
%% {ok, {SupFlags, ChildSpecs}}.
%% '''
%% @end
%% @since 3.0.0
-spec start_link(Opts :: opts()) -> gen_server:start_ret().
start_link(Opts) ->
case maps:get(name, Opts, undefined) of
undefined ->
gen_server:start_link(?MODULE, Opts, []);
Name ->
gen_server:start_link(Name, ?MODULE, Opts, [])
end.
%% @private
init(Opts) ->
EtsTable = register_ets_table(Opts),
maybe
{ok, Issuer} ?= get_issuer(Opts),
ProviderConfigurationOpts = maps:get(provider_configuration_opts, Opts, #{}),
{ok,
#state{
issuer = Issuer,
provider_configuration_opts = ProviderConfigurationOpts,
ets_table = EtsTable
},
{continue, load_configuration}}
end.
%% @private
handle_call(
get_provider_configuration, _From, #state{provider_configuration = Configuration} = State
) ->
{reply, Configuration, State};
handle_call(get_jwks, _From, #state{jwks = Jwks} = State) ->
{reply, Jwks, State}.
%% @private
handle_cast(refresh_configuration, State) ->
{noreply, State, {continue, load_configuration}};
handle_cast(refresh_jwks, State) ->
{noreply, State, {continue, load_jwks}};
handle_cast({refresh_jwks_for_unknown_kid, Kid}, #state{jwks = Jwks} = State) ->
case has_kid(Jwks, Kid) of
false ->
{noreply, State, {continue, load_jwks}};
true ->
{noreply, State};
unknown ->
{noreply, State}
end.
%% @private
handle_continue(
load_configuration,
#state{
issuer = Issuer,
provider_configuration_opts = ProviderConfigurationOpts,
configuration_refresh_timer = OldTimer,
ets_table = EtsTable
} =
State
) ->
maybe_cancel_timer(OldTimer),
maybe
{ok, {Configuration, Expiry}} ?=
oidcc_provider_configuration:load_configuration(
Issuer,
ProviderConfigurationOpts
),
{ok, NewTimer} = timer:send_after(Expiry, configuration_expired),
ok = store_in_ets(EtsTable, provider_configuration, Configuration),
{noreply,
State#state{
provider_configuration = Configuration, configuration_refresh_timer = NewTimer
},
{continue, load_jwks}}
else
{error, Reason} ->
{stop, {configuration_load_failed, Reason}, State}
end;
handle_continue(
load_jwks,
#state{
provider_configuration = Configuration,
provider_configuration_opts = ProviderConfigurationOpts,
jwks_refresh_timer = OldTimer,
ets_table = EtsTable
} =
State
) ->
#oidcc_provider_configuration{jwks_uri = JwksUri} = Configuration,
maybe_cancel_timer(OldTimer),
maybe
{ok, {Jwks, Expiry}} ?=
oidcc_provider_configuration:load_jwks(JwksUri, ProviderConfigurationOpts),
{ok, NewTimer} = timer:send_after(Expiry, jwks_expired),
ok = store_in_ets(EtsTable, jwks, Jwks),
{noreply, State#state{jwks = Jwks, jwks_refresh_timer = NewTimer}}
else
{error, Reason} ->
{stop, {jwks_load_failed, Reason}, State}
end.
%% @private
handle_info(configuration_expired, State) ->
{noreply, State#state{configuration_refresh_timer = undefined, jwks_refresh_timer = undefined},
{continue, load_configuration}};
handle_info(jwks_expired, State) ->
{noreply, State#state{jwks_refresh_timer = undefined}, {continue, load_jwks}}.
%% @doc Get Configuration
-spec get_provider_configuration(Name :: gen_server:server_ref()) ->
oidcc_provider_configuration:t().
get_provider_configuration(Name) ->
lookup_in_ets_or_call(Name, provider_configuration, get_provider_configuration).
%% @doc Get Parsed Jwks
-spec get_jwks(Name :: gen_server:server_ref()) -> jose_jwk:key().
get_jwks(Name) ->
lookup_in_ets_or_call(Name, jwks, get_jwks).
%% @doc Refresh Configuration
%%
%% <h2>Examples</h2>
%%
%% ```
%% {ok, Pid} =
%% oidcc_provider_configuration_worker:start_link(#{
%% issuer => <<"https://accounts.google.com">>
%% }).
%%
%% %% Later
%%
%% oidcc_provider_configuration_worker:refresh_configuration(Pid).
%% '''
%% @end
%% @since 3.0.0
-spec refresh_configuration(Name :: gen_server:server_ref()) -> ok.
refresh_configuration(Name) ->
refresh_configuration(Name, true).
-spec refresh_configuration(Name :: gen_server:server_ref(), Synchronous :: boolean()) -> ok.
refresh_configuration(Name, false) ->
gen_server:cast(Name, refresh_configuration);
refresh_configuration(Name, true) ->
refresh_configuration(Name, false),
gen_server:call(Name, get_provider_configuration),
ok.
%% @doc Refresh JWKs
%%
%% <h2>Examples</h2>
%%
%% ```
%% {ok, Pid} =
%% oidcc_provider_configuration_worker:start_link(#{
%% issuer => <<"https://accounts.google.com">>
%% }).
%%
%% %% Later
%%
%% oidcc_provider_configuration_worker:refresh_jwks(Pid).
%% '''
%% @end
%% @since 3.0.0
-spec refresh_jwks(Name :: gen_server:server_ref()) -> ok.
refresh_jwks(Name) -> refresh_jwks(Name, true).
-spec refresh_jwks(Name :: gen_server:server_ref(), Synchronous :: boolean()) -> ok.
refresh_jwks(Name, false) ->
gen_server:cast(Name, refresh_jwks);
refresh_jwks(Name, true) ->
refresh_jwks(Name, false),
gen_server:call(Name, get_jwks),
ok.
%% @doc Refresh JWKs if the provided `Kid' is not matching any currently loaded keys
%%
%% <h2>Examples</h2>
%%
%% ```
%% {ok, Pid} =
%% oidcc_provider_configuration_worker:start_link(#{
%% issuer => <<"https://accounts.google.com">>
%% }).
%%
%% oidcc_provider_configuration_worker:refresh_jwks_for_unknown_kid(Pid, <<"kid">>).
%% '''
%% @end
%% @since 3.0.0
-spec refresh_jwks_for_unknown_kid(Name :: gen_server:server_ref(), Kid :: binary()) ->
ok.
refresh_jwks_for_unknown_kid(Name, Kid) ->
refresh_jwks_for_unknown_kid(Name, Kid, true).
-spec refresh_jwks_for_unknown_kid(
Name :: gen_server:server_ref(), Kid :: binary(), Synchronous :: boolean()
) ->
ok.
refresh_jwks_for_unknown_kid(Name, Kid, false) ->
gen_server:cast(Name, {refresh_jwks_for_unknown_kid, Kid});
refresh_jwks_for_unknown_kid(Name, Kid, true) ->
refresh_jwks_for_unknown_kid(Name, Kid, false),
gen_server:call(Name, get_jwks),
ok.
-spec get_issuer(Opts :: opts()) -> {ok, binary()} | {error, issuer_required}.
get_issuer(Opts) ->
case maps:get(issuer, Opts, undefined) of
undefined ->
{error, issuer_required};
Issuer when erlang:is_binary(Issuer) ->
{ok, Issuer}
end.
%% Checking of existing kid values is a bit wonky because of partial support
%% in jose. see: https://github.com/potatosalad/erlang-jose/issues/28
-spec has_kid(Jwk :: jose_jwk:key(), Kid :: binary()) -> boolean() | unknown.
has_kid(#jose_jwk{fields = #{<<"kid">> := Kid}}, Kid) ->
true;
has_kid(#jose_jwk{fields = #{<<"kid">> := _}}, _Kid) ->
false;
has_kid(#jose_jwk{keys = {jose_jwk_set, Keys}}, Kid) ->
lists:foldl(
fun
(_Key, Acc) when is_boolean(Acc) ->
Acc;
(Key, unknown) ->
has_kid(Key, Kid)
end,
unknown,
Keys
).
-spec maybe_cancel_timer(Timer :: undefined | timer:tref()) -> ok.
maybe_cancel_timer(undefined) ->
ok;
maybe_cancel_timer(TRef) ->
{ok, cancel} = timer:cancel(TRef).
-spec store_in_ets(Table :: ets:table() | undefined, Key :: atom(), Value :: term()) -> ok.
store_in_ets(undefined, _Key, _Value) ->
ok;
store_in_ets(Table, Key, Value) ->
true = ets:insert(Table, [{Key, Value}]),
ok.
-spec lookup_in_ets_or_call(Name :: gen_server:server_ref(), Key :: atom(), Call :: term()) ->
term().
lookup_in_ets_or_call(Name, Key, Call) ->
maybe
{ok, TableName} ?= get_ets_table_name(Name),
[{Key, Value}] ?= ets:lookup(TableName, Key),
Value
else
%% Fall Back to synchronous gen_server lookup if ets table can't be
%% located or the value is not present yet
_ -> gen_server:call(Name, Call)
end.
-spec get_ets_table_name(WorkerRef :: gen_server:server_ref()) ->
{ok, gen_server:server_ref()} | error.
get_ets_table_name(WorkerName) when is_atom(WorkerName) ->
{ok, erlang:list_to_atom(erlang:atom_to_list(WorkerName) ++ "_table")};
get_ets_table_name(_Ref) ->
error.
-spec register_ets_table(Opts :: opts()) -> ets:table() | undefined.
register_ets_table(Opts) ->
case maps:get(name, Opts, undefined) of
{local, WorkerName} ->
Name = erlang:list_to_atom(erlang:atom_to_list(WorkerName) ++ "_table"),
ets:new(Name, [named_table, bag, protected, {read_concurrency, true}]);
_OtherName ->
undefined
end.