Update auth-jwt to v0.7.0 (#9320)

Adds support for distributed groups claims on Azure, necessary when a
user is a member of more than 200 groups.
This commit is contained in:
Theron Voran 2020-06-29 10:23:32 -07:00 committed by GitHub
parent 6bd5674345
commit c943235288
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 351 additions and 36 deletions

2
go.mod
View File

@ -70,7 +70,7 @@ require (
github.com/hashicorp/vault-plugin-auth-centrify v0.5.5
github.com/hashicorp/vault-plugin-auth-cf v0.5.4
github.com/hashicorp/vault-plugin-auth-gcp v0.6.2-0.20200428223335-82bd3a3ad5b3
github.com/hashicorp/vault-plugin-auth-jwt v0.6.2
github.com/hashicorp/vault-plugin-auth-jwt v0.7.0
github.com/hashicorp/vault-plugin-auth-kerberos v0.1.6
github.com/hashicorp/vault-plugin-auth-kubernetes v0.6.2
github.com/hashicorp/vault-plugin-auth-oci v0.5.5

6
go.sum
View File

@ -531,8 +531,9 @@ github.com/hashicorp/vault-plugin-auth-gcp v0.5.1/go.mod h1:eLj92eX8MPI4vY1jaazV
github.com/hashicorp/vault-plugin-auth-gcp v0.6.1 h1:WXTuja3WC2BdZekYCnzuZGoVvZTAGH8kSDUHzOK2PQY=
github.com/hashicorp/vault-plugin-auth-gcp v0.6.1/go.mod h1:8eBRzg+JIhAaDBfDndDAQKIhDrQ3WW8OPklxAYftNFs=
github.com/hashicorp/vault-plugin-auth-gcp v0.6.2-0.20200428223335-82bd3a3ad5b3/go.mod h1:U0fkAlxWTEyQ74lx8wlGdD493lP1DD/qpMjXgOEbwj0=
github.com/hashicorp/vault-plugin-auth-jwt v0.6.2 h1:fp6Rk89iPjDS8dyEK7lEauYE/UhkgkHbmwRZKuQA01U=
github.com/hashicorp/vault-plugin-auth-jwt v0.6.2/go.mod h1:SFadxIfoLGzugEjwUUmUaCGbsYEz2/jJymZDDQjEqYg=
github.com/hashicorp/vault-plugin-auth-jwt v0.7.0 h1:lHg02BB7IpUQbJStAPmGyS3KnZJC7PSEvc5LOZNPjHM=
github.com/hashicorp/vault-plugin-auth-jwt v0.7.0/go.mod h1:ZJJy4b0H3N7CSoJ6iPlWhV9EjHLSoB5NhP06CNm6ImU=
github.com/hashicorp/vault-plugin-auth-kerberos v0.1.5 h1:knWedzZ51g8Aj6Hyi1ATlQ/7jEx6nJeqFoCoHSrbQFI=
github.com/hashicorp/vault-plugin-auth-kerberos v0.1.5/go.mod h1:r4UqWITHYKmBeAMKPWqLo4V8bl/wNqoSIaQcMpeK9ss=
github.com/hashicorp/vault-plugin-auth-kerberos v0.1.6 h1:l5wu8J7aiQBLsTtkKhf1QQjGoeVjcfcput+uJ/pu2MM=
@ -697,8 +698,9 @@ github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc
github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg=
github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/pointerstructure v0.0.0-20190430161007-f252a8fd71c8 h1:1CO5wil3HuiVLrUQ2ovSTO+6AfNOA5EMkHHVyHE9IwA=
github.com/mitchellh/pointerstructure v0.0.0-20190430161007-f252a8fd71c8/go.mod h1:k4XwG94++jLVsSiTxo7qdIfXA9pj9EAeo0QsNNJOLZ8=
github.com/mitchellh/pointerstructure v1.0.0 h1:ATSdz4NWrmWPOF1CeCBU4sMCno2hgqdbSrRPFWQSVZI=
github.com/mitchellh/pointerstructure v1.0.0/go.mod h1:k4XwG94++jLVsSiTxo7qdIfXA9pj9EAeo0QsNNJOLZ8=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=
github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=

View File

@ -104,7 +104,28 @@ $ vault auth enable -plugin-name='jwt' plugin
Successfully enabled 'plugin' at 'jwt'!
```
#### Tests
### Provider-specific handling
Provider-specific handling can be added by writing an object that conforms to
one or more interfaces in [provider_config.go](provider_config.go). Some
interfaces will be required, like [CustomProvider](provider_config.go), and
others will be invoked if present during the login process (e.g. GroupsFetcher).
The interfaces themselves will be small (usually a single method) as it is
expected that the parts of the login that need specialization will be different
per provider. This pattern allows us to start with a minimal set and add
interfaces as necessary.
If a custom provider is configured on the backend object and satisfies a given
interface, the interface will be used during the relevant part of the login
flow. e.g. after an ID token has been received, the custom provider's
UserInfoFetcher interface will be used, if present, to fetch and merge
additional identity data.
The custom handlers will be standalone objects defined in their own file (one
per provider). They'll be part of the main jwtauth package to avoid potential
circular import issues.
### Tests
If you are developing this plugin and want to verify it is still
functioning (and you haven't broken anything else), we recommend

View File

@ -1,6 +1,6 @@
module github.com/hashicorp/vault-plugin-auth-jwt
go 1.13
go 1.14
require (
github.com/coreos/go-oidc v2.1.0+incompatible
@ -10,14 +10,16 @@ require (
github.com/hashicorp/go-hclog v0.12.0
github.com/hashicorp/go-sockaddr v1.0.2
github.com/hashicorp/go-uuid v1.0.2
github.com/hashicorp/vault/api v1.0.5-0.20200317185738-82f498082f02
github.com/hashicorp/vault/sdk v0.1.14-0.20200317185738-82f498082f02
github.com/hashicorp/go-version v1.2.0 // indirect
github.com/hashicorp/vault/api v1.0.5-0.20200215224050-f6547fa8e820
github.com/hashicorp/vault/sdk v0.1.14-0.20200215224050-f6547fa8e820
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
github.com/mitchellh/pointerstructure v0.0.0-20190430161007-f252a8fd71c8
github.com/mitchellh/pointerstructure v1.0.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/ryanuber/go-glob v1.0.0
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
github.com/stretchr/testify v1.3.0
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/text v0.3.2 // indirect
google.golang.org/appengine v1.5.0 // indirect

View File

@ -62,17 +62,19 @@ github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2I
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/vault/api v1.0.5-0.20200317185738-82f498082f02 h1:OGEV0U0+lb8SP5aZA1m456Sr3MYxFel2awVr55QRri0=
github.com/hashicorp/vault/api v1.0.5-0.20200317185738-82f498082f02/go.mod h1:3f12BMfgDGjTsTtIUj+ZKZwSobQpZtYGFIEehOv5z1o=
github.com/hashicorp/vault/api v1.0.5-0.20200215224050-f6547fa8e820 h1:biZidYDDEWnuOI9mXnJre8lwHKhb5ym85aSXk3oz/dc=
github.com/hashicorp/vault/api v1.0.5-0.20200215224050-f6547fa8e820/go.mod h1:3f12BMfgDGjTsTtIUj+ZKZwSobQpZtYGFIEehOv5z1o=
github.com/hashicorp/vault/sdk v0.1.14-0.20200215195600-2ca765f0a500 h1:tiMX2ewq4ble+e2zENzBvaH2dMoFHe80NbnrF5Ir9Kk=
github.com/hashicorp/vault/sdk v0.1.14-0.20200215195600-2ca765f0a500/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10=
github.com/hashicorp/vault/sdk v0.1.14-0.20200317185738-82f498082f02 h1:vVrOAVfunVvkTkE9iF3Fe1+PGPLwGIp3nP4qgHGrHFs=
github.com/hashicorp/vault/sdk v0.1.14-0.20200317185738-82f498082f02/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10=
github.com/hashicorp/vault/sdk v0.1.14-0.20200215224050-f6547fa8e820 h1:TmDZ1sS6gU0hFeFlFuyJVUwRPEzifZIHCBeS2WF2uSc=
github.com/hashicorp/vault/sdk v0.1.14-0.20200215224050-f6547fa8e820/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
@ -95,8 +97,8 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/pointerstructure v0.0.0-20190430161007-f252a8fd71c8 h1:1CO5wil3HuiVLrUQ2ovSTO+6AfNOA5EMkHHVyHE9IwA=
github.com/mitchellh/pointerstructure v0.0.0-20190430161007-f252a8fd71c8/go.mod h1:k4XwG94++jLVsSiTxo7qdIfXA9pj9EAeo0QsNNJOLZ8=
github.com/mitchellh/pointerstructure v1.0.0 h1:ATSdz4NWrmWPOF1CeCBU4sMCno2hgqdbSrRPFWQSVZI=
github.com/mitchellh/pointerstructure v1.0.0/go.mod h1:k4XwG94++jLVsSiTxo7qdIfXA9pj9EAeo0QsNNJOLZ8=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
@ -140,8 +142,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@ -81,6 +81,18 @@ func pathConfig(b *jwtAuthBackend) *framework.Path {
Type: framework.TypeString,
Description: "The value against which to match the 'iss' claim in a JWT. Optional.",
},
"provider_config": {
Type: framework.TypeMap,
Description: "Provider-specific configuration. Optional.",
DisplayAttrs: &framework.DisplayAttributes{
Name: "Provider Config",
Value: map[string]interface{}{
"provider": "gsuite",
"fetch_groups": true,
"gsuite_service_account": "ey4921...",
},
},
},
},
Operations: map[logical.Operation]framework.OperationHandler{
@ -157,6 +169,7 @@ func (b *jwtAuthBackend) pathConfigRead(ctx context.Context, req *logical.Reques
"jwks_url": config.JWKSURL,
"jwks_ca_pem": config.JWKSCAPEM,
"bound_issuer": config.BoundIssuer,
"provider_config": config.ProviderConfig,
},
}
@ -177,6 +190,7 @@ func (b *jwtAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque
JWTValidationPubKeys: d.Get("jwt_validation_pubkeys").([]string),
JWTSupportedAlgs: d.Get("jwt_supported_algs").([]string),
BoundIssuer: d.Get("bound_issuer").(string),
ProviderConfig: d.Get("provider_config").(map[string]interface{}),
}
// Run checks on values
@ -239,6 +253,9 @@ func (b *jwtAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque
return nil, errors.New("unknown condition")
}
// NOTE: the OIDC lib states that if nothing is passed into its config, it
// defaults to "RS256". So in the case of a zero value here it won't
// default to e.g. "none".
for _, a := range config.JWTSupportedAlgs {
switch a {
case oidc.RS256, oidc.RS384, oidc.RS512, oidc.ES256, oidc.ES384, oidc.ES512, oidc.PS256, oidc.PS384, oidc.PS512:
@ -263,6 +280,11 @@ func (b *jwtAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque
return logical.ErrorResponse("invalid response_mode: %q", config.OIDCResponseMode), nil
}
// Validate provider_config
if _, err := NewProviderConfig(config, ProviderMap()); err != nil {
return logical.ErrorResponse("invalid provider_config: %s", err), nil
}
entry, err := logical.StorageEntryJSON(configPath, config)
if err != nil {
return nil, err
@ -330,6 +352,7 @@ type jwtConfig struct {
JWTSupportedAlgs []string `json:"jwt_supported_algs"`
BoundIssuer string `json:"bound_issuer"`
DefaultRole string `json:"default_role"`
ProviderConfig map[string]interface{} `json:"provider_config"`
ParsedJWTPubKeys []interface{} `json:"-"`
}

View File

@ -334,10 +334,9 @@ func (b *jwtAuthBackend) createIdentity(allClaims map[string]interface{}, role *
return alias, groupAliases, nil
}
groupsClaimRaw := getClaim(b.Logger(), allClaims, role.GroupsClaim)
if groupsClaimRaw == nil {
return nil, nil, fmt.Errorf("%q claim not found in token", role.GroupsClaim)
groupsClaimRaw, err := b.fetchGroups(allClaims, role)
if err != nil {
return nil, nil, fmt.Errorf("failed to fetch groups: %s", err)
}
groups, ok := normalizeList(groupsClaimRaw)
@ -361,6 +360,28 @@ func (b *jwtAuthBackend) createIdentity(allClaims map[string]interface{}, role *
return alias, groupAliases, nil
}
// Checks if there's a custom provider_config and calls FetchGroups() if implemented
func (b *jwtAuthBackend) fetchGroups(allClaims map[string]interface{}, role *jwtRole) (interface{}, error) {
pConfig, err := NewProviderConfig(b.cachedConfig, ProviderMap())
if err != nil {
return nil, fmt.Errorf("failed to load custom provider config: %s", err)
}
// If the custom provider implements interface GroupsFetcher, call it,
// otherwise fall through to the default method
if pConfig != nil {
if gf, ok := pConfig.(GroupsFetcher); ok {
return gf.FetchGroups(b, allClaims, role)
}
}
groupsClaimRaw := getClaim(b.Logger(), allClaims, role.GroupsClaim)
if groupsClaimRaw == nil {
return nil, fmt.Errorf("%q claim not found in token", role.GroupsClaim)
}
return groupsClaimRaw, nil
}
const (
pathLoginHelpSyn = `
Authenticates to Vault using a JWT (or OIDC) token.

View File

@ -28,6 +28,7 @@ const (
errLoginFailed = "Vault login failed."
errNoResponse = "No response from provider."
errTokenVerification = "Token verification failed."
errNotOIDCFlow = "OIDC login is not configured for this mount"
noCode = "no_code"
)
@ -344,7 +345,7 @@ func (b *jwtAuthBackend) authURL(ctx context.Context, req *logical.Request, d *f
}
if config.authType() != OIDCFlow {
return logical.ErrorResponse("OIDC login is not configured for this mount"), nil
return logical.ErrorResponse(errNotOIDCFlow), nil
}
roleName := d.Get("role").(string)

View File

@ -0,0 +1,188 @@
package jwtauth
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/coreos/go-oidc"
log "github.com/hashicorp/go-hclog"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
)
const (
// The old MS graph API requires setting an api-version query parameter
windowsGraphHost = "graph.windows.net"
windowsAPIVersion = "1.6"
// Distributed claim fields
claimNamesField = "_claim_names"
claimSourcesField = "_claim_sources"
)
// AzureProvider is used for Azure-specific configuration
type AzureProvider struct {
// Context for azure calls
ctx context.Context
// OIDC provider
provider *oidc.Provider
}
// Initialize anything in the AzureProvider struct - satisfying the CustomProvider interface
func (a *AzureProvider) Initialize(jc *jwtConfig) error {
return nil
}
// SensitiveKeys - satisfying the CustomProvider interface
func (a *AzureProvider) SensitiveKeys() []string {
return []string{}
}
// FetchGroups - custom groups fetching for azure - satisfying GroupsFetcher interface
func (a *AzureProvider) FetchGroups(b *jwtAuthBackend, allClaims map[string]interface{}, role *jwtRole) (interface{}, error) {
groupsClaimRaw := getClaim(b.Logger(), allClaims, role.GroupsClaim)
if groupsClaimRaw == nil {
// If the "groups" claim is missing, it might be because the user is a
// member of more than 200 groups, which means the token contains
// distributed claim information. Attempt to look that up here.
azureClaimSourcesURL, err := a.getClaimSource(b.Logger(), allClaims, role)
if err != nil {
return nil, fmt.Errorf("unable to get claim sources: %s", err)
}
// Get provider because we'll need to get a new token for microsoft's
// graph API, specifically the old graph API
provider, err := b.getProvider(b.cachedConfig)
if err != nil {
return nil, fmt.Errorf("unable to get provider: %s", err)
}
a.provider = provider
a.ctx, err = b.createCAContext(b.providerCtx, b.cachedConfig.OIDCDiscoveryCAPEM)
if err != nil {
return nil, fmt.Errorf("unable to create CA Context: %s", err)
}
azureGroups, err := a.getAzureGroups(azureClaimSourcesURL, b.cachedConfig)
if err != nil {
return nil, fmt.Errorf("%q claim not found in token: %v", role.GroupsClaim, err)
}
groupsClaimRaw = azureGroups
}
b.Logger().Debug(fmt.Sprintf("groups claim raw is %v", groupsClaimRaw))
return groupsClaimRaw, nil
}
// In Azure, if you are indirectly member of more than 200 groups, they will
// send _claim_names and _claim_sources instead of the groups, per OIDC Core
// 1.0, section 5.6.2:
// https://openid.net/specs/openid-connect-core-1_0.html#AggregatedDistributedClaims
// In the future this could be used with other providers as well. Example:
//
// {
// "_claim_names": {
// "groups": "src1"
// },
// "_claim_sources": {
// "src1": {
// "endpoint": "https://graph.windows.net...."
// }
// }
// }
//
// For this to work, "profile" should be set in "oidc_scopes" in the vault oidc role.
//
func (a *AzureProvider) getClaimSource(logger log.Logger, allClaims map[string]interface{}, role *jwtRole) (string, error) {
// Get the source key for the groups claim
name := fmt.Sprintf("/%s/%s", claimNamesField, role.GroupsClaim)
groupsClaimSource := getClaim(logger, allClaims, name)
if groupsClaimSource == nil {
return "", fmt.Errorf("unable to locate groups claim %q in %s", role.GroupsClaim, claimNamesField)
}
// Get the endpoint source for the groups claim
endpoint := fmt.Sprintf("/%s/%s/endpoint", claimSourcesField, groupsClaimSource.(string))
val := getClaim(logger, allClaims, endpoint)
if val == nil {
return "", fmt.Errorf("unable to locate %s in claims", endpoint)
}
logger.Debug(fmt.Sprintf("found Azure Graph API endpoint for group membership: %v", val))
return fmt.Sprintf("%v", val), nil
}
// Fetch user groups from the Azure AD Graph API
func (a *AzureProvider) getAzureGroups(groupsURL string, c *jwtConfig) (interface{}, error) {
urlParsed, err := url.Parse(groupsURL)
if err != nil {
return nil, fmt.Errorf("failed to parse distributed groups source url %s: %s", groupsURL, err)
}
token, err := a.getAzureToken(c, urlParsed.Host)
if err != nil {
return nil, fmt.Errorf("unable to get token: %s", err)
}
payload := strings.NewReader("{\"securityEnabledOnly\": false}")
req, err := http.NewRequest("POST", groupsURL, payload)
if err != nil {
return nil, fmt.Errorf("error constructing groups endpoint request: %s", err)
}
req.Header.Add("content-type", "application/json")
req.Header.Add("authorization", fmt.Sprintf("Bearer %s", token))
// If endpoint is the old windows graph api, add api-version
if urlParsed.Host == windowsGraphHost {
query := req.URL.Query()
query.Add("api-version", windowsAPIVersion)
req.URL.RawQuery = query.Encode()
}
client := http.DefaultClient
if c, ok := a.ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
client = c
}
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("unable to call Azure AD Graph API: %s", err)
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("failed to read Azure AD Graph API response: %s", err)
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get groups: %s", string(body))
}
var target azureGroups
if err := json.Unmarshal(body, &target); err != nil {
return nil, fmt.Errorf("unabled to decode response: %s", err)
}
return target.Value, nil
}
// Login to Azure, using client id and secret.
func (a *AzureProvider) getAzureToken(c *jwtConfig, host string) (string, error) {
config := &clientcredentials.Config{
ClientID: c.OIDCClientID,
ClientSecret: c.OIDCClientSecret,
TokenURL: a.provider.Endpoint().TokenURL,
Scopes: []string{
"openid",
"profile",
"https://" + host + "/.default",
},
}
token, err := config.Token(a.ctx)
if err != nil {
return "", fmt.Errorf("failed to fetch Azure token: %s", err)
}
return token.AccessToken, nil
}
type azureGroups struct {
Value []interface{} `json:"value"`
}

View File

@ -0,0 +1,55 @@
package jwtauth
import (
"fmt"
)
// Provider-specific configuration interfaces
// All providers must implement the CustomProvider interface, and may implement
// others as needed.
// ProviderMap returns a map of provider names to custom types
func ProviderMap() map[string]CustomProvider {
return map[string]CustomProvider{
"azure": &AzureProvider{},
}
}
// CustomProvider - Any custom provider must implement this interface
type CustomProvider interface {
// Initialize should validate jwtConfig.ProviderConfig, set internal values
// and run any initialization necessary for subsequent calls to interface
// functions the provider implements
Initialize(*jwtConfig) error
// SensitiveKeys returns any fields in a provider's jwtConfig.ProviderConfig
// that should be masked or omitted when output
SensitiveKeys() []string
}
// NewProviderConfig - returns appropriate provider struct if provider_config is
// specified in jwtConfig. The provider map is provider name -to- instance of a
// CustomProvider.
func NewProviderConfig(jc *jwtConfig, providerMap map[string]CustomProvider) (CustomProvider, error) {
if len(jc.ProviderConfig) == 0 {
return nil, nil
}
provider, ok := jc.ProviderConfig["provider"].(string)
if !ok {
return nil, fmt.Errorf("'provider' field not found in provider_config")
}
newCustomProvider, ok := providerMap[provider]
if !ok {
return nil, fmt.Errorf("provider %q not found in custom providers", provider)
}
if err := newCustomProvider.Initialize(jc); err != nil {
return nil, fmt.Errorf("error initializing %q provider_config: %s", provider, err)
}
return newCustomProvider, nil
}
// GroupsFetcher - Optional support for custom groups handling
type GroupsFetcher interface {
// FetchGroups queries for groups claims during login
FetchGroups(*jwtAuthBackend, map[string]interface{}, *jwtRole) (interface{}, error)
}

4
vendor/modules.txt vendored
View File

@ -461,7 +461,7 @@ github.com/hashicorp/vault-plugin-auth-cf/util
# github.com/hashicorp/vault-plugin-auth-gcp v0.6.1
github.com/hashicorp/vault-plugin-auth-gcp/plugin
github.com/hashicorp/vault-plugin-auth-gcp/plugin/cache
# github.com/hashicorp/vault-plugin-auth-jwt v0.6.2
# github.com/hashicorp/vault-plugin-auth-jwt v0.7.0
github.com/hashicorp/vault-plugin-auth-jwt
# github.com/hashicorp/vault-plugin-auth-kerberos v0.1.6
github.com/hashicorp/vault-plugin-auth-kerberos
@ -678,7 +678,7 @@ github.com/mitchellh/hashstructure
github.com/mitchellh/iochan
# github.com/mitchellh/mapstructure v1.3.2
github.com/mitchellh/mapstructure
# github.com/mitchellh/pointerstructure v0.0.0-20190430161007-f252a8fd71c8
# github.com/mitchellh/pointerstructure v1.0.0
github.com/mitchellh/pointerstructure
# github.com/mitchellh/reflectwalk v1.0.1
github.com/mitchellh/reflectwalk