Skip to content

Commit

Permalink
fedramp: Allow login and refresh tokens
Browse files Browse the repository at this point in the history
To allow users to login against the development FedRAMP GovCloud
environment, we add a dependency on the AWS Cognito service. Whenever an
operation is requested, ROSA will use the refresh token to fetch a new
access token. This way it bypasses the OCM SDK refresh mechanism as the
token is always valid.
  • Loading branch information
vkareh committed May 19, 2022
1 parent 100a57a commit 02d1ba0
Show file tree
Hide file tree
Showing 14 changed files with 35,260 additions and 60 deletions.
126 changes: 89 additions & 37 deletions cmd/login/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ import (
"github.com/spf13/cobra"

"github.com/openshift/rosa/cmd/logout"
"github.com/openshift/rosa/pkg/fedramp"
"github.com/openshift/rosa/pkg/interactive"
"github.com/openshift/rosa/pkg/logging"
"github.com/openshift/rosa/pkg/ocm"
rprtr "github.com/openshift/rosa/pkg/reporter"
)

// #nosec G101
const uiTokenPage = "https://console.redhat.com/openshift/token/rosa"
var uiTokenPage string = "https://console.redhat.com/openshift/token/rosa"

var reAttempt bool

Expand All @@ -58,9 +59,8 @@ var Cmd = &cobra.Command{
"\t3. Environment variable (OCM_TOKEN)\n"+
"\t4. Configuration file\n"+
"\t5. Command-line prompt\n", uiTokenPage),
Example: " # Login to the OpenShift API with an existing token generated from " +
`https://console.redhat.com/openshift/token/rosa
rosa login --token=$OFFLINE_ACCESS_TOKEN`,
Example: fmt.Sprintf(` # Login to the OpenShift API with an existing token generated from %s
rosa login --token=$OFFLINE_ACCESS_TOKEN`, uiTokenPage),
Run: run,
}

Expand Down Expand Up @@ -110,7 +110,7 @@ func init() {
"token",
"t",
"",
"Access or refresh token generated from https://console.redhat.com/openshift/token/rosa.",
fmt.Sprintf("Access or refresh token generated from %s.", uiTokenPage),
)
flags.BoolVar(
&args.insecure,
Expand All @@ -119,14 +119,16 @@ func init() {
"Enables insecure communication with the server. This disables verification of TLS "+
"certificates and host names.",
)
fedramp.AddFlag(flags)
}

func run(cmd *cobra.Command, argv []string) {
reporter := rprtr.CreateReporterOrExit()
logger := logging.CreateLoggerOrExit(reporter)

// Check mandatory options:
if args.env == "" {
env := args.env
if env == "" {
reporter.Errorf("Option '--env' is mandatory")
os.Exit(1)
}
Expand All @@ -142,6 +144,15 @@ func run(cmd *cobra.Command, argv []string) {
}

token := args.token

if cfg.FedRAMP || fedramp.Enabled() {
cfg.FedRAMP = true
fedramp.Enable()
if cfg.RefreshToken != "" {
token = cfg.RefreshToken
}
}

haveReqs := token != ""

// Verify environment variables:
Expand All @@ -163,6 +174,16 @@ func run(cmd *cobra.Command, argv []string) {
haveReqs = armed
}

var frEnv string
if fedramp.Enabled() {
frEnv, err = ocm.GetEnv()
if err != nil {
reporter.Errorf("%s", err)
os.Exit(1)
}
uiTokenPage = fedramp.TokenPage[frEnv]
}

// Prompt the user for token:
if !haveReqs {
fmt.Println("To login to your Red Hat account, get an offline access token at", uiTokenPage)
Expand All @@ -182,6 +203,41 @@ func run(cmd *cobra.Command, argv []string) {
os.Exit(1)
}

if token != "" {
if fedramp.Enabled() || fedramp.IsCognitoToken(token) {
cfg.AccessToken = ""
cfg.RefreshToken = token
cfg.FedRAMP = true
fedramp.Enable()
} else {
// If a token has been provided parse it:
parser := new(jwt.Parser)
jwtToken, _, err := parser.ParseUnverified(token, jwt.MapClaims{})
if err != nil {
reporter.Errorf("Failed to parse token '%s': %v", token, err)
os.Exit(1)
}

// Put the token in the place of the configuration that corresponds to its type:
typ, err := tokenType(jwtToken)
if err != nil {
reporter.Errorf("Failed to extract type from 'typ' claim of token '%s': %v", token, err)
os.Exit(1)
}
switch typ {
case "Bearer", "":
cfg.AccessToken = token
cfg.RefreshToken = ""
case "Refresh", "Offline":
cfg.AccessToken = ""
cfg.RefreshToken = token
default:
reporter.Errorf("Don't know how to handle token type '%s' in token '%s'", typ, token)
os.Exit(1)
}
}
}

// Apply the default OpenID details if not explicitly provided by the user:
tokenURL := sdk.DefaultTokenURL
if args.tokenURL != "" {
Expand All @@ -194,11 +250,28 @@ func run(cmd *cobra.Command, argv []string) {

// If the value of the `--env` is any of the aliases then replace it with the corresponding
// real URL:
gatewayURL, ok := ocm.URLAliases[args.env]
gatewayURL, ok := ocm.URLAliases[env]
if !ok {
gatewayURL = args.env
gatewayURL = env
}

if fedramp.Enabled() {
if env == sdk.DefaultURL {
env = "production"
}
tokenURL, ok = fedramp.TokenURLs[env]
if !ok {
tokenURL = args.tokenURL
}
clientID, ok = fedramp.ClientIDs[env]
if !ok {
clientID = args.clientID
}
gatewayURL, ok = fedramp.URLAliases[env]
if !ok {
gatewayURL = env
}
}
// Update the configuration with the values given in the command line:
cfg.TokenURL = tokenURL
cfg.ClientID = clientID
Expand All @@ -207,34 +280,6 @@ func run(cmd *cobra.Command, argv []string) {
cfg.URL = gatewayURL
cfg.Insecure = args.insecure

if token != "" {
// If a token has been provided parse it:
parser := new(jwt.Parser)
jwtToken, _, err := parser.ParseUnverified(token, jwt.MapClaims{})
if err != nil {
reporter.Errorf("Failed to parse token '%s': %v", token, err)
os.Exit(1)
}

// Put the token in the place of the configuration that corresponds to its type:
typ, err := tokenType(jwtToken)
if err != nil {
reporter.Errorf("Failed to extract type from 'typ' claim of token '%s': %v", token, err)
os.Exit(1)
}
switch typ {
case "Bearer", "":
cfg.AccessToken = token
cfg.RefreshToken = ""
case "Refresh", "Offline":
cfg.AccessToken = ""
cfg.RefreshToken = token
default:
reporter.Errorf("Don't know how to handle token type '%s' in token '%s'", typ, token)
os.Exit(1)
}
}

// Create a connection and get the token to verify that the crendentials are correct:
ocmClient, err := ocm.NewClient().
Config(cfg).
Expand All @@ -255,7 +300,14 @@ func run(cmd *cobra.Command, argv []string) {
reporter.Errorf("Failed to close OCM connection: %v", err)
}
}()
accessToken, refreshToken, err := ocmClient.GetConnectionTokens()
var accessToken string
var refreshToken string
if fedramp.Enabled() {
refreshToken = cfg.RefreshToken
accessToken, err = fedramp.GetAccessToken(frEnv, cfg.RefreshToken, cfg.ClientID)
} else {
accessToken, refreshToken, err = ocmClient.GetConnectionTokens()
}
if err != nil {
reporter.Errorf("Failed to get token. Your session might be expired: %v", err)
reporter.Infof("Get a new offline access token at %s", uiTokenPage)
Expand Down
34 changes: 24 additions & 10 deletions pkg/aws/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
"github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
"github.com/aws/aws-sdk-go/service/cognitoidentityprovider/cognitoidentityprovideriface"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
"github.com/aws/aws-sdk-go/service/iam"
Expand Down Expand Up @@ -135,13 +137,15 @@ type Client interface {
IsAdminRole(roleName string) (bool, error)
DeleteInlineRolePolicies(roleName string) error
IsUserRole(roleName *string) (bool, error)
GetAccessToken(refreshToken string, clientID string) (string, error)
}

// ClientBuilder contains the information and logic needed to build a new AWS client.
type ClientBuilder struct {
logger *logrus.Logger
region *string
credentials *AccessKey
logger *logrus.Logger
region *string
credentials *AccessKey
skipcallerid bool
}

type awsClient struct {
Expand All @@ -152,6 +156,7 @@ type awsClient struct {
stsClient stsiface.STSAPI
cfClient cloudformationiface.CloudFormationAPI
servicequotasClient servicequotasiface.ServiceQuotasAPI
cognitoClient cognitoidentityprovideriface.CognitoIdentityProviderAPI
awsSession *session.Session
awsAccessKeys *AccessKey
}
Expand Down Expand Up @@ -181,6 +186,7 @@ func New(
stsClient stsiface.STSAPI,
cfClient cloudformationiface.CloudFormationAPI,
servicequotasClient servicequotasiface.ServiceQuotasAPI,
cognitoClient cognitoidentityprovideriface.CognitoIdentityProviderAPI,
awsSession *session.Session,
awsAccessKeys *AccessKey,

Expand All @@ -193,6 +199,7 @@ func New(
stsClient,
cfClient,
servicequotasClient,
cognitoClient,
awsSession,
awsAccessKeys,
}
Expand All @@ -209,6 +216,11 @@ func (b *ClientBuilder) Region(value string) *ClientBuilder {
return b
}

func (b *ClientBuilder) SkipCallerIdentity(value bool) *ClientBuilder {
b.skipcallerid = value
return b
}

func (b *ClientBuilder) AccessKeys(value *AccessKey) *ClientBuilder {
// fmt.Printf("Using new access key %s\n", value.AccessKeyID)
b.credentials = value
Expand Down Expand Up @@ -325,16 +337,18 @@ func (b *ClientBuilder) Build() (Client, error) {
stsClient: sts.New(sess),
cfClient: cloudformation.New(sess),
servicequotasClient: servicequotas.New(sess),
cognitoClient: cognitoidentityprovider.New(sess),
awsSession: sess,
}

_, root, err := getClientDetails(c)
if err != nil {
return nil, err
}

if root {
return nil, errors.New("using a root account is not supported, please use an IAM user instead")
if !b.skipcallerid {
_, root, err := getClientDetails(c)
if err != nil {
return nil, err
}
if root {
return nil, errors.New("using a root account is not supported, please use an IAM user instead")
}
}

return c, err
Expand Down
37 changes: 37 additions & 0 deletions pkg/aws/cognito.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Copyright (c) 2022 Red Hat, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package aws

import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
)

func (c *awsClient) GetAccessToken(refreshToken string, clientID string) (string, error) {
input := &cognitoidentityprovider.InitiateAuthInput{
AuthFlow: aws.String(cognitoidentityprovider.AuthFlowTypeRefreshTokenAuth),
AuthParameters: map[string]*string{
cognitoidentityprovider.AuthFlowTypeRefreshToken: aws.String(refreshToken),
},
ClientId: aws.String(clientID),
}
output, err := c.cognitoClient.InitiateAuth(input)
if err != nil {
return "", err
}
return aws.StringValue(output.AuthenticationResult.IdToken), nil
}
Loading

0 comments on commit 02d1ba0

Please sign in to comment.