Skip to content

Commit

Permalink
authorize: defined OAuth2.HandleResponseTypes
Browse files Browse the repository at this point in the history
Incorporated feedback from GitHub, did refactoring and renaming, added tests
  • Loading branch information
Aeneas Rekkas committed Jan 6, 2016
1 parent 1871702 commit 30b6e74
Show file tree
Hide file tree
Showing 12 changed files with 408 additions and 148 deletions.
33 changes: 14 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,20 @@ will be added in the close future.
```go
package main

import "github.com/ory-am/fosite"
import "github.com/ory-am/fosite/session"
import "github.com/ory-am/fosite/storage"
import(
"github.com/ory-am/fosite"
"github.com/ory-am/fosite/session"
"github.com/ory-am/fosite/storage"
"golang.org/x/net/context"
)

// Let's assume that we're in a http handler
func handleAuth(rw http.ResponseWriter, req *http.Request) {
store := fosite.NewPostgreSQLStore()
oauth2 := fosite.NewDefaultOAuth2(store)
ctx := context.Background()

authorizeRequest, err := oauth2.NewAuthorizeRequest(r, store)
authorizeRequest, err := oauth2.NewAuthorizeRequest(ctx, r)
if err != nil {
// ** This part of the API is not finalized yet **
// oauth2.RedirectError(rw, error)
Expand All @@ -153,7 +157,9 @@ func handleAuth(rw http.ResponseWriter, req *http.Request) {
// Once you have confirmed the users identity and consent that he indeed wants to give app XYZ authorization,
// you will use the user's id to create an authorize session
user := "12345"
session := fosite.NewAuthorizeSession(authorizeRequest, user)

// NewAuthorizeSessionSQL uses gob.encode to safely store data set with SetExtra
session := fosite.NewAuthorizeSessionSQL(authorizeRequest, user)

// You can store additional metadata, for example:
session.SetExtra(map[string]interface{}{
Expand All @@ -171,20 +177,9 @@ func handleAuth(rw http.ResponseWriter, req *http.Request) {

// Now is the time to handle the response types

// ** This part of the API is not finalized yet **
// response = &AuthorizeResponse{}
// err = oauth2.HandleResponseTypes(authorizeRequest, response, session)
// err = alsoHandleMyCustomResponseType(authorizeRequest, response, "fancyArguments", 1234)
//
// or
//
// this approach would make it possible to check if all response types could be served or not
// additionally, a callback for FinishAccessRequest could be provided
//
// response = &AuthorizeResponse{}
// oauth2.RegisterResponseTypeHandler("custom_type", alsoHandleMyCustomResponseType)
// err = oauth2.HandleResponseTypes(authorizeRequest, response, session)
// ****
// you can use a custom list of response type handlers by setting
// oauth2.ResponseTypeHandlers = []fosite.ResponseTypeHandler{}
response, err := oauth2.HandleResponseTypes(ctx, authorizeRequest, r)

// Almost done! The next step is going to persist the session in the database for later use.
// It is additionally going to output a result based on response_type.
Expand Down
91 changes: 91 additions & 0 deletions authorize_privates_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package fosite

import (
. "github.com/ory-am/fosite/client"
"github.com/stretchr/testify/assert"
"net/url"
"testing"
)

// TODO rfc6749 3.1. Authorization Endpoint
// The endpoint URI MAY include an "application/x-www-form-urlencoded" formatted (per Appendix B) query component
//
// rfc6749 3.1.2. Redirection Endpoint
// "The redirection endpoint URI MUST be an absolute URI as defined by [RFC3986] Section 4.3"
func TestGetRedirectURI(t *testing.T) {
for _, c := range []struct {
in string
isError bool
expected string
}{
{in: "", isError: true},
} {
values := url.Values{}
values.Set("redirect_uri", c.in)
res, err := redirectFromValues(values)
assert.Equal(t, c.isError, err != nil, "%s", err)
if err == nil {
assert.Equal(t, c.expected, res)
}
}
}

// rfc6749 10.6.
// Authorization Code Redirection URI Manipulation
// The authorization server MUST require public clients and SHOULD require confidential clients
// to register their redirection URIs. If a redirection URI is provided
// in the request, the authorization server MUST validate it against the
// registered value.
//
// rfc6819 4.4.1.7.
// Threat: Authorization "code" Leakage through Counterfeit Client
// The authorization server may also enforce the usage and validation
// of pre-registered redirect URIs (see Section 5.2.3.5).
func TestDoesClientWhiteListRedirect(t *testing.T) {
var err error
var redir string

for k, c := range []struct {
client Client
url string
isError bool
expected string
}{
{
client: &SecureClient{RedirectURIs: []string{""}},
url: "http://foo.com/cb",
isError: true,
},
{
client: &SecureClient{RedirectURIs: []string{"http://bar.com/cb"}},
url: "http://foo.com/cb",
isError: true,
},
{
client: &SecureClient{RedirectURIs: []string{"http://bar.com/cb"}},
url: "",
isError: false,
expected: "http://bar.com/cb",
},
{
client: &SecureClient{RedirectURIs: []string{""}},
url: "",
isError: true,
},
{
client: &SecureClient{RedirectURIs: []string{"http://bar.com/cb"}},
url: "http://bar.com/cb",
isError: false,
expected: "http://bar.com/cb",
},
{
client: &SecureClient{RedirectURIs: []string{"http://bar.com/cb"}},
url: "http://bar.com/cb123",
isError: true,
},
} {
redir, err = redirectFromClient(c.url, c.client)
assert.Equal(t, c.isError, err != nil, "%d: %s", k, err)
assert.Equal(t, c.expected, redir)
}
}
157 changes: 82 additions & 75 deletions authorize_test.go
Original file line number Diff line number Diff line change
@@ -1,98 +1,105 @@
package fosite
package fosite_test

import (
"github.com/golang/mock/gomock"
. "github.com/ory-am/fosite"
. "github.com/ory-am/fosite/client"
"github.com/ory-am/fosite/generator"
. "github.com/ory-am/fosite/internal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vektra/errors"
"golang.org/x/net/context"
"net/http"
"net/url"
"testing"
)

// TODO rfc6749 3.1. Authorization Endpoint
// The endpoint URI MAY include an "application/x-www-form-urlencoded" formatted (per Appendix B) query component
//
// rfc6749 3.1.2. Redirection Endpoint
// "The redirection endpoint URI MUST be an absolute URI as defined by [RFC3986] Section 4.3"
func TestGetRedirectURI(t *testing.T) {
for _, c := range []struct {
in string
isError bool
expected string
}{
{in: "", isError: true},
} {
values := url.Values{}
values.Set("redirect_uri", c.in)
res, err := redirectFromValues(values)
assert.Equal(t, c.isError, err != nil, "%s", err)
if err == nil {
assert.Equal(t, c.expected, res)
}
}
}

// rfc6749 10.6.
// Authorization Code Redirection URI Manipulation
// The authorization server MUST require public clients and SHOULD require confidential clients
// to register their redirection URIs. If a redirection URI is provided
// in the request, the authorization server MUST validate it against the
// registered value.
//
// rfc6819 4.4.1.7.
// Threat: Authorization "code" Leakage through Counterfeit Client
// The authorization server may also enforce the usage and validation
// of pre-registered redirect URIs (see Section 5.2.3.5).
func TestDoesClientWhiteListRedirect(t *testing.T) {
var err error
var redir string
func TestAuthorizeWorkflow(t *testing.T) {
ctrl := gomock.NewController(t)
store := NewMockStorage(ctrl)
gen := NewMockGenerator(ctrl)
defer ctrl.Finish()

for k, c := range []struct {
client Client
url string
isError bool
expected string
desc string
conf *OAuth2
r *http.Request
query url.Values
expectedError error
mock func()
expect *AuthorizeRequest
}{
{
client: &SecureClient{RedirectURIs: []string{""}},
url: "http://foo.com/cb",
isError: true,
},
{
client: &SecureClient{RedirectURIs: []string{"http://bar.com/cb"}},
url: "http://foo.com/cb",
isError: true,
},
{
client: &SecureClient{RedirectURIs: []string{"http://bar.com/cb"}},
url: "",
isError: false,
expected: "http://bar.com/cb",
},
{
client: &SecureClient{RedirectURIs: []string{""}},
url: "",
isError: true,
},
{
client: &SecureClient{RedirectURIs: []string{"http://bar.com/cb"}},
url: "http://bar.com/cb",
isError: false,
expected: "http://bar.com/cb",
},
{
client: &SecureClient{RedirectURIs: []string{"http://bar.com/cb"}},
url: "http://bar.com/cb123",
isError: true,
desc: "should pass",
conf: &OAuth2{
Store: store,
AuthorizeCodeGenerator: gen,
AllowedResponseTypes: []string{"code", "token"},
Lifetime: 3600,
},
query: url.Values{
"redirect_uri": []string{"http://foo.bar/cb"},
"client_id": []string{"1234"},
"response_type": []string{"code token"},
"state": []string{"strong-state"},
"scope": []string{"foo bar"},
},
mock: func() {
gen.EXPECT().Generate().Return(&generator.Token{Key: "foo", Signature: "bar"}, nil)
store.EXPECT().GetClient("1234").Return(&SecureClient{RedirectURIs: []string{"http://foo.bar/cb"}}, nil)
},
expect: &AuthorizeRequest{
RedirectURI: "http://foo.bar/cb",
Client: &SecureClient{ID: "1234", RedirectURIs: []string{"http://foo.bar/cb"}},
ResponseTypes: []string{"code", "token"},
State: "strong-state",
Scopes: []string{"foo", "bar"},
ExpiresIn: 3600,
Code: &generator.Token{Key: "foo", Signature: "bar"},
},
},
} {
redir, err = redirectFromClient(c.url, c.client)
assert.Equal(t, c.isError, err != nil, "%d: %s", k, err)
assert.Equal(t, c.expected, redir)
c.mock()
if c.r == nil {
c.r = &http.Request{Header: http.Header{}}
if c.query != nil {
c.r.URL = &url.URL{RawQuery: c.query.Encode()}
}
}

// equals to: c.conf = NewDefaultOAuth2(store)
c.conf.Store = store
authorizeRequest, err := c.conf.NewAuthorizeRequest(context.Background(), c.r)
require.Nil(t, err, "%d: %s", k, err)

userID := "user-id"
_ = NewAuthorizeSessionSQL(authorizeRequest, userID)

// if err := store.StoreAuthorizeSession(sess); err != nil {
// return err
// }

//response := NewAuthorizeResponse()
// err = oauth2.HandleResponseTypes(authorizeRequest, response, session)
// err = alsoHandleMyCustomResponseType(authorizeRequest, response, "fancyArguments", 1234)
//
// or
//
// this approach would make it possible to check if all response types could be served or not
// additionally, a callback for FinishAccessRequest could be provided
//
// response = &AuthorizeResponse{}
// oauth2.RegisterResponseTypeHandler("custom_type", alsoHandleMyCustomResponseType)
// err = oauth2.HandleResponseTypes(authorizeRequest, response, session)
// ****

// Almost done! The next step is going to persist the session in the database for later use.
// It is additionally going to output a result based on response_type.

// ** API not finalized yet **
// err := oauth2.FinishAuthorizeRequest(rw, response, session)
// ****
}
}

Expand Down
45 changes: 45 additions & 0 deletions handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package fosite

import (
"errors"
"golang.org/x/net/context"
"net/http"
)

var ErrInvalidResponseType = errors.New("This handler is unable handle any of the response types requested by the auhtorize request")
var ErrNoResponseTypeHandlerFound = errors.New("None of the handler's are able to handle this authorize request")

type ResponseTypeHandler interface {
// HandleResponseType handles an authorize request. To extend the handler's capabilities, the http request
// is passed along, if further information retrieval is required.
//
// If HandleResponseType fails, the handler implementation MUST return ErrInvalidResponseType.
HandleResponseType(context.Context, *Response, AuthorizeRequest, http.Request) error
}

// NewAuthorizeResponse iterates through all response type handlers and returns their result or
// ErrNoResponseTypeHandlerFound if none of the handler's were able to handle it.
//
// Important: Every ResponseTypeHandler should return ErrInvalidResponseType if it is unable to handle
// the given request and an arbitrary error if an error occurred
func (o *OAuth2) NewAuthorizeResponse(ctx context.Context, ar *AuthorizeRequest, r *http.Request) (*Response, error) {
var resp = new(Response)
var err error
var found bool

for _, h := range o.ResponseTypeHandlers {
// Dereference http request and authorize request so handler's can't mess with it.
err = h.HandleResponseType(ctx, resp, *ar, *r)
if err == nil {
found = true
} else if err != ErrInvalidResponseType {
return nil, err
}
}

if !found {
return nil, ErrNoResponseTypeHandlerFound
}

return resp, nil
}
Loading

0 comments on commit 30b6e74

Please sign in to comment.