refactor: change OAuth scope separator from colon to dot for IDP compatibility
Many identity providers (AWS Cognito, Okta, Azure AD) reject or mishandle colons in OAuth scope names. This migrates all custom scopes from `resource:action` to `resource.action` format (e.g., `notes:read` → `notes.read`), which is universally accepted and aligns with industry conventions (Microsoft, Google). Includes Alembic migration 004 for stored scope strings and ADR-024 documenting the rationale and RFC references. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
899b9c7191
commit
29fd0486c9
@@ -0,0 +1,139 @@
|
||||
# ADR-024: OAuth Scope Separator Migration (colon to dot)
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-04-07
|
||||
**Supersedes:** Scope naming conventions in ADR-004, ADR-009, ADR-011
|
||||
|
||||
## Context
|
||||
|
||||
The MCP server defines application-level OAuth scopes using the `resource:action`
|
||||
pattern (e.g., `notes:read`, `calendar:write`). While the colon separator is
|
||||
visually intuitive and used by some OAuth implementations, it causes
|
||||
interoperability problems with many identity providers.
|
||||
|
||||
### IDP Compatibility Issues
|
||||
|
||||
Several widely-deployed identity providers reject or mishandle colons in OAuth
|
||||
scope names:
|
||||
|
||||
- **Keycloak**: Accepts colons but requires special configuration for scope
|
||||
mappers; colons can conflict with realm-qualified scope names
|
||||
- **Auth0**: Permits colons but treats them as namespace delimiters in their
|
||||
Resource Server API, leading to unexpected scope resolution behavior
|
||||
- **Azure AD / Entra ID**: Uses colons internally for delegated permissions
|
||||
(e.g., `User.Read`) and may reject custom scopes containing colons
|
||||
- **AWS Cognito**: Restricts scope names to alphanumeric characters, hyphens,
|
||||
periods, and underscores — colons are not allowed
|
||||
- **Okta**: Custom scopes are restricted to `[a-zA-Z0-9._-]`; colons are
|
||||
explicitly rejected
|
||||
|
||||
### RFC References
|
||||
|
||||
The OAuth 2.0 framework (RFC 6749, Section 3.3) defines scope values as:
|
||||
|
||||
> scope-token = 1*( %x21 / %x23-5B / %x5D-7E )
|
||||
|
||||
This technically permits the colon character (`%x3A`), so colons are
|
||||
spec-compliant. However, the specification also notes:
|
||||
|
||||
> The authorization server MAY fully or partially ignore the scope requested by
|
||||
> the client, based on the authorization server policy or the resource owner's
|
||||
> instructions.
|
||||
|
||||
In practice, many authorization servers impose stricter character restrictions
|
||||
than the RFC minimum. The dot separator (`.`) is universally accepted across all
|
||||
major OAuth/OIDC implementations and is the de facto convention used by:
|
||||
|
||||
- Microsoft Identity Platform (`User.Read`, `Mail.Send`)
|
||||
- Google OAuth (`https://www.googleapis.com/auth/calendar.readonly`)
|
||||
- MCP specification examples in RFC 9728 (OAuth Protected Resource Metadata)
|
||||
|
||||
### RFC 9728 (OAuth Protected Resource Metadata)
|
||||
|
||||
RFC 9728 defines the Protected Resource Metadata endpoint used by this server
|
||||
(`/.well-known/oauth-protected-resource`). While the RFC does not mandate a
|
||||
specific scope naming convention, its examples and the broader OAuth ecosystem
|
||||
favor dot-separated scopes for maximum interoperability.
|
||||
|
||||
## Decision
|
||||
|
||||
Replace the colon (`:`) separator with a dot (`.`) in all application-level
|
||||
OAuth scope names:
|
||||
|
||||
| Before | After |
|
||||
|--------|-------|
|
||||
| `notes:read` | `notes.read` |
|
||||
| `notes:write` | `notes.write` |
|
||||
| `calendar:read` | `calendar.read` |
|
||||
| `calendar:write` | `calendar.write` |
|
||||
| `todo:read` | `todo.read` |
|
||||
| `todo:write` | `todo.write` |
|
||||
| `contacts:read` | `contacts.read` |
|
||||
| `contacts:write` | `contacts.write` |
|
||||
| `files:read` | `files.read` |
|
||||
| `files:write` | `files.write` |
|
||||
| `tables:read` | `tables.read` |
|
||||
| `tables:write` | `tables.write` |
|
||||
| `deck:read` | `deck.read` |
|
||||
| `deck:write` | `deck.write` |
|
||||
| `cookbook:read` | `cookbook.read` |
|
||||
| `cookbook:write` | `cookbook.write` |
|
||||
| `sharing:read` | `sharing.read` |
|
||||
| `sharing:write` | `sharing.write` |
|
||||
| `news:read` | `news.read` |
|
||||
| `news:write` | `news.write` |
|
||||
| `collectives:read` | `collectives.read` |
|
||||
| `collectives:write` | `collectives.write` |
|
||||
| `semantic:read` | `semantic.read` |
|
||||
|
||||
Standard OIDC scopes (`openid`, `profile`, `email`, `offline_access`) are
|
||||
unchanged — they are defined by OIDC Core and do not use separators.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Universal IDP compatibility**: Dot-separated scopes work with every major
|
||||
identity provider without special configuration
|
||||
- **Industry alignment**: Matches the naming convention used by Microsoft,
|
||||
Google, and other major OAuth implementations
|
||||
- **No logic changes**: The authorization system uses string comparison and
|
||||
`startswith()` prefix matching — changing the separator character requires no
|
||||
algorithmic changes
|
||||
|
||||
### Negative
|
||||
|
||||
- **Breaking change**: Existing OAuth clients, stored tokens, and IDP
|
||||
configurations must be updated to use the new scope names
|
||||
- **Migration required**: An Alembic database migration updates stored scope
|
||||
strings in `app_passwords` and `login_flow_sessions` tables
|
||||
|
||||
### Migration
|
||||
|
||||
- **Database**: Alembic migration `004` handles `REPLACE(scopes, ':', '.')`
|
||||
on stored scope JSON
|
||||
- **Keycloak**: The realm export (`keycloak/realm-export.json`) has been updated;
|
||||
existing Keycloak deployments must re-import or manually update scope
|
||||
definitions
|
||||
- **Nextcloud OIDC app**: The `astrolabe` OAuth client hook
|
||||
(`26-configure-astrolabe-oauth.sh`) has been updated with new scope names
|
||||
- **Existing MCP clients**: Must update their scope requests to use dot
|
||||
separators; old colon-separated scope requests will be rejected
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Hyphen separator (`notes-read`)
|
||||
|
||||
Rejected: While universally compatible, hyphens are commonly used within scope
|
||||
component names (e.g., hypothetical `file-share.read`), creating ambiguity about
|
||||
which hyphen is the separator.
|
||||
|
||||
### Underscore separator (`notes_read`)
|
||||
|
||||
Rejected: Also universally compatible but less conventional in the OAuth
|
||||
ecosystem. Dot is the dominant separator in industry practice.
|
||||
|
||||
### Keep colons with IDP-specific workarounds
|
||||
|
||||
Rejected: Requires per-IDP configuration, documentation, and ongoing maintenance.
|
||||
The root cause is a poor separator choice, not an IDP deficiency.
|
||||
Reference in New Issue
Block a user