Skip to content

Commit

Permalink
proxy: add cookies credentials issuer
Browse files Browse the repository at this point in the history
Signed-off-by: Jason Hutchinson <jhutchinson@wehco.com>
  • Loading branch information
Jason Hutchinson authored and arekkas committed Aug 16, 2018
1 parent 585672e commit 032d88e
Show file tree
Hide file tree
Showing 2 changed files with 285 additions and 0 deletions.
99 changes: 99 additions & 0 deletions proxy/credentials_issuer_cookies.go
@@ -0,0 +1,99 @@
package proxy

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"text/template"

"github.com/ory/oathkeeper/rule"
"github.com/pkg/errors"
)

type CredentialsCookiesConfig struct {
Cookies map[string]string `json:"cookies"`
}

type CredentialsCookies struct {
RulesCache *template.Template
}

func NewCredentialsIssuerCookies() *CredentialsCookies {
return &CredentialsCookies{
RulesCache: template.New("rules").
Option("missingkey=zero").
Funcs(template.FuncMap{
"print": func(i interface{}) string {
if i == nil {
return ""
}
return fmt.Sprintf("%v", i)
},
}),
}
}

func (a *CredentialsCookies) GetID() string {
return "cookies"
}

func (a *CredentialsCookies) Issue(r *http.Request, session *AuthenticationSession, config json.RawMessage, rl *rule.Rule) error {
if len(config) == 0 {
config = []byte("{}")
}

// Cache request cookies
requestCookies := r.Cookies()

// Remove existing cookies
r.Header.Del("Cookie")

// Keep track of rule cookies in a map
cookies := map[string]bool{}

var cfg CredentialsCookiesConfig
d := json.NewDecoder(bytes.NewBuffer(config))
d.DisallowUnknownFields()
if err := d.Decode(&cfg); err != nil {
return errors.WithStack(err)
}

for cookie, templateString := range cfg.Cookies {
var tmpl *template.Template
var err error

templateId := fmt.Sprintf("%s:%s", rl.ID, cookie)
tmpl = a.RulesCache.Lookup(templateId)
if tmpl == nil {
tmpl, err = a.RulesCache.New(templateId).Parse(templateString)
if err != nil {
return errors.Wrapf(err, `error parsing cookie template "%s" in rule "%s"`, templateString, rl.ID)
}
}

cookieValue := bytes.Buffer{}
err = tmpl.Execute(&cookieValue, session)
if err != nil {
return errors.Wrapf(err, `error executing cookie template "%s" in rule "%s"`, templateString, rl.ID)
}

r.AddCookie(&http.Cookie{
Name: cookie,
Value: cookieValue.String(),
})

cookies[cookie] = true
}

// Re-add previously set cookies that do not coincide with rule cookies
for _, cookie := range requestCookies {
// Test if cookie is handled by rule
if _, ok := cookies[cookie.Name]; !ok {
// Re-add cookie if not handled by rule
r.AddCookie(cookie)
}
}

return nil
}
186 changes: 186 additions & 0 deletions proxy/credentials_issuer_cookies_test.go
@@ -0,0 +1,186 @@
package proxy

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"testing"
"text/template"

"github.com/ory/oathkeeper/rule"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCredentialsIssuerCookies(t *testing.T) {
var testMap = map[string]struct {
Session *AuthenticationSession
Rule *rule.Rule
Config json.RawMessage
Request *http.Request
Match []*http.Cookie
Err error
}{
"Simple Subject": {
Session: &AuthenticationSession{Subject: "foo"},
Rule: &rule.Rule{ID: "test-rule"},
Config: json.RawMessage([]byte(`{"cookies": {"user": "{{ print .Subject }}"}}`)),
Request: &http.Request{Header: http.Header{}},
Match: []*http.Cookie{&http.Cookie{Name: "user", Value: "foo"}},
Err: nil,
},
"Unknown Config Field": {
Session: &AuthenticationSession{},
Rule: &rule.Rule{ID: "test-rule2"},
Config: json.RawMessage([]byte(`{"bar": "baz"}`)),
Request: &http.Request{Header: http.Header{}},
Match: []*http.Cookie{},
Err: errors.New(`json: unknown field "bar"`),
},
"Complex Subject": {
Session: &AuthenticationSession{Subject: "foo"},
Rule: &rule.Rule{ID: "test-rule3"},
Config: json.RawMessage([]byte(`{"cookies": {"user": "realm:resources:users:{{ print .Subject }}"}}`)),
Request: &http.Request{Header: http.Header{}},
Match: []*http.Cookie{&http.Cookie{Name: "user", Value: "realm:resources:users:foo"}},
Err: nil,
},
"Subject & Extras": {
Session: &AuthenticationSession{Subject: "foo", Extra: map[string]interface{}{"iss": "issuer", "aud": "audience"}},
Rule: &rule.Rule{ID: "test-rule4"},
Config: json.RawMessage([]byte(`{"cookies":{"user": "{{ print .Subject }}", "issuer": "{{ print .Extra.iss }}", "audience": "{{ print .Extra.aud }}"}}`)),
Request: &http.Request{Header: http.Header{}},
Match: []*http.Cookie{
&http.Cookie{Name: "user", Value: "foo"},
&http.Cookie{Name: "issuer", Value: "issuer"},
&http.Cookie{Name: "audience", Value: "audience"},
},
Err: nil,
},
"All In One Cookie": {
Session: &AuthenticationSession{Subject: "foo", Extra: map[string]interface{}{"iss": "issuer", "aud": "audience"}},
Rule: &rule.Rule{ID: "test-rule5"},
Config: json.RawMessage([]byte(`{"cookies":{"kitchensink": "{{ print .Subject }} {{ print .Extra.iss }} {{ print .Extra.aud }}"}}`)),
Request: &http.Request{Header: http.Header{}},
Match: []*http.Cookie{
&http.Cookie{Name: "kitchensink", Value: "foo issuer audience"},
},
Err: nil,
},
"Scrub Incoming Cookies": {
Session: &AuthenticationSession{Subject: "anonymous"},
Rule: &rule.Rule{ID: "test-rule6"},
Config: json.RawMessage([]byte(`{"cookies":{"user": "{{ print .Subject }}", "issuer": "{{ print .Extra.iss }}", "audience": "{{ print .Extra.aud }}"}}`)),
Request: &http.Request{
Header: http.Header{"Cookie": []string{"user=admin;issuer=issuer;audience=audience"}},
},
Match: []*http.Cookie{
&http.Cookie{Name: "user", Value: "anonymous"},
&http.Cookie{Name: "issuer", Value: ""},
&http.Cookie{Name: "audience", Value: ""},
},
Err: nil,
},
"Missing Extras": {
Session: &AuthenticationSession{Subject: "foo", Extra: map[string]interface{}{}},
Rule: &rule.Rule{ID: "test-rule7"},
Config: json.RawMessage([]byte(`{"cookies":{"issuer": "{{ print .Extra.iss }}"}}`)),
Request: &http.Request{Header: http.Header{}},
Match: []*http.Cookie{&http.Cookie{Name: "issuer", Value: ""}},
Err: nil,
},
"Nested Extras": {
Session: &AuthenticationSession{
Subject: "foo",
Extra: map[string]interface{}{
"nested": map[string]interface{}{
"int": int(10),
"float64": float64(3.14159),
"bool": true,
},
},
},
Rule: &rule.Rule{ID: "test-rule8"},
Config: json.RawMessage([]byte(`{"cookies":{
"nested-int": "{{ print .Extra.nested.int }}",
"nested-float64": "{{ print .Extra.nested.float64 }}",
"nested-bool": "{{ print .Extra.nested.bool}}",
"nested-nonexistent": "{{ print .Extra.nested.nil }}"
}}`)),
Request: &http.Request{Header: http.Header{}},
Match: []*http.Cookie{
&http.Cookie{Name: "nested-int", Value: "10"},
&http.Cookie{Name: "nested-float64", Value: "3.14159"},
&http.Cookie{Name: "nested-bool", Value: "true"},
&http.Cookie{Name: "nested-nonexistent", Value: ""},
},
Err: nil,
},
}

for testName, specs := range testMap {
t.Run(testName, func(t *testing.T) {
issuer := NewCredentialsIssuerCookies()

// Must return non-nil issuer
assert.NotNil(t, issuer)

// Issuer must return non-empty ID
assert.NotEmpty(t, issuer.GetID())

if specs.Err == nil {
require.NoError(t, issuer.Issue(specs.Request, specs.Session, specs.Config, specs.Rule))
} else {
err := issuer.Issue(specs.Request, specs.Session, specs.Config, specs.Rule)
assert.Equal(t, specs.Err.Error(), err.Error())
}

assert.Equal(t, serializeCookies(specs.Match), serializeCookies(specs.Request.Cookies()))
})
}

t.Run("Caching", func(t *testing.T) {
for _, specs := range testMap {
issuer := NewCredentialsIssuerCookies()

overrideCookies := []*http.Cookie{}

cache := template.New("rules")

var cfg CredentialsCookiesConfig
d := json.NewDecoder(bytes.NewBuffer(specs.Config))
d.Decode(&cfg)

for cookie, _ := range cfg.Cookies {
templateId := fmt.Sprintf("%s:%s", specs.Rule.ID, cookie)
cache.New(templateId).Parse("override")
overrideCookies = append(overrideCookies, &http.Cookie{Name: cookie, Value: "override"})
}

issuer.RulesCache = cache

if specs.Err == nil {
require.NoError(t, issuer.Issue(specs.Request, specs.Session, specs.Config, specs.Rule))
} else {
err := issuer.Issue(specs.Request, specs.Session, specs.Config, specs.Rule)
assert.Equal(t, specs.Err.Error(), err.Error())
}

assert.Equal(t, serializeCookies(overrideCookies), serializeCookies(specs.Request.Cookies()))
}
})
}

// assert.Equal doesn't handle []*http.Cookie comparisons very well, so
// converting them to a simple map[string]string makes testing easier
func serializeCookies(cookies []*http.Cookie) map[string]string {
out := map[string]string{}

for _, cookie := range cookies {
out[cookie.Name] = cookie.Value
}

return out
}

0 comments on commit 032d88e

Please sign in to comment.