]> git.openfl.eu Git - twitch-auth.git/commitdiff
implement implicit auth flow main
authorFl_GUI <flor.guilini@hotmail.com>
Sun, 15 Dec 2024 12:11:26 +0000 (13:11 +0100)
committerFl_GUI <flor.guilini@hotmail.com>
Sun, 15 Dec 2024 12:11:26 +0000 (13:11 +0100)
implicit/closehandler.go
implicit/config.go
implicit/end.html [new file with mode: 0644]
implicit/forward.html [new file with mode: 0644]
implicit/handler.go
implicit/implicit.go
implicit/implicit_test.go
x/implicit/main.go

index 492d249984922a7231e7119f687634d7d99a140d..c88e011145e26cb48d2a03596cd2c4a0e75d2e0e 100644 (file)
@@ -1,10 +1,16 @@
 package implicit
 
-import "net/http"
+import (
+       _ "embed"
+       "net/http"
+)
+
+//go:embed end.html
+var end []byte
 
 type CloseHandler struct {
 }
 
 func (*CloseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-       w.Write([]byte("<html><head></head><body>hello</body></html>"))
+       w.Write(end)
 }
index 7a1986b85df39156baceb6658443c46e9aab050d..1b414ba484dd67c59723e841c2ade0c35a91d5df 100644 (file)
@@ -1,8 +1,12 @@
 package implicit
 
 type Config struct {
-       ClientId    string
-       Port        string
-       Scopes      []string
+       // 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
+       // Scopes from https://dev.twitch.tv/docs/authentication/scopes/
+       Scopes []string
+       // Force the user to re-authorize your app
        ForceVerify bool
 }
diff --git a/implicit/end.html b/implicit/end.html
new file mode 100644 (file)
index 0000000..c48d116
--- /dev/null
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+  <head>
+<style>
+* {
+  font-family: mono;
+}
+</style>
+  </head>
+  <body>
+    <p>
+    Successfully authenticated! You may close this page now.
+    </p>
+  </body>
+</html>
diff --git a/implicit/forward.html b/implicit/forward.html
new file mode 100644 (file)
index 0000000..521e400
--- /dev/null
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title> Twitch implicit auth flow </title>
+<script>
+  function submit() {
+    document.forwardform.hash.value = document.location.hash.slice(1)
+    document.location.hash = ""
+    document.forwardform.submit()
+  }
+</script>
+</head>
+
+<body onload="submit()">
+  <form name="forwardform" method="POST">
+    <input type="hidden" name="hash" value=""/>
+  </form>
+</body>
+</html>
index b5b4a8da9bf76683a2282a87623036b58202b1d8..5b61ebab645bbe489c2da5036e387e4f7f51d730 100644 (file)
@@ -1,25 +1,59 @@
 package implicit
 
 import (
+       _ "embed"
+       "errors"
        "fmt"
        "net/http"
        "net/url"
+       "os"
 )
 
+//go:embed forward.html
+var forwardPage []byte
+
 type Handler struct {
        response    chan AuthResponse
        server      **http.Server
        passThrough http.Handler
+       state       string
 }
 
 func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-       h.response <- extractFromFragment(r.URL.Fragment)
-       h.passThrough.ServeHTTP(w, r)
-       close(h.response)
-       (*h.server).Close()
+       // Redirect from twitch to forward.html
+       if r.Method == http.MethodGet {
+               w.Write(forwardPage)
+               return
+       }
+       // Receive post from forward.html
+       if r.Method == http.MethodPost {
+               err := r.ParseForm()
+               if err != nil { // try again
+                       fmt.Fprintln(os.Stderr, err)
+                       w.Write(forwardPage)
+                       return
+               }
+               fragment, ok := r.PostForm["hash"]
+               if !ok || len(fragment) == 0 { // try again
+                       fmt.Fprintln(os.Stderr, err)
+                       w.Write(forwardPage)
+                       return
+               }
+
+               h.response <- h.extractFromFragment(fragment[0])
+               h.passThrough.ServeHTTP(w, r)
+               if f, ok := w.(http.Flusher); ok {
+                       f.Flush()
+               } else {
+                       w.WriteHeader(http.StatusOK)
+               }
+
+               close(h.response)
+               (*h.server).Close()
+       }
 }
 
-func extractFromFragment(fragment string) AuthResponse {
+func (h *Handler) extractFromFragment(fragment string) AuthResponse {
        q, err := url.ParseQuery(fragment)
        if err != nil {
                return AuthResponse{"", err}
@@ -30,5 +64,12 @@ func extractFromFragment(fragment string) AuthResponse {
        }
 
        accessToken := q.Get("access_token")
+       if accessToken == "" {
+               return AuthResponse{accessToken, errors.New("Did not receive an access stoken")}
+       }
+       state := q.Get("state")
+       if state != h.state {
+               return AuthResponse{accessToken, errors.New("Invalid state, forgery expected.")}
+       }
        return AuthResponse{accessToken, nil}
 }
index 84bbd69af7bd094d95a0c7359294a3b227fe7de7..686e747177cad5f71265c6309bac910f54877a8a 100644 (file)
@@ -1,13 +1,16 @@
 package implicit
 
 import (
+       "crypto/rand"
        "errors"
        "fmt"
+       "math/big"
        "net/http"
        "net/url"
        "strings"
 )
 
+// See AuthenticateWithHandler
 func Authenticate(c *Config) (string, <-chan AuthResponse) {
        return AuthenticateWithHandler(c, &CloseHandler{})
 }
@@ -18,8 +21,13 @@ func Authenticate(c *Config) (string, <-chan AuthResponse) {
 // 2) The handler serves a request
 // 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
+// flow will be closed.
 func AuthenticateWithHandler(c *Config, handler http.Handler) (string, <-chan AuthResponse) {
        responses := make(chan AuthResponse, 1)
+       var state string = genState()
 
        go func() {
                defer func() {
@@ -29,7 +37,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}}
+               s = &http.Server{Addr: c.Port, Handler: &Handler{responses, &s, handler, state}}
                fmt.Println("listening on:", s.Addr)
                err := s.ListenAndServe()
                if !errors.Is(err, http.ErrServerClosed) {
@@ -37,12 +45,12 @@ func AuthenticateWithHandler(c *Config, handler http.Handler) (string, <-chan Au
                }
        }()
 
-       authUrl := makeUrl(c)
+       authUrl := makeUrl(c, state)
 
        return authUrl, responses
 }
 
-func makeUrl(c *Config) string {
+func makeUrl(c *Config, state string) string {
        redirectUrl := fmt.Sprintf("http://localhost%s", c.Port)
 
        query := make(url.Values)
@@ -53,8 +61,20 @@ func makeUrl(c *Config) string {
        query.Add("redirect_uri", redirectUrl)
        query.Add("response_type", "token")
        query.Add("scope", strings.Join(c.Scopes, " "))
-       query.Add("state", "")
+       query.Add("state", state)
 
        return fmt.Sprintf("https://id.twitch.tv/oauth2/authorize?%s", query.Encode())
+}
 
+// Generate a cryptographically secure random sequence of characters
+func genState() string {
+       var b strings.Builder
+       for range 31 {
+               bigint, err := rand.Int(rand.Reader, big.NewInt(62))
+               if err != nil {
+                       return err.Error()
+               }
+               b.WriteByte(bigint.Text(62)[0])
+       }
+       return b.String()
 }
index 85988757eb901a82bf9236eae269fed5903a8494..0b3916e9a6445c81e5232ce2a027ad0588344bb2 100644 (file)
@@ -9,8 +9,8 @@ func TestMakeUrl(t *testing.T) {
        out := makeUrl(&Config{
                "hof5gwx0su6owfnys0yan9c87zr6t",
                ":1337",
-               []string{"channel:manage:polls", "channel:read:polls"}, true})
-       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"
+               []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"
 
        parsedUrl, err := url.Parse(out)
        if err != nil {
index 8c4b5ee0c09ef4be3b71593962dd3be4c5ff1713..8ac21cea953d5260502020b68690b3b5134539db 100644 (file)
@@ -11,12 +11,11 @@ func main() {
                "po7tssvlu80ow6c9ewdibq4jdgyzzb",
                ":1337",
                []string{},
-               false,
+               true,
        }
        url, response := implicit.Authenticate(c)
        fmt.Println(url)
        for r := range response {
                fmt.Printf("%#v\n", r)
        }
-       <-make(chan struct{})
 }