package auth import ( "context" "encoding/json" "fmt" "net/http" "strings" "time" "github.com/google/go-querystring/query" ) type Auth struct { client *http.Client clientId string clientSecret string redirectUri string stateStorage StateStorage } func New(clientId string, clientSecret string, redirectUri string) *Auth { return NewWithClient(clientId, clientSecret, redirectUri, http.DefaultClient) } func NewWithClient(clientId string, clientSecret string, redirectUri string, client *http.Client) *Auth { return &Auth{ client: client, clientId: clientId, clientSecret: clientSecret, redirectUri: redirectUri, stateStorage: NewHttpCookieStateStorage(StateStorageCookie), } } const TokenUrl = "https://id.twitch.tv/oauth2/token" type GetTokenParams struct { ClientId string `url:"client_id"` ClientSecret string `url:"client_secret"` Code string `url:"code"` GrantType string `url:"grant_type"` RedirectUri string `url:"redirect_uri"` } // GetToken exchanges an authorization code or refresh token for an access token. func (a *Auth) GetToken(ctx context.Context, params *GetTokenParams) (*Token, error) { v, err := query.Values(params) if err != nil { return nil, err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, TokenUrl, strings.NewReader(v.Encode())) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") res, err := a.client.Do(req) if err != nil { return nil, err } defer res.Body.Close() statusOK := res.StatusCode >= 200 && res.StatusCode < 300 if !statusOK { return nil, fmt.Errorf("failed to get token (%d)", res.StatusCode) } var token Token if err := json.NewDecoder(res.Body).Decode(&token); err != nil { return nil, err } token.Expiry = time.Now().Add(time.Duration(token.ExpiresIn) * time.Second) return &token, nil } // GetTokenFromCode exchanges an authorization code for an access token. // // https://dev.twitch.tv/docs/authentication/getting-tokens-oidc/#oidc-authorization-code-grant-flow func (a *Auth) GetTokenFromCode(ctx context.Context, code string) (*Token, error) { return a.GetToken(ctx, &GetTokenParams{ ClientId: a.clientId, ClientSecret: a.clientSecret, Code: code, GrantType: "authorization_code", RedirectUri: a.redirectUri, }) } // RefreshToken exchanges a refresh token for an access token. // // https://dev.twitch.tv/docs/authentication/refresh-tokens/ func (a *Auth) RefreshToken(ctx context.Context, token *Token) (*Token, error) { return a.GetToken(ctx, &GetTokenParams{ ClientId: a.clientId, ClientSecret: a.clientSecret, Code: token.RefreshToken, GrantType: "refresh_token", RedirectUri: a.redirectUri, }) } // WithStateStorage sets the instance's state storage, // which is used to store the state parameter between requests. // // By default, the http cookie state storage is used. func (a *Auth) WithStateStorage(storage StateStorage) *Auth { a.stateStorage = storage return a }