From 44dca4a18852de1cac6bffe4ab62e03433b8ee93 Mon Sep 17 00:00:00 2001 From: William Carroll Date: Sun, 9 Feb 2020 17:50:41 +0000 Subject: 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. --- monzo_ynab/auth.go | 101 ++++++++++++++++++++++++++++++++++ monzo_ynab/main.go | 146 +------------------------------------------------- monzo_ynab/tokens.nix | 14 ++++- 3 files changed, 115 insertions(+), 146 deletions(-) create mode 100644 monzo_ynab/auth.go (limited to 'monzo_ynab') diff --git a/monzo_ynab/auth.go b/monzo_ynab/auth.go new file mode 100644 index 000000000000..b66bacb10687 --- /dev/null +++ b/monzo_ynab/auth.go @@ -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 +} diff --git a/monzo_ynab/main.go b/monzo_ynab/main.go index 10dc9f3effe5..835cfadcb7db 100644 --- a/monzo_ynab/main.go +++ b/monzo_ynab/main.go @@ -10,157 +10,13 @@ package main import ( - "bytes" - "encoding/json" "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 //////////////////////////////////////////////////////////////////////////////// -// 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() { - accessToken := getAccessToken() - // 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) + fmt.Println("To be implemented...") } diff --git a/monzo_ynab/tokens.nix b/monzo_ynab/tokens.nix index 6b932e3975ad..a00191a20782 100644 --- a/monzo_ynab/tokens.nix +++ b/monzo_ynab/tokens.nix @@ -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"; srcs = [ ./tokens.go ]; deps = with briefcase.gopkgs; [ kv + utils + auth ]; } -- cgit 1.4.1