212 lines
7.0 KiB
Go
212 lines
7.0 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"strconv"
|
|
|
|
"github.com/99designs/keyring"
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/arn"
|
|
"github.com/aws/aws-sdk-go/aws/session"
|
|
"github.com/aws/aws-sdk-go/service/sts"
|
|
"github.com/stoggi/aws-oidc/provider"
|
|
|
|
"gopkg.in/alecthomas/kingpin.v2"
|
|
)
|
|
|
|
// AuthConfig defines a single OpenIDConnect provider
|
|
type AuthConfig struct {
|
|
// The name of the provider when definied in the TOML configuration file
|
|
Name string `toml:"name"`
|
|
|
|
// RoleARN the role in AWS that should be assumed with the identity token
|
|
RoleArn string `toml:"role_arn"`
|
|
|
|
// Duration in seconds that the temporary AWS credentials should last for
|
|
// Between 900 (15 minutes) and 43200 (12 hours)
|
|
Duration int64 `toml:"duration"`
|
|
|
|
// ProviderURL the endpoint that defines the OIDC provider.
|
|
// Should serve https://[ProviderURL]/.well-known/openid-configuration
|
|
ProviderURL string `toml:"provider_url"`
|
|
|
|
// ClientID configured with your OIDC provider
|
|
ClientID string `toml:"client_id"`
|
|
|
|
// ClientSecret should only be specified if your OIDC provider requires it.
|
|
// Normally with PKCE you don't require a client_secret.
|
|
ClientSecret string `toml:"client_secret"`
|
|
|
|
// DisablePKCE removes the code_challenge and code_verifier parameters of a
|
|
// proof key for code exchange OAuth flow. Only disbale this if your identity
|
|
// provider does not support PKCE.
|
|
DisablePKCE bool `toml:"disable_pkce"`
|
|
|
|
// DisableNonce removes a random nonce sent to the server, and added to the token
|
|
// This nonce is verified when the token is received by the command line app.
|
|
DisableNonce bool `toml:"disable_nonce"`
|
|
|
|
// AgentCommand contains the command and arguments that open a browser. The URL
|
|
// to be opened will be appended, or use a parameter of {} to substitute the URL.
|
|
AgentCommand []string `toml:"agent"`
|
|
}
|
|
|
|
// AwsCredentialHelperData for AWS credential process
|
|
// https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes
|
|
type AwsCredentialHelperData struct {
|
|
Version int `json:"Version"`
|
|
AccessKeyID string `json:"AccessKeyId"`
|
|
SecretAccessKey string `json:"SecretAccessKey"`
|
|
SessionToken string `json:"SessionToken"`
|
|
Expiration string `json:"Expiration,omitempty"`
|
|
}
|
|
|
|
func configureFlags(cmd *kingpin.CmdClause, authConfig *AuthConfig) {
|
|
|
|
cmd.Flag("role_arn", "The AWS role you want to assume").
|
|
Default(authConfig.RoleArn).
|
|
StringVar(&authConfig.RoleArn)
|
|
|
|
cmd.Flag("duration", "The duration to assume the role for in seconds").
|
|
Default(strconv.FormatInt(max(authConfig.Duration, 900), 10)).
|
|
Int64Var(&authConfig.Duration)
|
|
|
|
cmd.Flag("provider_url", "The OpenID Connect Provider URL").
|
|
Default(authConfig.ProviderURL).
|
|
StringVar(&authConfig.ProviderURL)
|
|
|
|
cmd.Flag("client_id", "The OpenID Connect Client ID").
|
|
Default(authConfig.ClientID).
|
|
StringVar(&authConfig.ClientID)
|
|
|
|
cmd.Flag("client_secret", "The OpenID Connect Client Secret").
|
|
StringVar(&authConfig.ClientSecret)
|
|
|
|
cmd.Flag("disable_pkce", "Disable the use of PKCE in the OIDC code flow").
|
|
BoolVar(&authConfig.DisablePKCE)
|
|
|
|
cmd.Flag("disable_nonce", "Disable the use of a nonce included and verified in the token").
|
|
BoolVar(&authConfig.DisableNonce)
|
|
|
|
cmd.Flag("agent", "The executable and arguments of the local browser to use").
|
|
StringsVar(&authConfig.AgentCommand)
|
|
}
|
|
|
|
// ConfigureAuth configures the auth command with arguments and flags
|
|
func ConfigureAuth(app *kingpin.Application, config *GlobalConfig) {
|
|
|
|
cmd := app.Command("auth", "Authenticate to the identity provider, and assume a role in AWS")
|
|
|
|
providers := append(config.AuthProvider, AuthConfig{Name: "default"})
|
|
|
|
for _, a := range providers {
|
|
authConfig := a
|
|
|
|
pcmd := cmd.Command(authConfig.Name, "Authenticate using the named profile in the config file")
|
|
configureFlags(pcmd, &authConfig)
|
|
|
|
pcmd.Action(func(c *kingpin.ParseContext) error {
|
|
if authConfig.ClientID == "" {
|
|
return fmt.Errorf("Missing ClientID for provider %s", authConfig.Name)
|
|
}
|
|
if _, err := url.ParseRequestURI(authConfig.ProviderURL); err != nil {
|
|
return fmt.Errorf("Missing ProviderURL, or invalid format for provider %s", authConfig.Name)
|
|
}
|
|
if len(authConfig.AgentCommand) == 0 {
|
|
return fmt.Errorf("Missing Agent command for provider %s", authConfig.Name)
|
|
}
|
|
if _, err := arn.Parse(authConfig.RoleArn); err != nil {
|
|
return fmt.Errorf("Missing RoleArn, or invalid format for provider %s", authConfig.Name)
|
|
}
|
|
|
|
AuthCommand(app, config, &authConfig)
|
|
return nil
|
|
})
|
|
|
|
if authConfig.Name == "default" {
|
|
pcmd.Default()
|
|
}
|
|
}
|
|
}
|
|
|
|
// AuthCommand executes the authentication with the selected OpenIDConnect provider
|
|
func AuthCommand(app *kingpin.Application, config *GlobalConfig, authConfig *AuthConfig) {
|
|
|
|
p := &provider.ProviderConfig{
|
|
ClientID: authConfig.ClientID,
|
|
ClientSecret: authConfig.ClientSecret,
|
|
ProviderURL: authConfig.ProviderURL,
|
|
PKCE: !authConfig.DisablePKCE,
|
|
Nonce: !authConfig.DisableNonce,
|
|
AgentCommand: authConfig.AgentCommand,
|
|
}
|
|
oauth2Token := provider.OAuth2Token{}
|
|
|
|
item, err := (*config.Keyring).Get(authConfig.ClientID)
|
|
if err != keyring.ErrKeyNotFound {
|
|
if err := json.Unmarshal(item.Data, &oauth2Token); err != nil {
|
|
// Log this error only, because we can attempt to recover by getting a new token
|
|
app.Errorf("Unable to unmarshal OAuth2Token from keychain: %v", err)
|
|
}
|
|
}
|
|
|
|
err = p.Authenticate(&oauth2Token)
|
|
app.FatalIfError(err, "Error authenticating with identity provider")
|
|
|
|
AWSCredentialsJSON, err := assumeRoleWithWebIdentity(authConfig, oauth2Token.IDToken)
|
|
app.FatalIfError(err, "Error assume role with web identity")
|
|
|
|
json, err := json.Marshal(&oauth2Token)
|
|
app.FatalIfError(err, "Error marshalling OAuth2 token")
|
|
err = (*config.Keyring).Set(keyring.Item{
|
|
Key: authConfig.ClientID,
|
|
Data: json,
|
|
Label: fmt.Sprintf("OAuth2 token for %s", authConfig.RoleArn),
|
|
Description: "OIDC OAuth2 Token",
|
|
})
|
|
app.FatalIfError(err, "Error storing OAuth2 Token in keychain")
|
|
|
|
fmt.Printf(AWSCredentialsJSON)
|
|
}
|
|
|
|
func assumeRoleWithWebIdentity(authConfig *AuthConfig, idToken string) (string, error) {
|
|
|
|
svc := sts.New(session.New())
|
|
|
|
input := &sts.AssumeRoleWithWebIdentityInput{
|
|
DurationSeconds: aws.Int64(authConfig.Duration),
|
|
RoleArn: aws.String(authConfig.RoleArn),
|
|
RoleSessionName: aws.String("aws-oidc"),
|
|
WebIdentityToken: aws.String(idToken),
|
|
}
|
|
|
|
assumeRoleResult, err := svc.AssumeRoleWithWebIdentity(input)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
expiry := *assumeRoleResult.Credentials.Expiration
|
|
credentialData := AwsCredentialHelperData{
|
|
Version: 1,
|
|
AccessKeyID: *assumeRoleResult.Credentials.AccessKeyId,
|
|
SecretAccessKey: *assumeRoleResult.Credentials.SecretAccessKey,
|
|
SessionToken: *assumeRoleResult.Credentials.SessionToken,
|
|
Expiration: expiry.Format("2006-01-02T15:04:05Z"),
|
|
}
|
|
|
|
credentialJSON, err := json.Marshal(&credentialData)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(credentialJSON), nil
|
|
}
|
|
|
|
func max(x, y int64) int64 {
|
|
if x > y {
|
|
return x
|
|
}
|
|
return y
|
|
}
|