LogoRobo.js

Authentication

Authenticate users in your Discord Activity

Authentication connects your activity to a Discord user's identity. The OAuth2 flow runs through the Embedded App SDK and a backend token endpoint.

How It Works

The frontend calls authorize() on the Embedded App SDK to get an authorization code. That code is sent to the /api/token backend endpoint, which exchanges it with Discord's OAuth2 API for an access token. The frontend then calls authenticate() with the token to complete the flow and receive the user's session data.

Diagram showing the OAuth2 flow: frontend calls authorize() getting a code, sends it to /api/token which exchanges with Discord API for access_token, then frontend calls authenticate()

FocusThe three-step OAuth2 flow: authorize, token exchange, authenticateZoom100%NotesCreate or capture a flow diagram: (1) Frontend calls SDK.authorize() -> gets code, (2) Frontend sends code to /api/token -> backend exchanges with Discord API -> gets access_token, (3) Frontend calls SDK.authenticate(token). Label participants: Frontend, Backend, Discord API.

React Setup

DiscordContextProvider handles the full authentication flow automatically when the authenticate prop is set:

src/app/App.tsx
<DiscordContextProvider authenticate scope={['identify', 'guilds']}>
  <Activity />
</DiscordContextProvider>
src/app/App.jsx
<DiscordContextProvider authenticate scope={['identify', 'guilds']}>
  <Activity />
</DiscordContextProvider>

The scope array determines which permissions are requested. Children render only after the flow completes or errors.

Accessing User Data

useDiscordSdk() returns a session object with the authenticated user's information:

src/app/Profile.tsx
import { useDiscordSdk } from '../hooks/useDiscordSdk'

export function Profile() {
  const { session } = useDiscordSdk()

  if (!session) {
    return null
  }

  return <div>{session.user.username}</div>
}
src/app/Profile.jsx
import { useDiscordSdk } from '../hooks/useDiscordSdk'

export function Profile() {
  const { session } = useDiscordSdk()

  if (!session) {
    return null
  }

  return <div>{session.user.username}</div>
}
FieldTypeDescription
session.user.idstringUser's Discord ID
session.user.usernamestringUsername
session.user.discriminatorstringDiscriminator
session.user.avatarstring | nullAvatar hash
session.user.public_flagsnumberPublic user flags

Construct an avatar URL from the user ID and avatar hash:

src/app/Profile.tsx
const avatarUrl = `https://cdn.discordapp.com/avatars/${session.user.id}/${session.user.avatar}.png?size=256`
src/app/Profile.jsx
const avatarUrl = `https://cdn.discordapp.com/avatars/${session.user.id}/${session.user.avatar}.png?size=256`

A Discord Activity displaying the authenticated user's avatar, username, and user ID retrieved from session data after OAuth2 authentication

FocusThe activity showing user profile data from the authenticated sessionZoom100%NotesShow an activity rendering user profile: avatar image (from CDN), username, and user ID. Clean, polished UI. Either in Discord embedded panel or localhost.

OAuth Scopes

ScopeDescription
identifyAccess user ID, username, avatar
guildsAccess user's guild list
guilds.members.readRead guild member data
rpc.voice.readAccess voice channel info

Additional scopes can be passed to the scope prop on DiscordContextProvider.

Token Endpoint

The /api/token.ts backend handler exchanges the authorization code for an access token. This keeps the client secret secure on the server:

src/api/token.ts
import type { RoboRequest } from '@robojs/server'

export default async (req: RoboRequest) => {
  const { code } = (await req.json()) as { code: string }

  const response = await fetch('https://discord.com/api/oauth2/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      client_id: process.env.VITE_DISCORD_CLIENT_ID!,
      client_secret: process.env.DISCORD_CLIENT_SECRET!,
      grant_type: 'authorization_code',
      code
    })
  })
  const { access_token } = await response.json()

  return { access_token }
}
src/api/token.js
export default async (req) => {
  const { code } = await req.json()

  const response = await fetch('https://discord.com/api/oauth2/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      client_id: process.env.VITE_DISCORD_CLIENT_ID,
      client_secret: process.env.DISCORD_CLIENT_SECRET,
      grant_type: 'authorization_code',
      code
    })
  })
  const { access_token } = await response.json()

  return { access_token }
}

Loading State

Use the loadingScreen prop to display a component while authentication is in progress:

src/app/App.tsx
<DiscordContextProvider authenticate loadingScreen={<div>Loading...</div>}>
  <Activity />
</DiscordContextProvider>
src/app/App.jsx
<DiscordContextProvider authenticate loadingScreen={<div>Loading...</div>}>
  <Activity />
</DiscordContextProvider>

The loading screen renders during the pending, loading, and authenticating statuses. It is replaced by children once the status reaches ready or error.

A Discord Activity showing the loading screen during authentication, with a loading indicator or 'Loading...' message while the OAuth2 flow is in progress

FocusThe loading screen rendered during authenticationZoom100%NotesShow the activity in loading state before authentication completes. Spinner, 'Loading...' text, or skeleton UI. Visible in Discord panel or browser.

Error Handling

ErrorCause
Missing redirect_uriNo redirect URI configured in Developer Portal
invalid_clientWrong client ID or secret
invalid_grantExpired or reused authorization code

Verify your credentials and Developer Portal OAuth2 settings if authentication fails.

Vanilla Implementation

For non-React projects, run the authorization flow manually:

src/app/main.ts
import { DiscordSDK } from '@discord/embedded-app-sdk'

const discordSdk = new DiscordSDK(import.meta.env.VITE_DISCORD_CLIENT_ID)
await discordSdk.ready()

const { code } = await discordSdk.commands.authorize({
  client_id: import.meta.env.VITE_DISCORD_CLIENT_ID,
  response_type: 'code',
  state: '',
  prompt: 'none',
  scope: ['identify', 'guilds']
})

const response = await fetch('/api/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ code })
})
const { access_token } = await response.json()

const auth = await discordSdk.commands.authenticate({ access_token })
src/app/main.js
import { DiscordSDK } from '@discord/embedded-app-sdk'

const discordSdk = new DiscordSDK(import.meta.env.VITE_DISCORD_CLIENT_ID)
await discordSdk.ready()

const { code } = await discordSdk.commands.authorize({
  client_id: import.meta.env.VITE_DISCORD_CLIENT_ID,
  response_type: 'code',
  state: '',
  prompt: 'none',
  scope: ['identify', 'guilds']
})

const response = await fetch('/api/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ code })
})
const { access_token } = await response.json()

const auth = await discordSdk.commands.authenticate({ access_token })

This performs the same flow as DiscordContextProvider: authorize, exchange code for token, then authenticate.

Next Steps

On this page