Skip to content

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

  1. 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.
  2. 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 (and state) appended.
  3. The application's server exchanges the code at Exchange OAuth2 token with its client credentials and receives an access token and a refresh token.
  4. 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
email 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:

{
  "error": "400 Bad Request",
  "code": "BAD_REQUEST",
  "message": "invalid_grant"
}
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.

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.