howdy/auth/routes

JSON endpoints for custom pages and API clients. Mount once at startup.

Values

pub fn api(
  identity: auth.Auth,
  at prefix: String,
) -> controller.Builder(Nil)

POST /login and /register deliver email tokens; POST /session exchanges for a browser cookie; POST /token exchanges for a bearer token. Both take {"token"}, or with auth.with_email_codes, {"email", "code"}. POST /password/register sends verification; /password/session and /password/token authenticate with email/password when enabled. POST /password sets or replaces the caller’s password; see auth.set_password for the fresh email-token session it requires. The endpoints that take an email address also take an optional group id, applied with auth.in_group; see auth.with_groups for when it is needed. It is the client’s claim about where it wants to sign in, not proof of membership: principal.user.group_id is the group to trust. POST /locale sets the caller’s locale, {"locale": "de"}, or clears it with {"locale": null}; see auth.set_locale. GET /me, GET /sessions, POST /sessions/revoke and POST /logout accept either transport, never both together. Cookie writes require Origin. Other endpoints allow absent Origin for nonbrowser clients, but reject any foreign/duplicate Origin.

Error bodies carry a stable code beside the English error, and answers that tell the reader something carry one beside message, so a client can show its own words; the bundled pages do. The codes are unauthorized, forbidden, not_found, invalid, conflict, validation, too_many_requests, unsupported_media_type and internal from the status, the more specific password_incorrect, password_unchanged, password_common, password_length, password_too_long, email_invalid, email_unchanged, email_mismatch, email_taken, mfa_enabled, passkey_limit, passkey_registered, passkey_name, provider_linked, credential_too_large, locale_unsupported and group_required, and for success token_sent, signup_token_sent, approval_sent and confirmation_sent.

Clients are rate limited by socket address. Behind a reverse proxy that is the proxy for everyone: use api_limited_by there.

pub fn api_limited_by(
  identity: auth.Auth,
  at prefix: String,
  key key: fn(context.Context(Nil)) -> option.Option(String),
) -> controller.Builder(Nil)

As api, identifying clients for rate limiting with key. Behind a trusted proxy, read the header it sets:

routes.api_limited_by(identity, at: "/api/auth", key: fn(ctx) {
  request.get_header(ctx.request, "fly-client-ip") |> option.from_result
})

Only use a header your ingress overwrites; a client-supplied value lets anyone choose their own bucket. None skips limiting for that request.

pub const credential_limit: Int

Requests per client per minute to endpoints that send email, exchange tokens or hash passwords.

pub fn providers(
  identity: auth.Auth,
  at prefix: String,
  success_path success: String,
  failure_path failure: String,
) -> controller.Builder(Nil)

Browser redirects for built-in identity providers. Mount once at startup. The callback URI is auth.origin(identity) <> at <> "/providers/google/callback". Destinations are fixed local paths. Login/link POSTs require an exact Origin.

pub fn providers_limited_by(
  identity: auth.Auth,
  at prefix: String,
  success_path success: String,
  failure_path failure: String,
  key key: fn(context.Context(Nil)) -> option.Option(String),
) -> controller.Builder(Nil)

As providers, with a trusted client key for proxy-aware rate limits/audit.

pub const session_limit: Int

Requests per client per minute to the signed-in endpoints, which pages call routinely.

pub fn signed_in(
  identity: auth.Auth,
  ctx: context.Context(Nil),
  res: response.Response(a),
  session: auth.Session,
) -> response.Response(a)

Make session the browser’s active session on a response of your own, setting the cookies the bundled routes set. For transports that obtain a session some other way, such as auth.impersonate from an operator tool. With multi-session enabled the session joins the browser’s accounts; otherwise the cookie is replaced and the previous session is left as it was.

pub fn sso(
  identity: auth.Auth,
  at prefix: String,
  success_path success: String,
  failure_path failure: String,
) -> controller.Builder(Nil)

Browser redirects for SSO connections; see howdy/auth/connection. Mount once at startup: connections created later are served without remounting. A connection’s redirect URI, to register at the customer’s provider, is auth.origin(identity) <> at <> "/sso/" <> connection.id <> "/callback". POST at <> "/sso/login" takes a form email and picks the connection by its domain. Destinations are fixed local paths. POSTs require an exact Origin.

pub fn sso_limited_by(
  identity: auth.Auth,
  at prefix: String,
  success_path success: String,
  failure_path failure: String,
  key key: fn(context.Context(Nil)) -> option.Option(String),
) -> controller.Builder(Nil)

As sso, with a trusted client key for proxy-aware rate limits/audit.

✨ Search Document