Skip to content

Commit 5f9a96c

Browse files
committed
wip
1 parent 4191661 commit 5f9a96c

File tree

3 files changed

+211
-4
lines changed

3 files changed

+211
-4
lines changed

pkg/cmd/login.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ func login(cmd *cobra.Command, args []string) {
6161
os.Exit(1)
6262
}
6363

64-
auth.GetAccessToken()
64+
err = auth.Login(ctx)
65+
if err != nil {
66+
fmt.Println(err)
67+
os.Exit(1)
68+
}
6569
}
6670

6771
func init() {

pkg/keycloak/rest.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ func (client *KeycloakClient) PostForm(ctx context.Context, url string, data url
102102
}
103103

104104
return resp, nil
105-
106105
}
107106

108107
func tryParseResponse(resp *http.Response, result any) error {

pkg/renkuapi/auth.go

Lines changed: 206 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
11
package renkuapi
22

33
import (
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+
1018
type 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

1530
func 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)
5074
func (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

Comments
 (0)