11package renkuapi
22
33import (
4+ "bytes"
5+ "context"
6+ "encoding/json"
47 "fmt"
8+ "net/http"
59 "net/url"
10+ "strings"
11+ "time"
612
713 "github.com/zalando/go-keyring"
814)
915
16+ const jsonContentType string = "application/json"
17+
1018type RenkuApiAuth struct {
11- baseURL * url.URL
12- issuerURL * url.URL
19+ baseURL * url.URL
20+ issuerURL * url.URL
21+ authenticationURI * url.URL
22+ tokenURI * url.URL
23+
24+ clientID string
25+ scope string
26+
27+ httpClient * http.Client
1328}
1429
1530func NewRenkuApiAuth (baseURL string ) (auth * RenkuApiAuth , err error ) {
@@ -26,6 +41,15 @@ func NewRenkuApiAuth(baseURL string) (auth *RenkuApiAuth, err error) {
2641 if auth .issuerURL == nil {
2742 auth .issuerURL = parsedURL .JoinPath ("auth/realms/Renku" )
2843 }
44+ if auth .clientID == "" {
45+ auth .clientID = "renku-cli"
46+ }
47+ if auth .scope == "" {
48+ auth .scope = "offline_access"
49+ }
50+ if auth .httpClient == nil {
51+ auth .httpClient = http .DefaultClient
52+ }
2953 return auth , nil
3054}
3155
@@ -50,3 +74,183 @@ func (auth *RenkuApiAuth) getRefreshTokenFromKeyring() (token string, err error)
5074func (auth * RenkuApiAuth ) getKeyringUserPrefix () string {
5175 return fmt .Sprintf ("rdu:%s" , auth .baseURL .String ())
5276}
77+
78+ func (auth * RenkuApiAuth ) Login (ctx context.Context ) error {
79+ err := auth .performLogin (ctx )
80+ if err != nil {
81+ return err
82+ }
83+ return nil
84+ }
85+
86+ func (auth * RenkuApiAuth ) performLogin (ctx context.Context ) error {
87+ deviceAuthorization , err := auth .startLogin (ctx )
88+ if err != nil {
89+ return err
90+ }
91+ fmt .Printf ("deviceAuthorization: %+v\n " , deviceAuthorization )
92+ return fmt .Errorf ("not implemented" )
93+ }
94+
95+ func (auth * RenkuApiAuth ) startLogin (ctx context.Context ) (result deviceAuthorization , err error ) {
96+ authenticationURI , err := auth .getAuthenticationURI (ctx )
97+ if err != nil {
98+ return result , err
99+ }
100+
101+ body := url.Values {}
102+ body .Set ("client_id" , auth .clientID )
103+ body .Set ("scope" , auth .scope )
104+
105+ var res deviceAuthorizationResponse
106+ _ , err = auth .postForm (ctx , authenticationURI .String (), body , & res )
107+ if err != nil {
108+ return result , err
109+ }
110+
111+ result = deviceAuthorization {
112+ DeviceCode : res .DeviceCode ,
113+ VerificationURIComplete : res .VerificationURIComplete ,
114+ ExpiresAt : time .Now ().Add (time .Second * time .Duration (res .ExpiresIn )),
115+ Interval : time .Second * time .Duration (res .Interval ),
116+ }
117+ if result .Interval == time .Duration (0 ) {
118+ result .Interval = time .Second * 5
119+ }
120+ return result , nil
121+ }
122+
123+ type deviceAuthorization struct {
124+ DeviceCode string
125+ VerificationURIComplete string
126+ ExpiresAt time.Time
127+ Interval time.Duration
128+ }
129+
130+ type deviceAuthorizationResponse struct {
131+ DeviceCode string `json:"device_code"`
132+ UserCode string `json:"user_code"`
133+ VerificationURI string `json:"verification_uri"`
134+ VerificationURIComplete string `json:"verification_uri_complete"`
135+ ExpiresIn int32 `json:"expires_in"`
136+ Interval int32 `json:"interval"`
137+ }
138+
139+ func (auth * RenkuApiAuth ) getAuthenticationURI (ctx context.Context ) (authenticationURI * url.URL , err error ) {
140+ if auth .authenticationURI != nil {
141+ return auth .authenticationURI , nil
142+ }
143+ err = auth .getOpenIDConfiguration (ctx )
144+ if err != nil {
145+ return nil , err
146+ }
147+ return auth .authenticationURI , nil
148+ }
149+
150+ func (auth * RenkuApiAuth ) getTokenURI (ctx context.Context ) (tokenURI * url.URL , err error ) {
151+ if auth .tokenURI != nil {
152+ return auth .tokenURI , nil
153+ }
154+ err = auth .getOpenIDConfiguration (ctx )
155+ if err != nil {
156+ return nil , err
157+ }
158+ return auth .tokenURI , nil
159+ }
160+
161+ func (auth * RenkuApiAuth ) getOpenIDConfiguration (ctx context.Context ) error {
162+ configurationURL := auth .issuerURL .JoinPath ("./.well-known/openid-configuration" )
163+ fmt .Printf ("configurationURL: %s\n " , configurationURL .String ())
164+ var result openIDConfigurationResponse
165+ _ , err := auth .get (ctx , configurationURL .String (), & result )
166+ if err != nil {
167+ return err
168+ }
169+
170+ fmt .Printf ("result: %+v\n " , result )
171+
172+ parsed , err := url .Parse (result .DeviceAuthorizationEndpoint )
173+ if err != nil {
174+ return err
175+ }
176+ auth .authenticationURI = parsed
177+
178+ parsed , err = url .Parse (result .TokenEndpoint )
179+ if err != nil {
180+ return err
181+ }
182+ auth .tokenURI = parsed
183+ return nil
184+ }
185+
186+ type openIDConfigurationResponse struct {
187+ DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"`
188+ TokenEndpoint string `json:"token_endpoint"`
189+ }
190+
191+ // TODO: refactor this method to avoid duplication with package keycloak
192+
193+ func (auth * RenkuApiAuth ) get (ctx context.Context , url string , result any ) (resp * http.Response , err error ) {
194+ req , err := http .NewRequestWithContext (ctx , "GET" , url , nil )
195+ if err != nil {
196+ return nil , err
197+ }
198+
199+ req .Header .Set ("Accept" , jsonContentType )
200+
201+ resp , err = auth .httpClient .Do (req )
202+ if err != nil {
203+ return resp , err
204+ }
205+
206+ var parseErr error
207+ if resp .Header .Get ("Content-Type" ) == jsonContentType {
208+ parseErr = tryParseResponse (resp , result )
209+ } else {
210+ return resp , fmt .Errorf ("Expected '%s' but got response with content type '%s'" , jsonContentType , resp .Header .Get ("Content-Type" ))
211+ }
212+ if resp .StatusCode >= 200 && resp .StatusCode < 300 && parseErr != nil {
213+ return resp , parseErr
214+ }
215+
216+ return resp , nil
217+ }
218+
219+ func (auth * RenkuApiAuth ) postForm (ctx context.Context , url string , data url.Values , result any ) (resp * http.Response , err error ) {
220+ req , err := http .NewRequestWithContext (ctx , "POST" , url , strings .NewReader (data .Encode ()))
221+ if err != nil {
222+ return nil , err
223+ }
224+
225+ req .Header .Set ("Accept" , jsonContentType )
226+ req .Header .Set ("Content-Type" , "application/x-www-form-urlencoded" )
227+
228+ resp , err = auth .httpClient .Do (req )
229+ if err != nil {
230+ return resp , err
231+ }
232+
233+ var parseErr error
234+ if resp .Header .Get ("Content-Type" ) == jsonContentType {
235+ parseErr = tryParseResponse (resp , result )
236+ } else {
237+ return resp , fmt .Errorf ("Expected '%s' but got response with content type '%s'" , jsonContentType , resp .Header .Get ("Content-Type" ))
238+ }
239+ if resp .StatusCode >= 200 && resp .StatusCode < 300 && parseErr != nil {
240+ return resp , parseErr
241+ }
242+
243+ return resp , nil
244+ }
245+
246+ func tryParseResponse (resp * http.Response , result any ) error {
247+ defer resp .Body .Close ()
248+
249+ outBuf := new (bytes.Buffer )
250+ _ , err := outBuf .ReadFrom (resp .Body )
251+ if err != nil {
252+ return err
253+ }
254+
255+ return json .Unmarshal (outBuf .Bytes (), result )
256+ }
0 commit comments