%%%-------------------------------------------------------------------
%% @doc Validate extracted authorization token using introspection.
%%
%% See: [https://datatracker.ietf.org/doc/html/rfc7662]
%%
%% This middleware should be used together with
%% {@link oidcc_cowboy_extract_authorization}.
%%
%% This middleware will send a introspection request for ever request. To avoid
%% this, provide a `cache' to {@link opts()}.
%%
%% <h2>Usage</h2>
%%
%% ```
%% OidccCowboyOpts = #{
%% provider => openid_confi_provider_name,
%% client_id => <<"client_id">>,
%% client_secret => <<"client_secret">>
%% },
%% Dispatch = cowboy_router:compile([
%% {'_', [
%% %% ...
%% ]}
%% ]),
%% {ok, _} = cowboy:start_clear(http, [{port, 8080}], #{
%% middlewares => [
%% oidcc_cowboy_extract_authorization,
%% oidcc_cowboy_introspect_token,
%% cowboy_router,
%% cowboy_handler
%% ],
%% env => #{
%% dispatch => Dispatch,
%% oidcc_cowboy_introspect_token => OidccCowboyOpts
%% }
%% })
%% '''
%% @end
%% @since 2.0.0
%%%-------------------------------------------------------------------
-module(oidcc_cowboy_introspect_token).
-behaviour(cowboy_middleware).
-include_lib("oidcc/include/oidcc_token_introspection.hrl").
-export([execute/2]).
-export_type([opts/0]).
-type opts() :: #{
provider := gen_server:server_ref(),
client_id := binary(),
client_secret := binary(),
token_introspection_opts => oidcc_token_introspection:opts(),
cache => oidcc_cowboy_cache:t(),
send_inactive_token_response => fun(
(
Req :: cowboy_req:req(),
Env :: cowboy_middleware:env(),
Introspection :: oidcc_token_introspection:t()
) -> {ok, cowboy_req:req(), cowboy_middleware:env()} | {stop, cowboy_req:req()}
)
}.
%% Options for the middleware
%%
%% <h2>Options</h2>
%%
%% <ul>
%% <li>`provider' - name of the
%% {@link oidcc_provider_configuration_worker}</li>
%% <li>`client_id' - OAuth Client ID to use for the token introspection</li>
%% <li>`client_secret' - OAuth Client Secret to use for the token
%% introspection</li>
%% <li>`token_introspection_opts' - Options to pass to the introspection</li>
%% <li>`send_inactive_token_response' - Customize Error Response for inactive
%% token</li>
%% <li>`cache' - Cache introspection response - See {@link oidcc_cowboy_cache}</li>
%% </ul>
%% @private
execute(#{oidcc_cowboy_extract_authorization := undefined} = Req, #{?MODULE := _Opts} = Env) ->
{ok, maps:put(?MODULE, undefined, Req), Env};
execute(#{oidcc_cowboy_extract_authorization := Token} = Req, #{?MODULE := Opts} = Env) ->
Provider = maps:get(provider, Opts),
ClientId = maps:get(client_id, Opts),
ClientSecret = maps:get(client_secret, Opts),
TokenIntrospectionOpts = maps:get(token_introspection_opts, Opts, #{}),
SendInactiveTokenResponse = maps:get(
send_inactive_token_response, Opts, fun send_inactive_token_response/3
),
Cache = maps:get(cache, Opts, oidcc_cowboy_cache_noop),
case Cache:get(introspection, Token, Req, Env) of
{ok, #oidcc_token_introspection{active = true} = Introspection} ->
{ok, maps:put(?MODULE, Introspection, Req), Env};
{ok, #oidcc_token_introspection{active = false} = Introspection} ->
SendInactiveTokenResponse(maps:put(?MODULE, Introspection, Req), Env, Introspection);
miss ->
case
oidcc:introspect_token(
Token, Provider, ClientId, ClientSecret, TokenIntrospectionOpts
)
of
{ok, #oidcc_token_introspection{active = true} = Introspection} ->
Cache:put(introspection, Token, Introspection, Req, Env),
{ok, maps:put(?MODULE, Introspection, Req), Env};
{ok, #oidcc_token_introspection{active = false} = Introspection} ->
SendInactiveTokenResponse(
maps:put(?MODULE, Introspection, Req), Env, Introspection
);
{error, Reason} ->
erlang:error(Reason)
end
end;
execute(#{oidcc_cowboy_extract_authorization := _Token} = _Req, #{} = _Env) ->
erlang:error(no_config_provided);
execute(#{} = _Req, #{?MODULE := _Opts} = _Env) ->
erlang:error(no_oidcc_cowboy_extract_authorization).
send_inactive_token_response(Req0, _Env, _Introspection) ->
Req = cowboy_req:reply(
401,
#{<<"content-type">> => <<"text/plain">>},
<<"The provided token is inactive">>,
Req0
),
{stop, Req}.