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)
}
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
}
--- /dev/null
+<!DOCTYPE html>
+<html>
+ <head>
+<style>
+* {
+ font-family: mono;
+}
+</style>
+ </head>
+ <body>
+ <p>
+ Successfully authenticated! You may close this page now.
+ </p>
+ </body>
+</html>
--- /dev/null
+<!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>
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}
}
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}
}
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{})
}
// 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() {
}()
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) {
}
}()
- 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)
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()
}
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 {
"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{})
}