From: Fl_GUI Date: Mon, 28 Jul 2025 20:04:13 +0000 (+0200) Subject: idk bunch of stuff X-Git-Url: https://git.openfl.eu/?a=commitdiff_plain;ds=sidebyside;p=twitch-auth.git idk bunch of stuff --- diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..19c3d4f --- /dev/null +++ b/client/client.go @@ -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 index 0000000..0a0e073 --- /dev/null +++ b/client/doc.go @@ -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 index 0000000..20b9a3e --- /dev/null +++ b/client/types.go @@ -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 index 0000000..75f8ba7 --- /dev/null +++ b/commontypes.go @@ -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 diff --git a/device/authorize.go b/device/authorize.go index a1ab86b..b9da94a 100644 --- a/device/authorize.go +++ b/device/authorize.go @@ -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 diff --git a/device/return.go b/device/returnTypes.go 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 index c8adb90..0000000 --- a/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module go.openfl.eu/twitch-auth - -go 1.23.1 diff --git a/implicit/config.go b/implicit/config.go index 1b414ba..6825cec 100644 --- a/implicit/config.go +++ b/implicit/config.go @@ -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 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 diff --git a/implicit/handler.go b/implicit/handler.go index 5b61eba..0dcdcff 100644 --- a/implicit/handler.go +++ b/implicit/handler.go @@ -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() + } } } diff --git a/implicit/implicit.go b/implicit/implicit.go index 686e747..5acbe43 100644 --- a/implicit/implicit.go +++ b/implicit/implicit.go @@ -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 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 index 0000000..67cda55 --- /dev/null +++ b/implicit/implicit.puml @@ -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 index 0000000..1b24d50 Binary files /dev/null and b/implicit/implicit_001.png differ diff --git a/implicit/implicit_test.go b/implicit/implicit_test.go index 0b3916e..fe41208 100644 --- a/implicit/implicit_test.go +++ b/implicit/implicit_test.go @@ -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 index 0000000..d3e2717 --- /dev/null +++ b/validate/poll.go @@ -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 index 0000000..dacf305 --- /dev/null +++ b/validate/types.go @@ -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 index 0000000..4c18bd3 --- /dev/null +++ b/validate/validate.go @@ -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 +} diff --git a/x/device/main.go b/x/device/main.go index 9f90d42..8b5c333 100644 --- a/x/device/main.go +++ b/x/device/main.go @@ -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 { diff --git a/x/implicit/main.go b/x/implicit/main.go index 8ac21ce..c4ea639 100644 --- a/x/implicit/main.go +++ b/x/implicit/main.go @@ -3,7 +3,7 @@ package main import ( "fmt" - "go.openfl.eu/twitch-auth/implicit" + "go.openfl.eu/twitch-api/auth/implicit" ) func main() {