Current section
Files
Jump to
Current section
Files
src/auth/oidc/adk_oidcc_authorization_code_adapter.erl
%% @doc Oidcc 3.7 authorization-code adapter with mandatory S256 PKCE.
%%
%% oidcc:retrieve_token/5 validates issuer, signature, audience, time claims,
%% authorization-party and the bound nonce before it returns. This adapter
%% extracts the validated provider subject and retains only the refresh
%% credential needed by the existing token manager. The local principal is a
%% separate issuer-bound identity and remains the credential-store scope; it
%% must not be compared to the provider's raw sub claim.
-module(adk_oidcc_authorization_code_adapter).
-behaviour(adk_authorization_code_adapter).
-export([validate_context/1, authorization_uri/2, exchange_code/3]).
-define(MAX_CLIENT_ID_BYTES, 4096).
-define(MAX_CLIENT_SECRET_BYTES, 16384).
-spec validate_context(map()) -> ok | {error, invalid_adapter_context}.
validate_context(#{provider_worker := Provider,
client_id := ClientId,
client_secret := ClientSecret} = Context) ->
case exact_keys(Context, [provider_worker, client_id, client_secret])
andalso valid_provider(Provider)
andalso valid_text(ClientId, ?MAX_CLIENT_ID_BYTES)
andalso valid_text(ClientSecret, ?MAX_CLIENT_SECRET_BYTES) of
true -> ok;
false -> {error, invalid_adapter_context}
end;
validate_context(_Context) ->
{error, invalid_adapter_context}.
-spec authorization_uri(map(),
adk_authorization_code_adapter:authorization_opts()) ->
{ok, binary()} | {error, authorization_unavailable}.
authorization_uri(#{provider_worker := Provider,
client_id := ClientId,
client_secret := ClientSecret}, Opts) ->
OidccOpts0 = #{state => maps:get(state, Opts),
nonce => maps:get(nonce, Opts),
pkce_verifier => maps:get(pkce_verifier, Opts),
require_pkce => true,
redirect_uri => maps:get(redirect_uri, Opts),
scopes => maps:get(scopes, Opts)},
OidccOpts = resource_extension(url_extension, Opts, OidccOpts0),
try oidcc:create_redirect_url(
Provider, ClientId, ClientSecret, OidccOpts) of
{ok, Uri0} ->
case safe_binary(Uri0) of
{ok, Uri} -> {ok, Uri};
error -> {error, authorization_unavailable}
end;
{error, _Reason} ->
{error, authorization_unavailable}
catch
_:_ -> {error, authorization_unavailable}
end;
authorization_uri(_Context, _Opts) ->
{error, authorization_unavailable}.
-spec exchange_code(map(), binary(),
adk_authorization_code_adapter:exchange_opts()) ->
{ok, adk_credential_store:credential()} |
{error, authorization_failed}.
exchange_code(#{provider_worker := Provider,
client_id := ClientId,
client_secret := ClientSecret}, Code, Opts)
when is_binary(Code), byte_size(Code) > 0 ->
OidccOpts0 = #{nonce => maps:get(nonce, Opts),
pkce_verifier => maps:get(pkce_verifier, Opts),
require_pkce => true,
redirect_uri => maps:get(redirect_uri, Opts),
scope => maps:get(scopes, Opts)},
OidccOpts = resource_extension(body_extension, Opts, OidccOpts0),
try oidcc:retrieve_token(
Code, Provider, ClientId, ClientSecret, OidccOpts) of
{ok, Token} ->
adk_oidcc_adapter_policy:validated_refresh_credential(
Token, ClientId, ClientSecret);
{error, _Reason} ->
{error, authorization_failed}
catch
_:_ -> {error, authorization_failed}
end;
exchange_code(_Context, _Code, _Opts) ->
{error, authorization_failed}.
resource_extension(Key, #{resource := Resource}, Opts)
when is_binary(Resource), byte_size(Resource) > 0 ->
Opts#{Key => [{<<"resource">>, Resource}]};
resource_extension(_Key, _FlowOpts, Opts) ->
Opts.
valid_provider(Provider) when is_pid(Provider) -> true;
valid_provider(Provider) when is_atom(Provider) -> Provider =/= undefined;
valid_provider(_) -> false.
valid_text(Value, Max) when is_binary(Value) ->
byte_size(Value) > 0 andalso byte_size(Value) =< Max;
valid_text(_, _) -> false.
exact_keys(Map, Allowed) ->
lists:sort(maps:keys(Map)) =:= lists:sort(Allowed).
safe_binary(Value) ->
try erlang:iolist_size(Value) of
Size when Size > 0, Size =< 8192 -> {ok, iolist_to_binary(Value)};
_ -> error
catch
_:_ -> error
end.