@@ -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
64121func (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
69139func (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+
74152func (auth * RenkuApiAuth ) getKeyringUserPrefix () string {
75153 return fmt .Sprintf ("rdu:%s" , auth .baseURL .String ())
76154}
77155
78156func (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
95191func (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
161257func (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