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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions