From 05f700bc93d6332310bd4b4b625b8a9e8e61b348 Mon Sep 17 00:00:00 2001 From: Ian Fijolek Date: Wed, 6 Jan 2021 13:44:29 -0500 Subject: [PATCH] Working oauth flow --- .gitignore | 1 + Makefile | 32 ++++++++++++++++ auth.go | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 73 ++++++++++++++++++++---------------- 4 files changed, 180 insertions(+), 32 deletions(-) create mode 100644 Makefile create mode 100644 auth.go diff --git a/.gitignore b/.gitignore index baa666d..e349301 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ slack-status slack-status-cli +dist/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..207fcaa --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +OUTPUT = slack-status +GOFILES = *.go go.mod go.sum +DIST_ARCH = darwin-amd64 linux-amd64 +DIST_TARGETS = $(addprefix dist/$(OUTPUT)-,$(DIST_ARCH)) + +.PHONY: default +default: slack-status + +.PHONY: all +all: dist + +.PHONY: test +test: + go test + +slack-status: $(GOFILES) + go build -o $(OUTPUT) + +.PHONY: dist +dist: $(DIST_TARGETS) + +$(DIST_TARGETS): $(GOFILES) + @mkdir -p ./dist + GOOS=$(word 3, $(subst -, ,$(@))) GOARCH=$(word 4, $(subst -, ,$(@))) \ + go build \ + -ldflags '-X "main.defaultClientID=$(CLIENT_ID)" -X "main.defaultClientSecret=$(CLIENT_SECRET)"' \ + -o $@ + +.PHONY: clean +clean: + rm ./slack-status + rm -fr ./dist diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..dd83d9b --- /dev/null +++ b/auth.go @@ -0,0 +1,106 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "os" + "strings" + + "github.com/slack-go/slack" +) + +var ( + // These are set via build flags but can be overriden via environment variables. + defaultClientID = "" + defaultClientSecret = "" +) + +func getEnvOrDefault(name, defaultValue string) string { + val, ok := os.LookupEnv(name) + if ok { + return val + } + + return defaultValue +} + +type slackApp struct { + clientID, clientSecret, redirectURI string + scopes, userScopes []string + listenHost, listenPath string +} + +func (app slackApp) getAuthURL() string { + scopes := strings.Join(app.scopes, ",") + userScopes := strings.Join(app.userScopes, ",") + + return fmt.Sprintf( + "https://slack.com/oauth/authorize?scope=%s&user_scope=%s&client_id=%s&redirect_uri=%s", + scopes, + userScopes, + app.clientID, + app.redirectURI, + ) +} + +func (app slackApp) listenForCode() (string, error) { + // start an http listener and listen for the redirect and return the code from params + var code string + + // Also, should generate TLS certificate to use since https is a required scheme + server := http.Server{Addr: app.listenHost} + + http.HandleFunc(app.listenPath, func(w http.ResponseWriter, r *http.Request) { + codes := r.URL.Query()["code"] + if len(codes) == 0 { + log.Fatal("no oauth code found in response") + } + + code = codes[0] + fmt.Fprintf(w, "Got code %s", code) + + // Shutdown after response + go func() { + if err := server.Shutdown(context.Background()); err != nil { + fmt.Println("Fatal?") + log.Fatal(err) + } + }() + }) + + if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + return "", err + } + + return code, nil +} + +func authenticate() (string, error) { + app := slackApp{ + userScopes: []string{"dnd:write", "users.profile:write"}, + scopes: []string{"dnd:write", "users.profile:write"}, + clientID: getEnvOrDefault("CLIENT_ID", defaultClientID), + clientSecret: getEnvOrDefault("CLIENT_SECRET", defaultClientSecret), + redirectURI: "http://localhost:8888/auth", + listenHost: "localhost:8888", + listenPath: "/auth", + } + + fmt.Println("To authenticate, go to the following URL:") + fmt.Println(app.getAuthURL()) + + code, err := app.listenForCode() + if err != nil { + return "", err + } + + accessToken, _, err := slack.GetOAuthToken(&http.Client{}, app.clientID, app.clientSecret, code, app.redirectURI) + if err != nil { + return "", err + } + + return accessToken, nil +} diff --git a/main.go b/main.go index a9ccd7b..a21511d 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "flag" "fmt" "io/ioutil" + "log" "os" "path/filepath" "strings" @@ -43,46 +44,29 @@ func getConfigFilePath(filename string) string { return filepath.Join(configDir, filename) } -// readWriteAccessToken will store and retrieve access tokens for future use. -func readWriteAccessToken(accessToken string) (string, error) { +// writeAccessToken writes the access token to a file for future use. +func writeAccessToken(accessToken string) error { tokenFile := getConfigFilePath("token") - if accessToken != "" { - err := ioutil.WriteFile(tokenFile, []byte(accessToken), 0600) - if err != nil { - fmt.Println("Error writing access token") - } - - return accessToken, err + if err := ioutil.WriteFile(tokenFile, []byte(accessToken), 0600); err != nil { + return fmt.Errorf("error writing access token %w", err) } + return nil +} + +// readAccessToken retreive access token from a file +func readAccessToken() (string, error) { + tokenFile := getConfigFilePath("token") + content, err := ioutil.ReadFile(tokenFile) if err != nil { - fmt.Println("No token provided on command line or in file") - - return "", err + return "", fmt.Errorf("error reading access token from file %w", err) } return string(content), nil } -// createClient will return a Slack client with the provided access token. -// If that token is empty, it will try to get one from the config folder. -func createClient(accessToken string) (*slack.Client, error) { - var err error - - accessToken, err = readWriteAccessToken(accessToken) - if err != nil { - fmt.Println("error reading access token") - - return nil, err - } - - client := slack.New(accessToken) - - return client, nil -} - // readDurationArgs will attempt to find a duration within command line args rather than flags. // It will look for a prefixed duration. eg. "5m :cowboy: Howdy y'all" and a postfix duration // following the word "for". eg. ":dancing: Dancing for 1h". @@ -158,15 +142,40 @@ func readFlags() statusInfo { } } +func getAccessToken(accessToken string) (string, error) { + // If provided, save and return + if accessToken != "" { + return accessToken, writeAccessToken(accessToken) + } + + // Try to get from stored file + accessToken, err := readAccessToken() + if accessToken != "" && err == nil { + // Successfully read from file + return accessToken, nil + } + + // Begin auth process to fetch a new token + accessToken, err = authenticate() + + if err == nil { + // Successful authentication, save the token + err = writeAccessToken(accessToken) + } + + return accessToken, err +} + func main() { args := readFlags() - client, err := createClient(args.accessToken) + accessToken, err := getAccessToken(args.accessToken) if err != nil { - fmt.Println("error getting client") - panic(err) + fmt.Println("error getting access token") + log.Fatal(err) } + client := slack.New(accessToken) err = client.SetUserCustomStatus(args.statusText, args.emoji, args.getExpirationTime()) if err != nil { fmt.Println("error setting status")