OAuth2¶
OAuth2 is how a user grants an application access to their account. The same consent step can also install the application's bot into a guild or a group DM. Application records, client secrets, and bot tokens belong to the Applications resource.
Authorisation code grant¶
- The application sends the user's browser to Authorise application (
GET /oauth2/authorize). TEBHOS redirects the browser to the consent screen of the web app,https://tebhos.com/oauth2/authorize, forwarding every query parameter. - When the user approves, the web app calls Grant OAuth2 consent. TEBHOS issues an authorisation code and returns the application's callback URL with
code(andstate) appended. - The application's server exchanges the code at Exchange OAuth2 token with its client credentials and receives an access token and a refresh token.
- The application calls Get OAuth2 user information or Get current OAuth2 authorisation with
Authorization: Bearer <access_token>.
Proof Key for Code Exchange (PKCE) is supported with the S256 and plain methods. The challenge is bound to the code when the code is issued. A code_challenge without code_challenge_method is treated as plain. An exchange for a code that has a challenge must supply a matching code_verifier.
Credentials and lifetimes¶
Treat authorisation codes, client secrets, access tokens, and refresh tokens as opaque secrets. Each one is 32 random bytes encoded as base64url.
| Credential | Lifetime |
|---|---|
| Authorisation code | Single use. TEBHOS does not currently expire unused codes, so exchange a code as soon as it arrives |
| Access token | 7 days (604800 seconds) from issue |
| Refresh token | No expiry; consumed when it is exchanged, and deleted by revocation |
A refresh exchange consumes the presented refresh token and issues a new pair. Access tokens issued earlier stay valid until they expire.
Route authentication¶
Authorise application, Grant OAuth2 consent, and the authorisation listing routes authenticate the user with a session token. The token, introspection, and revocation routes authenticate the application with its client ID and client secret instead, and key their rate limit on the client IP address. Get OAuth2 user information and Get current OAuth2 authorisation authenticate with the OAuth2 access token.
OAuth2 scopes¶
A scope names one thing the application may read or do on the user's behalf.
| Value | Description |
|---|---|
| identify | Read the user's identity through Get OAuth2 user information and Get current user, and include the user in Get current OAuth2 authorisation |
Add the user's email address to those responses. Has no effect without identify |
|
| guilds | Read the user's guilds through List current user guilds, Get guild and List guild roles |
| connections | Read the user's connections through List connections |
| bot | Install the application's bot into a selected guild or group DM. Grants no access-token capability of its own |
TEBHOS splits a scope string on whitespace and +, drops empty parts, and collapses repeated values. An unrecognised value rejects the request with invalid_scope.
Access tokens reach only the listed routes
An OAuth2 access token is accepted by Get OAuth2 user information, Get current OAuth2 authorisation and the scoped read routes named in the table above. A token without the route's scope receives 403 MISSING_OAUTH_SCOPE with a required_scope field. Every other REST route requires a user session token or a bot token, and answers a valid access token with 403 ACCESS_DENIED. See OAuth2 access tokens.
OAuth2 errors¶
OAuth2 failures use the ordinary error response. The OAuth2 error name is in message, sometimes followed by a colon and detail:
| message | Status | Meaning |
|---|---|---|
invalid_client |
400 | The client ID is missing, not a snowflake, or names no application |
invalid_request: ... |
400 | No client secret was supplied |
invalid_client_secret |
401 | The client secret does not match |
invalid_grant |
400 | The code or refresh token is missing, unknown, used, bound to another application, the redirect_uri does not match the code, or the PKCE verifier is missing or wrong |
unsupported_grant_type: ... |
400 | grant_type is neither authorization_code nor refresh_token |
invalid_scope: ... |
400 | A requested scope is unrecognised, or no scope was requested |
missing_redirect_uri |
400 | A redirect URI is required and was not supplied |
invalid_redirect_uri |
400 | The redirect URI is not registered for the application, or does not parse |
invalid_response_type_for_non_bot |
400 | response_type is not code on a request for a scope other than bot |
invalid_token |
401 | The access token is missing, unknown, or expired |
A client MUST branch on the HTTP status and on the error name at the start of message, never on the rest of the text.
OAuth2 token object¶
The pair a successful token exchange returns.
Structure¶
| Field | Type | Description |
|---|---|---|
| access_token | string | The bearer token the application sends in the Authorization header |
| token_type | string | Always Bearer |
| expires_in | integer | The access token lifetime in seconds, always 604800 |
| refresh_token | string | The refresh token that replaces this pair when exchanged |
| scope | string | The granted scopes, space separated in alphabetical order |
Example¶
{
"access_token": "M2xQb1RkY0hqTmZLc1B2WndBcUx1RWc2WXRJbk9iUmE",
"token_type": "Bearer",
"expires_in": 604800,
"refresh_token": "VGhpc0lzQUZha2VSZWZyZXNoVG9rZW5Gb3JEb2NzMDE",
"scope": "email identify"
}
OAuth2 user information object¶
Structure¶
| Field | Type | Description |
|---|---|---|
| sub | snowflake | The user ID, equal to id |
| id | snowflake | The user ID |
| username | string | The user's display name |
| discriminator | string | Always "0" |
| global_name | string | The user's display name |
| avatar | null | Always null on this object |
| email? | ?string | The email address, present only with the email scope |
| verified? | boolean | Always true, present only with the email scope |
username carries the display name
On this object both username and global_name hold the account's display name. To show the unique username, read the user through a session-authenticated route instead.
OAuth2 introspection object¶
Structure¶
| Field | Type | Description |
|---|---|---|
| active | boolean | Whether the token exists and is live |
| scope? | string | The granted scopes, space separated in alphabetical order |
| client_id? | snowflake | The application the token was issued to |
| token_type? | string | Bearer for an access token, refresh_token for a refresh token |
| iat? | integer | The issue time in Unix seconds |
| exp? | integer | The expiry time in Unix seconds, for an access token only |
| sub? | snowflake | The user the token was issued for |
An inactive result has only active: false. Every other field is present when active is true, except exp, which a refresh token never has.
Check client_id
Introspection reports any live token as active, including one issued to a different application. A resource server MUST compare client_id with its own application ID before trusting the result.
Current OAuth2 authorisation object¶
Structure¶
| Field | Type | Description |
|---|---|---|
| application | OAuth2 application summary object | The application the token was issued to |
| scopes | array[string] | The granted scopes, in alphabetical order |
| expires | ISO8601 timestamp | When the access token expires |
| user? | object | Present only when the token holds identify: {id, username, discriminator, global_name, avatar} with the same meanings as the user information object, plus email and verified with the email scope |
OAuth2 application summary object¶
Structure¶
| Field | Type | Description |
|---|---|---|
| id | snowflake | The application ID |
| name | string | The application name |
| icon | null | Always null on this object |
| description | null | Always null on this object |
| bot_public | boolean | Whether anyone can install the bot |
| bot_require_code_grant | boolean | Whether installing the bot requires a full code grant |
| flags | integer | Always 0 |
OAuth2 authorisation object¶
One application the current user has authorised.
Structure¶
| Field | Type | Description |
|---|---|---|
| application | object | {id, name} of the application |
| scopes | array[string] | The scopes of the user's live access tokens for the application, in no defined order |
An application appears while the user holds at least one live access token for it.
Authorise application¶
GET /oauth2/authorize
Starts an authorisation request. Always answers with a 302 redirect. Authentication is optional.
Query parameters¶
| Field | Type | Description |
|---|---|---|
| client_id | snowflake | The application being authorised |
| scope | string | The requested scopes, space separated |
| response_type? | string | code |
| redirect_uri? | string | A redirect URI registered by the application. Required unless the request asks for bot alone |
| state? | string | An opaque value returned unchanged on the callback |
| prompt? | string | none to skip the consent screen when the user already granted every requested scope |
| code_challenge? | string | The PKCE challenge |
| code_challenge_method? | string | S256 or plain (default plain) |
| guild_id? | snowflake | The guild pre-selected for the bot scope |
| channel_id? | snowflake | The group DM pre-selected for the bot scope |
| permissions? | string | The bot permission integer, as a decimal string |
| disable_guild_select? | string | true to lock the guild selector on the consent screen |
Response¶
| Status | Body | Condition |
|---|---|---|
| 302 | empty | The Location header is the consent screen, the application callback, or an error callback |
Without prompt=none, the redirect goes to https://tebhos.com/oauth2/authorize with every supplied parameter forwarded. Nothing is validated yet; the consent screen validates when it calls Grant OAuth2 consent.
With prompt=none, TEBHOS tries to issue a code silently:
| Outcome | Redirect |
|---|---|
The request is valid, the user is signed in, and every requested non-bot scope was already granted to this application |
The callback URL with code and state |
| No signed-in user | The error target with error=login_required |
client_id is not a snowflake |
The error target with error=invalid_client |
| Any other failure, including an ungranted scope | The error target with error=consent_required |
The error target is the supplied redirect_uri when the application has registered that exact string, and the web app URL otherwise, so this route never redirects to an arbitrary origin. The error redirect carries error and state, and no error_description.
Rate limit¶
60 requests per minute for each IP address, on the oauth:authorize bucket.
Grant OAuth2 consent¶
POST /oauth2/authorize/consent
Completes an authorisation request on behalf of the signed-in user, and installs the bot when the bot scope has a target. Requires a user session token. Returns {"redirect_to": string}, the callback URL with code and, when supplied, state.
This is the route the TEBHOS consent screen calls. A third-party application does not call it directly.
JSON body¶
| Field | Type | Description |
|---|---|---|
| client_id | snowflake | The application being authorised |
| scope | string | The requested scopes, space separated |
| redirect_uri? | string | A registered redirect URI. Required unless the request asks for bot alone and the application does not set bot_require_code_grant |
| state? | string | Appended to the callback URL |
| response_type? | string | Must be code (the default) on a request for any scope other than bot |
| code_challenge? | string | The PKCE challenge |
| code_challenge_method? | string | S256 or plain (default plain) |
| guild_id? | snowflake | The guild to install the bot into |
| channel_id? | snowflake | The group DM to install the bot into |
| permissions? | string | The bot permission integer as a decimal string. Bits outside the defined permission mask are dropped |
guild_id and channel_id are mutually exclusive. A bot-only request without redirect_uri resolves its callback to the web app URL.
Response¶
| Status | Body | Condition |
|---|---|---|
| 200 | {"redirect_to": string} |
Consent completed |
| 400 | error response | See OAuth2 errors; also both targets supplied, an invalid permissions value (invalid_permissions_integer, invalid_permissions_negative), an application without a bot (not_a_bot_application), a channel that is not a group DM, or a full group DM |
| 401 | error response | No valid user session |
| 403 | error response | The bot is private and the caller does not own the application; the caller lacks the permission to install into the guild; or the caller is not a member of the group DM |
| 404 | error response | The guild or channel does not exist |
| 409 | error response | The bot is already a member of the guild |
Bot installation¶
With the bot scope and a guild_id, the caller must be the guild owner or hold Manage Guild or Administrator in that guild. A caller without Administrator can only request permission bits they hold themselves.
The bot joins the guild with the caller recorded as inviter. When a non-zero permission mask is requested, TEBHOS also creates a role named after the application with exactly that mask (colour #99AAB5, not hoisted, not mentionable, placed above the existing roles) and assigns it to the bot. The guild's sessions receive a member add. The guild's member limit and ban list are not checked for a bot installed this way.
With the bot scope and a channel_id, the caller must be a recipient of the group DM. The bot is added as a recipient and a recipient-add system message is posted. A bot that is already a recipient is left as it is.
When the installation fails, the authorisation code issued by the same request is deleted, so the caller never receives a usable code.
Rate limit¶
60 requests per minute for each IP address, on the oauth:authorize bucket.
Exchange OAuth2 token¶
POST /oauth2/token
Exchanges an authorisation code or a refresh token for an OAuth2 token object.
The body is application/x-www-form-urlencoded. The application authenticates with HTTP Basic (Authorization: Basic base64(client_id:client_secret)) or with the client_id and client_secret form fields. When both are present, each form field takes precedence over the matching Basic value.
Form body: authorisation code¶
| Field | Type | Description |
|---|---|---|
| grant_type | string | authorization_code |
| code | string | The code from the callback |
| redirect_uri | string | The redirect URI the code was issued for, compared as an exact string. For a bot-only grant without one, it is the web app URL |
| code_verifier? | string | The PKCE verifier. Required when the code has a challenge; for S256 its SHA-256 digest is base64url-encoded and compared with the challenge |
| client_id? | snowflake | The client ID, when HTTP Basic is not used |
| client_secret? | string | The client secret, when HTTP Basic is not used |
Form body: refresh token¶
| Field | Type | Description |
|---|---|---|
| grant_type | string | refresh_token |
| refresh_token | string | The refresh token to exchange |
| client_id? | snowflake | The client ID, when HTTP Basic is not used |
| client_secret? | string | The client secret, when HTTP Basic is not used |
Response¶
| Status | Body | Condition |
|---|---|---|
| 200 | OAuth2 token object | A token pair was issued |
| 400 | error response | The form, the client, or the grant is invalid (see OAuth2 errors) |
| 401 | error response | The client secret is wrong (invalid_client_secret) |
| 429 | error response | The route bucket denied the request |
The client is authenticated before the grant is checked. A code is deleted before the tokens are issued, so replaying a code returns invalid_grant. There is no client-credentials grant: every token is bound to a user.
Rate limit¶
120 requests per minute for each IP address, on the oauth:token bucket.
Example¶
curl -X POST https://tebhos.com/api/v1/oauth2/token \
-u "1501314428688998182:<client_secret>" \
-d grant_type=authorization_code \
-d code=<code> \
-d redirect_uri=https://example.com/callback \
-d code_verifier=<verifier>
Get OAuth2 user information¶
GET /oauth2/userinfo
Returns the OAuth2 user information object for the user the access token was issued to. Requires Authorization: Bearer <access_token> with the identify scope.
Response¶
| Status | Body | Condition |
|---|---|---|
| 200 | OAuth2 user information object | The user was returned |
| 401 | error response | The token is missing, unknown, or expired, or the user no longer exists (invalid_token) |
| 403 | error response | The token lacks identify (MISSING_OAUTH_SCOPE) |
Rate limit¶
120 requests per minute for each client, on the oauth:introspect bucket.
Introspect OAuth2 token¶
POST /oauth2/introspect
Returns an OAuth2 introspection object for one access or refresh token. The body is application/x-www-form-urlencoded, and the application authenticates as for Exchange OAuth2 token.
Form body¶
| Field | Type | Description |
|---|---|---|
| token | string | The access or refresh token to inspect |
| client_id? | snowflake | The client ID, when HTTP Basic is not used |
| client_secret? | string | The client secret, when HTTP Basic is not used |
Response¶
| Status | Body | Condition |
|---|---|---|
| 200 | OAuth2 introspection object | Introspection completed, including an inactive result |
| 400 | error response | The client is missing or unknown, or no secret was supplied |
| 401 | error response | The client secret is wrong |
Rate limit¶
120 requests per minute for each IP address, on the oauth:introspect bucket.
Revoke OAuth2 token¶
POST /oauth2/token/revoke
Revokes a token. The body is application/x-www-form-urlencoded, and the application authenticates as for Exchange OAuth2 token.
Form body¶
| Field | Type | Description |
|---|---|---|
| token | string | The token to revoke |
| token_type_hint? | string | refresh_token when token is a refresh token. Without it, token is looked up as an access token only |
| client_id? | snowflake | The client ID, when HTTP Basic is not used |
| client_secret? | string | The client secret, when HTTP Basic is not used |
Revocation clears the whole grant
When the token matches, TEBHOS deletes every access token and every refresh token the user holds for the authenticated application.
A client that revokes a refresh token MUST send token_type_hint=refresh_token, otherwise nothing matches.
Response¶
| Status | Body | Condition |
|---|---|---|
| 200 | empty | The request was processed, including a token that matched nothing |
| 400 | error response | The client is missing or unknown, or no secret was supplied |
| 401 | error response | The client secret is wrong |
An unknown token returns 200, so the response never shows whether a token existed.
Rate limit¶
120 requests per minute for each IP address, on the oauth:introspect bucket.
Get current OAuth2 authorisation¶
GET /oauth2/@me
Returns the current OAuth2 authorisation object for the presented access token. Requires Authorization: Bearer <access_token>; no scope is needed. The user member is included only when the token holds identify.
Response¶
| Status | Body | Condition |
|---|---|---|
| 200 | current OAuth2 authorisation object | The authorisation was returned |
| 401 | error response | The token is missing, unknown, or expired, or its application no longer exists (invalid_token) |
List OAuth2 authorisations¶
GET /oauth2/@me/authorizations
Returns the OAuth2 authorisation objects the current user has granted. Requires a user session token. The list is not paginated.
Rate limit¶
60 requests per minute for each account, on the oauth_dev:clients:list bucket.
Revoke OAuth2 authorisation¶
DELETE /oauth2/@me/authorizations/{application_id}
Revokes the current user's access tokens for one application. Requires a user session token. Returns 204, also when nothing was granted.
Path parameters¶
| Field | Type | Description |
|---|---|---|
| application_id | snowflake | The application whose access is revoked |
Refresh tokens survive this route
This route deletes the user's access tokens for the application. To also delete the refresh tokens, so the application cannot obtain new access tokens, use Bulk revoke OAuth2 authorisations.
A bot the application installed stays in every guild and group DM it joined. The application is not notified.
Rate limit¶
120 requests per minute for each account, on the oauth:introspect bucket.
Bulk revoke OAuth2 authorisations¶
POST /oauth2/@me/authorizations/revoke
Revokes the current user's complete grant (access and refresh tokens) for several applications. Requires a user session token. Returns 204.
JSON body¶
| Field | Type | Description |
|---|---|---|
| application_ids | array[snowflake] | The applications to revoke. Repeated IDs are collapsed |
Every application is looked up before anything is deleted, so one unknown ID returns 404 and leaves every grant untouched. A value that is not a snowflake returns 400.
Rate limit¶
120 requests per minute for each account, on the oauth:introspect bucket.