Skip to content

Commit

Permalink
Add mutator for modifying authenticationSession with external API (#240)
Browse files Browse the repository at this point in the history
  • Loading branch information
kubadz authored and aeneasr committed Aug 16, 2019
1 parent d21179d commit b38b0f4
Show file tree
Hide file tree
Showing 12 changed files with 576 additions and 5 deletions.
14 changes: 13 additions & 1 deletion .schemas/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -441,10 +441,22 @@
}
}
},
"hydrator": {
"type": "object",
"title": "Hydrator",
"description": "The [`hydrator` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#hydrator).",
"additionalProperties": false,
"properties": {
"enabled": {
"title": "Enabled",
"type": "boolean"
}
}
},
"id_token": {
"type": "object",
"title": "ID Token (JSON Web Token)",
"description": "The [`header` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#header).",
"description": "The [`id_token` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#id_token).",
"additionalProperties": false,
"properties": {
"enabled": {
Expand Down
5 changes: 5 additions & 0 deletions docs/.oathkeeper.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ mutators:
# Set enabled to true if the mutator should be enabled and false to disable the mutator. Defaults to false.
enabled: true

# Configures the hydrator mutator
hydrator:
# Set enabled to true if the mutator should be enabled and false to disable the mutator. Defaults to false.
enabled: true

# Configures the id_token mutator
id_token:
# Set enabled to true if the mutator should be enabled and false to disable the mutator. Defaults to false.
Expand Down
2 changes: 2 additions & 0 deletions driver/configuration/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ type ProviderMutators interface {
MutatorIDTokenTTL() time.Duration

MutatorNoopIsEnabled() bool

MutatorHydratorIsEnabled() bool
}

func MustValidate(l logrus.FieldLogger, p Provider) {
Expand Down
1 change: 1 addition & 0 deletions driver/configuration/provider_viper.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func BindEnvs() {
ViperKeyMutatorCookieIsEnabled,
ViperKeyMutatorHeaderIsEnabled,
ViperKeyMutatorNoopIsEnabled,
ViperKeyMutatorHydratorIsEnabled,
ViperKeyMutatorIDTokenIsEnabled,
ViperKeyMutatorIDTokenIssuerURL,
ViperKeyMutatorIDTokenJWKSURL,
Expand Down
6 changes: 6 additions & 0 deletions driver/configuration/provider_viper_mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const (

ViperKeyMutatorNoopIsEnabled = "mutators.noop.enabled"

ViperKeyMutatorHydratorIsEnabled = "mutators.hydrator.enabled"

ViperKeyMutatorIDTokenIsEnabled = "mutators.id_token.enabled"
ViperKeyMutatorIDTokenIssuerURL = "mutators.id_token.issuer_url"
ViperKeyMutatorIDTokenJWKSURL = "mutators.id_token.jwks_url"
Expand Down Expand Up @@ -51,3 +53,7 @@ func (v *ViperProvider) MutatorIDTokenTTL() time.Duration {
func (v *ViperProvider) MutatorNoopIsEnabled() bool {
return viperx.GetBool(v.l, ViperKeyMutatorNoopIsEnabled, false)
}

func (v *ViperProvider) MutatorHydratorIsEnabled() bool {
return viperx.GetBool(v.l, ViperKeyMutatorHydratorIsEnabled, false)
}
4 changes: 4 additions & 0 deletions driver/configuration/provider_viper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ func TestViperProvider(t *testing.T) {
assert.True(t, p.MutatorHeaderIsEnabled())
})

t.Run("mutator=hydrator", func(t *testing.T) {
assert.True(t, p.MutatorHydratorIsEnabled())
})

t.Run("mutator=id_token", func(t *testing.T) {
assert.True(t, p.MutatorIDTokenIsEnabled())
assert.EqualValues(t, urlx.ParseOrPanic("https://my-oathkeeper/"), p.MutatorIDTokenIssuerURL())
Expand Down
1 change: 1 addition & 0 deletions driver/registry_memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ func (r *RegistryMemory) prepareMutators() {
mutate.NewMutatorHeader(r.c),
mutate.NewMutatorIDToken(r.c, r),
mutate.NewMutatorNoop(r.c),
mutate.NewMutatorHydrator(r.c),
}

r.mutators = map[string]mutate.Mutator{}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf
github.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7
github.com/bxcodec/faker v2.0.1+incompatible
github.com/cenkalti/backoff v2.1.1+incompatible
github.com/codegangsta/negroni v1.0.0 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/fsnotify/fsnotify v1.4.7
Expand Down
6 changes: 3 additions & 3 deletions pipeline/authn/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ type Authenticator interface {
}

type AuthenticationSession struct {
Subject string
Extra map[string]interface{}
Header http.Header
Subject string `json:"subject"`
Extra map[string]interface{} `json:"extra"`
Header http.Header `json:"header"`
}

func (a *AuthenticationSession) SetHeader(key, val string) {
Expand Down
3 changes: 2 additions & 1 deletion pipeline/authn/authenticator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"fmt"
"testing"

"github.com/ory/oathkeeper/pipeline/authn"
"github.com/stretchr/testify/assert"

"github.com/ory/oathkeeper/pipeline/authn"
)

const (
Expand Down
174 changes: 174 additions & 0 deletions pipeline/mutate/mutator_hydrator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* Copyright © 2017-2018 Aeneas Rekkas <[email protected]>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author Aeneas Rekkas <[email protected]>
* @copyright 2017-2018 Aeneas Rekkas <[email protected]>
* @license Apache-2.0
*/

package mutate

import (
"bytes"
"encoding/json"
"net/http"
"net/url"
"time"

"github.com/ory/oathkeeper/pipeline/authn"

"github.com/cenkalti/backoff"

"github.com/ory/x/httpx"

"github.com/pkg/errors"

"github.com/ory/oathkeeper/driver/configuration"
"github.com/ory/oathkeeper/pipeline"
)

const (
ErrMalformedResponseFromUpstreamAPI = "The call to an external API returned an invalid JSON object"
ErrMissingAPIURL = "Missing URL in mutator configuration"
ErrInvalidAPIURL = "Invalid URL in mutator configuration"
ErrNon200ResponseFromAPI = "The call to an external API returned a non-200 HTTP response"
ErrInvalidCredentials = "Invalid credentials were provided in mutator configuration"
ErrNoCredentialsProvided = "No credentials were provided in mutator configuration"
defaultNumberOfRetries = 3
defaultDelayInMilliseconds = 100
contentTypeHeaderKey = "Content-Type"
contentTypeJSONHeaderValue = "application/json"
)

type MutatorHydrator struct {
c configuration.Provider
client *http.Client
}

type BasicAuth struct {
Username string `json:"username"`
Password string `json:"password"`
}

type Auth struct {
Basic BasicAuth `json:"basic"`
}

type RetryConfig struct {
NumberOfRetries int `json:"number"`
DelayInMilliseconds int `json:"delayInMilliseconds"`
}

type externalAPIConfig struct {
Url string `json:"url"`
Auth *Auth `json:"auth,omitempty"`
Retry *RetryConfig `json:"retry,omitempty"`
}

type MutatorHydratorConfig struct {
Api externalAPIConfig `json:"api"`
}

func NewMutatorHydrator(c configuration.Provider) *MutatorHydrator {
return &MutatorHydrator{c: c, client: httpx.NewResilientClientLatencyToleranceSmall(nil)}
}

func (a *MutatorHydrator) GetID() string {
return "hydrator"
}

func (a *MutatorHydrator) Mutate(r *http.Request, session *authn.AuthenticationSession, config json.RawMessage, _ pipeline.Rule) error {
if len(config) == 0 {
config = []byte("{}")
}
var cfg MutatorHydratorConfig
d := json.NewDecoder(bytes.NewBuffer(config))
d.DisallowUnknownFields()
if err := d.Decode(&cfg); err != nil {
return errors.WithStack(err)
}

var b bytes.Buffer
err := json.NewEncoder(&b).Encode(session)
if err != nil {
return errors.WithStack(err)
}

if cfg.Api.Url == "" {
return errors.New(ErrMissingAPIURL)
} else if _, err := url.ParseRequestURI(cfg.Api.Url); err != nil {
return errors.New(ErrInvalidAPIURL)
}
req, err := http.NewRequest("POST", cfg.Api.Url, &b)
if err != nil {
return errors.WithStack(err)
}
for key, values := range r.Header {
for _, value := range values {
req.Header.Add(key, value)
}
}
if cfg.Api.Auth != nil {
credentials := cfg.Api.Auth.Basic
req.SetBasicAuth(credentials.Username, credentials.Password)
}
req.Header.Set(contentTypeHeaderKey, contentTypeJSONHeaderValue)

retryConfig := RetryConfig{defaultNumberOfRetries, defaultDelayInMilliseconds}
if cfg.Api.Retry != nil {
retryConfig = *cfg.Api.Retry
}
var res *http.Response
err = backoff.Retry(func() error {
res, err = a.client.Do(req)
if err != nil {
return errors.WithStack(err)
}
switch res.StatusCode {
case http.StatusOK:
return nil
case http.StatusUnauthorized:
if cfg.Api.Auth != nil {
return errors.New(ErrInvalidCredentials)
} else {
return errors.New(ErrNoCredentialsProvided)
}
default:
return errors.New(ErrNon200ResponseFromAPI)
}
}, backoff.WithMaxRetries(backoff.NewConstantBackOff(time.Millisecond*time.Duration(retryConfig.DelayInMilliseconds)), uint64(retryConfig.NumberOfRetries)))
if err != nil {
return err
}

sessionFromUpstream := authn.AuthenticationSession{}
err = json.NewDecoder(res.Body).Decode(&sessionFromUpstream)
if err != nil {
return errors.WithStack(err)
}
if sessionFromUpstream.Subject != session.Subject {
return errors.New(ErrMalformedResponseFromUpstreamAPI)
}
*session = sessionFromUpstream

return nil
}

func (a *MutatorHydrator) Validate() error {
if !a.c.MutatorHydratorIsEnabled() {
return errors.WithStack(ErrMutatorNotEnabled.WithReasonf(`Mutator "%s" is disabled per configuration.`, a.GetID()))
}
return nil
}
Loading

0 comments on commit b38b0f4

Please sign in to comment.