Skip to content

Commit cd9c226

Browse files
authored
Migrate to new STACKIT IDP (#404)
* Migrate to new STACKIT IDP * Add additional debug log * Remove warning on auth login command * Add email scope to IDP requests
1 parent 69cda1c commit cd9c226

File tree

5 files changed

+46
-33
lines changed

5 files changed

+46
-33
lines changed

docs/stackit_auth_login.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Logs in to the STACKIT CLI
55
### Synopsis
66

77
Logs in to the STACKIT CLI using a user account.
8+
The authentication is done via a web-based authorization flow, where the command will open a browser window in which you can login to your STACKIT account.
89

910
```
1011
stackit auth login [flags]

internal/cmd/auth/login/login.go

+4-10
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,16 @@ func NewCmd(p *print.Printer) *cobra.Command {
1515
cmd := &cobra.Command{
1616
Use: "login",
1717
Short: "Logs in to the STACKIT CLI",
18-
Long: "Logs in to the STACKIT CLI using a user account.",
19-
Args: args.NoArgs,
18+
Long: fmt.Sprintf("%s\n%s",
19+
"Logs in to the STACKIT CLI using a user account.",
20+
"The authentication is done via a web-based authorization flow, where the command will open a browser window in which you can login to your STACKIT account."),
21+
Args: args.NoArgs,
2022
Example: examples.Build(
2123
examples.NewExample(
2224
`Login to the STACKIT CLI. This command will open a browser window where you can login to your STACKIT account`,
2325
"$ stackit auth login"),
2426
),
2527
RunE: func(cmd *cobra.Command, args []string) error {
26-
p.Warn(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n",
27-
"Starting on July 9 2024, the new STACKIT Identity Provider (IDP) will be available.",
28-
"On this date, we will release a new version of the STACKIT CLI that will use the new IDP for user authentication.",
29-
"This also means that the user authentication on STACKIT CLI versions released before July 9 2024 is no longer guaranteed to work for all services.",
30-
"Please make sure to update your STACKIT CLI to the latest version after July 9 2024 to ensure that you can continue to use all STACKIT services.",
31-
"You can find more information regarding the new IDP at https://docs.stackit.cloud/stackit/en/release-notes-23101442.html#ReleaseNotes-2024-06-21-identity-provider",
32-
))
33-
3428
err := auth.AuthorizeUser(p, false)
3529
if err != nil {
3630
return fmt.Errorf("authorization failed: %w", err)

internal/pkg/auth/user_login.go

+26-11
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,18 @@ import (
2323
)
2424

2525
const (
26-
defaultIDPEndpoint = "https://auth.01.idp.eu01.stackit.cloud/oauth"
27-
cliClientID = "stackit-cli-client-id"
26+
defaultIDPEndpoint = "https://accounts.stackit.cloud/oauth/v2"
27+
cliClientID = "stackit-cli-0000-0000-000000000001"
2828

2929
loginSuccessPath = "/login-successful"
3030
stackitLandingPage = "https://www.stackit.de"
3131
htmlTemplatesPath = "templates"
3232
loginSuccessfulHTMLFile = "login-successful.html"
33+
34+
// The IDP doesn't support wildcards for the port,
35+
// so we configure a range of ports from 8000 to 8020
36+
defaultPort = 8000
37+
configuredPortRange = 20
3338
)
3439

3540
//go:embed templates/*
@@ -60,22 +65,32 @@ func AuthorizeUser(p *print.Printer, isReauthentication bool) error {
6065
}
6166
}
6267

63-
listener, err := net.Listen("tcp", ":0")
64-
if err != nil {
65-
return fmt.Errorf("bind port for login redirect: %w", err)
68+
var redirectURL string
69+
var listener net.Listener
70+
var listenerErr error
71+
var port int
72+
for i := range configuredPortRange {
73+
port = defaultPort + i
74+
portString := fmt.Sprintf(":%s", strconv.Itoa(port))
75+
p.Debug(print.DebugLevel, "trying to bind port %d for login redirect", port)
76+
listener, listenerErr = net.Listen("tcp", portString)
77+
if listenerErr == nil {
78+
redirectURL = fmt.Sprintf("http://localhost:%d", port)
79+
p.Debug(print.DebugLevel, "bound port %d for login redirect", port)
80+
break
81+
}
82+
p.Debug(print.DebugLevel, "unable to bind port %d for login redirect: %s", port, listenerErr)
6683
}
67-
address, ok := listener.Addr().(*net.TCPAddr)
68-
if !ok {
69-
return fmt.Errorf("assert listener address type to TCP address")
84+
if listenerErr != nil {
85+
return fmt.Errorf("unable to bind port for login redirect, tried from port %d to %d: %w", defaultPort, port, err)
7086
}
71-
redirectURL := fmt.Sprintf("http://localhost:%d", address.Port)
7287

7388
conf := &oauth2.Config{
7489
ClientID: cliClientID,
7590
Endpoint: oauth2.Endpoint{
7691
AuthURL: fmt.Sprintf("%s/authorize", idpEndpoint),
7792
},
78-
Scopes: []string{"openid"},
93+
Scopes: []string{"openid offline_access email"},
7994
RedirectURL: redirectURL,
8095
}
8196

@@ -98,7 +113,7 @@ func AuthorizeUser(p *print.Printer, isReauthentication bool) error {
98113
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
99114
p.Debug(print.DebugLevel, "received request from authentication server")
100115
// Close the server only if there was an error
101-
// Otherwise, it will redirect to the succesfull login page
116+
// Otherwise, it will redirect to the successful login page
102117
defer func() {
103118
if errServer != nil {
104119
fmt.Println(errServer)

internal/pkg/auth/user_token_flow.go

+4-8
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,18 @@ func (utf *userTokenFlow) RoundTrip(req *http.Request) (*http.Response, error) {
4343
}
4444

4545
accessTokenValid := false
46-
if accessTokenExpired, err := tokenExpired(utf.accessToken); err != nil {
46+
accessTokenExpired, err := tokenExpired(utf.accessToken)
47+
if err != nil {
4748
return nil, fmt.Errorf("check if access token has expired: %w", err)
4849
} else if !accessTokenExpired {
4950
accessTokenValid = true
50-
} else if refreshTokenExpired, err := tokenExpired(utf.refreshToken); err != nil {
51-
return nil, fmt.Errorf("check if refresh token has expired: %w", err)
52-
} else if !refreshTokenExpired {
51+
} else {
5352
utf.printer.Debug(print.DebugLevel, "access token expired, refreshing...")
5453
err = refreshTokens(utf)
5554
if err == nil {
5655
accessTokenValid = true
5756
} else {
58-
utf.printer.Debug(print.ErrorLevel, "refresh access token: %v", err)
57+
utf.printer.Debug(print.ErrorLevel, "refresh access token: %w", err)
5958
}
6059
}
6160

@@ -177,9 +176,6 @@ func buildRequestToRefreshTokens(utf *userTokenFlow) (*http.Request, error) {
177176
reqQuery.Set("token_format", "jwt")
178177
req.URL.RawQuery = reqQuery.Encode()
179178

180-
// without this header, the API returns error "An Authentication object was not found in the SecurityContext"
181-
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
182-
183179
return req, nil
184180
}
185181

internal/pkg/auth/user_token_flow_test.go

+11-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ func (rt *clientTransport) RoundTrip(req *http.Request) (*http.Response, error)
2828
if reqURL == rt.requestURL {
2929
return rt.roundTripRequest()
3030
}
31-
if fmt.Sprintf("https://%s", reqURL) == fmt.Sprintf("%s/token", defaultIDPEndpoint) {
31+
idpEndpoint, err := getIDPEndpoint()
32+
if err != nil {
33+
rt.t.Fatalf("get IDP endpoint for test: %v", err)
34+
}
35+
if fmt.Sprintf("https://%s", reqURL) == fmt.Sprintf("%s/token", idpEndpoint) {
3236
return rt.roundTripRefreshTokens()
3337
}
3438
rt.t.Fatalf("unexpected request to %q", reqURL)
@@ -163,6 +167,7 @@ func TestRoundTrip(t *testing.T) {
163167
desc: "tokens expired",
164168
accessTokenExpiresAt: time.Now().Add(-time.Hour),
165169
refreshTokenExpiresAt: time.Now().Add(-time.Hour),
170+
refreshTokensFails: true, // Fails because refresh token is expired
166171
isValid: true,
167172
expectedReautorizeUserCalled: true,
168173
expectedTokensRefreshed: true,
@@ -190,9 +195,10 @@ func TestRoundTrip(t *testing.T) {
190195
accessTokenExpiresAt: time.Now().Add(-time.Hour),
191196
refreshTokenExpiresAt: time.Now().Add(time.Hour),
192197
refreshTokenInvalid: true,
193-
isValid: false,
194-
expectedReautorizeUserCalled: false,
195-
expectedTokensRefreshed: false,
198+
refreshTokensFails: true, // Fails because refresh token is invalid
199+
isValid: true,
200+
expectedReautorizeUserCalled: true,
201+
expectedTokensRefreshed: true, // Refreshed during reauthorization
196202
},
197203
{
198204
desc: "refresh token invalid but unused",
@@ -207,6 +213,7 @@ func TestRoundTrip(t *testing.T) {
207213
desc: "authorize user fails",
208214
accessTokenExpiresAt: time.Now().Add(-time.Hour),
209215
refreshTokenExpiresAt: time.Now().Add(-time.Hour),
216+
refreshTokensFails: true, // Fails because refresh token is expired
210217
authorizeUserFails: true,
211218
isValid: false,
212219
expectedReautorizeUserCalled: true,

0 commit comments

Comments
 (0)