Skip to content

Commit

Permalink
oauth2: Introduce audience capabilities
Browse files Browse the repository at this point in the history
This patch allows clients to whitelist audiences and request that audiences are set for oauth2 access and refresh tokens

Closes #326

Signed-off-by: arekkas <aeneas@ory.am>
  • Loading branch information
arekkas committed Oct 29, 2018
1 parent 799fc70 commit 891c4a8
Show file tree
Hide file tree
Showing 47 changed files with 485 additions and 133 deletions.
1 change: 1 addition & 0 deletions access_request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func (f *Fosite) NewAccessRequest(ctx context.Context, r *http.Request, session
}

accessRequest.SetRequestedScopes(removeEmpty(strings.Split(r.PostForm.Get("scope"), " ")))
accessRequest.SetRequestedAudience(removeEmpty(strings.Split(r.PostForm.Get("audience"), " ")))
accessRequest.GrantTypes = removeEmpty(strings.Split(r.PostForm.Get("grant_type"), " "))
if len(accessRequest.GrantTypes) < 1 {
return accessRequest, errors.WithStack(ErrInvalidRequest.WithHint(`Request parameter "grant_type"" is missing`))
Expand Down
2 changes: 1 addition & 1 deletion access_request_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestNewAccessRequest(t *testing.T) {
defer ctrl.Finish()

client := &DefaultClient{}
fosite := &Fosite{Store: store, Hasher: hasher}
fosite := &Fosite{Store: store, Hasher: hasher, AudienceMatchingStrategy:DefaultAudienceMatchingStrategy}
for k, c := range []struct {
header http.Header
form url.Values
Expand Down
4 changes: 4 additions & 0 deletions access_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ func TestAccessRequest(t *testing.T) {
ar.GrantTypes = Arguments{"foobar"}
ar.Client = &DefaultClient{}
ar.GrantScope("foo")
ar.SetRequestedAudience(Arguments{"foo", "foo", "bar"})
ar.SetRequestedScopes(Arguments{"foo", "foo", "bar"})
assert.True(t, ar.GetGrantedScopes().Has("foo"))
assert.NotNil(t, ar.GetRequestedAt())
assert.Equal(t, ar.GrantTypes, ar.GetGrantTypes())
assert.Equal(t, Arguments{"foo", "bar"}, ar.Audience)
assert.Equal(t, Arguments{"foo", "bar"}, ar.Scopes)
assert.Equal(t, ar.Client, ar.GetClient())
}
59 changes: 59 additions & 0 deletions audience_strategy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package fosite

import (
"github.com/ory/go-convenience/stringsx"
"github.com/pkg/errors"
"net/http"
"net/url"
"strings"
)

type AudienceMatchingStrategy func(haystack []string, needle []string) error

func DefaultAudienceMatchingStrategy(haystack []string, needle []string) error {
if len(needle) == 0 {
return nil
}

for _, n := range needle {
nu, err := url.Parse(n)
if err != nil {
return errors.WithStack(ErrInvalidRequest.WithHintf(`Unable to parse requested audience "%s".`, n).WithDebug(err.Error()))
}

var found bool
for _, h := range haystack {
hu, err := url.Parse(h)
if err != nil {
return errors.WithStack(ErrInvalidRequest.WithHintf(`Unable to parse whitelisted audience "%s".`, h).WithDebug(err.Error()))
}

allowedPath := strings.TrimRight(hu.Path, "/")
if nu.Scheme == hu.Scheme &&
nu.Host == hu.Host &&
(
nu.Path == hu.Path ||
nu.Path == allowedPath ||
len(nu.Path) > len(allowedPath) && strings.TrimRight(nu.Path[:len(allowedPath)+1], "/") + "/" == allowedPath+"/") {
found = true
}
}

if !found {
return errors.WithStack(ErrInvalidRequest.WithHintf(`Requested audience "%s" has not been whitelisted by the OAuth 2.0 Client.`, n))
}
}

return nil
}

func (f *Fosite) validateAuthorizeAudience(r *http.Request, request *AuthorizeRequest) (error) {
audience := stringsx.Splitx(request.Form.Get("audience"), " ")

if err := f.AudienceMatchingStrategy(request.Client.GetAudience(), audience); err != nil {
return err
}

request.SetRequestedAudience(Arguments(audience))
return nil
}
95 changes: 95 additions & 0 deletions audience_strategy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package fosite

import (
"fmt"
"github.com/stretchr/testify/require"
"testing"
)

func TestDefaultAudienceMatchingStrategy(t *testing.T) {
for k, tc := range []struct {
h []string
n []string
err bool
}{
{
h: []string{},
n: []string{},
err: false,
},
{
h: []string{"http://foo/bar"},
n: []string{},
err: false,
},
{
h: []string{},
n: []string{"http://foo/bar"},
err: true,
},
{
h: []string{"https://cloud.ory.sh/api/users"},
n: []string{"https://cloud.ory.sh/api/users"},
err: false,
},
{
h: []string{"https://cloud.ory.sh/api/users"},
n: []string{"https://cloud.ory.sh/api/users/"},
err: false,
},
{
h: []string{"https://cloud.ory.sh/api/users/"},
n: []string{"https://cloud.ory.sh/api/users/"},
err: false,
},
{
h: []string{"https://cloud.ory.sh/api/users/"},
n: []string{"https://cloud.ory.sh/api/users"},
err: false,
},
{
h: []string{"https://cloud.ory.sh/api/users"},
n: []string{"https://cloud.ory.sh/api/users/1234"},
err: false,
},
{
h: []string{"https://cloud.ory.sh/api/users"},
n: []string{"https://cloud.ory.sh/api/users", "https://cloud.ory.sh/api/users/", "https://cloud.ory.sh/api/users/1234"},
err: false,
},
{
h: []string{"https://cloud.ory.sh/api/users", "https://cloud.ory.sh/api/tenants"},
n: []string{"https://cloud.ory.sh/api/users", "https://cloud.ory.sh/api/users/", "https://cloud.ory.sh/api/users/1234", "https://cloud.ory.sh/api/tenants"},
err: false,
},
{
h: []string{"https://cloud.ory.sh/api/users"},
n: []string{"https://cloud.ory.sh/api/users1234"},
err: true,
},
{
h: []string{"https://cloud.ory.sh/api/users"},
n: []string{"http://cloud.ory.sh/api/users"},
err: true,
},
{
h: []string{"https://cloud.ory.sh/api/users"},
n: []string{"https://cloud.ory.sh:8000/api/users"},
err: true,
},
{
h: []string{"https://cloud.ory.sh/api/users"},
n: []string{"https://cloud.ory.xyz/api/users"},
err: true,
},
} {
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
err := DefaultAudienceMatchingStrategy(tc.h, tc.n)
if tc.err {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
6 changes: 5 additions & 1 deletion authorize_request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func (f *Fosite) validateAuthorizeRedirectURI(r *http.Request, request *Authoriz
}

func (f *Fosite) validateAuthorizeScope(r *http.Request, request *AuthorizeRequest) error {
scope := removeEmpty(strings.Split(request.Form.Get("scope"), " "))
scope := stringsx.Splitx(request.Form.Get("scope"), " ")
for _, permission := range scope {
if !f.ScopeStrategy(request.Client.GetScopes(), permission) {
return errors.WithStack(ErrInvalidScope.WithHintf(`The OAuth 2.0 Client is not allowed to request scope "%s".`, permission))
Expand Down Expand Up @@ -243,6 +243,10 @@ func (f *Fosite) NewAuthorizeRequest(ctx context.Context, r *http.Request) (Auth
return request, err
}

if err := f.validateAuthorizeAudience(r, request); err != nil {
return request, err
}

if len(request.Form.Get("registration")) > 0 {
return request, errors.WithStack(ErrRegistrationNotSupported)
}
Expand Down
59 changes: 45 additions & 14 deletions authorize_request_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func TestNewAuthorizeRequest(t *testing.T) {
/* empty request */
{
desc: "empty request fails",
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy},
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy},
r: &http.Request{},
expectedError: ErrInvalidClient,
mock: func() {
Expand All @@ -71,7 +71,7 @@ func TestNewAuthorizeRequest(t *testing.T) {
/* invalid redirect uri */
{
desc: "invalid redirect uri fails",
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy},
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy},
query: url.Values{"redirect_uri": []string{"invalid"}},
expectedError: ErrInvalidClient,
mock: func() {
Expand All @@ -81,7 +81,7 @@ func TestNewAuthorizeRequest(t *testing.T) {
/* invalid client */
{
desc: "invalid client fails",
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy},
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy},
query: url.Values{"redirect_uri": []string{"https://foo.bar/cb"}},
expectedError: ErrInvalidClient,
mock: func() {
Expand All @@ -91,7 +91,7 @@ func TestNewAuthorizeRequest(t *testing.T) {
/* redirect client mismatch */
{
desc: "client and request redirects mismatch",
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy},
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy},
query: url.Values{
"client_id": []string{"1234"},
},
Expand All @@ -103,7 +103,7 @@ func TestNewAuthorizeRequest(t *testing.T) {
/* redirect client mismatch */
{
desc: "client and request redirects mismatch",
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy},
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy},
query: url.Values{
"redirect_uri": []string{""},
"client_id": []string{"1234"},
Expand All @@ -116,7 +116,7 @@ func TestNewAuthorizeRequest(t *testing.T) {
/* redirect client mismatch */
{
desc: "client and request redirects mismatch",
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy},
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy},
query: url.Values{
"redirect_uri": []string{"https://foo.bar/cb"},
"client_id": []string{"1234"},
Expand All @@ -129,7 +129,7 @@ func TestNewAuthorizeRequest(t *testing.T) {
/* no state */
{
desc: "no state",
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy},
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy},
query: url.Values{
"redirect_uri": []string{"https://foo.bar/cb"},
"client_id": []string{"1234"},
Expand All @@ -143,7 +143,7 @@ func TestNewAuthorizeRequest(t *testing.T) {
/* short state */
{
desc: "short state",
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy},
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy},
query: url.Values{
"redirect_uri": {"https://foo.bar/cb"},
"client_id": {"1234"},
Expand All @@ -158,7 +158,7 @@ func TestNewAuthorizeRequest(t *testing.T) {
/* fails because scope not given */
{
desc: "should fail because client does not have scope baz",
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy},
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy},
query: url.Values{
"redirect_uri": {"https://foo.bar/cb"},
"client_id": {"1234"},
Expand All @@ -171,27 +171,58 @@ func TestNewAuthorizeRequest(t *testing.T) {
},
expectedError: ErrInvalidScope,
},
/* fails because scope not given */
{
desc: "should fail because client does not have scope baz",
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy},
query: url.Values{
"redirect_uri": {"https://foo.bar/cb"},
"client_id": {"1234"},
"response_type": {"code token"},
"state": {"strong-state"},
"scope": {"foo bar"},
"audience": {"https://cloud.ory.sh/api https://www.ory.sh/api"},
},
mock: func() {
store.EXPECT().GetClient(gomock.Any(), "1234").Return(&DefaultClient{
RedirectURIs: []string{"https://foo.bar/cb"}, Scopes: []string{"foo", "bar"},
Audience: []string{"https://cloud.ory.sh/api"},
}, nil)
},
expectedError: ErrInvalidRequest,
},
/* success case */
{
desc: "should pass",
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy},
conf: &Fosite{Store: store, ScopeStrategy: ExactScopeStrategy, AudienceMatchingStrategy: DefaultAudienceMatchingStrategy},
query: url.Values{
"redirect_uri": {"https://foo.bar/cb"},
"client_id": {"1234"},
"response_type": {"code token"},
"state": {"strong-state"},
"scope": {"foo bar"},
"audience": {"https://cloud.ory.sh/api https://www.ory.sh/api"},
},
mock: func() {
store.EXPECT().GetClient(gomock.Any(), "1234").Return(&DefaultClient{ResponseTypes: []string{"code token"}, RedirectURIs: []string{"https://foo.bar/cb"}, Scopes: []string{"foo", "bar"}}, nil)
store.EXPECT().GetClient(gomock.Any(), "1234").Return(&DefaultClient{
ResponseTypes: []string{"code token"},
RedirectURIs: []string{"https://foo.bar/cb"},
Scopes: []string{"foo", "bar"},
Audience: []string{"https://cloud.ory.sh/api", "https://www.ory.sh/api"},
}, nil)
},
expect: &AuthorizeRequest{
RedirectURI: redir,
ResponseTypes: []string{"code", "token"},
State: "strong-state",
Request: Request{
Client: &DefaultClient{ResponseTypes: []string{"code token"}, RedirectURIs: []string{"https://foo.bar/cb"}, Scopes: []string{"foo", "bar"}},
Scopes: []string{"foo", "bar"},
Client: &DefaultClient{
ResponseTypes: []string{"code token"}, RedirectURIs: []string{"https://foo.bar/cb"},
Scopes: []string{"foo", "bar"},
Audience: []string{"https://cloud.ory.sh/api", "https://www.ory.sh/api"},
},
Scopes: []string{"foo", "bar"},
Audience: []string{"https://cloud.ory.sh/api", "https://www.ory.sh/api"},
},
},
},
Expand All @@ -210,7 +241,7 @@ func TestNewAuthorizeRequest(t *testing.T) {
assert.EqualError(t, errors.Cause(err), c.expectedError.Error())
} else {
require.NoError(t, err)
AssertObjectKeysEqual(t, c.expect, ar, "ResponseTypes", "Scopes", "Client", "RedirectURI", "State")
AssertObjectKeysEqual(t, c.expect, ar, "ResponseTypes", "Audience", "Scopes", "Client", "RedirectURI", "State")
assert.NotNil(t, ar.GetRequestedAt())
}
})
Expand Down
Loading

0 comments on commit 891c4a8

Please sign in to comment.