221 lines
4.4 KiB
Go
221 lines
4.4 KiB
Go
package auth
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"github.com/nicklaw5/helix/v2"
|
|
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// TODO; Handle scopes properly
|
|
|
|
type Auth struct {
|
|
HaveAuth bool
|
|
client *helix.Client
|
|
context *context.Context
|
|
}
|
|
|
|
var authTimeout time.Duration = time.Second * 60
|
|
|
|
func New(ctx *context.Context, c *helix.Client, s []string) (*Auth, error) {
|
|
var auth *Auth = &Auth{context: ctx, client: c}
|
|
|
|
// See about getting a token
|
|
c.SetRedirectURI("http://localhost:3000")
|
|
|
|
// See if there are any stored credentials.
|
|
creds, err := getUserToken()
|
|
if err != nil {
|
|
// Caller needs to get followup to get credentials via StartAuth()
|
|
return auth, nil
|
|
}
|
|
|
|
// Validate token in case what we've got on file is still good.
|
|
validate := func() error {
|
|
valid, resp, err := c.ValidateToken(creds.AccessToken)
|
|
if valid {
|
|
return nil
|
|
}
|
|
|
|
if err != nil {
|
|
log.Printf("Validation error: %v\n", err)
|
|
}
|
|
|
|
_ = resp
|
|
return err
|
|
}
|
|
|
|
// Attempt to refresh the token if possible.
|
|
refresh := func() (*helix.AccessCredentials, error) {
|
|
resp, err := c.RefreshUserAccessToken(creds.RefreshToken)
|
|
if err != nil {
|
|
log.Printf("Error refreshing token: %v\n", err)
|
|
return nil, err
|
|
}
|
|
|
|
return &resp.Data, nil
|
|
}
|
|
|
|
// FIXME: This should return an error.
|
|
if err := validate(); err != nil {
|
|
return auth, err
|
|
}
|
|
|
|
refreshedToken, err := refresh()
|
|
if err != nil {
|
|
return auth, err
|
|
}
|
|
|
|
// XXX: Maybe wrap this in a function.
|
|
c.SetUserAccessToken(refreshedToken.AccessToken)
|
|
c.SetRefreshToken(refreshedToken.RefreshToken)
|
|
storeUserToken(refreshedToken)
|
|
auth.HaveAuth = true
|
|
|
|
return auth, nil
|
|
}
|
|
|
|
// Returns path to a token file. Creates it if it does nt exist.
|
|
func tokenFilePath() (string, error) {
|
|
storePath, err := os.UserCacheDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
storePath = filepath.Join(storePath, "tremble")
|
|
err = os.Mkdir(storePath, 0755)
|
|
if err != nil && !os.IsExist(err) {
|
|
log.Printf("Error creating %s: %v\n", storePath, err)
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Join(storePath, "token.json"), nil
|
|
}
|
|
|
|
// Only reporting here for now since it's probably not worth trying to recover
|
|
// from an error here.
|
|
func storeUserToken(token *helix.AccessCredentials) {
|
|
tf, err := tokenFilePath()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
b, err := json.Marshal(token)
|
|
if err != nil {
|
|
log.Printf("Couldn't marshal %+v: %v\n", token, err)
|
|
return
|
|
}
|
|
|
|
err = os.WriteFile(tf, b, 0644)
|
|
if err != nil {
|
|
log.Printf("Error writing token: %v\n", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func getUserToken() (creds helix.AccessCredentials, err error) {
|
|
tf, err := tokenFilePath()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
b, err := os.ReadFile(tf)
|
|
if err != nil {
|
|
|
|
if err != os.ErrNotExist {
|
|
log.Printf("Couldn't read token file: %v\n", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err = json.Unmarshal(b, &creds); err != nil {
|
|
log.Printf("Error unmarshalling token: %v\n", err)
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (a *Auth) updateCode(code string) error {
|
|
resp, err := a.client.RequestUserAccessToken(code)
|
|
if err != nil {
|
|
log.Printf("error requesting user access token: %v\n", err)
|
|
return err
|
|
}
|
|
|
|
log.Printf("%+v\n", resp.Data)
|
|
|
|
a.client.SetUserAccessToken(resp.Data.AccessToken)
|
|
storeUserToken(&resp.Data)
|
|
return nil
|
|
}
|
|
|
|
func (a *Auth) StartAuth(userId string, s []string) (chan int, error) {
|
|
// Need to get authorization
|
|
authUrl := a.client.GetAuthorizationURL(&helix.AuthorizationURLParams{
|
|
ResponseType: "code",
|
|
Scopes: s,
|
|
State: userId,
|
|
ForceVerify: false,
|
|
})
|
|
|
|
// TODO: Check for MacOS here.
|
|
// Open browser and do the auth. Let the auth listener take care of the
|
|
// rest.
|
|
cmd := exec.Command("xdg-open", authUrl)
|
|
if err := cmd.Run(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
notify := make(chan int)
|
|
|
|
a.waitForAuth(notify)
|
|
|
|
return notify, nil
|
|
}
|
|
|
|
func (a *Auth) waitForAuth(notify chan int) {
|
|
authCtx, cancel := context.WithTimeout(*a.context, authTimeout)
|
|
srv := &http.Server{Addr: ":3000"}
|
|
|
|
var code string = ""
|
|
|
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
select {
|
|
case <-authCtx.Done():
|
|
// Authenticated or timed out
|
|
return
|
|
default:
|
|
// Just continue
|
|
}
|
|
code = r.URL.Query().Get("code")
|
|
|
|
if code != "" {
|
|
cancel()
|
|
|
|
err := srv.Shutdown(authCtx)
|
|
if err != nil {
|
|
log.Fatalf("Error shutting down auth server: %v\n", err)
|
|
}
|
|
|
|
a.updateCode(code)
|
|
notify <- 1
|
|
}
|
|
})
|
|
|
|
go func() {
|
|
defer srv.Close()
|
|
|
|
err := http.ListenAndServe(":3000", nil)
|
|
if err != nil {
|
|
log.Fatalf("Error running auth server: %v\n", err)
|
|
}
|
|
}()
|
|
}
|