Skip to content

scope emitted as JSON array in access token and introspection response (RFC 9068 §2.2.3 / RFC 7662 §2.2 require a space-delimited string) #957

Description

@hstern

Summary

In simpleidserver/idserver:6.0.4, the OAuth 2.0 scope claim is serialized as a JSON array in both:

  1. the access-token JWT payload, and
  2. the RFC 7662 token introspection response.

Per the relevant specifications, scope is a space-delimited string, not an array. This causes spec-compliant resource servers and introspection clients to reject otherwise-valid tokens.

Environment

  • Image: simpleidserver/idserver:6.0.4 (Docker Hub), StorageConfiguration__Type=INMEMORY, plain HTTP, default master realm.
  • Observed: June 2026.

Steps to reproduce

ISS=http://127.0.0.1:8080/master

# 1. Management token (seeded admin client)
MGMT=$(curl -s -X POST "$ISS/token" \
  -d grant_type=client_credentials -d client_id=SIDS-manager -d client_secret=password \
  -d "scope=clients scopes users" | jq -r .access_token)

# 2. Create an API scope + a confidential client
curl -s -X POST "$ISS/scopes" -H "Authorization: Bearer $MGMT" -H "Content-Type: application/json" \
  -d '{"name":"mail_read","type":1,"protocol":0,"is_exposed":true}'
curl -s -X POST "$ISS/clients" -H "Authorization: Bearer $MGMT" -H "Content-Type: application/json" \
  -d '{"id":"<uuid>","client_id":"demo","client_type":"MACHINE","is_public":false,
       "grant_types":["client_credentials"],"token_endpoint_auth_method":"client_secret_post",
       "access_token_type":"Jwt",
       "client_secrets":[{"id":"<uuid>","value":"demosecret","alg":0,"is_active":true}],
       "scopes":[{"name":"openid"},{"name":"mail_read"}]}'

# 3. Mint a token and decode the JWT payload
AT=$(curl -s -X POST "$ISS/token" \
  -d grant_type=client_credentials -d client_id=demo -d client_secret=demosecret \
  -d "scope=openid mail_read" | jq -r .access_token)
echo "$AT" | cut -d. -f2 | tr '_-' '/+' | base64 -d | jq '{scope, aud}'

# 4. Introspect it
curl -s -X POST "$ISS/token_info" -d client_id=demo -d client_secret=demosecret -d "token=$AT" | jq '{scope}'

Actual behaviour

JWT payload:

{
  "scope": ["openid", "mail_read"],
  "aud": ["demo"]
}

Introspection response:

{ "active": true, "scope": ["openid", "mail_read"], ... }

scope is a JSON array in both.

Expected behaviour

scope should be a single space-delimited string:

{ "scope": "openid mail_read" }

This is required by:

  • RFC 9068 (JWT Profile for OAuth 2.0 Access Tokens) §2.2.3 — the scope claim follows §3.3 of RFC 6749, i.e. a space-delimited list in a string.
  • RFC 6749 §3.3 — scope ABNF is a space-delimited string (scope = scope-token *( SP scope-token )).
  • RFC 7662 (OAuth 2.0 Token Introspection) §2.2 — scope: "A JSON string containing a space-separated list of scopes."

(aud correctly may be a string or array per JWT/RFC 9068; only scope is affected here.)

Impact

RFC-compliant consumers type scope as a string (as the specs mandate) and fail to deserialize SimpleIdServer's array form, rejecting valid tokens. We hit this with independent Go implementations of RFC 9068 and RFC 7662 — both error with the equivalent of "cannot unmarshal array into a string field" on the scope member, on both the JWT-validation path and the introspection path.

Suggested fix

Serialize scope as a space-delimited string in both the access-token JWT and the introspection response (ideally the spec-compliant default; a compatibility toggle would be a bonus). Happy to help test a fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions