Core Docs

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:

TierBehaviorExample ScopesUse Case




SimpleAlways allowed if token is validprofile.read, email.read, app.accessBasic app functionality
AdvancedRequires blacklist checkprofile.write, email.write, data.exportSensitive mutations
CriticalRequires 2FA re-verificationpassword.change, 2fa.modify, account.delete, app.revokeAccount 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




ScopeTierDescription














profile.readSimpleRead display name, avatar
profile.writeAdvancedChange display name, avatar
email.readSimpleRead email address
email.writeAdvancedChange email address
phone.readSimpleRead phone number
phone.writeAdvancedChange phone number
app.accessSimpleAccess the app at all
data.exportAdvancedExport user data from the app
password.changeCriticalChange account password
2fa.modifyCriticalEnable/disable/change 2FA
account.deleteCriticalDelete the master account
app.revokeCriticalRevoke app access
sessions.manageAdvancedView/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 → proceed


Critical 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.