-module(oidcc).
-feature(maybe_expr, enable).
-include("internal/doc.hrl").
?MODULEDOC("""
OpenID Connect High Level Interface
## Setup
```erlang
{ok, Pid} =
oidcc_provider_configuration_worker:start_link(#{
issuer => <<"https://accounts.google.com">>,
name => {local, google_config_provider}
}).
```
(or via a `m:supervisor`)
See `m:oidcc_provider_configuration_worker` for details
## Global Configuration
* `max_clock_skew` (default `0`) - Maximum allowed clock skew for JWT
`exp` / `nbf` validation, in seconds
""").
?MODULEDOC(#{since => <<"3.0.0">>}).
-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
## Examples
```erlang
{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
```
""").
?DOC(#{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, OtherOpts0} = extract_client_context_opts(Opts),
maybe
{ok, ClientContext0} ?=
oidcc_client_context:from_configuration_worker(
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
ClientContextOpts
),
{ok, ClientContext, OtherOpts} = oidcc_profile:apply_profiles(ClientContext0, OtherOpts0),
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.
## Examples
```erlang
%% Get AuthCode from Redirect
{ok, #oidcc_token{}} =
oidcc:retrieve_token(
AuthCode,
provider_name,
<<"client_id">>,
<<"client_secret">>,
#{redirect_uri => <<"https://example.com/callback">>}
).
```
""").
?DOC(#{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),
OptsWithRefresh0 = maps_put_new(refresh_jwks, RefreshJwksFun, OtherOpts),
maybe
{ok, ClientContext0} ?=
oidcc_client_context:from_configuration_worker(
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
ClientContextOpts
),
{ok, ClientContext, OptsWithRefresh} = oidcc_profile:apply_profiles(
ClientContext0, OptsWithRefresh0
),
oidcc_token:retrieve(AuthCode, ClientContext, OptsWithRefresh)
end.
?DOC("""
Load userinfo for the given token.
## Examples
```erlang
%% Get Token
{ok, #{<<"sub">> => Sub}} =
oidcc:retrieve_userinfo(
Token,
provider_name,
<<"client_id">>,
<<"client_secret">>,
#{}
).
```
""").
?DOC(#{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, OtherOpts0} = extract_client_context_opts(Opts),
maybe
{ok, ClientContext0} ?=
oidcc_client_context:from_configuration_worker(
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
ClientContextOpts
),
{ok, ClientContext, OtherOpts} = oidcc_profile:apply_profiles(ClientContext0, OtherOpts0),
oidcc_userinfo:retrieve(Token, ClientContext, OtherOpts)
end.
?DOC("""
Refresh Token.
## Examples
```erlang
%% 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">>}
).
```
""").
?DOC(#{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),
OptsWithRefresh0 = maps_put_new(refresh_jwks, RefreshJwksFun, OtherOpts),
maybe
{ok, ClientContext0} ?=
oidcc_client_context:from_configuration_worker(
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
ClientContextOpts
),
{ok, ClientContext, OptsWithRefresh} = oidcc_profile:apply_profiles(
ClientContext0, OptsWithRefresh0
),
oidcc_token:refresh(RefreshToken, ClientContext, OptsWithRefresh)
end.
?DOC("""
Introspect the given access token.
## Examples
```erlang
%% Get AccessToken
{ok, #oidcc_token_introspection{active = True}} =
oidcc:introspect_token(
AccessToken,
provider_name,
<<"client_id">>,
<<"client_secret">>,
#{}
).
```
""").
?DOC(#{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, OtherOpts0} = extract_client_context_opts(Opts),
maybe
{ok, ClientContext0} ?=
oidcc_client_context:from_configuration_worker(
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
ClientContextOpts
),
{ok, ClientContext, OtherOpts} = oidcc_profile:apply_profiles(ClientContext0, OtherOpts0),
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.
## Examples
```erlang
{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)
}
).
```
""").
?DOC(#{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),
OptsWithRefresh0 = maps_put_new(refresh_jwks, RefreshJwksFun, OtherOpts),
maybe
{ok, ClientContext0} ?=
oidcc_client_context:from_configuration_worker(
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
ClientContextOpts
),
{ok, ClientContext, OptsWithRefresh} = oidcc_profile:apply_profiles(
ClientContext0, OptsWithRefresh0
),
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.
## Examples
```erlang
{ok, #oidcc_token{}} =
oidcc:client_credentials_token(
provider_name,
<<"client_id">>,
<<"client_secret">>,
#{scope => [<<"scope">>]}
).
```
""").
?DOC(#{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),
OptsWithRefresh0 = maps_put_new(refresh_jwks, RefreshJwksFun, OtherOpts),
maybe
{ok, ClientContext0} ?=
oidcc_client_context:from_configuration_worker(
ProviderConfigurationWorkerName,
ClientId,
ClientSecret,
ClientContextOpts
),
{ok, ClientContext, OptsWithRefresh} = oidcc_profile:apply_profiles(
ClientContext0, OptsWithRefresh0
),
oidcc_token:client_credentials(ClientContext, OptsWithRefresh)
end.
?DOC("""
Create Initiate URI for Relaying Party initiated Logout.
See https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout.
## Examples
```erlang
%% 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
```
""").
?DOC(#{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, OtherOpts0} = extract_client_context_opts(Opts),
maybe
{ok, ClientContext0} ?=
oidcc_client_context:from_configuration_worker(
ProviderConfigurationWorkerName,
ClientId,
unauthenticated,
ClientContextOpts
),
{ok, ClientContext, OtherOpts} = oidcc_profile:apply_profiles(ClientContext0, OtherOpts0),
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)
}.