Added TOML cofiguration file support
* configuration file located at ~/.aws-oidc/config * sets default parameters, but can still be overridden on the cli * named AuthProviders are accessible via the auth [name] command Renamed exec command to auth. Upgraded auth command to take defaults from the config file. Added new command exec, that puts the temporary credentials as environment variables in the specified command Automatically append URL to end of auth command if not specified
This commit is contained in:
parent
c548dcfd72
commit
f8a7c0986f
75
README.md
75
README.md
@ -6,58 +6,69 @@ It outputs temporary AWS credentials in a JSON format that can be consumed by th
|
||||
|
||||
https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes
|
||||
|
||||
OneLogin Example:
|
||||
Example:
|
||||
|
||||
aws-oidc exec \
|
||||
--role_arn="arn:aws:iam::892845094662:role/onelogin-test-oidc" \
|
||||
--provider_url=https://openid-connect.onelogin.com/oidc \
|
||||
--client_id=97a61160-3c09-0137-8c69-0a1c3f4fd822144813 \
|
||||
--pkce \
|
||||
--nonce \
|
||||
-- open -b com.google.chrome -n --args --profile-directory=Default {}
|
||||
aws-oidc auth \
|
||||
--role_arn="arn:aws:iam::892845094662:role/onelogin-test-oidc" \
|
||||
--duration=3600 \
|
||||
--provider_url=https://openid-connect.onelogin.com/oidc \
|
||||
--client_id=97a61160-3c09-0137-8c69-0a1c3f4fd822144813 \
|
||||
--agent=open
|
||||
|
||||
Google Example:
|
||||
All the provider arguments can be specified in a TOML configuration file:
|
||||
|
||||
aws-oidc exec \
|
||||
--role_arn="arn:aws:iam::892845094662:role/web-identity-lanbda" \
|
||||
--provider_url=https://accounts.google.com \
|
||||
--client_id=430784603061-osbtei3s71l0bj6d8oegto0itefjmiq6.apps.googleusercontent.com \
|
||||
--client_secret=... \
|
||||
--pkce \
|
||||
--nonce \
|
||||
-- open -b com.google.chrome -n --args --profile-directory=Default {}
|
||||
region = "ap-southeast-2"
|
||||
|
||||
For some reason, even when using PKCE, google need a client_secret for applications not registered as Android, iOS or Chrome.
|
||||
[[AuthProvider]]
|
||||
name = "onelogin"
|
||||
role_arn = "arn:aws:iam::012345678901:role/role-name"
|
||||
duration = 900
|
||||
provider_url = "https://openid-connect.onelogin.com/oidc"
|
||||
client_id = "ef061080-43aa-0137-62f3-066d8813aeb888900"
|
||||
agent = ["open", "-b", "com.google.chrome", "-n", "--args", "--profile-directory=Default", "{}"]
|
||||
|
||||
[[AuthProvider]]
|
||||
name = "google"
|
||||
role_arn = "arn:aws:iam::012345678901:role/role-name"
|
||||
duration = 900
|
||||
provider_url = "https://accounts.google.com"
|
||||
client_id = "430784603061-osbtei3s71l0bj6d8oegto0itefjmiq6.apps.googleusercontent.com"
|
||||
agent = ["open", "-b", "com.google.chrome", "-n", "--args", "--profile-directory=Profile 1", "{}"]
|
||||
|
||||
This configuration file should be located in **~/.aws-oidc/config**
|
||||
|
||||
## Configure AWS Config
|
||||
|
||||
~/.aws/config
|
||||
Add the profiles for each role you want to assume to **~/.aws/config**. Specify the provider name from the configuration file, and override any default settings:
|
||||
|
||||
[profile oidc]
|
||||
credential_process = aws-oidc exec --role_arn=arn:aws:iam::892845094662:role/onelogin-test-oidc --provider_url=https://openid-connect.onelogin.com/oidc --client_id=97a61160-3c09-0137-8c69-0a1c3f4fd822144813 --pkce --nonce -- open -b com.google.chrome -n --args --profile-directory=Default {}
|
||||
[profile engineer]
|
||||
credential_process = aws-oidc auth onelogin --role_arn=arn:aws:iam::892845094662:role/onelogin-test-oidc --duration 7200
|
||||
|
||||
Now you can use the AWS cli as normal, and specify the profile:
|
||||
|
||||
$ aws --profile oidc sts get-caller-identity
|
||||
$ aws --profile engineer sts get-caller-identity
|
||||
{
|
||||
"UserId": "AROAJUTXNWXGCAEILMXTY:50904038",
|
||||
"Account": "892845094662",
|
||||
"Arn": "arn:aws:sts::892845094662:assumed-role/onelogin-test-oidc/50904038"
|
||||
}
|
||||
|
||||
## AWS Cognito
|
||||
## Run other commands with credentials
|
||||
|
||||
./aws-oidc exec \
|
||||
--provider_url=https://cognito-idp.us-west-2.amazonaws.com/us-west-2_eBYNmnpS9 \
|
||||
--client_id=70kdnvprlqf1daspkn0iikdngv \
|
||||
--pkce \
|
||||
--nonce \
|
||||
--no-reauth \
|
||||
-- open -b com.google.chrome -n --args --profile-directory=Default {}
|
||||
Most AWS SDK's should be able to pick up the profile parameter, and suppor the `credentials_process` setting in your **~/.aws/config** file. If not, you can run an arbitary command with the temporary credentials with `exec`:
|
||||
|
||||
aws-oidc exec engineer -- ./path/to/command with arguments
|
||||
|
||||
This will use the profiles defined in **~/.aws/config** to assume the role with `aws-oidc` and then set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables for the new process.
|
||||
|
||||
## Find roles that an oidc client could assume
|
||||
|
||||
aws-vault exec test-privileged-admin -- aws iam list-roles --query <<EOF '
|
||||
Use the `list` command to find roles that your claim and client_id can assume:
|
||||
|
||||
aws-oidc list --claim="openid-connect.onelogin.com/oidc:aud" --client_id="ef061080-43aa-0137-62f3-066d8813aeb888900"
|
||||
|
||||
Example using only the AWS CLI:
|
||||
aws iam list-roles --query <<EOF '
|
||||
Roles[?
|
||||
AssumeRolePolicyDocument.Statement[?
|
||||
Condition.StringEquals."openid-connect.onelogin.com/oidc:aud"
|
||||
@ -69,4 +80,4 @@ Now you can use the AWS cli as normal, and specify the profile:
|
||||
} | [?
|
||||
contains(ClientId, `ef061080-43aa-0137-62f3-066d8813aeb888900`)
|
||||
]'
|
||||
EOF
|
||||
EOF
|
||||
|
16
aws-oidc.go
16
aws-oidc.go
@ -1,16 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/stoggi/aws-oidc/cli"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
// Version is provided at compile time
|
||||
var Version = "dev"
|
||||
var labelText chan string
|
||||
|
||||
func main() {
|
||||
run(os.Args[1:], os.Exit)
|
||||
@ -23,12 +24,16 @@ func run(args []string, exit func(int)) {
|
||||
log.Fatalf("error opening file: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
log.SetOutput(f)
|
||||
log.Println("Starting...")
|
||||
wrt := io.MultiWriter(os.Stderr, f)
|
||||
log.SetOutput(wrt)
|
||||
|
||||
// Default configuration, values are overridden by command line options.
|
||||
config := cli.GlobalConfig{}
|
||||
if _, err := toml.DecodeFile(GetConfigFilePath(), &config); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
log.Printf("Error decoding TOML: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
app := kingpin.New(
|
||||
"aws-oidc",
|
||||
@ -38,9 +43,10 @@ func run(args []string, exit func(int)) {
|
||||
app.Version(Version)
|
||||
app.Terminate(exit)
|
||||
app.UsageWriter(os.Stdout)
|
||||
app.ErrorWriter(f)
|
||||
app.ErrorWriter(wrt)
|
||||
|
||||
cli.ConfigureGlobal(app, &config)
|
||||
cli.ConfigureAuth(app, &config)
|
||||
cli.ConfigureExec(app, &config)
|
||||
cli.ConfigureList(app, &config)
|
||||
|
||||
|
211
cli/auth.go
Normal file
211
cli/auth.go
Normal file
@ -0,0 +1,211 @@
|
||||
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
|
||||
}
|
234
cli/exec.go
234
cli/exec.go
@ -1,164 +1,140 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/99designs/keyring"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/sts"
|
||||
"github.com/stoggi/aws-oidc/provider"
|
||||
"golang.org/x/oauth2"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
// ExecConfig stores the parameters needed for an exec command
|
||||
type ExecConfig struct {
|
||||
RoleArn string
|
||||
Duration int64
|
||||
ProviderURL string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
PKCE bool
|
||||
Nonce bool
|
||||
ReAuth bool
|
||||
AgentCommand []string
|
||||
}
|
||||
|
||||
// json metadata for AWS credential process. Ref: 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"`
|
||||
}
|
||||
|
||||
type LambdaPayload struct {
|
||||
Role string `json:"role"`
|
||||
Token string `json:"token"`
|
||||
Profile string
|
||||
Command string
|
||||
Args []string
|
||||
Signals chan os.Signal
|
||||
}
|
||||
|
||||
// ConfigureExec configures the exec command with arguments and flags
|
||||
func ConfigureExec(app *kingpin.Application, config *GlobalConfig) {
|
||||
|
||||
execConfig := ExecConfig{}
|
||||
|
||||
cmd := app.Command("exec", "Execute a command with temporary AWS credentials")
|
||||
cmd := app.Command("exec", "Retrieve temporary credentials and set them as environment variables")
|
||||
|
||||
cmd.Default()
|
||||
cmd.Arg("profile", "Name of the profile").
|
||||
StringVar(&config.Profile)
|
||||
|
||||
cmd.Flag("role_arn", "The AWS role you want to assume").
|
||||
Required().
|
||||
StringVar(&execConfig.RoleArn)
|
||||
cmd.Arg("cmd", "Command to execute").
|
||||
Default(os.Getenv("SHELL")).
|
||||
StringVar(&execConfig.Command)
|
||||
|
||||
cmd.Flag("duration", "The duration to assume the role for in seconds").
|
||||
Default("3600").
|
||||
Int64Var(&execConfig.Duration)
|
||||
|
||||
cmd.Flag("provider_url", "The OpenID Connect Provider URL").
|
||||
Required().
|
||||
StringVar(&execConfig.ProviderURL)
|
||||
|
||||
cmd.Flag("client_id", "The OpenID Connect Client ID").
|
||||
Required().
|
||||
StringVar(&execConfig.ClientID)
|
||||
|
||||
cmd.Flag("client_secret", "The OpenID Connect Client Secret").
|
||||
Default("").
|
||||
StringVar(&execConfig.ClientSecret)
|
||||
|
||||
cmd.Flag("pkce", "Use PKCE in the OIDC code flow").
|
||||
Default("true").
|
||||
BoolVar(&execConfig.PKCE)
|
||||
|
||||
cmd.Flag("nonce", "Require a nonce included and verified in the token").
|
||||
Default("true").
|
||||
BoolVar(&execConfig.Nonce)
|
||||
|
||||
cmd.Flag("reauth", "Require reauthentication by the identity provider").
|
||||
Default("false").
|
||||
BoolVar(&execConfig.ReAuth)
|
||||
|
||||
cmd.Arg("agent", "The executable and arguments of the local browser to use").
|
||||
Default("open", "{}").
|
||||
StringsVar(&execConfig.AgentCommand)
|
||||
cmd.Arg("args", "Command arguments").
|
||||
StringsVar(&execConfig.Args)
|
||||
|
||||
cmd.Action(func(c *kingpin.ParseContext) error {
|
||||
execConfig.Signals = make(chan os.Signal)
|
||||
ExecCommand(app, config, &execConfig)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// ExecCommand retrieves temporary credentials and sets them as environment variables
|
||||
func ExecCommand(app *kingpin.Application, config *GlobalConfig, execConfig *ExecConfig) {
|
||||
|
||||
p := &provider.ProviderConfig{
|
||||
ClientID: execConfig.ClientID,
|
||||
ClientSecret: execConfig.ClientSecret,
|
||||
ProviderURL: execConfig.ProviderURL,
|
||||
PKCE: execConfig.PKCE,
|
||||
Nonce: execConfig.Nonce,
|
||||
ReAuth: execConfig.ReAuth,
|
||||
AgentCommand: execConfig.AgentCommand,
|
||||
if os.Getenv("AWS_OIDC") != "" {
|
||||
app.Fatalf("aws-vault sessions should be nested with care, unset $AWS_OIDC to force")
|
||||
return
|
||||
}
|
||||
oauth2Token := provider.OAuth2Token{}
|
||||
|
||||
item, err := (*config.Keyring).Get(execConfig.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)
|
||||
val, err := config.Session.Config.Credentials.Get()
|
||||
if err != nil {
|
||||
app.Fatalf("Unable to get credentials for profile: %s", config.Profile)
|
||||
}
|
||||
|
||||
env := environ(os.Environ())
|
||||
env.Set("AWS_OIDC", config.Profile)
|
||||
|
||||
env.Unset("AWS_ACCESS_KEY_ID")
|
||||
env.Unset("AWS_SECRET_ACCESS_KEY")
|
||||
env.Unset("AWS_CREDENTIAL_FILE")
|
||||
env.Unset("AWS_DEFAULT_PROFILE")
|
||||
env.Unset("AWS_PROFILE")
|
||||
|
||||
if config.Region != "" {
|
||||
log.Printf("Setting subprocess env: AWS_DEFAULT_REGION=%s, AWS_REGION=%s", config.Region, config.Region)
|
||||
env.Set("AWS_DEFAULT_REGION", config.Region)
|
||||
env.Set("AWS_REGION", config.Region)
|
||||
}
|
||||
|
||||
log.Println("Setting subprocess env: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY")
|
||||
env.Set("AWS_ACCESS_KEY_ID", val.AccessKeyID)
|
||||
env.Set("AWS_SECRET_ACCESS_KEY", val.SecretAccessKey)
|
||||
|
||||
if val.SessionToken != "" {
|
||||
log.Println("Setting subprocess env: AWS_SESSION_TOKEN, AWS_SECURITY_TOKEN")
|
||||
env.Set("AWS_SESSION_TOKEN", val.SessionToken)
|
||||
env.Set("AWS_SECURITY_TOKEN", val.SessionToken)
|
||||
}
|
||||
|
||||
cmd := exec.Command(execConfig.Command, execConfig.Args...)
|
||||
cmd.Env = env
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
signal.Notify(execConfig.Signals, os.Interrupt, os.Kill)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
app.Fatalf("%v", err)
|
||||
}
|
||||
// wait for the command to finish
|
||||
waitCh := make(chan error, 1)
|
||||
go func() {
|
||||
waitCh <- cmd.Wait()
|
||||
close(waitCh)
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case sig := <-execConfig.Signals:
|
||||
if err = cmd.Process.Signal(sig); err != nil {
|
||||
app.Errorf("%v", err)
|
||||
break
|
||||
}
|
||||
case err := <-waitCh:
|
||||
var waitStatus syscall.WaitStatus
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
waitStatus = exitError.Sys().(syscall.WaitStatus)
|
||||
os.Exit(waitStatus.ExitStatus())
|
||||
}
|
||||
if err != nil {
|
||||
app.Fatalf("%v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = p.Authenticate(&oauth2Token)
|
||||
app.FatalIfError(err, "Error authenticating with identity provider")
|
||||
|
||||
AWSCredentialsJSON, err := assumeRoleWithWebIdentity(execConfig, 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: execConfig.ClientID,
|
||||
Data: json,
|
||||
Label: fmt.Sprintf("OAuth2 token for %s", execConfig.RoleArn),
|
||||
Description: "OIDC OAuth2 Token",
|
||||
})
|
||||
app.FatalIfError(err, "Error storing OAuth2 Token in keychain")
|
||||
|
||||
fmt.Printf(AWSCredentialsJSON)
|
||||
}
|
||||
|
||||
func assumeRoleWithWebIdentity(execConfig *ExecConfig, idToken string) (string, error) {
|
||||
// environ is a slice of strings representing the environment, in the form "key=value".
|
||||
type environ []string
|
||||
|
||||
svc := sts.New(session.New())
|
||||
|
||||
input := &sts.AssumeRoleWithWebIdentityInput{
|
||||
DurationSeconds: aws.Int64(execConfig.Duration),
|
||||
RoleArn: aws.String(execConfig.RoleArn),
|
||||
RoleSessionName: aws.String("aws-oidc"),
|
||||
WebIdentityToken: aws.String(idToken),
|
||||
// Unset an environment variable by key
|
||||
func (e *environ) Unset(key string) {
|
||||
for i := range *e {
|
||||
if strings.HasPrefix((*e)[i], key+"=") {
|
||||
(*e)[i] = (*e)[len(*e)-1]
|
||||
*e = (*e)[:len(*e)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
|
||||
json, err := json.Marshal(&credentialData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(json), nil
|
||||
}
|
||||
|
||||
// Set adds an environment variable, replacing any existing ones of the same key
|
||||
func (e *environ) Set(key, val string) {
|
||||
e.Unset(key)
|
||||
*e = append(*e, key+"="+val)
|
||||
}
|
||||
|
@ -1,41 +1,54 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/99designs/aws-vault/vault"
|
||||
"github.com/99designs/keyring"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
// GlobalConfig used for defaults and command line arguments
|
||||
type GlobalConfig struct {
|
||||
//Region in AWS used by KMSAuth and BLESS
|
||||
Region string
|
||||
Region string
|
||||
Profile string
|
||||
AuthProvider []AuthConfig
|
||||
|
||||
AwsConfig *vault.Config
|
||||
Keyring *keyring.Keyring
|
||||
Session *session.Session
|
||||
Keyring *keyring.Keyring
|
||||
}
|
||||
|
||||
// ConfigureGlobal application arguments and flags
|
||||
func ConfigureGlobal(app *kingpin.Application, config *GlobalConfig) {
|
||||
|
||||
app.Flag("region", "The region in AWS").
|
||||
Default("ap-southeast-2").
|
||||
Default(config.Region).
|
||||
Envar("AWS_REGION").
|
||||
StringVar(&config.Region)
|
||||
|
||||
app.Flag("profile", "The profile to use as defined in the AWS config file").
|
||||
Default(config.Profile).
|
||||
Envar("AWS_PROFILE").
|
||||
StringVar(&config.Profile)
|
||||
|
||||
app.PreAction(func(c *kingpin.ParseContext) (err error) {
|
||||
|
||||
// Attempt to open the aws-vault keychain
|
||||
keychain, err := keyring.Open(keyring.Config{
|
||||
KeychainName: "aws-oidc",
|
||||
ServiceName: "aws-oidc",
|
||||
AllowedBackends: []keyring.BackendType{keyring.KeychainBackend},
|
||||
KeychainName: "aws-oidc",
|
||||
ServiceName: "aws-oidc",
|
||||
AllowedBackends: []keyring.BackendType{keyring.KeychainBackend},
|
||||
KeychainTrustApplication: true,
|
||||
})
|
||||
kingpin.FatalIfError(err, "Could not open aws-vault keychain")
|
||||
|
||||
//config.AwsConfig = awsConfig
|
||||
config.Keyring = &keychain
|
||||
|
||||
config.Session = session.Must(session.NewSessionWithOptions(session.Options{
|
||||
Config: aws.Config{Region: aws.String(config.Region)},
|
||||
Profile: config.Profile,
|
||||
SharedConfigState: session.SharedConfigEnable,
|
||||
}))
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
|
21
cli/list.go
21
cli/list.go
@ -5,16 +5,18 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/iam"
|
||||
jmespath "github.com/jmespath/go-jmespath"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
// ListConfig stores the parameters needed for a List command
|
||||
type ListConfig struct {
|
||||
ClientID string
|
||||
Claim string
|
||||
}
|
||||
|
||||
// ConfigureList configures the list command with arguments and flags
|
||||
func ConfigureList(app *kingpin.Application, config *GlobalConfig) {
|
||||
|
||||
listConfig := ListConfig{}
|
||||
@ -25,15 +27,20 @@ func ConfigureList(app *kingpin.Application, config *GlobalConfig) {
|
||||
Required().
|
||||
StringVar(&listConfig.ClientID)
|
||||
|
||||
cmd.Flag("claim", "The claim used in the IAM policies, prrovider:claim").
|
||||
Required().
|
||||
StringVar(&listConfig.Claim)
|
||||
|
||||
cmd.Action(func(c *kingpin.ParseContext) error {
|
||||
ListCommand(app, config, &listConfig)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// ListCommand retrieves the list of AWS roles that have trust policues that accept a given client_id
|
||||
func ListCommand(app *kingpin.Application, config *GlobalConfig, listConfig *ListConfig) {
|
||||
|
||||
svc := iam.New(session.New())
|
||||
svc := iam.New(config.Session)
|
||||
|
||||
input := &iam.ListRolesInput{}
|
||||
listRoleResult, err := svc.ListRoles(input)
|
||||
@ -47,9 +54,13 @@ func ListCommand(app *kingpin.Application, config *GlobalConfig, listConfig *Lis
|
||||
var d interface{}
|
||||
err = json.Unmarshal([]byte(decodedValue), &d)
|
||||
app.FatalIfError(err, "Unable to unmarshall AssumeRolePolicyDocument")
|
||||
v, err := jmespath.Search("contains(Statement[].Condition.StringEquals.\"openid-connect.onelogin.com/oidc:aud\", `abc`)", d)
|
||||
app.FatalIfError(err, "Unable to parse AssumeRolePolicyDocument")
|
||||
|
||||
fmt.Println(v)
|
||||
query := fmt.Sprintf("contains(Statement[].Condition.StringEquals.\"%s\", '%s')", listConfig.Claim, listConfig.ClientID)
|
||||
containsClientID, err := jmespath.Search(query, d)
|
||||
app.FatalIfError(err, "Unable to parse AssumeRolePolicyDocument")
|
||||
if containsClientID.(bool) {
|
||||
fmt.Println(*role.RoleName)
|
||||
fmt.Println(*role.Arn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,12 @@ func execDir() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetConfigFilePath returns the path of the configuration file
|
||||
func GetConfigFilePath() string {
|
||||
return filepath.Join(homeDir(), ".aws-oidc/config")
|
||||
}
|
||||
|
||||
// GetLogPath returns the path that should be used to store logs
|
||||
func GetLogPath() string {
|
||||
return filepath.Join(homeDir(), "./Library/Logs/aws-oidc.log")
|
||||
return filepath.Join(homeDir(), "Library/Logs/aws-oidc.log")
|
||||
}
|
||||
|
1
go.mod
1
go.mod
@ -3,6 +3,7 @@ module github.com/stoggi/aws-oidc
|
||||
require (
|
||||
github.com/99designs/aws-vault v4.5.1+incompatible
|
||||
github.com/99designs/keyring v0.0.0-20190110203331-82da6802f65f
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
|
||||
github.com/aulanov/go.dbus v0.0.0-20150729231527-25c3068a42a0 // indirect
|
||||
|
2
go.sum
2
go.sum
@ -3,6 +3,8 @@ github.com/99designs/aws-vault v4.5.1+incompatible h1:VjWncFWraO5K5HTRo34YMq2Mkp
|
||||
github.com/99designs/aws-vault v4.5.1+incompatible/go.mod h1:BKt7gBiUkiAOh7TP/c36gMpRJkIk5F8hStyQoWwC/Rw=
|
||||
github.com/99designs/keyring v0.0.0-20190110203331-82da6802f65f h1:WXiWWJrYCaOaYimBAXlRdRJ7qOisrYyMLYnCvvhHVms=
|
||||
github.com/99designs/keyring v0.0.0-20190110203331-82da6802f65f/go.mod h1:aKt8W/yd91/xHY6ixZAJZ2vYbhr3pP8DcrvuGSGNPJk=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
|
||||
|
@ -24,7 +24,6 @@ type ProviderConfig struct {
|
||||
ProviderURL string
|
||||
PKCE bool
|
||||
Nonce bool
|
||||
ReAuth bool
|
||||
AgentCommand []string
|
||||
}
|
||||
|
||||
@ -115,12 +114,10 @@ func (p *ProviderConfig) Authenticate(t *OAuth2Token) error {
|
||||
}
|
||||
|
||||
if t != nil {
|
||||
if err := t.Refresh(&config); err == nil {
|
||||
log.Println("Refreshed token successfully")
|
||||
if err := t.Refresh(&config); err != nil {
|
||||
return nil
|
||||
} else {
|
||||
log.Println(err)
|
||||
}
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
stateData := make([]byte, 32)
|
||||
@ -159,10 +156,6 @@ func (p *ProviderConfig) Authenticate(t *OAuth2Token) error {
|
||||
authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("nonce", nonce))
|
||||
}
|
||||
|
||||
if p.ReAuth {
|
||||
authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("acr_values", "onelogin:nist:level:1:re-auth"))
|
||||
}
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
url := config.AuthCodeURL(state, authCodeOptions...)
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
@ -211,13 +204,18 @@ func (p *ProviderConfig) Authenticate(t *OAuth2Token) error {
|
||||
|
||||
// Filter the commands, and replace "{}" with our callback url
|
||||
c := p.AgentCommand[:0]
|
||||
replacedURL := false
|
||||
for _, arg := range p.AgentCommand {
|
||||
if arg == "{}" {
|
||||
c = append(c, baseURL)
|
||||
replacedURL = true
|
||||
} else {
|
||||
c = append(c, arg)
|
||||
}
|
||||
}
|
||||
if !replacedURL {
|
||||
c = append(c, baseURL)
|
||||
}
|
||||
cmd := exec.Command(c[0], c[1:]...)
|
||||
cmd.Run()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user