From: Fl_GUI Date: Sun, 15 Dec 2024 12:11:26 +0000 (+0100) Subject: implement implicit auth flow X-Git-Url: https://git.openfl.eu/?a=commitdiff_plain;ds=inline;p=twitch-auth.git implement implicit auth flow --- diff --git a/implicit/closehandler.go b/implicit/closehandler.go index 492d249..c88e011 100644 --- a/implicit/closehandler.go +++ b/implicit/closehandler.go @@ -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("hello")) + w.Write(end) } diff --git a/implicit/config.go b/implicit/config.go index 7a1986b..1b414ba 100644 --- a/implicit/config.go +++ b/implicit/config.go @@ -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 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 index 0000000..c48d116 --- /dev/null +++ b/implicit/end.html @@ -0,0 +1,15 @@ + + + + + + +

+ Successfully authenticated! You may close this page now. +

+ + diff --git a/implicit/forward.html b/implicit/forward.html new file mode 100644 index 0000000..521e400 --- /dev/null +++ b/implicit/forward.html @@ -0,0 +1,19 @@ + + + + Twitch implicit auth flow + + + + +
+ +
+ + diff --git a/implicit/handler.go b/implicit/handler.go index b5b4a8d..5b61eba 100644 --- a/implicit/handler.go +++ b/implicit/handler.go @@ -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} } diff --git a/implicit/implicit.go b/implicit/implicit.go index 84bbd69..686e747 100644 --- a/implicit/implicit.go +++ b/implicit/implicit.go @@ -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() } diff --git a/implicit/implicit_test.go b/implicit/implicit_test.go index 8598875..0b3916e 100644 --- a/implicit/implicit_test.go +++ b/implicit/implicit_test.go @@ -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 { diff --git a/x/implicit/main.go b/x/implicit/main.go index 8c4b5ee..8ac21ce 100644 --- a/x/implicit/main.go +++ b/x/implicit/main.go @@ -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{}) }