Skip to content

Commit b1cda94

Browse files
committed
Better auth
1 parent d7922f4 commit b1cda94

File tree

6 files changed

+150
-46
lines changed

6 files changed

+150
-46
lines changed

auth_get_token.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package yaakcli
2+
3+
import (
4+
"crypto/subtle"
5+
"fmt"
6+
"golang.org/x/oauth2"
7+
"net/http"
8+
)
9+
10+
type OAuthRedirectHandler struct {
11+
State string
12+
CodeVerifier string
13+
OAuthConfig *oauth2.Config
14+
}
15+
16+
func (h *OAuthRedirectHandler) ExchangeCode(r *http.Request) (string, error) {
17+
state := r.URL.Query().Get("state")
18+
code := r.URL.Query().Get("code")
19+
20+
if subtle.ConstantTimeCompare([]byte(h.State), []byte(state)) == 0 {
21+
return "", fmt.Errorf("invalid state")
22+
}
23+
24+
if code == "" {
25+
return "", fmt.Errorf("missing code")
26+
}
27+
28+
token, err := h.OAuthConfig.Exchange(
29+
r.Context(),
30+
code,
31+
oauth2.VerifierOption(h.CodeVerifier),
32+
)
33+
if err != nil {
34+
return "", fmt.Errorf("could not exchange code for token: %w", err)
35+
}
36+
37+
return token.AccessToken, nil
38+
}
File renamed without changes.

auth_url.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package yaakcli
2+
3+
import (
4+
"crypto/rand"
5+
"crypto/sha256"
6+
"encoding/base64"
7+
"encoding/hex"
8+
"fmt"
9+
"io"
10+
11+
"golang.org/x/oauth2"
12+
)
13+
14+
type AuthURL struct {
15+
URL string
16+
State string
17+
CodeVerifier string
18+
}
19+
20+
func (u *AuthURL) String() string {
21+
return u.URL
22+
}
23+
24+
func AuthorizationURL(config *oauth2.Config) (*AuthURL, error) {
25+
codeVerifier, verifierErr := randomBytesInHex(32) // 64-character string here
26+
if verifierErr != nil {
27+
return nil, fmt.Errorf("could not create a code verifier: %w", verifierErr)
28+
}
29+
sha2 := sha256.New()
30+
_, _ = io.WriteString(sha2, codeVerifier)
31+
codeChallenge := base64.RawURLEncoding.EncodeToString(sha2.Sum(nil))
32+
33+
state, stateErr := randomBytesInHex(24)
34+
if stateErr != nil {
35+
return nil, fmt.Errorf("could not generate random state: %w", stateErr)
36+
}
37+
38+
authUrl := config.AuthCodeURL(
39+
state,
40+
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
41+
oauth2.SetAuthURLParam("code_challenge", codeChallenge),
42+
)
43+
44+
return &AuthURL{
45+
URL: authUrl,
46+
State: state,
47+
CodeVerifier: codeVerifier,
48+
}, nil
49+
}
50+
51+
func randomBytesInHex(count int) (string, error) {
52+
buf := make([]byte, count)
53+
_, err := io.ReadFull(rand.Reader, buf)
54+
if err != nil {
55+
return "", fmt.Errorf("could not generate %d random bytes: %w", count, err)
56+
}
57+
58+
return hex.EncodeToString(buf), nil
59+
}

cmd_login.go

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package yaakcli
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"github.com/pkg/browser"
78
"github.com/pterm/pterm"
89
"github.com/spf13/cobra"
10+
"golang.org/x/oauth2"
11+
"net"
912
"net/http"
10-
"net/url"
1113
"os"
1214
"os/signal"
1315
"time"
@@ -19,72 +21,84 @@ var loginCmd = &cobra.Command{
1921
Long: "Open a web browser to authenticate with Yaak. Works with all browsers including Safari.",
2022
Run: func(cmd *cobra.Command, args []string) {
2123
CheckError(deleteAuthToken())
22-
23-
pterm.Info.Println("Starting browser-based login...")
24-
25-
// Save the token to a config file
26-
confirm := pterm.DefaultInteractiveConfirm
27-
confirm.DefaultValue = true
28-
open, err := confirm.Show("Open default browser")
29-
CheckError(err)
30-
31-
if !open {
32-
os.Exit(0)
33-
return
34-
}
24+
baseURL := prodStagingDevStr("https://yaak.app", "https://todo.yaak.app", "http://localhost:9444")
3525

3626
// Create a channel to receive the auth token
3727
tokenChan := make(chan string, 1)
3828

3929
// Set up a simple HTTP server to handle the OAuth callback
40-
server := &http.Server{
41-
Addr: "localhost:8085",
30+
listener, err := net.Listen("tcp", "127.0.0.1:0")
31+
CheckError(err)
32+
33+
addr := listener.Addr().(*net.TCPAddr)
34+
redirectURL := fmt.Sprintf("http://127.0.0.1:%d/oauth/callback", addr.Port)
35+
36+
// Open the browser to the login page
37+
oauthConfig := oauth2.Config{
38+
ClientID: "yaak-cli",
39+
ClientSecret: "",
40+
Endpoint: oauth2.Endpoint{
41+
AuthURL: baseURL + "/login/oauth/authorize",
42+
TokenURL: baseURL + "/login/oauth/access_token",
43+
},
44+
RedirectURL: redirectURL,
45+
Scopes: nil,
4246
}
47+
loginURL, err := AuthorizationURL(&oauthConfig)
48+
CheckError(err)
4349

44-
// Define the handler for the callback
45-
http.HandleFunc("/oauth/callback", func(w http.ResponseWriter, r *http.Request) {
50+
mux := http.NewServeMux()
51+
52+
mux.HandleFunc("/oauth/callback", func(w http.ResponseWriter, r *http.Request) {
53+
h := OAuthRedirectHandler{
54+
State: loginURL.State,
55+
CodeVerifier: loginURL.CodeVerifier,
56+
OAuthConfig: &oauthConfig,
57+
}
58+
token, err := h.ExchangeCode(r)
4659
// Get the token from the query parameters
47-
token := r.URL.Query().Get("token")
48-
if token == "" {
60+
if err != nil {
4961
w.WriteHeader(http.StatusBadRequest)
50-
_, _ = fmt.Fprintf(w, "Error: No token provided")
62+
_, _ = fmt.Fprintf(w, "Failed to get access token: %s", err.Error())
5163
return
5264
}
5365

5466
// Send the token to the channel
5567
tokenChan <- token
5668

5769
// Return a success message to the browser
58-
redirectTo := prodStagingDevStr(
59-
"https://yaak.app/login-cli/success",
60-
"https://todo.yaak.app/login-cli/success",
61-
"http://localhost:9444/login-cli/success",
62-
)
70+
redirectTo := baseURL + "/login/oauth/success"
6371
http.Redirect(w, r, redirectTo, http.StatusFound)
6472
})
6573

74+
server := &http.Server{Handler: mux}
75+
6676
// Start the server in a goroutine
6777
go func() {
68-
if err := server.ListenAndServe(); err != http.ErrServerClosed {
78+
if err := server.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
6979
pterm.Error.Printf("HTTP server error: %v\n", err)
7080
os.Exit(1)
7181
}
7282
}()
7383

84+
pterm.Info.Println("Initiating login to", loginURL)
85+
86+
confirm := pterm.DefaultInteractiveConfirm
87+
confirm.DefaultValue = true
88+
open, err := confirm.Show("Open default browser")
89+
CheckError(err)
90+
91+
if !open {
92+
os.Exit(0)
93+
return
94+
}
95+
7496
// Set up a signal handler to gracefully shut down the server
7597
sigChan := make(chan os.Signal, 1)
7698
signal.Notify(sigChan, os.Interrupt)
7799

78-
// Open the browser to the login page
79-
redirect := "http://localhost:8085/oauth/callback"
80-
loginURL := prodStagingDevStr(
81-
"https://yaak.app/login-cli?redirect=",
82-
"https://todo.yaak.app/login-cli?redirect=",
83-
"http://localhost:9444/login-cli?redirect=",
84-
) + url.QueryEscape(redirect)
85-
86100
// Open the browser based on the operating system
87-
err = browser.OpenURL(loginURL)
101+
err = browser.OpenURL(loginURL.String())
88102
if err != nil {
89103
pterm.Error.Printf("Failed to open browser: %v\n", err)
90104
pterm.Info.Println("Please open the following URL manually:")

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/pterm/pterm v0.12.81
1111
github.com/spf13/cobra v1.8.1
1212
github.com/zalando/go-keyring v0.2.6
13+
golang.org/x/oauth2 v0.30.0
1314
)
1415

1516
require (

go.sum

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi
1919
github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=
2020
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
2121
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
22-
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
23-
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
2422
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
2523
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
2624
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@@ -65,8 +63,6 @@ github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEej
6563
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
6664
github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
6765
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
68-
github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4=
69-
github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo=
7066
github.com/pterm/pterm v0.12.81 h1:ju+j5I2++FO1jBKMmscgh5h5DPFDFMB7epEjSoKehKA=
7167
github.com/pterm/pterm v0.12.81/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw=
7268
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -103,6 +99,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
10399
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
104100
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
105101
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
102+
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
103+
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
106104
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
107105
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
108106
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -118,26 +116,20 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
118116
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
119117
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
120118
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
121-
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
122-
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
123119
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
124120
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
125121
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
126122
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
127123
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
128124
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
129125
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
130-
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
131-
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
132126
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
133127
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
134128
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
135129
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
136130
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
137131
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
138132
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
139-
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
140-
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
141133
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
142134
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
143135
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

0 commit comments

Comments
 (0)