一键导入
auth
Use when implementing authentication in pocopine apps — JWT verification, credentials, OAuth providers, session management, or guards
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when implementing authentication in pocopine apps — JWT verification, credentials, OAuth providers, session management, or guards
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when turning a Claude Design (or any mockup/design) into a well-structured pocopine app — choosing app architecture (a store plus layout/leaf components), translating inline styles into Pine Stylekit, wiring icons and resizable regions, and verifying the result.
Use when working with the pocopine Pine icons feature — the `icon!` proc macro for compile-time Rust SVG embedding, or the `<pine-icon>` template primitive with `register_icons!` for tree-shaking-friendly template rendering.
Use when building styles with Pine Stylekit, the Pocopine-native utility-CSS compiler, or working with @theme tokens and CSS generation in Rust/WASM projects
Use when building enter/leave transitions, layout animations, stagger effects, or spring-physics motion in pocopine components
Use when defining background jobs, enqueueing work, configuring workers, or troubleshooting job execution in pocopine apps
Use when setting up or working with Pocopine's managed .client.ts modules for importing npm SDKs (Firebase, analytics, etc.) with typed Rust facades
| name | auth |
| description | Use when implementing authentication in pocopine apps — JWT verification, credentials, OAuth providers, session management, or guards |
This teaches the pocopine authentication domain model, JWT verification, credentials (passwords/OTP), the Provider trait, and client-side session management. The framework is provider-neutral and opinionated about security defaults (algorithm pinning, constant-time auth, Argon2id).
Domain model (pocopine-auth)
Principal — request identity; .user() returns Option<Arc<AuthUser>>, .has_role(r) / .has_permission(p) for checksAuthUser — canonical user shape with id, email, email_verified, name, roles: Vec<Role>, permissions: Vec<Permission>, and claims: HashMap<String, serde_json::Value> for provider-specific fieldsRole(Cow<'static, str>) — stringly typed; .admin(), .staff(), .user(), .named(s) factory methodsPermission(Cow<'static, str>) — identical shape to RoleJWT verification (pocopine-auth-jwt)
JwtVerifier::custom(JwtConfig) / JwtVerifier::from_provider(P: Provider) — construct the verifierJwtConfig — declarative verification shape: keys: KeySource, issuer: Option<String>, audience: Option<Vec<String>>, algorithms: Vec<Algorithm> (pinned per config), sources: Vec<TokenSource> (Bearer, Cookie), claim_map: ClaimMap, leeway: DurationKeySource::Jwks { url, cache_ttl, refresh_cooldown } (OIDC, Firebase, Clerk, Auth0) / KeySource::Hmac { secret } (Supabase, pocopine-issued) / KeySource::StaticJwks(JwkSet) (tests)Algorithm::Rs256 | Hs256 | Es256 | ... — always pinned at config construction; never accepts alg: noneTokenSource::Bearer / TokenSource::Cookie(name) — where to find tokensClaimMap { id, email, email_verified, name, roles, permissions } — paths to extract claims; defaults to OIDC shapeProvider trait — app-local or third-party configs (Firebase, Okta, Cognito) implement this; fn jwt_config(self) -> Result<JwtConfig, JwtAuthError>JwtIssuer::hs256(secret, issuer, audience) / JwtIssuer::rs256(private_key, key_id, issuer, audience) — mint session tokensCredentials (pocopine-auth-credentials)
PasswordCredentials trait — app implements on own user type; methods: fn id(&self) -> &str, fn email(&self) -> &str, fn password_hash(&self) -> Option<&str>, fn to_auth_user(&self) -> AuthUserUserStore trait — async fn find_by_email(&self, email: &str) -> Result<Option<User>, Error>, find_by_id, create(email, password_hash)TokenStore trait — for ephemeral reset/verify tokens; async fn put(token_hash, record), take(token_hash) (single-use), purge_expired(now_ms)Credentials::new(secret, store, tokens) — builder; .with_session_ttl(Duration), .with_argon_params(Argon2Params), .with_issuer(name), .with_audience(name), .with_password_validator(closure)Argon2Params — OWASP defaults: m=64MiB, t=3, p=4; .owasp_minimum() rejects weaker configsinstall_routes(router): POST /_pocopine/auth/signup { email, password }, /login, /logout; returns { token: String, user: AuthUser }Client session (pocopine-auth-client, wasm-side)
auth_plugin() — builder; .login_route(path), .with_bearer_middleware(bool), .with_token_storage(impl TokenStorage), .with_cross_tab_sync(bool), .with_token_refresh(impl TokenRefresh), .wait_for_initial_auth_check(bool)AuthSession — plugin service; .sign_in(token, principal), .sign_out(), .principal(), .epoch() (bumped on identity change)BearerMiddleware — installs on fetch chain; adds Authorization: Bearer <token> header; drops response if identity changed mid-flightTokenStorage trait — fn load() -> Option<String>, save(token: &str), clear(); provided: LocalStorage::new(key), SessionStorage::new(key), InMemory (default)TokenRefresh trait — fn refresh(&self) -> TokenRefreshFuture (dyn Future<Output=Result<String, ServerError>>); coalesces concurrent 401s into single refresh callpredicate_guard(Predicate) — adapts require_auth(), require_role(r), require_permission(p), all_of(...), any_of(...) into route guardsGuards & predicates (pocopine-auth, both sides)
#[pocopine::server(guard = require_login)] — enforces authentication on the server; fails with ServerError::Unauthorized if anonymous#[pocopine::server(guard = require_role("admin"))] — enforces specific role#[pocopine::server(public)] — no guard; anonymous ok#[server(idempotent)] — marks read-only functions safe to replay on token refresh1. First-party email + password (server side)
// From crates/pocopine-auth-credentials examples
use pocopine_auth_credentials::Credentials;
use pocopine_auth_jwt::{JwtVerifier, SecretBytes};
use pocopine_server::{axum::Router, RouterAuthExt, Server};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let secret = SecretBytes::new(
std::env::var("POCOPINE_AUTH_SECRET")
.expect("set POCOPINE_AUTH_SECRET to >= 32 random bytes"),
);
let pool = sqlx::PgPool::connect(&std::env::var("DATABASE_URL").unwrap()).await?;
let creds = Credentials::new(
secret,
my_app::PgUserStore { pool: pool.clone() },
my_app::PgTokenStore { pool },
);
let verifier = JwtVerifier::custom(creds.verifier_config())?;
Server::new(Router::new())
.with_auth(verifier)
.plugin(creds)
.serve("0.0.0.0:3000")
.await
}
2. Firebase auth provider (app code)
// From docs/auth-jwt-providers.md
use pocopine_auth_jwt::{JwtVerifier, Provider};
use std::time::Duration;
#[non_exhaustive]
pub struct Firebase {
pub project_id: String,
pub session_cookie: bool,
pub cache_ttl: Duration,
pub refresh_cooldown: Duration,
pub leeway: Duration,
}
impl Firebase {
pub fn new(project_id: impl Into<String>) -> Self {
Self {
project_id: project_id.into(),
session_cookie: false,
cache_ttl: Duration::from_secs(3600),
refresh_cooldown: Duration::from_secs(30),
leeway: Duration::from_secs(60),
}
}
}
impl Provider for Firebase {
fn jwt_config(self) -> Result<JwtConfig, JwtAuthError> {
let mut sources = vec![TokenSource::Bearer];
if self.session_cookie {
sources.push(TokenSource::Cookie(Cow::Borrowed("__session")));
}
Ok(JwtConfig {
keys: KeySource::Jwks {
url: "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com".into(),
cache_ttl: self.cache_ttl,
refresh_cooldown: self.refresh_cooldown,
},
issuer: Some(format!("https://securetoken.google.com/{}", self.project_id)),
audience: Some(vec![self.project_id]),
algorithms: vec![Algorithm::Rs256],
leeway: self.leeway,
sources,
revocation: None,
claim_map: ClaimMap::oidc(),
required_scopes: vec![],
})
}
}
let verifier = JwtVerifier::from_provider(Firebase::new("my-project-id"))?;
3. Client-side session + guards
// From docs/auth-client.md
use pocopine_auth_client::{auth_plugin, predicate_guard, AuthSession};
use pocopine_auth::require_auth;
use pocopine_core::{Plugin, RouteComponent, RouteConfig};
#[derive(Default)]
pub struct Dashboard {
user_email: String,
}
impl RouteComponent for Dashboard {
fn config() -> RouteConfig<Self> {
RouteConfig::new()
.guard(predicate_guard(require_auth()))
}
}
impl Dashboard {
pub fn on_setup(&mut self, session: Plugin<AuthSession>) {
if let Some(user) = session.principal().user() {
self.user_email = user.email.clone().unwrap_or_default();
}
}
}
pocopine::app! {
components: [Dashboard],
plugins: [
auth_plugin()
.login_route("/login")
.with_bearer_middleware(true)
.with_token_storage(pocopine_auth_client::storage::LocalStorage::new("auth_token"))
.with_cross_tab_sync(true),
],
routes: [("/dashboard", Dashboard)],
}
4. Guarded server function
// From crates/pocopine/tests/auth_middleware.rs
use pocopine_auth::require_login;
#[pocopine::server(guard = require_login, idempotent)]
async fn get_dashboard_data() -> pocopine::ServerResult<DashboardData> {
let principal = pocopine::server::principal()?;
let user = principal.require_user()?;
// Use user.id, user.email, user.roles, etc.
Ok(DashboardData { /* ... */ })
}
JWT confusion attacks. JwtConfig::validate() runs at construction and rejects algorithm-pinning mismatches (HS256 + JWKS, RS256 + HMAC) with a panic. This is intentional and enforces security invariants at deployment time, not request time. Preset constructors are guaranteed correct; custom configs must get this right.
alg: none impossible by construction. The Algorithm enum has no None variant; the whitelist cannot include it.
Bearer vs. Cookie precedence. If both are configured and a request supplies both, Bearer wins (OIDC convention). The cookie is ignored.
JWKS cache on kid miss. If a token's kid (key ID) isn't in the cache, the verifier fetches fresh JWKS. To defend against adversarial clients sending random kid values, a per-config refresh_cooldown (default 30s) rate-limits fetches. Legitimate key rotation is usually hours apart.
Role no longer an enum. Old code using Role::Admin breaks; migrate to Role::admin() factory or .named("admin") for custom roles. This closes the magic-string deserialization footgun (a JWT claim "admin" won't auto-promote to a privileged variant).
Principal is cheap to clone. User is Option<Arc<AuthUser>>, so cloning the principal is O(1). Middleware and guards can pass it around freely.
Principal::user() is Option<Arc<AuthUser>>. Access the user with .user(), not direct field access; the field is private.
Credentials doesn't define a User type. The framework doesn't own user identity — apps do. Implement PasswordCredentials on your own struct (Postgres row, custom struct, anything). This lets apps that need multi-credential flows (password + OAuth + passkey) use a single user table.
PasswordCredentials::password_hash() returns Option<&str>. This is intentional — OAuth-only users have None, and the login handler treats that the same as "user not found," defending against "is this email a Google account?" enumeration.
Email lowercased before store lookup. The credentials router lowercases email before calling UserStore::find_by_email, so a plain UNIQUE constraint in Postgres suffices (no LOWER()-indexed expression needed).
Token storage is required for cross-tab sync. with_cross_tab_sync(true) without with_token_storage(...) broadcasts sign-in/out to peer tabs but they have no way to read the new token. The broadcast tells peers "something changed" but carries no token over the channel.
Route guards are UX, not authorization. Client-side guards prevent paint of forbidden pages. The security boundary is the server. Every guarded #[server] function must carry its own #[server(guard = ...)] policy; a determined attacker can edit their JWT in localStorage or call the function directly with curl.
Refresh only works on #[server(idempotent)]. Non-idempotent calls (those that create/delete/modify) are not retried on 401, even if refresh succeeds. Mark all GET-shaped and read-only POST calls with idempotent so the bearer middleware can refresh + replay them transparently.
Secret rotation requires coordination. POCOPINE_AUTH_SECRET rotation blocks until all verifiers are updated and in-flight tokens are expired.
RFCs
crates/pocopine-auth-jwt; one engine, declarative config per provider, algorithm pinning, JWKS caching, verifier middleware flow)pocopine-auth-credentials and the Provider trait (first-party email + password, argon2id, constant-time login, extensible provider shape for third-party IdPs)Crates
pocopine-auth — Principal, AuthUser, Role, Permission, Predicate (guard combinators), AuthProvider / SessionStore traitspocopine-auth-jwt — JwtVerifier, JwtConfig, KeySource, TokenSource, ClaimMap, Provider trait, JwtIssuer, Algorithm pinning, SecretBytespocopine-auth-credentials — Credentials builder, PasswordCredentials trait, UserStore / TokenStore traits, Argon2Params (OWASP defaults)pocopine-auth-client — auth_plugin(), AuthSession, BearerMiddleware, TokenStorage trait, TokenRefresh trait, predicate_guard()Docs
docs/auth-jwt-providers.md — Provider contract, integration test pattern, community provider crates listdocs/auth-credentials.md — Postgres + sqlx end-to-end walkthrough for signup/login/logout with PasswordCredentials, UserStore, TokenStoredocs/auth-client.md — Wasm-side full walkthrough: bearer middleware, AuthSession, token storage, cross-tab sync, refresh, guardsdocs/auth-phone-otp-tutorial.md — Build-it-yourself OTP pattern using the same primitives (Twilio + JwtIssuer, single-use hashed tokens, rate limiting); production-ready until pocopine-auth-otp ships