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.
cargo add jiayangIt needs Rust 1.92 or newer and a Tokio runtime. It fetches the platform’s keys with reqwest over rustls.
The verifier
Section titled “The verifier”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:
cargo add jiayang --features axumuse 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.
Webhooks
Section titled “Webhooks”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.
Errors
Section titled “Errors”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.
TLS in a container
Section titled “TLS in a container”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.
Run it locally
Section titled “Run it locally”jiayang dev starts your app with cargo run behind the platform’s front door on your machine, with real tokens. See Local development.
jiayang dev