Packages
oidcc
3.7.2
3.7.2
3.7.1
3.7.0
3.6.0
3.5.2
3.5.1
3.5.0
retired
3.4.0
3.3.0
3.2.6
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.2.0-beta.3
3.2.0-beta.2
retired
3.2.0-beta.1
retired
3.1.2
3.1.2-beta.1
retired
3.1.1
retired
3.1.0
retired
3.1.0-beta.2
retired
3.1.0-beta.1
retired
3.0.2
3.0.1
retired
3.0.0
retired
3.0.0-rc.6
retired
3.0.0-rc.5
retired
3.0.0-rc.4
retired
3.0.0-rc.3
retired
3.0.0-rc.2
retired
3.0.0-rc.1
retired
3.0.0-alpha.5
retired
3.0.0-alpha.4
retired
3.0.0-alpha.3
retired
3.0.0-alpha.2
retired
3.0.0-alpha.1
retired
2.0.0-alpha.2
retired
2.0.0-alpha.1
retired
1.8.1
retired
1.8.0
retired
1.7.0
retired
1.6.0
retired
1.5.1
retired
1.5.0
retired
1.4.0
retired
1.3.0
retired
1.2.1
retired
1.2.0
retired
1.1.0
retired
1.0.1
retired
1.0.0
retired
OpenID Connect client library for the BEAM.
Current section
Files
Jump to
Current section
Files
src/oidcc_token_introspection.erl
%% SPDX-FileCopyrightText: 2023 Erlang Ecosystem Foundation
%% SPDX-License-Identifier: Apache-2.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").
-include("oidcc_token_introspection.hrl").
-export([introspect/3]).
-export_type([error/0]).
-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(),
exp :: pos_integer(),
scope :: oidcc_scope:scopes(),
username :: binary(),
iss :: binary()
}.
?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(),
client_self_only => boolean()
}.
?DOC(#{since => <<"3.0.0">>}).
-type error() :: client_id_mismatch | introspection_not_supported | oidcc_http_util:error().
-telemetry_event(#{
event => [oidcc, load_configuration, start],
description => <<"Emitted at the start of introspecting the token">>,
measurements => <<"#{system_time => non_neg_integer()}">>,
metadata => <<"#{issuer => uri_string:uri_string(), client_id => binary()}">>
}).
-telemetry_event(#{
event => [oidcc, load_configuration, stop],
description => <<"Emitted at the end of introspecting the token">>,
measurements => <<"#{duration => integer(), monotonic_time => integer()}">>,
metadata => <<"#{issuer => uri_string:uri_string(), client_id => binary()}">>
}).
-telemetry_event(#{
event => [oidcc, load_configuration, exception],
description => <<"Emitted at the end of introspecting the token">>,
measurements => <<"#{duration => integer(), monotonic_time => integer()}">>,
metadata => <<"#{issuer => uri_string:uri_string(), client_id => binary()}">>
}).
?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()}
when
Token :: oidcc_token:t() | binary(),
ClientContext :: oidcc_client_context:authenticated_t(),
Opts :: opts().
introspect(
#oidcc_token{access = #oidcc_token_access{token = AccessToken}},
ClientContext,
Opts
) ->
introspect(AccessToken, ClientContext, Opts);
introspect(AccessToken, ClientContext, Opts) ->
#oidcc_client_context{
provider_configuration = Configuration,
client_id = ClientId,
client_secret = ClientSecret
} = ClientContext,
#oidcc_provider_configuration{
introspection_endpoint = Endpoint0,
issuer = Issuer,
introspection_endpoint_auth_methods_supported = SupportedAuthMethods,
introspection_endpoint_auth_signing_alg_values_supported = AllowAlgorithms
} = Configuration,
case Endpoint0 of
undefined ->
{error, introspection_not_supported};
_ ->
Header0 = [{"accept", "application/json"}],
Body0 = [{<<"token">>, AccessToken}],
RequestOpts = maps:get(request_opts, Opts, #{}),
TelemetryOpts = #{
topic => [oidcc, introspect_token],
extra_meta => #{issuer => Issuer, client_id => ClientId}
},
DpopOpts =
case Opts of
#{dpop_nonce := DpopNonce} ->
#{nonce => DpopNonce};
_ ->
#{}
end,
maybe
{ok, {Body, Header1}, AuthMethod} ?=
oidcc_auth_util:add_client_authentication(
Body0, Header0, SupportedAuthMethods, AllowAlgorithms, Opts, ClientContext
),
Endpoint = oidcc_auth_util:maybe_mtls_endpoint(
Endpoint0,
AuthMethod,
<<"introspection_endpoint">>,
ClientContext
),
Header = oidcc_auth_util:add_dpop_proof_header(
Header1, post, Endpoint, DpopOpts, ClientContext
),
Request =
{Endpoint, Header, "application/x-www-form-urlencoded",
uri_string:compose_query(Body)},
{ok, {{json, Token}, _Headers}} ?=
oidcc_http_util:request(post, Request, TelemetryOpts, RequestOpts),
{ok, TokenMap} ?= extract_response(Token),
client_match(TokenMap, ClientContext, maps:get(client_self_only, Opts, true))
else
{error, {use_dpop_nonce, NewDpopNonce, _}} when
DpopOpts =:= #{}
->
%% only retry automatically if we didn't use a nonce the first time
%% (to avoid infinite loops)
introspect(
AccessToken,
ClientContext,
Opts#{dpop_nonce => NewDpopNonce}
);
{error, Reason} ->
{error, Reason}
end
end.
-spec client_match(Introspection, ClientContext, ClientSelfOnly) ->
{ok, t()} | {error, error()}
when
Introspection :: t(),
ClientContext :: oidcc_client_context:t(),
ClientSelfOnly :: boolean().
client_match(Introspection, _, false) ->
{ok, Introspection};
client_match(
#oidcc_token_introspection{client_id = ClientId} = Introspection,
#oidcc_client_context{client_id = ClientId},
true
) ->
{ok, Introspection};
client_match(_Introspection, _ClientContext, true) ->
{error, client_id_mismatch}.
-spec extract_response(TokenMap) ->
{ok, t()}
when
TokenMap :: map().
extract_response(TokenMap) ->
Active =
case maps:get(<<"active">>, TokenMap, undefined) of
true ->
true;
_ ->
false
end,
Scope = maps:get(<<"scope">>, TokenMap, <<"">>),
Username = maps:get(<<"username">>, TokenMap, undefined),
TokenType = maps:get(<<"token_type">>, TokenMap, undefined),
Exp = maps:get(<<"exp">>, TokenMap, undefined),
Iat = maps:get(<<"iat">>, TokenMap, undefined),
Nbf = maps:get(<<"nbf">>, TokenMap, undefined),
Sub = maps:get(<<"sub">>, TokenMap, undefined),
Aud = maps:get(<<"aud">>, TokenMap, undefined),
Iss = maps:get(<<"iss">>, TokenMap, undefined),
Jti = maps:get(<<"jti">>, TokenMap, undefined),
ClientId = maps:get(<<"client_id">>, TokenMap, undefined),
{ok, #oidcc_token_introspection{
active = Active,
scope = oidcc_scope:parse(Scope),
client_id = ClientId,
username = Username,
exp = Exp,
token_type = TokenType,
iat = Iat,
nbf = Nbf,
sub = Sub,
aud = Aud,
iss = Iss,
jti = Jti,
extra = maps:without(
[
<<"scope">>,
<<"active">>,
<<"username">>,
<<"exp">>,
<<"client_id">>,
<<"token_type">>,
<<"iat">>,
<<"nbf">>,
<<"sub">>,
<<"aud">>,
<<"iss">>,
<<"jti">>
],
TokenMap
)
}}.