diff --git a/include/internal/doc.hrl b/include/internal/doc.hrl new file mode 100644 index 0000000..3fd1b10 --- /dev/null +++ b/include/internal/doc.hrl @@ -0,0 +1,8 @@ +%% TODO: Remove the following macros as soon as only OTP >= 27 is supported. +-if(?OTP_RELEASE >= 27). + -define(MODULEDOC(Str), -moduledoc(Str)). + -define(DOC(Str), -doc(Str)). +-else. + -define(MODULEDOC(Str), -compile([])). + -define(DOC(Str), -compile([])). +-endif. \ No newline at end of file diff --git a/mix.exs b/mix.exs index e6b7af9..59224f3 100644 --- a/mix.exs +++ b/mix.exs @@ -18,7 +18,6 @@ defmodule Oidcc.Mixfile do docs: &docs/0, description: to_string(@props[:description]), package: package(), - aliases: [docs: ["compile", &edoc_chunks/1, "docs"]], test_coverage: [ignore_modules: [Oidcc.RecordStruct]] ] end @@ -80,17 +79,4 @@ defmodule Oidcc.Mixfile do assets: %{"assets" => "assets"} ] end - - defp edoc_chunks(_args) do - base_path = Path.dirname(__ENV__.file) - doc_chunk_path = Application.app_dir(:oidcc, "doc") - - :ok = - :edoc.application(:oidcc, String.to_charlist(base_path), - doclet: :edoc_doclet_chunks, - layout: :edoc_layout_chunks, - preprocess: true, - dir: String.to_charlist(doc_chunk_path) - ) - end end diff --git a/src/oidcc.erl b/src/oidcc.erl index a7b0476..817afc3 100644 --- a/src/oidcc.erl +++ b/src/oidcc.erl @@ -1,33 +1,32 @@ -%%%------------------------------------------------------------------- -%% @doc OpenID Connect High Level Interface -%% -%%

Setup

-%% -%% ``` -%% {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 -%% -%%

Global Configuration

-%% -%% -%% @end -%% @since 3.0.0 -%%%------------------------------------------------------------------- -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]). @@ -37,24 +36,24 @@ -export([retrieve_token/5]). -export([retrieve_userinfo/5]). -%% @doc -%% Create Auth Redirect URL -%% -%%

Examples

-%% -%% ``` -%% {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 +?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, @@ -82,29 +81,29 @@ create_redirect_url(ProviderConfigurationWorkerName, ClientId, ClientSecret, Opt 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

-%% -%% ``` -%% %% 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 +?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, @@ -145,25 +144,25 @@ retrieve_token( oidcc_token:retrieve(AuthCode, ClientContext, OptsWithRefresh) end. -%% @doc -%% Load userinfo for the given token -%% -%%

Examples

-%% -%% ``` -%% %% Get Token -%% -%% {ok, #{<<"sub">> => Sub}} = -%% oidcc:retrieve_userinfo( -%% Token, -%% provider_name, -%% <<"client_id">>, -%% <<"client_secret">>, -%% #{} -%% ). -%% ''' -%% @end -%% @since 3.0.0 +?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, @@ -208,24 +207,25 @@ retrieve_userinfo( oidcc_userinfo:retrieve(Token, ClientContext, OtherOpts) end. -%% @doc Refresh Token -%% -%%

Examples

-%% -%% ``` -%% %% 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 +?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, @@ -281,25 +281,25 @@ refresh_token( oidcc_token:refresh(RefreshToken, ClientContext, OptsWithRefresh) end. -%% @doc -%% Introspect the given access token -%% -%%

Examples

-%% -%% ``` -%% %% Get AccessToken -%% -%% {ok, #oidcc_token_introspection{active = True}} = -%% oidcc:introspect_token( -%% AccessToken, -%% provider_name, -%% <<"client_id">>, -%% <<"client_secret">>, -%% #{} -%% ). -%% ''' -%% @end -%% @since 3.0.0 +?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, @@ -336,32 +336,33 @@ introspect_token( 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

-%% -%% ``` -%% {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 +?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, @@ -396,23 +397,24 @@ jwt_profile_token(Subject, ProviderConfigurationWorkerName, ClientId, ClientSecr 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

-%% -%% ``` -%% {ok, #oidcc_token{}} = -%% oidcc:client_credentials_token( -%% provider_name, -%% <<"client_id">>, -%% <<"client_secret">>, -%% #{scope => [<<"scope">>]} -%% ). -%% ''' -%% @end -%% @since 3.0.0 +?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, @@ -443,28 +445,28 @@ client_credentials_token(ProviderConfigurationWorkerName, ClientId, ClientSecret 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

-%% -%% ``` -%% %% 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 +?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, diff --git a/src/oidcc_auth_util.erl b/src/oidcc_auth_util.erl index 460b6e9..b4338a1 100644 --- a/src/oidcc_auth_util.erl +++ b/src/oidcc_auth_util.erl @@ -1,11 +1,11 @@ -%%%------------------------------------------------------------------- -%% @doc Authentication Utilities -%% @end -%%%------------------------------------------------------------------- -module(oidcc_auth_util). -feature(maybe_expr, enable). +-include("internal/doc.hrl"). +?MODULEDOC("Authentication Utilities"). +?MODULEDOC(#{since => <<"3.2.0">>}). + -include("oidcc_client_context.hrl"). -include("oidcc_provider_configuration.hrl"). @@ -13,6 +13,7 @@ -export_type([auth_method/0, error/0]). +?DOC(#{since => <<"3.2.0">>}). -type auth_method() :: none | client_secret_basic @@ -21,6 +22,7 @@ | private_key_jwt | tls_client_auth. +?DOC(#{since => <<"3.2.0">>}). -type error() :: no_supported_auth_method. -export([add_client_authentication/6]). @@ -28,7 +30,7 @@ -export([add_authorization_header/6]). -export([maybe_mtls_endpoint/4]). -%% @private +?DOC(false). -spec add_client_authentication( QueryList, Header, SupportedAuthMethods, AllowAlgorithms, Opts, ClientContext ) -> @@ -276,7 +278,7 @@ add_jwt_bearer_assertion(ClientAssertion, Body, Header, ClientContext) -> Header }. -%% @private +?DOC(false). -spec add_dpop_proof_header(Header, Method, Endpoint, Opts, ClientContext) -> Header when Header :: [oidcc_http_util:http_header()], Method :: post | get, @@ -298,7 +300,7 @@ add_dpop_proof_header(Header, Method, Endpoint, Opts, ClientContext) -> Header end. -%% @private +?DOC(false). -spec add_authorization_header( AccessToken, AccessTokenType, Method, Endpoint, Opts, ClientContext ) -> @@ -338,7 +340,7 @@ add_authorization_header( [oidcc_http_util:bearer_auth_header(AccessToken)] end. -%% @private +?DOC(false). -spec maybe_mtls_endpoint( Endpoint, auth_method(), MtlsEndpointName, ClientContext ) -> Endpoint when diff --git a/src/oidcc_authorization.erl b/src/oidcc_authorization.erl index bab2da0..64175b4 100644 --- a/src/oidcc_authorization.erl +++ b/src/oidcc_authorization.erl @@ -1,12 +1,11 @@ -%%%------------------------------------------------------------------- -%% @doc Functions to start an OpenID Connect Authorization -%% @end -%% @since 3.0.0 -%%%------------------------------------------------------------------- -module(oidcc_authorization). -feature(maybe_expr, enable). +-include("internal/doc.hrl"). +?MODULEDOC("Functions to start an OpenID Connect Authorization"). +?MODULEDOC(#{since => <<"3.0.0">>}). + -include("oidcc_client_context.hrl"). -include("oidcc_provider_configuration.hrl"). @@ -17,6 +16,25 @@ -export_type([error/0]). -export_type([opts/0]). +?DOC(""" +Configure authorization redirect URL. + +See https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest. + +## Parameters + +* `scopes` - list of scopes to request (defaults to `[<<"openid">>]`) +* `state` - state to pass to the provider +* `nonce` - nonce to pass to the provider +* `purpose` - purpose of the authorization request, see [https://cdn.connectid.com.au/specifications/oauth2-purpose-01.html] +* `require_purpose` - whether to require a `purpose` value +* `pkce_verifier` - PKCE verifier (random string), see [https://datatracker.ietf.org/doc/html/rfc7636#section-4.1] +* `require_pkce` - whether to require PKCE when getting the token +* `redirect_uri` - redirect target after authorization is completed +* `url_extension` - add custom query parameters to the authorization URL +* `response_mode` - response mode to use (defaults to `<<"query">>`) +"""). +?MODULEDOC(#{since => <<"3.0.0">>}). -type opts() :: #{ scopes => oidcc_scope:scopes(), @@ -30,27 +48,8 @@ url_extension => oidcc_http_util:query_params(), response_mode => binary() }. -%% Configure authorization redirect url -%% -%% See [https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest] -%% -%%

Parameters

-%% -%% +?MODULEDOC(#{since => <<"3.0.0">>}). -type error() :: {grant_type_not_supported, authorization_code} | par_required @@ -81,28 +80,28 @@ metadata => <<"#{issuer => uri_string:uri_string(), client_id => binary()}">> }). -%% @doc -%% Create Auth Redirect URL -%% -%% For a high level interface using {@link oidcc_provider_configuration_worker} -%% see {@link oidcc:create_redirect_url/4}. -%% -%%

Examples

-%% -%% ``` -%% {ok, ClientContext} = -%% oidcc_client_context:from_configuration_worker(provider_name, -%% <<"client_id">>, -%% <<"client_secret">>), -%% -%% {ok, RedirectUri} = -%% oidcc_authorization:create_redirect_url(ClientContext, -%% #{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 +?DOC(""" +Create Auth Redirect URL. + +For a high level interface using `m:oidcc_provider_configuration_worker` +see `oidcc:create_redirect_url/4`. + +## Examples + +```erlang +{ok, ClientContext} = + oidcc_client_context:from_configuration_worker(provider_name, + <<"client_id">>, + <<"client_secret">>), + +{ok, RedirectUri} = + oidcc_authorization:create_redirect_url(ClientContext, + #{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(ClientContext, Opts) -> {ok, Uri} | {error, error()} when ClientContext :: oidcc_client_context:t(), Opts :: opts(), diff --git a/src/oidcc_backoff.erl b/src/oidcc_backoff.erl index 85afd58..514f1dd 100644 --- a/src/oidcc_backoff.erl +++ b/src/oidcc_backoff.erl @@ -1,13 +1,14 @@ -%%%------------------------------------------------------------------- -%% @doc Backoff Handling -%% -%% Based on `db_connection': -%% [https://github.com/elixir-ecto/db_connection/blob/8ef1f2ea54922873590b8939f2dad6b031c5b49c/lib/db_connection/backoff.ex#L24] -%% @end -%% @since 3.2.0 -%%%------------------------------------------------------------------- -module(oidcc_backoff). +-include("internal/doc.hrl"). +?MODULEDOC(""" +Backoff Handling + +Based on [`db_connection`](https://github.com/elixir-ecto/db_connection/blob/8ef1f2ea54922873590b8939f2dad6b031c5b49c/lib/db_connection/backoff.ex#L24) +"""). +?MODULEDOC(#{since => <<"3.2.0">>}). + + -export_type([type/0]). -export_type([min/0]). -export_type([max/0]). @@ -15,15 +16,19 @@ -export([handle_retry/4]). +?DOC(#{since => <<"3.2.0">>}). -type type() :: stop | exponential | random | random_exponential. +?DOC(#{since => <<"3.2.0">>}). -type min() :: pos_integer(). +?DOC(#{since => <<"3.2.0">>}). -type max() :: pos_integer(). +?DOC(#{since => <<"3.2.0">>}). -opaque state() :: pos_integer() | {pos_integer(), pos_integer()}. -%% @private +?DOC(false). -spec handle_retry(Type, Min, Max, State) -> stop | {Wait, State} when Type :: type(), Min :: min(), Max :: max(), State :: undefined | state(), Wait :: pos_integer(). handle_retry(Type, Min, Max, State) when Min > 0, Max > 0, Max >= Min -> diff --git a/src/oidcc_client_context.erl b/src/oidcc_client_context.erl index 47e3490..ff3e250 100644 --- a/src/oidcc_client_context.erl +++ b/src/oidcc_client_context.erl @@ -1,24 +1,22 @@ -%%%------------------------------------------------------------------- -%% @doc Client Configuration for authorization, token exchange and -%% userinfo -%% -%% For most projects, it makes sense to use -%% {@link oidcc_provider_configuration_worker} and the high-level -%% interface of {@link oidcc}. In that case direct usage of this -%% module is not needed. -%% -%% To use the record, import the definition: -%% -%% ``` -%% -include_lib(["oidcc/include/oidcc_client_context.hrl"]). -%% ''' -%% @end -%% @since 3.0.0 -%%%------------------------------------------------------------------- -module(oidcc_client_context). -feature(maybe_expr, enable). +-include("internal/doc.hrl"). +?MODULEDOC(""" +Client Configuration for authorization, token exchange, and userinfo. + +For most projects, it makes sense to use `m:oidcc_provider_configuration_worker` and the high-level +interface of `oidcc`. In that case, direct usage of this module is not needed. + +To use the record, import the definition: + +```erlang +-include_lib(["oidcc/include/oidcc_client_context.hrl"]). +``` +"""). +?MODULEDOC(#{since => <<"3.0.0">>}). + -include("oidcc_client_context.hrl"). -include("oidcc_provider_configuration.hrl"). @@ -40,6 +38,7 @@ -type t() :: authenticated_t() | unauthenticated_t(). +?DOC(#{since => <<"3.0.0">>}). -type authenticated_t() :: #oidcc_client_context{ provider_configuration :: oidcc_provider_configuration:t(), jwks :: jose_jwk:key(), @@ -48,6 +47,7 @@ client_jwks :: jose_jwk:key() | none }. +?DOC(#{since => <<"3.0.0">>}). -type unauthenticated_t() :: #oidcc_client_context{ provider_configuration :: oidcc_provider_configuration:t(), jwks :: jose_jwk:key(), @@ -56,20 +56,26 @@ client_jwks :: none }. +?DOC(#{since => <<"3.0.0">>}). -type authenticated_opts() :: #{ client_jwks => jose_jwk:key() }. + +?DOC(#{since => <<"3.0.0">>}). -type unauthenticated_opts() :: #{}. +?DOC(#{since => <<"3.0.0">>}). -type opts() :: authenticated_opts() | unauthenticated_opts(). +?DOC(#{since => <<"3.0.0">>}). -type error() :: provider_not_ready. -%% @doc Create Client Context from a {@link oidcc_provider_configuration_worker} -%% -%% See {@link from_configuration_worker/4} -%% @end -%% @since 3.0.0 +?DOC(""" +Create Client Context from a `m:oidcc_provider_configuration_worker`. + +See `from_configuration_worker/4`. +"""). +?DOC(#{since => <<"3.0.0">>}). -spec from_configuration_worker (ProviderName, ClientId, ClientSecret) -> {ok, authenticated_t()} | {error, error()} when ProviderName :: gen_server:server_ref(), @@ -82,39 +88,40 @@ from_configuration_worker(ProviderName, ClientId, ClientSecret) -> from_configuration_worker(ProviderName, ClientId, ClientSecret, #{}). -%% @doc Create Client Context from a {@link oidcc_provider_configuration_worker} -%% -%%

Examples

-%% -%% ``` -%% {ok, Pid} = -%% oidcc_provider_configuration_worker:start_link(#{ -%% issuer => <<"https://login.salesforce.com">> -%% }), -%% -%% {ok, #oidcc_client_context{}} = -%% oidcc_client_context:from_configuration_worker(Pid, -%% <<"client_id">>, -%% <<"client_secret">>). -%% ''' -%% -%% ``` -%% {ok, Pid} = -%% oidcc_provider_configuration_worker:start_link(#{ -%% issuer => <<"https://login.salesforce.com">>, -%% name => {local, salesforce_provider} -%% }), -%% -%% {ok, #oidcc_client_context{}} = -%% oidcc_client_context:from_configuration_worker($ -%% salesforce_provider, -%% <<"client_id">>, -%% <<"client_secret">>, -%% #{client_jwks => jose_jwk:generate_key(16)} -%% ). -%% ''' -%% @end -%% @since 3.0.0 +?DOC(""" +Create Client Context from a `m:oidcc_provider_configuration_worker`. + +## Examples + +```erlang +{ok, Pid} = + oidcc_provider_configuration_worker:start_link(#{ + issuer => <<"https://login.salesforce.com">> + }), + +{ok, #oidcc_client_context{}} = + oidcc_client_context:from_configuration_worker(Pid, + <<"client_id">>, + <<"client_secret">>). +``` + +```erlang +{ok, Pid} = + oidcc_provider_configuration_worker:start_link(#{ + issuer => <<"https://login.salesforce.com">>, + name => {local, salesforce_provider} + }), + +{ok, #oidcc_client_context{}} = + oidcc_client_context:from_configuration_worker( + salesforce_provider, + <<"client_id">>, + <<"client_secret">>, + #{client_jwks => jose_jwk:generate_key(16)} + ). +``` +"""). +?DOC(#{since => <<"3.0.0">>}). -spec from_configuration_worker (ProviderName, ClientId, ClientSecret, Opts) -> {ok, authenticated_t()} | {error, error()} @@ -155,11 +162,12 @@ from_configuration_worker(ProviderName, ClientId, ClientSecret, Opts) -> from_configuration_worker(Pid, ClientId, ClientSecret, Opts) end. -%% @doc Create Client Context manually -%% -%% See {@link from_manual/5} -%% @end -%% @since 3.0.0 +?DOC(""" +Create Client Context manually. + +See `from_manual/5`. +"""). +?DOC(#{since => <<"3.0.0">>}). -spec from_manual (Configuration, Jwks, ClientId, ClientSecret) -> authenticated_t() when Configuration :: oidcc_provider_configuration:t(), @@ -174,30 +182,30 @@ from_configuration_worker(ProviderName, ClientId, ClientSecret, Opts) -> from_manual(Configuration, Jwks, ClientId, ClientSecret) -> from_manual(Configuration, Jwks, ClientId, ClientSecret, #{}). -%% @doc Create Client Context manually -%% -%%

Examples

-%% -%% ``` -%% {ok, Configuration} = -%% oidcc_provider_configuration:load_configuration(<<"https://login.salesforce.com">>, -%% []), -%% -%% #oidcc_provider_configuration{jwks_uri = JwksUri} = Configuration, -%% -%% {ok, Jwks} = oidcc_provider_configuration:load_jwks(JwksUri, []). -%% -%% #oidcc_client_context{} = -%% oidcc_client_context:from_manual( -%% Metadata, -%% Jwks, -%% <<"client_id">>, -%% <<"client_secret">>, -%% #{client_jwks => jose_jwk:generate_key(16)} -%% ). -%% ''' -%% @end -%% @since 3.0.0 +?DOC(""" +Create Client Context manually. + +## Examples + +```erlang +{ok, Configuration} = + oidcc_provider_configuration:load_configuration(<<"https://login.salesforce.com">>, []), + +#oidcc_provider_configuration{jwks_uri = JwksUri} = Configuration, + +{ok, Jwks} = oidcc_provider_configuration:load_jwks(JwksUri, []). + +#oidcc_client_context{} = + oidcc_client_context:from_manual( + Metadata, + Jwks, + <<"client_id">>, + <<"client_secret">>, + #{client_jwks => jose_jwk:generate_key(16)} + ). +``` +"""). +?DOC(#{since => <<"3.0.0">>}). -spec from_manual (Configuration, Jwks, ClientId, ClientSecret, Opts) -> authenticated_t() when Configuration :: oidcc_provider_configuration:t(), @@ -239,33 +247,34 @@ from_manual( client_jwks = maps:get(client_jwks, Opts, none) }. -%% @doc Apply OpenID Connect / OAuth2 Profiles to the context -%% -%% Currently, the only supported profiles are: -%% - `fapi2_security_profile' - https://openid.bitbucket.io/fapi/fapi-2_0-security-profile.html -%% - `fapi2_message_signing' - https://openid.bitbucket.io/fapi/fapi-2_0-message-signing.html -%% -%% It returns an updated `#oidcc_client_context{}' record and a map of options to -%% be merged into the `oidcc_authorization` and `oidcc_token` functions. -%% -%%

Examples

-%% -%% ``` -%% ClientContext = #oidcc_client_context{} = oidcc_client_context:from_...(...), -%% -%% {#oidcc_client_context{} = ClientContext1, Opts} = oidcc_client_context:apply_profiles( -%% ClientContext, -%% #{ -%% profiles => [fapi2_message_signing] -%% }), -%% -%% {ok, Uri} = oidcc_authorization:create_redirect_uri( -%% ClientContext1, -%% maps:merge(Opts, #{...}) -%% ). -%% ''' -%% @end -%% @since 3.2.0 +?DOC(""" +Apply OpenID Connect / OAuth2 Profiles to the context. + +Currently, the only supported profiles are: +- `fapi2_security_profile` - https://openid.bitbucket.io/fapi/fapi-2_0-security-profile.html +- `fapi2_message_signing` - https://openid.bitbucket.io/fapi/fapi-2_0-message-signing.html + +It returns an updated `t:t/0` record and a map of options to +be merged into the `m:oidcc_authorization` and `m:oidcc_token` functions. + +## Examples + +```erlang +ClientContext = #oidcc_client_context{} = oidcc_client_context:from_...(...), + +{#oidcc_client_context{} = ClientContext1, Opts} = oidcc_client_context:apply_profiles( + ClientContext, + #{ + profiles => [fapi2_message_signing] + }), + +{ok, Uri} = oidcc_authorization:create_redirect_uri( + ClientContext1, + maps:merge(Opts, #{...}) +). +``` +"""). +?DOC(#{since => <<"3.2.0">>}). -spec apply_profiles(ClientContext, oidcc_profile:opts()) -> {ok, ClientContext, oidcc_profile:opts_no_profiles()} | {error, oidcc_profile:error()} when diff --git a/src/oidcc_client_registration.erl b/src/oidcc_client_registration.erl index 2c3d1f5..c516cef 100644 --- a/src/oidcc_client_registration.erl +++ b/src/oidcc_client_registration.erl @@ -1,26 +1,27 @@ -%%%------------------------------------------------------------------- -%% @doc Dynamic Client Registration Utilities -%% -%% See [https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata] -%% -%%

Records

-%% -%% To use the record, import the definition: -%% -%% ``` -%% -include_lib(["oidcc/include/oidcc_client_registration.hrl"]). -%% ''' -%% -%%

Telemetry

-%% -%% See {@link 'Elixir.Oidcc.ClientRegistration'} -%% @end -%% @since 3.0.0 -%%%------------------------------------------------------------------- -module(oidcc_client_registration). -feature(maybe_expr, enable). +-include("internal/doc.hrl"). +?MODULEDOC(""" +Dynamic Client Registration Utilities. + +See https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata. + +## Records + +To use the record, import the definition: + +```erlang +-include_lib(["oidcc/include/oidcc_client_registration.hrl"]). +``` + +## Telemetry + +See [`Oidcc.ClientRegistration`](`m:'Elixir.Oidcc.ClientRegistration'`). +"""). +?MODULEDOC(#{since => <<"3.0.0">>}). + -include("oidcc_client_registration.hrl"). -include("oidcc_provider_configuration.hrl"). @@ -31,19 +32,29 @@ -export_type([response/0]). -export_type([t/0]). +?DOC(""" +Configure configuration loading / parsing. + +## Parameters + +* `initial_access_token` - Access Token for registration +* `request_opts` - config for HTTP request +"""). +?DOC(#{since => <<"3.0.0">>}). -type opts() :: #{ initial_access_token => binary() | undefined, request_opts => oidcc_http_util:request_opts() }. -% Configure configuration loading / parsing -% -%

Parameters

-% -% +?DOC(""" +Record containing Client Registration Metadata. + +See https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata and +https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ClientMetadata. + +All unrecognized fields are stored in `extra_fields`. +"""). +?DOC(#{since => <<"3.0.0">>}). -type t() :: #oidcc_client_registration{ %% OpenID Connect Dynamic Client Registration 1.0 @@ -115,13 +126,15 @@ %% Unknown Fields extra_fields :: #{binary() => term()} }. -%% Record containing Client Registration Metadata -%% -%% See [https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata] and -%% [https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ClientMetadata] -%% -%% All unrecognized fields are stored in `extra_fields'. +?DOC(""" +Record containing Client Registration Response. + +See https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse. + +All unrecognized fields are stored in `extra_fields`. +"""). +?DOC(#{since => <<"3.0.0">>}). -type response() :: #oidcc_client_registration_response{ client_id :: erlang:binary(), @@ -133,12 +146,9 @@ %% Unknown Fields extra_fields :: #{binary() => term()} }. -%% Record containing Client Registration Response -%% -%% See [https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse] -%% -%% All unrecognized fields are stored in `extra_fields'. + +?DOC(#{since => <<"3.0.0">>}). -type error() :: registration_not_supported | invalid_content_type @@ -166,28 +176,29 @@ metadata => <<"#{issuer => uri_string:uri_string()}">> }). -%% @doc Register Client -%% -%%

Examples

-%% -%% ``` -%% {ok, ProviderConfiguration} = -%% oidcc_provider_configuration:load_configuration("https://your.issuer"), -%% -%% {ok, #oidcc_client_registration_response{ -%% client_id = ClientId, -%% client_secret = ClientSecret -%% }} = -%% oidcc_client_registration:register( -%% ProviderConfiguration, -%% #oidcc_client_registration{ -%% redirect_uris = ["https://your.application.com/oidcc/callback"] -%% }, -%% #{initial_access_token => <<"optional token you got from the provider">>} -%% ). -%% ''' -%% @end -%% @since 3.0.0 +?DOC(""" +Register Client. + +## Examples + +```erlang +{ok, ProviderConfiguration} = + oidcc_provider_configuration:load_configuration("https://your.issuer"), + +{ok, #oidcc_client_registration_response{ + client_id = ClientId, + client_secret = ClientSecret +}} = + oidcc_client_registration:register( + ProviderConfiguration, + #oidcc_client_registration{ + redirect_uris = ["https://your.application.com/oidcc/callback"] + }, + #{initial_access_token => <<"optional token you got from the provider">>} + ). +``` +"""). +?DOC(#{since => <<"3.0.0">>}). -spec register(ProviderConfiguration, Registration, Opts) -> {ok, response()} | {error, error()} when diff --git a/src/oidcc_decode_util.erl b/src/oidcc_decode_util.erl index 31bced0..de2b0f3 100644 --- a/src/oidcc_decode_util.erl +++ b/src/oidcc_decode_util.erl @@ -1,10 +1,9 @@ -%%%------------------------------------------------------------------- -%% @doc Response Decoding Utils -%% @end -%% @since 3.0.0 -%%%------------------------------------------------------------------- -module(oidcc_decode_util). +-include("internal/doc.hrl"). +?MODULEDOC("Response Decoding Utils"). +?MODULEDOC(#{since => <<"3.0.0">>}). + -export([extract/3]). -export([parse_setting_binary/2]). -export([parse_setting_binary_list/2]). @@ -18,6 +17,7 @@ -export_type([error/0]). +?DOC(#{since => <<"3.0.0">>}). -type error() :: {missing_config_property, Key :: atom()} | {invalid_config_property, { @@ -34,7 +34,7 @@ Field :: atom() }}. -%% @private +?DOC(false). -spec extract( Map :: #{binary() => term()}, Keys :: [{required, Key, ParseFn} | {optional, Key, Default, ParseFn}], @@ -74,7 +74,7 @@ extract(Map1, [{optional, Key, Default, ParseFn} | RestKeys], Acc) -> extract(Map, [], Acc) -> {ok, {Acc, Map}}. -%% @private +?DOC(false). -spec parse_setting_uri(Setting :: term(), Field :: atom()) -> {ok, uri_string:uri_string()} | {error, error()}. parse_setting_uri(Setting, _Field) when is_binary(Setting) -> @@ -82,7 +82,7 @@ parse_setting_uri(Setting, _Field) when is_binary(Setting) -> parse_setting_uri(_Setting, Field) -> {error, {invalid_config_property, {uri, Field}}}. -%% @private +?DOC(false). -spec parse_setting_uri_https(Setting :: term(), Field :: atom()) -> {ok, uri_string:uri_string()} | {error, error()}. parse_setting_uri_https(Setting, Field) when is_binary(Setting) -> @@ -95,13 +95,13 @@ parse_setting_uri_https(Setting, Field) when is_binary(Setting) -> parse_setting_uri_https(_Setting, Field) -> {error, {invalid_config_property, {uri_https, Field}}}. -%% @private +?DOC(false). -spec parse_setting_uri_map(Setting :: term(), Field :: atom()) -> {ok, #{binary() => uri_string:uri_string()}} | {error, error()}. parse_setting_uri_map(Setting, Field) -> do_parse_setting_uri_map(Setting, Field, fun parse_setting_uri/2). -%% @private +?DOC(false). -spec parse_setting_uri_https_map(Setting :: term(), Field :: atom()) -> {ok, #{binary() => uri_string:uri_string()}} | {error, error()}. parse_setting_uri_https_map(Setting, Field) -> @@ -137,7 +137,7 @@ do_parse_setting_uri_map(#{} = Setting, Field, Parser) -> do_parse_setting_uri_map(_Setting, Field, _Parser) -> {error, {invalid_config_property, {uri_map, Field}}}. -%% @private +?DOC(false). -spec parse_setting_binary(Setting :: term(), Field :: atom()) -> {ok, binary()} | {error, error()}. parse_setting_binary(Setting, _Field) when is_binary(Setting) -> @@ -145,7 +145,7 @@ parse_setting_binary(Setting, _Field) when is_binary(Setting) -> parse_setting_binary(_Setting, Field) -> {error, {invalid_config_property, {binary, Field}}}. -%% @private +?DOC(false). -spec parse_setting_binary_list(Setting :: term(), Field :: atom()) -> {ok, [binary()]} | {error, error()}. parse_setting_binary_list(Setting, Field) when is_list(Setting) -> @@ -158,7 +158,7 @@ parse_setting_binary_list(Setting, Field) when is_list(Setting) -> parse_setting_binary_list(_Setting, Field) -> {error, {invalid_config_property, {list_of_binaries, Field}}}. -%% @private +?DOC(false). -spec parse_setting_number(Setting :: term(), Field :: atom()) -> {ok, integer()} | {error, error()}. parse_setting_number(Setting, _Field) when is_integer(Setting) -> @@ -166,7 +166,7 @@ parse_setting_number(Setting, _Field) when is_integer(Setting) -> parse_setting_number(_Setting, Field) -> {error, {invalid_config_property, {number, Field}}}. -%% @private +?DOC(false). -spec parse_setting_boolean(Setting :: term(), Field :: atom()) -> {ok, boolean()} | {error, error()}. parse_setting_boolean(Setting, _Field) when is_boolean(Setting) -> @@ -174,7 +174,7 @@ parse_setting_boolean(Setting, _Field) when is_boolean(Setting) -> parse_setting_boolean(_Setting, Field) -> {error, {invalid_config_property, {boolean, Field}}}. -%% @private +?DOC(false). -spec parse_setting_list_enum( Setting :: term(), Field :: atom(), diff --git a/src/oidcc_http_util.erl b/src/oidcc_http_util.erl index 964f450..ddf4c7f 100644 --- a/src/oidcc_http_util.erl +++ b/src/oidcc_http_util.erl @@ -1,11 +1,10 @@ -%%%------------------------------------------------------------------- -%% @doc HTTP Client Utilities -%% @end -%%%------------------------------------------------------------------- -module(oidcc_http_util). -feature(maybe_expr, enable). +-include("internal/doc.hrl"). +?MODULEDOC("HTTP Client Utilities"). + -export([basic_auth_header/2]). -export([bearer_auth_header/1]). -export([headers_to_cache_deadline/2]). @@ -15,38 +14,47 @@ http_header/0, error/0, httpc_error/0, query_params/0, telemetry_opts/0, request_opts/0 ]). +?DOC("See `uri_string:compose_query/1`."). +?DOC(#{since => <<"3.0.0">>}). -type query_params() :: [{unicode:chardata(), unicode:chardata() | true}]. -%% See {@link uri_string:compose_query/1} + +?DOC("See `httpc:request/5`."). +?DOC(#{since => <<"3.0.0">>}). -type http_header() :: {Field :: [byte()] | binary(), Value :: iodata()}. -%% See {@link httpc:request/5} + +?DOC(#{since => <<"3.0.0">>}). -type error() :: {http_error, StatusCode :: pos_integer(), HttpBodyResult :: binary() | map()} | {use_dpop_nonce, Nonce :: binary(), HttpBodyResult :: binary() | map()} | invalid_content_type | httpc_error(). + +?DOC("See `httpc:request/5` for additional errors."). +?DOC(#{since => <<"3.0.0">>}). -type httpc_error() :: term(). -%% See {@link httpc:request/5} for additional errors +?DOC(""" +See `httpc:request/5`. + +## Parameters + +* `timeout` - timeout for request +* `ssl` - TLS config +"""). +?DOC(#{since => <<"3.0.0">>}). -type request_opts() :: #{ timeout => timeout(), ssl => [ssl:tls_option()], httpc_profile => atom() | pid() }. -%% See {@link httpc:request/5} -%% -%%

Parameters

-%% -%% +?DOC(#{since => <<"3.0.0">>}). -type telemetry_opts() :: #{ topic := [atom()], extra_meta => map() }. -%% @private +?DOC(false). -spec basic_auth_header(User, Secret) -> http_header() when User :: binary(), Secret :: binary(). @@ -57,12 +65,12 @@ basic_auth_header(User, Secret) -> AuthData = base64:encode(RawAuth), {"authorization", [<<"Basic ">>, AuthData]}. -%% @private +?DOC(false). -spec bearer_auth_header(Token) -> http_header() when Token :: binary(). bearer_auth_header(Token) -> {"authorization", [<<"Bearer ">>, Token]}. -%% @private +?DOC(false). -spec request(Method, Request, TelemetryOpts, RequestOpts) -> {ok, {{json, term()} | {jwt, binary()}, [http_header()]}} | {error, error()} diff --git a/src/oidcc_jwt_util.erl b/src/oidcc_jwt_util.erl index 4223d41..42da9e4 100644 --- a/src/oidcc_jwt_util.erl +++ b/src/oidcc_jwt_util.erl @@ -1,11 +1,10 @@ -%%%------------------------------------------------------------------- -%% @doc JWT Utilities -%% @end -%%%------------------------------------------------------------------- -module(oidcc_jwt_util). -feature(maybe_expr, enable). +-include("internal/doc.hrl"). +?MODULEDOC("JWT Utilities"). + -include_lib("jose/include/jose_jwe.hrl"). -include_lib("jose/include/jose_jwk.hrl"). -include_lib("jose/include/jose_jws.hrl"). @@ -31,9 +30,11 @@ -export_type([error/0]). -export_type([refresh_jwks_for_unknown_kid_fun/0]). +?DOC(#{since => <<"3.0.0">>}). -type refresh_jwks_for_unknown_kid_fun() :: fun((Jwks :: jose_jwk:key(), Kid :: binary()) -> {ok, jose_jwk:key()} | {error, term()}). +?DOC(#{since => <<"3.0.0">>}). -type error() :: no_matching_key | invalid_jwt_token @@ -42,18 +43,19 @@ | {none_alg_used, Jwt :: #jose_jwt{}, Jws :: #jose_jws{}} | not_encrypted. +?DOC(#{since => <<"3.0.0">>}). -type claims() :: #{binary() => term()}. -%% Function to decide if the jwks should be reladed to find a matching key for `Kid' +%% Function to decide if the jwks should be reladed to find a matching key for `Kid` %% -%% A default function is provided in {@link oidcc:retrieve_token/5} -%% and {@link oidcc:retrieve_userinfo/5}. +%% A default function is provided in `oidcc:retrieve_token/5` +%% and `oidcc:retrieve_userinfo/5`. %% %% The default implementation does not implement any rate limiting. - -%% @private +%% %% Checking of jwk sets is a bit wonky because of partial support %% in jose. see: https://github.com/potatosalad/erlang-jose/issues/28 +?DOC(false). -spec verify_signature(Token, AllowAlgorithms, Jwks) -> {ok, {Jwt, Jws}} | {error, error()} @@ -111,7 +113,7 @@ verify_signature(Token, AllowAlgorithms, #jose_jwk{} = Jwks) -> {error, invalid_jwt_token} end. -%% @private +?DOC(false). -spec verify_claims(Claims, ExpClaims) -> ok | {error, {missing_claim, ExpClaim, Claims}} when Claims :: claims(), ExpClaim :: {binary(), term()}, @@ -133,7 +135,7 @@ verify_claims(Claims, ExpClaims) -> {error, {missing_claim, Claim, Claims}} end. -%% @private +?DOC(false). -spec client_secret_oct_keys(AllowedAlgorithms, ClientSecret) -> jose_jwk:key() | none when AllowedAlgorithms :: [binary()] | undefined, ClientSecret :: binary() | unauthenticated. @@ -153,7 +155,7 @@ client_secret_oct_keys(AllowedAlgorithms, ClientSecret) -> none end. -%% @private +?DOC(false). -spec merge_client_secret_oct_keys(Jwks :: jose_jwk:key(), AllowedAlgorithms, ClientSecret) -> jose_jwk:key() when @@ -167,7 +169,7 @@ merge_client_secret_oct_keys(Jwks, AllowedAlgorithms, ClientSecret) -> merge_jwks(Jwks, OctKeys) end. -%% @private +?DOC(false). -spec refresh_jwks_fun(ProviderConfigurationWorkerName) -> refresh_jwks_for_unknown_kid_fun() when @@ -181,7 +183,7 @@ refresh_jwks_fun(ProviderConfigurationWorkerName) -> {ok, oidcc_provider_configuration_worker:get_jwks(ProviderConfigurationWorkerName)} end. -%% @private +?DOC(false). -spec merge_jwks(Left :: jose_jwk:key(), Right :: jose_jwk:key()) -> jose_jwk:key(). merge_jwks(#jose_jwk{keys = {jose_jwk_set, LeftKeys}, fields = LeftFields}, #jose_jwk{ keys = {jose_jwk_set, RightKeys}, fields = RightFields @@ -194,13 +196,13 @@ merge_jwks(#jose_jwk{} = Left, #jose_jwk{keys = {jose_jwk_set, _RightKeys}} = Ri merge_jwks(Left, Right) -> merge_jwks(Left, #jose_jwk{keys = {jose_jwk_set, [Right]}}). -%% @private +?DOC(false). -spec sign(Jwt :: #jose_jwt{}, Jwk :: jose_jwk:key(), SupportedAlgorithms :: [binary()]) -> {ok, binary()} | {error, no_supported_alg_or_key}. sign(Jwt, Jwk, SupportedAlgorithms) -> sign(Jwt, Jwk, SupportedAlgorithms, #{}). -%% @private +?DOC(false). -spec sign( Jwt :: #jose_jwt{}, Jwk :: jose_jwk:key(), SupportedAlgorithms :: [binary()], JwsFields :: map() ) -> @@ -250,7 +252,7 @@ sign(Jwt, Jwk, [Algorithm | RestAlgorithms], JwsFields0) -> _ -> sign(Jwt, Jwk, RestAlgorithms, JwsFields0) end. -%% private +?DOC(false). -spec decrypt_and_verify( Jwt :: binary(), Jwks :: jose_jwk:key(), @@ -379,7 +381,7 @@ verify_decrypted_token(Jwt, SigningAlgs, Jwe, Jwks) -> {error, Reason} end. -%% @private +?DOC(false). -spec encrypt( Jwt :: binary(), Jwk :: jose_jwk:key(), @@ -435,7 +437,7 @@ encrypt(Jwt, Jwk, [Algorithm | _RestAlgorithms] = SupportedAlgorithms, Supported error -> encrypt(Jwt, Jwk, SupportedAlgorithms, SupportedEncValues, RestEncValues) end. -%% @private +?DOC(false). -spec thumbprint(Jwk :: jose_jwk:key()) -> {ok, binary()} | error. thumbprint(Jwk) -> evaluate_for_all_keys(Jwk, fun @@ -445,7 +447,7 @@ thumbprint(Jwk) -> {ok, jose_jwk:thumbprint(Key)} end). -%% @private +?DOC(false). -spec sign_dpop(Jwt :: #jose_jwt{}, Jwk :: jose_jwk:key(), SigningAlgSupported :: [binary()]) -> {ok, binary()} | {error, no_supported_alg_or_key}. sign_dpop(Jwt, Jwk, SigningAlgSupported) -> @@ -459,7 +461,7 @@ sign_dpop(Jwt, Jwk, SigningAlgSupported) -> }) end). -%% @private +?DOC(false). -spec evaluate_for_all_keys(Jwk :: jose_jwk:key(), fun((jose_jwk:key()) -> {ok, Result} | error)) -> {ok, Result} | error when @@ -478,14 +480,14 @@ evaluate_for_all_keys(#jose_jwk{keys = {jose_jwk_set, Keys}}, Callback) -> evaluate_for_all_keys(#jose_jwk{} = Jwk, Callback) -> Callback(Jwk). -%% @private +?DOC(false). -spec verify_not_none_alg(#jose_jws{}) -> ok | {error, none_alg_used}. verify_not_none_alg(#jose_jws{fields = #{<<"alg">> := <<"none">>}}) -> {error, none_alg_used}; verify_not_none_alg(#jose_jws{}) -> ok. -%% @private +?DOC(false). -spec peek_payload(binary()) -> {ok, #jose_jwt{}} | {error, invalid_jwt_token}. peek_payload(Jwt) -> try diff --git a/src/oidcc_logout.erl b/src/oidcc_logout.erl index 882b46a..140a279 100644 --- a/src/oidcc_logout.erl +++ b/src/oidcc_logout.erl @@ -1,12 +1,12 @@ -%%%------------------------------------------------------------------- -%% @doc Logout from the OpenID Provider -%% @end -%% @since 3.0.0 -%%%------------------------------------------------------------------- -module(oidcc_logout). -feature(maybe_expr, enable). + +-include("internal/doc.hrl"). +?MODULEDOC("Logout from the OpenID Provider."). +?MODULEDOC(#{since => <<"3.0.0">>}). + -include("oidcc_client_context.hrl"). -include("oidcc_provider_configuration.hrl"). -include("oidcc_token.hrl"). @@ -16,8 +16,23 @@ -export_type([error/0]). -export_type([initiate_url_opts/0]). +?DOC(#{since => <<"3.0.0">>}). -type error() :: end_session_endpoint_not_supported. +?DOC(""" +Configure Relaying Party initiated Logout URI. + +See https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout. + +## Parameters + +* `logout_hint` - logout_hint to pass to the provider +* `post_logout_redirect_uri` - Post Logout Redirect URI to pass to the provider +* `state` - state to pass to the provider +* `ui_locales` - UI locales to pass to the provider +* `extra_query_params` - extra query params to add to the URI +"""). +?DOC(#{since => <<"3.0.0">>}). -type initiate_url_opts() :: #{ logout_hint => binary(), post_logout_redirect_uri => uri_string:uri_string(), @@ -25,51 +40,37 @@ ui_locales => binary(), extra_query_params => oidcc_http_util:query_params() }. -%% Configure Relaying Party initiated Logout URI -%% -%% See [https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout] -%% -%%

Parameters

-%% -%% - -%% @doc -%% Initiate URI for Relaying Party initiated Logout -%% -%% See [https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout] -%% -%% For a high level interface using {@link oidcc_provider_configuration_worker} -%% see {@link oidcc:initiate_logout_url/4}. -%% -%%

Examples

-%% -%% ``` -%% {ok, ClientContext} = oidcc_client_context:from_configuration_worker( -%% provider_name, -%% <<"client_id">>, -%% unauthenticated -%% ), -%% -%% %% Get `Token` from `oidcc_token` -%% -%% {ok, RedirectUri} = -%% oidcc_logout:initiate_url( -%% Token, -%% ClientContext, -%% #{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 + +?DOC(""" +Initiate URI for Relaying Party initiated Logout. + +See https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout. + +For a high level interface using `m:oidcc_provider_configuration_worker` +see `oidcc:initiate_logout_url/4`. + +## Examples + +```erlang +{ok, ClientContext} = oidcc_client_context:from_configuration_worker( + provider_name, + <<"client_id">>, + unauthenticated +), + +%% Get `Token` from `oidcc_token` + +{ok, RedirectUri} = + oidcc_logout:initiate_url( + Token, + ClientContext, + #{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_url(Token, ClientContext, Opts) -> {ok, uri_string:uri_string()} | {error, error()} when diff --git a/src/oidcc_profile.erl b/src/oidcc_profile.erl index ae9dc51..0eb6462 100644 --- a/src/oidcc_profile.erl +++ b/src/oidcc_profile.erl @@ -1,12 +1,11 @@ -%%%------------------------------------------------------------------- -%% @doc OpenID Profile Utilities -%% @end -%% @since 3.2.0 -%%%------------------------------------------------------------------- -module(oidcc_profile). -feature(maybe_expr, enable). +-include("internal/doc.hrl"). +?MODULEDOC("OpenID Profile Utilities"). +?MODULEDOC(#{since => <<"3.2.0">>}). + -include("oidcc_client_context.hrl"). -include("oidcc_provider_configuration.hrl"). @@ -17,8 +16,11 @@ -export_type([opts_no_profiles/0]). -export_type([error/0]). +?DOC(#{since => <<"3.2.0">>}). -type profile() :: mtls_constrain | fapi2_security_profile | fapi2_message_signing | fapi2_connectid_au. + +?DOC(#{since => <<"3.2.0">>}). -type opts() :: #{ profiles => [profile()], require_pkce => boolean(), @@ -26,15 +28,19 @@ preferred_auth_methods => [oidcc_auth_util:auth_method()], request_opts => oidcc_http_util:request_opts() }. + +?DOC(#{since => <<"3.2.0">>}). -type opts_no_profiles() :: #{ require_pkce => boolean(), trusted_audiences => [binary()] | any, preferred_auth_methods => [oidcc_auth_util:auth_method()], request_opts => oidcc_http_util:request_opts() }. + +?DOC(#{since => <<"3.2.0">>}). -type error() :: {unknown_profile, atom()}. -%% @private +?DOC(false). -spec apply_profiles(ClientContext, opts()) -> {ok, ClientContext, opts_no_profiles()} | {error, error()} when diff --git a/src/oidcc_provider_configuration.erl b/src/oidcc_provider_configuration.erl index 8a833df..b9b3533 100644 --- a/src/oidcc_provider_configuration.erl +++ b/src/oidcc_provider_configuration.erl @@ -1,24 +1,25 @@ -%%%------------------------------------------------------------------- -%% @doc Tooling to load and parse Openid Configuration -%% -%%

Records

-%% -%% To use the record, import the definition: -%% -%% ``` -%% -include_lib(["oidcc/include/oidcc_provider_configuration.hrl"]). -%% ''' -%% -%%

Telemetry

-%% -%% See {@link 'Elixir.Oidcc.ProviderConfiguration'} -%% @end -%% @since 3.0.0 -%%%------------------------------------------------------------------- -module(oidcc_provider_configuration). -feature(maybe_expr, enable). +-include("internal/doc.hrl"). +?MODULEDOC(""" +Tooling to load and parse Openid Configuration. + +## Records + +To use the record, import the definition: + +```erlang +-include_lib(["oidcc/include/oidcc_provider_configuration.hrl"]). +``` + +## Telemetry + +See [`Oidcc.ProviderConfiguration`](`m:'Elixir.Oidcc.ProviderConfiguration'`). +"""). +?MODULEDOC(#{since => <<"3.0.0">>}). + -include("oidcc_provider_configuration.hrl"). -export([decode_configuration/1]). @@ -32,35 +33,48 @@ -export_type([quirks/0]). -export_type([t/0]). +?DOC(""" +Allow Specification Non-compliance. + +## Exceptions + +* `allow_unsafe_http` - Allow unsafe HTTP. Use this for development + providers and **never in production**. +* `document_overrides` - a map to merge with the real OIDD document, + in case the OP left out some values. +"""). +?DOC(#{since => <<"3.1.0">>}). -type quirks() :: #{ allow_unsafe_http => boolean(), document_overrides => map() }. -%% Allow Specification Non-compliance -%% -%%

Exceptions

-%% -%% +?DOC(""" +Configure configuration loading / parsing. + +## Parameters + +* `fallback_expiry` - How long to keep configuration cached if the server doesn't specify expiry. +* `request_opts` - config for HTTP request. +"""). +?DOC(#{since => <<"3.0.0">>}). -type opts() :: #{ fallback_expiry => timeout(), request_opts => oidcc_http_util:request_opts(), quirks => quirks() }. -%% Configure configuration loading / parsing -%% -%%

Parameters

-%% -%% +?DOC(""" +Record containing OpenID and OAuth 2.0 Configuration. + +See: +* https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata +* https://datatracker.ietf.org/doc/html/draft-jones-oauth-discovery-01#section-4.1 +* https://openid.net/specs/openid-connect-rpinitiated-1_0.html#OPMetadata + +All unrecognized fields are stored in `extra_fields`. +"""). +?DOC(#{since => <<"3.0.0">>}). -type t() :: #oidcc_provider_configuration{ issuer :: uri_string:uri_string(), @@ -127,14 +141,8 @@ mtls_endpoint_aliases :: #{binary() => uri_string:uri_string()}, extra_fields :: #{binary() => term()} }. -%% Record containing OpenID and OAuth 2.0 Configuration -%% -%% See [https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata], -%% [https://datatracker.ietf.org/doc/html/draft-jones-oauth-discovery-01#section-4.1] and -%% [https://openid.net/specs/openid-connect-rpinitiated-1_0.html#OPMetadata] -%% -%% All unrecognized fields are stored in `extra_fields'. +?DOC(#{since => <<"3.0.0">>}). -type error() :: invalid_content_type | {issuer_mismatch, Issuer :: binary()} @@ -185,16 +193,17 @@ metadata => <<"#{jwks_uri => uri_string:uri_string()}">> }). -%% @doc Load OpenID Configuration into a {@link oidcc_provider_configuration:t()} record -%% -%%

Examples

-%% -%% ``` -%% {ok, #oidcc_provider_configuration{}} = -%% oidcc_provider_configuration:load_configuration("https://accounts.google.com"). -%% ''' -%% @end -%% @since 3.0.0 +?DOC(""" +Load OpenID Configuration into a `t:oidcc_provider_configuration:t/0` record. + +## Examples + +```erlang +{ok, #oidcc_provider_configuration{}} = + oidcc_provider_configuration:load_configuration("https://accounts.google.com"). +``` +"""). +?DOC(#{since => <<"3.0.0">>}). -spec load_configuration(Issuer, Opts) -> {ok, {Configuration :: t(), Expiry :: pos_integer()}} | {error, error()} when @@ -234,24 +243,25 @@ load_configuration(Issuer0, Opts) -> {error, invalid_content_type} end. -%% @see load_configuration/2 -%% @since 3.1.0 +?DOC("See `load_configuration/2`."). +?DOC(#{since => <<"3.1.0">>}). -spec load_configuration(Issuer) -> {ok, {Configuration :: t(), Expiry :: pos_integer()}} | {error, error()} when Issuer :: uri_string:uri_string(). load_configuration(Issuer) -> load_configuration(Issuer, #{}). -%% @doc Load JWKs into a {@link jose_jwk:key()} record -%% -%%

Examples

-%% -%% ``` -%% {ok, #jose_jwk{}} = -%% oidcc_provider_configuration:load_jwks("https://www.googleapis.com/oauth2/v3/certs"). -%% ''' -%% @end -%% @since 3.0.0 +?DOC(""" +Load JWKs into a `t:jose_jwk:key/0` record. + +## Examples + +```erlang +{ok, #jose_jwk{}} = + oidcc_provider_configuration:load_jwks("https://www.googleapis.com/oauth2/v3/certs"). +``` +"""). +?DOC(#{since => <<"3.0.0">>}). -spec load_jwks(JwksUri, Opts) -> {ok, {Jwks :: jose_jwk:key(), Expiry :: pos_integer()}} | {error, term()} when @@ -274,21 +284,22 @@ load_jwks(JwksUri, Opts) -> {ok, {{_Format, _Body}, _Headers}} -> {error, invalid_content_type} end. -%% @doc Decode JSON into a {@link oidcc_provider_configuration:t()} record -%% -%%

Examples

-%% -%% ``` -%% {ok, {{"HTTP/1.1",200,"OK"}, _Headers, Body}} = -%% httpc:request("https://accounts.google.com/.well-known/openid-configuration"), -%% -%% {ok, DecodedJson} = your_json_lib:decode(Body), -%% -%% {ok, #oidcc_provider_configuration{}} = -%% oidcc_provider_configuration:decode_configuration(DecodedJson). -%% ''' -%% @end -%% @since 3.1.0 +?DOC(""" +Decode JSON into a `t:oidcc_provider_configuration:t/0` record. + +## Examples + +```erlang +{ok, {{"HTTP/1.1",200,"OK"}, _Headers, Body}} = + httpc:request("https://accounts.google.com/.well-known/openid-configuration"), + +{ok, DecodedJson} = your_json_lib:decode(Body), + +{ok, #oidcc_provider_configuration{}} = + oidcc_provider_configuration:decode_configuration(DecodedJson). +``` +"""). +?DOC(#{since => <<"3.1.0">>}). -spec decode_configuration(Configuration, Opts) -> {ok, t()} | {error, error()} when Configuration :: map(), Opts :: opts(). decode_configuration(Configuration0, Opts) -> @@ -567,8 +578,8 @@ decode_configuration(Configuration0, Opts) -> }} end. -%% @see decode_configuration/2 -%% @since 3.0.0 +?DOC("See `decode_configuration/2`."). +?DOC(#{since => <<"3.0.0">>}). -spec decode_configuration(Configuration) -> {ok, t()} | {error, error()} when Configuration :: map(). decode_configuration(Configuration) -> decode_configuration(Configuration, #{}). diff --git a/src/oidcc_provider_configuration_worker.erl b/src/oidcc_provider_configuration_worker.erl index 9e6d459..0c046ec 100644 --- a/src/oidcc_provider_configuration_worker.erl +++ b/src/oidcc_provider_configuration_worker.erl @@ -1,19 +1,20 @@ -%%%------------------------------------------------------------------- -%% @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). +-include("internal/doc.hrl"). +?MODULEDOC(""" +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. +"""). +?MODULEDOC(#{since => <<"3.0.0">>}). + -behaviour(gen_server). -include("oidcc_provider_configuration.hrl"). @@ -34,6 +35,20 @@ -export_type([opts/0]). +?DOC(""" +Configuration Options + +* `name` - The gen_server name of the provider. +* `issuer` - The issuer URI. +* `provider_configuration_opts` - Options for the provider configuration + fetching. +* `backoff_min` - The minimum backoff interval in ms (default: `1_000`). +* `backoff_max` - The maximum backoff interval in ms (default: `30_000`). +* `backoff_type` - The backoff strategy, `stop` for no backoff and to stop, + `exponential` for exponential, `random` for random, and `random_exponential` + for random exponential (default: `stop`). +"""). +?DOC(#{since => <<"3.0.0">>}). -type opts() :: #{ name => gen_server:server_name(), issuer := uri_string:uri_string(), @@ -42,21 +57,6 @@ backoff_max => oidcc_backoff:max(), backoff_type => oidcc_backoff:type() }. -%% Configuration Options -%% -%% -record(state, { provider_configuration = undefined :: #oidcc_provider_configuration{} | undefined, @@ -74,40 +74,40 @@ -type state() :: #state{}. -%% @doc Start Configuration Provider -%% -%%

Examples

-%% -%% ``` -%% {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 +?DOC(""" +Start Configuration Provider. + +## Examples + +```erlang +{ok, Pid} = + oidcc_provider_configuration_worker:start_link(#{ + issuer => <<"https://accounts.google.com">>, + name => {local, google_config_provider} + }). +``` + +```erlang +%% ... +-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}}. +``` +"""). +?DOC(#{since => <<"3.0.0">>}). -spec start_link(Opts :: opts()) -> gen_server:start_ret(). start_link(Opts) -> case maps:get(name, Opts, undefined) of @@ -117,7 +117,7 @@ start_link(Opts) -> gen_server:start_link(Name, ?MODULE, Opts, []) end. -%% @private +?DOC(false). init(Opts) -> EtsTable = register_ets_table(Opts), maybe @@ -135,7 +135,7 @@ init(Opts) -> {continue, load_configuration}} end. -%% @private +?DOC(false). handle_call( get_provider_configuration, _From, #state{provider_configuration = Configuration} = State ) -> @@ -143,7 +143,7 @@ handle_call( handle_call(get_jwks, _From, #state{jwks = Jwks} = State) -> {reply, Jwks, State}. -%% @private +?DOC(false). handle_cast(refresh_configuration, State) -> {noreply, State, {continue, load_configuration}}; handle_cast(refresh_jwks, State) -> @@ -163,7 +163,7 @@ handle_cast({refresh_jwks_for_unknown_kid, Kid}, #state{jwks = Jwks} = State) -> {noreply, State} end. -%% @private +?DOC(false). handle_continue( load_configuration, #state{ @@ -226,7 +226,7 @@ handle_continue( {error, Reason} -> handle_backoff_retry(jwks_load_failed, Reason, State) end. -%% @private +?DOC(false). handle_info(backoff_retry, State) -> {noreply, State, {continue, load_configuration}}; handle_info(configuration_expired, State) -> @@ -234,33 +234,34 @@ handle_info(configuration_expired, State) -> handle_info(jwks_expired, State) -> {noreply, State#state{jwks_refresh_timer = undefined}, {continue, load_jwks}}. -%% @doc Get Configuration +?DOC("Get Configuration."). -spec get_provider_configuration(Name :: gen_server:server_ref()) -> oidcc_provider_configuration:t() | undefined. get_provider_configuration(Name) -> lookup_in_ets_or_call(Name, provider_configuration, get_provider_configuration). -%% @doc Get Parsed Jwks +?DOC("Get Parsed Jwks."). -spec get_jwks(Name :: gen_server:server_ref()) -> jose_jwk:key() | undefined. get_jwks(Name) -> lookup_in_ets_or_call(Name, jwks, get_jwks). -%% @doc Refresh Configuration -%% -%%

Examples

-%% -%% ``` -%% {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 +?DOC(""" +Refresh Configuration. + +## Examples + +```erlang +{ok, Pid} = + oidcc_provider_configuration_worker:start_link(#{ + issuer => <<"https://accounts.google.com">> + }). + +%% Later + +oidcc_provider_configuration_worker:refresh_configuration(Pid). +``` +"""). +?DOC(#{since => <<"3.0.0">>}). -spec refresh_configuration(Name :: gen_server:server_ref()) -> ok. refresh_configuration(Name) -> refresh_configuration(Name, true). @@ -273,22 +274,23 @@ refresh_configuration(Name, true) -> gen_server:call(Name, get_provider_configuration), ok. -%% @doc Refresh JWKs -%% -%%

Examples

-%% -%% ``` -%% {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 +?DOC(""" +Refresh JWKs. + +## Examples + +```erlang +{ok, Pid} = + oidcc_provider_configuration_worker:start_link(#{ + issuer => <<"https://accounts.google.com">> + }). + +%% Later + +oidcc_provider_configuration_worker:refresh_jwks(Pid). +``` +"""). +?DOC(#{since => <<"3.0.0">>}). -spec refresh_jwks(Name :: gen_server:server_ref()) -> ok. refresh_jwks(Name) -> refresh_jwks(Name, true). @@ -300,20 +302,21 @@ refresh_jwks(Name, true) -> gen_server:call(Name, get_jwks), ok. -%% @doc Refresh JWKs if the provided `Kid' is not matching any currently loaded keys -%% -%%

Examples

-%% -%% ``` -%% {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 +?DOC(""" +Refresh JWKs if the provided `Kid` is not matching any currently loaded keys. + +## Examples + +```erlang +{ok, Pid} = + oidcc_provider_configuration_worker:start_link(#{ + issuer => <<"https://accounts.google.com">> + }). + +oidcc_provider_configuration_worker:refresh_jwks_for_unknown_kid(Pid, <<"kid">>). +``` +"""). +?DOC(#{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) -> @@ -339,8 +342,10 @@ get_issuer(Opts) -> {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 +?DOC(""" +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; diff --git a/src/oidcc_scope.erl b/src/oidcc_scope.erl index d5e546d..749093c 100644 --- a/src/oidcc_scope.erl +++ b/src/oidcc_scope.erl @@ -1,12 +1,11 @@ -%%%------------------------------------------------------------------- -%% @doc OpenID Scope Utilities -%% @end -%% @since 3.0.0 -%%%------------------------------------------------------------------- -module(oidcc_scope). -feature(maybe_expr, enable). +-include("internal/doc.hrl"). +?MODULEDOC("OpenID Scope Utilities"). +?MODULEDOC(#{since => <<"3.0.0">>}). + -export([parse/1]). -export([query_append_scope/2]). -export([scopes_to_bin/1]). @@ -14,20 +13,23 @@ -export_type([scopes/0]). -export_type([t/0]). +?DOC(#{since => <<"3.0.0">>}). -type scopes() :: [nonempty_binary() | atom() | nonempty_string()]. +?DOC(#{since => <<"3.0.0">>}). -type t() :: binary(). -%% @doc Compose {@link scopes()} into {@link t()} -%% -%%

Examples

-%% -%% ``` -%% <<"openid profile email">> = oidcc_scope:scopes_to_bin( -%% [<<"openid">>, profile, "email"]). -%% ''' -%% @end -%% @since 3.0.0 +?DOC(""" +Compose `t:scopes/0` into `t:t/0`. + +## Examples + +```erlang +<<"openid profile email">> = oidcc_scope:scopes_to_bin( + [<<"openid">>, profile, "email"]). +``` +"""). +?DOC(#{since => <<"3.0.0">>}). -spec scopes_to_bin(Scopes :: scopes()) -> t(). scopes_to_bin(Scopes) -> NormalizedScopes = @@ -45,7 +47,7 @@ scopes_to_bin(Scopes) -> SeparatedScopes = lists:join(<<" ">>, NormalizedScopes), list_to_binary(SeparatedScopes). -%% @private +?DOC(false). -spec query_append_scope(Scope, QueryList) -> QueryList when Scope :: t() | scopes(), QueryList :: [{unicode:chardata(), unicode:chardata() | true}]. @@ -56,15 +58,16 @@ query_append_scope(Scope, QueryList) when is_binary(Scope) -> query_append_scope(Scopes, QueryList) when is_list(Scopes) -> query_append_scope(scopes_to_bin(Scopes), QueryList). -%% @doc Parse {@link t()} into {@link scopes()} -%% -%%

Examples

-%% -%% ``` -%% [<<"openid">>, <<"profile">>] = oidcc_scope:parse(<<"openid profile">>). -%% ''' -%% @end -%% @since 3.0.0 +?DOC(""" +Parse `t:t/0` into `t:scopes/0`. + +## Examples + +```erlang +[<<"openid">>, <<"profile">>] = oidcc_scope:parse(<<"openid profile">>). +``` +"""). +?DOC(#{since => <<"3.0.0">>}). -spec parse(Scope :: t()) -> scopes(). parse(Scope) -> binary:split(Scope, [<<" ">>], [trim_all, global]). diff --git a/src/oidcc_token.erl b/src/oidcc_token.erl index 2d4e566..3e9fcdc 100644 --- a/src/oidcc_token.erl +++ b/src/oidcc_token.erl @@ -1,24 +1,25 @@ -%%%------------------------------------------------------------------- -%% @doc Facilitate OpenID Code/Token Exchanges -%% -%%

Records

-%% -%% To use the records, import the definition: -%% -%% ``` -%% -include_lib(["oidcc/include/oidcc_token.hrl"]). -%% ''' -%% -%%

Telemetry

-%% -%% See {@link 'Elixir.Oidcc.Token'} -%% @end -%% @since 3.0.0 -%%%------------------------------------------------------------------- -module(oidcc_token). -feature(maybe_expr, enable). +-include("internal/doc.hrl"). +?MODULEDOC(""" +Facilitate OpenID Code/Token Exchanges. + +## Records + +To use the records, import the definition: + +```erlang +-include_lib(["oidcc/include/oidcc_token.hrl"]). +``` + +## Telemetry + +See [`Oidcc.Token`](`m:'Elixir.Oidcc.Token'`). +"""). +?MODULEDOC(#{since => <<"3.0.0">>}). + -include("oidcc_client_context.hrl"). -include("oidcc_provider_configuration.hrl"). -include("oidcc_token.hrl"). @@ -52,37 +53,51 @@ -export_type([validate_jwt_opts/0]). -export_type([t/0]). +?DOC(""" +ID Token Wrapper. + +## Fields + +* `token` - The retrieved token. +* `claims` - Unpacked claims of the verified token. +"""). +?DOC(#{since => <<"3.0.0">>}). -type id() :: #oidcc_token_id{token :: binary(), claims :: oidcc_jwt_util:claims()}. -%% ID Token Wrapper -%% -%%

Fields

-%% -%% +?DOC(""" +Access Token Wrapper. + +## Fields + +* `token` - The retrieved token. +* `expires` - Number of seconds the token is valid. +"""). +?DOC(#{since => <<"3.0.0">>}). -type access() :: #oidcc_token_access{token :: binary(), expires :: pos_integer() | undefined, type :: binary()}. -%% Access Token Wrapper -%% -%%

Fields

-%% -%% +?DOC(""" +Refresh Token Wrapper. + +## Fields + +* `token` - The retrieved token. +"""). +?DOC(#{since => <<"3.0.0">>}). -type refresh() :: #oidcc_token_refresh{token :: binary()}. -%% Refresh Token Wrapper -%% -%%

Fields

-%% -%% +?DOC(""" +Token Response Wrapper. + +## Fields + +* `id` - `t:id/0`. +* `access` - `t:access/0`. +* `refresh` - `t:refresh/0`. +* `scope` - `t:oidcc_scope:scopes/0`. +"""). +?DOC(#{since => <<"3.0.0">>}). -type t() :: #oidcc_token{ id :: oidcc_token:id() | none, @@ -90,17 +105,28 @@ refresh :: oidcc_token:refresh() | none, scope :: oidcc_scope:scopes() }. -%% Token Response Wrapper -%% -%%

Fields

-%% -%% +?DOC(""" +Options for retrieving a token. + +See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3. + +## Fields + +* `pkce_verifier` - PKCE verifier (random string previously given to + `m:oidcc_authorization`), see + https://datatracker.ietf.org/doc/html/rfc7636#section-4.1. +* `require_pkce` - whether to require PKCE when getting the token. +* `nonce` - Nonce to check. +* `scope` - Scope to store with the token. +* `refresh_jwks` - How to handle tokens with an unknown `kid`. + See `t:oidcc_jwt_util:refresh_jwks_for_unknown_kid_fun/0`. +* `redirect_uri` - Redirect URI given to `oidcc_authorization:create_redirect_url/2`. +* `dpop_nonce` - if using DPoP, the `nonce` value to use in the proof claim. +* `trusted_audiences` - if present, a list of additional audience values to + accept. Defaults to `any` which allows any additional values. +"""). +?DOC(#{since => <<"3.0.0">>}). -type retrieve_opts() :: #{ pkce_verifier => binary(), @@ -116,28 +142,9 @@ dpop_nonce => binary(), trusted_audiences => [binary()] | any }. -%% Options for retrieving a token -%% -%% See [https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3] -%% -%%

Fields

-%% -%% +?DOC("See `t:refresh_opts_no_sub/0`."). +?DOC(#{since => <<"3.0.0">>}). -type refresh_opts_no_sub() :: #{ scope => oidcc_scope:scopes(), @@ -146,8 +153,9 @@ url_extension => oidcc_http_util:query_params(), body_extension => oidcc_http_util:query_params() }. -%% See {@link refresh_opts_no_sub()} + +?DOC(#{since => <<"3.0.0">>}). -type refresh_opts() :: #{ scope => oidcc_scope:scopes(), @@ -158,23 +166,26 @@ body_extension => oidcc_http_util:query_params() }. +?DOC(""" +Options for refreshing a token. + +See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3. + +## Fields + +* `scope` - Scope to store with the token. +* `refresh_jwks` - How to handle tokens with an unknown `kid`. + See `t:oidcc_jwt_util:refresh_jwks_for_unknown_kid_fun/0`. +* `expected_subject` - `sub` of the original token. +"""). +?DOC(#{since => <<"3.2.0">>}). -type validate_jarm_opts() :: #{ trusted_audiences => [binary()] | any }. -%% Options for refreshing a token -%% -%% See [https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3] -%% -%%

Fields

-%% -%% + +?DOC(#{since => <<"3.0.0">>}). -type jwt_profile_opts() :: #{ scope => oidcc_scope:scopes(), refresh_jwks => oidcc_jwt_util:refresh_jwks_for_unknown_kid_fun(), @@ -184,6 +195,7 @@ body_extension => oidcc_http_util:query_params() }. +?DOC(#{since => <<"3.0.0">>}). -type client_credentials_opts() :: #{ scope => oidcc_scope:scopes(), refresh_jwks => oidcc_jwt_util:refresh_jwks_for_unknown_kid_fun(), @@ -192,10 +204,12 @@ body_extension => oidcc_http_util:query_params() }. +?DOC(#{since => <<"3.0.0">>}). -type authorization_headers_opts() :: #{ dpop_nonce => binary() }. +?DOC(#{since => <<"3.2.0">>}). -type validate_jwt_opts() :: #{ signing_algs => [binary()] | undefined, @@ -203,6 +217,7 @@ encryption_encs => [binary()] | undefined }. +?DOC(#{since => <<"3.0.0">>}). -type error() :: {missing_claim, MissingClaim :: binary(), Claims :: oidcc_jwt_util:claims()} | pkce_verifier_required @@ -305,32 +320,32 @@ metadata => <<"#{issuer => uri_string:uri_string(), client_id => binary()}">> }). -%% @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 -%% -%% For a high level interface using {@link oidcc_provider_configuration_worker} -%% see {@link oidcc:retrieve_token/5}. -%% -%%

Examples

-%% -%% ``` -%% {ok, ClientContext} = -%% oidcc_client_context:from_configuration_worker(provider_name, -%% <<"client_id">>, -%% <<"client_secret">>), -%% -%% %% Get AuthCode from Redirect -%% -%% {ok, #oidcc_token{}} = -%% oidcc:retrieve(AuthCode, ClientContext, #{ -%% redirect_uri => <<"https://example.com/callback">>}). -%% ''' -%% @end -%% @since 3.0.0 +?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. + +For a high level interface using `m:oidcc_provider_configuration_worker` +see `oidcc:retrieve_token/5`. + +## Examples + +```erlang +{ok, ClientContext} = + oidcc_client_context:from_configuration_worker(provider_name, + <<"client_id">>, + <<"client_secret">>), + +%% Get AuthCode from Redirect + +{ok, #oidcc_token{}} = + oidcc:retrieve(AuthCode, ClientContext, #{ + redirect_uri => <<"https://example.com/callback">>}). +``` +"""). +?DOC(#{since => <<"3.0.0">>}). -spec retrieve(AuthCode, ClientContext, Opts) -> {ok, t()} | {error, error()} when @@ -371,30 +386,30 @@ retrieve(AuthCode, ClientContext, Opts) -> {error, {grant_type_not_supported, authorization_code}} end. -%% @doc -%% Validate the JARM response, returning the valid claims as a map. -%% -%% The response was sent to the local endpoint by the OpenId Connect provider, -%% using redirects -%% -%%

Examples

-%% -%% ``` -%% {ok, ClientContext} = -%% oidcc_client_context:from_configuration_worker(provider_name, -%% <<"client_id">>, -%% <<"client_secret">>), -%% -%% %% Get Response from Redirect -%% -%% {ok, #{<<"code">> := AuthCode}} = -%% oidcc:validate_jarm(Response, ClientContext, #{}), -%% -%% {ok, #oidcc_token{}} = oidcc:retrieve(AuthCode, ClientContext, -%% #{redirect_uri => <<"https://redirect.example/">>}. -%% ''' -%% @end -%% @since 3.2.0 +?DOC(""" +Validate the JARM response, returning the valid claims as a map. + +The response was sent to the local endpoint by the OpenId Connect provider, +using redirects. + +## Examples + +```erlang +{ok, ClientContext} = + oidcc_client_context:from_configuration_worker(provider_name, + <<"client_id">>, + <<"client_secret">>), + +%% Get Response from Redirect + +{ok, #{<<"code">> := AuthCode}} = + oidcc:validate_jarm(Response, ClientContext, #{}), + +{ok, #oidcc_token{}} = oidcc:retrieve(AuthCode, ClientContext, + #{redirect_uri => <<"https://redirect.example/">>}). +``` +"""). +?DOC(#{since => <<"3.2.0">>}). -spec validate_jarm(Response, ClientContext, Opts) -> {ok, oidcc_jwt_util:claims()} | {error, error()} when @@ -449,34 +464,35 @@ validate_jarm(Response, ClientContext, Opts) -> {ok, Claims} end. -%% @doc Refresh Token -%% -%% For a high level interface using {@link oidcc_provider_configuration_worker} -%% see {@link oidcc:refresh_token/5}. -%% -%%

Examples

-%% -%% ``` -%% {ok, ClientContext} = -%% oidcc_client_context:from_configuration_worker(provider_name, -%% <<"client_id">>, -%% <<"client_secret">>), -%% -%% %% Get AuthCode from Redirect -%% -%% {ok, Token} = -%% oidcc_token:retrieve(AuthCode, ClientContext, #{ -%% redirect_uri => <<"https://example.com/callback">>}). -%% -%% %% Later -%% -%% {ok, #oidcc_token{}} = -%% oidcc_token:refresh(Token, -%% ClientContext, -%% #{expected_subject => <<"sub_from_initial_id_token>>}). -%% ''' -%% @end -%% @since 3.0.0 +?DOC(""" +Refresh Token + +For a high level interface using `m:oidcc_provider_configuration_worker` +see `oidcc:refresh_token/5`. + +## Examples + +```erlang +{ok, ClientContext} = + oidcc_client_context:from_configuration_worker(provider_name, + <<"client_id">>, + <<"client_secret">>), + +%% Get AuthCode from Redirect + +{ok, Token} = + oidcc_token:retrieve(AuthCode, ClientContext, #{ + redirect_uri => <<"https://example.com/callback">>}). + +%% Later + +{ok, #oidcc_token{}} = + oidcc_token:refresh(Token, + ClientContext, + #{expected_subject => <<"sub_from_initial_id_token">>}). +``` +"""). +?DOC(#{since => <<"3.0.0">>}). -spec refresh (RefreshToken, ClientContext, Opts) -> {ok, t()} | {error, error()} @@ -536,34 +552,35 @@ refresh(RefreshToken, ClientContext, Opts) -> {error, {grant_type_not_supported, refresh_token}} end. -%% @doc Retrieve JSON Web Token (JWT) Profile Token -%% -%% See [https://datatracker.ietf.org/doc/html/rfc7523#section-4] -%% -%% For a high level interface using {@link oidcc_provider_configuration_worker} -%% see {@link oidcc:jwt_profile_token/6}. -%% -%%

Examples

-%% -%% ``` -%% {ok, ClientContext} = -%% oidcc_client_context:from_configuration_worker(provider_name, -%% <<"client_id">>, -%% <<"client_secret">>), -%% -%% {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">>, -%% ClientContext, -%% Key, -%% #{scope => [<<"scope">>], -%% kid => maps:get(<<"keyId">>, KeyMap)}). -%% ''' -%% @end -%% @since 3.0.0 +?DOC(""" +Retrieve JSON Web Token (JWT) Profile Token + +See [https://datatracker.ietf.org/doc/html/rfc7523#section-4] + +For a high level interface using {@link oidcc_provider_configuration_worker} +see {@link oidcc:jwt_profile_token/6}. + +## Examples + +```erlang +{ok, ClientContext} = + oidcc_client_context:from_configuration_worker(provider_name, + <<"client_id">>, + <<"client_secret">>), + +{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">>, + ClientContext, + Key, + #{scope => [<<"scope">>], + kid => maps:get(<<"keyId">>, KeyMap)}). +``` +"""). +?DOC(#{since => <<"3.0.0">>}). -spec jwt_profile(Subject, ClientContext, Jwk, Opts) -> {ok, t()} | {error, error()} when Subject :: binary(), ClientContext :: oidcc_client_context:t(), @@ -935,27 +952,26 @@ validate_id_token(IdToken, ClientContext, Opts) when is_map(Opts) -> end end. -%% @doc Validate JWT -%% -%% Validates a generic JWT (such as an access token) from the given provider. -%% Useful if the issuer is shared between multiple applications, and the access token -%% generated for a user at one client is used to validate their access at another client. -%% -%%

Examples

-%% -%% ``` -%% {ok, ClientContext} = -%% oidcc_client_context:from_configuration_worker(provider_name, -%% <<"client_id">>, -%% <<"client_secret">>), -%% -%% %% Get Jwt from Authorization header -%% -%% {ok, Claims} = -%% oidcc:validate_jwt(Jwt, ClientContext, Opts). -%% ''' -%% @end -%% @since 3.2.0 +?DOC(""" +Validate JWT + +Validates a generic JWT (such as an access token) from the given provider. +Useful if the issuer is shared between multiple applications, and the access token +generated for a user at one client is used to validate their access at another client. + +## Examples + +```erlang +{ok, ClientContext} = + oidcc_client_context:from_configuration_worker(provider_name, + <<"client_id">>, + <<"client_secret">>), +%% Get Jwt from Authorization header +{ok, Claims} = + oidcc:validate_jwt(Jwt, ClientContext, Opts). +``` +"""). +?DOC(#{since => <<"3.2.0">>}). -spec validate_jwt(Jwt, ClientContext, Opts) -> {ok, Claims} | {error, error()} when @@ -1003,26 +1019,25 @@ validate_jwt(Jwt, ClientContext, Opts) when is_map(Opts) -> {ok, Claims} end. -%% @doc Authorization headers -%% -%% Generate a map of authorization headers to use when using the given -%% access token to access an API endpoint. -%% -%%

Examples

-%% -%% ``` -%% {ok, ClientContext} = -%% oidcc_client_context:from_configuration_worker(provider_name, -%% <<"client_id">>, -%% <<"client_secret">>), -%% -%% %% Get Access Token record from somewhere -%% -%% Headers = -%% oidcc:authorization_headers(AccessTokenRecord, :get, Url, ClientContext). -%% ''' -%% @end -%% @since 3.2.0 +?DOC(""" +Authorization headers + +Generate a map of authorization headers to use when using the given +access token to access an API endpoint. + +## Examples + +```erlang +{ok, ClientContext} = + oidcc_client_context:from_configuration_worker(provider_name, + <<"client_id">>, + <<"client_secret">>), +%% Get Access Token record from somewhere +Headers = + oidcc:authorization_headers(AccessTokenRecord, :get, Url, ClientContext). +``` +"""). +?DOC(#{since => "3.2.0"}). -spec authorization_headers(AccessTokenRecord, Method, Endpoint, ClientContext) -> HeaderMap when AccessTokenRecord :: access(), Method :: post | get, diff --git a/src/oidcc_token_introspection.erl b/src/oidcc_token_introspection.erl index ac0a1ab..e259d7a 100644 --- a/src/oidcc_token_introspection.erl +++ b/src/oidcc_token_introspection.erl @@ -1,26 +1,27 @@ -%%%------------------------------------------------------------------- -%% @doc OAuth Token Introspection -%% -%% See [https://datatracker.ietf.org/doc/html/rfc7662] -%% -%%

Records

-%% -%% To use the records, import the definition: -%% -%% ``` -%% -include_lib(["oidcc/include/oidcc_token_introspection.hrl"]). -%% ''' -%% -%%

Telemetry

-%% -%% See {@link 'Elixir.Oidcc.TokenIntrospection'} -%% @end -%% @since 3.0.0 -%%%------------------------------------------------------------------- -module(oidcc_token_introspection). -feature(maybe_expr, enable). +-include("internal/doc.hrl"). +?MODULEDOC(""" +OAuth Token Introspection. + +See https://datatracker.ietf.org/doc/html/rfc7662. + +## Records + +To use the records, import the definition: + +```erlang +-include_lib(["oidcc/include/oidcc_token_introspection.hrl"]). +``` + +## Telemetry + +See [`Oidcc.TokenIntrospection`](`m:'Elixir.Oidcc.TokenIntrospection'`). +"""). +?MODULEDOC(#{since => <<"3.0.0">>}). + -include("oidcc_client_context.hrl"). -include("oidcc_provider_configuration.hrl"). -include("oidcc_token.hrl"). @@ -32,6 +33,12 @@ -export_type([opts/0]). -export_type([t/0]). +?DOC(""" +Introspection Result. + +See https://datatracker.ietf.org/doc/html/rfc7662#section-2.2. +"""). +?DOC(#{since => <<"3.0.0">>}). -type t() :: #oidcc_token_introspection{ active :: boolean(), client_id :: binary(), @@ -39,16 +46,16 @@ scope :: oidcc_scope:scopes(), username :: binary() }. -%% Introspection Result -%% -%% See [https://datatracker.ietf.org/doc/html/rfc7662#section-2.2] + +?DOC(#{since => <<"3.0.0">>}). -type opts() :: #{ preferred_auth_methods => [oidcc_auth_util:auth_method(), ...], request_opts => oidcc_http_util:request_opts(), dpop_nonce => binary() }. +?DOC(#{since => <<"3.0.0">>}). -type error() :: client_id_mismatch | introspection_not_supported | oidcc_http_util:error(). -telemetry_event(#{ @@ -72,27 +79,27 @@ metadata => <<"#{issuer => uri_string:uri_string(), client_id => binary()}">> }). -%% @doc -%% Introspect the given access token -%% -%% For a high level interface using {@link oidcc_provider_configuration_worker} -%% see {@link oidcc:introspect_token/5}. -%% -%%

Examples

-%% -%% ``` -%% {ok, ClientContext} = -%% oidcc_client_context:from_configuration_worker(provider_name, -%% <<"client_id">>, -%% <<"client_secret">>), -%% -%% %% Get AccessToken -%% -%% {ok, #oidcc_token_introspection{active = True}} = -%% oidcc_token_introspection:introspect(AccessToken, ClientContext, #{}). -%% ''' -%% @end -%% @since 3.0.0 +?DOC(""" +Introspect the given access token. + +For a high level interface using `m:oidcc_provider_configuration_worker` +see `oidcc:introspect_token/5`. + +## Examples + +```erlang +{ok, ClientContext} = + oidcc_client_context:from_configuration_worker(provider_name, + <<"client_id">>, + <<"client_secret">>), + +%% Get AccessToken + +{ok, #oidcc_token_introspection{active = True}} = + oidcc_token_introspection:introspect(AccessToken, ClientContext, #{}). +``` +"""). +?DOC(#{since => <<"3.0.0">>}). -spec introspect(Token, ClientContext, Opts) -> {ok, t()} | {error, error()} diff --git a/src/oidcc_userinfo.erl b/src/oidcc_userinfo.erl index 96fb036..157ea85 100644 --- a/src/oidcc_userinfo.erl +++ b/src/oidcc_userinfo.erl @@ -1,18 +1,19 @@ -%%%------------------------------------------------------------------- -%% @doc OpenID Connect Userinfo -%% -%% See [https://openid.net/specs/openid-connect-core-1_0.html#UserInfo] -%% -%%

Telemetry

-%% -%% See {@link 'Elixir.Oidcc.Userinfo'} -%% @end -%% @since 3.0.0 -%%%------------------------------------------------------------------- -module(oidcc_userinfo). -feature(maybe_expr, enable). +-include("internal/doc.hrl"). +?MODULEDOC(""" +OpenID Connect Userinfo + +See https://openid.net/specs/openid-connect-core-1_0.html#UserInfo + +## Telemetry + +See [`Oidcc.Userinfo`](`m:'Elixir.Oidcc.Userinfo'`). +"""). +?MODULEDOC(#{since => <<"3.0.0">>}). + -include("oidcc_client_context.hrl"). -include("oidcc_provider_configuration.hrl"). -include("oidcc_token.hrl"). @@ -28,34 +29,37 @@ -export_type([retrieve_opts/0]). -export_type([retrieve_opts_no_sub/0]). +?DOC("See `t:retrieve_opts/0`."). +?DOC(#{since => <<"3.0.0">>}). -type retrieve_opts_no_sub() :: #{ refresh_jwks => oidcc_jwt_util:refresh_jwks_for_unknown_kid_fun(), dpop_nonce => binary() }. -%% See {@link retrieve_opts()} +?DOC(""" +Configure userinfo request + +See https://openid.net/specs/openid-connect-core-1_0.html#UserInfoRequest + +## Parameters + +* `refresh_jwks` - How to handle tokens with an unknown `kid`. + See `t:oidcc_jwt_util:refresh_jwks_for_unknown_kid_fun/0` +* `expected_subject` - expected subject for the userinfo + (`sub` from id token) +* `dpop_nonce` - if using DPoP, the `nonce` value to use in the + proof claim +"""). +?DOC(#{since => <<"3.0.0">>}). -type retrieve_opts() :: #{ refresh_jwks => oidcc_jwt_util:refresh_jwks_for_unknown_kid_fun(), expected_subject => binary() | any, dpop_nonce => binary() }. -%% Configure userinfo request -%% -%% See [https://openid.net/specs/openid-connect-core-1_0.html#UserInfoRequest] -%% -%%

Parameters

-%% -%% +?DOC(#{since => <<"3.0.0">>}). -type error() :: {distributed_claim_not_found, {ClaimSource :: binary(), ClaimName :: binary()}} | no_access_token @@ -85,27 +89,27 @@ metadata => <<"#{issuer => uri_string:uri_string(), client_id => binary()}">> }). -%% @doc -%% Load userinfo for the given token -%% -%% For a high level interface using {@link oidcc_provider_configuration_worker} -%% see {@link oidcc:retrieve_userinfo/5}. -%% -%%

Examples

-%% -%% ``` -%% {ok, ClientContext} = -%% oidcc_client_context:from_configuration_worker(provider_name, -%% <<"client_id">>, -%% <<"client_secret">>), -%% -%% %% Get Token -%% -%% {ok, #{<<"sub">> => Sub}} = -%% oidcc_userinfo:retrieve(Token, ClientContext, #{}). -%% ''' -%% @end -%% @since 3.0.0 +?DOC(""" +Load userinfo for the given token + +For a high level interface using `m:oidcc_provider_configuration_worker`, see +`oidcc:retrieve_userinfo/5`. + +## Examples + +```erlang +{ok, ClientContext} = + oidcc_client_context:from_configuration_worker(provider_name, + <<"client_id">>, + <<"client_secret">>), + +%% Get Token + +{ok, #{<<"sub">> => Sub}} = + oidcc_userinfo:retrieve(Token, ClientContext, #{}). +``` +"""). +?DOC(#{since => <<"3.0.0">>}). -spec retrieve (Token, ClientContext, Opts) -> {ok, oidcc_jwt_util:claims()} | {error, error()} when Token :: oidcc_token:t(),