Consume auth library
Consume the newly relocated auth package. Additionally: - Debugged error where JSON was properly decoding but not populating the refreshTokenResponse struct, so my application was signaling false positive messages about token refresh events. - Logging more often and more data to help my troubleshooting - Refreshing tokens as soon as the app starts just to be safe - Clean up the code in general
This commit is contained in:
parent
44dca4a188
commit
c83594fb5a
1 changed files with 101 additions and 57 deletions
|
@ -8,17 +8,19 @@ package main
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"auth"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"kv"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
"kv"
|
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -79,17 +81,30 @@ var (
|
||||||
// Utils
|
// Utils
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Print the access and refresh tokens for debugging.
|
||||||
|
func logTokens(access string, refresh string) {
|
||||||
|
log.Printf("Access: %s\n", access)
|
||||||
|
log.Printf("Refresh: %s\n", refresh)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) String() string {
|
||||||
|
return fmt.Sprintf("state{\n\taccessToken: \"%s\",\n\trefreshToken: \"%s\"\n}\n", state.accessToken, state.refreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
// Schedule a token refresh for `expiresIn` seconds using the provided
|
// Schedule a token refresh for `expiresIn` seconds using the provided
|
||||||
// `refreshToken`. This will update the application state with the access token
|
// `refreshToken`. This will update the application state with the access token
|
||||||
// and schedule an additional token refresh for the newly acquired tokens.
|
// and schedule an additional token refresh for the newly acquired tokens.
|
||||||
func scheduleTokenRefresh(expiresIn int, refreshToken string) {
|
func scheduleTokenRefresh(expiresIn int, refreshToken string) {
|
||||||
duration := time.Second * time.Duration(expiresIn)
|
duration := time.Second * time.Duration(expiresIn)
|
||||||
timestamp := time.Now().Local().Add(duration)
|
timestamp := time.Now().Local().Add(duration)
|
||||||
|
// TODO(wpcarro): Consider adding a more human readable version that will
|
||||||
|
// log the number of hours, minutes, etc. until the next refresh.
|
||||||
log.Printf("Scheduling token refresh for %v\n", timestamp)
|
log.Printf("Scheduling token refresh for %v\n", timestamp)
|
||||||
time.Sleep(duration)
|
time.Sleep(duration)
|
||||||
log.Println("Refreshing tokens now...")
|
log.Println("Refreshing tokens now...")
|
||||||
access, refresh := refreshTokens(refreshToken)
|
access, refresh := refreshTokens(refreshToken)
|
||||||
log.Println("Successfully refreshed tokens.")
|
log.Println("Successfully refreshed tokens.")
|
||||||
|
logTokens(access, refresh)
|
||||||
chans.writes <- writeMsg{state{access, refresh}}
|
chans.writes <- writeMsg{state{access, refresh}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,22 +119,42 @@ func refreshTokens(refreshToken string) (string, string) {
|
||||||
"client_secret": {monzoClientSecret},
|
"client_secret": {monzoClientSecret},
|
||||||
"refresh_token": {refreshToken},
|
"refresh_token": {refreshToken},
|
||||||
})
|
})
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
// TODO(wpcarro): Considering panicking here.
|
||||||
|
utils.DebugResponse(res)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(res)
|
utils.DebugResponse(res)
|
||||||
log.Fatal("The request to Monzo to refresh our access token failed.", err)
|
log.Fatal("The request to Monzo to refresh our access token failed.", err)
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
payload := &refreshTokenResponse{}
|
payload := &refreshTokenResponse{}
|
||||||
err = json.NewDecoder(res.Body).Decode(payload)
|
err = json.NewDecoder(res.Body).Decode(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(res)
|
|
||||||
log.Fatal("Could not decode the JSON response from Monzo.", err)
|
log.Fatal("Could not decode the JSON response from Monzo.", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
go scheduleTokenRefresh(payload.ExpiresIn, payload.RefreshToken)
|
go scheduleTokenRefresh(payload.ExpiresIn, payload.RefreshToken)
|
||||||
|
|
||||||
|
// Interestingly, JSON decoding into the refreshTokenResponse can success
|
||||||
|
// even if the decoder doesn't populate any of the fields in the
|
||||||
|
// refreshTokenResponse struct. From what I read, it isn't possible to make
|
||||||
|
// these fields as required using an annotation, so this guard must suffice
|
||||||
|
// for now.
|
||||||
|
if payload.AccessToken == "" || payload.RefreshToken == "" {
|
||||||
|
log.Fatal("JSON parsed correctly but failed to populate token fields.")
|
||||||
|
}
|
||||||
|
|
||||||
return payload.AccessToken, payload.RefreshToken
|
return payload.AccessToken, payload.RefreshToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func persistTokens(access string, refresh string) {
|
||||||
|
log.Println("Persisting tokens...")
|
||||||
|
kv.Set("monzoAccessToken", access)
|
||||||
|
kv.Set("monzoRefreshToken", refresh)
|
||||||
|
log.Println("Successfully persisted tokens.")
|
||||||
|
}
|
||||||
|
|
||||||
// Listen for SIGINT and SIGTERM signals. When received, persist the access and
|
// Listen for SIGINT and SIGTERM signals. When received, persist the access and
|
||||||
// refresh tokens and shutdown the server.
|
// refresh tokens and shutdown the server.
|
||||||
func handleInterrupts() {
|
func handleInterrupts() {
|
||||||
|
@ -132,14 +167,8 @@ func handleInterrupts() {
|
||||||
go func() {
|
go func() {
|
||||||
sig := <-sigs
|
sig := <-sigs
|
||||||
log.Printf("Received signal to shutdown. %v\n", sig)
|
log.Printf("Received signal to shutdown. %v\n", sig)
|
||||||
// Persist existing tokens
|
state := getState()
|
||||||
log.Println("Persisting existing credentials...")
|
persistTokens(state.accessToken, state.refreshToken)
|
||||||
msg := readMsg{make(chan state)}
|
|
||||||
chans.reads <- msg
|
|
||||||
state := <-msg.sender
|
|
||||||
kv.Set("monzoAccessToken", state.accessToken)
|
|
||||||
kv.Set("monzoRefreshToken", state.refreshToken)
|
|
||||||
log.Println("Credentials persisted.")
|
|
||||||
done <- true
|
done <- true
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -148,6 +177,13 @@ func handleInterrupts() {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return our application state.
|
||||||
|
func getState() state {
|
||||||
|
msg := readMsg{make(chan state)}
|
||||||
|
chans.reads <- msg
|
||||||
|
return <-msg.sender
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// Main
|
// Main
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -158,11 +194,21 @@ func main() {
|
||||||
refreshToken := fmt.Sprintf("%v", kv.Get("monzoRefreshToken"))
|
refreshToken := fmt.Sprintf("%v", kv.Get("monzoRefreshToken"))
|
||||||
|
|
||||||
log.Println("Attempting to retrieve cached credentials...")
|
log.Println("Attempting to retrieve cached credentials...")
|
||||||
log.Printf("Access token: %s\n", accessToken)
|
logTokens(accessToken, refreshToken)
|
||||||
log.Printf("Refresh token: %s\n", refreshToken)
|
|
||||||
|
|
||||||
if accessToken == "" || refreshToken == "" {
|
if accessToken == "" || refreshToken == "" {
|
||||||
log.Fatal("Cannot start server without access or refresh tokens.")
|
log.Println("Cached credentials are absent. Authorizing client...")
|
||||||
|
authCode := auth.GetAuthCode(monzoClientId)
|
||||||
|
tokens := auth.GetTokensFromAuthCode(authCode, monzoClientId, monzoClientSecret)
|
||||||
|
accessToken, refreshToken = tokens.AccessToken, tokens.RefreshToken
|
||||||
|
go persistTokens(accessToken, refreshToken)
|
||||||
|
go scheduleTokenRefresh(tokens.ExpiresIn, refreshToken)
|
||||||
|
} else {
|
||||||
|
// If we have tokens, they may be expiring soon. We don't know because
|
||||||
|
// we aren't storing the expiration timestamp in the state or in the
|
||||||
|
// store. Until we have that information, and to be safe, let's refresh
|
||||||
|
// the tokens.
|
||||||
|
scheduleTokenRefresh(0, refreshToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gracefully shutdown.
|
// Gracefully shutdown.
|
||||||
|
@ -174,24 +220,24 @@ func main() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case msg := <-chans.reads:
|
case msg := <-chans.reads:
|
||||||
log.Printf("Reading from state.")
|
log.Println("Reading from state...")
|
||||||
log.Printf("Access Token: %s\n", state.accessToken)
|
log.Println(state)
|
||||||
log.Printf("Refresh Token: %s\n", state.refreshToken)
|
|
||||||
msg.sender <- *state
|
msg.sender <- *state
|
||||||
case msg := <-chans.writes:
|
case msg := <-chans.writes:
|
||||||
fmt.Printf("Writing new state: %v\n", msg.state)
|
log.Println("Writing to state.")
|
||||||
|
log.Printf("Old: %s\n", state)
|
||||||
*state = msg.state
|
*state = msg.state
|
||||||
|
log.Printf("New: %s\n", state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Listen to inbound requests.
|
// Listen to inbound requests.
|
||||||
fmt.Println("Listening on http://localhost:4242 ...")
|
fmt.Println("Listening on http://localhost:4242 ...")
|
||||||
log.Fatal(http.ListenAndServe(":4242", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
log.Fatal(http.ListenAndServe(":4242",
|
||||||
|
http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
if req.URL.Path == "/refresh-tokens" && req.Method == "POST" {
|
if req.URL.Path == "/refresh-tokens" && req.Method == "POST" {
|
||||||
msg := readMsg{make(chan state)}
|
state := getState()
|
||||||
chans.reads <- msg
|
|
||||||
state := <-msg.sender
|
|
||||||
go scheduleTokenRefresh(0, state.refreshToken)
|
go scheduleTokenRefresh(0, state.refreshToken)
|
||||||
fmt.Fprintf(w, "Done.")
|
fmt.Fprintf(w, "Done.")
|
||||||
|
|
||||||
|
@ -215,11 +261,9 @@ func main() {
|
||||||
} else if req.URL.Path == "/state" && req.Method == "GET" {
|
} else if req.URL.Path == "/state" && req.Method == "GET" {
|
||||||
// TODO(wpcarro): Ensure that this returns serialized state.
|
// TODO(wpcarro): Ensure that this returns serialized state.
|
||||||
w.Header().Set("Content-type", "application/json")
|
w.Header().Set("Content-type", "application/json")
|
||||||
msg := readMsg{make(chan state)}
|
state := getState()
|
||||||
chans.reads <- msg
|
|
||||||
state := <-msg.sender
|
|
||||||
payload, _ := json.Marshal(state)
|
payload, _ := json.Marshal(state)
|
||||||
fmt.Fprintf(w, "Application state: %s\n", bytes.NewBuffer(payload))
|
io.WriteString(w, string(payload))
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Unhandled request: %v\n", *req)
|
log.Printf("Unhandled request: %v\n", *req)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue