Auth authorization
Skill batur/ts-baseline-docs/.agents/skills/auth-authorization
TypeScript software architecture baseline with VS Code Agent Skills for AI coding, review, security, testing, API design, and delivery standards.
npx -y skills add batur/ts-baseline-docs --skill auth-authorizationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Defines authentication and authorization rules for TypeScript applications. Use when adding or reviewing auth guards, AuthContext, provider adapters, role/permission checks, tenant or organization boundaries, API key auth, 401/403/404 behavior, protected routes, and server-side access control.
The file declares its own license as Proprietary. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
13.3 KB, as published. Nobody here has run it
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