Move authorization logic into separate package
Relocated the logic for authorizing clients into a separate package that the tokens server now depends on. Moving this helped me separate concerns. I removed a few top-level variables and tried to write more pure versions of the authorization functions to avoid leaking Monzo-specific details.
This commit is contained in:
		
							parent
							
								
									4ea5a1bffa
								
							
						
					
					
						commit
						44dca4a188
					
				
					 3 changed files with 115 additions and 146 deletions
				
			
		
							
								
								
									
										101
									
								
								monzo_ynab/auth.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								monzo_ynab/auth.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,101 @@
 | 
				
			||||||
 | 
					package auth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					////////////////////////////////////////////////////////////////////////////////
 | 
				
			||||||
 | 
					// Dependencies
 | 
				
			||||||
 | 
					////////////////////////////////////////////////////////////////////////////////
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"utils"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					////////////////////////////////////////////////////////////////////////////////
 | 
				
			||||||
 | 
					// Constants
 | 
				
			||||||
 | 
					////////////////////////////////////////////////////////////////////////////////
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						BROWSER      = os.Getenv("BROWSER")
 | 
				
			||||||
 | 
						REDIRECT_URI = "http://localhost:8080/authorization-code"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					////////////////////////////////////////////////////////////////////////////////
 | 
				
			||||||
 | 
					// Types
 | 
				
			||||||
 | 
					////////////////////////////////////////////////////////////////////////////////
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This is the response returned from Monzo when we exchange our authorization
 | 
				
			||||||
 | 
					// code for an access token. While Monzo returns additional fields, I'm only
 | 
				
			||||||
 | 
					// interested in AccessToken and RefreshToken.
 | 
				
			||||||
 | 
					type accessTokenResponse struct {
 | 
				
			||||||
 | 
						AccessToken  string `json:"access_token"`
 | 
				
			||||||
 | 
						RefreshToken string `json:"refresh_token"`
 | 
				
			||||||
 | 
						ExpiresIn    int    `json:"expires_in"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Tokens struct {
 | 
				
			||||||
 | 
						AccessToken  string
 | 
				
			||||||
 | 
						RefreshToken string
 | 
				
			||||||
 | 
						ExpiresIn    int
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					////////////////////////////////////////////////////////////////////////////////
 | 
				
			||||||
 | 
					// Functions
 | 
				
			||||||
 | 
					////////////////////////////////////////////////////////////////////////////////
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Returns the access token and refresh tokens for the Monzo API.
 | 
				
			||||||
 | 
					func GetTokensFromAuthCode(authCode string, clientID string, clientSecret string) *Tokens {
 | 
				
			||||||
 | 
						res, err := http.PostForm("https://api.monzo.com/oauth2/token", url.Values{
 | 
				
			||||||
 | 
							"grant_type":    {"authorization_code"},
 | 
				
			||||||
 | 
							"client_id":     {clientID},
 | 
				
			||||||
 | 
							"client_secret": {clientSecret},
 | 
				
			||||||
 | 
							"redirect_uri":  {REDIRECT_URI},
 | 
				
			||||||
 | 
							"code":          {authCode},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						utils.FailOn(err)
 | 
				
			||||||
 | 
						defer res.Body.Close()
 | 
				
			||||||
 | 
						payload := &accessTokenResponse{}
 | 
				
			||||||
 | 
						json.NewDecoder(res.Body).Decode(payload)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &Tokens{payload.AccessToken, payload.RefreshToken, payload.ExpiresIn}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Open a web browser to allow the user to authorize this application. Return
 | 
				
			||||||
 | 
					// the authorization code sent from Monzo.
 | 
				
			||||||
 | 
					func GetAuthCode(clientID string) string {
 | 
				
			||||||
 | 
						// TODO(wpcarro): Consider generating a random string for the state when the
 | 
				
			||||||
 | 
						// application starts instead of hardcoding it here.
 | 
				
			||||||
 | 
						state := "xyz123"
 | 
				
			||||||
 | 
						url := fmt.Sprintf(
 | 
				
			||||||
 | 
							"https://auth.monzo.com/?client_id=%s&redirect_uri=%s&response_type=code&state=%s",
 | 
				
			||||||
 | 
							clientID, REDIRECT_URI, state)
 | 
				
			||||||
 | 
						exec.Command(BROWSER, url).Start()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						authCode := make(chan string)
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							log.Fatal(http.ListenAndServe(":8080",
 | 
				
			||||||
 | 
								http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 | 
				
			||||||
 | 
									// 1. Get authorization code from Monzo.
 | 
				
			||||||
 | 
									if req.URL.Path == "/authorization-code" {
 | 
				
			||||||
 | 
										params := req.URL.Query()
 | 
				
			||||||
 | 
										reqState := params["state"][0]
 | 
				
			||||||
 | 
										code := params["code"][0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										if reqState != state {
 | 
				
			||||||
 | 
											log.Fatalf("Value for state returned by Monzo does not equal our state. %s != %s", reqState, state)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										authCode <- code
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										fmt.Fprintf(w, "Authorized!")
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										log.Printf("Unhandled request: %v\n", *req)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								})))
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						result := <-authCode
 | 
				
			||||||
 | 
						return result
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -10,157 +10,13 @@
 | 
				
			||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
					 | 
				
			||||||
	"encoding/json"
 | 
					 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"net/http"
 | 
					 | 
				
			||||||
	"net/http/httputil"
 | 
					 | 
				
			||||||
	"net/url"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"os/exec"
 | 
					 | 
				
			||||||
	"utils"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
////////////////////////////////////////////////////////////////////////////////
 | 
					 | 
				
			||||||
// Constants
 | 
					 | 
				
			||||||
////////////////////////////////////////////////////////////////////////////////
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var (
 | 
					 | 
				
			||||||
	accountId    = os.Getenv("monzo_account_id")
 | 
					 | 
				
			||||||
	clientId     = os.Getenv("monzo_client_id")
 | 
					 | 
				
			||||||
	clientSecret = os.Getenv("monzo_client_secret")
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	redirectURI = "http://localhost:8080/authorization-code"
 | 
					 | 
				
			||||||
	// TODO(wpcarro): Consider generating a random string for the state when the
 | 
					 | 
				
			||||||
	// application starts instead of hardcoding it here.
 | 
					 | 
				
			||||||
	state = "xyz123"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
////////////////////////////////////////////////////////////////////////////////
 | 
					////////////////////////////////////////////////////////////////////////////////
 | 
				
			||||||
// Business Logic
 | 
					// Business Logic
 | 
				
			||||||
////////////////////////////////////////////////////////////////////////////////
 | 
					////////////////////////////////////////////////////////////////////////////////
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// This is the response returned from Monzo when we exchange our authorization
 | 
					 | 
				
			||||||
// code for an access token. While Monzo returns additional fields, I'm only
 | 
					 | 
				
			||||||
// interested in AccessToken and RefreshToken.
 | 
					 | 
				
			||||||
type accessTokenResponse struct {
 | 
					 | 
				
			||||||
	AccessToken  string `json:"access_token"`
 | 
					 | 
				
			||||||
	RefreshToken string `json:"refresh_token"`
 | 
					 | 
				
			||||||
	ExpiresIn    int    `json:"expires_in"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type setTokensRequest struct {
 | 
					 | 
				
			||||||
	AccessToken  string `json:"access_token"`
 | 
					 | 
				
			||||||
	RefreshToken string `json:"refresh_token"`
 | 
					 | 
				
			||||||
	ExpiresIn    int    `json:"expires_in"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Tokens struct {
 | 
					 | 
				
			||||||
	AccessToken  string `json:"access_token"`
 | 
					 | 
				
			||||||
	RefreshToken string `json:"refresh_token"`
 | 
					 | 
				
			||||||
	ExpiresIn    int    `json:"expires_in"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// TODO(wpcarro): Replace http.PostForm and other similar calls with
 | 
					 | 
				
			||||||
// client.postForm. The default http.Get and other methods doesn't timeout, so
 | 
					 | 
				
			||||||
// it's better to create a configured client with a value for the timeout.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Returns the access token and refresh tokens for the Monzo API.
 | 
					 | 
				
			||||||
func getTokens(code string) *Tokens {
 | 
					 | 
				
			||||||
	res, err := http.PostForm("https://api.monzo.com/oauth2/token", url.Values{
 | 
					 | 
				
			||||||
		"grant_type":    {"authorization_code"},
 | 
					 | 
				
			||||||
		"client_id":     {clientId},
 | 
					 | 
				
			||||||
		"client_secret": {clientSecret},
 | 
					 | 
				
			||||||
		"redirect_uri":  {redirectURI},
 | 
					 | 
				
			||||||
		"code":          {code},
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	utils.FailOn(err)
 | 
					 | 
				
			||||||
	defer res.Body.Close()
 | 
					 | 
				
			||||||
	payload := &accessTokenResponse{}
 | 
					 | 
				
			||||||
	json.NewDecoder(res.Body).Decode(payload)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return &Tokens{payload.AccessToken, payload.RefreshToken, payload.ExpiresIn}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// TODO(wpcarro): Prefer using an environment variable for the web browser
 | 
					 | 
				
			||||||
// instead of assuming it will be google-chrome.
 | 
					 | 
				
			||||||
// Open a web browser to allow the user to authorize this application. Return
 | 
					 | 
				
			||||||
// the authorization code sent from Monzo.
 | 
					 | 
				
			||||||
func getAuthCode() string {
 | 
					 | 
				
			||||||
	url := fmt.Sprintf("https://auth.monzo.com/?client_id=%s&redirect_uri=%s&response_type=code&state=%s", clientId, redirectURI, state)
 | 
					 | 
				
			||||||
	exec.Command("google-chrome", url).Start()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	authCode := make(chan string)
 | 
					 | 
				
			||||||
	go func() {
 | 
					 | 
				
			||||||
		log.Fatal(http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 | 
					 | 
				
			||||||
			// 1. Get authorization code from Monzo.
 | 
					 | 
				
			||||||
			if req.URL.Path == "/authorization-code" {
 | 
					 | 
				
			||||||
				params := req.URL.Query()
 | 
					 | 
				
			||||||
				reqState := params["state"][0]
 | 
					 | 
				
			||||||
				code := params["code"][0]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				if reqState != state {
 | 
					 | 
				
			||||||
					log.Fatalf("Value for state returned by Monzo does not equal our state. %s != %s", reqState, state)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				authCode <- code
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				fmt.Fprintf(w, "Authorized!")
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				log.Printf("Unhandled request: %v\n", *req)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		})))
 | 
					 | 
				
			||||||
	}()
 | 
					 | 
				
			||||||
	result := <-authCode
 | 
					 | 
				
			||||||
	return result
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// TODO(wpcarro): Move this logic out of here and into the tokens server.
 | 
					 | 
				
			||||||
func authorize() {
 | 
					 | 
				
			||||||
	authCode := getAuthCode()
 | 
					 | 
				
			||||||
	tokens := getTokens(authCode)
 | 
					 | 
				
			||||||
	client := &http.Client{}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	payload, _ := json.Marshal(setTokensRequest{
 | 
					 | 
				
			||||||
		tokens.AccessToken,
 | 
					 | 
				
			||||||
		tokens.RefreshToken,
 | 
					 | 
				
			||||||
		tokens.ExpiresIn})
 | 
					 | 
				
			||||||
	log.Printf("Access token: %s\n", tokens.AccessToken)
 | 
					 | 
				
			||||||
	log.Printf("Refresh token: %s\n", tokens.RefreshToken)
 | 
					 | 
				
			||||||
	log.Printf("Expires: %s\n", tokens.ExpiresIn)
 | 
					 | 
				
			||||||
	req, _ := http.NewRequest("POST", "http://localhost:4242/set-tokens", bytes.NewBuffer(payload))
 | 
					 | 
				
			||||||
	req.Header.Set("Content-Type", "application/json")
 | 
					 | 
				
			||||||
	_, err := client.Do(req)
 | 
					 | 
				
			||||||
	utils.FailOn(err)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Retrieves the access token from the tokens server.
 | 
					 | 
				
			||||||
func getAccessToken() string {
 | 
					 | 
				
			||||||
	return simpleGet("http://localhost:4242/token")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	accessToken := getAccessToken()
 | 
						fmt.Println("To be implemented...")
 | 
				
			||||||
	// authHeaders := map[string]string{
 | 
					 | 
				
			||||||
	// 	"Authorization": fmt.Sprintf("Bearer %s", accessToken),
 | 
					 | 
				
			||||||
	// }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	client := &http.Client{}
 | 
					 | 
				
			||||||
	form := url.Values{"account_id": {accountId}}
 | 
					 | 
				
			||||||
	req, _ := http.NewRequest("GET", "https://api.monzo.com/transactions", strings.NewReader(form.Encode()))
 | 
					 | 
				
			||||||
	req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken))
 | 
					 | 
				
			||||||
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
 | 
					 | 
				
			||||||
	bytes, _ := httputil.DumpRequest(req, true)
 | 
					 | 
				
			||||||
	fmt.Println(string(bytes))
 | 
					 | 
				
			||||||
	res, _ := client.Do(req)
 | 
					 | 
				
			||||||
	bytes, _ = httputil.DumpResponse(res, true)
 | 
					 | 
				
			||||||
	fmt.Println(string(bytes))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// res := simpleGet("https://api.monzo.com/accounts", authHeaders, true)
 | 
					 | 
				
			||||||
	// fmt.Println(res)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	os.Exit(0)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,12 +4,24 @@
 | 
				
			||||||
  ...
 | 
					  ...
 | 
				
			||||||
}:
 | 
					}:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
depot.buildGo.program {
 | 
					let
 | 
				
			||||||
 | 
					  auth = depot.buildGo.package {
 | 
				
			||||||
 | 
					    name = "auth";
 | 
				
			||||||
 | 
					    srcs = [
 | 
				
			||||||
 | 
					      ./auth.go
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					    deps = with briefcase.gopkgs; [
 | 
				
			||||||
 | 
					      utils
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					in depot.buildGo.program {
 | 
				
			||||||
  name = "token-server";
 | 
					  name = "token-server";
 | 
				
			||||||
  srcs = [
 | 
					  srcs = [
 | 
				
			||||||
    ./tokens.go
 | 
					    ./tokens.go
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
  deps = with briefcase.gopkgs; [
 | 
					  deps = with briefcase.gopkgs; [
 | 
				
			||||||
    kv
 | 
					    kv
 | 
				
			||||||
 | 
					    utils
 | 
				
			||||||
 | 
					    auth
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue