Fixed refresh token and storing in keychain.
Beginning to list roles given a clientid.
This commit is contained in:
parent
3c2e58c93e
commit
c548dcfd72
30
README.md
30
README.md
@ -47,10 +47,26 @@ Now you can use the AWS cli as normal, and specify the profile:
|
|||||||
|
|
||||||
## AWS Cognito
|
## AWS Cognito
|
||||||
|
|
||||||
./aws-oidc exec \
|
./aws-oidc exec \
|
||||||
--provider_url=https://cognito-idp.us-west-2.amazonaws.com/us-west-2_eBYNmnpS9 \
|
--provider_url=https://cognito-idp.us-west-2.amazonaws.com/us-west-2_eBYNmnpS9 \
|
||||||
--client_id=70kdnvprlqf1daspkn0iikdngv \
|
--client_id=70kdnvprlqf1daspkn0iikdngv \
|
||||||
--pkce \
|
--pkce \
|
||||||
--nonce \
|
--nonce \
|
||||||
--no-reauth \
|
--no-reauth \
|
||||||
-- open -b com.google.chrome -n --args --profile-directory=Default {}
|
-- open -b com.google.chrome -n --args --profile-directory=Default {}
|
||||||
|
|
||||||
|
## Find roles that an oidc client could assume
|
||||||
|
|
||||||
|
aws-vault exec test-privileged-admin -- aws iam list-roles --query <<EOF '
|
||||||
|
Roles[?
|
||||||
|
AssumeRolePolicyDocument.Statement[?
|
||||||
|
Condition.StringEquals."openid-connect.onelogin.com/oidc:aud"
|
||||||
|
]
|
||||||
|
].{
|
||||||
|
RoleName:RoleName,
|
||||||
|
Arn:Arn,
|
||||||
|
ClientId:AssumeRolePolicyDocument.Statement[*].Condition.StringEquals."openid-connect.onelogin.com/oidc:aud" | [0]
|
||||||
|
} | [?
|
||||||
|
contains(ClientId, `ef061080-43aa-0137-62f3-066d8813aeb888900`)
|
||||||
|
]'
|
||||||
|
EOF
|
@ -35,12 +35,14 @@ func run(args []string, exit func(int)) {
|
|||||||
"Assume roles in AWS using an OIDC identity provider",
|
"Assume roles in AWS using an OIDC identity provider",
|
||||||
)
|
)
|
||||||
|
|
||||||
app.Writer(os.Stdout)
|
|
||||||
app.Version(Version)
|
app.Version(Version)
|
||||||
app.Terminate(exit)
|
app.Terminate(exit)
|
||||||
|
app.UsageWriter(os.Stdout)
|
||||||
|
app.ErrorWriter(f)
|
||||||
|
|
||||||
cli.ConfigureGlobal(app, &config)
|
cli.ConfigureGlobal(app, &config)
|
||||||
cli.ConfigureExec(app, &config)
|
cli.ConfigureExec(app, &config)
|
||||||
|
cli.ConfigureList(app, &config)
|
||||||
|
|
||||||
kingpin.MustParse(app.Parse(args))
|
kingpin.MustParse(app.Parse(args))
|
||||||
}
|
}
|
||||||
|
47
cli/exec.go
47
cli/exec.go
@ -3,11 +3,13 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/99designs/keyring"
|
"github.com/99designs/keyring"
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
"github.com/aws/aws-sdk-go/service/sts"
|
"github.com/aws/aws-sdk-go/service/sts"
|
||||||
"github.com/stoggi/aws-oidc/provider"
|
"github.com/stoggi/aws-oidc/provider"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"gopkg.in/alecthomas/kingpin.v2"
|
"gopkg.in/alecthomas/kingpin.v2"
|
||||||
)
|
)
|
||||||
@ -90,7 +92,7 @@ func ConfigureExec(app *kingpin.Application, config *GlobalConfig) {
|
|||||||
|
|
||||||
func ExecCommand(app *kingpin.Application, config *GlobalConfig, execConfig *ExecConfig) {
|
func ExecCommand(app *kingpin.Application, config *GlobalConfig, execConfig *ExecConfig) {
|
||||||
|
|
||||||
providerConfig := &provider.ProviderConfig{
|
p := &provider.ProviderConfig{
|
||||||
ClientID: execConfig.ClientID,
|
ClientID: execConfig.ClientID,
|
||||||
ClientSecret: execConfig.ClientSecret,
|
ClientSecret: execConfig.ClientSecret,
|
||||||
ProviderURL: execConfig.ProviderURL,
|
ProviderURL: execConfig.ProviderURL,
|
||||||
@ -99,41 +101,36 @@ func ExecCommand(app *kingpin.Application, config *GlobalConfig, execConfig *Exe
|
|||||||
ReAuth: execConfig.ReAuth,
|
ReAuth: execConfig.ReAuth,
|
||||||
AgentCommand: execConfig.AgentCommand,
|
AgentCommand: execConfig.AgentCommand,
|
||||||
}
|
}
|
||||||
|
oauth2Token := provider.OAuth2Token{}
|
||||||
|
|
||||||
item, err := (*config.Keyring).Get(execConfig.ClientID)
|
item, err := (*config.Keyring).Get(execConfig.ClientID)
|
||||||
|
|
||||||
if err != keyring.ErrKeyNotFound {
|
if err != keyring.ErrKeyNotFound {
|
||||||
oauth2Token := provider.Oauth2Token{}
|
if err := json.Unmarshal(item.Data, &oauth2Token); err != nil {
|
||||||
err := json.Unmarshal(item.Data, &oauth2Token)
|
// Log this error only, because we can attempt to recover by getting a new token
|
||||||
// Maybe fail silently in case oauth2 lib is not backward compatible
|
app.Errorf("Unable to unmarshal OAuth2Token from keychain: %v", err)
|
||||||
app.FatalIfError(err, "Error parsing Oauth2 token from token : %v", err)
|
|
||||||
|
|
||||||
accessKeyJson, err := assumeRoleWithWebIdentity(execConfig, &oauth2Token)
|
|
||||||
if err == nil {
|
|
||||||
fmt.Println(accessKeyJson)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
oauth2Token, err := provider.Authenticate(providerConfig)
|
err = p.Authenticate(&oauth2Token)
|
||||||
|
app.FatalIfError(err, "Error authenticating with identity provider")
|
||||||
|
|
||||||
accessKeyJson, err := assumeRoleWithWebIdentity(execConfig, oauth2Token)
|
AWSCredentialsJSON, err := assumeRoleWithWebIdentity(execConfig, oauth2Token.IDToken)
|
||||||
app.FatalIfError(err, "Error assume role with web identity : %v", err)
|
app.FatalIfError(err, "Error assume role with web identity")
|
||||||
|
|
||||||
json, err := json.Marshal(&oauth2Token)
|
json, err := json.Marshal(&oauth2Token)
|
||||||
app.FatalIfError(err, "Can't serialize Oauth2 token : %v", err)
|
app.FatalIfError(err, "Error marshalling OAuth2 token")
|
||||||
|
err = (*config.Keyring).Set(keyring.Item{
|
||||||
(*config.Keyring).Set(keyring.Item{
|
Key: execConfig.ClientID,
|
||||||
Key: execConfig.ClientID,
|
Data: json,
|
||||||
Data: json,
|
Label: fmt.Sprintf("OAuth2 token for %s", execConfig.RoleArn),
|
||||||
Label: fmt.Sprintf("Oauth2 token for %s",execConfig.RoleArn),
|
Description: "OIDC OAuth2 Token",
|
||||||
Description:"OIDC JWT",
|
|
||||||
})
|
})
|
||||||
fmt.Printf(accessKeyJson)
|
app.FatalIfError(err, "Error storing OAuth2 Token in keychain")
|
||||||
|
|
||||||
|
fmt.Printf(AWSCredentialsJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assumeRoleWithWebIdentity(execConfig *ExecConfig, idToken string) (string, error) {
|
||||||
func assumeRoleWithWebIdentity(execConfig *ExecConfig, oauth2Token *provider.Oauth2Token) (string, error) {
|
|
||||||
|
|
||||||
svc := sts.New(session.New())
|
svc := sts.New(session.New())
|
||||||
|
|
||||||
@ -141,7 +138,7 @@ func assumeRoleWithWebIdentity(execConfig *ExecConfig, oauth2Token *provider.Oau
|
|||||||
DurationSeconds: aws.Int64(execConfig.Duration),
|
DurationSeconds: aws.Int64(execConfig.Duration),
|
||||||
RoleArn: aws.String(execConfig.RoleArn),
|
RoleArn: aws.String(execConfig.RoleArn),
|
||||||
RoleSessionName: aws.String("aws-oidc"),
|
RoleSessionName: aws.String("aws-oidc"),
|
||||||
WebIdentityToken: aws.String(oauth2Token.IDToken),
|
WebIdentityToken: aws.String(idToken),
|
||||||
}
|
}
|
||||||
|
|
||||||
assumeRoleResult, err := svc.AssumeRoleWithWebIdentity(input)
|
assumeRoleResult, err := svc.AssumeRoleWithWebIdentity(input)
|
||||||
|
55
cli/list.go
Normal file
55
cli/list.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
|
"github.com/aws/aws-sdk-go/service/iam"
|
||||||
|
jmespath "github.com/jmespath/go-jmespath"
|
||||||
|
"gopkg.in/alecthomas/kingpin.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListConfig struct {
|
||||||
|
ClientID string
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Action(func(c *kingpin.ParseContext) error {
|
||||||
|
ListCommand(app, config, &listConfig)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListCommand(app *kingpin.Application, config *GlobalConfig, listConfig *ListConfig) {
|
||||||
|
|
||||||
|
svc := iam.New(session.New())
|
||||||
|
|
||||||
|
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")
|
||||||
|
v, err := jmespath.Search("contains(Statement[].Condition.StringEquals.\"openid-connect.onelogin.com/oidc:aud\", `abc`)", d)
|
||||||
|
app.FatalIfError(err, "Unable to parse AssumeRolePolicyDocument")
|
||||||
|
|
||||||
|
fmt.Println(v)
|
||||||
|
}
|
||||||
|
}
|
1
go.mod
1
go.mod
@ -13,6 +13,7 @@ require (
|
|||||||
github.com/go-ini/ini v1.42.0 // indirect
|
github.com/go-ini/ini v1.42.0 // indirect
|
||||||
github.com/godbus/dbus v4.1.0+incompatible // indirect
|
github.com/godbus/dbus v4.1.0+incompatible // indirect
|
||||||
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // 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/keybase/go-keychain v0.0.0-20190408194155-7f2ef9fddce6 // indirect
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
github.com/kr/pretty v0.1.0 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@ -43,32 +44,59 @@ type TokenClaims struct {
|
|||||||
Groups []string `json:"groups"`
|
Groups []string `json:"groups"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Oauth2Token struct {
|
type OAuth2Token struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
TokenType string `json:"token_type,omitempty"`
|
TokenType string `json:"token_type,omitempty"`
|
||||||
RefreshToken string `json:"refresh_token,omitempty"`
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
Expiry time.Time `json:"expiry,omitempty"`
|
Expiry time.Time `json:"expiry,omitempty"`
|
||||||
IDToken string `json:"id_token,omitempty"`
|
IDToken string `json:"id_token,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func AuthenticateWithRefreshToken(p *ProviderConfig) {
|
func (t *OAuth2Token) Refresh(config *oauth2.Config) 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 Authenticate(p *ProviderConfig) (*Oauth2Token, error) {
|
func (p *ProviderConfig) Authenticate(t *OAuth2Token) error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
resultChannel := make(chan *oauth2.Token, 0)
|
resultChannel := make(chan *oauth2.Token, 0)
|
||||||
errorChannel := make(chan error, 0)
|
errorChannel := make(chan error, 0)
|
||||||
|
|
||||||
provider, err := oidc.NewProvider(ctx, p.ProviderURL)
|
provider, err := oidc.NewProvider(ctx, p.ProviderURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
defer listener.Close()
|
||||||
baseURL := "http://" + listener.Addr().String()
|
baseURL := "http://" + listener.Addr().String()
|
||||||
redirectURL := baseURL + "/auth/callback"
|
redirectURL := baseURL + "/auth/callback"
|
||||||
|
|
||||||
@ -86,15 +114,24 @@ func Authenticate(p *ProviderConfig) (*Oauth2Token, error) {
|
|||||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t != nil {
|
||||||
|
if err := t.Refresh(&config); err == nil {
|
||||||
|
log.Println("Refreshed token successfully")
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stateData := make([]byte, 32)
|
stateData := make([]byte, 32)
|
||||||
if _, err = rand.Read(stateData); err != nil {
|
if _, err = rand.Read(stateData); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
state := base64.URLEncoding.EncodeToString(stateData)
|
state := base64.URLEncoding.EncodeToString(stateData)
|
||||||
|
|
||||||
codeData := make([]byte, 32)
|
codeData := make([]byte, 32)
|
||||||
if _, err = rand.Read(codeData); err != nil {
|
if _, err = rand.Read(codeData); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
codeVerifier := base64.StdEncoding.EncodeToString(codeData)
|
codeVerifier := base64.StdEncoding.EncodeToString(codeData)
|
||||||
codeDigest := sha256.Sum256([]byte(codeVerifier))
|
codeDigest := sha256.Sum256([]byte(codeVerifier))
|
||||||
@ -192,19 +229,18 @@ func Authenticate(p *ProviderConfig) (*Oauth2Token, error) {
|
|||||||
select {
|
select {
|
||||||
case err := <-errorChannel:
|
case err := <-errorChannel:
|
||||||
server.Shutdown(ctx)
|
server.Shutdown(ctx)
|
||||||
return nil, err
|
return err
|
||||||
case res := <-resultChannel:
|
case res := <-resultChannel:
|
||||||
server.Shutdown(ctx)
|
server.Shutdown(ctx)
|
||||||
idtoken, ok := res.Extra("id_token").(string)
|
idtoken, ok := res.Extra("id_token").(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("Can't extract id_token")
|
return errors.New("Can't extract id_token")
|
||||||
}
|
}
|
||||||
return &Oauth2Token{
|
t.AccessToken = res.AccessToken
|
||||||
AccessToken:res.AccessToken,
|
t.RefreshToken = res.RefreshToken
|
||||||
RefreshToken:res.RefreshToken,
|
t.Expiry = res.Expiry
|
||||||
Expiry:res.Expiry,
|
t.TokenType = res.TokenType
|
||||||
TokenType:res.TokenType,
|
t.IDToken = idtoken
|
||||||
IDToken:idtoken,
|
return nil
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user