]> git.openfl.eu Git - twitch-auth.git/commitdiff
idk bunch of stuff main
authorFl_GUI <flor.guilini@hotmail.com>
Mon, 28 Jul 2025 20:04:13 +0000 (22:04 +0200)
committerFl_GUI <flor.guilini@hotmail.com>
Mon, 28 Jul 2025 20:04:13 +0000 (22:04 +0200)
19 files changed:
client/client.go [new file with mode: 0644]
client/doc.go [new file with mode: 0644]
client/types.go [new file with mode: 0644]
commontypes.go [new file with mode: 0644]
device/authorize.go
device/returnTypes.go [moved from device/return.go with 100% similarity]
go.mod [deleted file]
implicit/config.go
implicit/handler.go
implicit/implicit.go
implicit/implicit.png [new file with mode: 0644]
implicit/implicit.puml [new file with mode: 0644]
implicit/implicit_001.png [new file with mode: 0644]
implicit/implicit_test.go
validate/poll.go [new file with mode: 0644]
validate/types.go [new file with mode: 0644]
validate/validate.go [new file with mode: 0644]
x/device/main.go
x/implicit/main.go

diff --git a/client/client.go b/client/client.go
new file mode 100644 (file)
index 0000000..19c3d4f
--- /dev/null
@@ -0,0 +1,35 @@
+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
+}
diff --git a/client/doc.go b/client/doc.go
new file mode 100644 (file)
index 0000000..0a0e073
--- /dev/null
@@ -0,0 +1,2 @@
+// implements https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#client-credentials-grant-flow
+package client
diff --git a/client/types.go b/client/types.go
new file mode 100644 (file)
index 0000000..20b9a3e
--- /dev/null
@@ -0,0 +1,14 @@
+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"`
+}
diff --git a/commontypes.go b/commontypes.go
new file mode 100644 (file)
index 0000000..75f8ba7
--- /dev/null
@@ -0,0 +1,8 @@
+package auth
+
+type Token = string
+
+// Your client ID from https://dev.twitch.tv/docs/authentication/register-app/
+type ClientId = string
+
+type TokenSource <-chan Token
index a1ab86b691e0c9a623833273a01021ae83261d30..b9da94ad576e8c9b84220abc63705b22cd8c0201 100644 (file)
@@ -10,6 +10,7 @@ import (
        "time"
 )
 
+// see https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#device-code-grant-flow
 type Config struct {
        ClientId string
        Scopes   []string
similarity index 100%
rename from device/return.go
rename to device/returnTypes.go
diff --git a/go.mod b/go.mod
deleted file mode 100644 (file)
index c8adb90..0000000
--- a/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module go.openfl.eu/twitch-auth
-
-go 1.23.1
index 1b414ba484dd67c59723e841c2ade0c35a91d5df..6825cec9ab764a7e1daab7b2eada5fbd35ec873a 100644 (file)
@@ -1,10 +1,11 @@
 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
index 5b61ebab645bbe489c2da5036e387e4f7f51d730..0dcdcff74d9d3d75abf27581205b2e50645700da 100644 (file)
@@ -13,12 +13,17 @@ import (
 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 {
@@ -48,8 +53,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
                        w.WriteHeader(http.StatusOK)
                }
 
-               close(h.response)
-               (*h.server).Close()
+               if h.server != nil {
+                       close(h.response)
+                       (*h.server).Close()
+               }
        }
 }
 
index 686e747177cad5f71265c6309bac910f54877a8a..5acbe43a9048343e8ccd2fb214244021cbf75b47 100644 (file)
@@ -8,13 +8,32 @@ import (
        "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
@@ -22,8 +41,8 @@ func Authenticate(c *Config) (string, <-chan AuthResponse) {
 // 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)
@@ -37,8 +56,7 @@ func AuthenticateWithHandler(c *Config, handler http.Handler) (string, <-chan Au
                }()
 
                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}
@@ -50,15 +68,27 @@ func AuthenticateWithHandler(c *Config, handler http.Handler) (string, <-chan Au
        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)
diff --git a/implicit/implicit.png b/implicit/implicit.png
new file mode 100644 (file)
index 0000000..5f700ab
Binary files /dev/null and b/implicit/implicit.png differ
diff --git a/implicit/implicit.puml b/implicit/implicit.puml
new file mode 100644 (file)
index 0000000..67cda55
--- /dev/null
@@ -0,0 +1,67 @@
+@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
diff --git a/implicit/implicit_001.png b/implicit/implicit_001.png
new file mode 100644 (file)
index 0000000..1b24d50
Binary files /dev/null and b/implicit/implicit_001.png differ
index 0b3916e9a6445c81e5232ce2a027ad0588344bb2..fe41208ce8588aada52af4b24f01175524f1959f 100644 (file)
@@ -8,9 +8,9 @@ import (
 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 {
diff --git a/validate/poll.go b/validate/poll.go
new file mode 100644 (file)
index 0000000..d3e2717
--- /dev/null
@@ -0,0 +1,21 @@
+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
+}
diff --git a/validate/types.go b/validate/types.go
new file mode 100644 (file)
index 0000000..dacf305
--- /dev/null
@@ -0,0 +1,9 @@
+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"`
+}
diff --git a/validate/validate.go b/validate/validate.go
new file mode 100644 (file)
index 0000000..4c18bd3
--- /dev/null
@@ -0,0 +1,48 @@
+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
+}
index 9f90d42cac33df76148de65135ec24fe536cd949..8b5c3338390d208a2efb194da89a21c93a7d2c86 100644 (file)
@@ -4,8 +4,10 @@ import (
        "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
@@ -32,18 +34,15 @@ func promptMany(p string) []string {
 }
 
 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 {
index 8ac21cea953d5260502020b68690b3b5134539db..c4ea639c459e713d6cd2b6e51a6cb97e99fe7d79 100644 (file)
@@ -3,7 +3,7 @@ package main
 import (
        "fmt"
 
-       "go.openfl.eu/twitch-auth/implicit"
+       "go.openfl.eu/twitch-api/auth/implicit"
 )
 
 func main() {