aws-oidc working with google and onelogin openid connect.
This commit is contained in:
commit
653521ed9f
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
aws-oidc
|
46
README.md
Normal file
46
README.md
Normal file
@ -0,0 +1,46 @@
|
||||
# aws-oidc
|
||||
|
||||
Assume roles in AWS using an OpenID Connect Identity provider.
|
||||
|
||||
It outputs temporary AWS credentials in a JSON format that can be consumed by the credentials_process setting in ~/.aws/config.
|
||||
|
||||
https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes
|
||||
|
||||
OneLogin Example:
|
||||
|
||||
aws-oidc exec \
|
||||
--role_arn="arn:aws:iam::892845094662:role/onelogin-test-oidc" \
|
||||
--provider_url=https://openid-connect.onelogin.com/oidc \
|
||||
--client_id=97a61160-3c09-0137-8c69-0a1c3f4fd822144813 \
|
||||
--pkce \
|
||||
--nonce \
|
||||
-- open -b com.google.chrome -n --args --profile-directory=Default {}
|
||||
|
||||
Google Example:
|
||||
|
||||
aws-oidc exec \
|
||||
--role_arn="arn:aws:iam::892845094662:role/web-identity-lanbda" \
|
||||
--provider_url=https://accounts.google.com \
|
||||
--client_id=430784603061-osbtei3s71l0bj6d8oegto0itefjmiq6.apps.googleusercontent.com \
|
||||
--client_secret=... \
|
||||
--pkce \
|
||||
--nonce \
|
||||
-- open -b com.google.chrome -n --args --profile-directory=Default {}
|
||||
|
||||
For some reason, even when using PKCE, google need a client_secret for applications not registered as Android, iOS or Chrome.
|
||||
|
||||
## Configure AWS Config
|
||||
|
||||
~/.aws/config
|
||||
|
||||
[profile oidc]
|
||||
credential_process = aws-oidc exec --role_arn=arn:aws:iam::892845094662:role/onelogin-test-oidc --provider_url=https://openid-connect.onelogin.com/oidc --client_id=97a61160-3c09-0137-8c69-0a1c3f4fd822144813 --pkce --nonce -- open -b com.google.chrome -n --args --profile-directory=Default {}
|
||||
|
||||
Now you can use the AWS cli as normal, and specify the profile:
|
||||
|
||||
$ aws --profile oidc sts get-caller-identity
|
||||
{
|
||||
"UserId": "AROAJUTXNWXGCAEILMXTY:50904038",
|
||||
"Account": "892845094662",
|
||||
"Arn": "arn:aws:sts::892845094662:assumed-role/onelogin-test-oidc/50904038"
|
||||
}
|
46
aws-oidc.go
Normal file
46
aws-oidc.go
Normal file
@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/stoggi/aws-oidc/cli"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
// Version is provided at compile time
|
||||
var Version = "dev"
|
||||
var labelText chan string
|
||||
|
||||
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()
|
||||
|
||||
log.SetOutput(f)
|
||||
log.Println("Starting...")
|
||||
|
||||
// Default configuration, values are overridden by command line options.
|
||||
config := cli.GlobalConfig{}
|
||||
|
||||
app := kingpin.New(
|
||||
"aws-oidc",
|
||||
"Assume roles in AWS using an OIDC identity provider",
|
||||
)
|
||||
|
||||
app.Writer(os.Stdout)
|
||||
app.Version(Version)
|
||||
app.Terminate(exit)
|
||||
|
||||
cli.ConfigureGlobal(app, &config)
|
||||
cli.ConfigureExec(app, &config)
|
||||
|
||||
kingpin.MustParse(app.Parse(args))
|
||||
}
|
126
cli/exec.go
Normal file
126
cli/exec.go
Normal file
@ -0,0 +1,126 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/sts"
|
||||
"github.com/stoggi/aws-oidc/provider"
|
||||
|
||||
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
type ExecConfig struct {
|
||||
RoleArn string
|
||||
Duration int64
|
||||
ProviderURL string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
PKCE bool
|
||||
Nonce bool
|
||||
ReAuth bool
|
||||
AgentCommant []string
|
||||
}
|
||||
|
||||
// json metadata for AWS credential process. Ref: 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 ConfigureExec(app *kingpin.Application, config *GlobalConfig) {
|
||||
|
||||
execConfig := ExecConfig{}
|
||||
|
||||
cmd := app.Command("exec", "Execute a command with temporary AWS credentials")
|
||||
|
||||
cmd.Default()
|
||||
|
||||
cmd.Flag("role_arn", "The AWS role you want to assume").
|
||||
Required().
|
||||
StringVar(&execConfig.RoleArn)
|
||||
|
||||
cmd.Flag("duration", "The duration to assume the role for in seconds").
|
||||
Default("3600").
|
||||
Int64Var(&execConfig.Duration)
|
||||
|
||||
cmd.Flag("provider_url", "The OpenID Connect Provider URL").
|
||||
Required().
|
||||
StringVar(&execConfig.ProviderURL)
|
||||
|
||||
cmd.Flag("client_id", "The OpenID Connect Client ID").
|
||||
Required().
|
||||
StringVar(&execConfig.ClientID)
|
||||
|
||||
cmd.Flag("client_secret", "The OpenID Connect Client Secret").
|
||||
Default("").
|
||||
StringVar(&execConfig.ClientSecret)
|
||||
|
||||
cmd.Flag("pkce", "Use PKCE in the OIDC code flow").
|
||||
Default("true").
|
||||
BoolVar(&execConfig.PKCE)
|
||||
|
||||
cmd.Flag("nonce", "Require a nonce included and verified in the token").
|
||||
Default("true").
|
||||
BoolVar(&execConfig.Nonce)
|
||||
|
||||
cmd.Flag("reauth", "Require reauthentication by the identity provider").
|
||||
Default("false").
|
||||
BoolVar(&execConfig.ReAuth)
|
||||
|
||||
cmd.Arg("agent", "The executable and arguments of the local browser to use").
|
||||
Default("open", "{}").
|
||||
StringsVar(&execConfig.AgentCommant)
|
||||
|
||||
cmd.Action(func(c *kingpin.ParseContext) error {
|
||||
ExecCommand(app, config, &execConfig)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ExecCommand(app *kingpin.Application, config *GlobalConfig, execConfig *ExecConfig) {
|
||||
|
||||
providerConfig := &provider.ProviderConfig{
|
||||
ClientID: execConfig.ClientID,
|
||||
ClientSecret: execConfig.ClientSecret,
|
||||
ProviderURL: execConfig.ProviderURL,
|
||||
PKCE: execConfig.PKCE,
|
||||
Nonce: execConfig.Nonce,
|
||||
ReAuth: execConfig.ReAuth,
|
||||
AgentCommand: execConfig.AgentCommant,
|
||||
}
|
||||
|
||||
authResult, err := provider.Authenticate(providerConfig)
|
||||
app.FatalIfError(err, "Error authenticating to identity provider: %v", err)
|
||||
|
||||
svc := sts.New(session.New())
|
||||
input := &sts.AssumeRoleWithWebIdentityInput{
|
||||
DurationSeconds: aws.Int64(execConfig.Duration),
|
||||
RoleArn: aws.String(execConfig.RoleArn),
|
||||
RoleSessionName: aws.String(authResult.Token.Subject),
|
||||
WebIdentityToken: aws.String(authResult.JWT),
|
||||
}
|
||||
|
||||
assumeRoleResult, err := svc.AssumeRoleWithWebIdentity(input)
|
||||
app.FatalIfError(err, "Unable to assume role: %v", 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"),
|
||||
}
|
||||
|
||||
json, err := json.Marshal(&credentialData)
|
||||
if err != nil {
|
||||
app.Fatalf("Error creating credential json")
|
||||
}
|
||||
fmt.Printf(string(json))
|
||||
}
|
41
cli/global.go
Normal file
41
cli/global.go
Normal file
@ -0,0 +1,41 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/99designs/aws-vault/vault"
|
||||
"github.com/99designs/keyring"
|
||||
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
|
||||
|
||||
AwsConfig *vault.Config
|
||||
Keyring *keyring.Keyring
|
||||
}
|
||||
|
||||
// ConfigureGlobal application arguments and flags
|
||||
func ConfigureGlobal(app *kingpin.Application, config *GlobalConfig) {
|
||||
|
||||
app.Flag("region", "The region in AWS").
|
||||
Default("ap-southeast-2").
|
||||
StringVar(&config.Region)
|
||||
|
||||
app.PreAction(func(c *kingpin.ParseContext) (err error) {
|
||||
|
||||
// Attempt to open the aws-vault keychain
|
||||
keychain, err := keyring.Open(keyring.Config{
|
||||
KeychainName: "aws-vault",
|
||||
ServiceName: "aws-vault",
|
||||
AllowedBackends: []keyring.BackendType{keyring.KeychainBackend},
|
||||
})
|
||||
kingpin.FatalIfError(err, "Could not open aws-vault keychain")
|
||||
|
||||
//config.AwsConfig = awsConfig
|
||||
config.Keyring = &keychain
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
26
config_darwin.go
Normal file
26
config_darwin.go
Normal file
@ -0,0 +1,26 @@
|
||||
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 ""
|
||||
}
|
||||
|
||||
// GetLogPath returns the path that should be used to store logs
|
||||
func GetLogPath() string {
|
||||
return filepath.Join(homeDir(), "./Library/Logs/aws-oidc.log")
|
||||
}
|
19
go.mod
Normal file
19
go.mod
Normal file
@ -0,0 +1,19 @@
|
||||
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/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
|
||||
github.com/aws/aws-sdk-go v1.19.11
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible
|
||||
github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a // indirect
|
||||
github.com/go-ini/ini v1.42.0 // indirect
|
||||
github.com/keybase/go-keychain v0.0.0-20190408194155-7f2ef9fddce6 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // 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/square/go-jose.v2 v2.3.0 // indirect
|
||||
)
|
43
go.sum
Normal file
43
go.sum
Normal file
@ -0,0 +1,43 @@
|
||||
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/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/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/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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
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/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/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
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=
|
||||
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-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/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/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
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/square/go-jose.v2 v2.3.0 h1:nLzhkFyl5bkblqYBoiWJUt5JkWOzmiaBtCxdJAqJd3U=
|
||||
gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
185
provider/provider.go
Normal file
185
provider/provider.go
Normal file
@ -0,0 +1,185 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
oidc "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
|
||||
ReAuth bool
|
||||
AgentCommand []string
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
JWT string
|
||||
Token *oidc.IDToken
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
func Authenticate(p *ProviderConfig) (Result, error) {
|
||||
ctx := context.Background()
|
||||
resultChannel := make(chan Result, 0)
|
||||
errorChannel := make(chan error, 0)
|
||||
|
||||
provider, err := oidc.NewProvider(ctx, p.ProviderURL)
|
||||
if err != nil {
|
||||
return Result{"", nil}, err
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return Result{"", nil}, err
|
||||
}
|
||||
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"},
|
||||
}
|
||||
|
||||
stateData := make([]byte, 32)
|
||||
if _, err = rand.Read(stateData); err != nil {
|
||||
return Result{"", nil}, err
|
||||
}
|
||||
state := base64.URLEncoding.EncodeToString(stateData)
|
||||
|
||||
codeData := make([]byte, 32)
|
||||
if _, err = rand.Read(codeData); err != nil {
|
||||
return Result{"", nil}, 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)
|
||||
_, err = rand.Read(nonceData)
|
||||
nonce := base64.URLEncoding.EncodeToString(nonceData)
|
||||
|
||||
authCodeOptions := []oauth2.AuthCodeOption{}
|
||||
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))
|
||||
}
|
||||
|
||||
if p.ReAuth {
|
||||
authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("acr_values", "onelogin:nist:level:1:re-auth"))
|
||||
}
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
url := config.AuthCodeURL(state, authCodeOptions...)
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
})
|
||||
|
||||
http.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 <- Result{rawIDToken, idToken}
|
||||
})
|
||||
|
||||
// Filter the commands, and replace "{}" with our callback url
|
||||
c := p.AgentCommand[:0]
|
||||
for _, arg := range p.AgentCommand {
|
||||
if arg == "{}" {
|
||||
c = append(c, baseURL)
|
||||
} else {
|
||||
c = append(c, arg)
|
||||
}
|
||||
}
|
||||
cmd := exec.Command(c[0], c[1:]...)
|
||||
cmd.Run()
|
||||
|
||||
server := &http.Server{}
|
||||
go func() {
|
||||
server.Serve(listener)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-errorChannel:
|
||||
server.Shutdown(ctx)
|
||||
return Result{}, err
|
||||
case res := <-resultChannel:
|
||||
server.Shutdown(ctx)
|
||||
return res, nil
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user