Overview
Twitch uses OAuth 2.0 for authentication. The flow you choose depends on your application type:
| Flow | Use Case | User Interaction | Token Type |
|---|---|---|---|
| Client Credentials | Server-side apps, public data | None | App Access Token |
| Authorization Code | Web apps with backend | Browser redirect | User Access Token |
| Device Code | CLIs, TVs, IoT devices | Code on separate device | User Access Token |
| Implicit Grant | Single-page apps (SPAs) | Browser redirect | User Access Token |
Key Concepts:
- App Access Tokens: Identify your application, used for public API endpoints
- User Access Tokens: Identify a specific user, required for user-specific data and actions
- Scopes: Define what permissions your app requests (e.g.,
chat:read,channel:manage:redemptions) - Refresh Tokens: Used to get new access tokens without re-authenticating the user
Client Credentials Flow (App Access Token)
Use this flow for server-to-server API calls that don’t require user authorization. This is the simplest flow - your server exchanges its client credentials directly for an access token.
When to use: Fetching public data like user profiles, live streams, or game information.
Limitations: Cannot access user-specific data or perform actions on behalf of a user.
package main
import (
"context"
"fmt"
"log"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
})
// Get app access token
token, err := authClient.GetAppAccessToken(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Access Token: %s\n", token.AccessToken)
fmt.Printf("Expires In: %d seconds\n", token.ExpiresIn)
fmt.Printf("Token Type: %s\n", token.TokenType)
// Create Helix client with app token
client := helix.NewClient("your-client-id", authClient)
// Now make API calls (limited to endpoints that accept app tokens)
users, _ := client.GetUsers(ctx, &helix.GetUsersParams{
Logins: []string{"twitchdev"},
})
fmt.Printf("User: %s\n", users.Data[0].DisplayName)
}
Authorization Code Flow (User Access Token)
The most common flow for web applications. The user is redirected to Twitch to authorize your app, then redirected back with an authorization code that you exchange for tokens.
How it works:
- Generate an authorization URL with requested scopes and a CSRF state token
- Redirect user to Twitch to authorize your application
- User approves, Twitch redirects back with an authorization code
- Your server exchanges the code for access and refresh tokens
Security: Always validate the state parameter to prevent CSRF attacks. The state should be a cryptographically random string stored in the user’s session.
package main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
RedirectURI: "http://localhost:3000/callback",
Scopes: []string{
helix.ScopeUserReadEmail,
helix.ScopeChatRead,
helix.ScopeChatEdit,
},
})
// Generate authorization URL with CSRF state
state := "random-csrf-token"
url := authClient.GetCodeAuthURLWithState(state)
fmt.Printf("Open this URL in your browser:\n%s\n\n", url)
// Handle the OAuth callback
var authCode string
http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
// Verify state to prevent CSRF
if r.URL.Query().Get("state") != state {
http.Error(w, "Invalid state", http.StatusBadRequest)
return
}
authCode = r.URL.Query().Get("code")
fmt.Fprintf(w, "Authorization successful! You can close this window.")
// Exchange code for token
token, err := authClient.ExchangeCode(ctx, authCode)
if err != nil {
log.Printf("Token exchange failed: %v", err)
return
}
fmt.Printf("\nAccess Token: %s\n", token.AccessToken)
fmt.Printf("Refresh Token: %s\n", token.RefreshToken)
fmt.Printf("Scopes: %v\n", token.Scope)
})
log.Fatal(http.ListenAndServe(":3000", nil))
}
Device Code Flow
Designed for devices that don’t have a browser or have limited input capabilities (smart TVs, game consoles, CLI tools). The user authorizes on a separate device (like their phone) by entering a code.
How it works:
- Request a device code and user code from Twitch
- Display the user code and verification URL to the user
- User visits the URL on another device and enters the code
- Your app polls for the token until authorization completes or times out
User experience: The user sees something like “Go to twitch.tv/activate and enter code: ABCD-1234”
package main
import (
"context"
"fmt"
"log"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
Scopes: []string{
helix.ScopeUserReadEmail,
helix.ScopeChatRead,
},
})
// Request device code
deviceCode, err := authClient.GetDeviceCode(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Go to: %s\n", deviceCode.VerificationURI)
fmt.Printf("Enter code: %s\n", deviceCode.UserCode)
fmt.Printf("Expires in: %d seconds\n", deviceCode.ExpiresIn)
// Poll for token (blocks until user authorizes or timeout)
token, err := authClient.WaitForDeviceToken(ctx, deviceCode)
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nAuthorization successful!\n")
fmt.Printf("Access Token: %s\n", token.AccessToken)
}
Implicit Grant Flow
For client-side JavaScript applications (SPAs) where the client secret cannot be kept confidential. The access token is returned directly in the URL fragment.
Important limitations:
- No refresh tokens - user must re-authorize when token expires
- Token is exposed in browser history and potentially to JavaScript on the page
- Consider using Authorization Code Flow with PKCE for better security
When to use: Legacy SPAs or simple browser extensions where you can’t use a backend server.
package main
import (
"fmt"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
RedirectURI: "http://localhost:3000/callback",
Scopes: []string{
helix.ScopeUserReadEmail,
},
})
// Generate implicit auth URL
// Token will be in the URL fragment after redirect
state := "random-csrf-token"
url := authClient.GetImplicitAuthURLWithState(state)
fmt.Printf("Redirect user to:\n%s\n", url)
// After redirect, extract token from URL fragment:
// http://localhost:3000/callback#access_token=xxx&token_type=bearer&scope=user:read:email
}
Token Refresh
User access tokens expire (typically after 4 hours). Use the refresh token to get a new access token without requiring the user to re-authorize.
Important: Refresh tokens can also expire or be revoked. Always handle refresh failures by falling back to re-authentication.
Best practice: Store refresh tokens securely (encrypted in database) and update them after each refresh, as Twitch may issue new refresh tokens.
package main
import (
"context"
"fmt"
"log"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
})
// Refresh an expired token
refreshToken := "stored-refresh-token"
newToken, err := authClient.RefreshToken(ctx, refreshToken)
if err != nil {
log.Fatal(err)
}
fmt.Printf("New Access Token: %s\n", newToken.AccessToken)
fmt.Printf("New Refresh Token: %s\n", newToken.RefreshToken)
// Store the new refresh token for future use
}
Token Validation
Verify that a token is still valid and retrieve information about it. This is useful for checking token status before making API calls, or for getting the user ID associated with a token.
Returns: Client ID, user ID (for user tokens), login name, granted scopes, and time until expiration.
When to use: On app startup to check stored tokens, or periodically to ensure tokens haven’t been revoked.
package main
import (
"context"
"fmt"
"log"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
})
token := "access-token-to-validate"
validation, err := authClient.ValidateToken(ctx, token)
if err != nil {
log.Printf("Token is invalid: %v", err)
return
}
fmt.Printf("Client ID: %s\n", validation.ClientID)
fmt.Printf("User ID: %s\n", validation.UserID)
fmt.Printf("Login: %s\n", validation.Login)
fmt.Printf("Scopes: %v\n", validation.Scopes)
fmt.Printf("Expires In: %d seconds\n", validation.ExpiresIn)
}
Token Revocation
Invalidate a token so it can no longer be used. This is important for security when users log out of your application.
When to use: User logout, account deletion, or when you detect suspicious activity.
Note: Revoking an access token also invalidates its associated refresh token.
package main
import (
"context"
"fmt"
"log"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
})
token := "token-to-revoke"
err := authClient.RevokeToken(ctx, token)
if err != nil {
log.Fatal(err)
}
fmt.Println("Token revoked successfully")
}
Automatic Token Refresh
For long-running applications, you can set up automatic token refresh to ensure your tokens never expire during operation. The library will refresh tokens before they expire and notify you via a callback.
How it works: The auto-refresh mechanism monitors token expiration and refreshes proactively (before expiry). The callback lets you persist the new tokens.
Use case: Bots, dashboards, or any service that runs continuously.
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
})
// Set initial token
authClient.SetToken(&helix.TokenResponse{
AccessToken: "current-access-token",
RefreshToken: "current-refresh-token",
ExpiresIn: 3600,
})
// Start automatic refresh (refreshes when token is near expiry)
stopRefresh := authClient.StartAutoRefresh(ctx, func(token *helix.TokenResponse, err error) {
if err != nil {
log.Printf("Auto-refresh failed: %v", err)
return
}
fmt.Printf("Token auto-refreshed at %s\n", time.Now().Format(time.RFC3339))
// Store new token in database
})
// Stop auto-refresh when done
defer stopRefresh()
// Your application runs here...
select {}
}
OpenID Connect (OIDC)
OIDC extends OAuth 2.0 to provide identity verification. When you request the openid scope, you can retrieve verified user information like email address and profile picture.
When to use: User registration/login where you need verified identity information rather than just authorization.
Required scope: openid (optionally with user:read:email for email access)
package main
import (
"context"
"fmt"
"log"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
RedirectURI: "http://localhost:3000/callback",
Scopes: []string{
helix.ScopeOpenID,
helix.ScopeUserReadEmail,
},
})
// After getting an access token with openid scope...
// Get user info from OIDC endpoint
userInfo, err := authClient.GetOIDCUserInfo(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Subject (User ID): %s\n", userInfo.Sub)
fmt.Printf("Preferred Username: %s\n", userInfo.PreferredUsername)
fmt.Printf("Email: %s\n", userInfo.Email)
fmt.Printf("Email Verified: %v\n", userInfo.EmailVerified)
fmt.Printf("Picture: %s\n", userInfo.Picture)
}
Per-Request Token Override
When making concurrent requests that each require a different user token, use WithToken to override the client-level token on a per-request basis. This avoids creating multiple client instances.
When to use: Fetching scoped data for multiple channels concurrently (e.g., followers, subscriptions) where each channel requires its own user token.
package main
import (
"context"
"fmt"
"log"
"sync"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
// One client shared across all requests
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
})
client := helix.NewClient("your-client-id", authClient)
// Each channel has its own user token (obtained via Authorization Code flow)
type Channel struct {
BroadcasterID string
Token *helix.Token
}
channels := []Channel{
{BroadcasterID: "111", Token: &helix.Token{AccessToken: "user-token-a"}},
{BroadcasterID: "222", Token: &helix.Token{AccessToken: "user-token-b"}},
{BroadcasterID: "333", Token: &helix.Token{AccessToken: "user-token-c"}},
}
// Fetch followers for all channels concurrently
var wg sync.WaitGroup
for _, ch := range channels {
wg.Add(1)
go func(ch Channel) {
defer wg.Done()
// Override the client token for this request
reqCtx := helix.WithToken(ctx, ch.Token)
followers, err := client.GetChannelFollowers(reqCtx, &helix.GetChannelFollowersParams{
BroadcasterID: ch.BroadcasterID,
})
if err != nil {
log.Printf("Error fetching followers for %s: %v", ch.BroadcasterID, err)
return
}
fmt.Printf("Channel %s has %d followers\n", ch.BroadcasterID, *followers.Total)
}(ch)
}
wg.Wait()
}
Common Scope Combinations
The library provides pre-defined scope combinations for common use cases. These help you request the right permissions without having to look up individual scope names.
Principle of least privilege: Only request the scopes you actually need. Users are more likely to authorize apps that request minimal permissions.
// Chat bot scopes
helix.CommonScopes.Chat // chat:read, chat:edit
// Full bot functionality
helix.CommonScopes.Bot // chat scopes + whispers, user read
// Moderation tools
helix.CommonScopes.Moderation // ban, manage messages, automod, etc.
// Channel management
helix.CommonScopes.Channel // channel:manage:*, channel:read:*
// Full broadcaster access
helix.CommonScopes.Broadcaster // all broadcaster-level scopes