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
|
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/b-b3rn4rd/gocfn => github.com/stoggi/gocfn v0.0.0-20200214083946-6202cea979b9
|
||||||
|
|
||||||
|
replace github.com/stoggi/aws-oidc => ./internal/aws-oidc
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.38.0
|
cloud.google.com/go v0.38.0
|
||||||
github.com/AlecAivazis/survey/v2 v2.0.5
|
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