Packages
attesto_phoenix
1.4.0
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/dev_tls.ex
defmodule AttestoPhoenix.DevTLS do
@moduledoc """
A one-line, dev-only helper that wires a locally-trusted TLS certificate into a
Phoenix endpoint's `https:` listener.
attesto requires an **https** issuer (RFC 8414 §2: the issuer identifier MUST be
an `https` URL) — the discovery documents, DPoP `htu`, and RFC 9728 resource
identifiers are all https by spec, and attesto enforces that at config-build
time. So a plain `http://localhost` dev server can't drive the OAuth / MCP flow.
Rather than adding an https-disable switch (there is deliberately none in a
security library), this helper makes the RIGHT path frictionless: serve a
certificate from a local certificate authority — [mkcert](https://github.com/FiloSottile/mkcert) —
so `https://localhost` is trusted with no tunnel and no downgrade.
Generate the certificate once with `mix attesto_phoenix.gen.dev_https`, then in
`config/dev.exs`:
config :my_app, MyAppWeb.Endpoint, https: AttestoPhoenix.DevTLS.https_opts(port: 4443)
The MCP / OAuth issuer then becomes `https://localhost:4443`, so discovery,
DPoP, and the RFC 8707 resource identifiers all line up automatically.
## This is a dev-only convenience
It does **not** weaken any https requirement and it is not for production. In
production, TLS is terminated at the load balancer / ingress with a real CA
certificate, and the endpoint's `https:` listener (if any) uses that
certificate — never an mkcert one. `mix mkcert -install` trusts a CA on the
local machine only; never run it on a server or in CI.
This helper only wires a locally-trusted certificate into the dev endpoint. It
never touches the issuer/audience validation, and attesto's https-only
guarantee stays fully intact.
"""
# The conventional dev certificate location, matching what
# `mix attesto_phoenix.gen.dev_https` writes and what this helper loads.
@default_certfile "priv/cert/localhost.pem"
@default_keyfile "priv/cert/localhost-key.pem"
# The default mkcert https port. 4443 keeps the standard Phoenix dev http
# listener on 4000 free and needs no privileged bind.
@default_port 4443
# Bandit/Plug default per-header cap is ~10KB; large `Cookie`/`Authorization`
# (DPoP proofs, long access tokens) headers can exceed it. Raise it in dev so a
# request never 431s underfoot. Overridable via `:max_header_length`.
@default_max_header_length 65_536
@doc """
Returns the Phoenix endpoint `https:` keyword list for a locally-trusted
mkcert certificate.
## Options
* `:port` - the TLS port to listen on. Defaults to `#{@default_port}`.
* `:certfile` - path to the certificate. Defaults to the conventional
`#{@default_certfile}` resolved against the caller's app (see below).
* `:keyfile` - path to the private key. Defaults to the conventional
`#{@default_keyfile}` resolved against the caller's app.
* `:otp_app` - when given, the default cert/key paths resolve via
`Application.app_dir/2` (release-safe). When omitted, they resolve relative
to `File.cwd!/0` — the app root when running `mix phx.server`, which is the
idiomatic Phoenix-dev spelling (`phx.gen.cert` emits the same bare relative
paths).
* `:max_header_length` - the Bandit `http_1_options` max header length.
Defaults to `#{@default_max_header_length}`.
Returns:
[
port: 4443,
cipher_suite: :strong,
certfile: "…/priv/cert/localhost.pem",
keyfile: "…/priv/cert/localhost-key.pem",
http_1_options: [max_header_length: 65_536]
]
Raises `ArgumentError` if the certificate or key is missing, pointing you at
`mix attesto_phoenix.gen.dev_https`. It NEVER silently falls back to http.
## Examples
# config/dev.exs
config :my_app, MyAppWeb.Endpoint,
https: AttestoPhoenix.DevTLS.https_opts(port: 4443)
# release-safe path resolution
config :my_app, MyAppWeb.Endpoint,
https: AttestoPhoenix.DevTLS.https_opts(port: 4443, otp_app: :my_app)
"""
@spec https_opts(keyword()) :: keyword()
def https_opts(opts \\ []) when is_list(opts) do
port = Keyword.get(opts, :port, @default_port)
max_header_length = Keyword.get(opts, :max_header_length, @default_max_header_length)
certfile = resolve_path(opts, :certfile, @default_certfile)
keyfile = resolve_path(opts, :keyfile, @default_keyfile)
ensure_present!(certfile, keyfile)
[
port: port,
cipher_suite: :strong,
certfile: certfile,
keyfile: keyfile,
http_1_options: [max_header_length: max_header_length]
]
end
# An explicit `:certfile`/`:keyfile` is expanded as given (relative to cwd or
# absolute); otherwise the conventional path is resolved against the caller's
# app — via `Application.app_dir/2` when `:otp_app` is supplied, else relative
# to `File.cwd!/0`.
defp resolve_path(opts, key, default_rel) do
case Keyword.get(opts, key) do
nil -> default_path(opts, default_rel)
path -> Path.expand(path)
end
end
defp default_path(opts, rel) do
case Keyword.get(opts, :otp_app) do
nil -> Path.expand(rel, File.cwd!())
app when is_atom(app) -> Application.app_dir(app, rel)
end
end
defp ensure_present!(certfile, keyfile) do
missing = Enum.reject([certfile, keyfile], &File.exists?/1)
if missing != [] do
raise ArgumentError, """
AttestoPhoenix.DevTLS: locally-trusted certificate not found.
Missing:
#{Enum.map_join(missing, "\n", &" - #{&1}")}
Generate it once with mkcert:
mix attesto_phoenix.gen.dev_https
That installs the local CA (idempotent) and writes the cert/key pair the dev
endpoint loads. Then point your MCP / OAuth issuer at https://localhost:<port>.
This helper never falls back to plain http: attesto requires an https issuer
(RFC 8414 §2), and serving a locally-trusted cert is the tunnel-free way to
satisfy it.
"""
end
:ok
end
end