Skip to main content

stacks-auth

Use when implementing authentication, authorization, passkeys, TOTP/2FA, RBAC, gates, policies, session auth, token management, email verification, password resets, or rate limiting in a Stacks application. Covers the @stacksjs/auth package, config/auth.ts, app/Gates.ts, and app/Middleware/.

Informations de source

Dépôt
stacksjs/stacks
Dernière activité de la source
29 octobre 2026 à 02:17
Langue détectée de SKILL.md
anglais
Étoiles
636
Forks
20

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Explorateur de fichiers
2 fichiers

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
stacks-auth
description
Use when implementing authentication, authorization, passkeys, TOTP/2FA, RBAC, gates, policies, session auth, token management, email verification, password resets, or rate limiting in a Stacks application. Covers the @stacksjs/auth package, config/auth.ts, app/Gates.ts, and app/Middleware/.
license
MIT
compatibility
Bun >= 1.3.0, TypeScript
allowed-tools
Read Edit Write Bash Grep Glob
# Stacks Authentication & Authorization The `@stacksjs/auth` package provides comprehensive authentication and authorization for Stacks applications, built on `@stacksjs/ts-auth`. ## Key Paths - Core package source: `storage/framework/core/auth/src/` - Configuration: `config/auth.ts` - Security config: `config/security.ts` - Hashing config: `config/hashing.ts` - Application gates: `app/Gates.ts` - Application middleware: `app/Middleware/` - Middleware aliases: `app/Middleware.ts` - Auth types: `storage/framework/core/types/src/auth.ts` ## Source Files ``` auth/src/ ├── index.ts # All re-exports ├── authentication.ts # Auth class - core auth logic ├── authenticator.ts # 2FA and personal access client ├── client.ts # Client re-exports ├── middleware.ts # Auth middleware handler ├── rate-limiter.ts # RateLimiter class (5 attempts, 15min lockout) ├── passkey.ts # WebAuthn/Passkey support ├── password/reset.ts # Password reset flow ├── register.ts # User registration ├── user.ts # Auth user helpers ├── tokens.ts # Token CRUD, scopes, refresh tokens, OAuth clients ├── gate.ts # Authorization gates & policies ├── policy.ts # BasePolicy class + discovery ├── authorizable.ts # User authorization mixin ├── rbac.ts # Full RBAC system ├── email-verification.ts # Email verification flow └── session-auth.ts # Session-based SPA auth ``` ## Auth Class (authentication.ts) - Static Methods ### Login & Authentication - `Auth.attempt(credentials: AuthCredentials): Promise<boolean>` - validate credentials without creating token - `Auth.validate(credentials: AuthCredentials): Promise<boolean>` - checks credentials without setting the request's authenticated user - `Auth.login(credentials: AuthCredentials, options?: TokenCreateOptions): Promise<{ user, token } | null>` - login and create token - `Auth.loginUsingId(userId: number, options?: TokenCreateOptions): Promise<{ user, token } | null>` - login by user ID - `Auth.logout(): Promise<void>` - revoke current token ### Personal access tokens (Sanctum-shaped) `oauth_access_tokens` is polymorphic: `tokenable_type` holds the owner's TABLE name (`users`, `authors`) and `tokenable_id` its id there, so any model declaring `useAuth` can hold tokens - not only `User`. - `createToken(id, name, scopes, { tokenableType })` - mint one. Returns the plaintext ONCE (`plainTextToken`); the table stores a hash and nothing can recover it afterwards. `tokenableType` defaults to `users`. - `tokens(id, tokenableType?)` - list an owner's live tokens. - `tokenCan(scope)` / `tokenCanAll` / `tokenCanAny` / `tokenAbilities` - check the current request's token. - `revokeToken`, `revokeTokenById`, `revokeAllTokens(id, type?)`, `revokeOtherTokens(id, type?, { exceptTokenId? })` - revocation also revokes the paired refresh token, which a raw row delete does not. - `setTrailActor(id)` - attribute writes in a queue job or CLI run that has no request to read a user from. The `PersonalAccessToken` model maps the same table, so `owner.with('tokenable')` lists exactly what `createToken` minted. It deliberately generates no CRUD routes: minting and revoking both carry semantics a generic route does not. - `Auth.once(credentials: AuthCredentials): Promise<boolean>` - one-time auth without token - `Auth.requestUserTokenWithClient(credentials, clientId, clientSecret): Promise<{ token } | null>` authenticates both a legacy OAuth client and an end user password, then issues a user token. It is not the client credentials grant. - `Auth.requestToken(...)` is the deprecated compatibility alias for that same legacy exchange. New delegated integrations use the authorization-code provider. ### User State - `Auth.user(): Promise<UserModel | undefined>` - resolve the current request user through bearer/token-cookie state - `Auth.check(): Promise<boolean>` - is user authenticated? - `Auth.guest(): Promise<boolean>` - is user a guest? - `Auth.id(): Promise<number | undefined>` - get authenticated user ID - `Auth.setUser(user: UserModel | undefined): void` - set or clear the current request user ### Audited support viewing Enable `auth.impersonation` with an `eligible(userId)` policy covering suspended or deleted actors and subjects. `buddy migrate` installs the framework session and privileged-audit tables when enabled. `migrateImpersonationTables()` is the explicit installation API for isolated tests. Never install tables in a request. For tenant-scoped applications, configure `workspaceId(request, subjectId)` to resolve the viewed account's actual workspace membership. The resolver runs after support authority checks and its bounded identifier is included in the durable access audit. A raw client-supplied tenant header is not membership proof. Grant native RBAC role `super-admin` in guard `platform` through a trusted operator flow. `Auth.startImpersonation(subjectId, reason)` requires a live ordinary token, rejects nesting and self-viewing, and returns a separate 15-minute credential. The host should require a fresh password or passkey confirmation before calling it. Keep the real login credential. Attach the returned credential in the `x-stacks-impersonation` header alongside the ordinary bearer. Central `Auth.getUserFromToken()` resolves the viewed user only after checking the exact parent session, fresh platform role, actor/subject eligibility, expiry, revocation and a durable access audit. Support viewing permits GET only. HEAD is rejected because multiplexed application actions can handle it as a write. `requestImpersonation()` exposes trusted actor/subject metadata; ORM trails attribute writes to the original actor. `Auth.endImpersonation(credential)` requires the original actor request without the support header. Store viewing state independently from the actor's login and cached profile, show a persistent disclosure and Exit control, disable offline persistence, and clear the viewed page on expiry or denial. Platform administration routes must reject the support header and separately check the current platform role. `recordPrivilegedAccess(actorId, action, subjectId?)` is a fail-closed audit primitive for these routes; do not put request bodies or credentials in logs. For a privileged operation that must immediately observe role revocation by a different worker, use `hasFreshRole(user, roleName, guardName)` instead of the process-cached `hasRole`. It reads the configured RBAC store and propagates store errors. It does not authenticate the caller or create an audit trail; the operation must do both separately. ### Token Creation - `Auth.createTokenForUser(user, options?: TokenCreateOptions): Promise<NewAccessToken>` - `Auth.createToken(user, name?, abilities?): Promise<AuthToken>` ### Token Validation - `Auth.validateToken(token: string): Promise<boolean>` - validate bearer token - `Auth.getUserFromToken(token: string): Promise<UserModel | undefined>` - `Auth.currentAccessToken(): Promise<PersonalAccessToken | undefined>` ### Token Abilities (Scopes) - `Auth.tokenCan(ability: string): Promise<boolean>` - `Auth.tokenCant(ability: string): Promise<boolean>` - `Auth.tokenAbilities(): Promise<string[]>` - `Auth.tokenCanAll(abilities: string[]): Promise<boolean>` - `Auth.tokenCanAny(abilities: string[]): Promise<boolean>` ### Token Management - `Auth.tokens(userId?: number): Promise<PersonalAccessToken[]>` - `Auth.revokeToken(token: string): Promise<void>` - `Auth.revokeTokenById(tokenId: number): Promise<void>` - `Auth.revokeAllTokens(userId?: number): Promise<void>` - `Auth.revokeOtherTokens(userId?: number, { exceptTokenId? }): Promise<{ keptTokenId: number | null }>` - keeps `exceptTokenId`, else the request's own token; when neither resolves it revokes every token (fail closed) - `Auth.pruneExpiredTokens(): Promise<number>` - `Auth.pruneRevokedTokens(): Promise<number>` - `Auth.rotateToken(oldToken: string): Promise<AuthToken | null>` - `Auth.findToken(tokenId: number): Promise<PersonalAccessToken | null>` ### Utility - `Auth.guard(name?: string): typeof Auth` - select guard (returns self) - `Auth.viaRemember(): boolean` - always false currently - `Auth.clearState(): void` - clear cached user/token ## Token System (tokens.ts) ### Access Tokens - `tokens(userId: number): Promise<AccessToken[]>` - `findToken(plainTextToken: string): Promise<AccessToken | null>` - `currentAccessToken(): Promise<AccessToken | null>` - `createToken(userId, name?, scopes?, options?): Promise<PersonalAccessTokenResult>` - Options: `{ expiresInMinutes?, withRefreshToken?, refreshExpiresInDays? }` ### Refresh Tokens - `refreshToken(refreshTokenPlain, options?): Promise<RefreshTokenResult>` - `validateRefreshToken(refreshTokenPlain): Promise<boolean>` - `revokeRefreshToken(refreshTokenPlain): Promise<void>` - `revokeAllRefreshTokens(userId): Promise<void>` - `deleteExpiredRefreshTokens(): Promise<number>` - `deleteRevokedRefreshTokens(daysOld?): Promise<number>` ### Token Revocation - `revokeToken(plainTextToken): Promise<void>` - `revokeTokenById(tokenId): Promise<void>` - `revokeAllTokens(userId): Promise<void>` - `revokeOtherTokens(userId, type?, { exceptTokenId? }): Promise<{ keptTokenId: number | null }>` - same rule as `Auth.revokeOtherTokens` - `deleteExpiredTokens(): Promise<number>` - `deleteRevokedTokens(daysOld?): Promise<number>` ### Token Scopes - `tokenCan(scope): Promise<boolean>` - `tokenCant(scope): Promise<boolean>` - `tokenCanAll(scopes): Promise<boolean>` - `tokenCanAny(scopes): Promise<boolean>` - `tokenAbilities(): Promise<string[]>` - `parseScopes(scopes: string | string[] | null | undefined): TokenScopes` ### OAuth Clients - `clients(userId): Promise<OAuthClient[]>` - `findClient(clientId): Promise<OAuthClient | null>` - `createClient(options: CreateClientOptions): Promise<CreateClientResult>` - `revokeClient(clientId): Promise<void>` ### OAuth Provider and PKCE The authorization server is opt-in through `config/auth.ts` under `oauthProvider`. It is separate from social sign-in, where Stacks is the OAuth client. The provider profile is Authorization Code with S256 PKCE and rotating refresh tokens. Confidential client credentials are opt-in through `oauthProvider.clientCredentials` and mint access-only tokens without refresh tokens. Protected resource-server introspection is separately opt-in through `oauthProvider.introspection`; it authenticates a confidential client and requires that client to share a configured resource audience with the token. It does not support implicit or password grants or OpenID Connect. When enabled, the default auth route bundle registers: - `GET` and `POST /oauth/authorize` - `POST /oauth/token` - `POST /oauth/revoke` - `GET /.well-known/oauth-authorization-server` - owner-managed clients under `/auth/oauth/clients` - user-managed connected applications under `/auth/oauth/connections` Set a canonical `issuer`, then register every scope and resource explicitly. Resource audiences are absolute URIs and redirect URIs use exact matching. The consent view is `auth/oauth/consent` by default and can be overridden with `oauthProvider.consent.view`. Use `oauthProvider.consent.resolveWorkspace` when the application must bind consent to current server-owned workspace authority. Use `oauthProvider.subjectEligibility` when account status is represented by application data that is not simply the presence of a user row. The callback is rechecked before consent approval, delegated code exchange and refresh, and by introspection; returning false revokes the affected grant and reports the token inactive. Provider actions return 404 while `oauthProvider.enabled` is false. The token and revocation endpoints use protocol credentials and intentionally skip browser CSRF. Authorization approval, client management, and disconnect remain authenticated and CSRF protected. - `resolveOAuthProviderConfig(options)` returns `null` unless explicitly enabled - `generatePkceVerifier()` creates a 256-bit RFC 7636 verifier - `createS256CodeChallenge(verifier)` derives its S256 challenge - `verifyS256CodeChallenge(verifier, challenge)` validates without a plain fallback - `isValidPkceVerifier(value)` checks the required 43 to 128 character syntax ## Two-Factor Authentication (authenticator.ts) - `generateTwoFactorSecret(): string` - `generateTwoFactorToken(secret: Secret): Promise<Token>` - `verifyTwoFactorCode(token: Token, secret: Secret): Promise<boolean>` - `generateTwoFactorUri(user?, service?, secret?): string` - `createPersonalAccessClient(): Promise<Result<string, never>>` ### Re-exported from @stacksjs/ts-auth - `generateTOTP`, `verifyTOTP`, `generateTOTPSecret`, `totpKeyUri` ## Authorization Gates (gate.ts) Gate registrations, policies and before/after callbacks are shared by native module copies in one JavaScript process, including separately bundled Actions. `Gate.flush()` clears those registrations across every copy. This shares no request identity, token or authorization decision. Native `AuthorizationResponse` objects can cross bundle copies; arbitrary objects, forged prototypes and accessor fields are rejected. A conflicting shared-state version/shape refuses module initialization. This does not synchronize different processes or Workers. ### Gate Functions - `define<T>(ability: string, callback: GateCallback<T>): void` - `policy(model: string | { name }, policyClass: new () => Policy): void` - `before(callback): void` - run before any gate check - `after(callback): void` - run after any gate check - `allows(ability, user, ...args): Promise<boolean>` - `denies(ability, user, ...args): Promise<boolean>` - `can(ability, user, ...args): Promise<boolean>` - `cannot(ability, user, ...args): Promise<boolean>` - `any(abilities[], user, ...args): Promise<boolean>` - `all(abilities[], user, ...args): Promise<boolean>` - `none(abilities[], user, ...args): Promise<boolean>` - `authorize(ability, user, ...args): Promise<AuthorizationResponse>` - throws on deny - `inspect(ability, user, ...args): Promise<AuthorizationResponse>` - detailed result; invalid callback returns throw - `has(ability): boolean` - `hasPolicy(model): boolean` - `abilities(): string[]` - `getPolicyFor<T>(model: T): Policy<T> | null` - `flush(): void` - clear all gates ### Gate Facade - `Gate.define()`, `Gate.can()`, etc ### AuthorizationResponse Class - `static allow(message?): AuthorizationResponse` - `static deny(message?, code?): AuthorizationResponse` - `allowed(): boolean`, `denied(): boolean` - `authorize(): void` - throws AuthorizationException if denied ### Policy Interface Methods: `before?`, `viewAny?`, `view?`, `create?`, `update?`, `delete?`, `restore?`, `forceDelete?` ### BasePolicy Abstract Class Protected helpers: `allow(message?)`, `deny(message?, code?)`, `denyIf(condition)`, `denyUnless(condition)`, `allowIf(condition)` ## RBAC System (rbac.ts) ### Role Management - `Rbac.createRole(name, guardName?, description?): Promise<RoleRecord>` - `Rbac.findRole(name, guardName?): Promise<RoleRecord | null>`
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub