updating azure plugin and deps (#4191)
This commit is contained in:
parent
74452d7db7
commit
84ec79a93f
|
@ -0,0 +1,61 @@
|
||||||
|
## CoreOS Community Code of Conduct
|
||||||
|
|
||||||
|
### Contributor Code of Conduct
|
||||||
|
|
||||||
|
As contributors and maintainers of this project, and in the interest of
|
||||||
|
fostering an open and welcoming community, we pledge to respect all people who
|
||||||
|
contribute through reporting issues, posting feature requests, updating
|
||||||
|
documentation, submitting pull requests or patches, and other activities.
|
||||||
|
|
||||||
|
We are committed to making participation in this project a harassment-free
|
||||||
|
experience for everyone, regardless of level of experience, gender, gender
|
||||||
|
identity and expression, sexual orientation, disability, personal appearance,
|
||||||
|
body size, race, ethnicity, age, religion, or nationality.
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery
|
||||||
|
* Personal attacks
|
||||||
|
* Trolling or insulting/derogatory comments
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as physical or electronic addresses, without explicit permission
|
||||||
|
* Other unethical or unprofessional conduct.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct. By adopting this Code of Conduct,
|
||||||
|
project maintainers commit themselves to fairly and consistently applying these
|
||||||
|
principles to every aspect of managing this project. Project maintainers who do
|
||||||
|
not follow or enforce the Code of Conduct may be permanently removed from the
|
||||||
|
project team.
|
||||||
|
|
||||||
|
This code of conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community.
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting a project maintainer, Brandon Philips
|
||||||
|
<brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the Contributor Covenant
|
||||||
|
(http://contributor-covenant.org), version 1.2.0, available at
|
||||||
|
http://contributor-covenant.org/version/1/2/0/
|
||||||
|
|
||||||
|
### CoreOS Events Code of Conduct
|
||||||
|
|
||||||
|
CoreOS events are working conferences intended for professional networking and
|
||||||
|
collaboration in the CoreOS community. Attendees are expected to behave
|
||||||
|
according to professional standards and in accordance with their employer’s
|
||||||
|
policies on appropriate workplace behavior.
|
||||||
|
|
||||||
|
While at CoreOS events or related social networking opportunities, attendees
|
||||||
|
should not engage in discriminatory or offensive speech or actions including
|
||||||
|
but not limited to gender, sexuality, race, age, disability, or religion.
|
||||||
|
Speakers should be especially aware of these concerns.
|
||||||
|
|
||||||
|
CoreOS does not condone any statements by speakers contrary to these standards.
|
||||||
|
CoreOS reserves the right to deny entrance and/or eject from an event (without
|
||||||
|
refund) any individual found to be engaging in discriminatory or offensive
|
||||||
|
speech or actions.
|
||||||
|
|
||||||
|
Please bring any concerns to the immediate attention of designated on-site
|
||||||
|
staff, Brandon Philips <brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.
|
|
@ -23,6 +23,20 @@ import (
|
||||||
// updated.
|
// updated.
|
||||||
const keysExpiryDelta = 30 * time.Second
|
const keysExpiryDelta = 30 * time.Second
|
||||||
|
|
||||||
|
// NewRemoteKeySet returns a KeySet that can validate JSON web tokens by using HTTP
|
||||||
|
// GETs to fetch JSON web token sets hosted at a remote URL. This is automatically
|
||||||
|
// used by NewProvider using the URLs returned by OpenID Connect discovery, but is
|
||||||
|
// exposed for providers that don't support discovery or to prevent round trips to the
|
||||||
|
// discovery URL.
|
||||||
|
//
|
||||||
|
// The returned KeySet is a long lived verifier that caches keys based on cache-control
|
||||||
|
// headers. Reuse a common remote key set instead of creating new ones as needed.
|
||||||
|
//
|
||||||
|
// The behavior of the returned KeySet is undefined once the context is canceled.
|
||||||
|
func NewRemoteKeySet(ctx context.Context, jwksURL string) KeySet {
|
||||||
|
return newRemoteKeySet(ctx, jwksURL, time.Now)
|
||||||
|
}
|
||||||
|
|
||||||
func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *remoteKeySet {
|
func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *remoteKeySet {
|
||||||
if now == nil {
|
if now == nil {
|
||||||
now = time.Now
|
now = time.Now
|
||||||
|
@ -79,6 +93,14 @@ func (i *inflight) result() ([]jose.JSONWebKey, error) {
|
||||||
return i.keys, i.err
|
return i.keys, i.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *remoteKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
|
||||||
|
jws, err := jose.ParseSigned(jwt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
|
||||||
|
}
|
||||||
|
return r.verify(ctx, jws)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *remoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
|
func (r *remoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
|
||||||
// We don't support JWTs signed with multiple signatures.
|
// We don't support JWTs signed with multiple signatures.
|
||||||
keyID := ""
|
keyID := ""
|
||||||
|
|
|
@ -3,9 +3,13 @@ package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -31,6 +35,11 @@ const (
|
||||||
ScopeOfflineAccess = "offline_access"
|
ScopeOfflineAccess = "offline_access"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNoAtHash = errors.New("id token did not have an access token hash")
|
||||||
|
errInvalidAtHash = errors.New("access token hash does not match value in ID token")
|
||||||
|
)
|
||||||
|
|
||||||
// ClientContext returns a new Context that carries the provided HTTP client.
|
// ClientContext returns a new Context that carries the provided HTTP client.
|
||||||
//
|
//
|
||||||
// This method sets the same context key used by the golang.org/x/oauth2 package,
|
// This method sets the same context key used by the golang.org/x/oauth2 package,
|
||||||
|
@ -64,7 +73,7 @@ type Provider struct {
|
||||||
// Raw claims returned by the server.
|
// Raw claims returned by the server.
|
||||||
rawClaims []byte
|
rawClaims []byte
|
||||||
|
|
||||||
remoteKeySet *remoteKeySet
|
remoteKeySet KeySet
|
||||||
}
|
}
|
||||||
|
|
||||||
type cachedKeys struct {
|
type cachedKeys struct {
|
||||||
|
@ -120,7 +129,7 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, error) {
|
||||||
tokenURL: p.TokenURL,
|
tokenURL: p.TokenURL,
|
||||||
userInfoURL: p.UserInfoURL,
|
userInfoURL: p.UserInfoURL,
|
||||||
rawClaims: body,
|
rawClaims: body,
|
||||||
remoteKeySet: newRemoteKeySet(ctx, p.JWKSURL, time.Now),
|
remoteKeySet: NewRemoteKeySet(ctx, p.JWKSURL),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,6 +251,14 @@ type IDToken struct {
|
||||||
// and it's the user's responsibility to ensure it contains a valid value.
|
// and it's the user's responsibility to ensure it contains a valid value.
|
||||||
Nonce string
|
Nonce string
|
||||||
|
|
||||||
|
// at_hash claim, if set in the ID token. Callers can verify an access token
|
||||||
|
// that corresponds to the ID token using the VerifyAccessToken method.
|
||||||
|
AccessTokenHash string
|
||||||
|
|
||||||
|
// signature algorithm used for ID token, needed to compute a verification hash of an
|
||||||
|
// access token
|
||||||
|
sigAlgorithm string
|
||||||
|
|
||||||
// Raw payload of the id_token.
|
// Raw payload of the id_token.
|
||||||
claims []byte
|
claims []byte
|
||||||
}
|
}
|
||||||
|
@ -267,6 +284,34 @@ func (i *IDToken) Claims(v interface{}) error {
|
||||||
return json.Unmarshal(i.claims, v)
|
return json.Unmarshal(i.claims, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyAccessToken verifies that the hash of the access token that corresponds to the iD token
|
||||||
|
// matches the hash in the id token. It returns an error if the hashes don't match.
|
||||||
|
// It is the caller's responsibility to ensure that the optional access token hash is present for the ID token
|
||||||
|
// before calling this method. See https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
|
||||||
|
func (i *IDToken) VerifyAccessToken(accessToken string) error {
|
||||||
|
if i.AccessTokenHash == "" {
|
||||||
|
return errNoAtHash
|
||||||
|
}
|
||||||
|
var h hash.Hash
|
||||||
|
switch i.sigAlgorithm {
|
||||||
|
case RS256, ES256, PS256:
|
||||||
|
h = sha256.New()
|
||||||
|
case RS384, ES384, PS384:
|
||||||
|
h = sha512.New384()
|
||||||
|
case RS512, ES512, PS512:
|
||||||
|
h = sha512.New()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("oidc: unsupported signing algorithm %q", i.sigAlgorithm)
|
||||||
|
}
|
||||||
|
h.Write([]byte(accessToken)) // hash documents that Write will never return an error
|
||||||
|
sum := h.Sum(nil)[:h.Size()/2]
|
||||||
|
actual := base64.RawURLEncoding.EncodeToString(sum)
|
||||||
|
if actual != i.AccessTokenHash {
|
||||||
|
return errInvalidAtHash
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type idToken struct {
|
type idToken struct {
|
||||||
Issuer string `json:"iss"`
|
Issuer string `json:"iss"`
|
||||||
Subject string `json:"sub"`
|
Subject string `json:"sub"`
|
||||||
|
@ -274,6 +319,7 @@ type idToken struct {
|
||||||
Expiry jsonTime `json:"exp"`
|
Expiry jsonTime `json:"exp"`
|
||||||
IssuedAt jsonTime `json:"iat"`
|
IssuedAt jsonTime `json:"iat"`
|
||||||
Nonce string `json:"nonce"`
|
Nonce string `json:"nonce"`
|
||||||
|
AtHash string `json:"at_hash"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type audience []string
|
type audience []string
|
||||||
|
|
|
@ -11,6 +11,6 @@ LINTABLE=$( go list -tags=golint -f '
|
||||||
|
|
||||||
go test -v -i -race github.com/coreos/go-oidc/...
|
go test -v -i -race github.com/coreos/go-oidc/...
|
||||||
go test -v -race github.com/coreos/go-oidc/...
|
go test -v -race github.com/coreos/go-oidc/...
|
||||||
golint $LINTABLE
|
golint -set_exit_status $LINTABLE
|
||||||
go vet github.com/coreos/go-oidc/...
|
go vet github.com/coreos/go-oidc/...
|
||||||
go build -v ./example/...
|
go build -v ./example/...
|
||||||
|
|
|
@ -19,19 +19,54 @@ const (
|
||||||
issuerGoogleAccountsNoScheme = "accounts.google.com"
|
issuerGoogleAccountsNoScheme = "accounts.google.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
// keySet is an interface that lets us stub out verification policies for
|
// KeySet is a set of publc JSON Web Keys that can be used to validate the signature
|
||||||
// testing. Outside of testing, it's always backed by a remoteKeySet.
|
// of JSON web tokens. This is expected to be backed by a remote key set through
|
||||||
type keySet interface {
|
// provider metadata discovery or an in-memory set of keys delivered out-of-band.
|
||||||
verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error)
|
type KeySet interface {
|
||||||
|
// VerifySignature parses the JSON web token, verifies the signature, and returns
|
||||||
|
// the raw payload. Header and claim fields are validated by other parts of the
|
||||||
|
// package. For example, the KeySet does not need to check values such as signature
|
||||||
|
// algorithm, issuer, and audience since the IDTokenVerifier validates these values
|
||||||
|
// independently.
|
||||||
|
//
|
||||||
|
// If VerifySignature makes HTTP requests to verify the token, it's expected to
|
||||||
|
// use any HTTP client associated with the context through ClientContext.
|
||||||
|
VerifySignature(ctx context.Context, jwt string) (payload []byte, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDTokenVerifier provides verification for ID Tokens.
|
// IDTokenVerifier provides verification for ID Tokens.
|
||||||
type IDTokenVerifier struct {
|
type IDTokenVerifier struct {
|
||||||
keySet keySet
|
keySet KeySet
|
||||||
config *Config
|
config *Config
|
||||||
issuer string
|
issuer string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewVerifier returns a verifier manually constructed from a key set and issuer URL.
|
||||||
|
//
|
||||||
|
// It's easier to use provider discovery to construct an IDTokenVerifier than creating
|
||||||
|
// one directly. This method is intended to be used with provider that don't support
|
||||||
|
// metadata discovery, or avoiding round trips when the key set URL is already known.
|
||||||
|
//
|
||||||
|
// This constructor can be used to create a verifier directly using the issuer URL and
|
||||||
|
// JSON Web Key Set URL without using discovery:
|
||||||
|
//
|
||||||
|
// keySet := oidc.NewRemoteKeySet(ctx, "https://www.googleapis.com/oauth2/v3/certs")
|
||||||
|
// verifier := oidc.NewVerifier("https://accounts.google.com", keySet, config)
|
||||||
|
//
|
||||||
|
// Since KeySet is an interface, this constructor can also be used to supply custom
|
||||||
|
// public key sources. For example, if a user wanted to supply public keys out-of-band
|
||||||
|
// and hold them statically in-memory:
|
||||||
|
//
|
||||||
|
// // Custom KeySet implementation.
|
||||||
|
// keySet := newStatisKeySet(publicKeys...)
|
||||||
|
//
|
||||||
|
// // Verifier uses the custom KeySet implementation.
|
||||||
|
// verifier := oidc.NewVerifier("https://auth.example.com", keySet, config)
|
||||||
|
//
|
||||||
|
func NewVerifier(issuerURL string, keySet KeySet, config *Config) *IDTokenVerifier {
|
||||||
|
return &IDTokenVerifier{keySet: keySet, config: config, issuer: issuerURL}
|
||||||
|
}
|
||||||
|
|
||||||
// Config is the configuration for an IDTokenVerifier.
|
// Config is the configuration for an IDTokenVerifier.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Expected audience of the token. For a majority of the cases this is expected to be
|
// Expected audience of the token. For a majority of the cases this is expected to be
|
||||||
|
@ -59,21 +94,7 @@ type Config struct {
|
||||||
// The returned IDTokenVerifier is tied to the Provider's context and its behavior is
|
// The returned IDTokenVerifier is tied to the Provider's context and its behavior is
|
||||||
// undefined once the Provider's context is canceled.
|
// undefined once the Provider's context is canceled.
|
||||||
func (p *Provider) Verifier(config *Config) *IDTokenVerifier {
|
func (p *Provider) Verifier(config *Config) *IDTokenVerifier {
|
||||||
|
return NewVerifier(p.issuer, p.remoteKeySet, config)
|
||||||
return newVerifier(p.remoteKeySet, config, p.issuer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newVerifier(keySet keySet, config *Config, issuer string) *IDTokenVerifier {
|
|
||||||
// If SupportedSigningAlgs is empty defaults to only support RS256.
|
|
||||||
if len(config.SupportedSigningAlgs) == 0 {
|
|
||||||
config.SupportedSigningAlgs = []string{RS256}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &IDTokenVerifier{
|
|
||||||
keySet: keySet,
|
|
||||||
config: config,
|
|
||||||
issuer: issuer,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseJWT(p string) ([]byte, error) {
|
func parseJWT(p string) ([]byte, error) {
|
||||||
|
@ -135,13 +156,14 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok
|
||||||
}
|
}
|
||||||
|
|
||||||
t := &IDToken{
|
t := &IDToken{
|
||||||
Issuer: token.Issuer,
|
Issuer: token.Issuer,
|
||||||
Subject: token.Subject,
|
Subject: token.Subject,
|
||||||
Audience: []string(token.Audience),
|
Audience: []string(token.Audience),
|
||||||
Expiry: time.Time(token.Expiry),
|
Expiry: time.Time(token.Expiry),
|
||||||
IssuedAt: time.Time(token.IssuedAt),
|
IssuedAt: time.Time(token.IssuedAt),
|
||||||
Nonce: token.Nonce,
|
Nonce: token.Nonce,
|
||||||
claims: payload,
|
AccessTokenHash: token.AtHash,
|
||||||
|
claims: payload,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check issuer.
|
// Check issuer.
|
||||||
|
@ -165,7 +187,7 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok
|
||||||
return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, t.Audience)
|
return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, t.Audience)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("oidc: Invalid configuration. ClientID must be provided or SkipClientIDCheck must be set.")
|
return nil, fmt.Errorf("oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,11 +212,18 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok
|
||||||
}
|
}
|
||||||
|
|
||||||
sig := jws.Signatures[0]
|
sig := jws.Signatures[0]
|
||||||
if len(v.config.SupportedSigningAlgs) != 0 && !contains(v.config.SupportedSigningAlgs, sig.Header.Algorithm) {
|
supportedSigAlgs := v.config.SupportedSigningAlgs
|
||||||
return nil, fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", v.config.SupportedSigningAlgs, sig.Header.Algorithm)
|
if len(supportedSigAlgs) == 0 {
|
||||||
|
supportedSigAlgs = []string{RS256}
|
||||||
}
|
}
|
||||||
|
|
||||||
gotPayload, err := v.keySet.verify(ctx, jws)
|
if !contains(supportedSigAlgs, sig.Header.Algorithm) {
|
||||||
|
return nil, fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", supportedSigAlgs, sig.Header.Algorithm)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.sigAlgorithm = sig.Header.Algorithm
|
||||||
|
|
||||||
|
gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to verify signature: %v", err)
|
return nil, fmt.Errorf("failed to verify signature: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,11 @@ package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2017-12-01/compute"
|
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2017-12-01/compute"
|
||||||
|
@ -11,10 +14,9 @@ import (
|
||||||
"github.com/Azure/go-autorest/autorest/azure"
|
"github.com/Azure/go-autorest/autorest/azure"
|
||||||
"github.com/Azure/go-autorest/autorest/azure/auth"
|
"github.com/Azure/go-autorest/autorest/azure/auth"
|
||||||
oidc "github.com/coreos/go-oidc"
|
oidc "github.com/coreos/go-oidc"
|
||||||
)
|
"github.com/hashicorp/errwrap"
|
||||||
|
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||||
const (
|
"golang.org/x/oauth2"
|
||||||
issuerBaseURI = "https://sts.windows.net"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type computeClient interface {
|
type computeClient interface {
|
||||||
|
@ -30,39 +32,66 @@ type provider interface {
|
||||||
ComputeClient(subscriptionID string) computeClient
|
ComputeClient(subscriptionID string) computeClient
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ provider = &azureProvider{}
|
|
||||||
|
|
||||||
type azureProvider struct {
|
type azureProvider struct {
|
||||||
settings *azureSettings
|
oidcVerifier *oidc.IDTokenVerifier
|
||||||
oidcProvider *oidc.Provider
|
|
||||||
authorizer autorest.Authorizer
|
authorizer autorest.Authorizer
|
||||||
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAzureProvider(config *azureConfig) (*azureProvider, error) {
|
type oidcDiscoveryInfo struct {
|
||||||
|
Issuer string `json:"issuer"`
|
||||||
|
JWKSURL string `json:"jwks_uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAzureProvider(config *azureConfig) (*azureProvider, error) {
|
||||||
|
httpClient := cleanhttp.DefaultClient()
|
||||||
settings, err := getAzureSettings(config)
|
settings, err := getAzureSettings(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
issuer := fmt.Sprintf("%s/%s/", issuerBaseURI, settings.TenantID)
|
// In many OIDC providers, the discovery endpoint matches the issuer. For Azure AD, the discovery
|
||||||
oidcProvider, err := oidc.NewProvider(context.Background(), issuer)
|
// endpoint is the AD endpoint which does not match the issuer defined in the discovery payload. This
|
||||||
|
// makes a request to the discovery URL to determine the issuer and key set information to configure
|
||||||
|
// the OIDC verifier
|
||||||
|
discoveryURL := fmt.Sprintf("%s%s/.well-known/openid-configuration", settings.Environment.ActiveDirectoryEndpoint, settings.TenantID)
|
||||||
|
resp, err := httpClient.Get(discoveryURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
provider := &azureProvider{
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
settings: settings,
|
if err != nil {
|
||||||
oidcProvider: oidcProvider,
|
return nil, errwrap.Wrapf("unable to read response body: {{err}}", err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("%s: %s", resp.Status, body)
|
||||||
|
}
|
||||||
|
var discoveryInfo oidcDiscoveryInfo
|
||||||
|
if err := json.Unmarshal(body, &discoveryInfo); err != nil {
|
||||||
|
return nil, errwrap.Wrapf("unable to unmarshal discovery url: {{err}}", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OAuth2 client for querying VM data
|
// Create a remote key set from the discovery endpoint
|
||||||
|
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient)
|
||||||
|
remoteKeySet := oidc.NewRemoteKeySet(ctx, discoveryInfo.JWKSURL)
|
||||||
|
|
||||||
|
verifierConfig := &oidc.Config{
|
||||||
|
ClientID: settings.Resource,
|
||||||
|
SupportedSigningAlgs: []string{oidc.RS256},
|
||||||
|
}
|
||||||
|
oidcVerifier := oidc.NewVerifier(discoveryInfo.Issuer, remoteKeySet, verifierConfig)
|
||||||
|
|
||||||
|
// Create an OAuth2 client for retrieving VM data
|
||||||
|
var authorizer autorest.Authorizer
|
||||||
switch {
|
switch {
|
||||||
// Use environment/config first
|
// Use environment/config first
|
||||||
case settings.ClientSecret != "":
|
case settings.ClientSecret != "":
|
||||||
config := auth.NewClientCredentialsConfig(settings.ClientID, settings.ClientSecret, settings.TenantID)
|
config := auth.NewClientCredentialsConfig(settings.ClientID, settings.ClientSecret, settings.TenantID)
|
||||||
config.AADEndpoint = settings.Environment.ActiveDirectoryEndpoint
|
config.AADEndpoint = settings.Environment.ActiveDirectoryEndpoint
|
||||||
config.Resource = settings.Environment.ResourceManagerEndpoint
|
config.Resource = settings.Environment.ResourceManagerEndpoint
|
||||||
provider.authorizer, err = config.Authorizer()
|
authorizer, err = config.Authorizer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -70,25 +99,27 @@ func NewAzureProvider(config *azureConfig) (*azureProvider, error) {
|
||||||
default:
|
default:
|
||||||
config := auth.NewMSIConfig()
|
config := auth.NewMSIConfig()
|
||||||
config.Resource = settings.Environment.ResourceManagerEndpoint
|
config.Resource = settings.Environment.ResourceManagerEndpoint
|
||||||
provider.authorizer, err = config.Authorizer()
|
authorizer, err = config.Authorizer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return provider, nil
|
|
||||||
|
return &azureProvider{
|
||||||
|
authorizer: authorizer,
|
||||||
|
oidcVerifier: oidcVerifier,
|
||||||
|
httpClient: httpClient,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *azureProvider) Verifier() tokenVerifier {
|
func (p *azureProvider) Verifier() tokenVerifier {
|
||||||
verifierConfig := &oidc.Config{
|
return p.oidcVerifier
|
||||||
ClientID: p.settings.Resource,
|
|
||||||
SupportedSigningAlgs: []string{oidc.RS256},
|
|
||||||
}
|
|
||||||
return p.oidcProvider.Verifier(verifierConfig)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *azureProvider) ComputeClient(subscriptionID string) computeClient {
|
func (p *azureProvider) ComputeClient(subscriptionID string) computeClient {
|
||||||
client := compute.NewVirtualMachinesClient(subscriptionID)
|
client := compute.NewVirtualMachinesClient(subscriptionID)
|
||||||
client.Authorizer = p.authorizer
|
client.Authorizer = p.authorizer
|
||||||
|
client.Sender = p.httpClient
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,8 @@ package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/hashicorp/go-cleanhttp"
|
|
||||||
"github.com/hashicorp/vault/logical"
|
"github.com/hashicorp/vault/logical"
|
||||||
"github.com/hashicorp/vault/logical/framework"
|
"github.com/hashicorp/vault/logical/framework"
|
||||||
)
|
)
|
||||||
|
@ -23,8 +21,7 @@ type azureAuthBackend struct {
|
||||||
|
|
||||||
l sync.RWMutex
|
l sync.RWMutex
|
||||||
|
|
||||||
provider provider
|
provider provider
|
||||||
httpClient *http.Client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Backend(c *logical.BackendConfig) *azureAuthBackend {
|
func Backend(c *logical.BackendConfig) *azureAuthBackend {
|
||||||
|
@ -51,7 +48,6 @@ func Backend(c *logical.BackendConfig) *azureAuthBackend {
|
||||||
pathsRole(b),
|
pathsRole(b),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
b.httpClient = cleanhttp.DefaultClient()
|
|
||||||
|
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
@ -81,7 +77,7 @@ func (b *azureAuthBackend) getProvider(config *azureConfig) (provider, error) {
|
||||||
return b.provider, nil
|
return b.provider, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err := NewAzureProvider(config)
|
provider, err := newAzureProvider(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -199,32 +199,32 @@ func (b *azureAuthBackend) verifyResource(ctx context.Context, subscriptionID, r
|
||||||
|
|
||||||
// Ensure the principal id for the VM matches the verified token OID
|
// Ensure the principal id for the VM matches the verified token OID
|
||||||
if vm.Identity == nil {
|
if vm.Identity == nil {
|
||||||
return fmt.Errorf("vm client did not return identity information")
|
return errors.New("vm client did not return identity information")
|
||||||
}
|
}
|
||||||
if vm.Identity.PrincipalID == nil {
|
if vm.Identity.PrincipalID == nil {
|
||||||
return fmt.Errorf("vm principal id is empty")
|
return errors.New("vm principal id is empty")
|
||||||
}
|
}
|
||||||
if to.String(vm.Identity.PrincipalID) != claims.ObjectID {
|
if to.String(vm.Identity.PrincipalID) != claims.ObjectID {
|
||||||
return fmt.Errorf("token object id does not match virtual machine principal id")
|
return errors.New("token object id does not match virtual machine principal id")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check bound subsriptions
|
// Check bound subsriptions
|
||||||
if len(role.BoundSubscriptionsIDs) > 0 && !strutil.StrListContains(role.BoundSubscriptionsIDs, subscriptionID) {
|
if len(role.BoundSubscriptionsIDs) > 0 && !strutil.StrListContains(role.BoundSubscriptionsIDs, subscriptionID) {
|
||||||
return fmt.Errorf("subscription not authoirzed")
|
return errors.New("subscription not authorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check bound resource groups
|
// Check bound resource groups
|
||||||
if len(role.BoundResourceGroups) > 0 && !strutil.StrListContains(role.BoundResourceGroups, resourceGroupName) {
|
if len(role.BoundResourceGroups) > 0 && !strutil.StrListContains(role.BoundResourceGroups, resourceGroupName) {
|
||||||
return fmt.Errorf("resource group not authoirzed")
|
return errors.New("resource group not authorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check bound locations
|
// Check bound locations
|
||||||
if len(role.BoundLocations) > 0 {
|
if len(role.BoundLocations) > 0 {
|
||||||
if vm.Location == nil {
|
if vm.Location == nil {
|
||||||
return fmt.Errorf("vm location is empty")
|
return errors.New("vm location is empty")
|
||||||
}
|
}
|
||||||
if !strutil.StrListContains(role.BoundLocations, to.String(vm.Location)) {
|
if !strutil.StrListContains(role.BoundLocations, to.String(vm.Location)) {
|
||||||
return fmt.Errorf("token object id does not match virtual machine principal id")
|
return errors.New("location not authorized")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,13 +234,13 @@ func (b *azureAuthBackend) verifyResource(ctx context.Context, subscriptionID, r
|
||||||
func (b *azureAuthBackend) pathLoginRenew(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
func (b *azureAuthBackend) pathLoginRenew(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
roleName := req.Auth.InternalData["role"].(string)
|
roleName := req.Auth.InternalData["role"].(string)
|
||||||
if roleName == "" {
|
if roleName == "" {
|
||||||
return nil, fmt.Errorf("failed to fetch role_name during renewal")
|
return nil, errors.New("failed to fetch role_name during renewal")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that the Role still exists.
|
// Ensure that the Role still exists.
|
||||||
role, err := b.role(ctx, req.Storage, roleName)
|
role, err := b.role(ctx, req.Storage, roleName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to validate role %s during renewal:%s", roleName, err)
|
return nil, errwrap.Wrapf(fmt.Sprintf("failed to validate role %s during renewal: {{err}}", roleName), err)
|
||||||
}
|
}
|
||||||
if role == nil {
|
if role == nil {
|
||||||
return nil, fmt.Errorf("role %s does not exist during renewal", roleName)
|
return nil, fmt.Errorf("role %s does not exist during renewal", roleName)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package plugin
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -57,24 +58,29 @@ duration specified by this value. At each renewal, the token's
|
||||||
TTL will be set to the value of this parameter.`,
|
TTL will be set to the value of this parameter.`,
|
||||||
},
|
},
|
||||||
"bound_subscription_ids": &framework.FieldSchema{
|
"bound_subscription_ids": &framework.FieldSchema{
|
||||||
Type: framework.TypeCommaStringSlice,
|
Type: framework.TypeCommaStringSlice,
|
||||||
Description: ``,
|
Description: `Comma-separated list of subscription ids that login
|
||||||
|
is restricted to.`,
|
||||||
},
|
},
|
||||||
"bound_resource_groups": &framework.FieldSchema{
|
"bound_resource_groups": &framework.FieldSchema{
|
||||||
Type: framework.TypeCommaStringSlice,
|
Type: framework.TypeCommaStringSlice,
|
||||||
Description: ``,
|
Description: `Comma-separated list of resource groups that login
|
||||||
|
is restricted to.`,
|
||||||
},
|
},
|
||||||
"bound_group_ids": &framework.FieldSchema{
|
"bound_group_ids": &framework.FieldSchema{
|
||||||
Type: framework.TypeCommaStringSlice,
|
Type: framework.TypeCommaStringSlice,
|
||||||
Description: ``,
|
Description: `Comma-separated list of group ids that login
|
||||||
|
is restricted to.`,
|
||||||
},
|
},
|
||||||
"bound_service_principal_ids": &framework.FieldSchema{
|
"bound_service_principal_ids": &framework.FieldSchema{
|
||||||
Type: framework.TypeCommaStringSlice,
|
Type: framework.TypeCommaStringSlice,
|
||||||
Description: ``,
|
Description: `Comma-separated list of service principal ids that login
|
||||||
|
is restricted to.`,
|
||||||
},
|
},
|
||||||
"bound_locations": &framework.FieldSchema{
|
"bound_locations": &framework.FieldSchema{
|
||||||
Type: framework.TypeCommaStringSlice,
|
Type: framework.TypeCommaStringSlice,
|
||||||
Description: ``,
|
Description: `Comma-separated list of locations that login
|
||||||
|
is restricted to.`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ExistenceCheck: b.pathRoleExistenceCheck,
|
ExistenceCheck: b.pathRoleExistenceCheck,
|
||||||
|
@ -226,7 +232,7 @@ func (b *azureAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logica
|
||||||
// Create a new entry object if this is a CreateOperation
|
// Create a new entry object if this is a CreateOperation
|
||||||
if role == nil {
|
if role == nil {
|
||||||
if req.Operation == logical.UpdateOperation {
|
if req.Operation == logical.UpdateOperation {
|
||||||
return nil, fmt.Errorf("role entry not found during update operation")
|
return nil, errors.New("role entry not found during update operation")
|
||||||
}
|
}
|
||||||
role = new(azureRole)
|
role = new(azureRole)
|
||||||
}
|
}
|
||||||
|
|
|
@ -643,10 +643,10 @@
|
||||||
"revisionTime": "2018-02-10T00:12:50Z"
|
"revisionTime": "2018-02-10T00:12:50Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "8o2JlMI+LchFOAR1uGZ3u6cROkY=",
|
"checksumSHA1": "b6LGjzkSCWdrqH6dnS/Bf+BfdPU=",
|
||||||
"path": "github.com/coreos/go-oidc",
|
"path": "github.com/coreos/go-oidc",
|
||||||
"revision": "a93f71fdfe73d2c0f5413c0565eea0af6523a6df",
|
"revision": "065b426bd41667456c1a924468f507673629c46b",
|
||||||
"revisionTime": "2017-10-02T15:50:02Z"
|
"revisionTime": "2018-01-17T17:01:38Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "97BsbXOiZ8+Kr+LIuZkQFtSj7H4=",
|
"checksumSHA1": "97BsbXOiZ8+Kr+LIuZkQFtSj7H4=",
|
||||||
|
@ -1279,10 +1279,10 @@
|
||||||
"revisionTime": "2018-02-03T00:31:27Z"
|
"revisionTime": "2018-02-03T00:31:27Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "LmozvTcHq18DkRN7aloRXug7Yqg=",
|
"checksumSHA1": "C6sgT2K7ZAv4y/MFMWppVs7oqTM=",
|
||||||
"path": "github.com/hashicorp/vault-plugin-auth-azure/plugin",
|
"path": "github.com/hashicorp/vault-plugin-auth-azure/plugin",
|
||||||
"revision": "ba506e5d0aae1d115c2b5ad4ea8d3f06be77b45a",
|
"revision": "29767bb64e3fd21c7661a5008aa62a2eda1aebb8",
|
||||||
"revisionTime": "2018-03-21T20:06:11Z"
|
"revisionTime": "2018-03-23T20:42:11Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "xApm6AbAzE3wMLMaozaZ53pb7AU=",
|
"checksumSHA1": "xApm6AbAzE3wMLMaozaZ53pb7AU=",
|
||||||
|
|
Loading…
Reference in New Issue