~dricottone/nspotify

nspotify/auth.go -rw-r--r-- 3.0 KiB
1064d33fDominic Ricottone Noted one more TODO 5 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package main

// Prompt end users to authenticate with Spotify. Needed at startup.

import (
	"context"
	"net/http"
	"fmt"

	"github.com/zmb3/spotify/v2"
	auth "github.com/zmb3/spotify/v2/auth"
	log "github.com/sirupsen/logrus"
)

// These should be set by the linker.
// e.g. `go build -ldflags="-X 'main.CLIENTID=foobarbaz'"`
var (
	CLIENTID = ""
	CLIENTSECRET = ""
)

// Pull the cache directory information from the context.
func cache_info(ctx context.Context) string {
	dir, ok := ctx.Value("cachedir").(string)
	if !ok {
		dir = ""
	}

	return dir
}

// Pull the authentication URI information from the context.
func uri_info(ctx context.Context) (string, string) {
	port, ok := ctx.Value("authport").(int)
	if !ok {
		log.Warn("authenticating web server port is required; falling back to default :8080")
		port = 8080
	}

	full_uri := fmt.Sprintf("http://localhost:%d", port)
	short_uri := fmt.Sprintf(":%d", port)

	return full_uri, short_uri
}

// Serves the authenticator at `http://localhost:[authport]`. Prints that address
// and some instructions to STDOUT for the end user.
func ServeAuthenticator(ctx context.Context, ch chan<- *spotify.Client) *http.Server {
	full_uri, short_uri := uri_info(ctx)
	state := "nspotify"
	authenticator := auth.New(
		auth.WithClientID(CLIENTID),
		auth.WithClientSecret(CLIENTSECRET),
		auth.WithRedirectURL(full_uri),
		auth.WithScopes(auth.ScopeUserLibraryRead, auth.ScopeUserReadPlaybackState, auth.ScopeUserModifyPlaybackState))
	srv := &http.Server{Addr: short_uri}

	// Address and instructions for end user.
	fmt.Println("Log in to Spotify at:", authenticator.AuthURL(state))

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		tok, err := authenticator.Token(r.Context(), state, r)
		if err != nil {
			http.Error(w, "Failed to get token", http.StatusForbidden)
			log.WithError(err).Fatal("failed to get token")
		}

		if s := r.FormValue("state"); s != state {
			http.NotFound(w, r)
			log.Fatalf("invalid state: %s\n", s)
		}

		log.Debugf("login succeeded")

		client := spotify.New(authenticator.Client(ctx, tok))

		dir := cache_info(ctx)
		if dir != "" {
			WriteCache(dir, tok)
		}

		ch <- client
	})

	// Start server.
	go func() {
		if err := srv.ListenAndServe(); err != http.ErrServerClosed {
			log.WithError(err).Fatal("authenticating web server died")
		}
	}()

	return srv
}

// Authenticate with a cached token from `[cachedir]/token.json`.
func CachedAuthentication(ctx context.Context) *spotify.Client {
	dir := cache_info(ctx)
	if dir == "" {
		return nil
	}

	tok, err := ReadCache(dir)
	if err != nil {
		return nil
	}

	authenticator := auth.New(auth.WithScopes(auth.ScopeUserLibraryRead))

	return spotify.New(authenticator.Client(ctx, tok))
	
}

// Authenticates the end user.
func Authenticate(ctx context.Context) *spotify.Client {
	// Try cache first.
	if cached := CachedAuthentication(ctx); cached != nil {
		return cached
	}

	// Start server.
	ch := make(chan *spotify.Client)
	srv := ServeAuthenticator(ctx, ch)

	cli := <-ch

	// Kill server.
	srv.Shutdown(ctx)

	return cli
}