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.
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.