Files
mcp-nextcloud/docs/ADR-024-scope-separator-migration.md
T
Chris CoutinhoandClaude Opus 4.6 29fd0486c9 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>
2026-04-07 10:07:02 +02:00

140 lines
5.4 KiB
Markdown

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