Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/disgoorg/disgo/llms.txt

Use this file to discover all available pages before exploring further.

DisGo includes a complete OAuth2 client for implementing Discord’s OAuth2 authorization flow. This allows you to authenticate users, access their Discord data, and manage access tokens.

Creating an OAuth2 client

The OAuth2 client requires your application’s client ID and secret:
import (
    "github.com/disgoorg/disgo/oauth2"
    "github.com/disgoorg/snowflake/v2"
)

clientID := snowflake.MustParse("YOUR_CLIENT_ID")
clientSecret := "YOUR_CLIENT_SECRET"

client := oauth2.New(clientID, clientSecret)
Never expose your client secret in client-side code or public repositories. Keep it secure on your backend server.

Authorization flow

The OAuth2 authorization flow consists of three main steps:
1

Generate authorization URL

Generate a URL to redirect users to Discord’s authorization page:
params := oauth2.AuthorizationURLParams{
    RedirectURI: "https://yourapp.com/callback",
    Scopes: []discord.OAuth2Scope{
        discord.OAuth2ScopeIdentify,
        discord.OAuth2ScopeGuilds,
        discord.OAuth2ScopeEmail,
    },
}

authURL := client.GenerateAuthorizationURL(params)
// Redirect user to authURL
The state parameter is automatically generated for CSRF protection.
2

Handle callback

When the user authorizes your app, Discord redirects them to your callback URL with a code and state:
func handleCallback(w http.ResponseWriter, r *http.Request) {
    code := r.URL.Query().Get("code")
    state := r.URL.Query().Get("state")

    session, webhook, err := client.StartSession(code, state)
    if err != nil {
        // Handle error (invalid state, expired code, etc.)
        return
    }

    // Store session for the user
    saveUserSession(session)
}
3

Use the session

Use the session to access user data:
user, err := client.GetUser(session)
if err != nil {
    // Handle error
}

fmt.Printf("Logged in as %s#%s\n", user.Username, user.Discriminator)

Available OAuth2 scopes

You can request different scopes to access various user data:
  • OAuth2ScopeIdentify - Basic user info (username, avatar, etc.)
  • OAuth2ScopeEmail - User’s email address
  • OAuth2ScopeGuilds - List of guilds the user is in
  • OAuth2ScopeGuildsMembersRead - Guild member info
  • OAuth2ScopeConnections - User’s connected accounts
  • OAuth2ScopeRoleConnectionsWrite - Manage role connections
  • OAuth2ScopeWebhookIncoming - Create incoming webhooks
params := oauth2.AuthorizationURLParams{
    RedirectURI: "https://yourapp.com/callback",
    Scopes: []discord.OAuth2Scope{
        discord.OAuth2ScopeIdentify,
        discord.OAuth2ScopeEmail,
        discord.OAuth2ScopeGuilds,
        discord.OAuth2ScopeConnections,
    },
}

Session management

Session structure

A session contains the access token and metadata:
type Session struct {
    AccessToken  string                 // Token for API requests
    RefreshToken string                 // Token to refresh access
    Scopes       []discord.OAuth2Scope  // Granted scopes
    TokenType    discord.TokenType      // Usually "Bearer"
    Expiration   time.Time              // When token expires
}

Refreshing tokens

Access tokens expire after a period (typically 7 days). Refresh them before expiration:
if session.Expired() {
    newSession, err := client.RefreshSession(session)
    if err != nil {
        // Handle error
    }
    
    // Update stored session
    saveUserSession(newSession)
    session = newSession
}

Automatic verification

Verify and refresh if needed in one call:
session, err = client.VerifySession(session)
if err != nil {
    // Handle error
}
VerifySession checks if the session is expired and automatically refreshes it if necessary.

Accessing user data

Once you have a valid session, you can access various user data based on the granted scopes.

Get user information

Requires the identify scope:
user, err := client.GetUser(session)
if err != nil {
    if errors.Is(err, oauth2.ErrSessionExpired) {
        // Refresh session
    }
    if errors.Is(err, oauth2.ErrMissingOAuth2Scope(discord.OAuth2ScopeIdentify)) {
        // Missing required scope
    }
}

fmt.Printf("User: %s (%s)\n", user.Username, user.ID)
fmt.Printf("Avatar: %s\n", user.AvatarURL())

Get user guilds

Requires the guilds scope:
guilds, err := client.GetGuilds(session)
if err != nil {
    // Handle error
}

for _, guild := range guilds {
    fmt.Printf("Guild: %s (Owner: %t)\n", guild.Name, guild.Owner)
}

Get user connections

Requires the connections scope:
connections, err := client.GetConnections(session)
if err != nil {
    // Handle error
}

for _, conn := range connections {
    fmt.Printf("Connected: %s (%s)\n", conn.Type, conn.Name)
}

Get guild member info

Requires the guilds.members.read scope:
member, err := client.GetMember(session, guildID)
if err != nil {
    // Handle error
}

fmt.Printf("Joined: %s\n", member.JoinedAt)
fmt.Printf("Roles: %v\n", member.RoleIDs)

State controller

The state controller manages CSRF tokens for the OAuth2 flow. DisGo provides a default implementation, but you can customize it.

Custom state controller

type CustomStateController struct {
    // Your implementation
}

func (c *CustomStateController) NewState(redirectURI string) string {
    // Generate and store state
    return randomState
}

func (c *CustomStateController) UseState(state string) string {
    // Verify and consume state, return redirect URI
    return redirectURI
}

client := oauth2.New(clientID, clientSecret,
    oauth2.WithStateController(&CustomStateController{}),
)

Configure default state controller

import "time"

client := oauth2.New(clientID, clientSecret,
    oauth2.WithStateControllerOpts(
        oauth2.WithStateControllerMaxTTL(15*time.Minute),
        oauth2.WithStateControllerLogger(logger),
    ),
)

Guild installation

You can prompt users to add your bot to a guild during OAuth2:
params := oauth2.AuthorizationURLParams{
    RedirectURI: "https://yourapp.com/callback",
    Scopes: []discord.OAuth2Scope{
        discord.OAuth2ScopeIdentify,
        discord.OAuth2ScopeBot,
    },
    Permissions:        discord.PermissionSendMessages | discord.PermissionManageMessages,
    GuildID:            guildID, // Pre-select a guild
    DisableGuildSelect: false,
}

authURL := client.GenerateAuthorizationURL(params)
When using OAuth2ScopeBot, the user will be prompted to add your bot with the specified permissions.

Complete web server example

Here’s a complete example implementing OAuth2 in a web server:
package main

import (
    "encoding/json"
    "fmt"
    "log/slog"
    "net/http"
    "os"
    "sync"

    "github.com/disgoorg/disgo/discord"
    "github.com/disgoorg/disgo/oauth2"
    "github.com/disgoorg/snowflake/v2"
)

var (
    clientID     = snowflake.GetEnv("CLIENT_ID")
    clientSecret = os.Getenv("CLIENT_SECRET")
    baseURL      = os.Getenv("BASE_URL")
    
    client   *oauth2.Client
    sessions = make(map[string]oauth2.Session)
    mu       sync.RWMutex
)

func main() {
    client = oauth2.New(clientID, clientSecret)

    http.HandleFunc("/", handleRoot)
    http.HandleFunc("/login", handleLogin)
    http.HandleFunc("/callback", handleCallback)
    http.HandleFunc("/logout", handleLogout)

    slog.Info("Server starting on :8080")
    http.ListenAndServe(":8080", nil)
}

func handleRoot(w http.ResponseWriter, r *http.Request) {
    cookie, err := r.Cookie("session_id")
    if err != nil {
        // Not logged in
        fmt.Fprintf(w, `<a href="/login">Login with Discord</a>`)
        return
    }

    mu.RLock()
    session, ok := sessions[cookie.Value]
    mu.RUnlock()

    if !ok {
        fmt.Fprintf(w, `<a href="/login">Login with Discord</a>`)
        return
    }

    user, err := client.GetUser(session)
    if err != nil {
        slog.Error("failed to get user", slog.Any("err", err))
        http.Error(w, "Failed to get user data", http.StatusInternalServerError)
        return
    }

    data, _ := json.MarshalIndent(user, "", "  ")
    fmt.Fprintf(w, "<h1>Welcome %s!</h1>", user.Username)
    fmt.Fprintf(w, "<pre>%s</pre>", data)
    fmt.Fprintf(w, `<a href="/logout">Logout</a>`)
}

func handleLogin(w http.ResponseWriter, r *http.Request) {
    params := oauth2.AuthorizationURLParams{
        RedirectURI: baseURL + "/callback",
        Scopes: []discord.OAuth2Scope{
            discord.OAuth2ScopeIdentify,
            discord.OAuth2ScopeEmail,
            discord.OAuth2ScopeGuilds,
        },
    }

    authURL := client.GenerateAuthorizationURL(params)
    http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
}

func handleCallback(w http.ResponseWriter, r *http.Request) {
    code := r.URL.Query().Get("code")
    state := r.URL.Query().Get("state")

    if code == "" || state == "" {
        http.Error(w, "Missing code or state", http.StatusBadRequest)
        return
    }

    session, _, err := client.StartSession(code, state)
    if err != nil {
        slog.Error("failed to start session", slog.Any("err", err))
        http.Error(w, "Failed to authenticate", http.StatusInternalServerError)
        return
    }

    // Generate session ID
    sessionID := generateRandomID()

    mu.Lock()
    sessions[sessionID] = session
    mu.Unlock()

    http.SetCookie(w, &http.Cookie{
        Name:  "session_id",
        Value: sessionID,
        Path:  "/",
    })

    http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}

func handleLogout(w http.ResponseWriter, r *http.Request) {
    cookie, err := r.Cookie("session_id")
    if err == nil {
        mu.Lock()
        delete(sessions, cookie.Value)
        mu.Unlock()
    }

    http.SetCookie(w, &http.Cookie{
        Name:   "session_id",
        Value:  "",
        Path:   "/",
        MaxAge: -1,
    })

    http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}

func generateRandomID() string {
    // Implement secure random ID generation
    return "random_session_id"
}

Configuration options

  • WithLogger(*slog.Logger) - Set custom logger
  • WithRestClient(rest.Client) - Use custom REST client
  • WithRestClientConfigOpts(...rest.ClientConfigOpt) - Configure REST client
  • WithOAuth2(rest.OAuth2) - Custom OAuth2 REST implementation
  • WithStateController(StateController) - Custom state controller
  • WithStateControllerOpts(...StateControllerConfigOpt) - Configure state controller

Best practices

Never store sessions in cookies or client-side storage. Keep them server-side in a secure database:
// Good: Server-side session storage
sessionID := generateSecureRandomID()
database.StoreSession(sessionID, session)
http.SetCookie(w, &http.Cookie{
    Name:     "session_id",
    Value:    sessionID,
    HttpOnly: true,
    Secure:   true,
    SameSite: http.SameSiteStrictMode,
})
Always validate the state parameter to prevent CSRF attacks. The built-in state controller does this automatically:
session, _, err := client.StartSession(code, state)
if errors.Is(err, oauth2.ErrStateNotFound) {
    // Invalid or expired state
    return
}
Check for expiration before making API calls and refresh when needed:
if session.Expired() {
    session, err = client.RefreshSession(session)
    if err != nil {
        // Re-authenticate user
        return
    }
    updateStoredSession(session)
}
Only request the OAuth2 scopes your application actually needs. Users are more likely to authorize apps that request fewer permissions.