%%%-------------------------------------------------------------------
%% @doc OpenID Connect High Level Interface
%%
%% <h2>Setup</h2>
%%
%% ```
%% {ok, Pid} =
%% oidcc_provider_configuration_worker:start_link(#{
%% issuer => <<"https://accounts.google.com">>,
%% name => {local, google_config_provider}
%% }).
%% '''
%%
%% (or via a `supervisor')
%%
%% See {@link oidcc_provider_configuration_worker} for details
%%
%% <h2>Global Configuration</h2>
%%
%% <ul>
%% <li>`max_clock_skew' (default `0') - Maximum allowed clock skew for JWT
%% `exp' / `nbf' validation</li>
%% </ul>
%% @end
%% @since 3.0.0
%%%-------------------------------------------------------------------
-module(oidcc).
-feature(maybe_expr, enable).
-export([client_credentials_token/4]).
-export([create_redirect_url/4]).
-export([initiate_logout_url/4]).
-export([introspect_token/5]).
-export([jwt_profile_token/6]).
-export([refresh_token/5]).
-export([retrieve_token/5]).
-export([retrieve_userinfo/5]).
%% @doc
%% Create Auth Redirect URL
%%
%% <h2>Examples</h2>
%%
%% ```
%% {ok, RedirectUri} =
%% oidcc:create_redirect_url(
%% provider_name,
%% <<"client_id">>,
%% <<"client_secret">>
%% #{redirect_uri: <<"https://my.server/return"}
%% ),
%%
%% %% RedirectUri = https://my.provider/auth?scope=openid&response_type=code&client_id=client_id&redirect_uri=https%3A%2F%2Fmy.server%2Freturn
%% '''
%% @end
%% @since 3.0.0
-spec create_redirect_url(
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
Opts
) ->
{ok, Uri} | {error, oidcc_client_context:error() | oidcc_authorization:error()}
when
ProviderConfigurationWorkerName :: gen_server:server_ref(),
ClientId :: binary(),
ClientSecret :: binary() | unauthenticated,
Opts :: oidcc_authorization:opts() | oidcc_client_context:opts(),
Uri :: uri_string:uri_string().
create_redirect_url(ProviderConfigurationWorkerName, ClientId, ClientSecret, Opts) ->
{ClientContextOpts, OtherOpts} = extract_client_context_opts(Opts),
maybe
{ok, ClientContext} ?=
oidcc_client_context:from_configuration_worker(
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
ClientContextOpts
),
oidcc_authorization:create_redirect_url(ClientContext, OtherOpts)
end.
%% @doc
%% retrieve the token using the authcode received before and directly validate
%% the result.
%%
%% the authcode was sent to the local endpoint by the OpenId Connect provider,
%% using redirects
%%
%% <h2>Examples</h2>
%%
%% ```
%% %% Get AuthCode from Redirect
%%
%% {ok, #oidcc_token{}} =
%% oidcc:retrieve_token(
%% AuthCode,
%% provider_name,
%% <<"client_id">>,
%% <<"client_secret">>,
%% #{redirect_uri => <<"https://example.com/callback">>}
%% ).
%% '''
%% @end
%% @since 3.0.0
-spec retrieve_token(
AuthCode,
ProviderConfigurationWorkerName,
ClientId,
ClientSecret | unauthenticated,
Opts
) ->
{ok, oidcc_token:t()} | {error, oidcc_client_context:error() | oidcc_token:error()}
when
AuthCode :: binary(),
ProviderConfigurationWorkerName :: gen_server:server_ref(),
ClientId :: binary(),
ClientSecret :: binary(),
Opts :: oidcc_token:retrieve_opts() | oidcc_client_context:opts().
retrieve_token(
AuthCode,
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
Opts
) ->
{ClientContextOpts, OtherOpts} = extract_client_context_opts(Opts),
RefreshJwksFun = oidcc_jwt_util:refresh_jwks_fun(ProviderConfigurationWorkerName),
OptsWithRefresh = maps_put_new(refresh_jwks, RefreshJwksFun, OtherOpts),
maybe
{ok, ClientContext} ?=
oidcc_client_context:from_configuration_worker(
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
ClientContextOpts
),
oidcc_token:retrieve(AuthCode, ClientContext, OptsWithRefresh)
end.
%% @doc
%% Load userinfo for the given token
%%
%% <h2>Examples</h2>
%%
%% ```
%% %% Get Token
%%
%% {ok, #{<<"sub">> => Sub}} =
%% oidcc:retrieve_userinfo(
%% Token,
%% provider_name,
%% <<"client_id">>,
%% <<"client_secret">>,
%% #{}
%% ).
%% '''
%% @end
%% @since 3.0.0
-spec retrieve_userinfo
(
Token,
ProviderConfigurationWorkerName,
ClientId,
ClientSecret | unauthenticated,
Opts
) ->
{ok, map()} | {error, oidcc_client_context:error() | oidcc_userinfo:error()}
when
Token :: oidcc_token:t(),
ProviderConfigurationWorkerName :: gen_server:server_ref(),
ClientId :: binary(),
ClientSecret :: binary() | unauthenticated,
Opts :: oidcc_userinfo:retrieve_opts_no_sub() | oidcc_client_context:opts();
(Token, ProviderConfigurationWorkerName, ClientId, ClientSecret, Opts) ->
{ok, map()} | {error, any()}
when
Token :: binary(),
ProviderConfigurationWorkerName :: gen_server:server_ref(),
ClientId :: binary(),
ClientSecret :: binary(),
Opts :: oidcc_userinfo:retrieve_opts().
retrieve_userinfo(
Token,
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
Opts
) ->
{ClientContextOpts, OtherOpts} = extract_client_context_opts(Opts),
maybe
{ok, ClientContext} ?=
oidcc_client_context:from_configuration_worker(
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
ClientContextOpts
),
oidcc_userinfo:retrieve(Token, ClientContext, OtherOpts)
end.
%% @doc Refresh Token
%%
%% <h2>Examples</h2>
%%
%% ```
%% %% Get Token and wait for its expiry
%%
%% {ok, #oidcc_token{}} =
%% oidcc:refresh_token(
%% Token,
%% provider_name,
%% <<"client_id">>,
%% <<"client_secret">>,
%% #{expected_subject => <<"sub_from_initial_id_token>>}
%% ).
%% '''
%% @end
%% @since 3.0.0
-spec refresh_token
(
RefreshToken,
ProviderConfigurationWorkerName,
ClientId,
ClientSecret | unauthenticated,
Opts
) ->
{ok, oidcc_token:t()} | {error, oidcc_client_context:error() | oidcc_token:error()}
when
RefreshToken :: binary(),
ProviderConfigurationWorkerName :: gen_server:server_ref(),
ClientId :: binary(),
ClientSecret :: binary(),
Opts :: oidcc_token:refresh_opts() | oidcc_client_context:opts();
(
Token,
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
Opts
) ->
{ok, oidcc_token:t()} | {error, oidcc_client_context:error() | oidcc_token:error()}
when
Token :: oidcc_token:t(),
ProviderConfigurationWorkerName :: gen_server:server_ref(),
ClientId :: binary(),
ClientSecret :: binary(),
Opts :: oidcc_token:refresh_opts_no_sub().
refresh_token(
RefreshToken,
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
Opts
) ->
{ClientContextOpts, OtherOpts} = extract_client_context_opts(Opts),
RefreshJwksFun = oidcc_jwt_util:refresh_jwks_fun(ProviderConfigurationWorkerName),
OptsWithRefresh = maps_put_new(refresh_jwks, RefreshJwksFun, OtherOpts),
maybe
{ok, ClientContext} ?=
oidcc_client_context:from_configuration_worker(
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
ClientContextOpts
),
oidcc_token:refresh(RefreshToken, ClientContext, OptsWithRefresh)
end.
%% @doc
%% Introspect the given access token
%%
%% <h2>Examples</h2>
%%
%% ```
%% %% Get AccessToken
%%
%% {ok, #oidcc_token_introspection{active = True}} =
%% oidcc:introspect_token(
%% AccessToken,
%% provider_name,
%% <<"client_id">>,
%% <<"client_secret">>,
%% #{}
%% ).
%% '''
%% @end
%% @since 3.0.0
-spec introspect_token(
Token,
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
Opts
) ->
{ok, oidcc_token_introspection:t()}
| {error, oidcc_client_context:error() | oidcc_token_introspection:error()}
when
Token :: oidcc_token:t() | binary(),
ProviderConfigurationWorkerName :: gen_server:server_ref(),
ClientId :: binary(),
ClientSecret :: binary(),
Opts :: oidcc_token_introspection:opts() | oidcc_client_context:opts().
introspect_token(
Token,
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
Opts
) ->
{ClientContextOpts, OtherOpts} = extract_client_context_opts(Opts),
maybe
{ok, ClientContext} ?=
oidcc_client_context:from_configuration_worker(
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
ClientContextOpts
),
oidcc_token_introspection:introspect(Token, ClientContext, OtherOpts)
end.
%% @doc Retrieve JSON Web Token (JWT) Profile Token
%%
%% See [https://datatracker.ietf.org/doc/html/rfc7523#section-4]
%%
%% <h2>Examples</h2>
%%
%% ```
%% {ok, KeyJson} = file:read_file("jwt-profile.json"),
%% KeyMap = jose:decode(KeyJson),
%% Key = jose_jwk:from_pem(maps:get(<<"key">>, KeyMap)),
%%
%% {ok, #oidcc_token{}} =
%% oidcc_token:jwt_profile(
%% <<"subject">>,
%% provider_name,
%% <<"client_id">>,
%% <<"client_secret">>,
%% Key,
%% #{
%% scope => [<<"scope">>],
%% kid => maps:get(<<"keyId">>, KeyMap)
%% }
%% ).
%% '''
%% @end
%% @since 3.0.0
-spec jwt_profile_token(
Subject,
ProviderConfigurationWorkerName,
ClientId,
ClientSecret | unauthenticated,
Jwk,
Opts
) -> {ok, oidcc_token:t()} | {error, oidcc_client_context:error() | oidcc_token:error()} when
Subject :: binary(),
ProviderConfigurationWorkerName :: gen_server:server_ref(),
ClientId :: binary(),
ClientSecret :: binary(),
Jwk :: jose_jwk:key(),
Opts :: oidcc_token:jwt_profile_opts() | oidcc_client_context:opts().
jwt_profile_token(Subject, ProviderConfigurationWorkerName, ClientId, ClientSecret, Jwk, Opts) ->
{ClientContextOpts, OtherOpts} = extract_client_context_opts(Opts),
RefreshJwksFun = oidcc_jwt_util:refresh_jwks_fun(ProviderConfigurationWorkerName),
OptsWithRefresh = maps_put_new(refresh_jwks, RefreshJwksFun, OtherOpts),
maybe
{ok, ClientContext} ?=
oidcc_client_context:from_configuration_worker(
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
ClientContextOpts
),
oidcc_token:jwt_profile(Subject, ClientContext, Jwk, OptsWithRefresh)
end.
%% @doc Retrieve Client Credential Token
%%
%% See [https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.4]
%%
%% <h2>Examples</h2>
%%
%% ```
%% {ok, #oidcc_token{}} =
%% oidcc:client_credentials_token(
%% provider_name,
%% <<"client_id">>,
%% <<"client_secret">>,
%% #{scope => [<<"scope">>]}
%% ).
%% '''
%% @end
%% @since 3.0.0
-spec client_credentials_token(
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
Opts
) -> {ok, oidcc_token:t()} | {error, oidcc_client_context:error() | oidcc_token:error()} when
ProviderConfigurationWorkerName :: gen_server:server_ref(),
ClientId :: binary(),
ClientSecret :: binary(),
Opts :: oidcc_token:client_credentials_opts() | oidcc_client_context:opts().
client_credentials_token(ProviderConfigurationWorkerName, ClientId, ClientSecret, Opts) ->
{ClientContextOpts, OtherOpts} = extract_client_context_opts(Opts),
RefreshJwksFun = oidcc_jwt_util:refresh_jwks_fun(ProviderConfigurationWorkerName),
OptsWithRefresh = maps_put_new(refresh_jwks, RefreshJwksFun, OtherOpts),
maybe
{ok, ClientContext} ?=
oidcc_client_context:from_configuration_worker(
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
ClientContextOpts
),
oidcc_token:client_credentials(ClientContext, OptsWithRefresh)
end.
%% @doc
%% Create Initiate URI for Relaying Party initated Logout
%%
%% See [https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout]
%%
%% <h2>Examples</h2>
%%
%% ```
%% %% Get `Token` from `oidcc_token`
%%
%% {ok, RedirectUri} =
%% oidcc:initiate_logout_url(
%% Token,
%% provider_name,
%% <<"client_id">>,
%% #{post_logout_redirect_uri: <<"https://my.server/return"}
%% ),
%%
%% %% RedirectUri = https://my.provider/logout?id_token_hint=IDToken&client_id=ClientId&post_logout_redirect_uri=https%3A%2F%2Fmy.server%2Freturn
%% '''
%% @end
%% @since 3.0.0
-spec initiate_logout_url(
Token,
ProviderConfigurationWorkerName,
ClientId,
Opts
) ->
{ok, uri_string:uri_string()} | {error, oidcc_client_context:error() | oidcc_logout:error()}
when
Token :: IdToken | oidcc_token:t() | undefined,
IdToken :: binary(),
ProviderConfigurationWorkerName :: gen_server:server_ref(),
ClientId :: binary(),
Opts :: oidcc_logout:initiate_url_opts() | oidcc_client_context:unauthenticated_opts().
initiate_logout_url(Token, ProviderConfigurationWorkerName, ClientId, Opts) ->
{ClientContextOpts, OtherOpts} = extract_client_context_opts(Opts),
maybe
{ok, ClientContext} ?=
oidcc_client_context:from_configuration_worker(
ProviderConfigurationWorkerName,
ClientId,
unauthenticated,
ClientContextOpts
),
oidcc_logout:initiate_url(Token, ClientContext, OtherOpts)
end.
-spec maps_put_new(Key, Value, Map1) -> Map2 when
Key :: term(), Value :: term(), Map1 :: map(), Map2 :: map().
maps_put_new(Key, Value, Map) ->
case maps:is_key(Key, Map) of
true -> Map;
false -> maps:put(Key, Value, Map)
end.
-spec extract_client_context_opts(Opts) -> {ClientContextOpts, RestOpts} when
Opts :: RestOpts | ClientContextOpts,
RestOpts :: map(),
ClientContextOpts :: oidcc_client_context:opts().
extract_client_context_opts(Opts) ->
{
maps:with([client_jwks], Opts),
maps:without([client_jwks], Opts)
}.