Current section

Files

Jump to
erlang_adk src auth core adk_auth_sup.erl
Raw

src/auth/core/adk_auth_sup.erl

%% @doc Supervision tree for credential storage and token refresh.
%%
%% This supervisor is intentionally standalone until it is integrated into the
%% application's top-level tree. A rest_for_one strategy ensures loss of the
%% private credential table also replaces all refresh and cache state.
-module(adk_auth_sup).
-behaviour(supervisor).
-export([start_link/0, start_link/1, child_spec/1]).
-export([init/1]).
-spec start_link() -> supervisor:startlink_ret().
start_link() ->
start_link(#{}).
-spec start_link(map()) -> supervisor:startlink_ret().
start_link(Opts) when is_map(Opts) ->
case maps:get(name, Opts, ?MODULE) of
undefined -> supervisor:start_link(?MODULE, Opts);
Name when is_atom(Name) ->
supervisor:start_link({local, Name}, ?MODULE, Opts)
end.
-spec child_spec(map()) -> supervisor:child_spec().
child_spec(Opts) ->
#{id => maps:get(id, Opts, ?MODULE),
start => {?MODULE, start_link, [Opts]},
restart => permanent,
shutdown => infinity,
type => supervisor,
modules => [?MODULE]}.
init(Opts) ->
StoreModule = maps:get(credential_store_module, Opts,
adk_credential_store_ets),
StoreName = required_name(credential_store_name, Opts,
adk_credential_store_ets),
RefreshSupName = required_name(refresh_sup_name, Opts,
adk_token_refresh_sup),
TokenManagerName = required_name(token_manager_name, Opts,
adk_token_manager),
FlowExchangeSupName = required_name(
authorization_exchange_sup_name, Opts,
adk_authorization_flow_exchange_sup),
FlowName = required_name(authorization_flow_name, Opts,
adk_authorization_flow),
StoreOpts0 = maps:get(credential_store_opts, Opts, #{}),
StoreOpts = StoreOpts0#{name => StoreName,
id => credential_store},
RefreshOpts = #{name => RefreshSupName,
id => token_refresh_sup},
ManagerOpts0 = maps:get(token_manager_opts, Opts, #{}),
%% Provider profiles are immutable for the lifetime of this supervision
%% tree. A standalone supervisor accepts them directly; the application
%% tree falls back to an empty, network-free configuration unless the host
%% sets `auth_provider_profiles` before startup.
ProviderProfiles = maps:get(
provider_profiles, Opts,
maps:get(
provider_profiles, ManagerOpts0,
application:get_env(
erlang_adk, auth_provider_profiles, #{}))),
ManagerOpts = ManagerOpts0#{name => TokenManagerName,
store_module => StoreModule,
store_handle => StoreName,
refresh_sup => RefreshSupName,
provider_profiles => ProviderProfiles,
id => token_manager},
FlowExchangeOpts = #{name => FlowExchangeSupName,
id => authorization_flow_exchange_sup},
FlowOpts0 = maps:get(authorization_flow_opts, Opts, #{}),
%% Raw profiles can contain an OAuth client secret, so they are never
%% copied into a child specification. Embedders that do not use the
%% application environment provide a zero-arity module/function loader;
%% the safe MFA is all a supervisor report can expose.
ok = ensure_no_raw_flow_profiles(Opts, FlowOpts0),
ProfileLoader = maps:get(
authorization_profile_loader, Opts,
application:get_env(
erlang_adk,
auth_authorization_profile_loader,
undefined)),
FlowOptsBase = case ProfileLoader of
undefined -> FlowOpts0;
{Module, Function}
when is_atom(Module), Module =/= undefined,
is_atom(Function), Function =/= undefined ->
FlowOpts0#{profile_loader => {Module, Function}};
InvalidLoader ->
erlang:error({invalid_authorization_profile_loader,
InvalidLoader})
end,
FlowOpts1 = FlowOptsBase#{name => FlowName,
store_module => StoreModule,
store_handle => StoreName,
exchange_sup => FlowExchangeSupName,
id => authorization_flow},
%% For the normal application tree the flow reads immutable profiles from
%% auth_authorization_profiles inside init/1. Keeping them out of this
%% child spec prevents client secrets from appearing in supervisor reports.
FlowOpts = FlowOpts1,
SupFlags = #{strategy => rest_for_one,
intensity => 5,
period => 10},
Children = [StoreModule:child_spec(StoreOpts),
adk_token_refresh_sup:child_spec(RefreshOpts),
adk_token_manager:child_spec(ManagerOpts),
adk_authorization_flow_exchange_sup:child_spec(
FlowExchangeOpts),
adk_authorization_flow:child_spec(FlowOpts)],
{ok, {SupFlags, Children}}.
required_name(Key, Opts, Default) ->
case maps:get(Key, Opts, Default) of
Name when is_atom(Name), Name =/= undefined -> Name;
Invalid -> erlang:error({invalid_auth_component_name, Key, Invalid})
end.
ensure_no_raw_flow_profiles(Opts, FlowOpts) ->
case maps:is_key(authorization_profiles, Opts) orelse
maps:is_key(provider_profiles, FlowOpts) of
true -> erlang:error(raw_authorization_profiles_not_allowed);
false -> ok
end.