replace github.com/stoggi/aws-oidc with internal/aws-oidc
This commit is contained in:
parent
6d3a7a0d10
commit
2ae68a7e31
4
go.mod
4
go.mod
@ -1,9 +1,11 @@
|
||||
module github.com/stoggi/sshrimp
|
||||
|
||||
go 1.13
|
||||
go 1.14
|
||||
|
||||
replace github.com/b-b3rn4rd/gocfn => github.com/stoggi/gocfn v0.0.0-20200214083946-6202cea979b9
|
||||
|
||||
replace github.com/stoggi/aws-oidc => ./internal/aws-oidc
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.38.0
|
||||
github.com/AlecAivazis/survey/v2 v2.0.5
|
||||
|
1
internal/aws-oidc/.gitignore
vendored
Normal file
1
internal/aws-oidc/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
aws-oidc
|
21
internal/aws-oidc/LICENSE
Normal file
21
internal/aws-oidc/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Jeremy Stott
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
101
internal/aws-oidc/README.md
Normal file
101
internal/aws-oidc/README.md
Normal file
@ -0,0 +1,101 @@
|
||||
# aws-oidc
|
||||
|
||||
Assume roles in AWS using an OpenID Connect identity provider.
|
||||
|
||||

|
||||
|
||||
It is intended to be used as a `credentials_process` in ~/.aws/config that outputs temporary AWS credentials in a JSON format.
|
||||
|
||||
https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes
|
||||
|
||||
## Getting Started
|
||||
|
||||
Add the following to **~/.aws/config**:
|
||||
|
||||
[profile default]
|
||||
region = us-east-1
|
||||
credential_process = /Users/jeremy/projects/aws-oidc/aws-oidc auth google
|
||||
|
||||
And configure aws-oidc by creating **~/.aws-oidc/config** and setting the `role_arn` and `client_id`:
|
||||
|
||||
region = "us-east-1"
|
||||
|
||||
[[AuthProvider]]
|
||||
name = "google"
|
||||
role_arn = "arn:aws:iam::0123456789012:role/your-role-name"
|
||||
duration = 900
|
||||
provider_url = "https://accounts.google.com"
|
||||
client_id = "YOUR_CLIENT_ID"
|
||||
client_secret = "YOUR_CLIENT_SECRET" # only specify this if your OIDC provider requires it even when using PKCE
|
||||
agent = ["open", "-b", "com.google.chrome"]
|
||||
|
||||
Then you can assume the role using the AWS cli:
|
||||
|
||||
aws sts get-caller-identity
|
||||
|
||||
Most AWS SDK implementations should be able to use the `credential_process` configuration, including:
|
||||
|
||||
* aws-sdk-go
|
||||
* aws-cli
|
||||
* boto3
|
||||
|
||||
## Sign into the AWS Console
|
||||
|
||||
Use the `login` command to exchange the temporary credentials with an [AWS Console login URL](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html)
|
||||
|
||||
aws-oidc login
|
||||
|
||||

|
||||
|
||||
## Open Chrome with a particular profile
|
||||
|
||||
Open `chrome://version/` in the Chrome profile you want to authenticate in, and make a note of the last part of the profile path.
|
||||
|
||||
Update the `agent` option with the path in your **~/.aws-oidc/config** file:
|
||||
|
||||
agent = ["open", "-b", "com.google.chrome", "-n", "--args", "--profile-directory=Profile 1", "{}"]
|
||||
|
||||
## Configure More Roles
|
||||
|
||||
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 engineer]
|
||||
credential_process = aws-oidc auth onelogin --role_arn=arn:aws:iam::0123456789012:role/your-role-name --duration 7200
|
||||
|
||||
Make sure each authentication provider exists in **~/.aws-oidc/config**. You can also override any of the configured settings here on the command line.
|
||||
|
||||
To make use of this new role, simply specify the `profile` in your AWS SDK:
|
||||
|
||||
aws --profile engineer sts get-caller-identity
|
||||
|
||||
## Run other commands with AWS credentials
|
||||
|
||||
Most AWS SDK's should be able to pick up the profile parameter, and support 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 by calling `aws-oidc auth` and then set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN` environment variables for the new process.
|
||||
|
||||
## Find roles that an oidc client could assume
|
||||
|
||||
Use the `list` command to find roles that your claim and client_id can assume:
|
||||
|
||||
aws-oidc list --claim="accounts.google.com:aud" --client_id="CLIENT_ID"
|
||||
|
||||
Example using only the AWS CLI:
|
||||
|
||||
aws iam list-roles --query <<EOF '
|
||||
Roles[?
|
||||
AssumeRolePolicyDocument.Statement[?
|
||||
Condition.StringEquals."accounts.google.com:aud"
|
||||
]
|
||||
].{
|
||||
RoleName:RoleName,
|
||||
Arn:Arn,
|
||||
ClientId:AssumeRolePolicyDocument.Statement[*].Condition.StringEquals."accounts.google.com:aud" | [0]
|
||||
} | [?
|
||||
contains(ClientId, `CLIENT_ID`)
|
||||
]'
|
||||
EOF
|
||||
|
||||
Note, your default profile will need `iam:ListRoles` permission. To use a different profile use the `--profile` option.
|
55
internal/aws-oidc/aws-oidc.go
Normal file
55
internal/aws-oidc/aws-oidc.go
Normal file
@ -0,0 +1,55 @@
|
||||
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"
|
||||
|
||||
func main() {
|
||||
run(os.Args[1:], os.Exit)
|
||||
}
|
||||
|
||||
func run(args []string, exit func(int)) {
|
||||
|
||||
f, err := os.OpenFile(GetLogPath(), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
log.Fatalf("error opening file: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
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",
|
||||
"Assume roles in AWS using an OIDC identity provider",
|
||||
)
|
||||
|
||||
app.Version(Version)
|
||||
app.Terminate(exit)
|
||||
app.UsageWriter(os.Stdout)
|
||||
app.ErrorWriter(wrt)
|
||||
|
||||
cli.ConfigureGlobal(app, &config)
|
||||
cli.ConfigureAuth(app, &config)
|
||||
cli.ConfigureExec(app, &config)
|
||||
cli.ConfigureList(app, &config)
|
||||
cli.ConfigureLogin(app, &config)
|
||||
|
||||
kingpin.MustParse(app.Parse(args))
|
||||
}
|
211
internal/aws-oidc/cli/auth.go
Normal file
211
internal/aws-oidc/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
|
||||
}
|
140
internal/aws-oidc/cli/exec.go
Normal file
140
internal/aws-oidc/cli/exec.go
Normal file
@ -0,0 +1,140 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"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 {
|
||||
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", "Retrieve temporary credentials and set them as environment variables")
|
||||
|
||||
cmd.Arg("profile", "Name of the profile").
|
||||
StringVar(&config.Profile)
|
||||
|
||||
cmd.Arg("cmd", "Command to execute").
|
||||
Default(os.Getenv("SHELL")).
|
||||
StringVar(&execConfig.Command)
|
||||
|
||||
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) {
|
||||
|
||||
if os.Getenv("AWS_OIDC") != "" {
|
||||
app.Fatalf("aws-vault sessions should be nested with care, unset $AWS_OIDC to force")
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// environ is a slice of strings representing the environment, in the form "key=value".
|
||||
type environ []string
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
55
internal/aws-oidc/cli/global.go
Normal file
55
internal/aws-oidc/cli/global.go
Normal file
@ -0,0 +1,55 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"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
|
||||
Profile string
|
||||
AuthProvider []AuthConfig
|
||||
|
||||
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(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},
|
||||
KeychainTrustApplication: true,
|
||||
})
|
||||
kingpin.FatalIfError(err, "Could not open aws-vault keychain")
|
||||
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
|
||||
})
|
||||
|
||||
}
|
66
internal/aws-oidc/cli/list.go
Normal file
66
internal/aws-oidc/cli/list.go
Normal file
@ -0,0 +1,66 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"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{}
|
||||
|
||||
cmd := app.Command("list", "List roles that a ClientID can assume")
|
||||
|
||||
cmd.Flag("client_id", "The OpenID Connect Client ID").
|
||||
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(config.Session)
|
||||
|
||||
input := &iam.ListRolesInput{}
|
||||
listRoleResult, err := svc.ListRoles(input)
|
||||
app.FatalIfError(err, "Unable to list roles")
|
||||
|
||||
for _, role := range listRoleResult.Roles {
|
||||
|
||||
decodedValue, err := url.QueryUnescape(*role.AssumeRolePolicyDocument)
|
||||
app.FatalIfError(err, "Unable to urldecode document")
|
||||
|
||||
var d interface{}
|
||||
err = json.Unmarshal([]byte(decodedValue), &d)
|
||||
app.FatalIfError(err, "Unable to unmarshall AssumeRolePolicyDocument")
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
97
internal/aws-oidc/cli/login.go
Normal file
97
internal/aws-oidc/cli/login.go
Normal file
@ -0,0 +1,97 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
// LoginConfig stores the parameters needed for an login command
|
||||
type LoginConfig struct {
|
||||
Profile string
|
||||
}
|
||||
|
||||
type signinSession struct {
|
||||
SessionID string `json:"sessionId"`
|
||||
SessionKey string `json:"sessionKey"`
|
||||
SessionToken string `json:"sessionToken"`
|
||||
}
|
||||
|
||||
type signinToken struct {
|
||||
SigninToken string
|
||||
}
|
||||
|
||||
// ConfigureLogin configures the login command with arguments and flags
|
||||
func ConfigureLogin(app *kingpin.Application, config *GlobalConfig) {
|
||||
|
||||
loginConfig := LoginConfig{}
|
||||
|
||||
cmd := app.Command("login", "Login to the AWS console for a given profile")
|
||||
|
||||
cmd.Arg("profile", "Name of the profile").
|
||||
StringVar(&config.Profile)
|
||||
|
||||
cmd.Action(func(c *kingpin.ParseContext) error {
|
||||
LoginCommand(app, config, &loginConfig)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// LoginCommand exchanges temporary credentials for an AWS Console signin url
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html
|
||||
func LoginCommand(app *kingpin.Application, config *GlobalConfig, loginConfig *LoginConfig) {
|
||||
|
||||
// Retrieve credentials from current session. This will try and get credentials
|
||||
// using aws-oidc itself if configured in ~/.aws/config.
|
||||
val, err := config.Session.Config.Credentials.Get()
|
||||
if err != nil {
|
||||
app.Fatalf("Unable to get credentials for profile: %s", config.Profile)
|
||||
}
|
||||
|
||||
credentialData := signinSession{
|
||||
SessionID: val.AccessKeyID,
|
||||
SessionKey: val.SecretAccessKey,
|
||||
SessionToken: val.SessionToken,
|
||||
}
|
||||
credentialJSON, err := json.Marshal(&credentialData)
|
||||
if err != nil {
|
||||
app.Fatalf("Unable to marshal credentials for profile: %s", config.Profile)
|
||||
}
|
||||
|
||||
// Create the federation URL to exchange access keys for a session token
|
||||
tokenURL, _ := url.Parse("https://signin.aws.amazon.com/federation")
|
||||
tokenQuery := url.Values{}
|
||||
tokenQuery.Set("Action", "getSigninToken")
|
||||
tokenQuery.Set("Session", string(credentialJSON))
|
||||
tokenURL.RawQuery = tokenQuery.Encode()
|
||||
|
||||
var client = &http.Client{
|
||||
Timeout: time.Second * 60,
|
||||
}
|
||||
resp, err := client.Get(tokenURL.String())
|
||||
if err != nil {
|
||||
app.Fatalf("Unable to get signin token for profile: %s", config.Profile)
|
||||
} else if resp.StatusCode != 200 {
|
||||
app.Fatalf("GetSigninToken returned %d instead of 200 for profile: %s", resp.StatusCode, config.Profile)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
token := signinToken{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
|
||||
app.Fatalf("Unable to decode GetSigninToken response for profile: %s", config.Profile)
|
||||
}
|
||||
|
||||
// Create the federation URL to exchange the session token for a login URL
|
||||
loginURL, _ := url.Parse("https://signin.aws.amazon.com/federation")
|
||||
loginQuery := url.Values{}
|
||||
loginQuery.Set("Action", "login")
|
||||
loginQuery.Set("Destination", "https://console.aws.amazon.com/")
|
||||
loginQuery.Set("SigninToken", token.SigninToken)
|
||||
loginURL.RawQuery = loginQuery.Encode()
|
||||
|
||||
fmt.Println(loginURL)
|
||||
}
|
31
internal/aws-oidc/config_darwin.go
Normal file
31
internal/aws-oidc/config_darwin.go
Normal file
@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func homeDir() string {
|
||||
if currentUser, err := user.Current(); err == nil {
|
||||
return currentUser.HomeDir
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func execDir() string {
|
||||
if currentExecutable, err := os.Executable(); err == nil {
|
||||
return filepath.Dir(currentExecutable)
|
||||
}
|
||||
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")
|
||||
}
|
BIN
internal/aws-oidc/docs/aws-oidc-console-login.gif
Normal file
BIN
internal/aws-oidc/docs/aws-oidc-console-login.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 MiB |
BIN
internal/aws-oidc/docs/aws-oidc-lambda.gif
Normal file
BIN
internal/aws-oidc/docs/aws-oidc-lambda.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 MiB |
30
internal/aws-oidc/go.mod
Normal file
30
internal/aws-oidc/go.mod
Normal file
@ -0,0 +1,30 @@
|
||||
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
|
||||
github.com/aws/aws-sdk-go v1.19.11
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible
|
||||
github.com/danieljoos/wincred v1.0.1 // indirect
|
||||
github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a // indirect
|
||||
github.com/go-ini/ini v1.42.0 // indirect
|
||||
github.com/godbus/dbus v4.1.0+incompatible // indirect
|
||||
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af
|
||||
github.com/keybase/go-keychain v0.0.0-20190408194155-7f2ef9fddce6 // indirect
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect
|
||||
github.com/stretchr/testify v1.3.0 // indirect
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
gopkg.in/ini.v1 v1.42.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.3.0 // indirect
|
||||
)
|
84
internal/aws-oidc/go.sum
Normal file
84
internal/aws-oidc/go.sum
Normal file
@ -0,0 +1,84 @@
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/99designs/aws-vault v4.5.1+incompatible h1:VjWncFWraO5K5HTRo34YMq2MkpKYphZy5luMSe76pkg=
|
||||
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=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/aulanov/go.dbus v0.0.0-20150729231527-25c3068a42a0 h1:EEDvbomAQ+MFWqJ9FM6RXyJTkc4lckyWsbc5CGQkG1Y=
|
||||
github.com/aulanov/go.dbus v0.0.0-20150729231527-25c3068a42a0/go.mod h1:VHvUx+4lTCaJ8zUnEXF4cWEc9c8lnDt4PGLwlZ+3yaM=
|
||||
github.com/aws/aws-sdk-go v1.19.11 h1:tqaTGER6Byw3QvsjGW0p018U2UOqaJPeJuzoaF7jjoQ=
|
||||
github.com/aws/aws-sdk-go v1.19.11/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg=
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/danieljoos/wincred v1.0.1 h1:fcRTaj17zzROVqni2FiToKUVg3MmJ4NtMSGCySPIr/g=
|
||||
github.com/danieljoos/wincred v1.0.1/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a h1:mq+R6XEM6lJX5VlLyZIrUSP8tSuJp82xTK89hvBwJbU=
|
||||
github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
|
||||
github.com/go-ini/ini v1.42.0 h1:TWr1wGj35+UiWHlBA8er89seFXxzwFn11spilrrj+38=
|
||||
github.com/go-ini/ini v1.42.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
|
||||
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/keybase/go-keychain v0.0.0-20190408194155-7f2ef9fddce6 h1:hfM5TYph19rQBp3oOg4SVckf4ZmYrycciBJCWmxOcIE=
|
||||
github.com/keybase/go-keychain v0.0.0-20190408194155-7f2ef9fddce6/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA=
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
|
||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/square/go-jose.v2 v2.3.0 h1:nLzhkFyl5bkblqYBoiWJUt5JkWOzmiaBtCxdJAqJd3U=
|
||||
gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
253
internal/aws-oidc/provider/provider.go
Normal file
253
internal/aws-oidc/provider/provider.go
Normal file
@ -0,0 +1,253 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type ProviderConfig struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ProviderURL string
|
||||
PKCE bool
|
||||
Nonce bool
|
||||
AgentCommand []string
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
JWT string
|
||||
Token *oidc.IDToken
|
||||
Claims *TokenClaims
|
||||
}
|
||||
|
||||
type TokenClaims struct {
|
||||
Issuer string `json:"iss"`
|
||||
Audience string `json:"aud"`
|
||||
Subject string `json:"sub"`
|
||||
Picture string `json:"picture"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
|
||||
type OAuth2Token struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
Expiry time.Time `json:"expiry,omitempty"`
|
||||
IDToken string `json:"id_token,omitempty"`
|
||||
}
|
||||
|
||||
func refresh(config oauth2.Config, t *OAuth2Token) error {
|
||||
ctx := context.Background()
|
||||
|
||||
tokenSourceToken := oauth2.Token{
|
||||
AccessToken: t.AccessToken,
|
||||
TokenType: t.TokenType,
|
||||
RefreshToken: t.RefreshToken,
|
||||
Expiry: t.Expiry,
|
||||
}
|
||||
ts := config.TokenSource(ctx, tokenSourceToken.WithExtra(map[string]interface{}{
|
||||
"id_token": t.IDToken,
|
||||
}))
|
||||
|
||||
res, err := ts.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idtoken, ok := res.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return errors.New("can't extract id_token")
|
||||
}
|
||||
t.AccessToken = res.AccessToken
|
||||
t.RefreshToken = res.RefreshToken
|
||||
t.Expiry = res.Expiry
|
||||
t.TokenType = res.TokenType
|
||||
t.IDToken = idtoken
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p ProviderConfig) Authenticate(t *OAuth2Token) error {
|
||||
ctx := context.Background()
|
||||
resultChannel := make(chan *oauth2.Token)
|
||||
errorChannel := make(chan error)
|
||||
Mux := http.NewServeMux()
|
||||
server := &http.Server{
|
||||
Handler: Mux,
|
||||
}
|
||||
|
||||
provider, err := oidc.NewProvider(ctx, p.ProviderURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer listener.Close()
|
||||
baseURL := "http://" + listener.Addr().String()
|
||||
redirectURL := baseURL + "/auth/callback"
|
||||
|
||||
oidcConfig := &oidc.Config{
|
||||
ClientID: p.ClientID,
|
||||
SupportedSigningAlgs: []string{"RS256"},
|
||||
}
|
||||
verifier := provider.Verifier(oidcConfig)
|
||||
|
||||
config := oauth2.Config{
|
||||
ClientID: p.ClientID,
|
||||
ClientSecret: p.ClientSecret,
|
||||
Endpoint: provider.Endpoint(),
|
||||
RedirectURL: redirectURL,
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||
}
|
||||
|
||||
if t != nil {
|
||||
if err := refresh(config, t); err == nil {
|
||||
return nil
|
||||
}
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
stateData := make([]byte, 32)
|
||||
if _, err = rand.Read(stateData); err != nil {
|
||||
return err
|
||||
}
|
||||
state := base64.URLEncoding.EncodeToString(stateData)
|
||||
|
||||
codeData := make([]byte, 32)
|
||||
if _, err = rand.Read(codeData); err != nil {
|
||||
return err
|
||||
}
|
||||
codeVerifier := base64.StdEncoding.EncodeToString(codeData)
|
||||
codeDigest := sha256.Sum256([]byte(codeVerifier))
|
||||
codeChallenge := base64.URLEncoding.EncodeToString(codeDigest[:])
|
||||
codeChallengeEncoded := strings.Replace(codeChallenge, "=", "", -1)
|
||||
|
||||
nonceData := make([]byte, 32)
|
||||
_, _ = rand.Read(nonceData)
|
||||
nonce := base64.URLEncoding.EncodeToString(nonceData)
|
||||
|
||||
var authCodeOptions []oauth2.AuthCodeOption
|
||||
var tokenCodeOptions []oauth2.AuthCodeOption
|
||||
|
||||
if p.PKCE {
|
||||
authCodeOptions = append(authCodeOptions,
|
||||
oauth2.SetAuthURLParam("code_challenge", codeChallengeEncoded),
|
||||
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
|
||||
)
|
||||
tokenCodeOptions = append(tokenCodeOptions,
|
||||
oauth2.SetAuthURLParam("code_verifier", codeVerifier),
|
||||
)
|
||||
}
|
||||
|
||||
if p.Nonce {
|
||||
authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("nonce", nonce))
|
||||
}
|
||||
|
||||
Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
url := config.AuthCodeURL(state, authCodeOptions...)
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
})
|
||||
|
||||
Mux.HandleFunc("/auth/callback", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("state") != state {
|
||||
http.Error(w, "state did not match", http.StatusBadRequest)
|
||||
errorChannel <- errors.New("state did not match")
|
||||
return
|
||||
}
|
||||
|
||||
oauth2Token, err := config.Exchange(ctx, r.URL.Query().Get("code"), tokenCodeOptions...)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
|
||||
errorChannel <- errors.New("failed to exchange token: " + err.Error())
|
||||
return
|
||||
}
|
||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError)
|
||||
errorChannel <- errors.New("no id_token field in oauth2 token")
|
||||
return
|
||||
}
|
||||
idToken, err := verifier.Verify(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
|
||||
errorChannel <- errors.New("failed to verify ID Token: " + err.Error())
|
||||
return
|
||||
}
|
||||
if p.Nonce && idToken.Nonce != nonce {
|
||||
http.Error(w, "Failed to verify Nonce", http.StatusInternalServerError)
|
||||
errorChannel <- errors.New("failed to verify Nonce")
|
||||
return
|
||||
}
|
||||
|
||||
var claims = new(TokenClaims)
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
errorChannel <- errors.New("failed to verify Claims: " + err.Error())
|
||||
return
|
||||
}
|
||||
w.Write([]byte("Signed in successfully, return to cli app"))
|
||||
resultChannel <- oauth2Token
|
||||
})
|
||||
|
||||
// Filter the commands, and replace "{}" with our callback url
|
||||
c := make([]string, 0, len(p.AgentCommand))
|
||||
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)
|
||||
}
|
||||
|
||||
//TODO Drop privileges
|
||||
cmd := exec.Command(c[0], c[1:]...)
|
||||
cmd.Start()
|
||||
cmd.Process.Release()
|
||||
|
||||
go func() {
|
||||
server.Serve(listener)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-errorChannel:
|
||||
server.Shutdown(ctx)
|
||||
return err
|
||||
case res := <-resultChannel:
|
||||
server.Shutdown(ctx)
|
||||
IDToken, ok := res.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return errors.New("can't extract id_token")
|
||||
}
|
||||
t.AccessToken = res.AccessToken
|
||||
t.RefreshToken = res.RefreshToken
|
||||
t.Expiry = res.Expiry
|
||||
t.TokenType = res.TokenType
|
||||
t.IDToken = IDToken
|
||||
return nil
|
||||
case <-time.After(2 * time.Minute):
|
||||
server.Shutdown(ctx)
|
||||
return errors.New("no oauth2 flow callback received within last 2 minutes, exiting")
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user