Arc.Identity.OnBehalfOf (cratis_arc v0.1.0)

Copy Markdown View Source

Acting on behalf of a user: verifies the RS256 assertion Cratis AuthProxy signs for an on-behalf session and attaches the actor to the represented user's principal.

Inside such a request two identities matter. The represented user is the request principal, and decides what the request may do: roles, tenant and authorization are theirs. The actor is who actually acts, and is available as principal.actor and recorded by Chronicle as the cause of every event, on behalf of the represented user.

The ingress forwards the usual x-ms-client-principal* headers plus x-cratis-on-behalf-actor (the actor's base64 client principal) and x-cratis-on-behalf-assertion (a compact RS256 JWS). Enable it with the on_behalf_of option, which also needs trust_forwarded_identity_headers:

config :my_app, MyApp.Arc,
  trust_forwarded_identity_headers: true,
  on_behalf_of: [
    enabled: true,
    issuer: "https://authproxy.example.com",
    audience: "https://app.example.com",
    service: "portal",
    clock_skew: 30,
    signing_keys: [[key_id: "authproxy-2026", public_key_pem: "-----BEGIN PUBLIC KEY-----\n..."]]
  ]

service is optional: set it when the ingress fronts several services with the same keys, so an assertion issued for another service is refused. clock_skew is in seconds. Signing keys must be RSA public keys of at least 2048 bits in PEM form; a private key is refused so the signing secret never reaches configuration.

Arc accepts a forward only when the assertion passes every check: RS256 with a configured kid and no crit header; the issuer; the audience; nbf and exp present, now within them allowing for the skew, and a lifetime of at most five minutes; the purpose on-behalf-forward; the service, when configured; jti present and not used before; the represented user's principal hash, subject and id header; the actor hash and subject; the tenant, when the request carries the tenant header; and htm and htu, when present. The actor must be a valid client principal whose userId differs from the represented user's.

The replay check is in memory and per application process. With several nodes a captured assertion can be replayed once to each other node within its lifetime. The ingress must strip every inbound x-cratis-on-behalf-* and x-ms-client-principal* header.

Summary

Functions

Whether on-behalf forwarding may be used at all: the host trusts forwarded identity headers and the feature is enabled. Returns nil or the failure.

Verifies an on-behalf forward and returns the actor.

Whether the request carries either on-behalf header.

Reads and validates the configured signing keys. {:ok, %{key_id => rsa_public_key}} or {:error, problem}.

Functions

actor_header()

admit(config)

@spec admit(keyword()) :: String.t() | nil

Whether on-behalf forwarding may be used at all: the host trusts forwarded identity headers and the feature is enabled. Returns nil or the failure.

assertion_header()

attach(forward, represented, app, config, keys, now)

@spec attach(
  map(),
  Arc.Principal.t(),
  module(),
  keyword(),
  {:ok, map()} | {:error, String.t()},
  integer()
) :: {:ok, Arc.Principal.t(), integer()} | {:error, String.t()}

Verifies an on-behalf forward and returns the actor.

forward is %{principal:, principal_id:, actor:, assertion:, tenant:, method:, path:} with the raw header values. represented is the represented user's principal. Returns {:ok, actor, valid_until} (unix seconds) or {:error, reason}.

present?(has_header)

@spec present?((String.t() -> boolean())) :: boolean()

Whether the request carries either on-behalf header.

signing_keys(config)

@spec signing_keys(keyword()) :: {:ok, map()} | {:error, String.t()}

Reads and validates the configured signing keys. {:ok, %{key_id => rsa_public_key}} or {:error, problem}.