Skip to content

Latest commit

 

History

History
439 lines (330 loc) · 9.41 KB

File metadata and controls

439 lines (330 loc) · 9.41 KB
layout title description
default
Auth API
OAuth 2.0 authentication for YouTube APIs with automatic token refresh.

Overview

Handle OAuth 2.0 authentication for YouTube APIs:

OAuth Flow: Complete Google OAuth 2.0 implementation

  • Generate authorization URLs
  • Exchange codes for tokens
  • Automatic token refresh

Device Code Flow: For limited-input devices

  • TVs, consoles, and CLI applications
  • User enters code on separate device
  • Automatic polling for authorization

Service Accounts: Server-to-server authentication

  • JWT-based authentication
  • No user interaction required
  • Domain-wide delegation support

Token Management: Secure token handling

  • Thread-safe token storage
  • Automatic refresh before expiry
  • Callbacks for token events

Scopes

const (
    // ScopeLiveChat grants read access to live chat messages.
    ScopeLiveChat = "https://www.googleapis.com/auth/youtube"

    // ScopeLiveChatModerate grants moderator access to live chat.
    ScopeLiveChatModerate = "https://www.googleapis.com/auth/youtube.force-ssl"

    // ScopeReadOnly grants read-only access to YouTube account.
    ScopeReadOnly = "https://www.googleapis.com/auth/youtube.readonly"

    // ScopeUpload grants access to upload videos and manage playlists.
    ScopeUpload = "https://www.googleapis.com/auth/youtube.upload"

    // ScopePartner grants access to YouTube Analytics.
    ScopePartner = "https://www.googleapis.com/auth/youtubepartner"
)

NewAuthClient

Create a new OAuth client with configuration.

authClient := auth.NewAuthClient(auth.Config{
    ClientID:     "your-client-id",
    ClientSecret: "your-client-secret",
    RedirectURL:  "http://localhost:8080/callback",
    Scopes: []string{
        auth.ScopeLiveChat,
        auth.ScopeLiveChatModerate,
    },
})

Options

authClient := auth.NewAuthClient(config,
    auth.WithHTTPClient(customHTTPClient),
    auth.WithToken(existingToken),
    auth.WithRefreshEarly(5*time.Minute),
    auth.WithOnTokenRefresh(func(token *auth.Token) {
        // Save token to storage
    }),
    auth.WithOnRefreshError(func(err error) {
        log.Printf("Token refresh failed: %v", err)
    }),
)

OAuth Flow

AuthorizationURL

Generate the URL to redirect users for authorization.

state := generateRandomState() // Use a cryptographically secure random string
url := authClient.AuthorizationURL(state)

// Redirect user to url
http.Redirect(w, r, url, http.StatusFound)

Options

url := authClient.AuthorizationURL(state,
    auth.WithPrompt("consent"),      // Force consent screen
    auth.WithLoginHint("user@example.com"), // Hint which account
)

Exchange

Exchange an authorization code for a token.

// In your callback handler:
code := r.URL.Query().Get("code")

token, err := authClient.Exchange(ctx, code)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Access token: %s\n", token.AccessToken)
fmt.Printf("Expires: %s\n", token.Expiry)

Token Management

Token

Get the current token (thread-safe).

token := authClient.Token()
if token != nil {
    fmt.Printf("Access token: %s\n", token.AccessToken)
    fmt.Printf("Valid: %v\n", token.Valid())
}

SetToken

Set a token (e.g., loaded from storage).

authClient.SetToken(&auth.Token{
    AccessToken:  savedAccessToken,
    RefreshToken: savedRefreshToken,
    Expiry:       savedExpiry,
    Scopes:       savedScopes,
})

AccessToken

Get a valid access token, automatically refreshing if expired.

accessToken, err := authClient.AccessToken(ctx)
if err != nil {
    log.Fatal(err)
}
// Use accessToken for API calls

Refresh

Manually refresh the access token.

newToken, err := authClient.Refresh(ctx)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("New access token: %s\n", newToken.AccessToken)

Auto-Refresh

StartAutoRefresh

Start automatic token refresh in the background.

err := authClient.StartAutoRefresh(ctx)
if err != nil {
    log.Fatal(err)
}

// Token will be refreshed automatically before expiry

StopAutoRefresh

Stop the auto-refresh goroutine.

authClient.StopAutoRefresh()

Token Type

type Token struct {
    AccessToken  string
    TokenType    string
    RefreshToken string
    Expiry       time.Time
    Scopes       []string
}

Methods

// Check if token is valid (not expired)
valid := token.Valid()

// Get access token (thread-safe)
accessToken := token.GetAccessToken()

// Clone token (for safe passing between goroutines)
clone := token.Clone()

// Serialize to JSON
data, err := token.MarshalJSON()

// Deserialize from JSON
err := token.UnmarshalJSON(data)

Example: Complete OAuth Flow

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"

    "github.com/Its-donkey/yougopher/youtube/auth"
)

func main() {
    authClient := auth.NewAuthClient(auth.Config{
        ClientID:     "your-client-id",
        ClientSecret: "your-client-secret",
        RedirectURL:  "http://localhost:8080/callback",
        Scopes:       []string{auth.ScopeLiveChat, auth.ScopeLiveChatModerate},
    },
        auth.WithOnTokenRefresh(func(token *auth.Token) {
            log.Println("Token refreshed, save to storage...")
        }),
    )

    // Step 1: Redirect to authorization URL
    http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
        state := "random-state-string" // Use crypto/rand in production
        url := authClient.AuthorizationURL(state)
        http.Redirect(w, r, url, http.StatusFound)
    })

    // Step 2: Handle callback
    http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
        code := r.URL.Query().Get("code")

        token, err := authClient.Exchange(r.Context(), code)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        // Start auto-refresh
        authClient.StartAutoRefresh(context.Background())

        fmt.Fprintf(w, "Authenticated! Token expires: %s", token.Expiry)
    })

    log.Println("Visit http://localhost:8080/login to authenticate")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Device Code Flow

For devices with limited input capabilities (TVs, consoles, CLI applications).

NewDeviceClient

Create a device code flow client.

deviceClient := auth.NewDeviceClient(auth.DeviceConfig{
    ClientID: "your-client-id",
    Scopes:   []string{auth.ScopeLiveChat},
})

RequestDeviceCode

Initiate the device authorization flow.

authResp, err := deviceClient.RequestDeviceCode(ctx)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Go to %s\n", authResp.VerificationURL)
fmt.Printf("Enter code: %s\n", authResp.UserCode)
fmt.Printf("Code expires in %d seconds\n", authResp.ExpiresIn)

PollForToken

Poll for authorization (blocking).

token, err := deviceClient.PollForToken(ctx, authResp)
if err != nil {
    var deviceErr *auth.DeviceAuthError
    if errors.As(err, &deviceErr) {
        if deviceErr.IsAccessDenied() {
            log.Println("User denied access")
        }
        if deviceErr.IsExpired() {
            log.Println("Code expired, request new code")
        }
    }
    log.Fatal(err)
}

fmt.Printf("Authenticated! Access token: %s\n", token.AccessToken)

PollForTokenAsync

Poll asynchronously with cancel support.

tokenCh, errCh, cancel := deviceClient.PollForTokenAsync(ctx, authResp)

select {
case token := <-tokenCh:
    fmt.Printf("Got token: %s\n", token.AccessToken)
case err := <-errCh:
    log.Fatal(err)
case <-time.After(5 * time.Minute):
    cancel() // Cancel polling
    log.Fatal("Timeout waiting for authorization")
}

Service Account Authentication

For server-to-server communication without user interaction.

From JSON Credentials

Load from Google Cloud service account JSON file.

jsonData, err := os.ReadFile("service-account.json")
if err != nil {
    log.Fatal(err)
}

client, err := auth.NewServiceAccountClientFromJSON(jsonData,
    []string{auth.ScopeReadOnly, auth.ScopePartner},
)
if err != nil {
    log.Fatal(err)
}

From Config

Create from configuration values.

client, err := auth.NewServiceAccountClient(auth.ServiceAccountConfig{
    Email:      "service@project.iam.gserviceaccount.com",
    PrivateKey: pemEncodedPrivateKey,
    Scopes:     []string{auth.ScopeReadOnly},
})

Options

client, err := auth.NewServiceAccountClient(config,
    auth.WithServiceAccountHTTPClient(customHTTPClient),
    auth.WithServiceAccountRefreshEarly(10*time.Minute),
    auth.WithSubject("user@domain.com"), // Domain-wide delegation
    auth.WithServiceAccountOnTokenRefresh(func(token *auth.Token) {
        log.Println("Token refreshed")
    }),
)

AccessToken

Get a valid access token (fetches new token if needed).

accessToken, err := client.AccessToken(ctx)
if err != nil {
    log.Fatal(err)
}
// Use accessToken for API calls

FetchToken

Explicitly fetch a new token.

token, err := client.FetchToken(ctx)
if err != nil {
    log.Fatal(err)
}

Auto-Refresh

Service accounts support auto-refresh like OAuth clients.

err := client.StartAutoRefresh(ctx)
if err != nil {
    log.Fatal(err)
}

// Token will be refreshed automatically before expiry
defer client.StopAutoRefresh()

Thread Safety

AuthClient, DeviceClient, and ServiceAccountClient are all safe for concurrent use. All token operations are protected by mutex locks.