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 our signing keys with reqwest over rustls.
The verifier
Section titled “The verifier”Build one Verifier at startup and share it. It caches our signing 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. We set them.
The caller is a jiayang::User:
pub struct User { pub kind: Kind, // Kind::User, or Kind::Service for a bypass token or an agent pub sub: String, // "usr_…", "service:<token id>", or "agent:<agent id>" pub email: Option<String>, // None for a bypass token or an agent pub role: String, // "viewer", "editor" or "owner" pub workspace_id: String, pub agent_name: Option<String>, // the agent's name when an agent called, None for everyone else}user.is_agent() says whether one of the workspace’s agents made the request. It and agent_name are new in version 0.2.0: with an earlier version, change its version in Cargo.toml to "0.2".
let by = match &user.agent_name { Some(name) => format!("{name} (agent)"), None => user.email.clone().unwrap_or_else(|| user.sub.clone()),};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, dev: false }). dev is whether JIAYANG_DEV is 1, which only jiayang dev sets. It became a field in version 0.3.0.
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 verified. On a route without the layer it verifies the request itself, using the Verifier in the router’s state, which .with_state(verifier) puts there.
| 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. 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, put the webhook routes in a router of their own behind JiayangWebhook, and merge it after the user layer so that layer doesn’t see 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: JIAYANG_IDENTITY_ISSUER and JIAYANG_JWKS_URL must use https, except under jiayang dev (JIAYANG_DEV=1, on this machine) |
From from_env or new. The issuer or key set is http, and JIAYANG_DEV isn’t 1. Only jiayang dev uses http |
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, because its audience is wrong for that call.
TLS in a container
Section titled “TLS in a container”The crate trusts the public root certificates it ships with, and the certificates in the file SSL_CERT_FILE names.
A container app’s outbound HTTPS goes through our proxy, and we set SSL_CERT_FILE to that proxy’s certificate. rustls doesn’t read the environment, so the crate reads SSL_CERT_FILE itself. Without it, the key fetch from a container would fail and every caller would be refused.
Run it locally
Section titled “Run it locally”jiayang dev runs your app with cargo run on your machine behind a copy of our front door, with real tokens. See Local development.
jiayang devNext steps
Section titled “Next steps”- SDK overview: every claim in the token, and each check.
- Local development.
- Webhooks and public paths.