Skip to content
Lenso
English
Esc
navigateopen⌘Jpreview
On this page

Auth Plugin

Authenticate adapter-selected credentials, propagate a signed ActorAssertion, and authorize inside the target Plugin.

Auth is an ordinary bound Capability, not ambient Kernel middleware. The current contract is lenso.auth@1 with one authenticate Request Operation.

Ingress Adapter -> CredentialEvidence -> Auth Plugin
               <- absent | signed ActorAssertion | Domain Error
Target Plugin  <- verified, audience-limited assertion

Authenticate selected evidence

An HTTP Adapter may select a bearer token, but headers and cookies do not enter the Auth contract. Invoke the bound Capability with protocol-neutral evidence:

use lenso_auth_sdk::{
    AuthOutcome, CredentialEvidence, authenticate_request, decode_auth_response,
};
use lenso_capability_auth::{Auth, AUTHENTICATE_OPERATION};

let response = app
    .invoke::<Auth>(
        "api-ingress",
        AUTHENTICATE_OPERATION,
        authenticate_request(Some(CredentialEvidence::new("bearer", token))),
    )
    .await??;

match decode_auth_response(response)? {
    AuthOutcome::Absent => { /* continue anonymously or reject */ }
    AuthOutcome::Authenticated(assertion) => {
        // Attach it only to the downstream invocation being authorized.
        let context = assertion.attach(app.invocation_context(None, cancellation))?;
    }
}

The typed Domain Errors are invalid, expired, revoked, and unsupported. Transport failures, deadlines, and unavailable providers remain Runtime Failures.

For a complete HTTP Endpoint flow, including the lenso-http-auth helper, application-owned UserActor / AdminActor extractors, 401 Problem Details, WWW-Authenticate, activation-time clients, and status mapping, see Web Capabilities.

Authorize in the target Rust Plugin

Define the actor type the target actually needs, then verify issuer, Ed25519 proof, audience, and validity before projection:

use lenso_auth_sdk::{
    ActorAssertion, ActorAssertionVerifier, ActorProjectionError, FixedClock,
    TypedActor,
};

#[derive(Debug, Eq, PartialEq)]
struct OrdersActor { user_id: String }

impl TypedActor for OrdersActor {
    fn from_assertion(
        assertion: &ActorAssertion,
    ) -> Result<Self, ActorProjectionError> {
        if assertion.actor_kind() != "user" {
            return Err(ActorProjectionError::UnexpectedActorKind {
                expected: "user".into(),
                actual: assertion.actor_kind().into(),
            });
        }
        Ok(Self { user_id: assertion.subject().into() })
    }
}

let verifier = ActorAssertionVerifier::from_public_key_base64(
    "auth.api-token",
    configured_public_key,
)?;
let actor = verifier.project_context::<OrdersActor>(
    &context,
    "orders.api@1",
    "read",
    &FixedClock::new(now),
)?;

The expected audience is exactly orders.api@1:read. A target receives the public verification key, never the Auth signing key.

Issue and revoke API tokens

The first concrete Provider is the PostgreSQL-backed API Token Plugin. Schema setup and token operations are explicit operator work, not App boot side effects:

use std::collections::BTreeMap;
use lenso_auth_api_token_module::{ApiTokenAuthOperator, IssueApiToken};
use time::{Duration, OffsetDateTime};

ApiTokenAuthOperator::setup(database_url, "auth_api").await?;
let operator = ApiTokenAuthOperator::connect(database_url, "auth_api").await?;
let issued = operator.issue(token_pepper, IssueApiToken {
    subject: "user-123".into(),
    actor_kind: "user".into(),
    assurance: "api-token".into(),
    audience: vec!["orders.api@1:read".into()],
    claims: BTreeMap::new(),
    expires_at: OffsetDateTime::now_utc() + Duration::days(30),
}).await?;

let token = issued.expose_secret(); // show once; Debug redacts it
operator.revoke_session(issued.session_id()).await?;

The database stores only a keyed token digest and checks durable token/session revocation on every authentication.

Implement another Auth Provider

Implement the generated AuthProvider::authenticate trait and expose it through AuthEndpoint. A provider must:

  1. accept only schemes it explicitly supports;
  2. return absent when no evidence was selected;
  3. map credential outcomes to the four typed Domain Errors;
  4. issue a short-lived assertion for exact Capability/Operation audiences; and
  5. keep credential stores, revocation, and signing material private.

OAuth, password, cookie, and Organization/RBAC behavior are not currently implemented by lenso.auth@1. They should be separate ingress or Provider Plugins behind this same Capability, not new Kernel behavior.

Bun target Plugins

The Bun Adapter transports sealed invocation extensions unchanged. A Bun target must perform the same issuer, Ed25519 proof, audience, and time checks before it uses subject or claims. The essential target-side check is:

type WireExtension = {
  key: string;
  value: number[];
  issuer?: string;
  audience?: string[];
  proof?: string;
  sealed?: boolean;
};
type ActorAssertion = {
  actor_kind: string;
  assurance: string;
  audience: string[];
  claims?: Record<string, unknown>;
  expires_at: string;
  issued_at: string;
  issuer: string;
  parent_provenance?: string;
  proof: string;
  subject: string;
};

const base64url = (value: string) => new Uint8Array(Buffer.from(value, "base64url"));

async function bindActor(
  extensions: WireExtension[] | undefined,
  publicKey: string,
  issuer: string,
  capabilityId: string,
  operation: string,
  now = new Date(),
): Promise<ActorAssertion> {
  const audience = `${capabilityId}:${operation}`;
  const extension = extensions?.find((value) => value.key === "lenso.auth.actor-assertion");
  if (!extension?.sealed || extension.issuer !== issuer
    || !extension.audience?.includes(audience) || !extension.proof
    || extension.value.length === 0) {
    throw new Error("actor assertion is not target-bound");
  }
  const assertion = JSON.parse(
    new TextDecoder().decode(new Uint8Array(extension.value)),
  ) as ActorAssertion;
  const issuedAt = Date.parse(assertion.issued_at);
  const expiresAt = Date.parse(assertion.expires_at);
  if (assertion.issuer !== issuer || assertion.proof !== extension.proof
    || JSON.stringify(assertion.audience) !== JSON.stringify(extension.audience)
    || !assertion.audience.includes(audience) || assertion.subject.length === 0
    || assertion.actor_kind.length === 0 || assertion.assurance.length === 0
    || !Number.isFinite(issuedAt) || !Number.isFinite(expiresAt)
    || issuedAt >= expiresAt || now.getTime() < issuedAt || now.getTime() >= expiresAt) {
    throw new Error("actor assertion is invalid");
  }
  const payload = JSON.stringify({
    actor_kind: assertion.actor_kind,
    assurance: assertion.assurance,
    audience: assertion.audience,
    claims: assertion.claims ?? null,
    expires_at: assertion.expires_at,
    issued_at: assertion.issued_at,
    issuer: assertion.issuer,
    parent_provenance: assertion.parent_provenance ?? null,
    subject: assertion.subject,
  });
  const key = await crypto.subtle.importKey(
    "raw", base64url(publicKey), { name: "Ed25519" }, false, ["verify"],
  );
  const valid = await crypto.subtle.verify(
    "Ed25519", key, base64url(assertion.proof), new TextEncoder().encode(payload),
  );
  if (!valid) throw new Error("actor assertion proof is invalid");
  return assertion;
}

There is not yet a published Bun Auth helper, so keep this logic centralized and tested; do not trust extensions[].value by decoding JSON alone. The current Rust SDK is the semantic source, and the Bun helper must produce the same verification result before becoming a public package.

Current availability

Component Source status Package status
lenso-capability-auth generated lenso.auth@1 contract publishable crate
lenso-auth-sdk assertion issue, verify, attenuation, typed projection publishable crate
API Token Auth Plugin PostgreSQL provider, operator workflow, revocation source implemented; crate intentionally private for now
Bun Auth helper sealed extensions cross the Adapter public verifier package not implemented

Last updated on September 6, 2026

Was this page helpful?