DiscordPHP Documentation

OAuth2 sessions and game linking

DiscordPHP runs in a long-lived CLI process. $discord->sessions manages player OAuth2 tokens separately from the bot's HTTP client. Native Social SDK clients still own player presence, relationships, game invites and audio. See Discord's Social SDK overview.

Linking a Discord account

For a confidential backend, configure the clientSecret option when creating the Discord client and wait until the application's ID is known. Forward an approved authorization callback to that process only after your application has checked the callback's state against the authenticated player's saved state and handled any error response. Use the exact registered redirect URI. DiscordPHP does not own your browser session or validate callback state.

Discord's authorization-code callbacks also include an iss parameter set to https://discord.com . If your application supports multiple OAuth2 providers, validate this issuer against the expected provider saved with the authorization request before exchanging the code. For a Discord authorization request, require exactly one string value equal to https://discord.com ; reject missing, malformed, duplicate or mismatched issuer values. Detect duplicate parameters before a query parser discards them. Do not choose the token endpoint from an untrusted callback value. Continue validating state and handling error responses as above.

This check belongs in your application's callback handler, before calling exchangeAuthorizationCode() : the helper receives a code and redirect URI, not the browser callback. Accept the additional iss field in callback parsers that previously allowed only code and state . See Discord's Issuer Identification documentation.

Test callbacks with valid, missing, malformed, duplicate and mismatched issuer values, as well as invalid state and denied authorization. Rejected callbacks must not send a token exchange request.

use Discord\OAuth2\Session;
use Discord\Parts\User\User;

// $code, $redirectUri and $playerId come from your validated linking flow.
$discord->sessions->exchangeAuthorizationCode($code, $redirectUri, $playerId)
    ->then(function (Session $session) {
        return $session->getCurrentUser();
    })
    ->then(function (User $user) {
        echo 'Linked Discord user: '.$user->id.PHP_EOL;
        // Persist the mapping from your authenticated player to this Discord ID.
    });

This exchanges the code using application credentials and form encoding, then hydrates a Session and stores its token under the optional key. Omit the key for a session that is not stored. The default ArrayTokenStore lasts only until the process exits; configure a durable tokenStore for persistent links. Keep tokens and the client secret on the backend.

Request the scopes needed by your integration. identify permits getCurrentUser() ; game stats writes require application_identities.write . Native SDK presence uses openid sdk.social_layer_presence ; communication uses openid sdk.social_layer . This exchange helper is for confidential clients with a secret; public-client PKCE authorization remains a separate flow. See the web linking guide and SDK scopes.

Unlinking and revocation

// $session is the linked player's Session.
$discord->sessions->revoke($session)->then(function (bool $deleted) {
    if (! $deleted) {
        throw new RuntimeException('Discord revoked the grant, but local token cleanup failed.');
    }
    // Clear your account mapping and other token-store keys for the player.
});

revoke() calls Discord first, using the application's credentials. On success it forgets this session's key in memory and deletes its stored token; unkeyed sessions resolve to true . A rejected Discord request leaves local state available for retry. A token-store rejection propagates; a false deletion result reports incomplete local cleanup after remote revocation. Stop using retained Session objects after revocation.

Discord invalidates the authorization's access and refresh tokens together. Only the supplied session's local key is removed, because your application owns the mapping between Discord users and arbitrary storage keys. Remove any other keys and account records for that player yourself. forget($key) only deletes local state; it does not revoke authorization at Discord. See token revocation and unlinking accounts.

Out-of-band deauthorization

Users may revoke your application from Discord. Subscribe to APPLICATION_DEAUTHORIZED in the application's Webhook Events settings and configure $discord->getWebhookEvents() as a signed webhook receiver. The event's handler emits a User ; use its id to look up your player mapping, call forget() for all of that player's stored keys, and clear the mapping. The library cannot infer your storage keys from the Discord user ID. These notifications arrive through Webhook Events, separately from Gateway delivery.

Provisional accounts

createProvisionalAccount() and exchangeExternalToken() already provide backend issuance. Reacquire provisional tokens with the original issuance method; ordinary OAuth refresh is for linked-account grants. In particular, Discord deprecates the OIDC provisional refresh-token grant. Unlinking a provisional account uses unmergeProvisionalAccount() or unmergeExternalAccount() . See managing provisional accounts.

Issuance origin and renewal

Tokens issued by the manager record a local origin : authorization-code exchange is ORIGIN_OAUTH2 , bot provisional issuance is ORIGIN_PROVISIONAL_BOT , external exchange is ORIGIN_PROVISIONAL_EXTERNAL , and child exchange is ORIGIN_CHILD . isRefreshable() permits the OAuth refresh grant only for ordinary OAuth and legacy unknown origins with a refresh token. This policy describes issuance, not whether the account has subsequently been linked.

refresh() rejects known provisional and child tokens with a DomainException explaining how to reacquire them; no request is sent. resume() does the same when such a token expires or enters the existing 60-second expiry leeway. Unexpired tokens resume normally. Use Discord's response expires_in ; backend provisional tokens normally last seven days, while Public Client provisional tokens last one hour.

Obtain a fresh provider credential yourself, then call exchangeExternalToken($providerType, $freshProviderToken, $sameKey) . The library stores only the returned Discord token and origin. It does not store the provider credential, replay a JWT, or install a native SDK callback. Reacquisition opens a new Session : use that returned session for later requests. Previously held session references keep their original token.

Migration

open() cannot infer origin from a response, scopes, JWT contents, or the presence of a refresh token. Classify manually imported tokens explicitly:

use Discord\OAuth2\AccessToken;

$token = AccessToken::fromResponse(
    $response,
    origin: AccessToken::ORIGIN_PROVISIONAL_EXTERNAL
);
$discord->sessions->open($token, $playerKey);

For a token obtained with the native Public Client, use ORIGIN_PROVISIONAL_PUBLIC and repeat that client's issuance method yourself. DiscordPHP does not implement the native Public Client.

Existing constructor calls, responses opened directly, and stored entries without origin remain ORIGIN_UNKNOWN . They retain the previous behavior: an expired token with a refresh token attempts ordinary OAuth refresh; an expired token without one resumes unchanged. This compatibility choice cannot identify older provisional entries. Audit those keys using your application's issuance records and replace them with freshly issued, classified tokens; do not guess from scopes or change every unknown entry to ordinary OAuth.

Custom stores must retain origin along with the existing token fields. Use AccessToken::jsonSerialize() and AccessToken::fromArray() . Both bundled stores already preserve the metadata. Invalid origin values fail validation instead of enabling an OAuth refresh grant.

Search results