diff --git a/docs/stackit_auth.md b/docs/stackit_auth.md index 5c891583a..3f9406c46 100644 --- a/docs/stackit_auth.md +++ b/docs/stackit_auth.md @@ -31,6 +31,7 @@ stackit auth [flags] * [stackit](./stackit.md) - Manage STACKIT resources using the command line * [stackit auth activate-service-account](./stackit_auth_activate-service-account.md) - Authenticates using a service account +* [stackit auth get-access-token](./stackit_auth_get-access-token.md) - Prints a short-lived access token. * [stackit auth login](./stackit_auth_login.md) - Logs in to the STACKIT CLI * [stackit auth logout](./stackit_auth_logout.md) - Logs the user account out of the STACKIT CLI diff --git a/docs/stackit_auth_get-access-token.md b/docs/stackit_auth_get-access-token.md new file mode 100644 index 000000000..cc5218002 --- /dev/null +++ b/docs/stackit_auth_get-access-token.md @@ -0,0 +1,40 @@ +## stackit auth get-access-token + +Prints a short-lived access token. + +### Synopsis + +Prints a short-lived access token which can be used e.g. for API calls. + +``` +stackit auth get-access-token [flags] +``` + +### Examples + +``` + Print a short-lived access token + $ stackit auth get-access-token +``` + +### Options + +``` + -h, --help Help for "stackit auth get-access-token" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit auth](./stackit_auth.md) - Authenticates the STACKIT CLI + diff --git a/internal/cmd/auth/auth.go b/internal/cmd/auth/auth.go index 5513451d0..2a8b3a7f2 100644 --- a/internal/cmd/auth/auth.go +++ b/internal/cmd/auth/auth.go @@ -2,6 +2,7 @@ package auth import ( activateserviceaccount "github.com/stackitcloud/stackit-cli/internal/cmd/auth/activate-service-account" + getaccesstoken "github.com/stackitcloud/stackit-cli/internal/cmd/auth/get-access-token" "github.com/stackitcloud/stackit-cli/internal/cmd/auth/login" "github.com/stackitcloud/stackit-cli/internal/cmd/auth/logout" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -27,4 +28,5 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) { cmd.AddCommand(login.NewCmd(p)) cmd.AddCommand(logout.NewCmd(p)) cmd.AddCommand(activateserviceaccount.NewCmd(p)) + cmd.AddCommand(getaccesstoken.NewCmd(p)) } diff --git a/internal/cmd/auth/get-access-token/get_access_token.go b/internal/cmd/auth/get-access-token/get_access_token.go new file mode 100644 index 000000000..cbaa82eb6 --- /dev/null +++ b/internal/cmd/auth/get-access-token/get_access_token.go @@ -0,0 +1,50 @@ +package getaccesstoken + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "get-access-token", + Short: "Prints a short-lived access token.", + Long: "Prints a short-lived access token which can be used e.g. for API calls.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Print a short-lived access token`, + "$ stackit auth get-access-token"), + ), + RunE: func(_ *cobra.Command, _ []string) error { + userSessionExpired, err := auth.UserSessionExpired() + if err != nil { + return err + } + if userSessionExpired { + return &cliErr.SessionExpiredError{} + } + + accessToken, err := auth.GetAccessToken() + if err != nil { + return err + } + + accessTokenExpired, err := auth.TokenExpired(accessToken) + if err != nil { + return err + } + if accessTokenExpired { + return &cliErr.AccessTokenExpiredError{} + } + + p.Info("%s\n", accessToken) + return nil + }, + } + return cmd +} diff --git a/internal/pkg/auth/auth.go b/internal/pkg/auth/auth.go index a036227ed..fdd64354d 100644 --- a/internal/pkg/auth/auth.go +++ b/internal/pkg/auth/auth.go @@ -31,7 +31,7 @@ func AuthenticationConfig(p *print.Printer, reauthorizeUserRoutine func(p *print return nil, fmt.Errorf("authentication flow not set") } - userSessionExpired, err := userSessionExpired() + userSessionExpired, err := UserSessionExpired() if err != nil { return nil, fmt.Errorf("check if user session expired: %w", err) } @@ -42,7 +42,7 @@ func AuthenticationConfig(p *print.Printer, reauthorizeUserRoutine func(p *print if userSessionExpired { return nil, fmt.Errorf("session expired") } - accessToken, err := getAccessToken() + accessToken, err := GetAccessToken() if err != nil { return nil, fmt.Errorf("get service account access token: %w", err) } @@ -73,7 +73,7 @@ func AuthenticationConfig(p *print.Printer, reauthorizeUserRoutine func(p *print return authCfgOption, nil } -func userSessionExpired() (bool, error) { +func UserSessionExpired() (bool, error) { sessionExpiresAtString, err := GetAuthField(SESSION_EXPIRES_AT_UNIX) if err != nil { return false, fmt.Errorf("get %s: %w", SESSION_EXPIRES_AT_UNIX, err) @@ -87,7 +87,7 @@ func userSessionExpired() (bool, error) { return now.After(sessionExpiresAt), nil } -func getAccessToken() (string, error) { +func GetAccessToken() (string, error) { accessToken, err := GetAuthField(ACCESS_TOKEN) if err != nil { return "", fmt.Errorf("get %s: %w", ACCESS_TOKEN, err) diff --git a/internal/pkg/auth/user_token_flow.go b/internal/pkg/auth/user_token_flow.go index 93d10b13a..8a49c6b45 100644 --- a/internal/pkg/auth/user_token_flow.go +++ b/internal/pkg/auth/user_token_flow.go @@ -44,7 +44,7 @@ func (utf *userTokenFlow) RoundTrip(req *http.Request) (*http.Response, error) { } accessTokenValid := false - accessTokenExpired, err := tokenExpired(utf.accessToken) + accessTokenExpired, err := TokenExpired(utf.accessToken) if err != nil { return nil, fmt.Errorf("check if access token has expired: %w", err) } else if !accessTokenExpired { @@ -108,7 +108,7 @@ func reauthenticateUser(utf *userTokenFlow) error { return nil } -func tokenExpired(token string) (bool, error) { +func TokenExpired(token string) (bool, error) { // We can safely use ParseUnverified because we are not authenticating the user at this point. // We're just checking the expiration time tokenParsed, _, err := jwt.NewParser().ParseUnverified(token, &jwt.RegisteredClaims{}) diff --git a/internal/pkg/errors/errors.go b/internal/pkg/errors/errors.go index e67efde5b..9c83fb4f1 100644 --- a/internal/pkg/errors/errors.go +++ b/internal/pkg/errors/errors.go @@ -32,6 +32,22 @@ You can authenticate as a user by running: or use a service account by running: $ stackit auth activate-service-account` + SESSION_EXPIRED = `Session is expired. Please log in again first. + +You can authenticate as a user by running: +$ stackit auth login + +or use a service account by running: +$ stackit auth activate-service-account` + + ACCESS_TOKEN_EXPIRED = `Access token is expired. Please log in again first. + +You can authenticate as a user by running: +$ stackit auth login + +or use a service account by running: +$ stackit auth activate-service-account` + FAILED_SERVICE_ACCOUNT_ACTIVATION = `could not setup authentication based on the provided service account credentials. Please double check if they are correctly configured. @@ -230,6 +246,18 @@ func (e *AuthError) Error() string { return FAILED_AUTH } +type SessionExpiredError struct{} + +func (e *SessionExpiredError) Error() string { + return SESSION_EXPIRED +} + +type AccessTokenExpiredError struct{} + +func (e *AccessTokenExpiredError) Error() string { + return ACCESS_TOKEN_EXPIRED +} + type ActivateServiceAccountError struct{} func (e *ActivateServiceAccountError) Error() string {