Skip to content

Commit 9fd7844

Browse files
make retrieval of idp well known config reusable
1 parent de1f9a7 commit 9fd7844

File tree

4 files changed

+155
-145
lines changed

4 files changed

+155
-145
lines changed

internal/pkg/auth/user_login.go

Lines changed: 2 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -51,23 +51,9 @@ type apiClient interface {
5151

5252
// AuthorizeUser implements the PKCE OAuth2 flow.
5353
func AuthorizeUser(p *print.Printer, isReauthentication bool) error {
54-
idpWellKnownConfigURL, err := getIDPWellKnownConfigURL()
54+
idpWellKnownConfig, err := retrieveIDPWellKnownConfig(p)
5555
if err != nil {
56-
return fmt.Errorf("get IDP well-known configuration: %w", err)
57-
}
58-
if idpWellKnownConfigURL != defaultWellKnownConfig {
59-
p.Warn("You are using a custom identity provider well-known configuration (%s) for authentication.\n", idpWellKnownConfigURL)
60-
err := p.PromptForEnter("Press Enter to proceed with the login...")
61-
if err != nil {
62-
return err
63-
}
64-
}
65-
66-
p.Debug(print.DebugLevel, "get IDP well-known configuration from %s", idpWellKnownConfigURL)
67-
httpClient := &http.Client{}
68-
idpWellKnownConfig, err := parseWellKnownConfiguration(httpClient, idpWellKnownConfigURL)
69-
if err != nil {
70-
return fmt.Errorf("parse IDP well-known configuration: %w", err)
56+
return err
7157
}
7258

7359
idpClientID, err := GetIDPClientID()
@@ -352,48 +338,3 @@ func openBrowser(pageUrl string) error {
352338
}
353339
return nil
354340
}
355-
356-
// parseWellKnownConfiguration gets the well-known OpenID configuration from the provided URL and returns it as a JSON
357-
// the method also stores the IDP token endpoint in the authentication storage
358-
func parseWellKnownConfiguration(httpClient apiClient, wellKnownConfigURL string) (wellKnownConfig *wellKnownConfig, err error) {
359-
req, _ := http.NewRequest("GET", wellKnownConfigURL, http.NoBody)
360-
res, err := httpClient.Do(req)
361-
if err != nil {
362-
return nil, fmt.Errorf("make the request: %w", err)
363-
}
364-
365-
// Process the response
366-
defer func() {
367-
closeErr := res.Body.Close()
368-
if closeErr != nil {
369-
err = fmt.Errorf("close response body: %w", closeErr)
370-
}
371-
}()
372-
body, err := io.ReadAll(res.Body)
373-
if err != nil {
374-
return nil, fmt.Errorf("read response body: %w", err)
375-
}
376-
377-
err = json.Unmarshal(body, &wellKnownConfig)
378-
if err != nil {
379-
return nil, fmt.Errorf("unmarshal response: %w", err)
380-
}
381-
if wellKnownConfig == nil {
382-
return nil, fmt.Errorf("nil well-known configuration response")
383-
}
384-
if wellKnownConfig.Issuer == "" {
385-
return nil, fmt.Errorf("found no issuer")
386-
}
387-
if wellKnownConfig.AuthorizationEndpoint == "" {
388-
return nil, fmt.Errorf("found no authorization endpoint")
389-
}
390-
if wellKnownConfig.TokenEndpoint == "" {
391-
return nil, fmt.Errorf("found no token endpoint")
392-
}
393-
394-
err = SetAuthField(IDP_TOKEN_ENDPOINT, wellKnownConfig.TokenEndpoint)
395-
if err != nil {
396-
return nil, fmt.Errorf("set token endpoint in the authentication storage: %w", err)
397-
}
398-
return wellKnownConfig, err
399-
}

internal/pkg/auth/user_login_test.go

Lines changed: 0 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ import (
55
"io"
66
"net/http"
77
"strings"
8-
"testing"
9-
10-
"github.com/google/go-cmp/cmp"
11-
"github.com/zalando/go-keyring"
128
)
139

1410
type apiClientMocked struct {
@@ -28,83 +24,3 @@ func (a *apiClientMocked) Do(_ *http.Request) (*http.Response, error) {
2824
Body: io.NopCloser(strings.NewReader(a.getResponse)),
2925
}, nil
3026
}
31-
32-
func TestParseWellKnownConfig(t *testing.T) {
33-
tests := []struct {
34-
name string
35-
getFails bool
36-
getResponse string
37-
isValid bool
38-
expected *wellKnownConfig
39-
}{
40-
{
41-
name: "success",
42-
getFails: false,
43-
getResponse: `{"issuer":"issuer","authorization_endpoint":"auth","token_endpoint":"token"}`,
44-
isValid: true,
45-
expected: &wellKnownConfig{
46-
Issuer: "issuer",
47-
AuthorizationEndpoint: "auth",
48-
TokenEndpoint: "token",
49-
},
50-
},
51-
{
52-
name: "get_fails",
53-
getFails: true,
54-
getResponse: "",
55-
isValid: false,
56-
expected: nil,
57-
},
58-
{
59-
name: "empty_response",
60-
getFails: true,
61-
getResponse: "",
62-
isValid: false,
63-
expected: nil,
64-
},
65-
{
66-
name: "missing_issuer",
67-
getFails: true,
68-
getResponse: `{"authorization_endpoint":"auth","token_endpoint":"token"}`,
69-
isValid: false,
70-
expected: nil,
71-
},
72-
{
73-
name: "missing_authorization",
74-
getFails: true,
75-
getResponse: `{"issuer":"issuer","token_endpoint":"token"}`,
76-
isValid: false,
77-
expected: nil,
78-
},
79-
{
80-
name: "missing_token",
81-
getFails: true,
82-
getResponse: `{"issuer":"issuer","authorization_endpoint":"auth"}`,
83-
isValid: false,
84-
expected: nil,
85-
},
86-
}
87-
for _, tt := range tests {
88-
t.Run(tt.name, func(t *testing.T) {
89-
keyring.MockInit()
90-
91-
testClient := apiClientMocked{
92-
tt.getFails,
93-
tt.getResponse,
94-
}
95-
96-
got, err := parseWellKnownConfiguration(&testClient, "")
97-
98-
if tt.isValid && err != nil {
99-
t.Fatalf("expected no error, got %v", err)
100-
}
101-
if !tt.isValid && err == nil {
102-
t.Fatalf("expected error, got none")
103-
}
104-
105-
if tt.isValid && !cmp.Equal(*got, *tt.expected) {
106-
t.Fatalf("expected %v, got %v", tt.expected, got)
107-
}
108-
})
109-
}
110-
}

internal/pkg/auth/utils.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package auth
22

33
import (
4+
"encoding/json"
45
"fmt"
6+
"io"
7+
"net/http"
58

69
"github.com/spf13/viper"
710
"github.com/stackitcloud/stackit-cli/internal/pkg/config"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
812
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
913
)
1014

@@ -39,3 +43,70 @@ func GetIDPClientID() (string, error) {
3943

4044
return idpClientID, nil
4145
}
46+
47+
func retrieveIDPWellKnownConfig(p *print.Printer) (*wellKnownConfig, error) {
48+
idpWellKnownConfigURL, err := getIDPWellKnownConfigURL()
49+
if err != nil {
50+
return nil, fmt.Errorf("get IDP well-known configuration: %w", err)
51+
}
52+
if idpWellKnownConfigURL != defaultWellKnownConfig {
53+
p.Warn("You are using a custom identity provider well-known configuration (%s) for authentication.\n", idpWellKnownConfigURL)
54+
err := p.PromptForEnter("Press Enter to proceed with the login...")
55+
if err != nil {
56+
return nil, err
57+
}
58+
}
59+
60+
p.Debug(print.DebugLevel, "get IDP well-known configuration from %s", idpWellKnownConfigURL)
61+
httpClient := &http.Client{}
62+
idpWellKnownConfig, err := parseWellKnownConfiguration(httpClient, idpWellKnownConfigURL)
63+
if err != nil {
64+
return nil, fmt.Errorf("parse IDP well-known configuration: %w", err)
65+
}
66+
return idpWellKnownConfig, nil
67+
}
68+
69+
// parseWellKnownConfiguration gets the well-known OpenID configuration from the provided URL and returns it as a JSON
70+
// the method also stores the IDP token endpoint in the authentication storage
71+
func parseWellKnownConfiguration(httpClient apiClient, wellKnownConfigURL string) (wellKnownConfig *wellKnownConfig, err error) {
72+
req, _ := http.NewRequest("GET", wellKnownConfigURL, http.NoBody)
73+
res, err := httpClient.Do(req)
74+
if err != nil {
75+
return nil, fmt.Errorf("make the request: %w", err)
76+
}
77+
78+
// Process the response
79+
defer func() {
80+
closeErr := res.Body.Close()
81+
if closeErr != nil {
82+
err = fmt.Errorf("close response body: %w", closeErr)
83+
}
84+
}()
85+
body, err := io.ReadAll(res.Body)
86+
if err != nil {
87+
return nil, fmt.Errorf("read response body: %w", err)
88+
}
89+
90+
err = json.Unmarshal(body, &wellKnownConfig)
91+
if err != nil {
92+
return nil, fmt.Errorf("unmarshal response: %w", err)
93+
}
94+
if wellKnownConfig == nil {
95+
return nil, fmt.Errorf("nil well-known configuration response")
96+
}
97+
if wellKnownConfig.Issuer == "" {
98+
return nil, fmt.Errorf("found no issuer")
99+
}
100+
if wellKnownConfig.AuthorizationEndpoint == "" {
101+
return nil, fmt.Errorf("found no authorization endpoint")
102+
}
103+
if wellKnownConfig.TokenEndpoint == "" {
104+
return nil, fmt.Errorf("found no token endpoint")
105+
}
106+
107+
err = SetAuthField(IDP_TOKEN_ENDPOINT, wellKnownConfig.TokenEndpoint)
108+
if err != nil {
109+
return nil, fmt.Errorf("set token endpoint in the authentication storage: %w", err)
110+
}
111+
return wellKnownConfig, err
112+
}

internal/pkg/auth/utils_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package auth
33
import (
44
"testing"
55

6+
"github.com/google/go-cmp/cmp"
67
"github.com/spf13/viper"
78
"github.com/stackitcloud/stackit-cli/internal/pkg/config"
9+
"github.com/zalando/go-keyring"
810
)
911

1012
func TestGetWellKnownConfig(t *testing.T) {
@@ -118,3 +120,83 @@ func TestGetIDPClientID(t *testing.T) {
118120
})
119121
}
120122
}
123+
124+
func TestParseWellKnownConfig(t *testing.T) {
125+
tests := []struct {
126+
name string
127+
getFails bool
128+
getResponse string
129+
isValid bool
130+
expected *wellKnownConfig
131+
}{
132+
{
133+
name: "success",
134+
getFails: false,
135+
getResponse: `{"issuer":"issuer","authorization_endpoint":"auth","token_endpoint":"token"}`,
136+
isValid: true,
137+
expected: &wellKnownConfig{
138+
Issuer: "issuer",
139+
AuthorizationEndpoint: "auth",
140+
TokenEndpoint: "token",
141+
},
142+
},
143+
{
144+
name: "get_fails",
145+
getFails: true,
146+
getResponse: "",
147+
isValid: false,
148+
expected: nil,
149+
},
150+
{
151+
name: "empty_response",
152+
getFails: true,
153+
getResponse: "",
154+
isValid: false,
155+
expected: nil,
156+
},
157+
{
158+
name: "missing_issuer",
159+
getFails: true,
160+
getResponse: `{"authorization_endpoint":"auth","token_endpoint":"token"}`,
161+
isValid: false,
162+
expected: nil,
163+
},
164+
{
165+
name: "missing_authorization",
166+
getFails: true,
167+
getResponse: `{"issuer":"issuer","token_endpoint":"token"}`,
168+
isValid: false,
169+
expected: nil,
170+
},
171+
{
172+
name: "missing_token",
173+
getFails: true,
174+
getResponse: `{"issuer":"issuer","authorization_endpoint":"auth"}`,
175+
isValid: false,
176+
expected: nil,
177+
},
178+
}
179+
for _, tt := range tests {
180+
t.Run(tt.name, func(t *testing.T) {
181+
keyring.MockInit()
182+
183+
testClient := apiClientMocked{
184+
tt.getFails,
185+
tt.getResponse,
186+
}
187+
188+
got, err := parseWellKnownConfiguration(&testClient, "")
189+
190+
if tt.isValid && err != nil {
191+
t.Fatalf("expected no error, got %v", err)
192+
}
193+
if !tt.isValid && err == nil {
194+
t.Fatalf("expected error, got none")
195+
}
196+
197+
if tt.isValid && !cmp.Equal(*got, *tt.expected) {
198+
t.Fatalf("expected %v, got %v", tt.expected, got)
199+
}
200+
})
201+
}
202+
}

0 commit comments

Comments
 (0)