OAuth device authorization endpoint issues device codes to clients that did not register the device code grant type #2109

Closed
opened 2026-08-06 17:55:46 +00:00 by mmaudet · 0 comments
Contributor

Summary

POST /_continuwuity/oauth2/device issues a device code to any registered client,
without checking that the client registered
urn:ietf:params:oauth:grant-type:device_code in its grant_types.

The token endpoint does enforce that check, so no access token can be obtained this
way. The practical effect is that a client which never registered the grant can still
start a device authorization flow, have the server display an approval prompt, get a
user to approve it, and only then fail at the token request.

Environment

  • Observed against a deployed v26.7.2 instance.
  • Still present on main at 1b6bc83b9f70da341d062b6cdb7ced38bd497a93 (read from source,
    not re-run against a build of main).
  • Introduced with the flow itself in 4c9426a13f7427d3318861f49aa36dfe7ee5055e
    (feat: Add support for OAuth2 device auth flow).
  • Default config; OAuth is reachable in the default hybrid compatibility mode.

Reproduction

SERVER=https://your.continuwuity.example

# 1. Register a client that declares only the authorization code and refresh token
#    grants. It deliberately does NOT declare the device code grant.
CLIENT_ID=$(curl -sS -X POST "$SERVER/_continuwuity/oauth2/client/register" \
  -H 'Content-Type: application/json' \
  -d '{
        "client_name":      "grant-type-probe",
        "client_uri":       "https://example.com",
        "application_type": "native",
        "redirect_uris":    ["https://example.com/callback"],
        "response_types":   ["code"],
        "grant_types":      ["authorization_code", "refresh_token"]
      }' | jq -r .client_id)

# 2. Ask the device authorization endpoint for a device code.
curl -sS -i -X POST "$SERVER/_continuwuity/oauth2/device" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode "client_id=$CLIENT_ID" \
  --data-urlencode 'scope=urn:matrix:client:api:* urn:matrix:client:device:PROBEDEVICE1'

Observed — step 2 returns 200 OK and a usable device code:

{
  "device_code": "…",
  "user_code": "483920",
  "verification_uri": "https://your.continuwuity.example/_continuwuity/oauth2/grant/device_code",
  "verification_uri_complete": "https://your.continuwuity.example/_continuwuity/oauth2/grant/device_code?user_code=483920",
  "expires_in": 60
}

Expected400 Bad Request with the error code RFC 6749 §5.2 assigns to this
condition:

{
  "error": "unauthorized_client",
  "error_description": "Client cannot request this grant type"
}

For completeness, the flow does dead-end at the token endpoint today — this step
already behaves correctly:

curl -sS -i -X POST "$SERVER/_continuwuity/oauth2/grant/token" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode "client_id=$CLIENT_ID" \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:device_code' \
  --data-urlencode "device_code=<device_code from step 2>"
# 400 {"error":"invalid_grant","error_description":"Client cannot request this grant type"}

Specification

RFC 8628 §3.2 (Device Authorization Response):

In the event of an error (such as an invalidly configured client), the authorization
server responds in the same way as the token endpoint specified in Section 5.2 of
[RFC6749].

RFC 6749 §5.2 defines the error code for exactly this condition:

unauthorized_client
The authenticated client is not authorized to use this authorization grant type.

and prescribes the status code:

The authorization server responds with an HTTP 400 (Bad Request) status code (unless
specified otherwise) …

grant_types is the client metadata field registered through RFC 7591 dynamic client
registration, which the server already stores and already honours at the token endpoint.
The Matrix Client-Server API lists the device authorisation grant among the OAuth 2.0
grant types it supports (spec v1.19), so this is the flow as Matrix specifies it, not an
unused corner of RFC 8628.

Impact

This is a conformance gap, not an authorization bypass. Service::issue_token is the
only path to an access token, and it refuses the grant type before doing anything else,
so a device code obtained this way is inert.

What it does cost:

  • A misregistered client fails late and confusingly, after the user has already typed a
    user code and approved access, instead of failing at the first request.
  • The server will render an "approve this client" prompt on behalf of a client it is
    never going to serve. The user reasonably believes they granted something; nothing was
    granted. A conformant server would never have shown the prompt.
  • Matrix Authentication Service, in the same situation, refuses the device authorization
    request outright — so clients tested only against Continuwuity can ship a registration
    that MAS rejects.

The token endpoint reports the same condition as invalid_grant. RFC 6749 §5.2 reserves
invalid_grant for a grant that is "invalid, expired, revoked, does not match the
redirection URI used in the authorization request, or was issued to another client", and
defines unauthorized_client for a client "not authorized to use this authorization
grant type". Happy to leave that alone if you would rather not change an existing error
code.

## Summary `POST /_continuwuity/oauth2/device` issues a device code to any registered client, without checking that the client registered `urn:ietf:params:oauth:grant-type:device_code` in its `grant_types`. The token endpoint *does* enforce that check, so no access token can be obtained this way. The practical effect is that a client which never registered the grant can still start a device authorization flow, have the server display an approval prompt, get a user to approve it, and only then fail at the token request. ## Environment - Observed against a deployed **v26.7.2** instance. - Still present on `main` at `1b6bc83b9f70da341d062b6cdb7ced38bd497a93` (read from source, not re-run against a build of `main`). - Introduced with the flow itself in `4c9426a13f7427d3318861f49aa36dfe7ee5055e` (`feat: Add support for OAuth2 device auth flow`). - Default config; OAuth is reachable in the default `hybrid` compatibility mode. ## Reproduction ```bash SERVER=https://your.continuwuity.example # 1. Register a client that declares only the authorization code and refresh token # grants. It deliberately does NOT declare the device code grant. CLIENT_ID=$(curl -sS -X POST "$SERVER/_continuwuity/oauth2/client/register" \ -H 'Content-Type: application/json' \ -d '{ "client_name": "grant-type-probe", "client_uri": "https://example.com", "application_type": "native", "redirect_uris": ["https://example.com/callback"], "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"] }' | jq -r .client_id) # 2. Ask the device authorization endpoint for a device code. curl -sS -i -X POST "$SERVER/_continuwuity/oauth2/device" \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode "client_id=$CLIENT_ID" \ --data-urlencode 'scope=urn:matrix:client:api:* urn:matrix:client:device:PROBEDEVICE1' ``` **Observed** — step 2 returns `200 OK` and a usable device code: ```json { "device_code": "…", "user_code": "483920", "verification_uri": "https://your.continuwuity.example/_continuwuity/oauth2/grant/device_code", "verification_uri_complete": "https://your.continuwuity.example/_continuwuity/oauth2/grant/device_code?user_code=483920", "expires_in": 60 } ``` **Expected** — `400 Bad Request` with the error code RFC 6749 §5.2 assigns to this condition: ```json { "error": "unauthorized_client", "error_description": "Client cannot request this grant type" } ``` For completeness, the flow does dead-end at the token endpoint today — this step already behaves correctly: ```bash curl -sS -i -X POST "$SERVER/_continuwuity/oauth2/grant/token" \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode "client_id=$CLIENT_ID" \ --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:device_code' \ --data-urlencode "device_code=<device_code from step 2>" # 400 {"error":"invalid_grant","error_description":"Client cannot request this grant type"} ``` ## Specification RFC 8628 §3.2 (Device Authorization Response): > In the event of an error (such as an invalidly configured client), the authorization > server responds in the same way as the token endpoint specified in Section 5.2 of > [RFC6749]. RFC 6749 §5.2 defines the error code for exactly this condition: > **unauthorized_client** > The authenticated client is not authorized to use this authorization grant type. and prescribes the status code: > The authorization server responds with an HTTP 400 (Bad Request) status code (unless > specified otherwise) … `grant_types` is the client metadata field registered through RFC 7591 dynamic client registration, which the server already stores and already honours at the token endpoint. The Matrix Client-Server API lists the device authorisation grant among the OAuth 2.0 grant types it supports (spec v1.19), so this is the flow as Matrix specifies it, not an unused corner of RFC 8628. ## Impact This is a conformance gap, not an authorization bypass. `Service::issue_token` is the only path to an access token, and it refuses the grant type before doing anything else, so a device code obtained this way is inert. What it does cost: - A misregistered client fails late and confusingly, after the user has already typed a user code and approved access, instead of failing at the first request. - The server will render an "approve this client" prompt on behalf of a client it is never going to serve. The user reasonably believes they granted something; nothing was granted. A conformant server would never have shown the prompt. - Matrix Authentication Service, in the same situation, refuses the device authorization request outright — so clients tested only against Continuwuity can ship a registration that MAS rejects. ## Related: error code at the token endpoint The token endpoint reports the same condition as `invalid_grant`. RFC 6749 §5.2 reserves `invalid_grant` for a grant that is "invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client", and defines `unauthorized_client` for a client "not authorized to use this authorization grant type". Happy to leave that alone if you would rather not change an existing error code.
nex closed this issue 2026-08-07 08:52:21 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
continuwuation/continuwuity#2109
No description provided.