Skip to content

Commit 9032055

Browse files
committed
wip: login
1 parent 49dcf8a commit 9032055

File tree

3 files changed

+216
-10
lines changed

3 files changed

+216
-10
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/SwissDataScienceCenter/renku-dev-utils
33
go 1.24.2
44

55
require (
6+
github.com/golang-jwt/jwt/v5 v5.3.0
67
github.com/spf13/cobra v1.10.1
78
github.com/zalando/go-keyring v0.2.6
89
k8s.io/api v0.34.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
2828
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
2929
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
3030
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
31+
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
32+
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
3133
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
3234
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
3335
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=

pkg/renkuapi/auth.go

Lines changed: 213 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ import (
77
"fmt"
88
"net/http"
99
"net/url"
10+
"os/exec"
11+
"runtime"
1012
"strings"
1113
"time"
1214

15+
"github.com/SwissDataScienceCenter/renku-dev-utils/pkg/executils"
16+
"github.com/golang-jwt/jwt/v5"
1317
"github.com/zalando/go-keyring"
1418
)
1519

@@ -24,6 +28,9 @@ type RenkuApiAuth struct {
2428
clientID string
2529
scope string
2630

31+
accessToken string
32+
refreshToken string
33+
2734
httpClient *http.Client
2835
}
2936

@@ -53,29 +60,105 @@ func NewRenkuApiAuth(baseURL string) (auth *RenkuApiAuth, err error) {
5360
return auth, nil
5461
}
5562

56-
func (auth *RenkuApiAuth) GetAccessToken() (token string, err error) {
63+
func (auth *RenkuApiAuth) GetAccessToken(ctx context.Context) (token string, err error) {
64+
// Use access token if valid
65+
token = auth.accessToken
66+
if token != "" && isTokenValid(token) {
67+
return token, nil
68+
}
5769
token, err = auth.getAccessTokenFromKeyring()
58-
fmt.Println(token)
59-
fmt.Println(err)
70+
if err != nil {
71+
token = ""
72+
}
73+
if token != "" && isTokenValid(token) {
74+
return token, nil
75+
}
6076

61-
return "", fmt.Errorf("not implemented")
77+
// Refresh the access token if possible
78+
refreshToken := auth.refreshToken
79+
if refreshToken == "" {
80+
refreshToken, err = auth.getRefreshTokenFromKeyring()
81+
if err != nil {
82+
refreshToken = ""
83+
}
84+
}
85+
if refreshToken == "" {
86+
return "", fmt.Errorf("could not get access token")
87+
}
88+
tokenResult, err := auth.postRefeshToken(ctx, refreshToken)
89+
if err != nil {
90+
return token, nil
91+
}
92+
auth.accessToken = tokenResult.AccessToken
93+
auth.refreshToken = tokenResult.RefreshToken
94+
err = auth.saveAccessTokenToKeyring()
95+
if err != nil {
96+
return auth.accessToken, err
97+
}
98+
err = auth.saveRefreshTokenToKeyring()
99+
if err != nil {
100+
return auth.accessToken, err
101+
}
102+
return auth.accessToken, nil
103+
}
104+
105+
func isTokenValid(token string) (isValid bool) {
106+
claims := jwt.RegisteredClaims{}
107+
parser := jwt.NewParser()
108+
_, _, err := parser.ParseUnverified(token, &claims)
109+
if err != nil {
110+
return false
111+
}
112+
exp, err := claims.GetExpirationTime()
113+
if err != nil || exp == nil {
114+
return false
115+
}
116+
now := time.Now()
117+
leeway := time.Second * 10
118+
return now.Before(exp.Add(-leeway))
62119
}
63120

64121
func (auth *RenkuApiAuth) getAccessTokenFromKeyring() (token string, err error) {
65122
kUser := fmt.Sprintf("%s:%s", auth.getKeyringUserPrefix(), "access_token")
66-
return keyring.Get(keyringService, kUser)
123+
token, err = keyring.Get(keyringService, kUser)
124+
if err != nil {
125+
return token, err
126+
}
127+
auth.accessToken = token
128+
return token, nil
129+
}
130+
131+
func (auth *RenkuApiAuth) saveAccessTokenToKeyring() (err error) {
132+
if auth.accessToken == "" {
133+
return fmt.Errorf("access_token is not set")
134+
}
135+
kUser := fmt.Sprintf("%s:%s", auth.getKeyringUserPrefix(), "access_token")
136+
return keyring.Set(keyringService, kUser, auth.accessToken)
67137
}
68138

69139
func (auth *RenkuApiAuth) getRefreshTokenFromKeyring() (token string, err error) {
70140
kUser := fmt.Sprintf("%s:%s", auth.getKeyringUserPrefix(), "refresh_token")
71141
return keyring.Get(keyringService, kUser)
72142
}
73143

144+
func (auth *RenkuApiAuth) saveRefreshTokenToKeyring() (err error) {
145+
if auth.refreshToken == "" {
146+
return fmt.Errorf("refresh_token is not set")
147+
}
148+
kUser := fmt.Sprintf("%s:%s", auth.getKeyringUserPrefix(), "refresh_token")
149+
return keyring.Set(keyringService, kUser, auth.refreshToken)
150+
}
151+
74152
func (auth *RenkuApiAuth) getKeyringUserPrefix() string {
75153
return fmt.Sprintf("rdu:%s", auth.baseURL.String())
76154
}
77155

78156
func (auth *RenkuApiAuth) Login(ctx context.Context) error {
157+
// TODO: check username from API
158+
token, _ := auth.GetAccessToken(ctx)
159+
if token != "" {
160+
return nil
161+
}
79162
err := auth.performLogin(ctx)
80163
if err != nil {
81164
return err
@@ -88,8 +171,21 @@ func (auth *RenkuApiAuth) performLogin(ctx context.Context) error {
88171
if err != nil {
89172
return err
90173
}
91-
fmt.Printf("deviceAuthorization: %+v\n", deviceAuthorization)
92-
return fmt.Errorf("not implemented")
174+
err = openBrowser(ctx, deviceAuthorization.VerificationURIComplete)
175+
if err != nil {
176+
return err
177+
}
178+
tokenResult, err := auth.pollTokenEndpoint(ctx, deviceAuthorization)
179+
if err != nil {
180+
return err
181+
}
182+
auth.accessToken = tokenResult.AccessToken
183+
auth.refreshToken = tokenResult.RefreshToken
184+
err = auth.saveAccessTokenToKeyring()
185+
if err != nil {
186+
return err
187+
}
188+
return auth.saveRefreshTokenToKeyring()
93189
}
94190

95191
func (auth *RenkuApiAuth) startLogin(ctx context.Context) (result deviceAuthorization, err error) {
@@ -160,15 +256,12 @@ func (auth *RenkuApiAuth) getTokenURI(ctx context.Context) (tokenURI *url.URL, e
160256

161257
func (auth *RenkuApiAuth) getOpenIDConfiguration(ctx context.Context) error {
162258
configurationURL := auth.issuerURL.JoinPath("./.well-known/openid-configuration")
163-
fmt.Printf("configurationURL: %s\n", configurationURL.String())
164259
var result openIDConfigurationResponse
165260
_, err := auth.get(ctx, configurationURL.String(), &result)
166261
if err != nil {
167262
return err
168263
}
169264

170-
fmt.Printf("result: %+v\n", result)
171-
172265
parsed, err := url.Parse(result.DeviceAuthorizationEndpoint)
173266
if err != nil {
174267
return err
@@ -254,3 +347,113 @@ func tryParseResponse(resp *http.Response, result any) error {
254347

255348
return json.Unmarshal(outBuf.Bytes(), result)
256349
}
350+
351+
// TODO: refactor this to avoid duplication with opendeployment.go
352+
func openBrowser(ctx context.Context, openURL string) error {
353+
if runtime.GOOS == "darwin" {
354+
fmt.Printf("Opening: %s\n", openURL)
355+
cmd := exec.CommandContext(ctx, "open", openURL)
356+
_, err := executils.FormatOutput(cmd.Output())
357+
if err != nil {
358+
return err
359+
}
360+
return nil
361+
}
362+
363+
if runtime.GOOS == "linux" {
364+
fmt.Printf("Opening: %s\n", openURL)
365+
cmd := exec.CommandContext(ctx, "xdg-open", openURL)
366+
_, err := executils.FormatOutput(cmd.Output())
367+
if err != nil {
368+
return err
369+
}
370+
return nil
371+
}
372+
373+
fmt.Printf("Open this link in your browser: %s\n", openURL)
374+
return nil
375+
}
376+
377+
func (auth *RenkuApiAuth) pollTokenEndpoint(ctx context.Context, deviceAuthorization deviceAuthorization) (result tokenResult, err error) {
378+
deadline, cancel := context.WithDeadline(ctx, deviceAuthorization.ExpiresAt.Add(deviceAuthorization.Interval))
379+
defer cancel()
380+
381+
ticker := time.NewTicker(deviceAuthorization.Interval)
382+
defer ticker.Stop()
383+
384+
for {
385+
select {
386+
case <-deadline.Done():
387+
return result, deadline.Err()
388+
case <-ticker.C:
389+
result, err := auth.postToken(deadline, deviceAuthorization.DeviceCode)
390+
if err == nil {
391+
return result, nil
392+
}
393+
}
394+
}
395+
}
396+
397+
func (auth *RenkuApiAuth) postToken(ctx context.Context, deviceCode string) (result tokenResult, err error) {
398+
tokenURI, err := auth.getTokenURI(ctx)
399+
if err != nil {
400+
return result, err
401+
}
402+
403+
body := url.Values{}
404+
body.Set("client_id", auth.clientID)
405+
body.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
406+
body.Set("device_code", deviceCode)
407+
408+
var res tokenResponse
409+
_, err = auth.postForm(ctx, tokenURI.String(), body, &res)
410+
if err != nil {
411+
return result, err
412+
}
413+
414+
result = tokenResult{
415+
AccessToken: res.AccessToken,
416+
RefreshToken: res.RefreshToken,
417+
}
418+
return result, nil
419+
}
420+
421+
type tokenResult struct {
422+
AccessToken string
423+
RefreshToken string
424+
}
425+
426+
type tokenResponse struct {
427+
AccessToken string `json:"access_token"`
428+
ExpiresIn int32 `json:"expires_in"`
429+
RefreshToken string `json:"refresh_token"`
430+
RefreshExpiresIn int32 `json:"refresh_expires_in"`
431+
TokenType string `json:"token_type"`
432+
NotBeforePolicy int32 `json:"not-before-policy"`
433+
SessionState string `json:"session_state"`
434+
Scope string `json:"scope"`
435+
}
436+
437+
func (auth *RenkuApiAuth) postRefeshToken(ctx context.Context, refreshToken string) (result tokenResult, err error) {
438+
tokenURI, err := auth.getTokenURI(ctx)
439+
if err != nil {
440+
return result, err
441+
}
442+
443+
body := url.Values{}
444+
body.Set("client_id", auth.clientID)
445+
body.Set("grant_type", "refresh_token")
446+
body.Set("refresh_token", refreshToken)
447+
448+
var res tokenResponse
449+
_, err = auth.postForm(ctx, tokenURI.String(), body, &res)
450+
if err != nil {
451+
return result, err
452+
}
453+
454+
result = tokenResult{
455+
AccessToken: res.AccessToken,
456+
RefreshToken: res.RefreshToken,
457+
}
458+
return result, nil
459+
}

0 commit comments

Comments
 (0)