Current section
Files
Jump to
Current section
Files
lib/anubis/server/authorization/introspection_validator.ex
defmodule Anubis.Server.Authorization.IntrospectionValidator do
@moduledoc """
Token validator using RFC 7662 Token Introspection.
Validates opaque bearer tokens by POSTing them to an authorization server's
introspection endpoint. Supports HTTP Basic authentication with client credentials.
## Configuration
validator: {Anubis.Server.Authorization.IntrospectionValidator,
introspection_endpoint: "https://auth.example.com/introspect",
client_id: "my-client",
client_secret: "my-secret"
}
## Options
* `:introspection_endpoint` — URL of the introspection endpoint (required)
* `:client_id` — client ID for Basic authentication (optional)
* `:client_secret` — client secret for Basic authentication (optional)
"""
@behaviour Anubis.Server.Authorization.Validator
use Anubis.Logging
@http_receive_timeout 5_000
@http_pool_timeout 1_000
@spec validate_token(String.t(), map()) :: {:ok, map()} | {:error, term()}
@impl true
def validate_token(token, %{validator: {_mod, opts}}) when is_binary(token) and is_list(opts) do
endpoint = Keyword.fetch!(opts, :introspection_endpoint)
headers = build_headers(opts)
request_body = URI.encode_query(%{"token" => token, "token_type_hint" => "access_token"})
request = Finch.build(:post, endpoint, headers, request_body)
case Finch.request(request, Anubis.Finch,
receive_timeout: @http_receive_timeout,
pool_timeout: @http_pool_timeout
) do
{:ok, %Finch.Response{status: 200, body: resp_body}} ->
parse_introspection_response(resp_body)
{:ok, %Finch.Response{status: status}} ->
Logging.server_event("introspection_http_error", %{status: status}, level: :warning)
{:error, {:introspection_error, status}}
{:error, reason} ->
Logging.server_event("introspection_request_failed", %{reason: inspect(reason)}, level: :error)
{:error, {:introspection_failed, reason}}
end
end
defp build_headers(opts) do
base_headers = [{"content-type", "application/x-www-form-urlencoded"}]
case {Keyword.get(opts, :client_id), Keyword.get(opts, :client_secret)} do
{id, secret} when is_binary(id) and is_binary(secret) ->
credentials = Base.encode64("#{URI.encode_www_form(id)}:#{URI.encode_www_form(secret)}")
[{"authorization", "Basic #{credentials}"} | base_headers]
_ ->
base_headers
end
end
defp parse_introspection_response(body) do
case JSON.decode(body) do
{:ok, %{"active" => true} = claims} ->
{:ok, claims}
{:ok, %{"active" => false}} ->
{:error, :token_inactive}
{:ok, _} ->
{:error, :token_inactive}
{:error, reason} ->
Logging.server_event("introspection_parse_error", %{reason: inspect(reason)}, level: :error)
{:error, :invalid_introspection_response}
end
end
end