Skip to content

Rust

The jiayang crate verifies the identity token the edge sends your app and gives you the caller. It works with any http::HeaderMap, and has an axum layer and extractor behind a feature.

Terminal window
cargo add jiayang

It needs Rust 1.92 or newer and a Tokio runtime. It fetches the platform’s keys with reqwest over rustls.

Build one Verifier at startup and share it. It caches the platform’s keys, so building one per request would fetch them every time. It’s Clone, and clones share the cache.

// Once, at startup. Reads JIAYANG_APP_ID, JIAYANG_IDENTITY_ISSUER and JIAYANG_JWKS_URL.
let verifier = jiayang::Verifier::from_env()?;
// Per request, from any http::HeaderMap: axum's, hyper's, your own.
let user = verifier.require_user(&headers).await?;
println!("hello {}", user.email.as_deref().unwrap_or(&user.sub));

from_env fails if any of the three variables is missing. The platform sets them.

The caller is a jiayang::User:

pub struct User {
pub kind: Kind, // Kind::User, or Kind::Service for a bypass token
pub sub: String, // "usr_…", or "service:<token id>"
pub email: Option<String>, // None for a bypass token
pub role: String, // "viewer", "editor" or "owner"
pub workspace_id: String,
}

See the SDK overview for what each field means and what the SDK checks.

verifier.verify(token).await checks a token you already have, the same way. To pass the values yourself, use Verifier::new(Config { app_id, issuer, jwks_url }).

use jiayang::Role;
user.require_role(Role::Editor)?; // Err(Forbidden) unless the caller is an editor or an owner
if user.has_role(Role::Owner) {
// show the settings link
}

Role is ordered: Role::Viewer < Role::Editor < Role::Owner.

Turn on the feature:

Terminal window
cargo add jiayang --features axum
use axum::{Router, routing::{get, post}};
use jiayang::{Role, User, Verifier, axum::Jiayang};
let verifier = Verifier::from_env()?;
let app = Router::new()
.route("/", get(index))
.route("/orders", post(place_order).layer(Jiayang::new(verifier.clone()).requires(Role::Editor)))
.layer(Jiayang::new(verifier.clone()))
.with_state(verifier);
async fn index(user: User) -> String {
format!("hello {}", user.email.as_deref().unwrap_or(&user.sub))
}

The Jiayang layer verifies before your handler runs, and answers the requests it refuses itself. .requires(Role::Editor) also refuses a caller with less than that role.

User is an extractor. Behind the layer it reads what the layer already verified. On a route without the layer it verifies the request itself, which needs the Verifier in the router’s state: that’s what .with_state(verifier) is for.

Status Body When
401 unauthorized No valid caller
403 forbidden: this needs editor .requires(Role::Editor) and the caller has less

Both come with Cache-Control: no-store. The rejection type is jiayang::axum::Rejection.

A delivery on a verified public path arrives with a token of kind webhook, not a person’s. require_webhook checks it and says which provider signed the delivery. require_user refuses a webhook’s token, and require_webhook refuses a person’s.

use jiayang::Provider;
let hook = verifier.require_webhook(&headers, &[Provider::Stripe]).await?;
// hook.delivery is Stripe's event id. Skip one you've handled.

With axum, the webhook’s routes get a router of their own behind JiayangWebhook, merged after the user layer so that layer never sees the delivery. The Webhook extractor reads what JiayangWebhook verified.

use jiayang::{Provider, Webhook, axum::{Jiayang, JiayangWebhook}};
let hooks = Router::new()
.route("/hooks/stripe", post(stripe))
.layer(JiayangWebhook::new(verifier.clone(), [Provider::Stripe]));
let app = Router::new()
.route("/", get(index))
.layer(Jiayang::new(verifier.clone()))
.merge(hooks)
.with_state(verifier);
async fn stripe(hook: Webhook, body: String) -> StatusCode {
StatusCode::NO_CONTENT
}

JiayangWebhook answers 401 unauthorized itself to anything but a verified delivery from its providers. The Webhook extractor only reads what the layer verified, and refuses with 401 on a route without it.

Name the providers a route takes. A delivery from any other is refused, and so is every delivery when the list is empty. Provider::from_name("stripe") reads a name from your own config. The delivery is a jiayang::Webhook:

pub struct Webhook {
pub provider: Provider, // Provider::Stripe and the rest
pub pattern: String, // the public path it came in on, like "/hooks/stripe"
pub delivery: Option<String>, // the provider's signed id, None where it signs none
pub signed_at: Option<u64>, // unix seconds, None where the provider signs no time
pub workspace_id: String,
}

The body arrives byte for byte as the provider sent it, so any body extractor reads it as usual. verifier.verify_webhook(token, &[Provider::Stripe]) checks a webhook’s token you already have.

require_user and require_webhook return Err(Unauthorized), and require_role returns Err(Forbidden). Their Display says why, for your logs:

Error Why
unauthorized: no identity token The request has no X-Jiayang-Identity header
unauthorized: invalid identity token The token failed a check
unauthorized: unknown signing key No key in the key set matches the token’s kid
unauthorized: couldn't fetch the identity keys The key set couldn’t be fetched, and there’s no fresh copy
unauthorized: JIAYANG_APP_ID, JIAYANG_IDENTITY_ISSUER and JIAYANG_JWKS_URL must be set From from_env or new, when a value is empty
unauthorized: not a webhook this route takes From require_webhook: a provider the route doesn’t name signed the delivery
unauthorized: invalid webhook token From require_webhook: the token’s webhook claims failed a check
unauthorized: no provider named From require_webhook: the list of providers was empty, so it refuses everything
unauthorized: no verified webhook From the Webhook extractor, on a route without JiayangWebhook
forbidden: this needs editor From require_role(Role::Editor)

A person’s token given to require_webhook, or a webhook’s given to require_user, fails as invalid identity token: its audience is the other kind’s.

The crate trusts the public root certificates it ships with. It also trusts the certificates in the file SSL_CERT_FILE names.

A container app’s outbound HTTPS goes through the platform’s proxy, and the platform sets SSL_CERT_FILE to that proxy’s certificate. rustls reads no environment on its own, so the crate reads it for you. Without it, fetching the keys from a container would fail and every caller would be refused.

jiayang dev starts your app with cargo run behind the platform’s front door on your machine, with real tokens. See Local development.

Terminal window
jiayang dev