Scope Model
Scope Model
> Status: Placeholder. The three-tier scope system described below is the target model for Phase 2+. For Phase 1 (current), app authorization is a simple yes/no — the user authorizes the app for their data (personal info, profile, email) without granular scoping. The authorization screen says "This app may request your personal info, like name, email, etc." and the user approves or denies.
>
> The scope registry and tier system below are preserved as the design target. Implementation begins when apps need differentiated permissions.
The three-tier permission system that controls what an access token can do.
Three-Tier Scope System
Scopes control what an access token can do. core.auth implements a three-tier model with increasing security requirements:
| Tier | Behavior | Example Scopes | Use Case |
| Simple | Always allowed if token is valid | profile.read, email.read, app.access | Basic app functionality |
| Advanced | Requires blacklist check | profile.write, email.write, data.export | Sensitive mutations |
| Critical | Requires 2FA re-verification | password.change, 2fa.modify, account.delete, app.revoke | Account security operations |
Why Three Tiers?
Not all operations carry the same risk. Reading a user's display name is low-risk. Changing their password is high-risk. Treating them the same is either too permissive (everything is easy) or too restrictive (everything requires 2FA).
The three-tier model lets you be permissive for routine operations while adding friction only where it matters.
Scope Assignment
Scopes are assigned at two levels:
App-Level (What the App Can Request)
// registered_apps.allowed_scopes
["profile.read", "email.read", "app.access", "profile.write"]An app can only request scopes from its
allowed_scopes set. If an app has ["profile.read"] and tries to request password.change, core.auth rejects the request.Token-Level (What This Token Has)
// Access token payload
{
"sub": "user-uuid",
"app": "app-slug",
"scopes": ["profile.read", "email.read", "app.access"],
"sid": "session-uuid",
"iat": 1718899200,
"exp": 1718899800
}A user can further restrict scopes per-app. For example: "I allow this todo app to read my profile but not my email." The token only gets the intersection of (app's allowed scopes) ∩ (user's granted scopes).
Scope Hierarchy
registered_apps.allowed_scopes (maximum this app can request)
∩
user_app_access.granted_scopes (what user allows for this app)
=
access_token.scopes (what this token can actually do)Standard Scope Registry
| Scope | Tier | Description |
profile.read | Simple | Read display name, avatar |
profile.write | Advanced | Change display name, avatar |
email.read | Simple | Read email address |
email.write | Advanced | Change email address |
phone.read | Simple | Read phone number |
phone.write | Advanced | Change phone number |
app.access | Simple | Access the app at all |
data.export | Advanced | Export user data from the app |
password.change | Critical | Change account password |
2fa.modify | Critical | Enable/disable/change 2FA |
account.delete | Critical | Delete the master account |
app.revoke | Critical | Revoke app access |
sessions.manage | Advanced | View/revoke other sessions |
Custom scopes can be defined per-app, but must be registered in
allowed_scopes and classified into a tier.Critical Scope Verification
When an access token attempts a critical-scope operation, the app must redirect the user through a 2FA re-verification flow at core.auth. This proves the user is present and recently verified their identity.
Flow
App requests password change
│
▼
App checks: is this a critical scope? → yes
│
▼
App redirects to core.auth/verify-2fa
│
▼
User completes 2FA challenge (TOTP code, passkey, etc.)
│
▼
core.auth issues critical_action_token (2 min TTL, single-use)
│
▼
App includes critical_action_token in the password change request
│
▼
core.auth validates both access token + critical action token → proceedCritical Action Token
A short-lived (2 minutes), single-use token that proves the user recently completed 2FA. It's bound to:
- The specific user
- The specific access token session
- The specific scope requested
It cannot be reused, it cannot be used for a different scope, and it cannot be used after 2 minutes.
Why Not Just Check 2FA on the Access Token?
Because 2FA verification is a one-time event. If you bake it into the access token, the token is "2FA-verified" for its entire 10-minute lifetime — even if the user walked away from the computer. The critical action token approach requires the user to be present at the moment of the critical action.
Scope Checking Middleware
For apps integrating with core.auth, scope checking should be middleware:
// Pseudocode
function checkScope($required_scope, $access_token) {
$payload = decodeAccessToken($access_token);
if (!in_array($required_scope, $payload['scopes'])) {
if (isCriticalScope($required_scope)) {
return redirect('/auth/verify-2fa?scope=' . $required_scope);
}
return 403; // Forbidden
}
if (isAdvancedScope($required_scope)) {
if (isBlacklisted($access_token)) {
return 401; // Token revoked
}
}
return proceed();
}Next: Token Mechanics — HMAC signing, refresh rotation, family detection.