Eternaltwin

Home | /api | v1

/api/v1/oauth_consent

The data behind the consent screen. Defined in crates/rest/src/oauth_consent.rs.

Internal to the official website. These two routes back the /consent page of the Angular application. They are not versioned in practice, they have no stability guarantee, and an application has no reason to call them: the supported entry point is GET /oauth/authorize, documented in Eternaltwin for OAuth and listed under Internal endpoints.

They live under /api/v1/ rather than under /oauth/ on purpose: everything under /oauth/ is proxied straight to the backend and never reaches the website, while the consent page has to be an Angular route. Keeping its data endpoints on the API path lets the proxy rule stay a plain prefix.

The path is oauth_consent, with an underscore, as it is mounted in crates/rest/src/lib.rs.

The pending authorization

Both routes carry the authorization request as it arrived at /oauth/authorize — in the query string for the read, inside the body for the write:

NameTypeMeaning
client_idstringThe client's key or UUID.
redirect_uristringCallback the client asked for.
response_typestringcode.
scopestringSpace-separated scope tokens: base, forum:read, forum:write, forum:moderate.
statestringOpaque value echoed back to the client.

Nothing about the pending request is trusted because it came back from the browser. Both routes re-validate it from scratch, exactly as /oauth/authorize does, so the page cannot be made to describe a client that would be refused a moment later.

GET /

Describes what the pending request asks for.

Requires a signed-in user who has accepted the terms of service — the page is reached by a redirect from /oauth/authorize, which already sends a guest to the sign-in screen and an unaccepting user to /tos-accept.

Example

GET /api/v1/oauth_consent?client_id=eternalfest%40clients&response_type=code&scope=forum%3Awrite&state=xyz
Cookie: sid=b8be19ef-2d61-44de-b7d2-9c34ccb8a763
{
  "client": {
    "type": "OauthClient",
    "id": "d19e61a3-83d3-410f-84ec-49aaab841559",
    "key": "eternalfest@clients",
    "display_name": "Eternalfest"
  },
  "app_uri": "https://eternalfest.net/",
  "requested_scopes": {
    "base": true,
    "forum_read": true,
    "forum_write": true,
    "forum_moderate": false
  },
  "approved_scopes": {
    "base": true,
    "forum_read": false,
    "forum_write": false,
    "forum_moderate": false
  }
}
FieldMeaning
clientWho is asking. Display name and key come from the store, never from the request.
app_uriHomepage of the application, so the user can tell which "Eternalfest" is asking.
requested_scopesWhat this request asks for, with the hierarchy already resolved: forum:moderate implies forum:write, which implies forum:read.
approved_scopesWhat the user had already approved for this client, or null if they were never asked.

The screen uses the two scope sets to separate "this is new" from "you already agreed to this".

Errors

StatusBodyWhen
400{"error": "invalid authorization request"}Unknown client, callback that does not match, scope the client may not request.
401{"error": "not signed in"}The session expired, or the terms have not been accepted.
500{"error": "internal error"}—

POST /

Records the answer and says where to send the browser.

Body

The five authorization fields above, flattened, plus:

FieldTypeMeaning
approvedbooleantrue to approve, false to deny.

Example

POST /api/v1/oauth_consent
Content-Type: application/json
Cookie: sid=b8be19ef-2d61-44de-b7d2-9c34ccb8a763

{
  "client_id": "eternalfest@clients",
  "response_type": "code",
  "scope": "forum:write",
  "state": "xyz",
  "approved": true
}
{
  "redirect_uri": "https://eternalfest.net/oauth/callback?code=…&state=xyz"
}

Both answers end at the client's registered callback: approving with a code, denying with the RFC 6749 access_denied error. A client that asked a question is owed an answer — one left waiting on a redirect that never comes cannot tell a refusal from a network failure.

Approving a request wider than a standing consent stores the union of the two, never the replacement.

Errors

Same as the read: 400 for an invalid request, 401 when not signed in, 500 otherwise.