Skip to content

Commit

Permalink
feat: token-based login
Browse files Browse the repository at this point in the history
  • Loading branch information
Enda Phelan committed Dec 8, 2020
1 parent c9805af commit bd562a3
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 6 deletions.
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Go to [releases](https://github.com/bf2fc6cc711aee1a0c2a/cli/releases) to downlo

## Getting Started

1. Login to RHOAS
### Login to RHOAS

```shell
rhoas login --insecure
Expand All @@ -20,7 +20,19 @@ This will redirect you to log in to https://sso.redhat.com/realms/redhat-externa
rhoas login
```

2. Use available Kafka commands
> NOTE: Work is ongoing to get a rhoas-cli client on Red Hat SSO. Until then you will not be able to interact with the control plane using this login flow. To workaround this, please use token-based login, which will be removed as soon as a client is available.
### Login with offline token

This login flow will not be available in the official release of the RHOAS CLI, but should be used to login to https://sso.redhat.com for now if you want to interact with the control plane API.

```shell
rhoas login --token $TOKEN
```

> NOTE: You can obtain an offline token from [cloud.redhat.com](https://cloud.redhat.com/openshift/token)
### Use available Kafka commands

```
rhoas kafka
Expand Down
85 changes: 83 additions & 2 deletions pkg/cmd/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/bf2fc6cc711aee1a0c2a/cli/pkg/browser"
"github.com/bf2fc6cc711aee1a0c2a/cli/pkg/connection"
"github.com/dgrijalva/jwt-go"

"github.com/bf2fc6cc711aee1a0c2a/cli/pkg/auth/pkce"
"github.com/bf2fc6cc711aee1a0c2a/cli/pkg/config"
Expand Down Expand Up @@ -73,6 +74,7 @@ var args struct {
authURL string
clientID string
insecureSkipTLSVerify bool
token string
}

// NewLoginCmd gets the command that's log the user in
Expand All @@ -87,6 +89,8 @@ func NewLoginCmd() *cobra.Command {
cmd.Flags().StringVar(&args.url, "url", stagingURL, "URL of the API gateway. The value can be the complete URL or an alias. The valid aliases are 'production', 'staging', 'integration', 'development' and their shorthands.")
cmd.Flags().BoolVar(&args.insecureSkipTLSVerify, "insecure", false, "Enables insecure communication with the server. This disables verification of TLS certificates and host names.")
cmd.Flags().StringVar(&args.clientID, "client-id", defaultClientID, "OpenID client identifier.")
cmd.Flags().StringVar(&args.authURL, "auth-url", connection.DefaultAuthURL, "SSO Authentication server")
cmd.Flags().StringVarP(&args.token, "token", "t", "", "access token that can be used for login")

return cmd
}
Expand All @@ -95,6 +99,8 @@ func NewLoginCmd() *cobra.Command {
func runLogin(cmd *cobra.Command, _ []string) error {
cfg, _ := config.Load()
cfg.SetInsecure(args.insecureSkipTLSVerify)
cfg.SetClientID(args.clientID)
cfg.SetAuthURL(args.authURL)

// If the value of the `--url` is any of the aliases then replace it with the corresponding
// real URL:
Expand All @@ -111,14 +117,17 @@ func runLogin(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("Scheme missing from URL '%v'. Please add either 'https' or 'https'.", unparsedGatewayURL)
}

authURL := connection.DefaultAuthURL
if args.token != "" {
cfg.SetURL(gatewayURL.String())
return loginWithToken(args.token, cfg)
}

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

parentCtx, cancel := context.WithCancel(context.Background())
ctx := oidc.ClientContext(parentCtx, httpClient)
provider, err := oidc.NewProvider(ctx, authURL)
provider, err := oidc.NewProvider(ctx, args.authURL)
if err != nil {
return err
}
Expand Down Expand Up @@ -161,6 +170,8 @@ func runLogin(cmd *cobra.Command, _ []string) error {
Addr: redirectURL.Host,
}

fmt.Fprintln(os.Stderr, "Logging in...")

sm.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, authCodeURL, http.StatusFound)
})
Expand Down Expand Up @@ -239,3 +250,73 @@ func createTransport(insecure bool) *http.Transport {
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
}
}

// tokenType extracts the value of the `typ` claim. It returns the value as a string, or the empty
// string if there is no such claim.
func tokenType(token *jwt.Token) (typ string, err error) {
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
err = fmt.Errorf("expected map claims but got %T", claims)
return
}
claim, ok := claims["typ"]
if !ok {
return
}
value, ok := claim.(string)
if !ok {
err = fmt.Errorf("expected string 'typ' but got %T", claim)
return
}
typ = value
return
}

func loginWithToken(token string, cfg *config.Config) error {
fmt.Fprintln(os.Stderr, "Logging in...")
var parsedToken *jwt.Token
parser := new(jwt.Parser)
parsedToken, _, err := parser.ParseUnverified(token, jwt.MapClaims{})
if err != nil {
return fmt.Errorf("Can't parse token '%s': %w", args.token, err)
}
tokenType, err := tokenType(parsedToken)
if err != nil {
return fmt.Errorf("Can't extract type from 'typ' claim of token '%s': %w", token, err)
}

switch tokenType {
case "Bearer":
cfg.AccessToken = args.token
case "Refresh", "Offline":
cfg.RefreshToken = args.token
case "":
return fmt.Errorf("Don't know how to handle empty type in token '%s'", args.token)
default:
return fmt.Errorf("Don't know how to handle token type '%s' in token '%s'", tokenType, args.token)
}

// Create a connection and get the token to verify that the crendentials are correct:
connection, err := cfg.Connection()
if err != nil {
return fmt.Errorf("Can't create connection: %w", err)
}
accessToken, refreshToken, err := connection.RefreshTokens(context.TODO())
if err != nil {
return fmt.Errorf("Can't get token: %w", err)
}
cfg.SetAccessToken(accessToken)
cfg.SetRefreshToken(refreshToken)
cfg.SetClientID(args.clientID)
cfg.SetInsecure(args.insecureSkipTLSVerify)
cfg.SetAccessToken(accessToken)
cfg.SetRefreshToken(refreshToken)
err = config.Save(cfg)
if err != nil {
return fmt.Errorf("Unable to save config: %w", err)
}

fmt.Fprintln(os.Stderr, "Successfully logged in to RHOAS")

return nil
}
10 changes: 10 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Config struct {
RefreshToken string `json:"refresh_token,omitempty" doc:"Offline or refresh token."`
Services ServiceConfigMap `json:"services"`
URL string `json:"url,omitempty" doc:"URL of the API gateway. The value can be the complete URL or an alias. The valid aliases are 'production', 'staging' and 'integration'."`
AuthURL string `json:"auth_url" doc:"URL of the authentication server"`
ClientID string `json:"client_id,omitempty" doc:"OpenID client identifier."`
Insecure bool `json:"insecure,omitempty" doc:"Enables insecure communication with the server. This disables verification of TLS certificates and host names."`
Scopes []string `json:"scopes,omitempty" doc:"OpenID scope. If this option is used it will replace completely the default scopes. Can be repeated multiple times to specify multiple scopes."`
Expand Down Expand Up @@ -56,6 +57,10 @@ func (c *Config) SetURL(url string) {
c.URL = url
}

func (c *Config) SetAuthURL(authURL string) {
c.AuthURL = authURL
}

func (c *Config) SetInsecure(insecure bool) {
c.Insecure = insecure
}
Expand Down Expand Up @@ -175,6 +180,11 @@ func (c *Config) Connection() (conn *connection.Connection, err error) {
if c.URL != "" {
builder.WithURL(c.URL)
}
if c.AuthURL == "" {
c.AuthURL = connection.DefaultAuthURL
}
builder.WithAuthURL(c.AuthURL)

builder.WithInsecure(c.Insecure)

conn, err = builder.Build()
Expand Down
8 changes: 7 additions & 1 deletion pkg/connection/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Builder struct {
clientID string
scopes []string
apiURL string
authURL string
transportWrapper TransportWrapper
}

Expand Down Expand Up @@ -68,6 +69,11 @@ func (b *Builder) WithURL(url string) *Builder {
return b
}

func (b *Builder) WithAuthURL(authURL string) *Builder {
b.authURL = authURL
return b
}

func (b *Builder) WithClientID(clientID string) *Builder {
b.clientID = clientID
return b
Expand Down Expand Up @@ -138,7 +144,7 @@ func (b *Builder) BuildContext(ctx context.Context) (connection *Connection, err
return
}

authURL, err := url.Parse(DefaultAuthURL)
authURL, err := url.Parse(b.authURL)
if err != nil {
err = fmt.Errorf("can't parse Auth URL '%s': %w", DefaultAuthURL, err)
return
Expand Down
2 changes: 1 addition & 1 deletion pkg/connection/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
// Default values:
const (
// #nosec G101
DefaultAuthURL = "https://sso.qa.redhat.com/auth/realms/redhat-external"
DefaultAuthURL = "https://sso.redhat.com/auth/realms/redhat-external"
DefaultClientID = "rhoas-cli"
DefaultURL = "https://api.openshift.com"
)
Expand Down

0 comments on commit bd562a3

Please sign in to comment.