Skip to content

Commit

Permalink
feat(login): add token-based login
Browse files Browse the repository at this point in the history
  • Loading branch information
Enda Phelan committed Mar 11, 2021
1 parent dba949b commit 41b769a
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 50 deletions.
2 changes: 1 addition & 1 deletion cmd/rhoas/pkged.go

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/commands/rhoas_login.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ $ rhoas login --print-sso-url
--mas-auth-url string The URL of the MAS-SSO Authentication server. (default "https://keycloak-edge-redhat-rhoam-user-sso.apps.mas-sso-stage.1gzl.s1.devshift.org/auth/realms/mas-sso-staging")
--print-sso-url Prints the console login URL, which you can use to log in to RHOAS from a different web browser. This is useful if you need to log in with different credentials than the credentials you used in your default web browser.
--scope stringArray Override the default OpenID scope. To specify multiple scopes, use a separate --scope for each scope. (default [openid])
-t, --token string Allows you to log in using an offline token, which can be obtained at https://cloud.redhat.com/openshift/token.
....

=== Options inherited from parent commands
Expand Down
7 changes: 7 additions & 0 deletions locales/cmd/login/active.en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ one = "The URL of the SSO Authentication server."
description = 'Description for the --auth-url flag'
one = "The URL of the MAS-SSO Authentication server."

[login.flag.token]
one = "Allows you to log in using an offline token, which can be obtained at https://cloud.redhat.com/openshift/token."

[login.flag.printSsoUrl]
description = 'Description for the --print-sso-url'
one = "Prints the console login URL, which you can use to log in to RHOAS from a different web browser. This is useful if you need to log in with different credentials than the credentials you used in your default web browser."
Expand Down Expand Up @@ -80,6 +83,10 @@ one = 'Redirected to callback URL: {{.URL}}'
description = 'Log in success message'
one = 'You are now logged in as "{{.Username}}"'

[login.log.info.loginSuccessNoUsername]
description = 'Log in success message'
one = 'You are now logged in'

[login.log.info.openSSOUrl]
description = 'Info message for opening auth URL instructions'
one = 'Open the following URL in your browser to log in:'
Expand Down
5 changes: 4 additions & 1 deletion locales/cmd/whoami/active.en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ description = 'Examples of how to use the command'
one = '''
# print current username
$ rhoas whoami
'''
'''

[whoami.log.info.tokenHasNoUsername]
one = 'Token has no username'
126 changes: 80 additions & 46 deletions pkg/cmd/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"fmt"
"net/http"
"net/url"
"os"

"github.com/bf2fc6cc711aee1a0c2a/cli/pkg/auth/login"
"github.com/bf2fc6cc711aee1a0c2a/cli/pkg/auth/token"
Expand All @@ -25,11 +24,12 @@ import (
)

const (
devURL = "http://localhost:8000"
productionURL = "https://api.openshift.com"
stagingURL = "https://api.stage.openshift.com"
integrationURL = "https://api-integration.6943.hive-integration.openshiftapps.com"
defaultClientID = "rhoas-cli-prod"
devURL = "http://localhost:8000"
productionURL = "https://api.openshift.com"
stagingURL = "https://api.stage.openshift.com"
integrationURL = "https://api-integration.6943.hive-integration.openshiftapps.com"
defaultClientID = "rhoas-cli-prod"
defaultOfflineTokenClientID = "cloud-services"
)

// When the value of the `--url` option is one of the keys of this map it will be replaced by the
Expand All @@ -48,9 +48,10 @@ var urlAliases = map[string]string{
}

type Options struct {
Config config.IConfig
Logger func() (logging.Logger, error)
IO *iostreams.IOStreams
Config config.IConfig
Logger func() (logging.Logger, error)
Connection factory.ConnectionFunc
IO *iostreams.IOStreams

url string
authURL string
Expand All @@ -59,14 +60,16 @@ type Options struct {
scopes []string
insecureSkipTLSVerify bool
printURL bool
offlineToken string
}

// NewLoginCmd gets the command that's log the user in
func NewLoginCmd(f *factory.Factory) *cobra.Command {
opts := &Options{
Config: f.Config,
Logger: f.Logger,
IO: f.IOStreams,
Config: f.Config,
Connection: f.Connection,
Logger: f.Logger,
IO: f.IOStreams,
}

cmd := &cobra.Command{
Expand All @@ -75,6 +78,9 @@ func NewLoginCmd(f *factory.Factory) *cobra.Command {
Long: localizer.MustLocalizeFromID("login.cmd.longDescription"),
Example: localizer.MustLocalizeFromID("login.cmd.example"),
RunE: func(cmd *cobra.Command, _ []string) error {
if opts.offlineToken != "" && opts.clientID == defaultClientID {
opts.clientID = defaultOfflineTokenClientID
}
return runLogin(opts)
},
}
Expand All @@ -86,6 +92,7 @@ func NewLoginCmd(f *factory.Factory) *cobra.Command {
cmd.Flags().StringVar(&opts.masAuthURL, "mas-auth-url", connection.DefaultMasAuthURL, localizer.MustLocalizeFromID("login.flag.masAuthUrl"))
cmd.Flags().BoolVar(&opts.printURL, "print-sso-url", false, localizer.MustLocalizeFromID("login.flag.printSsoUrl"))
cmd.Flags().StringArrayVar(&opts.scopes, "scope", connection.DefaultScopes, localizer.MustLocalizeFromID("login.flag.scope"))
cmd.Flags().StringVarP(&opts.offlineToken, "token", "t", "", localizer.MustLocalizeFromID("login.flag.token"))

return cmd
}
Expand Down Expand Up @@ -117,44 +124,51 @@ func runLogin(opts *Options) (err error) {
}))
}

tr := createTransport(opts.insecureSkipTLSVerify)
httpClient := &http.Client{Transport: tr}

loginExec := &login.AuthorizationCodeGrant{
HTTPClient: httpClient,
Scopes: opts.scopes,
Logger: logger,
IO: opts.IO,
Config: opts.Config,
ClientID: opts.clientID,
PrintURL: opts.printURL,
if opts.offlineToken == "" {
tr := createTransport(opts.insecureSkipTLSVerify)
httpClient := &http.Client{Transport: tr}

loginExec := &login.AuthorizationCodeGrant{
HTTPClient: httpClient,
Scopes: opts.scopes,
Logger: logger,
IO: opts.IO,
Config: opts.Config,
ClientID: opts.clientID,
PrintURL: opts.printURL,
}

ssoCfg := &login.SSOConfig{
AuthURL: opts.authURL,
RedirectPath: "sso-redhat-callback",
}

masSsoCfg := &login.SSOConfig{
AuthURL: opts.masAuthURL,
RedirectPath: "mas-sso-callback",
}

if err = loginExec.Execute(context.Background(), ssoCfg, masSsoCfg); err != nil {
return err
}
}

ssoCfg := &login.SSOConfig{
AuthURL: opts.authURL,
RedirectPath: "sso-redhat-callback",
}

masSsoCfg := &login.SSOConfig{
AuthURL: opts.masAuthURL,
RedirectPath: "mas-sso-callback",
}

if err = loginExec.Execute(context.Background(), ssoCfg, masSsoCfg); err != nil {
return err
if opts.offlineToken != "" {
if err = loginWithOfflineToken(opts); err != nil {
return err
}
}

cfg, err := opts.Config.Load()
if err != nil {
logger.Error(err)
os.Exit(1)
return err
}

cfg.APIUrl = gatewayURL.String()
cfg.Insecure = opts.insecureSkipTLSVerify
cfg.ClientID = opts.clientID
cfg.AuthURL = opts.authURL
cfg.MasAuthURL = opts.masAuthURL
cfg.APIUrl = gatewayURL.String()
cfg.Scopes = opts.scopes

if err = opts.Config.Save(cfg); err != nil {
Expand All @@ -163,19 +177,39 @@ func runLogin(opts *Options) (err error) {

username, ok := token.GetUsername(cfg.AccessToken)
if !ok {
username = "unknown"
logger.Info("\n", localizer.MustLocalizeFromID("login.log.info.loginSuccessNoUsername"))
} else {
logger.Info("\n", localizer.MustLocalize(&localizer.Config{
MessageID: "login.log.info.loginSuccess",
TemplateData: map[string]interface{}{
"Username": username,
},
}))
}

logger.Info("\n", localizer.MustLocalize(&localizer.Config{
MessageID: "login.log.info.loginSuccess",
TemplateData: map[string]interface{}{
"Username": username,
},
}))

return nil
}

func loginWithOfflineToken(opts *Options) (err error) {
cfg, err := opts.Config.Load()
if err != nil {
return err
}
cfg.Insecure = opts.insecureSkipTLSVerify
cfg.ClientID = opts.clientID
cfg.AuthURL = opts.authURL
cfg.MasAuthURL = opts.masAuthURL
cfg.Scopes = opts.scopes
cfg.RefreshToken = opts.offlineToken

if err = opts.Config.Save(cfg); err != nil {
return err
}

_, err = opts.Connection(connection.DefaultConfigSkipMasAuth)
return err
}

func createTransport(insecure bool) *http.Transport {
// #nosec 402
return &http.Transport{
Expand Down
16 changes: 14 additions & 2 deletions pkg/cmd/whoami/whoami.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/bf2fc6cc711aee1a0c2a/cli/pkg/cmd/factory"
"github.com/bf2fc6cc711aee1a0c2a/cli/pkg/connection"
"github.com/bf2fc6cc711aee1a0c2a/cli/pkg/iostreams"
"github.com/bf2fc6cc711aee1a0c2a/cli/pkg/logging"

"github.com/spf13/cobra"
)
Expand All @@ -17,13 +18,15 @@ type Options struct {
Config config.IConfig
Connection factory.ConnectionFunc
IO *iostreams.IOStreams
Logger func() (logging.Logger, error)
}

func NewWhoAmICmd(f *factory.Factory) *cobra.Command {
opts := &Options{
Config: f.Config,
Connection: f.Connection,
IO: f.IOStreams,
Logger: f.Logger,
}

cmd := &cobra.Command{
Expand All @@ -45,6 +48,11 @@ func runCmd(opts *Options) (err error) {
return err
}

logger, err := opts.Logger()
if err != nil {
return err
}

_, err = opts.Connection(connection.DefaultConfigSkipMasAuth)
if err != nil {
return err
Expand All @@ -54,9 +62,13 @@ func runCmd(opts *Options) (err error) {

tknClaims, _ := token.MapClaims(accessTkn)

userName, _ := tknClaims["preferred_username"]
userName, ok := tknClaims["preferred_username"]

fmt.Fprintln(opts.IO.Out, userName)
if ok {
fmt.Fprintln(opts.IO.Out, userName)
} else {
logger.Info(localizer.MustLocalizeFromID("whoami.log.info.tokenHasNoUsername"))
}

return nil
}

0 comments on commit 41b769a

Please sign in to comment.