Auth & Authorization Skill
When to use this skill
Use this skill when a task touches:
- authentication, login state, sessions, JWTs, bearer tokens, cookies, API keys, SSO, or auth providers
- authorization, roles, permissions, guards, policies, or access checks
- tenant, organization, workspace, project, team, or account boundaries
- protected API routes, protected frontend pages, middleware, route handlers, or use-cases
- Supabase Auth, Auth.js/NextAuth, Firebase Auth, custom JWT, API key auth, or enterprise SSO
- 401, 403, or security-sensitive 404 behavior
- review of whether a user is allowed to read, update, delete, invite, bill, generate, export, or administer something
If the task is only UI visibility, still check whether server-side authorization exists.
Goal
Separate identity from permission.
Authentication answers:
Who is this caller?
Authorization answers:
Can this caller perform this action on this resource in this context?
Do not treat a logged-in user as automatically authorized.
Core rules
- Authentication and authorization are separate concerns.
- Authentication is centralized in guards, middleware, or provider adapters.
- Application/use-case code receives a normalized
AuthContext. - Application/use-case code must not receive raw tokens, raw sessions, provider SDK user objects, request headers, or cookies.
- Provider SDKs are infrastructure details and must stay behind adapters.
- Authorization is enforced server-side.
- Client-side permission checks are UX helpers only; they are not security controls.
- Business-sensitive use-cases must check permission before performing mutations or sensitive reads.
- Tenant-scoped resources must enforce
organizationId/tenantIdboundaries at query and use-case level. - Role may be used for user management, but business authorization should be permission-based.
- Permissions use the
resource:actionformat. - API keys must be hashed at rest and shown only once at creation time.
- Missing or invalid auth returns
401. - Authenticated but unauthorized access returns
403. - Cross-tenant or resource-enumeration-sensitive access may return
404.
Standard AuthContext
Use a normalized auth context instead of provider-specific objects.
export type AuthType = "session" | "bearer-token" | "api-key";
export type AuthContext = {
userId: string;
organizationId?: string;
roles: string[];
permissions: string[];
authType: AuthType;
};
For API keys:
export type ApiKeyAuthContext = {
authType: "api-key";
apiKeyId: string;
organizationId: string;
permissions: string[];
};
When a use-case needs authentication, pass AuthContext explicitly:
await updateProject({
projectId,
input,
authContext,
});
Do not pass:
request.headers.authorization
Do not pass provider SDK session objects into use-cases.
Authentication provider adapters
Auth providers are interchangeable details.
Supported provider examples:
- Supabase Auth
- Auth.js / NextAuth
- Firebase Auth
- custom JWT
- session cookie
- API key auth
- enterprise SSO
Use an adapter interface:
export interface AuthProvider {
authenticate(request: Request): Promise<AuthContext | null>;
}
A route/guard can normalize provider-specific auth:
const authContext = await AUTH_GUARD.requireUser(request);
Application/domain logic must not import auth provider SDKs. Those imports belong in adapters, infrastructure, middleware, or composition root.
Guard standard
Protected routes must be explicit.
export async function updateProjectController(request: Request) {
const authContext = await AUTH_GUARD.requireUser(request);
const input = UPDATE_PROJECT_SCHEMA.parse(await request.json());
const result = await updateProject({
projectId,
input,
authContext,
});
return { data: serializeProject(result) };
}
The controller/route handler should authenticate, validate input, call a use-case, and serialize output. It should not contain business authorization rules unless the framework requires a thin guard layer.
Permission model
Prefer permission-based business authorization.
Permission format:
resource:action
Examples:
user:read
user:update
project:create
project:update
project:delete
billing:manage
organization:invite-member
api-key:create
api-key:revoke
ai-generation:create
Roles may map to permissions, but do not hard-code role checks in many places if permissions can express the rule.
Avoid:
if (authContext.roles.includes("admin")) {
// allow everything
}
Prefer:
await PERMISSIONS.require(authContext, "project:update", {
organizationId: project.organizationId,
});
Authorization location
Authorization must be enforced in the application/use-case layer for business-sensitive operations.
Good:
export async function updateProject(params: {
projectId: string;
input: UpdateProjectInput;
authContext: AuthContext;
}) {
const project = await PROJECT_REPOSITORY.findByIdForOrganization(
params.projectId,
params.authContext.organizationId,
);
if (!project) {
throw new AppError(PROJECT_ERROR_CODE.PROJECT_NOT_FOUND, "Project not found.", 404);
}
await PERMISSIONS.require(params.authContext, "project:update", {
organizationId: project.organizationId,
});
return PROJECT_REPOSITORY.update(project.id, params.input);
}
Bad:
// UI hides the button, so backend does not check permission.
Bad:
// Controller checks a role, but the use-case can be called elsewhere without authorization.
Tenant and organization boundary
For multi-tenant systems, permission checks are not enough. Queries must also be tenant-safe.
Prefer repository methods like:
findByIdForOrganization(id: string, organizationId: string)
Avoid tenant-scoped generic methods like:
findById(id: string)
unless they are clearly internal/admin-only:
findByIdForAdmin(id: string)
Tenant-scoped resource access must include organizationId or tenantId in the query.
For cross-tenant access, prefer 404 when resource existence should not be leaked.
HTTP status behavior
Use this status mapping:
| Situation | Status |
|---|---|
| No token/session/API key | 401 |
| Invalid token/session/API key | 401 |
| Expired token/session | 401 |
| Authenticated but missing permission | 403 |
| Resource does not exist | 404 |
| Resource belongs to another tenant and existence should be hidden | 404 |
| Resource exists but user is not allowed and existence is not sensitive | 403 |
| API key lacks required scope | 403 |
Use the standard error envelope:
{
"error": {
"code": "FORBIDDEN",
"message": "You do not have permission to perform this action.",
"requestId": "req_123"
}
}
Do not return provider raw auth errors to clients.
API key auth
API keys are credentials and must be handled as secrets.
Rules:
- Store only a hash of the API key.
- Show the raw key only once when created.
- Use a prefix for identification and environment separation.
- Associate keys with organization/tenant context.
- Assign explicit permissions/scopes.
- Support revocation.
- Never log raw API keys.
- Never return raw API keys after creation.
Example prefix style:
sk_live_...
sk_test_...
Supabase Auth notes
When using Supabase:
- Supabase is an auth provider adapter, not a domain dependency.
- Supabase JS client must not leak into domain/application logic.
- Client-side anon access must rely on reviewed RLS policies.
- Service role key is server-only.
- Server privileged operations must go through backend adapters/use-cases.
- RLS does not replace application-level business authorization when use-cases execute privileged operations.
Firebase Auth notes
When using Firebase:
- Firebase Auth/Admin SDK belongs in an adapter.
- Firestore Security Rules are security controls and must be reviewed/tested.
- Backend use-cases still require explicit permission checks when using admin privileges.
- Do not trust client-side checks alone.
Auth transport selection
Do not force one transport for every project.
Use these defaults:
- frontend/fullstack apps: session cookie or provider session can be used
- public/backend APIs: bearer token or API key can be used
- automation/service integrations: API key or signed webhook can be used
- enterprise apps: SSO can be added behind the provider adapter
The normalized AuthContext remains stable regardless of transport.
Coding workflow
When adding or changing auth-related code:
- Identify whether the task is authentication, authorization, or both.
- Determine the caller type: user session, bearer token, API key, webhook, system job, or admin.
- Normalize caller identity into
AuthContext. - Validate input before use-case logic.
- Load tenant-scoped resources with tenant-safe repository/query methods.
- Perform server-side permission checks in the use-case/application layer.
- Return 401/403/404 according to the status rules.
- Do not leak provider error details, resource existence, tokens, or secrets.
- Add or update tests for protected paths, forbidden paths, and tenant boundaries.
- Update OpenAPI security definitions if the API contract changed.
- Update docs/ADR if the auth model or permission model changed.
Review checklist
Check every auth-related change for:
- Does application/use-case code receive
AuthContext, not raw token/session/header/cookie? - Is authentication centralized in guard/middleware/provider adapter?
- Is provider SDK usage isolated to adapter/infrastructure/composition code?
- Is authorization enforced server-side?
- Does the use-case check permission before sensitive read/write?
- Is the permission expressed as
resource:action? - Are role checks limited and not used as broad bypasses?
- Are tenant-scoped queries filtered by
organizationIdortenantId? - Does cross-tenant access avoid leaking resource existence when needed?
- Are 401, 403, and 404 used correctly?
- Are API keys hashed and never logged?
- Are API key scopes/permissions explicit?
- Are Supabase RLS / Firestore Security Rules reviewed when used?
- Are tests added for unauthenticated, unauthorized, allowed, and cross-tenant cases?
- Is OpenAPI security metadata updated for protected endpoints?
Required tests
For protected endpoints/use-cases, add tests for:
- unauthenticated request returns 401
- invalid token/session/API key returns 401
- authenticated user missing permission returns 403
- cross-tenant resource access returns 404 or 403 according to project policy
- allowed permission succeeds
- tenant-scoped query cannot access another organization’s resource
- API key auth works with valid key
- API key missing scope returns 403
- API key raw value is not stored
- provider adapter maps provider failures safely
Business-sensitive use-cases must not be merged without authorization tests.
Good examples
Use-case receives AuthContext
export async function createProject(params: {
input: CreateProjectInput;
authContext: AuthContext;
}) {
await PERMISSIONS.require(params.authContext, "project:create", {
organizationId: params.authContext.organizationId,
});
return PROJECT_REPOSITORY.createForOrganization({
organizationId: params.authContext.organizationId,
name: params.input.name,
});
}
Tenant-safe read
const project = await PROJECT_REPOSITORY.findByIdForOrganization(
projectId,
authContext.organizationId,
);
if (!project) {
throw new AppError(PROJECT_ERROR_CODE.PROJECT_NOT_FOUND, "Project not found.", 404);
}
API key hashed storage
const rawKey = createApiKeySecret();
const keyHash = await hashSecret(rawKey);
await API_KEY_REPOSITORY.create({
keyHash,
prefix: getKeyPrefix(rawKey),
organizationId,
permissions,
});
Anti-patterns
Do not:
- pass raw
Authorizationheaders into use-cases - pass provider SDK user/session objects into use-cases
- import Supabase/Firebase/Auth.js/JWT SDKs in domain/application logic
- rely on frontend button hiding as authorization
- use
role === "admin"as a broad bypass without permission reasoning - query tenant-scoped resources by ID alone
- store API keys in plain text
- log tokens, cookies, sessions, API keys, or auth provider raw errors
- return raw provider auth errors to clients
- return 403 for cross-tenant access when resource enumeration is sensitive
- allow background jobs/system users to bypass permissions without explicit system context
Related skills
Use together with:
security-baselinefor secure-by-default checksapi-designfor protected endpoint designerror-handlingfor 401/403/404 error mappingvalidationfor auth input validationdatabase-persistencefor tenant-safe persistence accesstestingfor auth and tenant boundary testsopenapifor security scheme documentation