Skip to content

Commit

Permalink
Migrate to new STACKIT IDP (#404)
Browse files Browse the repository at this point in the history
* Migrate to new STACKIT IDP

* Add additional debug log

* Remove warning on auth login command

* Add email scope to IDP requests
  • Loading branch information
joaopalet authored Jul 8, 2024
1 parent 69cda1c commit cd9c226
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 33 deletions.
1 change: 1 addition & 0 deletions docs/stackit_auth_login.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Logs in to the STACKIT CLI
### Synopsis

Logs in to the STACKIT CLI using a user account.
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.

```
stackit auth login [flags]
Expand Down
14 changes: 4 additions & 10 deletions internal/cmd/auth/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,16 @@ func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "login",
Short: "Logs in to the STACKIT CLI",
Long: "Logs in to the STACKIT CLI using a user account.",
Args: args.NoArgs,
Long: fmt.Sprintf("%s\n%s",
"Logs in to the STACKIT CLI using a user account.",
"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."),
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Login to the STACKIT CLI. This command will open a browser window where you can login to your STACKIT account`,
"$ stackit auth login"),
),
RunE: func(cmd *cobra.Command, args []string) error {
p.Warn(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n",
"Starting on July 9 2024, the new STACKIT Identity Provider (IDP) will be available.",
"On this date, we will release a new version of the STACKIT CLI that will use the new IDP for user authentication.",
"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.",
"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.",
"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",
))

err := auth.AuthorizeUser(p, false)
if err != nil {
return fmt.Errorf("authorization failed: %w", err)
Expand Down
37 changes: 26 additions & 11 deletions internal/pkg/auth/user_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,18 @@ import (
)

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

loginSuccessPath = "/login-successful"
stackitLandingPage = "https://www.stackit.de"
htmlTemplatesPath = "templates"
loginSuccessfulHTMLFile = "login-successful.html"

// The IDP doesn't support wildcards for the port,
// so we configure a range of ports from 8000 to 8020
defaultPort = 8000
configuredPortRange = 20
)

//go:embed templates/*
Expand Down Expand Up @@ -60,22 +65,32 @@ func AuthorizeUser(p *print.Printer, isReauthentication bool) error {
}
}

listener, err := net.Listen("tcp", ":0")
if err != nil {
return fmt.Errorf("bind port for login redirect: %w", err)
var redirectURL string
var listener net.Listener
var listenerErr error
var port int
for i := range configuredPortRange {
port = defaultPort + i
portString := fmt.Sprintf(":%s", strconv.Itoa(port))
p.Debug(print.DebugLevel, "trying to bind port %d for login redirect", port)
listener, listenerErr = net.Listen("tcp", portString)
if listenerErr == nil {
redirectURL = fmt.Sprintf("http://localhost:%d", port)
p.Debug(print.DebugLevel, "bound port %d for login redirect", port)
break
}
p.Debug(print.DebugLevel, "unable to bind port %d for login redirect: %s", port, listenerErr)
}
address, ok := listener.Addr().(*net.TCPAddr)
if !ok {
return fmt.Errorf("assert listener address type to TCP address")
if listenerErr != nil {
return fmt.Errorf("unable to bind port for login redirect, tried from port %d to %d: %w", defaultPort, port, err)
}
redirectURL := fmt.Sprintf("http://localhost:%d", address.Port)

conf := &oauth2.Config{
ClientID: cliClientID,
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("%s/authorize", idpEndpoint),
},
Scopes: []string{"openid"},
Scopes: []string{"openid offline_access email"},
RedirectURL: redirectURL,
}

Expand All @@ -98,7 +113,7 @@ func AuthorizeUser(p *print.Printer, isReauthentication bool) error {
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
p.Debug(print.DebugLevel, "received request from authentication server")
// Close the server only if there was an error
// Otherwise, it will redirect to the succesfull login page
// Otherwise, it will redirect to the successful login page
defer func() {
if errServer != nil {
fmt.Println(errServer)
Expand Down
12 changes: 4 additions & 8 deletions internal/pkg/auth/user_token_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,18 @@ func (utf *userTokenFlow) RoundTrip(req *http.Request) (*http.Response, error) {
}

accessTokenValid := false
if accessTokenExpired, err := tokenExpired(utf.accessToken); err != nil {
accessTokenExpired, err := tokenExpired(utf.accessToken)
if err != nil {
return nil, fmt.Errorf("check if access token has expired: %w", err)
} else if !accessTokenExpired {
accessTokenValid = true
} else if refreshTokenExpired, err := tokenExpired(utf.refreshToken); err != nil {
return nil, fmt.Errorf("check if refresh token has expired: %w", err)
} else if !refreshTokenExpired {
} else {
utf.printer.Debug(print.DebugLevel, "access token expired, refreshing...")
err = refreshTokens(utf)
if err == nil {
accessTokenValid = true
} else {
utf.printer.Debug(print.ErrorLevel, "refresh access token: %v", err)
utf.printer.Debug(print.ErrorLevel, "refresh access token: %w", err)
}
}

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

// without this header, the API returns error "An Authentication object was not found in the SecurityContext"
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

return req, nil
}

Expand Down
15 changes: 11 additions & 4 deletions internal/pkg/auth/user_token_flow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ func (rt *clientTransport) RoundTrip(req *http.Request) (*http.Response, error)
if reqURL == rt.requestURL {
return rt.roundTripRequest()
}
if fmt.Sprintf("https://%s", reqURL) == fmt.Sprintf("%s/token", defaultIDPEndpoint) {
idpEndpoint, err := getIDPEndpoint()
if err != nil {
rt.t.Fatalf("get IDP endpoint for test: %v", err)
}
if fmt.Sprintf("https://%s", reqURL) == fmt.Sprintf("%s/token", idpEndpoint) {
return rt.roundTripRefreshTokens()
}
rt.t.Fatalf("unexpected request to %q", reqURL)
Expand Down Expand Up @@ -163,6 +167,7 @@ func TestRoundTrip(t *testing.T) {
desc: "tokens expired",
accessTokenExpiresAt: time.Now().Add(-time.Hour),
refreshTokenExpiresAt: time.Now().Add(-time.Hour),
refreshTokensFails: true, // Fails because refresh token is expired
isValid: true,
expectedReautorizeUserCalled: true,
expectedTokensRefreshed: true,
Expand Down Expand Up @@ -190,9 +195,10 @@ func TestRoundTrip(t *testing.T) {
accessTokenExpiresAt: time.Now().Add(-time.Hour),
refreshTokenExpiresAt: time.Now().Add(time.Hour),
refreshTokenInvalid: true,
isValid: false,
expectedReautorizeUserCalled: false,
expectedTokensRefreshed: false,
refreshTokensFails: true, // Fails because refresh token is invalid
isValid: true,
expectedReautorizeUserCalled: true,
expectedTokensRefreshed: true, // Refreshed during reauthorization
},
{
desc: "refresh token invalid but unused",
Expand All @@ -207,6 +213,7 @@ func TestRoundTrip(t *testing.T) {
desc: "authorize user fails",
accessTokenExpiresAt: time.Now().Add(-time.Hour),
refreshTokenExpiresAt: time.Now().Add(-time.Hour),
refreshTokensFails: true, // Fails because refresh token is expired
authorizeUserFails: true,
isValid: false,
expectedReautorizeUserCalled: true,
Expand Down

0 comments on commit cd9c226

Please sign in to comment.