replace github.com/stoggi/aws-oidc with internal/aws-oidc

This commit is contained in:
lordwelch 2020-08-07 14:49:29 -07:00
parent 6d3a7a0d10
commit 2ae68a7e31
16 changed files with 1148 additions and 1 deletions

4
go.mod
View File

@ -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
View File

@ -0,0 +1 @@
aws-oidc

21
internal/aws-oidc/LICENSE Normal file
View 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
View File

@ -0,0 +1,101 @@
# aws-oidc
Assume roles in AWS using an OpenID Connect identity provider.
![example](docs/aws-oidc-lambda.gif)
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
![example](docs/aws-oidc-console-login.gif)
## 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.

View 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))
}

View 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
}

View 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)
}

View 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
})
}

View 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)
}
}
}

View 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)
}

View 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")
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

30
internal/aws-oidc/go.mod Normal file
View 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
View 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=

View 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")
}
}