Skip to content

Commit

Permalink
Support provider specific handling (#118)
Browse files Browse the repository at this point in the history
Adds a provider_config field and initial interface definitions to allow for
custom provider config handling.

Co-authored-by: Jim Kalafut <jkalafut@hashicorp.com>
Co-authored-by: Clint <catsby@users.noreply.github.com>
  • Loading branch information
3 people authored Jun 17, 2020
1 parent 175b36b commit 71af593
Show file tree
Hide file tree
Showing 33 changed files with 6,411 additions and 21 deletions.
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ jobs:
name: "Update Go"
command: |
sudo rm -rf /usr/local/go
wget https://dl.google.com/go/go1.12.7.linux-amd64.tar.gz
sudo tar -xvf go1.12.7.linux-amd64.tar.gz
wget https://dl.google.com/go/go1.14.4.linux-amd64.tar.gz
sudo tar -xvf go1.14.4.linux-amd64.tar.gz
sudo mv go /usr/local
- run:
name: "Set Env Vars"
Expand Down
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ 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/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 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
github.com/stretchr/testify v1.3.0
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/text v0.3.2 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ 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=
Expand Down Expand Up @@ -95,8 +97,6 @@ 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=
Expand Down
44 changes: 32 additions & 12 deletions path_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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,
},
}

Expand All @@ -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
Expand Down Expand Up @@ -266,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
Expand Down Expand Up @@ -321,18 +340,19 @@ func (b *jwtAuthBackend) createCAContext(ctx context.Context, caPEM string) (con
}

type jwtConfig struct {
OIDCDiscoveryURL string `json:"oidc_discovery_url"`
OIDCDiscoveryCAPEM string `json:"oidc_discovery_ca_pem"`
OIDCClientID string `json:"oidc_client_id"`
OIDCClientSecret string `json:"oidc_client_secret"`
OIDCResponseMode string `json:"oidc_response_mode"`
OIDCResponseTypes []string `json:"oidc_response_types"`
JWKSURL string `json:"jwks_url"`
JWKSCAPEM string `json:"jwks_ca_pem"`
JWTValidationPubKeys []string `json:"jwt_validation_pubkeys"`
JWTSupportedAlgs []string `json:"jwt_supported_algs"`
BoundIssuer string `json:"bound_issuer"`
DefaultRole string `json:"default_role"`
OIDCDiscoveryURL string `json:"oidc_discovery_url"`
OIDCDiscoveryCAPEM string `json:"oidc_discovery_ca_pem"`
OIDCClientID string `json:"oidc_client_id"`
OIDCClientSecret string `json:"oidc_client_secret"`
OIDCResponseMode string `json:"oidc_response_mode"`
OIDCResponseTypes []string `json:"oidc_response_types"`
JWKSURL string `json:"jwks_url"`
JWKSCAPEM string `json:"jwks_ca_pem"`
JWTValidationPubKeys []string `json:"jwt_validation_pubkeys"`
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:"-"`
}
Expand Down
106 changes: 106 additions & 0 deletions path_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/go-test/deep"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/stretchr/testify/assert"
)

func TestConfig_JWT_Read(t *testing.T) {
Expand All @@ -26,6 +27,7 @@ func TestConfig_JWT_Read(t *testing.T) {
"jwks_url": "",
"jwks_ca_pem": "",
"bound_issuer": "http://vault.example.com/",
"provider_config": map[string]interface{}{},
}

req := &logical.Request{
Expand Down Expand Up @@ -133,6 +135,7 @@ func TestConfig_JWT_Write(t *testing.T) {
JWTSupportedAlgs: []string{},
OIDCResponseTypes: []string{},
BoundIssuer: "http://vault.example.com/",
ProviderConfig: map[string]interface{}{},
}

conf, err := b.(*jwtAuthBackend).config(context.Background(), storage)
Expand Down Expand Up @@ -168,6 +171,7 @@ func TestConfig_JWKS_Update(t *testing.T) {
"jwt_validation_pubkeys": []string{},
"jwt_supported_algs": []string{},
"bound_issuer": "",
"provider_config": map[string]interface{}{},
}

req := &logical.Request{
Expand Down Expand Up @@ -337,6 +341,7 @@ func TestConfig_OIDC_Write(t *testing.T) {
JWTSupportedAlgs: []string{},
OIDCResponseTypes: []string{},
OIDCDiscoveryURL: "https://team-vault.auth0.com/",
ProviderConfig: map[string]interface{}{},
}

conf, err := b.(*jwtAuthBackend).config(context.Background(), storage)
Expand Down Expand Up @@ -396,6 +401,107 @@ func TestConfig_OIDC_Write(t *testing.T) {
}
}

func TestConfig_OIDC_Write_ProviderConfig(t *testing.T) {
b, storage := getBackend(t)
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: configPath,
Storage: storage,
Data: nil,
}

t.Run("valid provider_config", func(t *testing.T) {
req.Data = map[string]interface{}{
"oidc_discovery_url": "https://team-vault.auth0.com/",
"provider_config": map[string]interface{}{
"provider": "empty",
"extraOptions": "abound",
},
}

resp, err := b.HandleRequest(context.Background(), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}

expected := &jwtConfig{
JWTValidationPubKeys: []string{},
JWTSupportedAlgs: []string{},
OIDCResponseTypes: []string{},
OIDCDiscoveryURL: "https://team-vault.auth0.com/",
ProviderConfig: map[string]interface{}{
"provider": "empty",
"extraOptions": "abound",
},
}

conf, err := b.(*jwtAuthBackend).config(context.Background(), storage)
if err != nil {
t.Fatal(err)
}

if diff := deep.Equal(expected, conf); diff != nil {
t.Fatal(diff)
}
})

t.Run("unknown provider in provider_config", func(t *testing.T) {
req.Data = map[string]interface{}{
"oidc_discovery_url": "https://team-vault.auth0.com/",
"provider_config": map[string]interface{}{
"provider": "unknown",
},
}

resp, err := b.HandleRequest(context.Background(), req)
assert.NoError(t, err)
assert.True(t, resp.IsError())
assert.EqualError(t, resp.Error(), "invalid provider_config: provider \"unknown\" not found in custom providers")
})

t.Run("provider_config missing provider", func(t *testing.T) {
req.Data = map[string]interface{}{
"oidc_discovery_url": "https://team-vault.auth0.com/",
"provider_config": map[string]interface{}{
"not-provider": "oops",
},
}

resp, err := b.HandleRequest(context.Background(), req)
assert.NoError(t, err)
assert.True(t, resp.IsError())
assert.EqualError(t, resp.Error(), "invalid provider_config: 'provider' field not found in provider_config")
})

t.Run("provider_config not set", func(t *testing.T) {
req.Data = map[string]interface{}{
"oidc_discovery_url": "https://team-vault.auth0.com/",
}

resp, err := b.HandleRequest(context.Background(), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}

expected := &jwtConfig{
JWTValidationPubKeys: []string{},
JWTSupportedAlgs: []string{},
OIDCResponseTypes: []string{},
OIDCDiscoveryURL: "https://team-vault.auth0.com/",
ProviderConfig: map[string]interface{}{},
}

conf, err := b.(*jwtAuthBackend).config(context.Background(), storage)
if err != nil {
t.Fatal(err)
}

if diff := deep.Equal(expected, conf); diff != nil {
t.Fatal(diff)
}
})
}

const (
testJWTPubKey = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9
Expand Down
61 changes: 61 additions & 0 deletions provider_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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{
// TODO: remove "empty" provider when actual providers are added
"empty": &EmptyProvider{},
}
}

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

// Example interfaces that are implemented by one or more provider types
// // UserInfoFetcher - Optional support for custom UserInfo handling
// type UserInfoFetcher interface {
// FetchUserInfo(context.Context, *oidc.Provider, *oauth2.Token, claims) error
// }

// // GroupsFetcher - Optional support for custom groups handling
// type GroupsFetcher interface {
// FetchGroups(context.Context, *oauth2.Token, claims) error
// }
Loading

0 comments on commit 71af593

Please sign in to comment.