Fixed refresh token and storing in keychain.

Beginning to list roles given a clientid.
This commit is contained in:
Jeremy Stott 2019-04-20 23:12:20 +12:00
parent 3c2e58c93e
commit c548dcfd72
6 changed files with 162 additions and 55 deletions

View File

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

View File

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

View File

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

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

View File

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