Packages
attesto_phoenix
0.6.23
2.1.0
2.0.2
2.0.1
2.0.0
1.4.0
1.3.0
1.2.0
1.1.0
1.0.0
0.20.0
0.19.1
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.2
0.14.1
0.14.0
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.0
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
Phoenix/Ecto OAuth 2.0 / OIDC authorization server layer over attesto: authorization, token, PAR, revocation, discovery, JWKS, UserInfo, protected-resource plugs, and Ecto-backed token stores.
Current section
Files
Jump to
Current section
Files
lib/attesto_phoenix/controller/jwks_controller.ex
defmodule AttestoPhoenix.Controller.JWKSController do
@moduledoc """
`GET /.well-known/jwks.json` - the JSON Web Key Set (RFC 7517 §5).
Publishes the public halves of the issuer's signing keys as a JWK Set so a
resource server (or any client) can verify issued JWTs without a shared
secret. A verifier fetches this set, then selects the key whose `kid` matches
the token's JWS header (RFC 7515 §4.1.4). This is the document the
authorization-server metadata's `jwks_uri` points at (RFC 8414 §2).
The set carries every verification key, so it covers a rotation window:
tokens minted under the outgoing key still verify while the incoming key is
also published. Only public key material is emitted; private components never
appear (RFC 7517 §1).
This endpoint is unauthenticated public metadata, and its response is the same
for every caller, so it is marked publicly cacheable (RFC 9111 §5.2.2). The
JWK Set construction is delegated to `Attesto.JWKS`; this controller owns only
the HTTP binding and the cache policy.
## Configuration
Built on `AttestoPhoenix.Config`. The set is derived entirely from
configuration; this controller holds no policy of its own:
* `:keystore` - the `Attesto.Keystore` whose `verification_pems/0` are
published. The host owns where the keys come from.
The configured `AttestoPhoenix.Config` is read from
`conn.private[:attesto_phoenix_config]`, placed there by the host's router
pipeline.
"""
use Phoenix.Controller, formats: [:json]
import Plug.Conn
alias Attesto.JWKS
alias AttestoPhoenix.Config
# RFC 7517 §8.5.1 registers `application/jwk-set+json` for a JWK Set document.
@jwk_set_media_type "application/jwk-set+json"
# RFC 9111 §5.2.2.1: public cache lifetime, seconds. A verifier may hold the
# set for this long before re-fetching; kept below a typical rotation window
# so a newly published key is picked up promptly.
@cache_max_age_seconds 600
# The configured AttestoPhoenix.Config is threaded through the connection's
# private storage by the host pipeline.
@config_key :attesto_phoenix_config
@doc """
Handle `GET /.well-known/jwks.json` (RFC 7517 §5).
Builds the public JWK Set from the configured keystore's verification keys and
renders it as a publicly cacheable JSON document.
"""
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
def show(conn, _params) do
config = fetch_config!(conn)
# Keep the published key metadata aligned with the token signing path:
# `from_config/1` preserves the keystore's per-key `alg` metadata.
jwk_set = JWKS.from_config(attesto_config(config))
conn
|> put_public_cache()
|> put_resp_content_type(@jwk_set_media_type)
|> json(jwk_set)
end
# RFC 9111 §5.2.2.5 / §5.2.2.1: the set is identical for every caller and may
# be shared by intermediary caches for `max-age` seconds.
defp put_public_cache(conn) do
put_resp_header(conn, "cache-control", "public, max-age=#{@cache_max_age_seconds}")
end
defp fetch_config!(conn) do
case conn.private do
%{@config_key => %Config{} = config} ->
config
_missing ->
raise ArgumentError,
"AttestoPhoenix.Controller.JWKSController: no %AttestoPhoenix.Config{} " <>
"in conn.private[#{inspect(@config_key)}]; wire the host pipeline that assigns it"
end
end
defp attesto_config(config) do
Config.to_attesto_config(config, principal_kinds_extra(config))
end
defp principal_kinds_extra(%Config{principal_kinds: kinds})
when is_list(kinds) and kinds != [] do
[principal_kinds: kinds]
end
defp principal_kinds_extra(%Config{principal_kinds: callback}) when not is_nil(callback) do
case invoke(callback, []) do
kinds when is_list(kinds) and kinds != [] -> [principal_kinds: kinds]
_other -> []
end
end
defp principal_kinds_extra(_config), do: []
defp invoke(fun, args) when is_function(fun), do: apply(fun, args)
defp invoke({module, fun}, args) when is_atom(module) and is_atom(fun),
do: apply(module, fun, args)
defp invoke({module, fun, extra}, args) when is_atom(module) and is_atom(fun),
do: apply(module, fun, args ++ extra)
end