--- /dev/null
+package client
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/url"
+
+ "go.openfl.eu/twitch-api/auth"
+)
+
+func Authenticate(c *Config) (Response, error) {
+ form := url.Values{}
+ form.Add("client_id", c.ClientId)
+ form.Add("client_secret", c.ClientSecret)
+ form.Add("grant_type", "client_credentials")
+ resp, err := http.PostForm("https://id.twitch.tv/oauth2/token", form)
+
+ var response Response
+ if err != nil {
+ return response, err
+ }
+
+ defer resp.Body.Close()
+
+ err = json.NewDecoder(resp.Body).Decode(&response)
+ return response, err
+}
+
+func (r Response) ToTokensource() auth.TokenSource {
+ source := make(chan auth.Token)
+ go func() {
+ source <- r.AccessToken
+ }()
+ return source
+}
--- /dev/null
+// implements https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#client-credentials-grant-flow
+package client
--- /dev/null
+package client
+
+import "go.openfl.eu/twitch-api/auth"
+
+type Config struct {
+ ClientId auth.ClientId
+ ClientSecret string
+}
+
+type Response struct {
+ AccessToken auth.Token `json:"access_token"`
+ ExpiresIn uint `json:"expires_in"`
+ TokenType string `json:"bearer"`
+}
--- /dev/null
+package auth
+
+type Token = string
+
+// Your client ID from https://dev.twitch.tv/docs/authentication/register-app/
+type ClientId = string
+
+type TokenSource <-chan Token
"time"
)
+// see https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#device-code-grant-flow
type Config struct {
ClientId string
Scopes []string
+++ /dev/null
-module go.openfl.eu/twitch-auth
-
-go 1.23.1
package implicit
+import "go.openfl.eu/twitch-api/auth"
+
type Config struct {
- // Your client ID from https://dev.twitch.tv/docs/authentication/register-app/
- ClientId string
- // The OAuth redirection url port. The full OAuth Redirect URL should be http://localhost:<Port>
- Port string
+ ClientId auth.ClientId
+ // The OAuth redirection url.
+ Redirect string
// Scopes from https://dev.twitch.tv/docs/authentication/scopes/
Scopes []string
// Force the user to re-authorize your app
var forwardPage []byte
type Handler struct {
- response chan AuthResponse
+ response chan AuthResponse
+ // optional pointer to http server pointer, which is initialized later.
server **http.Server
passThrough http.Handler
state string
}
+func (h *Handler) GetResponse() <-chan AuthResponse {
+ return h.response
+}
+
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Redirect from twitch to forward.html
if r.Method == http.MethodGet {
w.WriteHeader(http.StatusOK)
}
- close(h.response)
- (*h.server).Close()
+ if h.server != nil {
+ close(h.response)
+ (*h.server).Close()
+ }
}
}
"net/http"
"net/url"
"strings"
+
+ "go.openfl.eu/twitch-api/auth"
)
+func ToTokenSource(response <-chan AuthResponse) (auth.TokenSource, <-chan error) {
+ tokens := make(chan auth.Token)
+ errors := make(chan error)
+ go func() {
+ for resp := range response {
+ if resp.Err != nil {
+ errors <- resp.Err
+ } else {
+ tokens <- auth.Token(resp.AccessCode)
+ }
+ }
+ }()
+ return tokens, errors
+}
+
// See AuthenticateWithHandler
func Authenticate(c *Config) (string, <-chan AuthResponse) {
return AuthenticateWithHandler(c, &CloseHandler{})
}
+// Get a url for a user to log into and the authentication response.
+//
// There will be none or one AuthResponse on the channel.
// When there is one, the following actions are taken:
// 1) The response is written to the channel
// 3) The channel is closed
// 4) Internal resources are cleaned up
//
-// If your own handler is not a static page, redirect to your own server.
-// By the time the handler's request is received, the server for the auth
+// If your own handler is not a static page, redirect to your own server in the handler.
+// By the time the handler's request is received by the browser, the server for the auth
// flow will be closed.
func AuthenticateWithHandler(c *Config, handler http.Handler) (string, <-chan AuthResponse) {
responses := make(chan AuthResponse, 1)
}()
var s *http.Server
- s = &http.Server{Addr: c.Port, Handler: &Handler{responses, &s, handler, state}}
- fmt.Println("listening on:", s.Addr)
+ s = &http.Server{Addr: c.Redirect, Handler: &Handler{responses, &s, handler, state}}
err := s.ListenAndServe()
if !errors.Is(err, http.ErrServerClosed) {
responses <- AuthResponse{"", err}
return authUrl, responses
}
-func makeUrl(c *Config, state string) string {
- redirectUrl := fmt.Sprintf("http://localhost%s", c.Port)
+// Create a http handler that receives the access token by capturing
+// the twitch redirect.
+//
+// The passThrough handler is used to present a page to the user
+// after successful login. Defaults to CloseHandler when a nil value is given
+func AuthenticationHandler(c *Config, passThrough http.Handler) (string, *Handler) {
+ responses := make(chan AuthResponse, 1)
+ if passThrough == nil {
+ passThrough = &CloseHandler{}
+ }
+ var state string = genState()
+ return makeUrl(c, state), &Handler{responses, nil, passThrough, state}
+}
+func makeUrl(c *Config, state string) string {
query := make(url.Values)
query.Add("client_id", c.ClientId)
if c.ForceVerify {
query.Add("force_verify", "true")
}
- query.Add("redirect_uri", redirectUrl)
+ query.Add("redirect_uri", c.Redirect)
query.Add("response_type", "token")
query.Add("scope", strings.Join(c.Scopes, " "))
query.Add("state", state)
--- /dev/null
+@startuml
+
+Title headless flow
+
+actor user
+participant client
+participant library
+participant handler
+participant server
+participant browser
+participant twitch
+
+client -> library: start authentication flow
+library -> server: start server
+library --> client: login URL
+client -> user: present URL
+user -> twitch: twitch login and grant flow
+twitch --> browser: redirect with token
+browser -> server: fetch page
+server -> handler
+handler --> browser
+browser -> browser: put access token in html form
+browser -> server: POST form with access token
+server -> handler
+handler -> handler: pass through handler
+handler --> client: give access token
+handler --> browser: pass through handler
+handler -> server: stop server
+@enduml
+
+@startuml
+Title server flow
+
+actor user
+participant client
+participant library
+participant handler
+participant server
+participant browser
+participant twitch
+
+group setup
+client -> library: create handler
+library --> client
+client -> server: add handler at URL
+client -> server: start server
+end
+client -> user: present login
+note left
+The login url is the
+exact url on which the
+handler is registered
+to the server
+end note
+user -> twitch: twitch login and grant flow
+twitch --> browser: redirect with token
+browser -> server: fetch page
+server -> handler
+handler --> browser
+browser -> browser: put access token in html form
+browser -> server: POST form with access token
+server -> handler
+handler -> handler: pass through
+handler -> client: give access token
+handler --> browser: pass through handler
+
+@enduml
func TestMakeUrl(t *testing.T) {
out := makeUrl(&Config{
"hof5gwx0su6owfnys0yan9c87zr6t",
- ":1337",
+ "http://localhost:1337/login",
[]string{"channel:manage:polls", "channel:read:polls"}, true}, "abcd1234")
- expect := "https://id.twitch.tv/oauth2/authorize?response_type=token&client_id=hof5gwx0su6owfnys0yan9c87zr6t&redirect_uri=http://localhost:1337&scope=channel%3Amanage%3Apolls+channel%3Aread%3Apolls&state=abcd1234"
+ expect := "https://id.twitch.tv/oauth2/authorize?response_type=token&client_id=hof5gwx0su6owfnys0yan9c87zr6t&redirect_uri=http://localhost:1337/login&scope=channel%3Amanage%3Apolls+channel%3Aread%3Apolls&state=abcd1234"
parsedUrl, err := url.Parse(out)
if err != nil {
--- /dev/null
+package validate
+
+import (
+ "errors"
+ "time"
+
+ "go.openfl.eu/twitch-api/auth"
+)
+
+var PollPeriod = time.Hour
+
+func PollUntilFailure(token auth.Token) error {
+ tic := time.NewTicker(PollPeriod)
+ for range tic.C {
+ _, err := Validate(token)
+ if errors.Is(err, InvalidTokenError) {
+ return err
+ }
+ }
+ return nil
+}
--- /dev/null
+package validate
+
+type ValidReturn struct {
+ ClientId string `json:"client_id"`
+ Login string `json:"login"`
+ Scopes []string `json:"scopes"`
+ UserId string `json:"user_id"`
+ ExpiresIn int `json:"expires_in"`
+}
--- /dev/null
+package validate
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+
+ "go.openfl.eu/twitch-api/auth"
+)
+
+var InvalidTokenError = errors.New("the token is invalid.")
+
+func Validate(token auth.Token) (v ValidReturn, err error) {
+ req, err := http.NewRequest(http.MethodGet, "https://id.twitch.tv/oauth2/validate", nil)
+ if err != nil {
+ return v, err
+ }
+
+ req.Header["Authorization"] = []string{fmt.Sprintf("OAuth %s", token)}
+
+ resp, err := http.DefaultClient.Do(req)
+
+ // network error
+ if err != nil {
+ return v, err
+ }
+
+ defer resp.Body.Close()
+
+ // token error
+ if resp.StatusCode == http.StatusUnauthorized {
+ return v, InvalidTokenError
+ }
+
+ // other response codes
+ if resp.StatusCode != http.StatusOK {
+ return v, fmt.Errorf("unexpected status code '%s' when validating access token: %v", resp.Status, InvalidTokenError)
+ }
+
+ // finally get the output
+ err = json.NewDecoder(resp.Body).Decode(&v)
+ if err != nil {
+ return v, fmt.Errorf("couldn't decode validation response: %v\n", err)
+ }
+
+ return v, nil
+}
"bufio"
"fmt"
"os"
+ "time"
- "go.openfl.eu/twitch-auth/device"
+ "go.openfl.eu/twitch-api/auth/device"
+ "go.openfl.eu/twitch-api/auth/validate"
)
var input *bufio.Scanner
}
func main() {
+ validate.PollPeriod = time.Second * 10
+
input = bufio.NewScanner(os.Stdin)
input.Split(bufio.ScanLines)
- /*c := &device.Config{
+ c := &device.Config{
prompt("Client id please"),
promptMany("Scopes please"),
}
- */
- c := &device.Config{
- "9mcopb33ssgli53u6cor8ou2pvyb0g",
- []string{},
- }
for token := range device.Authenticate(c) {
if token.Err != nil {
import (
"fmt"
- "go.openfl.eu/twitch-auth/implicit"
+ "go.openfl.eu/twitch-api/auth/implicit"
)
func main() {