package auth
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
"strings"
)
var (
clientID = os.Getenv("TWITCH_CLIENT_ID")
clientSecret = os.Getenv("TWITCH_CLIENT_SECRET")
redirectURI = "http://localhost:8080/callback"
scopes = []string{
"chat:read",
"chat:edit",
"channel:read:subscriptions",
"user:read:email",
}
)
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
Scope []string `json:"scope"`
TokenType string `json:"token_type"`
}
// start http server for oauth login & callback
func StartOAuthServer(addr string) error {
if clientID == "" || clientSecret == "" {
return fmt.Errorf("TWITCH_CLIENT_ID and TWITCH_CLIENT_SECRET must be set")
}
http.HandleFunc("/", handleIndex)
http.HandleFunc("/callback", handleCallback)
http.HandleFunc("/refresh", handleRefresh)
log.Printf("OAuth server listening on %s", addr)
return http.ListenAndServe(addr, nil)
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
authURL := fmt.Sprintf(
"https://id.twitch.tv/oauth2/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&force_verify=true",
url.QueryEscape(clientID),
url.QueryEscape(redirectURI),
url.QueryEscape(strings.Join(scopes, " ")),
)
html := fmt.Sprintf(`
Twitch OAuth Login
Login with Twitch
`, authURL)
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(html))
}
func handleCallback(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
if code == "" {
http.Error(w, "Missing code in query", http.StatusBadRequest)
return
}
token, err := exchangeCodeForToken(code)
if err != nil {
http.Error(w, "Failed to get token: "+err.Error(), http.StatusInternalServerError)
return
}
jsonData, _ := json.MarshalIndent(token, "", " ")
w.Header().Set("Content-Type", "application/ijson")
w.Write(jsonData)
}
func handleRefresh(w http.ResponseWriter, r *http.Request) {
refresh := r.URL.Query().Get("refresh_token")
if refresh == "" {
http.Error(w, "Missing refresh_token parameter", http.StatusBadRequest)
return
}
tr, err := ExchangeRefreshToken(refresh)
if err != nil {
http.Error(w, "Refresh failed: "+err.Error(), http.StatusInternalServerError)
return
}
data, _ := json.MarshalIndent(tr, "", " ")
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}
func exchangeCodeForToken(code string) (*TokenResponse, error) {
data := url.Values{}
data.Set("client_id", clientID)
data.Set("client_secret", clientSecret)
data.Set("code", code)
data.Set("grant_type", "authorization_code")
data.Set("redirect_uri", redirectURI)
resp, err := http.Post(
"https://id.twitch.tv/oauth2/token",
"application/x-www-form-urlencoded",
strings.NewReader(data.Encode()),
)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("token exchange failed: %s", resp.Status)
}
var tokenResp TokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return nil, err
}
return &tokenResp, nil
}
// refresh tokens programtically
func ExchangeRefreshToken(refreshToken string) (*TokenResponse, error) {
form := url.Values{}
form.Set("client_id", clientID)
form.Set("client_secret", clientSecret)
form.Set("grant_type", "refresh_token")
form.Set("refresh_token", refreshToken)
resp, err := http.Post(
"https://id.twitch.tv/oauth2/token",
"application/x-www-form-urlencoded",
strings.NewReader(form.Encode()),
)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("refresh failed: %s", resp.Status)
}
var tr TokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tr); err != nil {
return nil, err
}
return &tr, nil
}