diff --git a/.travis.yml b/.travis.yml index 47e045fb..a9563467 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,15 +3,18 @@ language: go go_import_path: github.com/ory/fosite go: - - 1.7 - - 1.8 + - 1.9.x env: - - GO15VENDOREXPERIMENT=1 + - DEP_VERSION="0.3.2" + +before_install: + - curl -L -s https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 -o $GOPATH/bin/dep + - chmod +x $GOPATH/bin/dep install: - - go get github.com/mattn/goveralls golang.org/x/tools/cmd/cover github.com/pierrre/gotestcover github.com/Masterminds/glide - - glide install + - dep ensure + - go get github.com/mattn/goveralls golang.org/x/tools/cmd/cover github.com/pierrre/gotestcover github.com/bradfitz/goimports script: - touch ./coverage.tmp @@ -19,4 +22,4 @@ script: echo 'mode: atomic' > coverage.txt - | go list ./... | grep -v /vendor | grep -v /internal | xargs -n1 -I{} sh -c 'go test -race -covermode=atomic -coverprofile=coverage.tmp -coverpkg $(go list ./... | grep -v /vendor | grep -v /internal | tr "\n" ",") {} && tail -n +2 coverage.tmp >> coverage.txt || exit 255' && rm coverage.tmp - - goveralls -coverprofile="coverage.txt" + - goveralls -coverprofile="coverage.txt" \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ffd0dee3..e869527c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,7 +96,7 @@ To make a pull request, you will need a GitHub account; if you are unclear on th 1. Create a feature branch off of `master` so that changes do not get mixed up. 1. [Rebase](https://git-scm.com/book/en/Git-Branching-Rebasing) your local changes against the `master` branch. -1. Run the full project test suite with the `go test $(glide novendor)` (or equivalent) command and confirm that it passes. +1. Run the full project test suite with the `go test ./...` (or equivalent) command and confirm that it passes. 1. Run `gofmt -s` (if the project is written in Go). 1. Accept the Developer's Certificate of Origin on all commits (see above). 1. Ensure that each commit has a subsystem prefix (ex: `controller: `). diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 00000000..cc221cad --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,135 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/asaskevich/govalidator" + packages = ["."] + revision = "73945b6115bfbbcc57d89b7316e28109364124e1" + version = "v7" + +[[projects]] + name = "github.com/davecgh/go-spew" + packages = ["spew"] + revision = "346938d642f2ec3594ed81d874461961cd0faa76" + version = "v1.1.0" + +[[projects]] + name = "github.com/dgrijalva/jwt-go" + packages = ["."] + revision = "dbeaa9332f19a944acb5736b4456cfcc02140e29" + version = "v3.1.0" + +[[projects]] + name = "github.com/golang/mock" + packages = ["gomock"] + revision = "13f360950a79f5864a972c786a10a50e44b69541" + version = "v1.0.0" + +[[projects]] + branch = "master" + name = "github.com/golang/protobuf" + packages = ["proto"] + revision = "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9" + +[[projects]] + name = "github.com/gorilla/context" + packages = ["."] + revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a" + version = "v1.1" + +[[projects]] + name = "github.com/gorilla/mux" + packages = ["."] + revision = "24fca303ac6da784b9e8269f724ddeb0b2eea5e7" + version = "v1.5.0" + +[[projects]] + branch = "master" + name = "github.com/gtank/cryptopasta" + packages = ["."] + revision = "1f550f6f2f69009f6ae57347c188e0a67cd4e500" + +[[projects]] + branch = "master" + name = "github.com/mohae/deepcopy" + packages = ["."] + revision = "c48cc78d482608239f6c4c92a4abd87eb8761c90" + +[[projects]] + branch = "master" + name = "github.com/moul/http2curl" + packages = ["."] + revision = "9ac6cf4d929b2fa8fd2d2e6dec5bb0feb4f4911d" + +[[projects]] + name = "github.com/oleiade/reflections" + packages = ["."] + revision = "2b6ec3da648e3e834dc41bad8d9ed7f2dc6a9496" + version = "v1.0.0" + +[[projects]] + name = "github.com/parnurzeal/gorequest" + packages = ["."] + revision = "a578a48e8d6ca8b01a3b18314c43c6716bb5f5a3" + version = "v0.2.15" + +[[projects]] + name = "github.com/pborman/uuid" + packages = ["."] + revision = "e790cca94e6cc75c7064b1332e63811d4aae1a53" + version = "v1.1" + +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + +[[projects]] + name = "github.com/pmezard/go-difflib" + packages = ["difflib"] + revision = "792786c7400a136282c1664665ae0a8db921c6c2" + version = "v1.0.0" + +[[projects]] + name = "github.com/stretchr/testify" + packages = ["assert","require"] + revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" + version = "v1.1.4" + +[[projects]] + branch = "master" + name = "golang.org/x/crypto" + packages = ["bcrypt","blowfish"] + revision = "2509b142fb2b797aa7587dad548f113b2c0f20ce" + +[[projects]] + branch = "master" + name = "golang.org/x/net" + packages = ["context","context/ctxhttp","idna","publicsuffix"] + revision = "4b14673ba32bee7f5ac0f990a48f033919fd418b" + +[[projects]] + branch = "master" + name = "golang.org/x/oauth2" + packages = [".","clientcredentials","internal"] + revision = "bb50c06baba3d0c76f9d125c0719093e315b5b44" + +[[projects]] + branch = "master" + name = "golang.org/x/text" + packages = ["collate","collate/build","internal/colltab","internal/gen","internal/tag","internal/triegen","internal/ucd","language","secure/bidirule","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable"] + revision = "6eab0e8f74e86c598ec3b6fad4888e0c11482d48" + +[[projects]] + name = "google.golang.org/appengine" + packages = ["internal","internal/base","internal/datastore","internal/log","internal/remote_api","internal/urlfetch","urlfetch"] + revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a" + version = "v1.0.0" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "1ec88100ce674d2223851185b8712fcf94ba459b7b891314b79849ad95916938" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 00000000..02215b1d --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,74 @@ + +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" + + +[[constraint]] + name = "github.com/asaskevich/govalidator" + version = "7.0.0" + +[[constraint]] + name = "github.com/dgrijalva/jwt-go" + version = "3.1.0" + +[[constraint]] + name = "github.com/golang/mock" + version = "1.0.0" + +[[constraint]] + name = "github.com/gorilla/mux" + version = "1.5.0" + +[[constraint]] + branch = "master" + name = "github.com/gtank/cryptopasta" + +[[constraint]] + branch = "master" + name = "github.com/mohae/deepcopy" + +[[constraint]] + name = "github.com/oleiade/reflections" + version = "1.0.0" + +[[constraint]] + name = "github.com/parnurzeal/gorequest" + version = "0.2.15" + +[[constraint]] + name = "github.com/pborman/uuid" + version = "1.1.0" + +[[constraint]] + name = "github.com/pkg/errors" + version = "0.8.0" + +[[constraint]] + name = "github.com/stretchr/testify" + version = "1.1.4" + +[[constraint]] + branch = "master" + name = "golang.org/x/crypto" + +[[constraint]] + branch = "master" + name = "golang.org/x/oauth2" diff --git a/HISTORY.md b/HISTORY.md index dcc8174e..8fd3df9b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -5,6 +5,13 @@ bumps (`0.1.0` -> `0.2.0`). +- [0.15.0](#0150) +- [0.14.0](#0140) +- [0.13.0](#0130) + - [Breaking changes](#breaking-changes) +- [0.12.0](#0120) + - [Breaking changes](#breaking-changes-1) + - [Improved cryptographic methods](#improved-cryptographic-methods) - [0.11.0](#0110) - [Non-breaking changes](#non-breaking-changes) - [Storage adapter](#storage-adapter) @@ -19,7 +26,7 @@ bumps (`0.1.0` -> `0.2.0`). - [0.10.0](#0100) - [0.9.0](#090) - [0.8.0](#080) - - [Breaking changes](#breaking-changes) + - [Breaking changes](#breaking-changes-2) - [`ClientManager`](#clientmanager) - [`OAuth2Provider`](#oauth2provider) - [0.7.0](#070) @@ -32,6 +39,50 @@ bumps (`0.1.0` -> `0.2.0`). +## 0.16.0 + +This patch introduces `SendDebugMessagesToClients` to the Fosite struct which enables/disables sending debug information to +clients. Debug information may contain sensitive information as it forwards error messages from, for example, storage +implementations. For this reason, `RevealDebugPayloads` defaults to false. Keep in mind that the information may be +very helpful when specific OAuth 2.0 requests fail and we generally recommend displaying debug information. + +Additionally, error keys for JSON changed which caused a new minor version, speicifically +[`statusCode` was changed to `status_code`](https://github.com/ory/fosite/pull/242/files#diff-dd25e0e0a594c3f3592c1c717039b85eR221). + + +## 0.15.0 + +This release focuses on improving compatibility with OpenID Connect Certification and better error context. + +* Error handling is improved by explicitly adding debug information (e.g. "Token invalid because it was not found +in the database") to the error object. Previously, the original error was prepended which caused weird formatting issues. +* Allows client credentials in POST body at the `/oauth2/token` endpoint. Please note that this method is not recommended +to be used, unless the client making the request is unable to use HTTP Basic Authorization. +* Allows public clients (without secret) to access the `/oauth2/token` endpoint which was previously only possible by adding an arbitrary +secret. + +This release has no breaking changes to the external API but due to the nature of the changes, it is released +as a new major version. + +## 0.14.0 + +Improves error contexts. A breaking code changes to the public API was reverted with 0.14.1. + +## 0.13.0 + +### Breaking changes + +`glide` was replaced with `dep`. + +## 0.12.0 + +### Breaking changes + +#### Improved cryptographic methods + +* The minimum required secret length used to generate signatures of access tokens has increased from 16 to 32 byte. +* The algorithm used to generate access tokens using the HMAC-SHA strategy has changed from HMAC-SHA256 to HMAC-SHA512. + ## 0.11.0 ### Non-breaking changes diff --git a/README.md b/README.md index b4cfdc38..736990ad 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ was out there, so we decided to build it ourselves. The core public API is almost stable as most changes will only touch the inner workings. -We strongly encourage vendoring fosite using [glide](https://glide.sh) or comparable tools. +We strongly encourage vendoring fosite using [dep](https://github.com/golang/dep) or comparable tools. ## Example @@ -69,10 +69,9 @@ of code. You can run this minimalistic example by doing ``` -go get github.com/Masterminds/glide go get github.com/ory/fosite-example cd $GOPATH/src/github.com/ory/fosite-example -glide install +dep ensure go install github.com/ory/fosite-example fosite-example ``` @@ -148,7 +147,7 @@ GOPATH environment variable. go get -d github.com/ory/fosite ``` -We recommend to use [Glide](https://github.com/Masterminds/glide) or [Godep](https://github.com/tools/godep) to +We recommend to use [dep](https://github.com/golang/dep) to mitigate compatibility breaks that come with new api versions. ## Documentation @@ -385,10 +384,10 @@ go get -d github.com/ory/fosite cd $GOPATH/src/github.com/ory/fosite git status git remote add myfork -go test $(glide novendor) +go test ./.. ``` -Simple, right? Now you are ready to go! Make sure to run `go test $(glide novendor)` often, detecting problems with your code +Simple, right? Now you are ready to go! Make sure to run `go test ./...` often, detecting problems with your code rather sooner than later. Please read [CONTRIBUTE.md] before creating pull requests and issues. ### Refresh mock objects diff --git a/access_error.go b/access_error.go index e5b7b8bd..f54b7c63 100644 --- a/access_error.go +++ b/access_error.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -7,13 +21,17 @@ import ( ) func (c *Fosite) WriteAccessError(rw http.ResponseWriter, _ AccessRequester, err error) { - writeJsonError(rw, err) + c.writeJsonError(rw, err) } -func writeJsonError(rw http.ResponseWriter, err error) { +func (c *Fosite) writeJsonError(rw http.ResponseWriter, err error) { rw.Header().Set("Content-Type", "application/json;charset=UTF-8") rfcerr := ErrorToRFC6749Error(err) + if !c.SendDebugMessagesToClients { + rfcerr.Debug = "" + } + js, err := json.Marshal(rfcerr) if err != nil { http.Error(rw, fmt.Sprintf(`{"error": "%s"}`, err.Error()), http.StatusInternalServerError) diff --git a/access_error_test.go b/access_error_test.go index f4ecc745..79840b2e 100644 --- a/access_error_test.go +++ b/access_error_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite_test import ( @@ -6,6 +20,8 @@ import ( "net/http/httptest" "testing" + "fmt" + "github.com/golang/mock/gomock" . "github.com/ory/fosite" . "github.com/ory/fosite/internal" @@ -33,29 +49,43 @@ func TestWriteAccessError_RFC6749(t *testing.T) { f := &Fosite{} for k, c := range []struct { - err error - code string + err *RFC6749Error + code string + debug bool }{ - {ErrInvalidRequest, "invalid_request"}, - {ErrInvalidClient, "invalid_client"}, - {ErrInvalidGrant, "invalid_grant"}, - {ErrInvalidScope, "invalid_scope"}, - {ErrUnauthorizedClient, "unauthorized_client"}, - {ErrUnsupportedGrantType, "unsupported_grant_type"}, + {ErrInvalidRequest.WithDebug("some-debug"), "invalid_request", false}, + {ErrInvalidClient.WithDebug("some-debug"), "invalid_client", false}, + {ErrInvalidGrant.WithDebug("some-debug"), "invalid_grant", false}, + {ErrInvalidScope.WithDebug("some-debug"), "invalid_scope", false}, + {ErrUnauthorizedClient.WithDebug("some-debug"), "unauthorized_client", false}, + {ErrUnsupportedGrantType.WithDebug("some-debug"), "unsupported_grant_type", false}, } { - rw := httptest.NewRecorder() - f.WriteAccessError(rw, nil, c.err) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + f.SendDebugMessagesToClients = c.debug + + rw := httptest.NewRecorder() + f.WriteAccessError(rw, nil, c.err) + + var params struct { + Error string `json:"error"` // specified by RFC, required + Description string `json:"error_description"` // specified by RFC, optional + Debug string `json:"error_debug"` + Hint string `json:"error_hint"` + } - var params struct { - Error string `json:"error"` // specified by RFC, required - Description string `json:"error_description"` // specified by RFC, optional - } + require.NotNil(t, rw.Body) + err := json.NewDecoder(rw.Body).Decode(¶ms) + require.NoError(t, err) - require.NotNil(t, rw.Body, "(%d) %s: nil body", k, c.code) - err := json.NewDecoder(rw.Body).Decode(¶ms) - require.NoError(t, err, "(%d) %s", k, c.code) + assert.Equal(t, c.code, params.Error) + assert.Equal(t, c.err.Description, params.Description) + assert.Equal(t, c.err.Hint, params.Hint) - assert.Equal(t, c.code, params.Error, "(%d) %s: error", k, c.code) - assert.Equal(t, c.err.Error(), params.Description, "(%d) %s: description", k, c.code) + if !c.debug { + assert.Empty(t, params.Debug) + } else { + assert.Equal(t, "some-debug", params.Debug) + } + }) } } diff --git a/access_request.go b/access_request.go index 810b7de3..984b9839 100644 --- a/access_request.go +++ b/access_request.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite type AccessRequest struct { diff --git a/access_request_handler.go b/access_request_handler.go index 4155d4c2..c0eeda37 100644 --- a/access_request_handler.go +++ b/access_request_handler.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -40,9 +54,9 @@ func (f *Fosite) NewAccessRequest(ctx context.Context, r *http.Request, session accessRequest := NewAccessRequest(session) if r.Method != "POST" { - return accessRequest, errors.Wrap(ErrInvalidRequest, "HTTP method is not POST") + return accessRequest, errors.WithStack(ErrInvalidRequest.WithDebug("HTTP method is not POST")) } else if err := r.ParseForm(); err != nil { - return accessRequest, errors.Wrap(ErrInvalidRequest, err.Error()) + return accessRequest, errors.WithStack(ErrInvalidRequest.WithDebug(err.Error())) } accessRequest.Form = r.PostForm @@ -53,28 +67,24 @@ func (f *Fosite) NewAccessRequest(ctx context.Context, r *http.Request, session accessRequest.SetRequestedScopes(removeEmpty(strings.Split(r.PostForm.Get("scope"), " "))) accessRequest.GrantTypes = removeEmpty(strings.Split(r.PostForm.Get("grant_type"), " ")) if len(accessRequest.GrantTypes) < 1 { - return accessRequest, errors.Wrap(ErrInvalidRequest, "No grant type given") + return accessRequest, errors.WithStack(ErrInvalidRequest.WithDebug("No grant type given")) } // Decode client_id and client_secret which should be in "application/x-www-form-urlencoded" format. - var clientID, clientSecret string - if id, secret, ok := r.BasicAuth(); !ok { - return accessRequest, errors.Wrap(ErrInvalidRequest, "HTTP authorization header missing or invalid") - } else if clientID, err = url.QueryUnescape(id); err != nil { - return accessRequest, errors.Wrap(ErrInvalidRequest, `The client id in the HTTP authorization header could not be decoded from "application/x-www-form-urlencoded"`) - } else if clientSecret, err = url.QueryUnescape(secret); err != nil { - return accessRequest, errors.Wrap(ErrInvalidRequest, `The client secret in the HTTP authorization header could not be decoded from "application/x-www-form-urlencoded"`) + clientID, clientSecret, err := clientCredentialsFromRequest(r) + if err != nil { + return accessRequest, err } client, err := f.Store.GetClient(ctx, clientID) if err != nil { - return accessRequest, errors.Wrap(ErrInvalidClient, err.Error()) + return accessRequest, errors.WithStack(ErrInvalidClient.WithDebug(err.Error())) } if !client.IsPublic() { // Enforce client authentication if err := f.Hasher.Compare(client.GetHashedSecret(), []byte(clientSecret)); err != nil { - return accessRequest, errors.Wrap(ErrInvalidClient, err.Error()) + return accessRequest, errors.WithStack(ErrInvalidClient.WithDebug(err.Error())) } } accessRequest.Client = client @@ -83,7 +93,7 @@ func (f *Fosite) NewAccessRequest(ctx context.Context, r *http.Request, session for _, loader := range f.TokenEndpointHandlers { if err := loader.HandleTokenEndpointRequest(ctx, accessRequest); err == nil { found = true - } else if errors.Cause(err) == ErrUnknownRequest { + } else if errors.Cause(err).Error() == ErrUnknownRequest.Error() { // do nothing } else if err != nil { return accessRequest, err @@ -95,3 +105,32 @@ func (f *Fosite) NewAccessRequest(ctx context.Context, r *http.Request, session } return accessRequest, nil } + +func clientCredentialsFromRequest(r *http.Request) (clientID, clientSecret string, err error) { + if id, secret, ok := r.BasicAuth(); !ok { + return clientCredentialsFromRequestBody(r) + } else if clientID, err = url.QueryUnescape(id); err != nil { + return "", "", errors.WithStack(ErrInvalidRequest.WithDebug(`The client id in the HTTP authorization header could not be decoded from "application/x-www-form-urlencoded"`)) + } else if clientSecret, err = url.QueryUnescape(secret); err != nil { + return "", "", errors.WithStack(ErrInvalidRequest.WithDebug(`The client secret in the HTTP authorization header could not be decoded from "application/x-www-form-urlencoded"`)) + } + + return clientID, clientSecret, nil +} + +func clientCredentialsFromRequestBody(r *http.Request) (clientID, clientSecret string, err error) { + clientID = r.PostForm.Get("client_id") + clientSecret = r.PostForm.Get("client_secret") + + if clientID == "" { + return "", "", errors.WithStack(ErrInvalidRequest.WithDebug("Client credentials missing or malformed in both HTTP Authorization header and HTTP POST body")) + } + + if clientID, err = url.QueryUnescape(clientID); err != nil { + return "", "", errors.WithStack(ErrInvalidRequest.WithDebug(`The client id in the HTTP authorization header could not be decoded from "application/x-www-form-urlencoded"`)) + } else if clientSecret, err = url.QueryUnescape(clientSecret); err != nil { + return "", "", errors.WithStack(ErrInvalidRequest.WithDebug(`The client secret in the HTTP authorization header could not be decoded from "application/x-www-form-urlencoded"`)) + } + + return clientID, clientSecret, nil +} diff --git a/access_request_handler_test.go b/access_request_handler_test.go index c4dbbc90..3dc740a4 100644 --- a/access_request_handler_test.go +++ b/access_request_handler_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite_test import ( @@ -12,6 +26,7 @@ import ( "github.com/ory/fosite/internal" "github.com/pkg/errors" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewAccessRequest(t *testing.T) { @@ -35,6 +50,7 @@ func TestNewAccessRequest(t *testing.T) { { header: http.Header{}, expectErr: ErrInvalidRequest, + form: url.Values{}, method: "POST", mock: func() {}, }, @@ -52,7 +68,7 @@ func TestNewAccessRequest(t *testing.T) { method: "POST", form: url.Values{ "grant_type": {"foo"}, - "client_id": {"foo"}, + "client_id": {""}, }, expectErr: ErrInvalidRequest, mock: func() {}, @@ -173,24 +189,26 @@ func TestNewAccessRequest(t *testing.T) { }, }, } { - r := &http.Request{ - Header: c.header, - PostForm: c.form, - Form: c.form, - Method: c.method, - } - c.mock() - ctx := NewContext() - fosite.TokenEndpointHandlers = c.handlers - ar, err := fosite.NewAccessRequest(ctx, r, new(DefaultSession)) - assert.True(t, errors.Cause(err) == c.expectErr, "%d\nwant: %s \ngot: %s", k, c.expectErr, err) - if err != nil { - t.Logf("Error occured: %v", err) - } else { - AssertObjectKeysEqual(t, c.expect, ar, "GrantTypes", "Client") - assert.NotNil(t, ar.GetRequestedAt()) - } - t.Logf("Passed test case %d", k) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + r := &http.Request{ + Header: c.header, + PostForm: c.form, + Form: c.form, + Method: c.method, + } + c.mock() + ctx := NewContext() + fosite.TokenEndpointHandlers = c.handlers + ar, err := fosite.NewAccessRequest(ctx, r, new(DefaultSession)) + + if c.expectErr != nil { + assert.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + AssertObjectKeysEqual(t, c.expect, ar, "GrantTypes", "Client") + assert.NotNil(t, ar.GetRequestedAt()) + } + }) } } diff --git a/access_request_test.go b/access_request_test.go index 8494ff45..cbd782f0 100644 --- a/access_request_test.go +++ b/access_request_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( diff --git a/access_response.go b/access_response.go index 1a9b16f1..11d121ea 100644 --- a/access_response.go +++ b/access_response.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( diff --git a/access_response_test.go b/access_response_test.go index 46a5a172..963fec71 100644 --- a/access_response_test.go +++ b/access_response_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite_test import ( diff --git a/access_response_writer.go b/access_response_writer.go index ef07bac1..e190c207 100644 --- a/access_response_writer.go +++ b/access_response_writer.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -12,14 +26,15 @@ func (f *Fosite) NewAccessResponse(ctx context.Context, requester AccessRequeste response := NewAccessResponse() for _, tk = range f.TokenEndpointHandlers { - if err = tk.PopulateTokenEndpointResponse(ctx, requester, response); errors.Cause(err) == ErrUnknownRequest { + if err = tk.PopulateTokenEndpointResponse(ctx, requester, response); err == nil { + } else if errors.Cause(err).Error() == ErrUnknownRequest.Error() { } else if err != nil { return nil, err } } if response.GetAccessToken() == "" || response.GetTokenType() == "" { - return nil, errors.Wrap(ErrServerError, "Access token or token type not set") + return nil, errors.WithStack(ErrServerError.WithDebug("Access token or token type not set")) } return response, nil diff --git a/access_response_writer_test.go b/access_response_writer_test.go index 6d3f8ceb..f37d3c6b 100644 --- a/access_response_writer_test.go +++ b/access_response_writer_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite_test import ( @@ -5,11 +19,14 @@ import ( "context" + "fmt" + "github.com/golang/mock/gomock" . "github.com/ory/fosite" "github.com/ory/fosite/internal" "github.com/pkg/errors" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewAccessResponse(t *testing.T) { @@ -67,11 +84,17 @@ func TestNewAccessResponse(t *testing.T) { }, }, } { - f.TokenEndpointHandlers = c.handlers - c.mock() - ar, err := f.NewAccessResponse(nil, nil) - assert.True(t, errors.Cause(err) == c.expectErr, "%d", k) - assert.Equal(t, ar, c.expect) - t.Logf("Passed test case %d", k) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + f.TokenEndpointHandlers = c.handlers + c.mock() + ar, err := f.NewAccessResponse(nil, nil) + + if c.expectErr != nil { + assert.EqualError(t, errors.Cause(err), c.expectErr.Error()) + } else { + require.NoError(t, err) + assert.Equal(t, ar, c.expect) + } + }) } } diff --git a/access_write.go b/access_write.go index a016c8be..a86433d9 100644 --- a/access_write.go +++ b/access_write.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( diff --git a/access_write_test.go b/access_write_test.go index df077f41..84eb2d7d 100644 --- a/access_write_test.go +++ b/access_write_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite_test import ( diff --git a/arguments.go b/arguments.go index 776389da..9fb48c18 100644 --- a/arguments.go +++ b/arguments.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import "strings" @@ -26,6 +40,16 @@ func (r Arguments) Has(items ...string) bool { return true } +func (r Arguments) HasOneOf(items ...string) bool { + for _, item := range items { + if StringInSlice(item, r) { + return true + } + } + + return false +} + func (r Arguments) Exact(name string) bool { return name == strings.Join(r, " ") } diff --git a/arguments_test.go b/arguments_test.go index ded18d8e..dbec3cfd 100644 --- a/arguments_test.go +++ b/arguments_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -146,3 +160,30 @@ func TestArgumentsMatches(t *testing.T) { t.Logf("Passed test case %d", k) } } + +func TestArgumentsOneOf(t *testing.T) { + for k, c := range []struct { + args Arguments + oneOf []string + expect bool + }{ + { + args: Arguments{"baz", "bar"}, + oneOf: []string{"foo", "bar"}, + expect: true, + }, + { + args: Arguments{"foo", "baz"}, + oneOf: []string{"foo", "bar"}, + expect: true, + }, + { + args: Arguments{"baz"}, + oneOf: []string{"foo", "bar"}, + expect: false, + }, + } { + assert.Equal(t, c.expect, c.args.HasOneOf(c.oneOf...), "%d", k) + t.Logf("Passed test case %d", k) + } +} diff --git a/authorize_error.go b/authorize_error.go index bd971e1b..5cca4a23 100644 --- a/authorize_error.go +++ b/authorize_error.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -9,6 +23,10 @@ import ( func (c *Fosite) WriteAuthorizeError(rw http.ResponseWriter, ar AuthorizeRequester, err error) { rfcerr := ErrorToRFC6749Error(err) if !ar.IsRedirectURIValid() { + if !c.SendDebugMessagesToClients { + rfcerr.Debug = "" + } + js, err := json.MarshalIndent(rfcerr, "", "\t") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) @@ -26,6 +44,13 @@ func (c *Fosite) WriteAuthorizeError(rw http.ResponseWriter, ar AuthorizeRequest query.Add("error", rfcerr.Name) query.Add("error_description", rfcerr.Description) query.Add("state", ar.GetState()) + if c.SendDebugMessagesToClients && rfcerr.Debug != "" { + query.Add("error_debug", rfcerr.Debug) + } + + if rfcerr.Hint != "" { + query.Add("error_hint", rfcerr.Hint) + } if ar.GetResponseTypes().Exact("token") || len(ar.GetResponseTypes()) > 1 { redirectURI.Fragment = query.Encode() diff --git a/authorize_error_test.go b/authorize_error_test.go index 0d059088..c39d1bb0 100644 --- a/authorize_error_test.go +++ b/authorize_error_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite_test import ( @@ -5,6 +19,8 @@ import ( "net/url" "testing" + "fmt" + "github.com/golang/mock/gomock" . "github.com/ory/fosite" . "github.com/ory/fosite/internal" @@ -26,11 +42,6 @@ import ( // additional query parameters. The endpoint URI MUST NOT include a // fragment component. func TestWriteAuthorizeError(t *testing.T) { - ctrl := gomock.NewController(t) - rw := NewMockResponseWriter(ctrl) - req := NewMockAuthorizeRequester(ctrl) - defer ctrl.Finish() - var urls = []string{ "https://foobar.com/", "https://foobar.com/?foo=bar", @@ -41,28 +52,45 @@ func TestWriteAuthorizeError(t *testing.T) { purls = append(purls, purl) } - oauth2 := &Fosite{} header := http.Header{} for k, c := range []struct { err error - mock func() - checkHeader func(int) + debug bool + mock func(*MockResponseWriter, *MockAuthorizeRequester) + checkHeader func(*testing.T, int) }{ { err: ErrInvalidGrant, - mock: func() { + mock: func(rw *MockResponseWriter, req *MockAuthorizeRequester) { req.EXPECT().IsRedirectURIValid().Return(false) rw.EXPECT().Header().Return(header) rw.EXPECT().WriteHeader(http.StatusBadRequest) rw.EXPECT().Write(gomock.Any()) }, - checkHeader: func(k int) { - assert.Equal(t, "application/json", header.Get("Content-Type"), "%d", k) + checkHeader: func(t *testing.T, k int) { + assert.Equal(t, "application/json", header.Get("Content-Type")) }, }, { - err: ErrInvalidRequest, - mock: func() { + debug: true, + err: ErrInvalidRequest.WithDebug("with-debug"), + mock: func(rw *MockResponseWriter, req *MockAuthorizeRequester) { + req.EXPECT().IsRedirectURIValid().Return(true) + req.EXPECT().GetRedirectURI().Return(copyUrl(purls[0])) + req.EXPECT().GetState().Return("foostate") + req.EXPECT().GetResponseTypes().MaxTimes(2).Return(Arguments([]string{"code"})) + rw.EXPECT().Header().Return(header) + rw.EXPECT().WriteHeader(http.StatusFound) + }, + checkHeader: func(t *testing.T, k int) { + a, _ := url.Parse("https://foobar.com/?error=invalid_request&error_debug=with-debug&error_description=The+request+is+missing+a+required+parameter%2C+includes+an+invalid+parameter+value%2C+includes+a+parameter+more+than+once%2C+or+is+otherwise+malformed&error_hint=Make+sure+that+the+various+parameters+are+correct%2C+be+aware+of+case+sensitivity+and+trim+your+parameters.+Make+sure+that+the+client+you+are+using+has+exactly+whitelisted+the+redirect_uri+you+specified.&state=foostate") + b, _ := url.Parse(header.Get("Location")) + assert.Equal(t, a, b) + }, + }, + { + err: ErrInvalidRequest.WithDebug("with-debug"), + mock: func(rw *MockResponseWriter, req *MockAuthorizeRequester) { req.EXPECT().IsRedirectURIValid().Return(true) req.EXPECT().GetRedirectURI().Return(copyUrl(purls[0])) req.EXPECT().GetState().Return("foostate") @@ -70,15 +98,15 @@ func TestWriteAuthorizeError(t *testing.T) { rw.EXPECT().Header().Return(header) rw.EXPECT().WriteHeader(http.StatusFound) }, - checkHeader: func(k int) { - a, _ := url.Parse("https://foobar.com/?error=invalid_request&error_description=The+request+is+missing+a+required+parameter%2C+includes+an+invalid+parameter+value%2C+includes+a+parameter+more+than+once%2C+or+is+otherwise+malformed&state=foostate") + checkHeader: func(t *testing.T, k int) { + a, _ := url.Parse("https://foobar.com/?error=invalid_request&error_description=The+request+is+missing+a+required+parameter%2C+includes+an+invalid+parameter+value%2C+includes+a+parameter+more+than+once%2C+or+is+otherwise+malformed&error_hint=Make+sure+that+the+various+parameters+are+correct%2C+be+aware+of+case+sensitivity+and+trim+your+parameters.+Make+sure+that+the+client+you+are+using+has+exactly+whitelisted+the+redirect_uri+you+specified.&state=foostate") b, _ := url.Parse(header.Get("Location")) - assert.Equal(t, a, b, "%d", k) + assert.Equal(t, a, b) }, }, { err: ErrInvalidRequest, - mock: func() { + mock: func(rw *MockResponseWriter, req *MockAuthorizeRequester) { req.EXPECT().IsRedirectURIValid().Return(true) req.EXPECT().GetRedirectURI().Return(copyUrl(purls[1])) req.EXPECT().GetState().Return("foostate") @@ -86,15 +114,15 @@ func TestWriteAuthorizeError(t *testing.T) { rw.EXPECT().Header().Return(header) rw.EXPECT().WriteHeader(http.StatusFound) }, - checkHeader: func(k int) { - a, _ := url.Parse("https://foobar.com/?error=invalid_request&error_description=The+request+is+missing+a+required+parameter%2C+includes+an+invalid+parameter+value%2C+includes+a+parameter+more+than+once%2C+or+is+otherwise+malformed&foo=bar&state=foostate") + checkHeader: func(t *testing.T, k int) { + a, _ := url.Parse("https://foobar.com/?error=invalid_request&error_description=The+request+is+missing+a+required+parameter%2C+includes+an+invalid+parameter+value%2C+includes+a+parameter+more+than+once%2C+or+is+otherwise+malformed&error_hint=Make+sure+that+the+various+parameters+are+correct%2C+be+aware+of+case+sensitivity+and+trim+your+parameters.+Make+sure+that+the+client+you+are+using+has+exactly+whitelisted+the+redirect_uri+you+specified.&foo=bar&state=foostate") b, _ := url.Parse(header.Get("Location")) - assert.Equal(t, a, b, "%d", k) + assert.Equal(t, a, b) }, }, { err: ErrInvalidRequest, - mock: func() { + mock: func(rw *MockResponseWriter, req *MockAuthorizeRequester) { req.EXPECT().IsRedirectURIValid().Return(true) req.EXPECT().GetRedirectURI().Return(copyUrl(purls[0])) req.EXPECT().GetState().Return("foostate") @@ -102,16 +130,16 @@ func TestWriteAuthorizeError(t *testing.T) { rw.EXPECT().Header().Return(header) rw.EXPECT().WriteHeader(http.StatusFound) }, - checkHeader: func(k int) { + checkHeader: func(t *testing.T, k int) { a, _ := url.Parse("https://foobar.com/") - a.Fragment = "error=invalid_request&error_description=The+request+is+missing+a+required+parameter%2C+includes+an+invalid+parameter+value%2C+includes+a+parameter+more+than+once%2C+or+is+otherwise+malformed&state=foostate" + a.Fragment = "error=invalid_request&error_description=The+request+is+missing+a+required+parameter%2C+includes+an+invalid+parameter+value%2C+includes+a+parameter+more+than+once%2C+or+is+otherwise+malformed&error_hint=Make+sure+that+the+various+parameters+are+correct%2C+be+aware+of+case+sensitivity+and+trim+your+parameters.+Make+sure+that+the+client+you+are+using+has+exactly+whitelisted+the+redirect_uri+you+specified.&state=foostate" b, _ := url.Parse(header.Get("Location")) - assert.Equal(t, a, b, "%d", k) + assert.Equal(t, a, b) }, }, { err: ErrInvalidRequest, - mock: func() { + mock: func(rw *MockResponseWriter, req *MockAuthorizeRequester) { req.EXPECT().IsRedirectURIValid().Return(true) req.EXPECT().GetRedirectURI().Return(copyUrl(purls[1])) req.EXPECT().GetState().Return("foostate") @@ -119,16 +147,16 @@ func TestWriteAuthorizeError(t *testing.T) { rw.EXPECT().Header().Return(header) rw.EXPECT().WriteHeader(http.StatusFound) }, - checkHeader: func(k int) { + checkHeader: func(t *testing.T, k int) { a, _ := url.Parse("https://foobar.com/?foo=bar") - a.Fragment = "error=invalid_request&error_description=The+request+is+missing+a+required+parameter%2C+includes+an+invalid+parameter+value%2C+includes+a+parameter+more+than+once%2C+or+is+otherwise+malformed&state=foostate" + a.Fragment = "error=invalid_request&error_description=The+request+is+missing+a+required+parameter%2C+includes+an+invalid+parameter+value%2C+includes+a+parameter+more+than+once%2C+or+is+otherwise+malformed&error_hint=Make+sure+that+the+various+parameters+are+correct%2C+be+aware+of+case+sensitivity+and+trim+your+parameters.+Make+sure+that+the+client+you+are+using+has+exactly+whitelisted+the+redirect_uri+you+specified.&state=foostate" b, _ := url.Parse(header.Get("Location")) - assert.Equal(t, a.String(), b.String(), "%d", k) + assert.Equal(t, a.String(), b.String()) }, }, { err: ErrInvalidRequest, - mock: func() { + mock: func(rw *MockResponseWriter, req *MockAuthorizeRequester) { req.EXPECT().IsRedirectURIValid().Return(true) req.EXPECT().GetRedirectURI().Return(copyUrl(purls[0])) req.EXPECT().GetState().Return("foostate") @@ -136,16 +164,16 @@ func TestWriteAuthorizeError(t *testing.T) { rw.EXPECT().Header().Return(header) rw.EXPECT().WriteHeader(http.StatusFound) }, - checkHeader: func(k int) { + checkHeader: func(t *testing.T, k int) { a, _ := url.Parse("https://foobar.com/") - a.Fragment = "error=invalid_request&error_description=The+request+is+missing+a+required+parameter%2C+includes+an+invalid+parameter+value%2C+includes+a+parameter+more+than+once%2C+or+is+otherwise+malformed&state=foostate" + a.Fragment = "error=invalid_request&error_description=The+request+is+missing+a+required+parameter%2C+includes+an+invalid+parameter+value%2C+includes+a+parameter+more+than+once%2C+or+is+otherwise+malformed&error_hint=Make+sure+that+the+various+parameters+are+correct%2C+be+aware+of+case+sensitivity+and+trim+your+parameters.+Make+sure+that+the+client+you+are+using+has+exactly+whitelisted+the+redirect_uri+you+specified.&state=foostate" b, _ := url.Parse(header.Get("Location")) - assert.Equal(t, a, b, "%d", k) + assert.Equal(t, a, b) }, }, { - err: ErrInvalidRequest, - mock: func() { + err: ErrInvalidRequest.WithDebug("with-debug"), + mock: func(rw *MockResponseWriter, req *MockAuthorizeRequester) { req.EXPECT().IsRedirectURIValid().Return(true) req.EXPECT().GetRedirectURI().Return(copyUrl(purls[1])) req.EXPECT().GetState().Return("foostate") @@ -153,23 +181,51 @@ func TestWriteAuthorizeError(t *testing.T) { rw.EXPECT().Header().Return(header) rw.EXPECT().WriteHeader(http.StatusFound) }, - checkHeader: func(k int) { + checkHeader: func(t *testing.T, k int) { a, _ := url.Parse("https://foobar.com/?foo=bar") - a.Fragment = "error=invalid_request&error_description=The+request+is+missing+a+required+parameter%2C+includes+an+invalid+parameter+value%2C+includes+a+parameter+more+than+once%2C+or+is+otherwise+malformed&state=foostate" + a.Fragment = "error=invalid_request&error_description=The+request+is+missing+a+required+parameter%2C+includes+an+invalid+parameter+value%2C+includes+a+parameter+more+than+once%2C+or+is+otherwise+malformed&error_hint=Make+sure+that+the+various+parameters+are+correct%2C+be+aware+of+case+sensitivity+and+trim+your+parameters.+Make+sure+that+the+client+you+are+using+has+exactly+whitelisted+the+redirect_uri+you+specified.&state=foostate" b, _ := url.Parse(header.Get("Location")) - assert.Equal(t, a.String(), b.String(), "%d", k) + assert.Equal(t, a.String(), b.String()) + }, + }, + { + debug: true, + err: ErrInvalidRequest.WithDebug("with-debug"), + mock: func(rw *MockResponseWriter, req *MockAuthorizeRequester) { + req.EXPECT().IsRedirectURIValid().Return(true) + req.EXPECT().GetRedirectURI().Return(copyUrl(purls[1])) + req.EXPECT().GetState().Return("foostate") + req.EXPECT().GetResponseTypes().MaxTimes(2).Return(Arguments([]string{"code", "token"})) + rw.EXPECT().Header().Return(header) + rw.EXPECT().WriteHeader(http.StatusFound) + }, + checkHeader: func(t *testing.T, k int) { + a, _ := url.Parse("https://foobar.com/?foo=bar") + a.Fragment = "error=invalid_request&error_debug=with-debug&error_description=The+request+is+missing+a+required+parameter%2C+includes+an+invalid+parameter+value%2C+includes+a+parameter+more+than+once%2C+or+is+otherwise+malformed&error_hint=Make+sure+that+the+various+parameters+are+correct%2C+be+aware+of+case+sensitivity+and+trim+your+parameters.+Make+sure+that+the+client+you+are+using+has+exactly+whitelisted+the+redirect_uri+you+specified.&state=foostate" + b, _ := url.Parse(header.Get("Location")) + assert.Equal(t, a.String(), b.String()) }, }, } { - c.mock() - oauth2.WriteAuthorizeError(rw, req, c.err) - c.checkHeader(k) - header = http.Header{} - t.Logf("Passed test case %d", k) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + oauth2 := &Fosite{ + SendDebugMessagesToClients: c.debug, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + rw := NewMockResponseWriter(ctrl) + req := NewMockAuthorizeRequester(ctrl) + + c.mock(rw, req) + oauth2.WriteAuthorizeError(rw, req, c.err) + c.checkHeader(t, k) + header = http.Header{} + }) } } func copyUrl(u *url.URL) *url.URL { - url, _ := url.Parse(u.String()) - return url + u2, _ := url.Parse(u.String()) + return u2 } diff --git a/authorize_helper.go b/authorize_helper.go index 256b4763..efab8caa 100644 --- a/authorize_helper.go +++ b/authorize_helper.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -21,7 +35,7 @@ func GetRedirectURIFromRequestValues(values url.Values) (string, error) { // The endpoint URI MAY include an "application/x-www-form-urlencoded" formatted (per Appendix B) query component redirectURI, err := url.QueryUnescape(values.Get("redirect_uri")) if err != nil { - return "", errors.Wrap(ErrInvalidRequest, "redirect_uri parameter malformed or missing") + return "", errors.WithStack(ErrInvalidRequest.WithDebug("redirect_uri parameter malformed or missing")) } return redirectURI, nil } @@ -71,7 +85,7 @@ func MatchRedirectURIWithClientRedirectURIs(rawurl string, client Client) (*url. } } - return nil, errors.Wrap(ErrInvalidRequest, "redirect_uri parameter does not match with registered client redirect urls") + return nil, errors.WithStack(ErrInvalidRequest.WithDebug("redirect_uri parameter does not match with registered client redirect urls")) } // IsValidRedirectURI validates a redirect_uri as specified in: diff --git a/authorize_helper_test.go b/authorize_helper_test.go index 7d48216d..041ff123 100644 --- a/authorize_helper_test.go +++ b/authorize_helper_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -156,7 +170,7 @@ func TestIsRedirectURISecure(t *testing.T) { {u: "wta://auth", err: false}, } { uu, err := url.Parse(c.u) - require.Nil(t, err) + require.NoError(t, err) assert.Equal(t, !c.err, IsRedirectURISecure(uu), "case %d", d) } } diff --git a/authorize_request.go b/authorize_request.go index 3a192cb2..9228f6fb 100644 --- a/authorize_request.go +++ b/authorize_request.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( diff --git a/authorize_request_handler.go b/authorize_request_handler.go index 31ac2564..412b3b4e 100644 --- a/authorize_request_handler.go +++ b/authorize_request_handler.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -6,6 +20,8 @@ import ( "context" + "fmt" + "github.com/pkg/errors" ) @@ -17,7 +33,7 @@ func (c *Fosite) NewAuthorizeRequest(ctx context.Context, r *http.Request) (Auth } if err := r.ParseForm(); err != nil { - return request, errors.Wrap(ErrInvalidRequest, err.Error()) + return request, errors.WithStack(ErrInvalidRequest.WithDebug(err.Error())) } request.Form = r.Form @@ -30,15 +46,15 @@ func (c *Fosite) NewAuthorizeRequest(ctx context.Context, r *http.Request) (Auth // Fetch redirect URI from request rawRedirURI, err := GetRedirectURIFromRequestValues(r.Form) if err != nil { - return request, errors.Wrap(ErrInvalidRequest, err.Error()) + return request, errors.WithStack(ErrInvalidRequest.WithDebug(err.Error())) } // Validate redirect uri redirectURI, err := MatchRedirectURIWithClientRedirectURIs(rawRedirURI, client) if err != nil { - return request, errors.Wrap(ErrInvalidRequest, err.Error()) + return request, errors.WithStack(ErrInvalidRequest.WithDebug(err.Error())) } else if !IsValidRedirectURI(redirectURI) { - return request, errors.Wrap(ErrInvalidRequest, "not a valid redirect uri") + return request, errors.WithStack(ErrInvalidRequest.WithDebug("not a valid redirect uri")) } request.RedirectURI = redirectURI @@ -58,7 +74,7 @@ func (c *Fosite) NewAuthorizeRequest(ctx context.Context, r *http.Request) (Auth state := r.Form.Get("state") if len(state) < MinParameterEntropy { // We're assuming that using less then 8 characters for the state can not be considered "unguessable" - return request, errors.Wrapf(ErrInvalidState, "state length must at least be %d characters long", MinParameterEntropy) + return request, errors.WithStack(ErrInvalidState.WithDebug(fmt.Sprintf("State length must at least be %d characters long", MinParameterEntropy))) } request.State = state diff --git a/authorize_request_handler_test.go b/authorize_request_handler_test.go index 50a2c328..0580120a 100644 --- a/authorize_request_handler_test.go +++ b/authorize_request_handler_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite_test import ( @@ -7,11 +21,14 @@ import ( "context" + "fmt" + "github.com/golang/mock/gomock" . "github.com/ory/fosite" . "github.com/ory/fosite/internal" "github.com/pkg/errors" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // Should pass @@ -157,23 +174,23 @@ func TestNewAuthorizeRequest(t *testing.T) { }, }, } { - t.Logf("Joining test case %d", k) - 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()} + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + 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()} + } } - } - ar, err := c.conf.NewAuthorizeRequest(context.Background(), c.r) - assert.Equal(t, c.expectedError == nil, err == nil, "%d: %s\n%s", k, c.desc, err) - if c.expectedError != nil { - assert.Equal(t, errors.Cause(err), c.expectedError, "%d: %s\n%s", k, c.desc, err) - } else { - AssertObjectKeysEqual(t, c.expect, ar, "ResponseTypes", "Scopes", "Client", "RedirectURI", "State") - assert.NotNil(t, ar.GetRequestedAt()) - } - t.Logf("Passed test case %d", k) + ar, err := c.conf.NewAuthorizeRequest(context.Background(), c.r) + if c.expectedError != nil { + assert.EqualError(t, errors.Cause(err), c.expectedError.Error()) + } else { + require.NoError(t, err) + AssertObjectKeysEqual(t, c.expect, ar, "ResponseTypes", "Scopes", "Client", "RedirectURI", "State") + assert.NotNil(t, ar.GetRequestedAt()) + } + }) } } diff --git a/authorize_request_test.go b/authorize_request_test.go index b6704bcb..c848c0f8 100644 --- a/authorize_request_test.go +++ b/authorize_request_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -77,7 +91,7 @@ func TestAuthorizeRequest(t *testing.T) { ar: &AuthorizeRequest{ Request: Request{ Client: &DefaultClient{RedirectURIs: []string{"https://foobar.com/cb"}}, - RequestedAt: time.Now(), + RequestedAt: time.Now().UTC(), Scopes: []string{"foo", "bar"}, }, RedirectURI: urlparse("https://foobar.com/cb"), diff --git a/authorize_response.go b/authorize_response.go index f72636f2..fc6158ef 100644 --- a/authorize_response.go +++ b/authorize_response.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( diff --git a/authorize_response_test.go b/authorize_response_test.go index 46f30086..1288bee6 100644 --- a/authorize_response_test.go +++ b/authorize_response_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( diff --git a/authorize_response_writer.go b/authorize_response_writer.go index a04c7907..3b0bd3b6 100644 --- a/authorize_response_writer.go +++ b/authorize_response_writer.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( diff --git a/authorize_response_writer_test.go b/authorize_response_writer_test.go index 19e34752..47be6625 100644 --- a/authorize_response_writer_test.go +++ b/authorize_response_writer_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite_test import ( diff --git a/authorize_write.go b/authorize_write.go index 55800099..b192168c 100644 --- a/authorize_write.go +++ b/authorize_write.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( diff --git a/authorize_write_test.go b/authorize_write_test.go index be1e9efb..a9045ef6 100644 --- a/authorize_write_test.go +++ b/authorize_write_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite_test import ( diff --git a/client.go b/client.go index c4f910c1..f31ab975 100644 --- a/client.go +++ b/client.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite // Client represents a client or an app. diff --git a/client_manager.go b/client_manager.go index 9ccd56f2..5d2081ec 100644 --- a/client_manager.go +++ b/client_manager.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import "context" diff --git a/client_test.go b/client_test.go index 671b2924..32cfe7dd 100644 --- a/client_test.go +++ b/client_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( diff --git a/compose/compose.go b/compose/compose.go index dd9de608..49dd2c04 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package compose import ( @@ -42,6 +56,7 @@ func Compose(config *Config, storage interface{}, strategy interface{}, hasher f RevocationHandlers: fosite.RevocationHandlers{}, Hasher: hasher, ScopeStrategy: config.GetScopeStrategy(), + SendDebugMessagesToClients: config.SendDebugMessagesToClients, } for _, factory := range factories { diff --git a/compose/compose_oauth2.go b/compose/compose_oauth2.go index a665d8e0..7a09c021 100644 --- a/compose/compose_oauth2.go +++ b/compose/compose_oauth2.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package compose import ( @@ -15,6 +29,7 @@ func OAuth2AuthorizeExplicitFactory(config *Config, storage interface{}, strateg AuthCodeLifespan: config.GetAuthorizeCodeLifespan(), AccessTokenLifespan: config.GetAccessTokenLifespan(), ScopeStrategy: config.GetScopeStrategy(), + //TokenRevocationStorage: storage.(oauth2.TokenRevocationStorage), } } diff --git a/compose/compose_openid.go b/compose/compose_openid.go index 60955a8d..56bb59e2 100644 --- a/compose/compose_openid.go +++ b/compose/compose_openid.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package compose import ( diff --git a/compose/compose_strategy.go b/compose/compose_strategy.go index 460bb4ac..f4ec088a 100644 --- a/compose/compose_strategy.go +++ b/compose/compose_strategy.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package compose import ( diff --git a/compose/config.go b/compose/config.go index f0bac50b..7ef4389f 100644 --- a/compose/config.go +++ b/compose/config.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package compose import ( @@ -22,6 +36,12 @@ type Config struct { // DisableRefreshTokenValidation sets the introspection endpoint to disable refresh token validation. DisableRefreshTokenValidation bool + // SendDebugMessagesToClients if set to true, includes error debug messages in response payloads. Be aware that sensitive + // data may be exposed, depending on your implementation of Fosite. Such sensitive data might include database error + // codes or other information. Proceed with caution! + SendDebugMessagesToClients bool + + // ScopeStrategy sets the scope strategy that should be supported, for example fosite.WildcardScopeStrategy. ScopeStrategy fosite.ScopeStrategy } diff --git a/context.go b/context.go index 66cb4947..5abeaad7 100644 --- a/context.go +++ b/context.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import "context" diff --git a/equalKeys_test.go b/equalKeys_test.go index 112a93fb..51ef9821 100644 --- a/equalKeys_test.go +++ b/equalKeys_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite_test import ( @@ -26,9 +40,9 @@ func AssertObjectKeysEqual(t *testing.T, a, b interface{}, keys ...string) { assert.True(t, len(keys) > 0, "No keys provided.") for _, k := range keys { c, err := reflections.GetField(a, k) - assert.Nil(t, err) + assert.NoError(t, err) d, err := reflections.GetField(b, k) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, c, d, "%s", k) } } @@ -37,9 +51,9 @@ func AssertObjectKeysNotEqual(t *testing.T, a, b interface{}, keys ...string) { assert.True(t, len(keys) > 0, "No keys provided.") for _, k := range keys { c, err := reflections.GetField(a, k) - assert.Nil(t, err) + assert.NoError(t, err) d, err := reflections.GetField(b, k) - assert.Nil(t, err) + assert.NoError(t, err) assert.NotEqual(t, c, d, "%s", k) } } @@ -48,9 +62,9 @@ func RequireObjectKeysEqual(t *testing.T, a, b interface{}, keys ...string) { assert.True(t, len(keys) > 0, "No keys provided.") for _, k := range keys { c, err := reflections.GetField(a, k) - assert.Nil(t, err) + assert.NoError(t, err) d, err := reflections.GetField(b, k) - assert.Nil(t, err) + assert.NoError(t, err) require.Equal(t, c, d, "%s", k) } } @@ -58,9 +72,9 @@ func RequireObjectKeysNotEqual(t *testing.T, a, b interface{}, keys ...string) { assert.True(t, len(keys) > 0, "No keys provided.") for _, k := range keys { c, err := reflections.GetField(a, k) - assert.Nil(t, err) + assert.NoError(t, err) d, err := reflections.GetField(b, k) - assert.Nil(t, err) + assert.NoError(t, err) require.NotEqual(t, c, d, "%s", k) } } diff --git a/errors.go b/errors.go index 971a5fd0..224d2017 100644 --- a/errors.go +++ b/errors.go @@ -1,269 +1,228 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( + "fmt" "net/http" "github.com/pkg/errors" ) var ( - ErrRequestUnauthorized = errors.New("The request could not be authorized") - ErrRequestForbidden = errors.New("The request is not allowed") - ErrInvalidRequest = errors.New("The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed") - ErrUnauthorizedClient = errors.New("The client is not authorized to request a token using this method") - ErrAccessDenied = errors.New("The resource owner or authorization server denied the request") - ErrUnsupportedResponseType = errors.New("The authorization server does not support obtaining a token using this method") - ErrInvalidScope = errors.New("The requested scope is invalid, unknown, or malformed") - ErrServerError = errors.New("The authorization server encountered an unexpected condition that prevented it from fulfilling the request") - ErrTemporarilyUnavailable = errors.New("The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server") - ErrUnsupportedGrantType = errors.New("The authorization grant type is not supported by the authorization server") - ErrInvalidGrant = errors.New("The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client") - ErrInvalidClient = errors.New("Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)") - ErrInvalidState = errors.Errorf("The state is missing or has less than %d characters and is therefore considered too weak", MinParameterEntropy) - ErrInsufficientEntropy = errors.Errorf("The request used a security parameter (e.g., anti-replay, anti-csrf) with insufficient entropy (minimum of %d characters)", MinParameterEntropy) - ErrMisconfiguration = errors.New("The request failed because of an internal error that is probably caused by misconfiguration") - ErrNotFound = errors.New("Could not find the requested resource(s)") - ErrInvalidTokenFormat = errors.New("Invalid token format") - ErrTokenSignatureMismatch = errors.New("Token signature mismatch") - ErrTokenExpired = errors.New("Token expired") - ErrScopeNotGranted = errors.New("The token was not granted the requested scope") - ErrTokenClaim = errors.New("The token failed validation due to a claim mismatch") - ErrInactiveToken = errors.New("Token is inactive because it is malformed, expired or otherwise invalid") + ErrUnknownRequest = &RFC6749Error{ + Name: errUnknownErrorName, + Description: "The handler is not responsible for this request", + Code: http.StatusBadRequest, + } + ErrRequestForbidden = &RFC6749Error{ + Name: errRequestForbidden, + Description: "The request is not allowed", + Hint: "You are not allowed to perform this action.", + Code: http.StatusForbidden, + } + ErrInvalidRequest = &RFC6749Error{ + Name: errInvalidRequestName, + Description: "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed", + Hint: "Make sure that the various parameters are correct, be aware of case sensitivity and trim your parameters. Make sure that the client you are using has exactly whitelisted the redirect_uri you specified.", + Code: http.StatusBadRequest, + } + ErrUnauthorizedClient = &RFC6749Error{ + Name: errUnauthorizedClientName, + Description: "The client is not authorized to request a token using this method", + Hint: "Make sure that client id and secret are correctly specified and that the client exists.", + Code: http.StatusBadRequest, + } + ErrAccessDenied = &RFC6749Error{ + Name: errAccessDeniedName, + Description: "The resource owner or authorization server denied the request", + Hint: "Make sure that the request you are making is valid. Maybe the credential or request parameters you are using are limited in scope or otherwise restricted.", + Code: http.StatusForbidden, + } + ErrUnsupportedResponseType = &RFC6749Error{ + Name: errUnsupportedResponseTypeName, + Description: "The authorization server does not support obtaining a token using this method", + Code: http.StatusBadRequest, + } + ErrInvalidScope = &RFC6749Error{ + Name: errInvalidScopeName, + Description: "The requested scope is invalid, unknown, or malformed", + Code: http.StatusBadRequest, + } + ErrServerError = &RFC6749Error{ + Name: errServerErrorName, + Description: "The authorization server encountered an unexpected condition that prevented it from fulfilling the request", + Code: http.StatusInternalServerError, + } + ErrTemporarilyUnavailable = &RFC6749Error{ + Name: errTemporarilyUnavailableName, + Description: "The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server", + Code: http.StatusServiceUnavailable, + } + ErrUnsupportedGrantType = &RFC6749Error{ + Name: errUnsupportedGrantTypeName, + Description: "The authorization grant type is not supported by the authorization server", + Code: http.StatusBadRequest, + } + ErrInvalidGrant = &RFC6749Error{ + Name: errInvalidGrantName, + Description: "The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client", + Code: http.StatusBadRequest, + } + ErrInvalidClient = &RFC6749Error{ + Name: errInvalidClientName, + Description: "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)", + Code: http.StatusUnauthorized, + } + ErrInvalidState = &RFC6749Error{ + Name: errInvalidStateName, + Description: fmt.Sprintf("The state is missing or has less than %d characters and is therefore considered too weak", MinParameterEntropy), + Code: http.StatusBadRequest, + } + ErrMisconfiguration = &RFC6749Error{ + Name: errMisconfigurationName, + Description: "The request failed because of an internal error that is probably caused by misconfiguration", + Code: http.StatusInternalServerError, + } + ErrInsufficientEntropy = &RFC6749Error{ + Name: errInsufficientEntropyName, + Description: fmt.Sprintf("The request used a security parameter (e.g., anti-replay, anti-csrf) with insufficient entropy (minimum of %d characters)", MinParameterEntropy), + Code: http.StatusBadRequest, + } + ErrNotFound = &RFC6749Error{ + Name: errNotFoundName, + Description: "Could not find the requested resource(s)", + Code: http.StatusNotFound, + } + ErrRequestUnauthorized = &RFC6749Error{ + Name: errRequestUnauthorizedName, + Description: "The request could not be authorized", + Hint: "Check that you provided valid credentials in the right format.", + Code: http.StatusUnauthorized, + } + ErrTokenSignatureMismatch = &RFC6749Error{ + Name: errTokenSignatureMismatchName, + Description: "Token signature mismatch", + Hint: "Check that you provided a valid token in the right format.", + Code: http.StatusBadRequest, + } + ErrInvalidTokenFormat = &RFC6749Error{ + Name: errInvalidTokenFormatName, + Description: "Invalid token format", + Hint: "Check that you provided a valid token in the right format.", + Code: http.StatusBadRequest, + } + ErrTokenExpired = &RFC6749Error{ + Name: errTokenExpiredName, + Description: "Token expired", + Hint: "The token expired.", + Code: http.StatusUnauthorized, + } + ErrScopeNotGranted = &RFC6749Error{ + Name: errScopeNotGrantedName, + Description: "The token was not granted the requested scope", + Hint: "The resource owner did not grant the requested scope.", + Code: http.StatusForbidden, + } + ErrTokenClaim = &RFC6749Error{ + Name: errTokenClaimName, + Description: "The token failed validation due to a claim mismatch", + Hint: "One or more token claims failed validation.", + Code: http.StatusUnauthorized, + } + ErrInactiveToken = &RFC6749Error{ + Name: errTokenInactiveName, + Description: "Token is inactive because it is malformed, expired or otherwise invalid", + Hint: "Token validation failed.", + Code: http.StatusUnauthorized, + } + ErrRevokationClientMismatch = &RFC6749Error{ + Name: errRevokationClientMismatchName, + Description: "Token was not issued to the client making the revokation request", + Code: http.StatusBadRequest, + } + ErrLoginRequired = &RFC6749Error{ + Name: errLoginRequired, + Description: "The Authorization Server requires End-User authentication", + Code: http.StatusBadRequest, + } + ErrInteractionRequired = &RFC6749Error{ + Description: "The Authorization Server requires End-User interaction of some form to proceed", + Name: errInteractionRequired, + Code: http.StatusBadRequest, + } + ErrConsentRequired = &RFC6749Error{ + Description: "The Authorization Server requires End-User consent", + Name: errConsentRequired, + Code: http.StatusBadRequest, + } ) const ( - errRequestUnauthorized = "request_unauthorized" - errRequestForbidden = "request_forbidden" - errInvalidRequestName = "invalid_request" - errUnauthorizedClientName = "unauthorized_client" - errAccessDeniedName = "access_denied" - errUnsupportedResponseTypeName = "unsupported_response_type" - errInvalidScopeName = "invalid_scope" - errServerErrorName = "server_error" - errTemporarilyUnavailableName = "temporarily_unavailable" - errUnsupportedGrantTypeName = "unsupported_grant_type" - errInvalidGrantName = "invalid_grant" - errInvalidClientName = "invalid_client" - UnknownErrorName = "unknown_error" - errNotFound = "not_found" - errInvalidState = "invalid_state" - errMisconfiguration = "misconfiguration" - errInsufficientEntropy = "insufficient_entropy" - errInvalidTokenFormat = "invalid_token" - errTokenSignatureMismatch = "token_signature_mismatch" - errTokenExpired = "token_expired" - errScopeNotGranted = "scope_not_granted" - errTokenClaim = "token_claim" - errTokenInactive = "token_inactive" + errConsentRequired = "consent_required" + errInteractionRequired = "interaction_required" + errLoginRequired = "login_required" + errRequestUnauthorizedName = "request_unauthorized" + errRequestForbidden = "request_forbidden" + errInvalidRequestName = "invalid_request" + errUnauthorizedClientName = "unauthorized_client" + errAccessDeniedName = "access_denied" + errUnsupportedResponseTypeName = "unsupported_response_type" + errInvalidScopeName = "invalid_scope" + errServerErrorName = "server_error" + errTemporarilyUnavailableName = "temporarily_unavailable" + errUnsupportedGrantTypeName = "unsupported_grant_type" + errInvalidGrantName = "invalid_grant" + errInvalidClientName = "invalid_client" + errNotFoundName = "not_found" + errInvalidStateName = "invalid_state" + errMisconfigurationName = "misconfiguration" + errInsufficientEntropyName = "insufficient_entropy" + errInvalidTokenFormatName = "invalid_token" + errTokenSignatureMismatchName = "token_signature_mismatch" + errTokenExpiredName = "token_expired" + errScopeNotGrantedName = "scope_not_granted" + errTokenClaimName = "token_claim" + errTokenInactiveName = "token_inactive" + errAuthorizaionCodeInactiveName = "authorization_code_inactive" + errUnknownErrorName = "error" + errRevokationClientMismatchName = "revokation_client_mismatch" ) -type RFC6749Error struct { - Name string `json:"error"` - Description string `json:"error_description"` - Hint string `json:"-"` - Code int `json:"statusCode"` - Debug string `json:"-"` -} - func ErrorToRFC6749Error(err error) *RFC6749Error { if e, ok := err.(*RFC6749Error); ok { return e + } else if e, ok := errors.Cause(err).(*RFC6749Error); ok { + return e } - - switch errors.Cause(err) { - case ErrInactiveToken: - { - { - return &RFC6749Error{ - Name: errTokenInactive, - Description: ErrInactiveToken.Error(), - Debug: err.Error(), - Hint: "Token validation failed.", - Code: http.StatusUnauthorized, - } - } - } - case ErrTokenClaim: - { - return &RFC6749Error{ - Name: errTokenClaim, - Description: ErrTokenClaim.Error(), - Debug: err.Error(), - Hint: "One or more token claims failed validation.", - Code: http.StatusUnauthorized, - } - } - case ErrScopeNotGranted: - { - return &RFC6749Error{ - Name: errScopeNotGranted, - Description: ErrScopeNotGranted.Error(), - Debug: err.Error(), - Hint: "The resource owner did not grant the requested scope.", - Code: http.StatusForbidden, - } - } - case ErrTokenExpired: - { - return &RFC6749Error{ - Name: errTokenExpired, - Description: ErrTokenExpired.Error(), - Debug: err.Error(), - Hint: "The token expired.", - Code: http.StatusUnauthorized, - } - } - case ErrInvalidTokenFormat: - { - return &RFC6749Error{ - Name: errInvalidTokenFormat, - Description: ErrInvalidTokenFormat.Error(), - Debug: err.Error(), - Hint: "Check that you provided a valid token in the right format.", - Code: http.StatusBadRequest, - } - } - case ErrTokenSignatureMismatch: - { - return &RFC6749Error{ - Name: errTokenSignatureMismatch, - Description: ErrTokenSignatureMismatch.Error(), - Debug: err.Error(), - Hint: "Check that you provided a valid token in the right format.", - Code: http.StatusBadRequest, - } - } - case ErrRequestUnauthorized: - { - return &RFC6749Error{ - Name: errRequestUnauthorized, - Description: ErrRequestUnauthorized.Error(), - Debug: err.Error(), - Hint: "Check that you provided valid credentials in the right format.", - Code: http.StatusUnauthorized, - } - } - case ErrRequestForbidden: - { - return &RFC6749Error{ - Name: errRequestForbidden, - Description: ErrRequestForbidden.Error(), - Debug: err.Error(), - Hint: "You are not allowed to perform this action.", - Code: http.StatusForbidden, - } - } - case ErrInvalidRequest: - return &RFC6749Error{ - Name: errInvalidRequestName, - Description: ErrInvalidRequest.Error(), - Debug: err.Error(), - Hint: "Make sure that the various parameters are correct, be aware of case sensitivity and trim your parameters. Make sure that the client you are using has exactly whitelisted the redirect_uri you specified.", - Code: http.StatusBadRequest, - } - case ErrUnauthorizedClient: - return &RFC6749Error{ - Name: errUnauthorizedClientName, - Description: ErrUnauthorizedClient.Error(), - Debug: err.Error(), - Hint: "Make sure that client id and secret are correctly specified and that the client exists.", - Code: http.StatusUnauthorized, - } - case ErrAccessDenied: - return &RFC6749Error{ - Name: errAccessDeniedName, - Description: ErrAccessDenied.Error(), - Debug: err.Error(), - Hint: "Make sure that the request you are making is valid. Maybe the credential or request parameters you are using are limited in scope or otherwise restricted.", - Code: http.StatusForbidden, - } - case ErrUnsupportedResponseType: - return &RFC6749Error{ - Name: errUnsupportedResponseTypeName, - Description: ErrUnsupportedResponseType.Error(), - Debug: err.Error(), - Code: http.StatusBadRequest, - } - case ErrInvalidScope: - return &RFC6749Error{ - Name: errInvalidScopeName, - Description: ErrInvalidScope.Error(), - Debug: err.Error(), - Code: http.StatusBadRequest, - } - case ErrServerError: - return &RFC6749Error{ - Name: errServerErrorName, - Description: ErrServerError.Error(), - Debug: err.Error(), - Code: http.StatusInternalServerError, - } - case ErrTemporarilyUnavailable: - return &RFC6749Error{ - Name: errTemporarilyUnavailableName, - Description: ErrTemporarilyUnavailable.Error(), - Debug: err.Error(), - Code: http.StatusServiceUnavailable, - } - case ErrUnsupportedGrantType: - return &RFC6749Error{ - Name: errUnsupportedGrantTypeName, - Description: ErrUnsupportedGrantType.Error(), - Debug: err.Error(), - Code: http.StatusBadRequest, - } - case ErrInvalidGrant: - return &RFC6749Error{ - Name: errInvalidGrantName, - Description: ErrInvalidGrant.Error(), - Debug: err.Error(), - Code: http.StatusBadRequest, - } - case ErrInvalidClient: - return &RFC6749Error{ - Name: errInvalidClientName, - Description: ErrInvalidClient.Error(), - Debug: err.Error(), - Code: http.StatusUnauthorized, - } - case ErrInvalidState: - return &RFC6749Error{ - Name: errInvalidState, - Description: ErrInvalidState.Error(), - Debug: err.Error(), - Code: http.StatusBadRequest, - } - case ErrInsufficientEntropy: - return &RFC6749Error{ - Name: errInsufficientEntropy, - Description: ErrInsufficientEntropy.Error(), - Debug: err.Error(), - Code: http.StatusBadRequest, - } - case ErrMisconfiguration: - return &RFC6749Error{ - Name: errMisconfiguration, - Description: ErrMisconfiguration.Error(), - Debug: err.Error(), - Code: http.StatusInternalServerError, - } - case ErrNotFound: - return &RFC6749Error{ - Name: errNotFound, - Description: ErrNotFound.Error(), - Debug: err.Error(), - Code: http.StatusNotFound, - } - default: - return &RFC6749Error{ - Name: UnknownErrorName, - Description: "The error is unrecognizable.", - Debug: err.Error(), - Code: http.StatusInternalServerError, - } + return &RFC6749Error{ + Name: errUnknownErrorName, + Description: "The error is unrecognizable.", + Debug: err.Error(), + Code: http.StatusInternalServerError, } } +type RFC6749Error struct { + Name string `json:"error"` + Description string `json:"error_description"` + Hint string `json:"error_hint,omitempty"` + Code int `json:"status_code,omitempty"` + Debug string `json:"error_debug,omitempty"` +} + func (e *RFC6749Error) Status() string { return http.StatusText(e.Code) } @@ -287,3 +246,9 @@ func (e *RFC6749Error) Details() []map[string]interface{} { func (e *RFC6749Error) StatusCode() int { return e.Code } + +func (e *RFC6749Error) WithDebug(debug string) *RFC6749Error { + err := *e + err.Debug = debug + return &err +} diff --git a/errors_test.go b/errors_test.go index 62e9129f..29ccfb1c 100644 --- a/errors_test.go +++ b/errors_test.go @@ -1,26 +1,14 @@ package fosite import ( - native "errors" "testing" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) -func TestErrorToRFC6749(t *testing.T) { - assert.Equal(t, UnknownErrorName, ErrorToRFC6749Error(errors.New("")).Name) - assert.Equal(t, UnknownErrorName, ErrorToRFC6749Error(native.New("")).Name) - - assert.Equal(t, errInvalidRequestName, ErrorToRFC6749Error(errors.WithStack(ErrInvalidRequest)).Name) - assert.Equal(t, errUnauthorizedClientName, ErrorToRFC6749Error(errors.WithStack(ErrUnauthorizedClient)).Name) - assert.Equal(t, errAccessDeniedName, ErrorToRFC6749Error(errors.WithStack(ErrAccessDenied)).Name) - assert.Equal(t, errUnsupportedResponseTypeName, ErrorToRFC6749Error(errors.WithStack(ErrUnsupportedResponseType)).Name) - assert.Equal(t, errInvalidScopeName, ErrorToRFC6749Error(errors.WithStack(ErrInvalidScope)).Name) - assert.Equal(t, errServerErrorName, ErrorToRFC6749Error(errors.WithStack(ErrServerError)).Name) - assert.Equal(t, errTemporarilyUnavailableName, ErrorToRFC6749Error(errors.WithStack(ErrTemporarilyUnavailable)).Name) - assert.Equal(t, errUnsupportedGrantTypeName, ErrorToRFC6749Error(errors.WithStack(ErrUnsupportedGrantType)).Name) - assert.Equal(t, errInvalidGrantName, ErrorToRFC6749Error(errors.WithStack(ErrInvalidGrant)).Name) - assert.Equal(t, errInvalidClientName, ErrorToRFC6749Error(errors.WithStack(ErrInvalidClient)).Name) - assert.Equal(t, errInvalidState, ErrorToRFC6749Error(errors.WithStack(ErrInvalidState)).Name) +func TestAddDebug(t *testing.T) { + err := ErrRevokationClientMismatch.WithDebug("debug") + assert.NotEqual(t, err, ErrRevokationClientMismatch) + assert.Empty(t, ErrRevokationClientMismatch.Debug) + assert.NotEmpty(t, err.Debug) } diff --git a/fosite.go b/fosite.go index f01f8e5f..bb5c3c6f 100644 --- a/fosite.go +++ b/fosite.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -69,4 +83,9 @@ type Fosite struct { RevocationHandlers RevocationHandlers Hasher Hasher ScopeStrategy ScopeStrategy + + // SendDebugMessagesToClients if set to true, includes error debug messages in response payloads. Be aware that sensitive + // data may be exposed, depending on your implementation of Fosite. Such sensitive data might include database error + // codes or other information. Proceed with caution! + SendDebugMessagesToClients bool } diff --git a/fosite_test.go b/fosite_test.go index 09d0a3e6..315bfa45 100644 --- a/fosite_test.go +++ b/fosite_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite_test import ( diff --git a/generate-mocks.sh b/generate-mocks.sh index a309df6f..8720b39e 100755 --- a/generate-mocks.sh +++ b/generate-mocks.sh @@ -8,9 +8,7 @@ mockgen -package internal -destination internal/authorize_code_storage.go github mockgen -package internal -destination internal/access_token_storage.go github.com/ory/fosite/handler/oauth2 AccessTokenStorage mockgen -package internal -destination internal/refresh_token_strategy.go github.com/ory/fosite/handler/oauth2 RefreshTokenStorage mockgen -package internal -destination internal/oauth2_client_storage.go github.com/ory/fosite/handler/oauth2 ClientCredentialsGrantStorage -mockgen -package internal -destination internal/oauth2_explicit_storage.go github.com/ory/fosite/handler/oauth2 AuthorizeCodeGrantStorage mockgen -package internal -destination internal/oauth2_owner_storage.go github.com/ory/fosite/handler/oauth2 ResourceOwnerPasswordCredentialsGrantStorage -mockgen -package internal -destination internal/oauth2_refresh_storage.go github.com/ory/fosite/handler/oauth2 RefreshTokenGrantStorage mockgen -package internal -destination internal/oauth2_revoke_storage.go github.com/ory/fosite/handler/oauth2 TokenRevocationStorage mockgen -package internal -destination internal/openid_id_token_storage.go github.com/ory/fosite/handler/openid OpenIDConnectRequestStorage mockgen -package internal -destination internal/access_token_strategy.go github.com/ory/fosite/handler/oauth2 AccessTokenStrategy diff --git a/glide.lock b/glide.lock deleted file mode 100644 index 7a8864aa..00000000 --- a/glide.lock +++ /dev/null @@ -1,78 +0,0 @@ -hash: d92ad78a0e95699af758182303644f7316bb846aa8efbd29301f27cf690b59f3 -updated: 2017-07-08T21:49:06.7880537+02:00 -imports: -- name: github.com/asaskevich/govalidator - version: 4918b99a7cb949bb295f3c7bbaf24b577d806e35 -- name: github.com/dgrijalva/jwt-go - version: d2709f9f1f31ebcda9651b03077758c1f3a0018c -- name: github.com/golang/mock - version: 93f6609a15b7de76bd49259f1f9a6b58df358936 - subpackages: - - gomock -- name: github.com/mohae/deepcopy - version: 491d3605edfb866af34a48075bd4355ac1bf46ca -- name: github.com/oleiade/reflections - version: 2b6ec3da648e3e834dc41bad8d9ed7f2dc6a9496 -- name: github.com/pborman/uuid - version: a97ce2ca70fa5a848076093f05e639a89ca34d06 -- name: github.com/pkg/errors - version: 645ef00459ed84a119197bfb8d8205042c6df63d -- name: github.com/rakyll/hey - version: 886125b9f23d5bdb23dbb1000f2bff4b757ccfb1 - subpackages: - - requester -- name: golang.org/x/crypto - version: 453249f01cfeb54c3d549ddb75ff152ca243f9d8 - subpackages: - - bcrypt - - blowfish -- name: golang.org/x/net - version: dd2d9a67c97da0afa00d5726e28086007a0acce5 - subpackages: - - context - - http2 - - http2/hpack - - idna - - lex/httplex - - publicsuffix -testImports: -- name: github.com/davecgh/go-spew - version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 - subpackages: - - spew -- name: github.com/golang/protobuf - version: 69b215d01a5606c843240eab4937eab3acee6530 - subpackages: - - proto -- name: github.com/gorilla/context - version: 1ea25387ff6f684839d82767c1733ff4d4d15d0a -- name: github.com/gorilla/mux - version: bcd8bc72b08df0f70df986b97f95590779502d31 -- name: github.com/moul/http2curl - version: 4e24498b31dba4683efb9d35c1c8a91e2eda28c8 -- name: github.com/parnurzeal/gorequest - version: a578a48e8d6ca8b01a3b18314c43c6716bb5f5a3 -- name: github.com/pmezard/go-difflib - version: d8ed2627bdf02c080bf22230dbb337003b7aba2d - subpackages: - - difflib -- name: github.com/stretchr/testify - version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0 - subpackages: - - assert - - require -- name: golang.org/x/oauth2 - version: b9780ec78894ab900c062d58ee3076cd9b2a4501 - subpackages: - - clientcredentials - - internal -- name: google.golang.org/appengine - version: 3a452f9e00122ead39586d68ffdb9c6e1326af3c - subpackages: - - internal - - internal/base - - internal/datastore - - internal/log - - internal/remote_api - - internal/urlfetch - - urlfetch diff --git a/glide.yaml b/glide.yaml deleted file mode 100644 index 4b92a8d3..00000000 --- a/glide.yaml +++ /dev/null @@ -1,31 +0,0 @@ -package: github.com/ory/fosite -import: -- package: github.com/asaskevich/govalidator - version: ~6.0.0 -- package: github.com/dgrijalva/jwt-go - version: ~3.0.0 -- package: github.com/golang/mock - subpackages: - - gomock -- package: github.com/oleiade/reflections - version: ~1.0.0 -- package: github.com/pborman/uuid - version: ~1.0.0 -- package: github.com/pkg/errors - version: ~0.8.0 -- package: golang.org/x/crypto - subpackages: - - bcrypt -testImport: -- package: github.com/gorilla/mux - version: ~1.4.0 -- package: github.com/parnurzeal/gorequest - version: ~0.2.15 -- package: golang.org/x/oauth2 - subpackages: - - clientcredentials -- package: github.com/stretchr/testify - version: ~1.1.4 - subpackages: - - assert - - require diff --git a/handler.go b/handler.go index c504befe..4e224f36 100644 --- a/handler.go +++ b/handler.go @@ -1,13 +1,23 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( "context" - - "github.com/pkg/errors" ) -var ErrUnknownRequest = errors.New("The handler is not responsible for this request.") - type AuthorizeEndpointHandler interface { // HandleAuthorizeRequest handles an authorize endpoint request. To extend the handler's capabilities, the http request // is passed along, if further information retrieval is required. If the handler feels that he is not responsible for @@ -48,5 +58,5 @@ type TokenEndpointHandler interface { // token as well. type RevocationHandler interface { // RevokeToken handles access and refresh token revocation. - RevokeToken(ctx context.Context, token string, tokenType TokenType) error + RevokeToken(ctx context.Context, token string, tokenType TokenType, client Client) error } diff --git a/handler/oauth2/flow_authorize_code_auth.go b/handler/oauth2/flow_authorize_code_auth.go index 32c50b00..1be33bbe 100644 --- a/handler/oauth2/flow_authorize_code_auth.go +++ b/handler/oauth2/flow_authorize_code_auth.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -19,6 +33,7 @@ type AuthorizeExplicitGrantHandler struct { RefreshTokenStrategy RefreshTokenStrategy AuthorizeCodeStrategy AuthorizeCodeStrategy CoreStorage CoreStorage + //TokenRevocationStorage TokenRevocationStorage // AuthCodeLifespan defines the lifetime of an authorize code. AuthCodeLifespan time.Duration @@ -40,13 +55,13 @@ func (c *AuthorizeExplicitGrantHandler) HandleAuthorizeEndpointRequest(ctx conte } if !fosite.IsRedirectURISecure(ar.GetRedirectURI()) { - return errors.Wrap(fosite.ErrInvalidRequest, "Redirect URL is using an insecure protocol, http is only allowed for hosts with suffix `localhost`, for example: http://myapp.localhost/") + return errors.WithStack(fosite.ErrInvalidRequest.WithDebug("Redirect URL is using an insecure protocol, http is only allowed for hosts with suffix `localhost`, for example: http://myapp.localhost/")) } client := ar.GetClient() for _, scope := range ar.GetRequestedScopes() { if !c.ScopeStrategy(client.GetScopes(), scope) { - return errors.Wrap(fosite.ErrInvalidScope, fmt.Sprintf("The client is not allowed to request scope %s", scope)) + return errors.WithStack(fosite.ErrInvalidScope.WithDebug(fmt.Sprintf("The client is not allowed to request scope %s", scope))) } } @@ -56,12 +71,12 @@ func (c *AuthorizeExplicitGrantHandler) HandleAuthorizeEndpointRequest(ctx conte func (c *AuthorizeExplicitGrantHandler) IssueAuthorizeCode(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { code, signature, err := c.AuthorizeCodeStrategy.GenerateAuthorizeCode(ctx, ar) if err != nil { - return errors.Wrap(fosite.ErrServerError, err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } - ar.GetSession().SetExpiresAt(fosite.AuthorizeCode, time.Now().Add(c.AuthCodeLifespan)) + ar.GetSession().SetExpiresAt(fosite.AuthorizeCode, time.Now().UTC().Add(c.AuthCodeLifespan)) if err := c.CoreStorage.CreateAuthorizeCodeSession(ctx, signature, ar); err != nil { - return errors.Wrap(fosite.ErrServerError, err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } resp.AddQuery("code", code) diff --git a/handler/oauth2/flow_authorize_code_auth_test.go b/handler/oauth2/flow_authorize_code_auth_test.go index f321a060..1cf95bae 100644 --- a/handler/oauth2/flow_authorize_code_auth_test.go +++ b/handler/oauth2/flow_authorize_code_auth_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -74,9 +88,9 @@ func TestAuthorizeCode_HandleAuthorizeEndpointRequest(t *testing.T) { }, GrantedScopes: fosite.Arguments{"a", "b"}, Session: &fosite.DefaultSession{ - ExpiresAt: map[fosite.TokenType]time.Time{fosite.AccessToken: time.Now().Add(time.Hour)}, + ExpiresAt: map[fosite.TokenType]time.Time{fosite.AccessToken: time.Now().UTC().Add(time.Hour)}, }, - RequestedAt: time.Now(), + RequestedAt: time.Now().UTC(), }, State: "superstate", RedirectURI: parseUrl("https://asdf.de/cb"), diff --git a/handler/oauth2/flow_authorize_code_token.go b/handler/oauth2/flow_authorize_code_token.go index 274cb693..d8c59426 100644 --- a/handler/oauth2/flow_authorize_code_token.go +++ b/handler/oauth2/flow_authorize_code_token.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -18,22 +32,34 @@ func (c *AuthorizeExplicitGrantHandler) HandleTokenEndpointRequest(ctx context.C } if !request.GetClient().GetGrantTypes().Has("authorization_code") { - return errors.Wrap(errors.WithStack(fosite.ErrInvalidGrant), "The client is not allowed to use grant type authorization_code") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use grant type authorization_code")) } code := request.GetRequestForm().Get("code") signature := c.AuthorizeCodeStrategy.AuthorizeCodeSignature(code) authorizeRequest, err := c.CoreStorage.GetAuthorizeCodeSession(ctx, signature, request.GetSession()) - if errors.Cause(err) == fosite.ErrNotFound { - return errors.Wrap(fosite.ErrInvalidRequest, err.Error()) + if err != nil && errors.Cause(err).Error() == fosite.ErrNotFound.Error() { + // If an authorize code is used twice (which is likely the case here), we should try and invalidate any previously + // issued access and refresh tokens. + // reqID := authorizeRequest.GetID() + // + var debug string + // if revErr := c.TokenRevocationStorage.RevokeAccessToken(ctx, reqID); revErr != nil { + // debug += revErr.Error() + "\n" + // } + // if revErr := c.TokenRevocationStorage.RevokeRefreshToken(ctx, reqID); revErr != nil { + // debug += revErr.Error() + "\n" + // } + + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug(debug)) } else if err != nil { - return errors.Wrap(errors.WithStack(fosite.ErrServerError), err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } // The authorization server MUST verify that the authorization code is valid // This needs to happen after store retrieval for the session to be hydrated properly if err := c.AuthorizeCodeStrategy.ValidateAuthorizeCode(ctx, request, code); err != nil { - return errors.Wrap(errors.WithStack(fosite.ErrInvalidRequest), err.Error()) + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug(err.Error())) } // Override scopes @@ -43,7 +69,7 @@ func (c *AuthorizeExplicitGrantHandler) HandleTokenEndpointRequest(ctx context.C // confidential client, or if the client is public, ensure that the // code was issued to "client_id" in the request, if authorizeRequest.GetClient().GetID() != request.GetClient().GetID() { - return errors.Wrap(errors.WithStack(fosite.ErrInvalidRequest), "Client ID mismatch") + return errors.WithStack(fosite.ErrInvalidRequest.WithDebug("Client ID mismatch")) } // ensure that the "redirect_uri" parameter is present if the @@ -52,7 +78,7 @@ func (c *AuthorizeExplicitGrantHandler) HandleTokenEndpointRequest(ctx context.C // their values are identical. forcedRedirectURI := authorizeRequest.GetRequestForm().Get("redirect_uri") if forcedRedirectURI != "" && forcedRedirectURI != request.GetRequestForm().Get("redirect_uri") { - return errors.Wrap(errors.WithStack(fosite.ErrInvalidRequest), "Redirect URI mismatch") + return errors.WithStack(fosite.ErrInvalidRequest.WithDebug("Redirect URI mismatch")) } // Checking of POST client_id skipped, because: @@ -61,7 +87,8 @@ func (c *AuthorizeExplicitGrantHandler) HandleTokenEndpointRequest(ctx context.C // client MUST authenticate with the authorization server as described // in Section 3.2.1. request.SetSession(authorizeRequest.GetSession()) - request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().Add(c.AccessTokenLifespan)) + request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.AccessTokenLifespan)) + request.SetID(authorizeRequest.GetID()) return nil } @@ -76,10 +103,10 @@ func (c *AuthorizeExplicitGrantHandler) PopulateTokenEndpointResponse(ctx contex signature := c.AuthorizeCodeStrategy.AuthorizeCodeSignature(code) authorizeRequest, err := c.CoreStorage.GetAuthorizeCodeSession(ctx, signature, requester.GetSession()) if err != nil { - return errors.Wrap(errors.WithStack(fosite.ErrServerError), err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } else if err := c.AuthorizeCodeStrategy.ValidateAuthorizeCode(ctx, requester, code); err != nil { // This needs to happen after store retrieval for the session to be hydrated properly - return errors.Wrap(errors.WithStack(fosite.ErrInvalidRequest), err.Error()) + return errors.WithStack(fosite.ErrInvalidRequest.WithDebug(err.Error())) } for _, scope := range authorizeRequest.GetGrantedScopes() { @@ -88,30 +115,30 @@ func (c *AuthorizeExplicitGrantHandler) PopulateTokenEndpointResponse(ctx contex access, accessSignature, err := c.AccessTokenStrategy.GenerateAccessToken(ctx, requester) if err != nil { - return errors.Wrap(errors.WithStack(fosite.ErrServerError), err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } var refresh, refreshSignature string - if authorizeRequest.GetGrantedScopes().Has("offline") { + if authorizeRequest.GetGrantedScopes().HasOneOf("offline", "offline_access") { refresh, refreshSignature, err = c.RefreshTokenStrategy.GenerateRefreshToken(ctx, requester) if err != nil { - return errors.Wrap(errors.WithStack(fosite.ErrServerError), err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } } if err := c.CoreStorage.DeleteAuthorizeCodeSession(ctx, signature); err != nil { - return errors.Wrap(errors.WithStack(fosite.ErrServerError), err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } else if err := c.CoreStorage.CreateAccessTokenSession(ctx, accessSignature, requester); err != nil { - return errors.Wrap(errors.WithStack(fosite.ErrServerError), err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } else if refreshSignature != "" { if err := c.CoreStorage.CreateRefreshTokenSession(ctx, refreshSignature, requester); err != nil { - return errors.Wrap(errors.WithStack(fosite.ErrServerError), err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } } responder.SetAccessToken(access) responder.SetTokenType("bearer") - responder.SetExpiresIn(getExpiresIn(requester, fosite.AccessToken, c.AccessTokenLifespan, time.Now())) + responder.SetExpiresIn(getExpiresIn(requester, fosite.AccessToken, c.AccessTokenLifespan, time.Now().UTC())) responder.SetScopes(requester.GetGrantedScopes()) if refresh != "" { responder.SetExtra("refresh_token", refresh) diff --git a/handler/oauth2/flow_authorize_code_token_test.go b/handler/oauth2/flow_authorize_code_token_test.go index b8e70c53..4a223027 100644 --- a/handler/oauth2/flow_authorize_code_token_test.go +++ b/handler/oauth2/flow_authorize_code_token_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -10,6 +24,10 @@ import ( //"github.com/ory/fosite/internal" "time" + "context" + + "fmt" + "github.com/ory/fosite/storage" "github.com/pkg/errors" "github.com/stretchr/testify/assert" @@ -30,6 +48,7 @@ func TestAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { RefreshTokenStrategy: strategy, ScopeStrategy: fosite.HierarchicScopeStrategy, AccessTokenLifespan: time.Minute, + //TokenRevocationStorage: store, } for _, c := range []struct { areq *fosite.AccessRequest @@ -54,7 +73,7 @@ func TestAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { GrantTypes: fosite.Arguments{"authorization_code"}, }, Session: &fosite.DefaultSession{}, - RequestedAt: time.Now(), + RequestedAt: time.Now().UTC(), }, }, description: "should fail because authcode not found", @@ -74,7 +93,7 @@ func TestAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { GrantTypes: fosite.Arguments{"authorization_code"}, }, Session: &fosite.DefaultSession{}, - RequestedAt: time.Now(), + RequestedAt: time.Now().UTC(), }, }, description: "should fail because validation failed", @@ -93,7 +112,7 @@ func TestAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { }, GrantedScopes: fosite.Arguments{"foo", "offline"}, Session: &fosite.DefaultSession{}, - RequestedAt: time.Now(), + RequestedAt: time.Now().UTC(), }, }, setup: func(t *testing.T, areq *fosite.AccessRequest) { @@ -148,8 +167,10 @@ func TestAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { CoreStorage: store, AuthorizeCodeStrategy: hmacshaStrategy, ScopeStrategy: fosite.HierarchicScopeStrategy, + //TokenRevocationStorage: store, + AuthCodeLifespan: time.Minute, } - for _, c := range []struct { + for i, c := range []struct { areq *fosite.AccessRequest authreq *fosite.AuthorizeRequest description string @@ -167,19 +188,21 @@ func TestAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { areq: &fosite.AccessRequest{ GrantTypes: fosite.Arguments{"authorization_code"}, Request: fosite.Request{ - Client: &fosite.DefaultClient{ID: "foo"}, - Session: &fosite.DefaultSession{}, + Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{""}}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), }, }, description: "should fail because client is not granted this grant type", - expectErr: fosite.ErrInvalidRequest, + expectErr: fosite.ErrInvalidGrant, }, { areq: &fosite.AccessRequest{ GrantTypes: fosite.Arguments{"authorization_code"}, Request: fosite.Request{ - Client: &fosite.DefaultClient{GrantTypes: []string{"authorization_code"}}, - Session: &fosite.DefaultSession{}, + Client: &fosite.DefaultClient{GrantTypes: []string{"authorization_code"}}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), }, }, description: "should fail because authcode could not be retrieved (1)", @@ -188,26 +211,28 @@ func TestAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { require.NoError(t, err) areq.Form = url.Values{"code": {token}} }, - expectErr: fosite.ErrInvalidRequest, + expectErr: fosite.ErrInvalidGrant, }, { areq: &fosite.AccessRequest{ GrantTypes: fosite.Arguments{"authorization_code"}, Request: fosite.Request{ - Form: url.Values{"code": {"foo.bar"}}, - Client: &fosite.DefaultClient{GrantTypes: []string{"authorization_code"}}, - Session: &fosite.DefaultSession{}, + Form: url.Values{"code": {"foo.bar"}}, + Client: &fosite.DefaultClient{GrantTypes: []string{"authorization_code"}}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), }, }, description: "should fail because authcode validation failed", - expectErr: fosite.ErrInvalidRequest, + expectErr: fosite.ErrInvalidGrant, }, { areq: &fosite.AccessRequest{ GrantTypes: fosite.Arguments{"authorization_code"}, Request: fosite.Request{ - Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{"authorization_code"}}, - Session: &fosite.DefaultSession{}, + Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{"authorization_code"}}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), }, }, authreq: &fosite.AuthorizeRequest{ @@ -230,8 +255,9 @@ func TestAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { areq: &fosite.AccessRequest{ GrantTypes: fosite.Arguments{"authorization_code"}, Request: fosite.Request{ - Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{"authorization_code"}}, - Session: &fosite.DefaultSession{}, + Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{"authorization_code"}}, + Session: &fosite.DefaultSession{}, + RequestedAt: time.Now().UTC(), }, }, authreq: &fosite.AuthorizeRequest{ @@ -258,7 +284,7 @@ func TestAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{"authorization_code"}}, Form: url.Values{"redirect_uri": []string{"request-redir"}}, Session: &fosite.DefaultSession{}, - RequestedAt: time.Now().Add(time.Hour), + RequestedAt: time.Now().UTC(), }, }, authreq: &fosite.AuthorizeRequest{ @@ -266,7 +292,7 @@ func TestAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { Client: &fosite.DefaultClient{ID: "foo", GrantTypes: []string{"authorization_code"}}, Session: &fosite.DefaultSession{}, Scopes: fosite.Arguments{"a", "b"}, - RequestedAt: time.Now().Add(time.Hour), + RequestedAt: time.Now().UTC(), }, }, description: "should pass", @@ -279,16 +305,18 @@ func TestAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { }, }, } { - t.Run("case="+c.description, func(t *testing.T) { + t.Run(fmt.Sprintf("case=%d/description=%s", i, c.description), func(t *testing.T) { if c.setup != nil { c.setup(t, c.areq, c.authreq) } - err := h.HandleTokenEndpointRequest(nil, c.areq) + t.Logf("Processing %+v", c.areq.Client) + + err := h.HandleTokenEndpointRequest(context.Background(), c.areq) if c.expectErr != nil { - require.EqualError(t, errors.Cause(err), c.expectErr.Error(), "%v", err) + require.EqualError(t, errors.Cause(err), c.expectErr.Error()) } else { - require.NoError(t, err, "%v", err) + require.NoError(t, err) } }) } diff --git a/handler/oauth2/flow_authorize_implicit.go b/handler/oauth2/flow_authorize_implicit.go index 336ce763..38f884d5 100644 --- a/handler/oauth2/flow_authorize_implicit.go +++ b/handler/oauth2/flow_authorize_implicit.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -34,17 +48,17 @@ func (c *AuthorizeImplicitGrantTypeHandler) HandleAuthorizeEndpointRequest(ctx c } if !ar.GetClient().GetResponseTypes().Has("token") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use response type token") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use response type token")) } if !ar.GetClient().GetGrantTypes().Has("implicit") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use grant type implicit") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use grant type implicit")) } client := ar.GetClient() for _, scope := range ar.GetRequestedScopes() { if !c.ScopeStrategy(client.GetScopes(), scope) { - return errors.Wrap(fosite.ErrInvalidScope, fmt.Sprintf("The client is not allowed to request scope %s", scope)) + return errors.WithStack(fosite.ErrInvalidScope.WithDebug(fmt.Sprintf("The client is not allowed to request scope %s", scope))) } } @@ -55,19 +69,20 @@ func (c *AuthorizeImplicitGrantTypeHandler) HandleAuthorizeEndpointRequest(ctx c } func (c *AuthorizeImplicitGrantTypeHandler) IssueImplicitAccessToken(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { + ar.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.AccessTokenLifespan)) + // Generate the code token, signature, err := c.AccessTokenStrategy.GenerateAccessToken(ctx, ar) if err != nil { - return errors.Wrap(fosite.ErrServerError, err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } - ar.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().Add(c.AccessTokenLifespan)) if err := c.AccessTokenStorage.CreateAccessTokenSession(ctx, signature, ar); err != nil { - return errors.Wrap(fosite.ErrServerError, err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } resp.AddFragment("access_token", token) - resp.AddFragment("expires_in", strconv.FormatInt(int64(getExpiresIn(ar, fosite.AccessToken, c.AccessTokenLifespan, time.Now())/time.Second), 10)) + resp.AddFragment("expires_in", strconv.FormatInt(int64(getExpiresIn(ar, fosite.AccessToken, c.AccessTokenLifespan, time.Now().UTC())/time.Second), 10)) resp.AddFragment("token_type", "bearer") resp.AddFragment("state", ar.GetState()) resp.AddFragment("scope", strings.Join(ar.GetGrantedScopes(), " ")) diff --git a/handler/oauth2/flow_authorize_implicit_test.go b/handler/oauth2/flow_authorize_implicit_test.go index 7e543fb8..b494f717 100644 --- a/handler/oauth2/flow_authorize_implicit_test.go +++ b/handler/oauth2/flow_authorize_implicit_test.go @@ -1,14 +1,30 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( "testing" "time" + "fmt" + "github.com/golang/mock/gomock" "github.com/ory/fosite" "github.com/ory/fosite/internal" "github.com/pkg/errors" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAuthorizeImplicit_EndpointHandler(t *testing.T) { @@ -75,9 +91,14 @@ func TestAuthorizeImplicit_EndpointHandler(t *testing.T) { expectErr: nil, }, } { - c.setup() - err := h.HandleAuthorizeEndpointRequest(nil, areq, aresp) - assert.True(t, errors.Cause(err) == c.expectErr, "(%d) %s\n%s\n%s", k, c.description, err, c.expectErr) - t.Logf("Passed test case %d", k) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + c.setup() + err := h.HandleAuthorizeEndpointRequest(nil, areq, aresp) + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + } + }) } } diff --git a/handler/oauth2/flow_client_credentials.go b/handler/oauth2/flow_client_credentials.go index 37876ef2..43d72496 100644 --- a/handler/oauth2/flow_client_credentials.go +++ b/handler/oauth2/flow_client_credentials.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -26,7 +40,7 @@ func (c *ClientCredentialsGrantHandler) HandleTokenEndpointRequest(_ context.Con client := request.GetClient() for _, scope := range request.GetRequestedScopes() { if !c.ScopeStrategy(client.GetScopes(), scope) { - return errors.Wrap(fosite.ErrInvalidScope, fmt.Sprintf("The client is not allowed to request scope %s", scope)) + return errors.WithStack(fosite.ErrInvalidScope.WithDebug(fmt.Sprintf("The client is not allowed to request scope %s", scope))) } } @@ -34,11 +48,11 @@ func (c *ClientCredentialsGrantHandler) HandleTokenEndpointRequest(_ context.Con // This requirement is already fulfilled because fosite requries all token requests to be authenticated as described // in https://tools.ietf.org/html/rfc6749#section-3.2.1 if client.IsPublic() { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is public and thus not allowed to use grant type client_credentials") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is public and thus not allowed to use grant type client_credentials")) } // if the client is not public, he has already been authenticated by the access request handler. - request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().Add(c.AccessTokenLifespan)) + request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.AccessTokenLifespan)) return nil } @@ -49,7 +63,7 @@ func (c *ClientCredentialsGrantHandler) PopulateTokenEndpointResponse(ctx contex } if !request.GetClient().GetGrantTypes().Has("client_credentials") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use grant type client_credentials") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use grant type client_credentials")) } return c.IssueAccessToken(ctx, request, response) diff --git a/handler/oauth2/flow_client_credentials_storage.go b/handler/oauth2/flow_client_credentials_storage.go index 0ef5da74..8ccf1f0a 100644 --- a/handler/oauth2/flow_client_credentials_storage.go +++ b/handler/oauth2/flow_client_credentials_storage.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 type ClientCredentialsGrantStorage interface { diff --git a/handler/oauth2/flow_client_credentials_test.go b/handler/oauth2/flow_client_credentials_test.go index 03d2c29c..fe68e915 100644 --- a/handler/oauth2/flow_client_credentials_test.go +++ b/handler/oauth2/flow_client_credentials_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -5,11 +19,12 @@ import ( "testing" "time" + "fmt" + "github.com/golang/mock/gomock" "github.com/ory/fosite" "github.com/ory/fosite/internal" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestClientCredentials_HandleTokenEndpointRequest(t *testing.T) { @@ -53,10 +68,15 @@ func TestClientCredentials_HandleTokenEndpointRequest(t *testing.T) { }, }, } { - c.mock() - err := h.HandleTokenEndpointRequest(nil, areq) - assert.True(t, errors.Cause(err) == c.expectErr, "(%d) %s\n%s\n%s", k, c.description, err, c.expectErr) - t.Logf("Passed test case %d", k) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + c.mock() + err := h.HandleTokenEndpointRequest(nil, areq) + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + } + }) } } @@ -108,9 +128,14 @@ func TestClientCredentials_PopulateTokenEndpointResponse(t *testing.T) { }, }, } { - c.mock() - err := h.PopulateTokenEndpointResponse(nil, areq, aresp) - assert.True(t, errors.Cause(err) == c.expectErr, "(%d) %s\n%s\n%s", k, c.description, err, c.expectErr) - t.Logf("Passed test case %d", k) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + c.mock() + err := h.PopulateTokenEndpointResponse(nil, areq, aresp) + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + } + }) } } diff --git a/handler/oauth2/flow_refresh.go b/handler/oauth2/flow_refresh.go index 1d7bb387..b49d3f13 100644 --- a/handler/oauth2/flow_refresh.go +++ b/handler/oauth2/flow_refresh.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -26,7 +40,7 @@ func (c *RefreshTokenGrantHandler) HandleTokenEndpointRequest(ctx context.Contex } if !request.GetClient().GetGrantTypes().Has("refresh_token") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use grant type refresh_token") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use grant type refresh_token")) } refresh := request.GetRequestForm().Get("refresh_token") @@ -34,23 +48,23 @@ func (c *RefreshTokenGrantHandler) HandleTokenEndpointRequest(ctx context.Contex signature := c.RefreshTokenStrategy.RefreshTokenSignature(refresh) originalRequest, err := c.TokenRevocationStorage.GetRefreshTokenSession(ctx, signature, request.GetSession()) if errors.Cause(err) == fosite.ErrNotFound { - return errors.Wrap(fosite.ErrInvalidRequest, err.Error()) + return errors.WithStack(fosite.ErrInvalidRequest.WithDebug(err.Error())) } else if err != nil { - return errors.Wrap(fosite.ErrServerError, err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } else if err := c.RefreshTokenStrategy.ValidateRefreshToken(ctx, request, refresh); err != nil { // The authorization server MUST ... validate the refresh token. // This needs to happen after store retrieval for the session to be hydrated properly - return errors.Wrap(fosite.ErrInvalidRequest, err.Error()) + return errors.WithStack(fosite.ErrInvalidRequest.WithDebug(err.Error())) } - if !originalRequest.GetGrantedScopes().Has("offline") { - return errors.Wrap(fosite.ErrScopeNotGranted, "The client is not allowed to use grant type refresh_token") + if !originalRequest.GetGrantedScopes().HasOneOf("offline", "offline_access") { + return errors.WithStack(fosite.ErrScopeNotGranted.WithDebug("The client is not allowed to use grant type refresh_token")) } // The authorization server MUST ... and ensure that the refresh token was issued to the authenticated client if originalRequest.GetClient().GetID() != request.GetClient().GetID() { - return errors.Wrap(fosite.ErrInvalidRequest, "Client ID mismatch") + return errors.WithStack(fosite.ErrInvalidRequest.WithDebug("Client ID mismatch")) } request.SetSession(originalRequest.GetSession().Clone()) @@ -59,7 +73,7 @@ func (c *RefreshTokenGrantHandler) HandleTokenEndpointRequest(ctx context.Contex request.GrantScope(scope) } - request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().Add(c.AccessTokenLifespan)) + request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.AccessTokenLifespan)) return nil } @@ -71,30 +85,30 @@ func (c *RefreshTokenGrantHandler) PopulateTokenEndpointResponse(ctx context.Con accessToken, accessSignature, err := c.AccessTokenStrategy.GenerateAccessToken(ctx, requester) if err != nil { - return errors.Wrap(fosite.ErrServerError, err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } refreshToken, refreshSignature, err := c.RefreshTokenStrategy.GenerateRefreshToken(ctx, requester) if err != nil { - return errors.Wrap(fosite.ErrServerError, err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } signature := c.RefreshTokenStrategy.RefreshTokenSignature(requester.GetRequestForm().Get("refresh_token")) if ts, err := c.TokenRevocationStorage.GetRefreshTokenSession(ctx, signature, nil); err != nil { - return err + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } else if err := c.TokenRevocationStorage.RevokeAccessToken(ctx, ts.GetID()); err != nil { - return err + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } else if err := c.TokenRevocationStorage.RevokeRefreshToken(ctx, ts.GetID()); err != nil { - return err + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } else if err := c.TokenRevocationStorage.CreateAccessTokenSession(ctx, accessSignature, requester); err != nil { - return err + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } else if err := c.TokenRevocationStorage.CreateRefreshTokenSession(ctx, refreshSignature, requester); err != nil { - return err + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } responder.SetAccessToken(accessToken) responder.SetTokenType("bearer") - responder.SetExpiresIn(getExpiresIn(requester, fosite.AccessToken, c.AccessTokenLifespan, time.Now())) + responder.SetExpiresIn(getExpiresIn(requester, fosite.AccessToken, c.AccessTokenLifespan, time.Now().UTC())) responder.SetScopes(requester.GetGrantedScopes()) responder.SetExtra("refresh_token", refreshToken) return nil diff --git a/handler/oauth2/flow_refresh_test.go b/handler/oauth2/flow_refresh_test.go index 608d5844..4e0fa701 100644 --- a/handler/oauth2/flow_refresh_test.go +++ b/handler/oauth2/flow_refresh_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -103,13 +117,13 @@ func TestRefreshFlow_HandleTokenEndpointRequest(t *testing.T) { Scopes: fosite.Arguments{"foo", "bar"}, Session: sess, Form: url.Values{"foo": []string{"bar"}}, - RequestedAt: time.Now().Add(-time.Hour).Round(time.Hour), + RequestedAt: time.Now().UTC().Add(-time.Hour).Round(time.Hour), }) require.NoError(t, err) }, expect: func(t *testing.T) { assert.NotEqual(t, sess, areq.Session) - assert.NotEqual(t, time.Now().Add(-time.Hour).Round(time.Hour), areq.RequestedAt) + assert.NotEqual(t, time.Now().UTC().Add(-time.Hour).Round(time.Hour), areq.RequestedAt) assert.Equal(t, fosite.Arguments{"foo", "offline"}, areq.GrantedScopes) assert.Equal(t, fosite.Arguments{"foo", "bar"}, areq.Scopes) assert.NotEqual(t, url.Values{"foo": []string{"bar"}}, areq.Form) diff --git a/handler/oauth2/flow_resource_owner.go b/handler/oauth2/flow_resource_owner.go index 749ea222..4b5f9a48 100644 --- a/handler/oauth2/flow_resource_owner.go +++ b/handler/oauth2/flow_resource_owner.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -29,30 +43,30 @@ func (c *ResourceOwnerPasswordCredentialsGrantHandler) HandleTokenEndpointReques } if !request.GetClient().GetGrantTypes().Has("password") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use grant type password") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use grant type password")) } username := request.GetRequestForm().Get("username") password := request.GetRequestForm().Get("password") if username == "" || password == "" { - return errors.Wrap(fosite.ErrInvalidRequest, "Username or password missing") + return errors.WithStack(fosite.ErrInvalidRequest.WithDebug("Username or password missing")) } else if err := c.ResourceOwnerPasswordCredentialsGrantStorage.Authenticate(ctx, username, password); errors.Cause(err) == fosite.ErrNotFound { - return errors.Wrap(fosite.ErrInvalidRequest, err.Error()) + return errors.WithStack(fosite.ErrInvalidRequest.WithDebug(err.Error())) } else if err != nil { - return errors.Wrap(fosite.ErrServerError, err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } client := request.GetClient() for _, scope := range request.GetRequestedScopes() { if !c.ScopeStrategy(client.GetScopes(), scope) { - return errors.Wrap(fosite.ErrInvalidScope, fmt.Sprintf("The client is not allowed to request scope %s", scope)) + return errors.WithStack(fosite.ErrInvalidScope.WithDebug(fmt.Sprintf("The client is not allowed to request scope %s", scope))) } } // Credentials must not be passed around, potentially leaking to the database! delete(request.GetRequestForm(), "password") - request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().Add(c.AccessTokenLifespan)) + request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.AccessTokenLifespan)) return nil } @@ -63,13 +77,13 @@ func (c *ResourceOwnerPasswordCredentialsGrantHandler) PopulateTokenEndpointResp } var refresh, refreshSignature string - if requester.GetGrantedScopes().Has("offline") { + if requester.GetGrantedScopes().HasOneOf("offline", "offline_access") { var err error refresh, refreshSignature, err = c.RefreshTokenStrategy.GenerateRefreshToken(ctx, requester) if err != nil { - return errors.Wrap(fosite.ErrServerError, err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } else if err := c.ResourceOwnerPasswordCredentialsGrantStorage.CreateRefreshTokenSession(ctx, refreshSignature, requester); err != nil { - return errors.Wrap(fosite.ErrServerError, err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } } diff --git a/handler/oauth2/flow_resource_owner_storage.go b/handler/oauth2/flow_resource_owner_storage.go index e9f45779..87dce06e 100644 --- a/handler/oauth2/flow_resource_owner_storage.go +++ b/handler/oauth2/flow_resource_owner_storage.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( diff --git a/handler/oauth2/flow_resource_owner_test.go b/handler/oauth2/flow_resource_owner_test.go index 38aabed7..6df5e3c6 100644 --- a/handler/oauth2/flow_resource_owner_test.go +++ b/handler/oauth2/flow_resource_owner_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -5,11 +19,14 @@ import ( "testing" "time" + "fmt" + "github.com/golang/mock/gomock" "github.com/ory/fosite" "github.com/ory/fosite/internal" "github.com/pkg/errors" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestResourceOwnerFlow_HandleTokenEndpointRequest(t *testing.T) { @@ -70,13 +87,19 @@ func TestResourceOwnerFlow_HandleTokenEndpointRequest(t *testing.T) { }, }, } { - c.setup() - err := h.HandleTokenEndpointRequest(nil, areq) - assert.True(t, errors.Cause(err) == c.expectErr, "(%d) %s\n%s\n%s", k, c.description, err, c.expectErr) - t.Logf("Passed test case %d", k) - if c.check != nil { - c.check(areq) - } + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + c.setup() + err := h.HandleTokenEndpointRequest(nil, areq) + + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + if c.check != nil { + c.check(areq) + } + } + }) } } @@ -141,12 +164,17 @@ func TestResourceOwnerFlow_PopulateTokenEndpointResponse(t *testing.T) { }, }, } { - c.setup() - err := h.PopulateTokenEndpointResponse(nil, areq, aresp) - assert.True(t, errors.Cause(err) == c.expectErr, "(%d) %s\n%s\n%s", k, c.description, err, c.expectErr) - if c.expect != nil { - c.expect() - } - t.Logf("Passed test case %d", k) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + c.setup() + err := h.PopulateTokenEndpointResponse(nil, areq, aresp) + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + if c.expect != nil { + c.expect() + } + } + }) } } diff --git a/handler/oauth2/helper.go b/handler/oauth2/helper.go index e0c6b12d..85af6162 100644 --- a/handler/oauth2/helper.go +++ b/handler/oauth2/helper.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -24,7 +38,7 @@ func (h *HandleHelper) IssueAccessToken(ctx context.Context, requester fosite.Ac responder.SetAccessToken(token) responder.SetTokenType("bearer") - responder.SetExpiresIn(getExpiresIn(requester, fosite.AccessToken, h.AccessTokenLifespan, time.Now())) + responder.SetExpiresIn(getExpiresIn(requester, fosite.AccessToken, h.AccessTokenLifespan, time.Now().UTC())) responder.SetScopes(requester.GetGrantedScopes()) return nil } diff --git a/handler/oauth2/helper_test.go b/handler/oauth2/helper_test.go index 11e18ece..e3771287 100644 --- a/handler/oauth2/helper_test.go +++ b/handler/oauth2/helper_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -13,7 +27,7 @@ import ( ) func TestGetExpiresIn(t *testing.T) { - now := time.Now() + now := time.Now().UTC() r := fosite.NewAccessRequest(&fosite.DefaultSession{ ExpiresAt: map[fosite.TokenType]time.Time{ fosite.AccessToken: now.Add(time.Hour), diff --git a/handler/oauth2/introspector.go b/handler/oauth2/introspector.go index db7b513e..cefd0034 100644 --- a/handler/oauth2/introspector.go +++ b/handler/oauth2/introspector.go @@ -1,8 +1,24 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( "context" + "fmt" + "github.com/ory/fosite" "github.com/pkg/errors" ) @@ -45,7 +61,7 @@ func matchScopes(ss fosite.ScopeStrategy, granted, scopes []string) error { } if !ss(granted, scope) { - return errors.Wrapf(fosite.ErrInvalidScope, "Scope %s was not granted", scope) + return errors.WithStack(fosite.ErrInvalidScope.WithDebug(fmt.Sprintf("Scope %s was not granted", scope))) } } @@ -56,7 +72,7 @@ func (c *CoreValidator) introspectAccessToken(ctx context.Context, token string, sig := c.CoreStrategy.AccessTokenSignature(token) or, err := c.CoreStorage.GetAccessTokenSession(ctx, sig, accessRequest.GetSession()) if err != nil { - return errors.Wrap(fosite.ErrRequestUnauthorized, err.Error()) + return errors.WithStack(fosite.ErrRequestUnauthorized.WithDebug(err.Error())) } else if err := c.CoreStrategy.ValidateAccessToken(ctx, or, token); err != nil { return err } @@ -74,7 +90,7 @@ func (c *CoreValidator) introspectRefreshToken(ctx context.Context, token string or, err := c.CoreStorage.GetRefreshTokenSession(ctx, sig, accessRequest.GetSession()) if err != nil { - return errors.Wrap(fosite.ErrRequestUnauthorized, err.Error()) + return errors.WithStack(fosite.ErrRequestUnauthorized.WithDebug(err.Error())) } else if err := c.CoreStrategy.ValidateRefreshToken(ctx, or, token); err != nil { return err } diff --git a/handler/oauth2/introspector_jwt.go b/handler/oauth2/introspector_jwt.go index 16999176..2f1f2fe7 100644 --- a/handler/oauth2/introspector_jwt.go +++ b/handler/oauth2/introspector_jwt.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( diff --git a/handler/oauth2/introspector_jwt_test.go b/handler/oauth2/introspector_jwt_test.go index 21f5c902..0f2ea24a 100644 --- a/handler/oauth2/introspector_jwt_test.go +++ b/handler/oauth2/introspector_jwt_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -5,11 +19,13 @@ import ( "strings" "testing" + "fmt" + "github.com/ory/fosite" "github.com/ory/fosite/internal" "github.com/ory/fosite/token/jwt" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIntrospectJWT(t *testing.T) { @@ -87,19 +103,21 @@ func TestIntrospectJWT(t *testing.T) { }, }, } { - if c.scopes == nil { - c.scopes = []string{} - } - areq := fosite.NewAccessRequest(nil) - err := v.IntrospectToken(nil, c.token(), fosite.AccessToken, areq, c.scopes) - - assert.True(t, errors.Cause(err) == c.expectErr, "(%d) %s\n%s\n%s", k, c.description, err, c.expectErr) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + if c.scopes == nil { + c.scopes = []string{} + } - if err == nil { - assert.Equal(t, "peter", areq.Session.GetSubject()) - } + areq := fosite.NewAccessRequest(nil) + err := v.IntrospectToken(nil, c.token(), fosite.AccessToken, areq, c.scopes) - t.Logf("Passed test case %d", k) + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + assert.Equal(t, "peter", areq.Session.GetSubject()) + } + }) } } diff --git a/handler/oauth2/introspector_test.go b/handler/oauth2/introspector_test.go index c582c67f..5d468aab 100644 --- a/handler/oauth2/introspector_test.go +++ b/handler/oauth2/introspector_test.go @@ -1,6 +1,21 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( + "fmt" "net/http" "testing" @@ -8,7 +23,7 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/internal" "github.com/pkg/errors" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIntrospectToken(t *testing.T) { @@ -76,9 +91,15 @@ func TestIntrospectToken(t *testing.T) { }, }, } { - c.setup() - err := v.IntrospectToken(nil, fosite.AccessTokenFromRequest(httpreq), fosite.AccessToken, areq, []string{}) - assert.True(t, errors.Cause(err) == c.expectErr, "(%d) %s\n%s\n%s", k, c.description, err, c.expectErr) - t.Logf("Passed test case %d", k) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + c.setup() + err := v.IntrospectToken(nil, fosite.AccessTokenFromRequest(httpreq), fosite.AccessToken, areq, []string{}) + + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + } + }) } } diff --git a/handler/oauth2/revocation.go b/handler/oauth2/revocation.go index 23dcfe12..b36fd06b 100644 --- a/handler/oauth2/revocation.go +++ b/handler/oauth2/revocation.go @@ -1,9 +1,24 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( "context" "github.com/ory/fosite" + "github.com/pkg/errors" ) type TokenRevocationHandler struct { @@ -14,7 +29,7 @@ type TokenRevocationHandler struct { // RevokeToken implements https://tools.ietf.org/html/rfc7009#section-2.1 // The token type hint indicates which token type check should be performed first. -func (r *TokenRevocationHandler) RevokeToken(ctx context.Context, token string, tokenType fosite.TokenType) error { +func (r *TokenRevocationHandler) RevokeToken(ctx context.Context, token string, tokenType fosite.TokenType, client fosite.Client) error { discoveryFuncs := []func() (request fosite.Requester, err error){ func() (request fosite.Requester, err error) { // Refresh token @@ -42,6 +57,10 @@ func (r *TokenRevocationHandler) RevokeToken(ctx context.Context, token string, return err } + if ar.GetClient().GetID() != client.GetID() { + return errors.WithStack(fosite.ErrRevokationClientMismatch) + } + requestID := ar.GetID() r.TokenRevocationStorage.RevokeRefreshToken(ctx, requestID) r.TokenRevocationStorage.RevokeAccessToken(ctx, requestID) diff --git a/handler/oauth2/revocation_storage.go b/handler/oauth2/revocation_storage.go index 5137d902..194610f7 100644 --- a/handler/oauth2/revocation_storage.go +++ b/handler/oauth2/revocation_storage.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( diff --git a/handler/oauth2/revocation_test.go b/handler/oauth2/revocation_test.go index 935eaa4b..9d6e653d 100644 --- a/handler/oauth2/revocation_test.go +++ b/handler/oauth2/revocation_test.go @@ -1,13 +1,28 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( "testing" + "fmt" + "github.com/golang/mock/gomock" "github.com/ory/fosite" "github.com/ory/fosite/internal" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRevokeToken(t *testing.T) { @@ -31,16 +46,31 @@ func TestRevokeToken(t *testing.T) { description string mock func() expectErr error + client fosite.Client }{ + { + description: "should fail - token was issued to another client", + expectErr: fosite.ErrRevokationClientMismatch, + client: &fosite.DefaultClient{ID: "bar"}, + mock: func() { + token = "foo" + tokenType = fosite.RefreshToken + rtStrat.EXPECT().RefreshTokenSignature(token) + store.EXPECT().GetRefreshTokenSession(gomock.Any(), gomock.Any(), gomock.Any()).Return(ar, nil) + ar.EXPECT().GetClient().Return(&fosite.DefaultClient{ID: "foo"}) + }, + }, { description: "should pass - refresh token discovery first; refresh token found", expectErr: nil, + client: &fosite.DefaultClient{ID: "bar"}, mock: func() { token = "foo" tokenType = fosite.RefreshToken rtStrat.EXPECT().RefreshTokenSignature(token) store.EXPECT().GetRefreshTokenSession(gomock.Any(), gomock.Any(), gomock.Any()).Return(ar, nil) ar.EXPECT().GetID() + ar.EXPECT().GetClient().Return(&fosite.DefaultClient{ID: "bar"}) store.EXPECT().RevokeRefreshToken(gomock.Any(), gomock.Any()) store.EXPECT().RevokeAccessToken(gomock.Any(), gomock.Any()) }, @@ -48,12 +78,14 @@ func TestRevokeToken(t *testing.T) { { description: "should pass - access token discovery first; access token found", expectErr: nil, + client: &fosite.DefaultClient{ID: "bar"}, mock: func() { token = "foo" tokenType = fosite.AccessToken atStrat.EXPECT().AccessTokenSignature(token) store.EXPECT().GetAccessTokenSession(gomock.Any(), gomock.Any(), gomock.Any()).Return(ar, nil) ar.EXPECT().GetID() + ar.EXPECT().GetClient().Return(&fosite.DefaultClient{ID: "bar"}) store.EXPECT().RevokeRefreshToken(gomock.Any(), gomock.Any()) store.EXPECT().RevokeAccessToken(gomock.Any(), gomock.Any()) }, @@ -61,6 +93,7 @@ func TestRevokeToken(t *testing.T) { { description: "should pass - refresh token discovery first; refresh token not found", expectErr: nil, + client: &fosite.DefaultClient{ID: "bar"}, mock: func() { token = "foo" tokenType = fosite.AccessToken @@ -70,6 +103,7 @@ func TestRevokeToken(t *testing.T) { rtStrat.EXPECT().RefreshTokenSignature(token) store.EXPECT().GetRefreshTokenSession(gomock.Any(), gomock.Any(), gomock.Any()).Return(ar, nil) ar.EXPECT().GetID() + ar.EXPECT().GetClient().Return(&fosite.DefaultClient{ID: "bar"}) store.EXPECT().RevokeRefreshToken(gomock.Any(), gomock.Any()) store.EXPECT().RevokeAccessToken(gomock.Any(), gomock.Any()) }, @@ -77,6 +111,7 @@ func TestRevokeToken(t *testing.T) { { description: "should pass - access token discovery first; access token not found", expectErr: nil, + client: &fosite.DefaultClient{ID: "bar"}, mock: func() { token = "foo" tokenType = fosite.RefreshToken @@ -86,13 +121,15 @@ func TestRevokeToken(t *testing.T) { atStrat.EXPECT().AccessTokenSignature(token) store.EXPECT().GetAccessTokenSession(gomock.Any(), gomock.Any(), gomock.Any()).Return(ar, nil) ar.EXPECT().GetID() + ar.EXPECT().GetClient().Return(&fosite.DefaultClient{ID: "bar"}) store.EXPECT().RevokeRefreshToken(gomock.Any(), gomock.Any()) store.EXPECT().RevokeAccessToken(gomock.Any(), gomock.Any()) }, }, { - description: "should pass - refresh token discovery first; both tokens not found", + description: "should fail - refresh token discovery first; both tokens not found", expectErr: fosite.ErrNotFound, + client: &fosite.DefaultClient{ID: "bar"}, mock: func() { token = "foo" tokenType = fosite.RefreshToken @@ -104,8 +141,9 @@ func TestRevokeToken(t *testing.T) { }, }, { - description: "should pass - access token discovery first; both tokens not found", + description: "should fail - access token discovery first; both tokens not found", expectErr: fosite.ErrNotFound, + client: &fosite.DefaultClient{ID: "bar"}, mock: func() { token = "foo" tokenType = fosite.AccessToken @@ -117,9 +155,15 @@ func TestRevokeToken(t *testing.T) { }, }, } { - c.mock() - err := h.RevokeToken(nil, token, tokenType) - assert.True(t, errors.Cause(err) == c.expectErr, "(%d) %s\n%s\n%s", k, c.description, err, c.expectErr) - t.Logf("Passed test case %d", k) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + c.mock() + err := h.RevokeToken(nil, token, tokenType, c.client) + + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + } + }) } } diff --git a/handler/oauth2/storage.go b/handler/oauth2/storage.go index bf276103..40510dfc 100644 --- a/handler/oauth2/storage.go +++ b/handler/oauth2/storage.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( diff --git a/handler/oauth2/strategy.go b/handler/oauth2/strategy.go index 9df84ba2..ec178937 100644 --- a/handler/oauth2/strategy.go +++ b/handler/oauth2/strategy.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( diff --git a/handler/oauth2/strategy_hmacsha.go b/handler/oauth2/strategy_hmacsha.go index 1ce2e216..1898d5cb 100644 --- a/handler/oauth2/strategy_hmacsha.go +++ b/handler/oauth2/strategy_hmacsha.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -33,11 +47,11 @@ func (h HMACSHAStrategy) GenerateAccessToken(_ context.Context, _ fosite.Request func (h HMACSHAStrategy) ValidateAccessToken(_ context.Context, r fosite.Requester, token string) (err error) { var exp = r.GetSession().GetExpiresAt(fosite.AccessToken) - if exp.IsZero() && r.GetRequestedAt().Add(h.AccessTokenLifespan).Before(time.Now()) { - return errors.Wrap(fosite.ErrTokenExpired, fmt.Sprintf("Access token expired at %s", r.GetRequestedAt().Add(h.AccessTokenLifespan))) + if exp.IsZero() && r.GetRequestedAt().Add(h.AccessTokenLifespan).Before(time.Now().UTC()) { + return errors.WithStack(fosite.ErrTokenExpired.WithDebug(fmt.Sprintf("Access token expired at %s", r.GetRequestedAt().Add(h.AccessTokenLifespan)))) } - if !exp.IsZero() && exp.Before(time.Now()) { - return errors.Wrap(fosite.ErrTokenExpired, fmt.Sprintf("Access token expired at %s", exp)) + if !exp.IsZero() && exp.Before(time.Now().UTC()) { + return errors.WithStack(fosite.ErrTokenExpired.WithDebug(fmt.Sprintf("Access token expired at %s", exp))) } return h.Enigma.Validate(token) } @@ -56,11 +70,11 @@ func (h HMACSHAStrategy) GenerateAuthorizeCode(_ context.Context, _ fosite.Reque func (h HMACSHAStrategy) ValidateAuthorizeCode(_ context.Context, r fosite.Requester, token string) (err error) { var exp = r.GetSession().GetExpiresAt(fosite.AuthorizeCode) - if exp.IsZero() && r.GetRequestedAt().Add(h.AuthorizeCodeLifespan).Before(time.Now()) { - return errors.Wrap(fosite.ErrTokenExpired, fmt.Sprintf("Authorize code expired at %s", r.GetRequestedAt().Add(h.AuthorizeCodeLifespan))) + if exp.IsZero() && r.GetRequestedAt().Add(h.AuthorizeCodeLifespan).Before(time.Now().UTC()) { + return errors.WithStack(fosite.ErrTokenExpired.WithDebug(fmt.Sprintf("Authorize code expired at %s", r.GetRequestedAt().Add(h.AuthorizeCodeLifespan)))) } - if !exp.IsZero() && exp.Before(time.Now()) { - return errors.Wrap(fosite.ErrTokenExpired, fmt.Sprintf("Authorize code expired at %s", exp)) + if !exp.IsZero() && exp.Before(time.Now().UTC()) { + return errors.WithStack(fosite.ErrTokenExpired.WithDebug(fmt.Sprintf("Authorize code expired at %s", exp))) } return h.Enigma.Validate(token) diff --git a/handler/oauth2/strategy_hmacsha_test.go b/handler/oauth2/strategy_hmacsha_test.go index a4773348..5856919a 100644 --- a/handler/oauth2/strategy_hmacsha_test.go +++ b/handler/oauth2/strategy_hmacsha_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -5,13 +19,15 @@ import ( "testing" "time" + "fmt" + "github.com/ory/fosite" "github.com/ory/fosite/token/hmac" "github.com/stretchr/testify/assert" ) var hmacshaStrategy = HMACSHAStrategy{ - Enigma: &hmac.HMACStrategy{GlobalSecret: []byte("foobarfoobarfoobarfoobar")}, + Enigma: &hmac.HMACStrategy{GlobalSecret: []byte("foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar")}, AccessTokenLifespan: time.Hour * 24, AuthorizeCodeLifespan: time.Hour * 24, } @@ -22,8 +38,8 @@ var hmacExpiredCase = fosite.Request{ }, Session: &fosite.DefaultSession{ ExpiresAt: map[fosite.TokenType]time.Time{ - fosite.AccessToken: time.Now().Add(-time.Hour), - fosite.AuthorizeCode: time.Now().Add(-time.Hour), + fosite.AccessToken: time.Now().UTC().Add(-time.Hour), + fosite.AuthorizeCode: time.Now().UTC().Add(-time.Hour), }, }, } @@ -34,14 +50,14 @@ var hmacValidCase = fosite.Request{ }, Session: &fosite.DefaultSession{ ExpiresAt: map[fosite.TokenType]time.Time{ - fosite.AccessToken: time.Now().Add(time.Hour), - fosite.AuthorizeCode: time.Now().Add(time.Hour), + fosite.AccessToken: time.Now().UTC().Add(time.Hour), + fosite.AuthorizeCode: time.Now().UTC().Add(time.Hour), }, }, } func TestHMACAccessToken(t *testing.T) { - for _, c := range []struct { + for k, c := range []struct { r fosite.Request pass bool }{ @@ -54,29 +70,31 @@ func TestHMACAccessToken(t *testing.T) { pass: false, }, } { - token, signature, err := hmacshaStrategy.GenerateAccessToken(nil, &c.r) - assert.Nil(t, err, "%s", err) - assert.Equal(t, strings.Split(token, ".")[1], signature) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + token, signature, err := hmacshaStrategy.GenerateAccessToken(nil, &c.r) + assert.NoError(t, err) + assert.Equal(t, strings.Split(token, ".")[1], signature) - err = hmacshaStrategy.ValidateAccessToken(nil, &c.r, token) - if c.pass { - assert.Nil(t, err, "%s", err) - validate := hmacshaStrategy.Enigma.Signature(token) - assert.Equal(t, signature, validate) - } else { - assert.NotNil(t, err, "%s", err) - } + err = hmacshaStrategy.ValidateAccessToken(nil, &c.r, token) + if c.pass { + assert.NoError(t, err) + validate := hmacshaStrategy.Enigma.Signature(token) + assert.Equal(t, signature, validate) + } else { + assert.Error(t, err) + } + }) } } func TestHMACRefreshToken(t *testing.T) { token, signature, err := hmacshaStrategy.GenerateRefreshToken(nil, &hmacValidCase) - assert.Nil(t, err, "%s", err) + assert.NoError(t, err) assert.Equal(t, strings.Split(token, ".")[1], signature) validate := hmacshaStrategy.Enigma.Signature(token) err = hmacshaStrategy.ValidateRefreshToken(nil, &hmacValidCase, token) - assert.Nil(t, err, "%s", err) + assert.NoError(t, err) assert.Equal(t, signature, validate) } @@ -94,17 +112,19 @@ func TestHMACAuthorizeCode(t *testing.T) { pass: false, }, } { - token, signature, err := hmacshaStrategy.GenerateAuthorizeCode(nil, &c.r) - assert.Nil(t, err, "%s", err) - assert.Equal(t, strings.Split(token, ".")[1], signature) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + token, signature, err := hmacshaStrategy.GenerateAuthorizeCode(nil, &c.r) + assert.NoError(t, err) + assert.Equal(t, strings.Split(token, ".")[1], signature) - err = hmacshaStrategy.ValidateAuthorizeCode(nil, &c.r, token) - if c.pass { - assert.Nil(t, err, "%d: %s", k, err) - validate := hmacshaStrategy.Enigma.Signature(token) - assert.Equal(t, signature, validate) - } else { - assert.NotNil(t, err, "%d: %s", k, err) - } + err = hmacshaStrategy.ValidateAuthorizeCode(nil, &c.r, token) + if c.pass { + assert.NoError(t, err) + validate := hmacshaStrategy.Enigma.Signature(token) + assert.Equal(t, signature, validate) + } else { + assert.Error(t, err) + } + }) } } diff --git a/handler/oauth2/strategy_jwt.go b/handler/oauth2/strategy_jwt.go index b1dff88f..f3e6cae1 100644 --- a/handler/oauth2/strategy_jwt.go +++ b/handler/oauth2/strategy_jwt.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -105,27 +119,27 @@ func (h *RS256JWTStrategy) validate(token string) (t *jwtx.Token, err error) { if e, ok := errors.Cause(err).(*jwtx.ValidationError); ok { switch e.Errors { case jwtx.ValidationErrorMalformed: - err = errors.Wrap(fosite.ErrInvalidTokenFormat, err.Error()) + err = errors.WithStack(fosite.ErrInvalidTokenFormat.WithDebug(err.Error())) case jwtx.ValidationErrorUnverifiable: - err = errors.Wrap(fosite.ErrTokenSignatureMismatch, err.Error()) + err = errors.WithStack(fosite.ErrTokenSignatureMismatch.WithDebug(err.Error())) case jwtx.ValidationErrorSignatureInvalid: - err = errors.Wrap(fosite.ErrTokenSignatureMismatch, err.Error()) + err = errors.WithStack(fosite.ErrTokenSignatureMismatch.WithDebug(err.Error())) case jwtx.ValidationErrorAudience: - err = errors.Wrap(fosite.ErrTokenClaim, err.Error()) + err = errors.WithStack(fosite.ErrTokenClaim.WithDebug(err.Error())) case jwtx.ValidationErrorExpired: - err = errors.Wrap(fosite.ErrTokenExpired, err.Error()) + err = errors.WithStack(fosite.ErrTokenExpired.WithDebug(err.Error())) case jwtx.ValidationErrorIssuedAt: - err = errors.Wrap(fosite.ErrTokenClaim, err.Error()) + err = errors.WithStack(fosite.ErrTokenClaim.WithDebug(err.Error())) case jwtx.ValidationErrorIssuer: - err = errors.Wrap(fosite.ErrTokenClaim, err.Error()) + err = errors.WithStack(fosite.ErrTokenClaim.WithDebug(err.Error())) case jwtx.ValidationErrorNotValidYet: - err = errors.Wrap(fosite.ErrTokenClaim, err.Error()) + err = errors.WithStack(fosite.ErrTokenClaim.WithDebug(err.Error())) case jwtx.ValidationErrorId: - err = errors.Wrap(fosite.ErrTokenClaim, err.Error()) + err = errors.WithStack(fosite.ErrTokenClaim.WithDebug(err.Error())) case jwtx.ValidationErrorClaimsInvalid: - err = errors.Wrap(fosite.ErrTokenClaim, err.Error()) + err = errors.WithStack(fosite.ErrTokenClaim.WithDebug(err.Error())) default: - err = errors.Wrap(fosite.ErrRequestUnauthorized, err.Error()) + err = errors.WithStack(fosite.ErrRequestUnauthorized.WithDebug(err.Error())) } } } @@ -143,7 +157,7 @@ func (h *RS256JWTStrategy) generate(tokenType fosite.TokenType, requester fosite claims.ExpiresAt = jwtSession.GetExpiresAt(tokenType) if claims.IssuedAt.IsZero() { - claims.IssuedAt = time.Now() + claims.IssuedAt = time.Now().UTC() } if claims.Issuer == "" { diff --git a/handler/oauth2/strategy_jwt_session.go b/handler/oauth2/strategy_jwt_session.go index 0fa73829..d1983e79 100644 --- a/handler/oauth2/strategy_jwt_session.go +++ b/handler/oauth2/strategy_jwt_session.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( diff --git a/handler/oauth2/strategy_jwt_test.go b/handler/oauth2/strategy_jwt_test.go index 7755219b..c41b592b 100644 --- a/handler/oauth2/strategy_jwt_test.go +++ b/handler/oauth2/strategy_jwt_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package oauth2 import ( @@ -5,6 +19,8 @@ import ( "testing" "time" + "fmt" + "github.com/ory/fosite" "github.com/ory/fosite/internal" "github.com/ory/fosite/token/jwt" @@ -30,15 +46,15 @@ var jwtValidCase = func(tokenType fosite.TokenType) *fosite.Request { Issuer: "fosite", Subject: "peter", Audience: "group0", - IssuedAt: time.Now(), - NotBefore: time.Now(), + IssuedAt: time.Now().UTC(), + NotBefore: time.Now().UTC(), Extra: make(map[string]interface{}), }, JWTHeader: &jwt.Headers{ Extra: make(map[string]interface{}), }, ExpiresAt: map[fosite.TokenType]time.Time{ - tokenType: time.Now().Add(time.Hour), + tokenType: time.Now().UTC().Add(time.Hour), }, }, } @@ -57,22 +73,23 @@ var jwtExpiredCase = func(tokenType fosite.TokenType) *fosite.Request { Issuer: "fosite", Subject: "peter", Audience: "group0", - IssuedAt: time.Now(), - NotBefore: time.Now(), + IssuedAt: time.Now().UTC(), + NotBefore: time.Now().UTC(), + ExpiresAt: time.Now().UTC().Add(-time.Minute), Extra: make(map[string]interface{}), }, JWTHeader: &jwt.Headers{ Extra: make(map[string]interface{}), }, ExpiresAt: map[fosite.TokenType]time.Time{ - tokenType: time.Now().Add(-time.Hour), + tokenType: time.Now().UTC().Add(-time.Hour), }, }, } } func TestAccessToken(t *testing.T) { - for _, c := range []struct { + for k, c := range []struct { r *fosite.Request pass bool }{ @@ -85,17 +102,19 @@ func TestAccessToken(t *testing.T) { pass: false, }, } { - token, signature, err := j.GenerateAccessToken(nil, c.r) - assert.Nil(t, err, "%s", err) - assert.Equal(t, strings.Split(token, ".")[2], signature) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + token, signature, err := j.GenerateAccessToken(nil, c.r) + assert.NoError(t, err) + assert.Equal(t, strings.Split(token, ".")[2], signature) - validate := j.signature(token) - err = j.ValidateAccessToken(nil, c.r, token) - if c.pass { - assert.Nil(t, err, "%s", err) - assert.Equal(t, signature, validate) - } else { - assert.NotNil(t, err, "%s", err) - } + validate := j.signature(token) + err = j.ValidateAccessToken(nil, c.r, token) + if c.pass { + assert.NoError(t, err) + assert.Equal(t, signature, validate) + } else { + assert.Error(t, err) + } + }) } } diff --git a/handler/openid/errors.go b/handler/openid/errors.go index 1245f2b5..cce51683 100644 --- a/handler/openid/errors.go +++ b/handler/openid/errors.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import "github.com/pkg/errors" diff --git a/handler/openid/flow_explicit_auth.go b/handler/openid/flow_explicit_auth.go index c94208ce..30b7c973 100644 --- a/handler/openid/flow_explicit_auth.go +++ b/handler/openid/flow_explicit_auth.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import ( @@ -20,15 +34,15 @@ func (c *OpenIDConnectExplicitHandler) HandleAuthorizeEndpointRequest(ctx contex } if !ar.GetClient().GetResponseTypes().Has("id_token", "code") { - return errors.Wrap(fosite.ErrInvalidRequest, "The client is not allowed to use response type id_token and code") + return errors.WithStack(fosite.ErrInvalidRequest.WithDebug("The client is not allowed to use response type id_token and code")) } if len(resp.GetCode()) == 0 { - return errors.Wrap(fosite.ErrMisconfiguration, "Authorization code has not been issued yet") + return errors.WithStack(fosite.ErrMisconfiguration.WithDebug("Authorization code has not been issued yet")) } if err := c.OpenIDConnectRequestStorage.CreateOpenIDConnectSession(ctx, resp.GetCode(), ar); err != nil { - return errors.Wrap(fosite.ErrServerError, err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } // there is no need to check for https, because it has already been checked by core.explicit diff --git a/handler/openid/flow_explicit_auth_test.go b/handler/openid/flow_explicit_auth_test.go index 9f082588..65733b0b 100644 --- a/handler/openid/flow_explicit_auth_test.go +++ b/handler/openid/flow_explicit_auth_test.go @@ -1,14 +1,30 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import ( "testing" + "fmt" + "github.com/golang/mock/gomock" "github.com/ory/fosite" "github.com/ory/fosite/internal" "github.com/ory/fosite/token/jwt" "github.com/pkg/errors" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var j = &DefaultStrategy{ @@ -76,9 +92,15 @@ func TestExplicit_HandleAuthorizeEndpointRequest(t *testing.T) { }, }, } { - c.setup() - err := h.HandleAuthorizeEndpointRequest(nil, areq, aresp) - assert.True(t, errors.Cause(err) == c.expectErr, "(%d) %s\n%s\n%s", k, c.description, err, c.expectErr) - t.Logf("Passed test case %d", k) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + c.setup() + err := h.HandleAuthorizeEndpointRequest(nil, areq, aresp) + + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + } + }) } } diff --git a/handler/openid/flow_explicit_token.go b/handler/openid/flow_explicit_token.go index fad5c270..9aee4145 100644 --- a/handler/openid/flow_explicit_token.go +++ b/handler/openid/flow_explicit_token.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import ( @@ -8,7 +22,7 @@ import ( ) func (c *OpenIDConnectExplicitHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error { - return fosite.ErrUnknownRequest + return errors.WithStack(fosite.ErrUnknownRequest) } func (c *OpenIDConnectExplicitHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error { @@ -18,21 +32,21 @@ func (c *OpenIDConnectExplicitHandler) PopulateTokenEndpointResponse(ctx context authorize, err := c.OpenIDConnectRequestStorage.GetOpenIDConnectSession(ctx, requester.GetRequestForm().Get("code"), requester) if errors.Cause(err) == ErrNoSessionFound { - return errors.Wrap(fosite.ErrUnknownRequest, err.Error()) + return errors.WithStack(fosite.ErrUnknownRequest.WithDebug(err.Error())) } else if err != nil { - return errors.Wrap(fosite.ErrServerError, err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } if !authorize.GetGrantedScopes().Has("openid") { - return errors.Wrap(fosite.ErrMisconfiguration, "The an openid connect session was found but the openid scope is missing in it") + return errors.WithStack(fosite.ErrMisconfiguration.WithDebug("The an openid connect session was found but the openid scope is missing in it")) } if !requester.GetClient().GetGrantTypes().Has("authorization_code") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use the authorization_code grant type") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use the authorization_code grant type")) } if !requester.GetClient().GetResponseTypes().Has("id_token") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use response type id_token") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use response type id_token")) } return c.IssueExplicitIDToken(ctx, authorize, responder) diff --git a/handler/openid/flow_explicit_token_test.go b/handler/openid/flow_explicit_token_test.go index 6950716e..79ad52ed 100644 --- a/handler/openid/flow_explicit_token_test.go +++ b/handler/openid/flow_explicit_token_test.go @@ -1,14 +1,31 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import ( "testing" + "fmt" + "github.com/golang/mock/gomock" "github.com/ory/fosite" "github.com/ory/fosite/internal" "github.com/ory/fosite/token/jwt" "github.com/pkg/errors" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestHandleTokenEndpointRequest(t *testing.T) { @@ -17,7 +34,7 @@ func TestHandleTokenEndpointRequest(t *testing.T) { areq.Client = &fosite.DefaultClient{ ResponseTypes: fosite.Arguments{"id_token"}, } - assert.True(t, errors.Cause(h.HandleTokenEndpointRequest(nil, areq)) == fosite.ErrUnknownRequest) + assert.EqualError(t, h.HandleTokenEndpointRequest(nil, areq), fosite.ErrUnknownRequest.Error()) } func TestExplicit_PopulateTokenEndpointResponse(t *testing.T) { @@ -90,9 +107,15 @@ func TestExplicit_PopulateTokenEndpointResponse(t *testing.T) { }, }, } { - c.setup() - err := h.PopulateTokenEndpointResponse(nil, areq, aresp) - assert.True(t, errors.Cause(err) == c.expectErr, "(%d) %s\n%s\n%s", k, c.description, err, c.expectErr) - t.Logf("Passed test case %d", k) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + c.setup() + err := h.PopulateTokenEndpointResponse(nil, areq, aresp) + + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + } + }) } } diff --git a/handler/openid/flow_hybrid.go b/handler/openid/flow_hybrid.go index c8ca817f..cb858692 100644 --- a/handler/openid/flow_hybrid.go +++ b/handler/openid/flow_hybrid.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import ( @@ -31,11 +45,11 @@ func (c *OpenIDConnectHybridHandler) HandleAuthorizeEndpointRequest(ctx context. } if ar.GetResponseTypes().Matches("token") && !ar.GetClient().GetResponseTypes().Has("token") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use the token response type") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use the token response type")) } else if ar.GetResponseTypes().Matches("code") && !ar.GetClient().GetResponseTypes().Has("code") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use the code response type") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use the code response type")) } else if ar.GetResponseTypes().Matches("id_token") && !ar.GetClient().GetResponseTypes().Has("id_token") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use the id_token response type") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use the id_token response type")) } sess, ok := ar.GetSession().(Session) @@ -46,21 +60,21 @@ func (c *OpenIDConnectHybridHandler) HandleAuthorizeEndpointRequest(ctx context. client := ar.GetClient() for _, scope := range ar.GetRequestedScopes() { if !c.ScopeStrategy(client.GetScopes(), scope) { - return errors.Wrap(fosite.ErrInvalidScope, fmt.Sprintf("The client is not allowed to request scope %s", scope)) + return errors.WithStack(fosite.ErrInvalidScope.WithDebug(fmt.Sprintf("The client is not allowed to request scope %s", scope))) } } claims := sess.IDTokenClaims() if ar.GetResponseTypes().Has("code") { if !ar.GetClient().GetGrantTypes().Has("authorization_code") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use the authorization_code grant type") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use the authorization_code grant type")) } code, signature, err := c.AuthorizeExplicitGrantHandler.AuthorizeCodeStrategy.GenerateAuthorizeCode(ctx, ar) if err != nil { - return errors.Wrap(fosite.ErrServerError, err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } else if err := c.AuthorizeExplicitGrantHandler.CoreStorage.CreateAuthorizeCodeSession(ctx, signature, ar); err != nil { - return errors.Wrap(fosite.ErrServerError, err.Error()) + return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error())) } resp.AddFragment("code", code) @@ -75,9 +89,9 @@ func (c *OpenIDConnectHybridHandler) HandleAuthorizeEndpointRequest(ctx context. if ar.GetResponseTypes().Has("token") { if !ar.GetClient().GetGrantTypes().Has("implicit") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use the implicit grant type") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use the implicit grant type")) } else if err := c.AuthorizeImplicitGrantTypeHandler.IssueImplicitAccessToken(ctx, ar, resp); err != nil { - return errors.Wrap(err, err.Error()) + return errors.WithStack(err) } ar.SetResponseTypeHandled("token") @@ -98,7 +112,7 @@ func (c *OpenIDConnectHybridHandler) HandleAuthorizeEndpointRequest(ctx context. } if err := c.IDTokenHandleHelper.IssueImplicitIDToken(ctx, ar, resp); err != nil { - return errors.Wrap(err, err.Error()) + return errors.WithStack(err) } ar.SetResponseTypeHandled("id_token") diff --git a/handler/openid/flow_hybrid_test.go b/handler/openid/flow_hybrid_test.go index a78c9e7d..c5350a76 100644 --- a/handler/openid/flow_hybrid_test.go +++ b/handler/openid/flow_hybrid_test.go @@ -1,9 +1,25 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import ( "testing" "time" + "fmt" + "github.com/golang/mock/gomock" "github.com/ory/fosite" "github.com/ory/fosite/handler/oauth2" @@ -11,8 +27,8 @@ import ( "github.com/ory/fosite/storage" "github.com/ory/fosite/token/hmac" "github.com/ory/fosite/token/jwt" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var idStrategy = &DefaultStrategy{ @@ -23,7 +39,7 @@ var idStrategy = &DefaultStrategy{ var hmacStrategy = &oauth2.HMACSHAStrategy{ Enigma: &hmac.HMACStrategy{ - GlobalSecret: []byte("some-super-cool-secret-that-nobody-knows"), + GlobalSecret: []byte("some-super-cool-secret-that-nobody-knows-nobody-knows"), }, } @@ -152,18 +168,19 @@ func TestHybrid_HandleAuthorizeEndpointRequest(t *testing.T) { }, }, } { - c.setup() - err := h.HandleAuthorizeEndpointRequest(nil, areq, aresp) - condition := errors.Cause(err) == c.expectErr - assert.True(t, condition, "(%d) %s\n%s\n%s", k, c.description, err, c.expectErr) - if condition { - t.Logf("Passed test case %d", k) - } else { - t.Logf("Failed test case %d", k) - } + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + c.setup() + err := h.HandleAuthorizeEndpointRequest(nil, areq, aresp) + + if c.expectErr != nil { + require.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + } - if c.check != nil { - c.check() - } + if c.check != nil { + c.check() + } + }) } } diff --git a/handler/openid/flow_implicit.go b/handler/openid/flow_implicit.go index 1f460b3f..2fe9ca72 100644 --- a/handler/openid/flow_implicit.go +++ b/handler/openid/flow_implicit.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import ( @@ -29,19 +43,19 @@ func (c *OpenIDConnectImplicitHandler) HandleAuthorizeEndpointRequest(ctx contex } if !ar.GetClient().GetGrantTypes().Has("implicit") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use implicit grant type") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use implicit grant type")) } if ar.GetResponseTypes().Exact("id_token") && !ar.GetClient().GetResponseTypes().Has("id_token") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use response type id_token") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use response type id_token")) } else if ar.GetResponseTypes().Matches("token", "id_token") && !ar.GetClient().GetResponseTypes().Has("token", "id_token") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use response type token and id_token") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use response type token and id_token")) } client := ar.GetClient() for _, scope := range ar.GetRequestedScopes() { if !c.ScopeStrategy(client.GetScopes(), scope) { - return errors.Wrap(fosite.ErrInvalidScope, fmt.Sprintf("The client is not allowed to request scope %s", scope)) + return errors.WithStack(fosite.ErrInvalidScope.WithDebug(fmt.Sprintf("The client is not allowed to request scope %s", scope))) } } @@ -53,7 +67,7 @@ func (c *OpenIDConnectImplicitHandler) HandleAuthorizeEndpointRequest(ctx contex claims := sess.IDTokenClaims() if ar.GetResponseTypes().Has("token") { if err := c.AuthorizeImplicitGrantTypeHandler.IssueImplicitAccessToken(ctx, ar, resp); err != nil { - return errors.Wrap(err, err.Error()) + return errors.WithStack(err) } ar.SetResponseTypeHandled("token") @@ -68,7 +82,7 @@ func (c *OpenIDConnectImplicitHandler) HandleAuthorizeEndpointRequest(ctx contex } if err := c.IssueImplicitIDToken(ctx, ar, resp); err != nil { - return errors.Wrap(err, err.Error()) + return errors.WithStack(err) } // there is no need to check for https, because implicit flow does not require https diff --git a/handler/openid/flow_implicit_test.go b/handler/openid/flow_implicit_test.go index b710d62a..0395185f 100644 --- a/handler/openid/flow_implicit_test.go +++ b/handler/openid/flow_implicit_test.go @@ -1,15 +1,30 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import ( "testing" "time" + "fmt" + "github.com/golang/mock/gomock" "github.com/ory/fosite" "github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/storage" "github.com/ory/fosite/token/jwt" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) @@ -148,12 +163,18 @@ func TestImplicit_HandleAuthorizeEndpointRequest(t *testing.T) { }, }, } { - c.setup() - err := h.HandleAuthorizeEndpointRequest(nil, areq, aresp) - assert.True(t, errors.Cause(err) == c.expectErr, "(%d) %s\n%s\n%s", k, c.description, err, c.expectErr) - t.Logf("Passed test case %d", k) - if c.check != nil { - c.check() - } + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + c.setup() + err := h.HandleAuthorizeEndpointRequest(nil, areq, aresp) + + if c.expectErr != nil { + assert.EqualError(t, err, c.expectErr.Error()) + } else { + assert.NoError(t, err) + if c.check != nil { + c.check() + } + } + }) } } diff --git a/handler/openid/flow_refresh_token.go b/handler/openid/flow_refresh_token.go index f7020d30..02b5fd38 100644 --- a/handler/openid/flow_refresh_token.go +++ b/handler/openid/flow_refresh_token.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import ( @@ -22,11 +36,11 @@ func (c *OpenIDConnectRefreshHandler) HandleTokenEndpointRequest(ctx context.Con } if !request.GetClient().GetGrantTypes().Has("refresh_token") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use the authorization_code grant type") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use the authorization_code grant type")) } if !request.GetClient().GetResponseTypes().Has("id_token") { - return errors.Wrap(fosite.ErrUnknownRequest, "The client is not allowed to use response type id_token") + return errors.WithStack(fosite.ErrUnknownRequest.WithDebug("The client is not allowed to use response type id_token")) } sess, ok := request.GetSession().(Session) @@ -49,11 +63,11 @@ func (c *OpenIDConnectRefreshHandler) PopulateTokenEndpointResponse(ctx context. } if !requester.GetClient().GetGrantTypes().Has("refresh_token") { - return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use the authorization_code grant type") + return errors.WithStack(fosite.ErrInvalidGrant.WithDebug("The client is not allowed to use the authorization_code grant type")) } if !requester.GetClient().GetResponseTypes().Has("id_token") { - return errors.Wrap(errors.WithStack(fosite.ErrUnknownRequest), "The client is not allowed to use response type id_token") + return errors.WithStack(fosite.ErrUnknownRequest.WithDebug("The client is not allowed to use response type id_token")) } return c.IssueExplicitIDToken(ctx, requester, responder) diff --git a/handler/openid/flow_refresh_token_test.go b/handler/openid/flow_refresh_token_test.go index e8a00b0e..09d96d33 100644 --- a/handler/openid/flow_refresh_token_test.go +++ b/handler/openid/flow_refresh_token_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import ( diff --git a/handler/openid/helper.go b/handler/openid/helper.go index ceaf10d2..a873585d 100644 --- a/handler/openid/helper.go +++ b/handler/openid/helper.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import ( diff --git a/handler/openid/helper_test.go b/handler/openid/helper_test.go index a14e49d7..8740ba47 100644 --- a/handler/openid/helper_test.go +++ b/handler/openid/helper_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import ( @@ -80,7 +94,7 @@ func TestIssueExplicitToken(t *testing.T) { resp.EXPECT().SetExtra("id_token", gomock.Any()) h := &IDTokenHandleHelper{IDTokenStrategy: strat} err := h.IssueExplicitIDToken(nil, ar, resp) - assert.Nil(t, err, "%s", err) + assert.NoError(t, err) } func TestIssueImplicitToken(t *testing.T) { @@ -97,5 +111,5 @@ func TestIssueImplicitToken(t *testing.T) { resp.EXPECT().AddFragment("id_token", gomock.Any()) h := &IDTokenHandleHelper{IDTokenStrategy: strat} err := h.IssueImplicitIDToken(nil, ar, resp) - assert.Nil(t, err, "%s", err) + assert.NoError(t, err) } diff --git a/handler/openid/storage.go b/handler/openid/storage.go index 515b524c..4d2711cd 100644 --- a/handler/openid/storage.go +++ b/handler/openid/storage.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import ( diff --git a/handler/openid/strategy.go b/handler/openid/strategy.go index ded8b5a7..e86ced0a 100644 --- a/handler/openid/strategy.go +++ b/handler/openid/strategy.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import ( diff --git a/handler/openid/strategy_jwt.go b/handler/openid/strategy_jwt.go index 86490d9b..4f0806ea 100644 --- a/handler/openid/strategy_jwt.go +++ b/handler/openid/strategy_jwt.go @@ -1,9 +1,27 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import ( "context" "time" + "fmt" + "strconv" + + jwtgo "github.com/dgrijalva/jwt-go" "github.com/mohae/deepcopy" "github.com/ory/fosite" "github.com/ory/fosite/token/jwt" @@ -31,7 +49,9 @@ type DefaultSession struct { func NewDefaultSession() *DefaultSession { return &DefaultSession{ - Claims: &jwt.IDTokenClaims{}, + Claims: &jwt.IDTokenClaims{ + RequestedAt: time.Now().UTC(), + }, Headers: &jwt.Headers{}, } } @@ -109,24 +129,76 @@ func (h DefaultStrategy) GenerateIDToken(_ context.Context, requester fosite.Req } claims := sess.IDTokenClaims() - if requester.GetRequestForm().Get("max_age") != "" && (claims.AuthTime.IsZero() || claims.AuthTime.After(time.Now())) { - return "", errors.New("Failed to generate id token because authentication time claim is required when max_age is set and can not be in the future") - } - if claims.Subject == "" { return "", errors.New("Failed to generate id token because subject is an empty string") } + if requester.GetRequestForm().Get("grant_type") != "refresh_token" { + maxAge, err := strconv.ParseInt(requester.GetRequestForm().Get("max_age"), 10, 64) + if err != nil { + maxAge = 0 + } + + if maxAge > 0 { + if claims.AuthTime.IsZero() || claims.AuthTime.After(time.Now()) { + return "", errors.New("Failed to generate id token because authentication time claim is required when max_age is set and can not be in the future") + } else if claims.AuthTime.Add(time.Second * time.Duration(maxAge)).Before(time.Now()) { + return "", errors.WithStack(fosite.ErrLoginRequired.WithDebug("Failed to generate id token because authentication time does not satisfy max_age time")) + } + } + + prompt := requester.GetRequestForm().Get("prompt") + if prompt != "" { + if claims.AuthTime.IsZero() || claims.AuthTime.After(time.Now()) { + return "", errors.New("Unable to determine validity of prompt parameter because auth_time is missing in id token claims") + } + } + + switch prompt { + case "none": + if claims.AuthTime.After(claims.RequestedAt) { + return "", errors.WithStack(fosite.ErrLoginRequired.WithDebug("Failed to generate id token because prompt was set to \"none\" but auth_time happened after the authorization request was registered, indicating that the user was logged in during this request which is not allowed")) + } + break + case "login": + if claims.AuthTime.Before(claims.RequestedAt) { + return "", errors.WithStack(fosite.ErrLoginRequired.WithDebug("Failed to generate id token because prompt was set to \"login\" but auth_time happened before the authorization request was registered, indicating that the user was not re-authenticated which is forbidden")) + } + break + } + + // If acr_values was requested but no acr value was provided in the ID token, fall back to level 0 which means least + // confidence in authentication. + if requester.GetRequestForm().Get("acr_values") != "" && claims.AuthenticationContextClassReference == "" { + claims.AuthenticationContextClassReference = "0" + } + + if tokenHintString := requester.GetRequestForm().Get("id_token_hint"); tokenHintString != "" { + tokenHint, err := h.RS256JWTStrategy.Decode(tokenHintString) + if err != nil { + return "", errors.WithStack(fosite.ErrInvalidRequest.WithDebug(fmt.Sprintf("Unable to decode id token from id_token_hint parameter because %s", err.Error()))) + } + + if hintClaims, ok := tokenHint.Claims.(jwtgo.MapClaims); !ok { + return "", errors.WithStack(fosite.ErrInvalidRequest.WithDebug("Unable to decode id token from id_token_hint to *jwt.StandardClaims")) + } else if hintSub, _ := hintClaims["sub"].(string); hintSub == "" { + return "", errors.WithStack(fosite.ErrInvalidRequest.WithDebug("Provided id token from id_token_hint does not have a subject")) + } else if hintSub != claims.Subject { + return "", errors.WithStack(fosite.ErrLoginRequired.WithDebug(fmt.Sprintf("Subject from authorization mismatches id token subject from id_token_hint"))) + } + } + } + if claims.ExpiresAt.IsZero() { - claims.ExpiresAt = time.Now().Add(h.Expiry) + claims.ExpiresAt = time.Now().UTC().Add(h.Expiry) } - if claims.ExpiresAt.Before(time.Now()) { + if claims.ExpiresAt.Before(time.Now().UTC()) { return "", errors.New("Failed to generate id token because expiry claim can not be in the past") } if claims.AuthTime.IsZero() { - claims.AuthTime = time.Now() + claims.AuthTime = time.Now().UTC() } if claims.Issuer == "" { @@ -145,7 +217,7 @@ func (h DefaultStrategy) GenerateIDToken(_ context.Context, requester fosite.Req claims.Nonce = nonce claims.Audience = requester.GetClient().GetID() - claims.IssuedAt = time.Now() + claims.IssuedAt = time.Now().UTC() token, _, err = h.RS256JWTStrategy.Generate(claims.ToMapClaims(), sess.IDTokenHeaders()) return token, err diff --git a/handler/openid/strategy_jwt_test.go b/handler/openid/strategy_jwt_test.go index ecd0cef3..afe20809 100644 --- a/handler/openid/strategy_jwt_test.go +++ b/handler/openid/strategy_jwt_test.go @@ -1,9 +1,25 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package openid import ( "testing" "time" + "fmt" + "github.com/ory/fosite" "github.com/ory/fosite/token/jwt" "github.com/stretchr/testify/assert" @@ -12,8 +28,9 @@ import ( func TestJWTStrategy_GenerateIDToken(t *testing.T) { var req *fosite.AccessRequest for k, c := range []struct { - setup func() - expectErr bool + description string + setup func() + expectErr bool }{ { setup: func() { @@ -32,7 +49,7 @@ func TestJWTStrategy_GenerateIDToken(t *testing.T) { req = fosite.NewAccessRequest(&DefaultSession{ Claims: &jwt.IDTokenClaims{ Subject: "peter", - AuthTime: time.Now(), + AuthTime: time.Now().UTC(), }, Headers: &jwt.Headers{}, }) @@ -46,7 +63,7 @@ func TestJWTStrategy_GenerateIDToken(t *testing.T) { req = fosite.NewAccessRequest(&DefaultSession{ Claims: &jwt.IDTokenClaims{ Subject: "peter", - ExpiresAt: time.Now().Add(-time.Hour), + ExpiresAt: time.Now().UTC().Add(-time.Hour), }, Headers: &jwt.Headers{}, }) @@ -88,12 +105,157 @@ func TestJWTStrategy_GenerateIDToken(t *testing.T) { }, expectErr: false, }, + { + description: "should pass because max_age was requested and auth_time happened after initial request time", + setup: func() { + req = fosite.NewAccessRequest(&DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "peter", + AuthTime: time.Now().UTC(), + }, + Headers: &jwt.Headers{}, + }) + req.Form.Set("max_age", "60") + }, + expectErr: false, + }, + { + description: "should fail because max_age was requested and auth_time has expired", + setup: func() { + req = fosite.NewAccessRequest(&DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "peter", + AuthTime: time.Now().Add(-time.Hour).UTC(), + }, + Headers: &jwt.Headers{}, + }) + req.Form.Set("max_age", "60") + }, + expectErr: true, + }, + { + description: "should fail because prompt=none was requested and auth_time indicates fresh login", + setup: func() { + req = fosite.NewAccessRequest(&DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "peter", + AuthTime: time.Now().UTC(), + RequestedAt: time.Now().Add(-time.Minute), + }, + Headers: &jwt.Headers{}, + }) + req.Form.Set("prompt", "none") + }, + expectErr: true, + }, + { + description: "should pass because prompt=none was requested and auth_time indicates fresh login but grant type is refresh_token", + setup: func() { + req = fosite.NewAccessRequest(&DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "peter", + AuthTime: time.Now().UTC(), + RequestedAt: time.Now().Add(-time.Minute), + }, + Headers: &jwt.Headers{}, + }) + req.Form.Set("prompt", "none") + req.Form.Set("grant_type", "refresh_token") + }, + expectErr: false, + }, + { + description: "should pass because prompt=none was requested and auth_time indicates old login", + setup: func() { + req = fosite.NewAccessRequest(&DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "peter", + AuthTime: time.Now().Add(-time.Hour).UTC(), + RequestedAt: time.Now().Add(-time.Minute), + }, + Headers: &jwt.Headers{}, + }) + req.Form.Set("prompt", "none") + }, + expectErr: false, + }, + { + description: "should pass because prompt=login was requested and auth_time indicates fresh login", + setup: func() { + req = fosite.NewAccessRequest(&DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "peter", + AuthTime: time.Now().UTC(), + RequestedAt: time.Now().Add(-time.Minute), + }, + Headers: &jwt.Headers{}, + }) + req.Form.Set("prompt", "login") + }, + expectErr: false, + }, + { + description: "should fail because prompt=login was requested and auth_time indicates old login", + setup: func() { + req = fosite.NewAccessRequest(&DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "peter", + AuthTime: time.Now().Add(-time.Hour).UTC(), + RequestedAt: time.Now().Add(-time.Minute), + }, + Headers: &jwt.Headers{}, + }) + req.Form.Set("prompt", "login") + }, + expectErr: true, + }, + { + description: "should pass because id_token_hint subject matches subject from claims", + setup: func() { + req = fosite.NewAccessRequest(&DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "peter", + AuthTime: time.Now().Add(-time.Hour).UTC(), + RequestedAt: time.Now().Add(-time.Minute), + }, + Headers: &jwt.Headers{}, + }) + token, _ := j.GenerateIDToken(nil, fosite.NewAccessRequest(&DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "peter", + }, + Headers: &jwt.Headers{}, + })) + req.Form.Set("id_token_hint", token) + }, + expectErr: false, + }, + { + description: "should fail because id_token_hint subject does not match subject from claims", + setup: func() { + req = fosite.NewAccessRequest(&DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "peter", + AuthTime: time.Now().Add(-time.Hour).UTC(), + RequestedAt: time.Now().Add(-time.Minute), + }, + Headers: &jwt.Headers{}, + }) + token, _ := j.GenerateIDToken(nil, fosite.NewAccessRequest(&DefaultSession{ + Claims: &jwt.IDTokenClaims{Subject: "alice"}, Headers: &jwt.Headers{}, + })) + req.Form.Set("id_token_hint", token) + }, + expectErr: true, + }, } { - c.setup() - token, err := j.GenerateIDToken(nil, req) - assert.Equal(t, c.expectErr, err != nil, "%d: %s", k, err) - if !c.expectErr { - assert.NotEmpty(t, token) - } + t.Run(fmt.Sprintf("case=%d/description=%s", k, c.description), func(t *testing.T) { + c.setup() + token, err := j.GenerateIDToken(nil, req) + assert.Equal(t, c.expectErr, err != nil, "%d: %s", k, err) + if !c.expectErr { + assert.NotEmpty(t, token) + } + }) } } diff --git a/hash.go b/hash.go index 5321c3e9..d25a61c4 100644 --- a/hash.go +++ b/hash.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite // Hasher defines how a oauth2-compatible hasher should look like. diff --git a/hash_bcrypt.go b/hash_bcrypt.go index 5243c065..2b5396dc 100644 --- a/hash_bcrypt.go +++ b/hash_bcrypt.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( diff --git a/hash_bcrypt_test.go b/hash_bcrypt_test.go index 49faecef..0b488005 100644 --- a/hash_bcrypt_test.go +++ b/hash_bcrypt_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -13,7 +27,7 @@ func TestHash(t *testing.T) { } password := []byte("foo") hash, err := h.Hash(password) - assert.Nil(t, err) + assert.NoError(t, err) assert.NotNil(t, hash) assert.NotEqual(t, hash, password) } @@ -24,10 +38,10 @@ func TestCompareEquals(t *testing.T) { } password := []byte("foo") hash, err := h.Hash(password) - assert.Nil(t, err) + assert.NoError(t, err) assert.NotNil(t, hash) err = h.Compare(hash, password) - assert.Nil(t, err) + assert.NoError(t, err) } func TestCompareDifferent(t *testing.T) { @@ -36,8 +50,8 @@ func TestCompareDifferent(t *testing.T) { } password := []byte("foo") hash, err := h.Hash(password) - assert.Nil(t, err) + assert.NoError(t, err) assert.NotNil(t, hash) err = h.Compare(hash, []byte(uuid.NewRandom())) - assert.NotNil(t, err) + assert.Error(t, err) } diff --git a/helper.go b/helper.go index 62e5a673..4e640dd6 100644 --- a/helper.go +++ b/helper.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( diff --git a/helper_test.go b/helper_test.go index 97c98b10..c3093c2d 100644 --- a/helper_test.go +++ b/helper_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( diff --git a/integration/authorize_code_grant_test.go b/integration/authorize_code_grant_test.go index 14d6ac9a..9d2348c3 100644 --- a/integration/authorize_code_grant_test.go +++ b/integration/authorize_code_grant_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package integration_test import ( @@ -5,6 +19,8 @@ import ( "net/http" + "fmt" + "github.com/ory/fosite" "github.com/ory/fosite/compose" "github.com/ory/fosite/handler/oauth2" @@ -44,22 +60,23 @@ func runAuthorizeCodeGrantTest(t *testing.T, strategy interface{}) { authStatusCode: http.StatusOK, }, } { - c.setup() + t.Run(fmt.Sprintf("case=%d/description=%s", k, c.description), func(t *testing.T) { + c.setup() - resp, err := http.Get(oauthClient.AuthCodeURL(state)) - require.Nil(t, err) - require.Equal(t, c.authStatusCode, resp.StatusCode, "(%d) %s", k, c.description) + resp, err := http.Get(oauthClient.AuthCodeURL(state)) + require.NoError(t, err) + require.Equal(t, c.authStatusCode, resp.StatusCode) - if resp.StatusCode == http.StatusOK { - token, err := oauthClient.Exchange(goauth.NoContext, resp.Request.URL.Query().Get("code")) - require.Nil(t, err, "(%d) %s", k, c.description) - require.NotEmpty(t, token.AccessToken, "(%d) %s", k, c.description) + if resp.StatusCode == http.StatusOK { + token, err := oauthClient.Exchange(goauth.NoContext, resp.Request.URL.Query().Get("code")) + require.NoError(t, err) + require.NotEmpty(t, token.AccessToken) - httpClient := oauthClient.Client(goauth.NoContext, token) - resp, err := httpClient.Get(ts.URL + "/info") - require.Nil(t, err, "(%d) %s", k, c.description) - assert.Equal(t, http.StatusNoContent, resp.StatusCode, "(%d) %s", k, c.description) - } - t.Logf("Passed test case (%d) %s", k, c.description) + httpClient := oauthClient.Client(goauth.NoContext, token) + resp, err := httpClient.Get(ts.URL + "/info") + require.NoError(t, err) + assert.Equal(t, http.StatusNoContent, resp.StatusCode) + } + }) } } diff --git a/integration/authorize_implicit_grant_test.go b/integration/authorize_implicit_grant_test.go index fb6adf08..19bbd40b 100644 --- a/integration/authorize_implicit_grant_test.go +++ b/integration/authorize_implicit_grant_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package integration_test import ( @@ -8,6 +22,8 @@ import ( "testing" "time" + "fmt" + "github.com/ory/fosite" "github.com/ory/fosite/compose" "github.com/ory/fosite/handler/oauth2" @@ -47,36 +63,37 @@ func runTestAuthorizeImplicitGrant(t *testing.T, strategy interface{}) { authStatusCode: http.StatusOK, }, } { - c.setup() - - var callbackURL *url.URL - authURL := strings.Replace(oauthClient.AuthCodeURL(state), "response_type=code", "response_type=token", -1) - client := &http.Client{ - CheckRedirect: func(req *http.Request, via []*http.Request) error { - callbackURL = req.URL - return errors.New("Dont follow redirects") - }, - } - resp, err := client.Get(authURL) - require.NotNil(t, err) + t.Run(fmt.Sprintf("case=%d/description=%s", k, c.description), func(t *testing.T) { + c.setup() - if resp.StatusCode == http.StatusOK { - fragment, err := url.ParseQuery(callbackURL.Fragment) - require.Nil(t, err) - expires, err := strconv.Atoi(fragment.Get("expires_in")) - require.Nil(t, err) - token := &goauth.Token{ - AccessToken: fragment.Get("access_token"), - TokenType: fragment.Get("token_type"), - RefreshToken: fragment.Get("refresh_token"), - Expiry: time.Now().Add(time.Duration(expires) * time.Second), + var callbackURL *url.URL + authURL := strings.Replace(oauthClient.AuthCodeURL(state), "response_type=code", "response_type=token", -1) + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + callbackURL = req.URL + return errors.New("Dont follow redirects") + }, } + resp, err := client.Get(authURL) + require.Error(t, err) + + if resp.StatusCode == http.StatusOK { + fragment, err := url.ParseQuery(callbackURL.Fragment) + require.NoError(t, err) + expires, err := strconv.Atoi(fragment.Get("expires_in")) + require.NoError(t, err) + token := &goauth.Token{ + AccessToken: fragment.Get("access_token"), + TokenType: fragment.Get("token_type"), + RefreshToken: fragment.Get("refresh_token"), + Expiry: time.Now().UTC().Add(time.Duration(expires) * time.Second), + } - httpClient := oauthClient.Client(goauth.NoContext, token) - resp, err := httpClient.Get(ts.URL + "/info") - require.Nil(t, err, "(%d) %s", k, c.description) - assert.Equal(t, http.StatusNoContent, resp.StatusCode, "(%d) %s", k, c.description) - } - t.Logf("Passed test case (%d) %s", k, c.description) + httpClient := oauthClient.Client(goauth.NoContext, token) + resp, err := httpClient.Get(ts.URL + "/info") + require.NoError(t, err) + assert.Equal(t, http.StatusNoContent, resp.StatusCode) + } + }) } } diff --git a/integration/client_credentials_grant_test.go b/integration/client_credentials_grant_test.go index c9bae2ed..9a90b0bf 100644 --- a/integration/client_credentials_grant_test.go +++ b/integration/client_credentials_grant_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package integration_test import ( diff --git a/integration/helper_endpoints_test.go b/integration/helper_endpoints_test.go index ded3aa52..4600eb08 100644 --- a/integration/helper_endpoints_test.go +++ b/integration/helper_endpoints_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package integration_test import ( @@ -20,7 +34,7 @@ func tokenRevocationHandler(t *testing.T, oauth2 fosite.OAuth2Provider, session err := oauth2.NewRevocationRequest(ctx, req) if err != nil { t.Logf("Revoke request failed because %s.", err.Error()) - t.Logf("Stack: %v", err.(stackTracer).StackTrace()) + // t.Logf("Stack: %v", err.(stackTracer).StackTrace()) } oauth2.WriteRevocationResponse(rw, err) } @@ -32,7 +46,7 @@ func tokenIntrospectionHandler(t *testing.T, oauth2 fosite.OAuth2Provider, sessi ar, err := oauth2.NewIntrospectionRequest(ctx, req, session) if err != nil { t.Logf("Introspection request failed because %s.", err.Error()) - t.Logf("Stack: %s", err.(stackTracer).StackTrace()) + // t.Logf("Stack: %s", err.(stackTracer).StackTrace()) oauth2.WriteIntrospectionError(rw, err) return } @@ -45,10 +59,9 @@ func tokenInfoHandler(t *testing.T, oauth2 fosite.OAuth2Provider, session fosite return func(rw http.ResponseWriter, req *http.Request) { ctx := fosite.NewContext() if _, err := oauth2.IntrospectToken(ctx, fosite.AccessTokenFromRequest(req), fosite.AccessToken, session); err != nil { - rfcerr := fosite.ErrorToRFC6749Error(err) t.Logf("Info request failed because `%s`.", err.Error()) - t.Logf("Stack: %s", err.(stackTracer).StackTrace()) - http.Error(rw, rfcerr.Description, rfcerr.Code) + // t.Logf("Stack: %s", err.(stackTracer).StackTrace()) + http.Error(rw, errors.Cause(err).(*fosite.RFC6749Error).Description, errors.Cause(err).(*fosite.RFC6749Error).Code) return } @@ -64,7 +77,7 @@ func authEndpointHandler(t *testing.T, oauth2 fosite.OAuth2Provider, session fos if err != nil { t.Logf("Access request failed because %s.", err.Error()) t.Logf("Request: %s.", ar) - t.Logf("Stack: %s.", err.(stackTracer).StackTrace()) + // t.Logf("Stack: %s.", err.(stackTracer).StackTrace()) oauth2.WriteAuthorizeError(rw, ar, err) return } @@ -86,7 +99,7 @@ func authEndpointHandler(t *testing.T, oauth2 fosite.OAuth2Provider, session fos if err != nil { t.Logf("Access request failed because %s.", err.Error()) t.Logf("Request: %s.", ar) - t.Logf("Stack: %s.", err.(stackTracer).StackTrace()) + // t.Logf("Stack: %s.", err.(stackTracer).StackTrace()) oauth2.WriteAuthorizeError(rw, ar, err) return } @@ -123,7 +136,7 @@ func tokenEndpointHandler(t *testing.T, provider fosite.OAuth2Provider) func(rw if err != nil { t.Logf("Access request failed because %s.", err.Error()) t.Logf("Request: %s.", accessRequest) - t.Logf("Stack: %v.", err.(stackTracer).StackTrace()) + // t.Logf("Stack: %v.", err.(stackTracer).StackTrace()) provider.WriteAccessError(rw, accessRequest, err) return } @@ -136,7 +149,7 @@ func tokenEndpointHandler(t *testing.T, provider fosite.OAuth2Provider) func(rw if err != nil { t.Logf("Access request failed because %s.", err.Error()) t.Logf("Request: %s.", accessRequest) - t.Logf("Stack: %v.", err.(stackTracer).StackTrace()) + // t.Logf("Stack: %v.", err.(stackTracer).StackTrace()) provider.WriteAccessError(rw, accessRequest, err) return } diff --git a/integration/helper_setup_test.go b/integration/helper_setup_test.go index 741b1f09..d31bd4c9 100644 --- a/integration/helper_setup_test.go +++ b/integration/helper_setup_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package integration_test import ( diff --git a/integration/introspect_token_test.go b/integration/introspect_token_test.go index 266f106d..f96db5e2 100644 --- a/integration/introspect_token_test.go +++ b/integration/introspect_token_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package integration_test import ( @@ -5,6 +19,8 @@ import ( "encoding/json" + "fmt" + "github.com/ory/fosite" "github.com/ory/fosite/compose" "github.com/ory/fosite/handler/oauth2" @@ -48,11 +64,11 @@ func runIntrospectTokenTest(t *testing.T, strategy oauth2.AccessTokenStrategy, i oauthClient := newOAuth2AppClient(ts) a, err := oauthClient.Token(goauth.NoContext) - require.Nil(t, err) + require.NoError(t, err) b, err := oauthClient.Token(goauth.NoContext) - require.Nil(t, err) + require.NoError(t, err) - for _, c := range []struct { + for k, c := range []struct { prepare func(*gorequest.SuperAgent) *gorequest.SuperAgent isActive bool scopes string @@ -93,28 +109,31 @@ func runIntrospectTokenTest(t *testing.T, strategy oauth2.AccessTokenStrategy, i scopes: "", }, } { - res := struct { - Active bool `json:"active"` - ClientId string `json:"client_id"` - Scope string `json:"scope"` - ExpiresAt float64 `json:"exp"` - IssuedAt float64 `json:"iat"` - }{} - s := gorequest.New() - s = s.Post(ts.URL + "/introspect"). - Type("form"). - SendStruct(map[string]string{"token": b.AccessToken, "scope": c.scopes}) - _, bytes, errs := c.prepare(s).End() + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + res := struct { + Active bool `json:"active"` + ClientId string `json:"client_id"` + Scope string `json:"scope"` + ExpiresAt float64 `json:"exp"` + IssuedAt float64 `json:"iat"` + }{} + s := gorequest.New() + s = s.Post(ts.URL + "/introspect"). + Type("form"). + SendStruct(map[string]string{"token": b.AccessToken, "scope": c.scopes}) + _, bytes, errs := c.prepare(s).End() + + assert.Nil(t, json.Unmarshal([]byte(bytes), &res)) + t.Logf("Got answer: %s", bytes) - assert.Nil(t, json.Unmarshal([]byte(bytes), &res)) - t.Logf("Got answer: %s", bytes) - assert.Len(t, errs, 0) - assert.Equal(t, c.isActive, res.Active) - if c.isActive { - assert.Equal(t, "fosite", res.Scope) - assert.True(t, res.ExpiresAt > 0) - assert.True(t, res.IssuedAt > 0) - assert.True(t, res.IssuedAt < res.ExpiresAt) - } + assert.Len(t, errs, 0) + assert.Equal(t, c.isActive, res.Active) + if c.isActive { + assert.Equal(t, "fosite", res.Scope) + assert.True(t, res.ExpiresAt > 0) + assert.True(t, res.IssuedAt > 0) + assert.True(t, res.IssuedAt < res.ExpiresAt) + } + }) } } diff --git a/integration/oidc_explicit_test.go b/integration/oidc_explicit_test.go index 1baaf7b1..70fed210 100644 --- a/integration/oidc_explicit_test.go +++ b/integration/oidc_explicit_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package integration_test import ( @@ -6,58 +20,145 @@ import ( "fmt" + "io/ioutil" + "strings" + "time" + "github.com/ory/fosite/compose" "github.com/ory/fosite/handler/openid" "github.com/ory/fosite/internal" "github.com/ory/fosite/token/jwt" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" ) -func TestOpenIDConnectExplicitFlow(t *testing.T) { - session := &defaultSession{ +func newIDSession(j *jwt.IDTokenClaims) *defaultSession { + return &defaultSession{ DefaultSession: &openid.DefaultSession{ - Claims: &jwt.IDTokenClaims{ - Subject: "peter", - }, + Claims: j, Headers: &jwt.Headers{}, }, } - f := compose.ComposeAllEnabled(new(compose.Config), fositeStore, []byte("some-secret-thats-random"), internal.MustRSAKey()) - ts := mockServer(t, f, session) +} - defer ts.Close() - oauthClient := newOAuth2Client(ts) - fositeStore.Clients["my-client"].RedirectURIs[0] = ts.URL + "/callback" +func TestOpenIDConnectExplicitFlow(t *testing.T) { + f := compose.ComposeAllEnabled(new(compose.Config), fositeStore, []byte("some-secret-thats-random-some-secret-thats-random-"), internal.MustRSAKey()) - var state string for k, c := range []struct { description string - setup func() + setup func(oauthClient *oauth2.Config) string authStatusCode int + authCodeURL string + session *defaultSession + expectAuthErr string + expectTokenErr string }{ { + session: newIDSession(&jwt.IDTokenClaims{Subject: "peter"}), + description: "should pass", + setup: func(oauthClient *oauth2.Config) string { + oauthClient.Scopes = []string{"openid"} + return oauthClient.AuthCodeURL("12345678901234567890") + "&nonce=11234123" + }, + authStatusCode: http.StatusOK, + }, + { + session: newIDSession(&jwt.IDTokenClaims{Subject: "peter"}), + description: "should fail because nonce is not long enough", + setup: func(oauthClient *oauth2.Config) string { + oauthClient.Scopes = []string{"openid"} + return oauthClient.AuthCodeURL("12345678901234567890") + "&nonce=1" + }, + authStatusCode: http.StatusOK, + expectTokenErr: "insufficient_entropy", + }, + { + session: newIDSession(&jwt.IDTokenClaims{Subject: "peter"}), + description: "should fail because state is not long enough", + setup: func(oauthClient *oauth2.Config) string { + oauthClient.Scopes = []string{"openid"} + return oauthClient.AuthCodeURL("123") + "&nonce=1234567890" + }, + expectAuthErr: "invalid_state", + authStatusCode: http.StatusNotAcceptable, // code from internal test callback handler when error occurs + }, + { + session: newIDSession(&jwt.IDTokenClaims{ + Subject: "peter", + RequestedAt: time.Now().UTC(), + AuthTime: time.Now().Add(time.Second).UTC(), + }), description: "should pass", - setup: func() { - state = "12345678901234567890" + setup: func(oauthClient *oauth2.Config) string { + oauthClient.Scopes = []string{"openid"} + return oauthClient.AuthCodeURL("12345678901234567890") + "&nonce=1234567890&prompt=login" + }, + authStatusCode: http.StatusOK, + }, + { + session: newIDSession(&jwt.IDTokenClaims{ + Subject: "peter", + RequestedAt: time.Now().UTC(), + AuthTime: time.Now().Add(-time.Minute).UTC(), + }), + description: "should fail because authentication was in the past", + setup: func(oauthClient *oauth2.Config) string { oauthClient.Scopes = []string{"openid"} + return oauthClient.AuthCodeURL("12345678901234567890") + "&nonce=1234567890&prompt=login" + }, + authStatusCode: http.StatusOK, + expectTokenErr: "login_required", + }, + { + session: newIDSession(&jwt.IDTokenClaims{ + Subject: "peter", + RequestedAt: time.Now().UTC(), + AuthTime: time.Now().Add(-time.Minute).UTC(), + }), + description: "should pass because authorization was in the past and no login was required", + setup: func(oauthClient *oauth2.Config) string { + oauthClient.Scopes = []string{"openid"} + return oauthClient.AuthCodeURL("12345678901234567890") + "&nonce=1234567890&prompt=none" }, authStatusCode: http.StatusOK, }, } { - c.setup() - - resp, err := http.Get(oauthClient.AuthCodeURL(state) + "&nonce=1234567890") - require.Nil(t, err) - require.Equal(t, c.authStatusCode, resp.StatusCode, "(%d) %s", k, c.description) - - if resp.StatusCode == http.StatusOK { - token, err := oauthClient.Exchange(oauth2.NoContext, resp.Request.URL.Query().Get("code")) - fmt.Printf("after exchange: %s\n\n", fositeStore.AuthorizeCodes) - require.Nil(t, err, "(%d) %s", k, c.description) - require.NotEmpty(t, token.AccessToken, "(%d) %s", k, c.description) - require.NotEmpty(t, token.Extra("id_token"), "(%d) %s", k, c.description) - } - t.Logf("Passed test case (%d) %s", k, c.description) + t.Run(fmt.Sprintf("case=%d/description=%s", k, c.description), func(t *testing.T) { + ts := mockServer(t, f, c.session) + defer ts.Close() + + oauthClient := newOAuth2Client(ts) + fositeStore.Clients["my-client"].RedirectURIs[0] = ts.URL + "/callback" + + resp, err := http.Get(c.setup(oauthClient)) + require.NoError(t, err) + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + require.Equal(t, c.authStatusCode, resp.StatusCode, "Got response: %s", body) + if resp.StatusCode >= 400 { + assert.Equal(t, c.expectAuthErr, strings.Replace(string(body), "error: ", "", 1)) + } + + if c.expectAuthErr != "" { + assert.Empty(t, resp.Request.URL.Query().Get("code")) + } + + if resp.StatusCode == http.StatusOK { + time.Sleep(time.Second) + + token, err := oauthClient.Exchange(oauth2.NoContext, resp.Request.URL.Query().Get("code")) + + if c.expectTokenErr != "" { + assert.Error(t, err) + assert.True(t, strings.Contains(err.Error(), c.expectTokenErr), err.Error()) + } else { + require.NoError(t, err) + assert.NotEmpty(t, token.AccessToken) + assert.NotEmpty(t, token.Extra("id_token")) + } + } + }) } } diff --git a/integration/oidc_implicit_hybrid_test.go b/integration/oidc_implicit_hybrid_test.go index f51c8ae6..ee861a79 100644 --- a/integration/oidc_implicit_hybrid_test.go +++ b/integration/oidc_implicit_hybrid_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package integration_test import ( @@ -13,6 +27,8 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/oauth2" + "fmt" + "github.com/ory/fosite/compose" "github.com/ory/fosite/handler/openid" "github.com/ory/fosite/internal" @@ -28,7 +44,7 @@ func TestOIDCImplicitFlow(t *testing.T) { Headers: &jwt.Headers{}, }, } - f := compose.ComposeAllEnabled(new(compose.Config), fositeStore, []byte("some-secret-thats-random"), internal.MustRSAKey()) + f := compose.ComposeAllEnabled(new(compose.Config), fositeStore, []byte("some-secret-thats-random-some-secret-thats-random-"), internal.MustRSAKey()) ts := mockServer(t, f, session) defer ts.Close() @@ -64,7 +80,6 @@ func TestOIDCImplicitFlow(t *testing.T) { hasToken: true, hasIdToken: true, }, - { responseType: "token%20id_token%20code", @@ -94,60 +109,61 @@ func TestOIDCImplicitFlow(t *testing.T) { hasIdToken: true, }, } { - c.setup() - - var callbackURL *url.URL - authURL := strings.Replace(oauthClient.AuthCodeURL(state), "response_type=code", "response_type="+c.responseType, -1) + "&nonce=" + c.nonce - client := &http.Client{ - CheckRedirect: func(req *http.Request, via []*http.Request) error { - callbackURL = req.URL - return errors.New("Dont follow redirects") - }, - } - resp, err := client.Get(authURL) - require.NotNil(t, err, "(%d) %s", k, c.description) - - t.Logf("Response (%d): %s", k, callbackURL.String()) - fragment, err := url.ParseQuery(callbackURL.Fragment) - require.Nil(t, err, "(%d) %s", k, c.description) - - if c.hasToken { - assert.NotEmpty(t, fragment.Get("access_token"), "(%d) %s", k, c.description) - } else { - assert.Empty(t, fragment.Get("access_token"), "(%d) %s", k, c.description) - } - - if c.hasCode { - assert.NotEmpty(t, fragment.Get("code"), "(%d) %s", k, c.description) - } else { - assert.Empty(t, fragment.Get("code"), "(%d) %s", k, c.description) - } - - if c.hasIdToken { - assert.NotEmpty(t, fragment.Get("id_token"), "(%d) %s", k, c.description) - } else { - assert.Empty(t, fragment.Get("id_token"), "(%d) %s", k, c.description) - } - - if !c.hasToken { - continue - } - - expires, err := strconv.Atoi(fragment.Get("expires_in")) - require.Nil(t, err, "(%d) %s", k, c.description) - - token := &oauth2.Token{ - AccessToken: fragment.Get("access_token"), - TokenType: fragment.Get("token_type"), - RefreshToken: fragment.Get("refresh_token"), - Expiry: time.Now().Add(time.Duration(expires) * time.Second), - } - - httpClient := oauthClient.Client(oauth2.NoContext, token) - resp, err = httpClient.Get(ts.URL + "/info") - require.Nil(t, err, "(%d) %s", k, c.description) - assert.Equal(t, http.StatusNoContent, resp.StatusCode, "(%d) %s", k, c.description) - t.Logf("Passed test case (%d) %s", k, c.description) - + t.Run(fmt.Sprintf("case=%d/description=%s", k, c.description), func(t *testing.T) { + c.setup() + + var callbackURL *url.URL + authURL := strings.Replace(oauthClient.AuthCodeURL(state), "response_type=code", "response_type="+c.responseType, -1) + "&nonce=" + c.nonce + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + callbackURL = req.URL + return errors.New("Dont follow redirects") + }, + } + resp, err := client.Get(authURL) + require.Error(t, err) + + t.Logf("Response (%d): %s", k, callbackURL.String()) + fragment, err := url.ParseQuery(callbackURL.Fragment) + require.NoError(t, err) + + if c.hasToken { + assert.NotEmpty(t, fragment.Get("access_token")) + } else { + assert.Empty(t, fragment.Get("access_token")) + } + + if c.hasCode { + assert.NotEmpty(t, fragment.Get("code")) + } else { + assert.Empty(t, fragment.Get("code")) + } + + if c.hasIdToken { + assert.NotEmpty(t, fragment.Get("id_token")) + } else { + assert.Empty(t, fragment.Get("id_token")) + } + + if !c.hasToken { + return + } + + expires, err := strconv.Atoi(fragment.Get("expires_in")) + require.NoError(t, err) + + token := &oauth2.Token{ + AccessToken: fragment.Get("access_token"), + TokenType: fragment.Get("token_type"), + RefreshToken: fragment.Get("refresh_token"), + Expiry: time.Now().UTC().Add(time.Duration(expires) * time.Second), + } + + httpClient := oauthClient.Client(oauth2.NoContext, token) + resp, err = httpClient.Get(ts.URL + "/info") + require.NoError(t, err) + assert.Equal(t, http.StatusNoContent, resp.StatusCode) + t.Logf("Passed test case (%d) %s", k, c.description) + }) } } diff --git a/integration/placeholder.go b/integration/placeholder.go index 76ab1b72..429eee64 100644 --- a/integration/placeholder.go +++ b/integration/placeholder.go @@ -1 +1,15 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package integration diff --git a/integration/refresh_token_grant_test.go b/integration/refresh_token_grant_test.go index 06a9e458..9c09a4d1 100644 --- a/integration/refresh_token_grant_test.go +++ b/integration/refresh_token_grant_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package integration_test import ( @@ -23,7 +37,7 @@ func TestRefreshTokenFlow(t *testing.T) { Headers: &jwt.Headers{}, }, } - f := compose.ComposeAllEnabled(new(compose.Config), fositeStore, []byte("some-secret-thats-random"), internal.MustRSAKey()) + f := compose.ComposeAllEnabled(new(compose.Config), fositeStore, []byte("some-secret-thats-random-some-secret-thats-random-"), internal.MustRSAKey()) ts := mockServer(t, f, session) defer ts.Close() @@ -92,7 +106,7 @@ func TestRefreshTokenFlow(t *testing.T) { require.NoError(t, err) c.check(token, refreshed) } else { - require.NotNil(t, err) + require.Error(t, err) } }) } diff --git a/integration/resource_owner_password_credentials_grant_test.go b/integration/resource_owner_password_credentials_grant_test.go index 6e98f14c..37a1e3f0 100644 --- a/integration/resource_owner_password_credentials_grant_test.go +++ b/integration/resource_owner_password_credentials_grant_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package integration_test import ( diff --git a/integration/revoke_token_test.go b/integration/revoke_token_test.go index 03980892..3b7eb89a 100644 --- a/integration/revoke_token_test.go +++ b/integration/revoke_token_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package integration_test import ( @@ -29,20 +43,20 @@ func runRevokeTokenTest(t *testing.T, strategy oauth2.AccessTokenStrategy) { oauthClient := newOAuth2AppClient(ts) token, err := oauthClient.Token(goauth.NoContext) - assert.Nil(t, err) + require.NoError(t, err) resp, _, errs := gorequest.New().Post(ts.URL+"/revoke"). SetBasicAuth(oauthClient.ClientID, oauthClient.ClientSecret). Type("form"). SendStruct(map[string]string{"token": "asdf"}).End() - assert.Len(t, errs, 0) + require.Len(t, errs, 0) assert.Equal(t, 200, resp.StatusCode) resp, _, errs = gorequest.New().Post(ts.URL+"/revoke"). SetBasicAuth(oauthClient.ClientID, oauthClient.ClientSecret). Type("form"). SendStruct(map[string]string{"token": token.AccessToken}).End() - assert.Len(t, errs, 0) + require.Len(t, errs, 0) assert.Equal(t, 200, resp.StatusCode) hres, _, errs := gorequest.New().Get(ts.URL+"/info"). diff --git a/internal/access_request.go b/internal/access_request.go index 136ab8f9..6a285802 100644 --- a/internal/access_request.go +++ b/internal/access_request.go @@ -136,6 +136,14 @@ func (_mr *_MockAccessRequesterRecorder) Merge(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Merge", arg0) } +func (_m *MockAccessRequester) SetID(_param0 string) { + _m.ctrl.Call(_m, "SetID", _param0) +} + +func (_mr *_MockAccessRequesterRecorder) SetID(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "SetID", arg0) +} + func (_m *MockAccessRequester) SetRequestedScopes(_param0 fosite.Arguments) { _m.ctrl.Call(_m, "SetRequestedScopes", _param0) } diff --git a/internal/authorize_request.go b/internal/authorize_request.go index a4393561..b2f9c1d9 100644 --- a/internal/authorize_request.go +++ b/internal/authorize_request.go @@ -176,6 +176,14 @@ func (_mr *_MockAuthorizeRequesterRecorder) Merge(arg0 interface{}) *gomock.Call return _mr.mock.ctrl.RecordCall(_mr.mock, "Merge", arg0) } +func (_m *MockAuthorizeRequester) SetID(_param0 string) { + _m.ctrl.Call(_m, "SetID", _param0) +} + +func (_mr *_MockAuthorizeRequesterRecorder) SetID(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "SetID", arg0) +} + func (_m *MockAuthorizeRequester) SetRequestedScopes(_param0 fosite.Arguments) { _m.ctrl.Call(_m, "SetRequestedScopes", _param0) } diff --git a/internal/key.go b/internal/key.go index 2bc3d5a0..743970f4 100644 --- a/internal/key.go +++ b/internal/key.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package internal import ( diff --git a/internal/oauth2_explicit_storage.go b/internal/oauth2_explicit_storage.go index 51e513cf..6356a7f7 100644 --- a/internal/oauth2_explicit_storage.go +++ b/internal/oauth2_explicit_storage.go @@ -1,6 +1,20 @@ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/ory/fosite/handler/oauth2 (interfaces: AuthorizeCodeGrantStorage) +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package internal import ( diff --git a/internal/oauth2_refresh_storage.go b/internal/oauth2_refresh_storage.go index b8750bb7..feb78dae 100644 --- a/internal/oauth2_refresh_storage.go +++ b/internal/oauth2_refresh_storage.go @@ -1,6 +1,20 @@ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/ory/fosite/handler/oauth2 (interfaces: RefreshTokenGrantStorage) +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package internal import ( diff --git a/internal/request.go b/internal/request.go index f908933a..8a2ab851 100644 --- a/internal/request.go +++ b/internal/request.go @@ -126,6 +126,14 @@ func (_mr *_MockRequesterRecorder) Merge(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Merge", arg0) } +func (_m *MockRequester) SetID(_param0 string) { + _m.ctrl.Call(_m, "SetID", _param0) +} + +func (_mr *_MockRequesterRecorder) SetID(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "SetID", arg0) +} + func (_m *MockRequester) SetRequestedScopes(_param0 fosite.Arguments) { _m.ctrl.Call(_m, "SetRequestedScopes", _param0) } diff --git a/internal/revoke_handler.go b/internal/revoke_handler.go index 309be522..82f9134b 100644 --- a/internal/revoke_handler.go +++ b/internal/revoke_handler.go @@ -31,12 +31,12 @@ func (_m *MockRevocationHandler) EXPECT() *_MockRevocationHandlerRecorder { return _m.recorder } -func (_m *MockRevocationHandler) RevokeToken(_param0 context.Context, _param1 string, _param2 fosite.TokenType) error { - ret := _m.ctrl.Call(_m, "RevokeToken", _param0, _param1, _param2) +func (_m *MockRevocationHandler) RevokeToken(_param0 context.Context, _param1 string, _param2 fosite.TokenType, _param3 fosite.Client) error { + ret := _m.ctrl.Call(_m, "RevokeToken", _param0, _param1, _param2, _param3) ret0, _ := ret[0].(error) return ret0 } -func (_mr *_MockRevocationHandlerRecorder) RevokeToken(arg0, arg1, arg2 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "RevokeToken", arg0, arg1, arg2) +func (_mr *_MockRevocationHandlerRecorder) RevokeToken(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "RevokeToken", arg0, arg1, arg2, arg3) } diff --git a/internal/rw.go b/internal/rw.go index af37774a..0dce4267 100644 --- a/internal/rw.go +++ b/internal/rw.go @@ -1,6 +1,20 @@ // Automatically generated by MockGen. DO NOT EDIT! // Source: rw.go +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package internal import ( diff --git a/introspect.go b/introspect.go index 0d0601b2..97cbac8f 100644 --- a/introspect.go +++ b/introspect.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -38,17 +52,18 @@ func (f *Fosite) IntrospectToken(ctx context.Context, token string, tokenType To ar := NewAccessRequest(session) for _, validator := range f.TokenIntrospectionHandlers { - if err := errors.Cause(validator.IntrospectToken(ctx, token, tokenType, ar, scopes)); err == ErrUnknownRequest { + if err := errors.Cause(validator.IntrospectToken(ctx, token, tokenType, ar, scopes)); err == nil { + found = true + } else if err.Error() == ErrUnknownRequest.Error() { // Nothing to do } else if err != nil { - return nil, errors.Wrap(err, "A validator returned an error") - } else { - found = true + rfcerr := ErrorToRFC6749Error(err) + return nil, errors.WithStack(rfcerr.WithDebug("A validator returned an error")) } } if !found { - return nil, errors.Wrap(ErrRequestUnauthorized, "No validator felt responsible for validating the token") + return nil, errors.WithStack(ErrRequestUnauthorized.WithDebug("No validator felt responsible for validating the token")) } return ar, nil diff --git a/introspect_test.go b/introspect_test.go index e4e62c0a..d4abec50 100644 --- a/introspect_test.go +++ b/introspect_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite_test import ( @@ -6,13 +20,15 @@ import ( "context" + "fmt" + "github.com/golang/mock/gomock" . "github.com/ory/fosite" "github.com/ory/fosite/compose" "github.com/ory/fosite/internal" "github.com/ory/fosite/storage" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAccessTokenFromRequestNoToken(t *testing.T) { @@ -96,9 +112,14 @@ func TestIntrospect(t *testing.T) { }, }, } { - c.setup() - _, err := f.IntrospectToken(nil, AccessTokenFromRequest(req), AccessToken, nil, c.scopes...) - assert.True(t, errors.Cause(err) == c.expectErr, "(%d) %s\n%s\n%s", k, c.description, err, c.expectErr) - t.Logf("Passed test case %d", k) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + c.setup() + _, err := f.IntrospectToken(nil, AccessTokenFromRequest(req), AccessToken, nil, c.scopes...) + if c.expectErr != nil { + assert.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + } + }) } } diff --git a/introspection_request_handler.go b/introspection_request_handler.go index 6afbb7e4..a6bd0731 100644 --- a/introspection_request_handler.go +++ b/introspection_request_handler.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -5,6 +19,8 @@ import ( "net/http" "strings" + "fmt" + "github.com/pkg/errors" ) @@ -89,9 +105,9 @@ import ( // token=mF_9.B5f-4.1JqM&token_type_hint=access_token func (f *Fosite) NewIntrospectionRequest(ctx context.Context, r *http.Request, session Session) (IntrospectionResponder, error) { if r.Method != "POST" { - return &IntrospectionResponse{Active: false}, errors.Wrap(ErrInvalidRequest, "HTTP method is not POST") + return &IntrospectionResponse{Active: false}, errors.WithStack(ErrInvalidRequest.WithDebug("HTTP method is not POST")) } else if err := r.ParseForm(); err != nil { - return &IntrospectionResponse{Active: false}, errors.Wrap(ErrInvalidRequest, err.Error()) + return &IntrospectionResponse{Active: false}, errors.WithStack(ErrInvalidRequest.WithDebug(err.Error())) } token := r.PostForm.Get("token") @@ -99,32 +115,32 @@ func (f *Fosite) NewIntrospectionRequest(ctx context.Context, r *http.Request, s scope := r.PostForm.Get("scope") if clientToken := AccessTokenFromRequest(r); clientToken != "" { if token == clientToken { - return &IntrospectionResponse{Active: false}, errors.Wrap(ErrRequestUnauthorized, "Bearer and introspection token are identical") + return &IntrospectionResponse{Active: false}, errors.WithStack(ErrRequestUnauthorized.WithDebug("Bearer and introspection token are identical")) } if _, err := f.IntrospectToken(ctx, clientToken, AccessToken, session.Clone()); err != nil { - return &IntrospectionResponse{Active: false}, errors.Wrap(ErrRequestUnauthorized, "HTTP Authorization header missing, malformed or credentials used are invalid") + return &IntrospectionResponse{Active: false}, errors.WithStack(ErrRequestUnauthorized.WithDebug("HTTP Authorization header missing.WithDebug(malformed or credentials used are invalid")) } } else { clientID, clientSecret, ok := r.BasicAuth() if !ok { - return &IntrospectionResponse{Active: false}, errors.Wrap(ErrRequestUnauthorized, "HTTP Authorization header missing, malformed or credentials used are invalid") + return &IntrospectionResponse{Active: false}, errors.WithStack(ErrRequestUnauthorized.WithDebug("HTTP Authorization header missing.WithDebug(malformed or credentials used are invalid")) } client, err := f.Store.GetClient(ctx, clientID) if err != nil { - return &IntrospectionResponse{Active: false}, errors.Wrap(ErrRequestUnauthorized, "HTTP Authorization header missing, malformed or credentials used are invalid") + return &IntrospectionResponse{Active: false}, errors.WithStack(ErrRequestUnauthorized.WithDebug("HTTP Authorization header missing.WithDebug(malformed or credentials used are invalid")) } // Enforce client authentication if err := f.Hasher.Compare(client.GetHashedSecret(), []byte(clientSecret)); err != nil { - return &IntrospectionResponse{Active: false}, errors.Wrap(ErrRequestUnauthorized, "HTTP Authorization header missing, malformed or credentials used are invalid") + return &IntrospectionResponse{Active: false}, errors.WithStack(ErrRequestUnauthorized.WithDebug("HTTP Authorization header missing.WithDebug(malformed or credentials used are invalid")) } } ar, err := f.IntrospectToken(ctx, token, TokenType(tokenType), session, strings.Split(scope, " ")...) if err != nil { - return &IntrospectionResponse{Active: false}, errors.Wrapf(ErrInactiveToken, "Validator returned error %s", err.Error()) + return &IntrospectionResponse{Active: false}, errors.WithStack(ErrInactiveToken.WithDebug(fmt.Sprintf("Validator returned error %s", err.Error()))) } return &IntrospectionResponse{ diff --git a/introspection_request_handler_test.go b/introspection_request_handler_test.go index a49eb574..6bb8e5b6 100644 --- a/introspection_request_handler_test.go +++ b/introspection_request_handler_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite_test import ( @@ -5,6 +19,8 @@ import ( "net/url" "testing" + "fmt" + "github.com/golang/mock/gomock" "github.com/ory/fosite" . "github.com/ory/fosite" @@ -13,6 +29,7 @@ import ( "github.com/ory/fosite/storage" "github.com/pkg/errors" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIntrospectionResponse(t *testing.T) { @@ -88,12 +105,16 @@ func TestNewIntrospectionRequest(t *testing.T) { isActive: true, }, } { - c.setup() - res, err := f.NewIntrospectionRequest(nil, httpreq, &DefaultSession{}) - assert.True(t, errors.Cause(err) == c.expectErr, "(%d) %s\n%s\n%s", k, c.description, err, c.expectErr) - if res != nil { - assert.Equal(t, c.isActive, res.IsActive()) - } - t.Logf("Passed test case %d", k) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + c.setup() + res, err := f.NewIntrospectionRequest(nil, httpreq, &DefaultSession{}) + + if c.expectErr != nil { + assert.EqualError(t, err, c.expectErr.Error()) + } else { + require.NoError(t, err) + assert.Equal(t, c.isActive, res.IsActive()) + } + }) } } diff --git a/introspection_response_writer.go b/introspection_response_writer.go index 34a02bca..ebaceb44 100644 --- a/introspection_response_writer.go +++ b/introspection_response_writer.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -36,7 +50,7 @@ func (f *Fosite) WriteIntrospectionError(rw http.ResponseWriter, err error) { switch errors.Cause(err) { case ErrInvalidRequest, ErrRequestUnauthorized: - writeJsonError(rw, err) + f.writeJsonError(rw, err) return } diff --git a/introspection_response_writer_test.go b/introspection_response_writer_test.go index f27a21ee..40eef834 100644 --- a/introspection_response_writer_test.go +++ b/introspection_response_writer_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite_test import ( diff --git a/oauth2.go b/oauth2.go index 775b5c6b..677711c1 100644 --- a/oauth2.go +++ b/oauth2.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -153,6 +167,9 @@ type IntrospectionResponder interface { // Requester is an abstract interface for handling requests in Fosite. type Requester interface { + // SetID sets the unique identifier. + SetID(id string) + // GetID returns a unique identifier. GetID() string diff --git a/request.go b/request.go index ffb566a2..d76d2d70 100644 --- a/request.go +++ b/request.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -24,7 +38,7 @@ func NewRequest() *Request { Scopes: Arguments{}, GrantedScopes: Arguments{}, Form: url.Values{}, - RequestedAt: time.Now(), + RequestedAt: time.Now().UTC(), } } @@ -35,6 +49,10 @@ func (a *Request) GetID() string { return a.ID } +func (a *Request) SetID(id string) { + a.ID = id +} + func (a *Request) GetRequestForm() url.Values { return a.Form } diff --git a/request_test.go b/request_test.go index 024dfc9d..e061f944 100644 --- a/request_test.go +++ b/request_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite_test import ( @@ -11,7 +25,7 @@ import ( func TestRequest(t *testing.T) { r := &Request{ - RequestedAt: time.Now(), + RequestedAt: time.Now().UTC(), Client: &DefaultClient{}, Scopes: Arguments{}, GrantedScopes: []string{}, @@ -29,7 +43,7 @@ func TestRequest(t *testing.T) { func TestMergeRequest(t *testing.T) { a := &Request{ - RequestedAt: time.Now(), + RequestedAt: time.Now().UTC(), Client: &DefaultClient{ID: "123"}, Scopes: Arguments{"asdff"}, GrantedScopes: []string{"asdf"}, @@ -37,7 +51,7 @@ func TestMergeRequest(t *testing.T) { Session: new(DefaultSession), } b := &Request{ - RequestedAt: time.Now(), + RequestedAt: time.Now().UTC(), Client: &DefaultClient{}, Scopes: Arguments{}, GrantedScopes: []string{}, diff --git a/revoke_handler.go b/revoke_handler.go index 75a0f186..6f5dc55f 100644 --- a/revoke_handler.go +++ b/revoke_handler.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -29,25 +43,25 @@ import ( // server and does not influence the revocation response. func (f *Fosite) NewRevocationRequest(ctx context.Context, r *http.Request) error { if r.Method != "POST" { - return errors.Wrap(ErrInvalidRequest, "HTTP method is not POST") + return errors.WithStack(ErrInvalidRequest.WithDebug("HTTP method is not POST")) } else if err := r.ParseForm(); err != nil { - return errors.Wrap(ErrInvalidRequest, err.Error()) + return errors.WithStack(ErrInvalidRequest.WithDebug(err.Error())) } clientID, clientSecret, ok := r.BasicAuth() if !ok { - return errors.Wrap(ErrInvalidRequest, "HTTP Authorization header missing or invalid") + return errors.WithStack(ErrInvalidRequest.WithDebug("HTTP Authorization header missing or invalid")) } client, err := f.Store.GetClient(ctx, clientID) if err != nil { - return errors.Wrap(ErrInvalidClient, err.Error()) + return errors.WithStack(ErrInvalidClient.WithDebug(err.Error())) } // Enforce client authentication for confidential clients if !client.IsPublic() { if err := f.Hasher.Compare(client.GetHashedSecret(), []byte(clientSecret)); err != nil { - return errors.Wrap(ErrInvalidClient, err.Error()) + return errors.WithStack(ErrInvalidClient.WithDebug(err.Error())) } } @@ -56,9 +70,9 @@ func (f *Fosite) NewRevocationRequest(ctx context.Context, r *http.Request) erro var found bool for _, loader := range f.RevocationHandlers { - if err := loader.RevokeToken(ctx, token, tokenTypeHint); err == nil { + if err := loader.RevokeToken(ctx, token, tokenTypeHint, client); err == nil { found = true - } else if errors.Cause(err) == ErrUnknownRequest { + } else if errors.Cause(err).Error() == ErrUnknownRequest.Error() { // do nothing } else if err != nil { return err @@ -84,20 +98,24 @@ func (f *Fosite) NewRevocationRequest(ctx context.Context, r *http.Request) erro // purpose of the revocation request, invalidating the particular token, // is already achieved. func (f *Fosite) WriteRevocationResponse(rw http.ResponseWriter, err error) { - switch errors.Cause(err) { - case ErrInvalidRequest: + if err == nil { + rw.WriteHeader(http.StatusOK) + return + } + + switch errors.Cause(err).Error() { + case ErrInvalidRequest.Error(): fallthrough - case ErrInvalidClient: + case ErrInvalidClient.Error(): rw.Header().Set("Content-Type", "application/json;charset=UTF-8") - rfcerr := ErrorToRFC6749Error(err) - js, err := json.Marshal(rfcerr) + js, err := json.Marshal(ErrInvalidClient) if err != nil { http.Error(rw, fmt.Sprintf(`{"error": "%s"}`, err.Error()), http.StatusInternalServerError) return } - rw.WriteHeader(rfcerr.Code) + rw.WriteHeader(ErrInvalidClient.Code) rw.Write(js) default: // 200 OK diff --git a/revoke_handler_test.go b/revoke_handler_test.go index a1eeaa61..13fd1e52 100644 --- a/revoke_handler_test.go +++ b/revoke_handler_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite_test import ( @@ -5,6 +19,8 @@ import ( "net/url" "testing" + "fmt" + "github.com/golang/mock/gomock" . "github.com/ory/fosite" "github.com/ory/fosite/internal" @@ -94,7 +110,7 @@ func TestNewRevocationRequest(t *testing.T) { client.EXPECT().GetHashedSecret().Return([]byte("foo")) client.EXPECT().IsPublic().Return(false) hasher.EXPECT().Compare(gomock.Eq([]byte("foo")), gomock.Eq([]byte("bar"))).Return(nil) - handler.EXPECT().RevokeToken(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + handler.EXPECT().RevokeToken(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) }, handlers: RevocationHandlers{handler}, }, @@ -113,7 +129,7 @@ func TestNewRevocationRequest(t *testing.T) { client.EXPECT().GetHashedSecret().Return([]byte("foo")) client.EXPECT().IsPublic().Return(false) hasher.EXPECT().Compare(gomock.Eq([]byte("foo")), gomock.Eq([]byte("bar"))).Return(nil) - handler.EXPECT().RevokeToken(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + handler.EXPECT().RevokeToken(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) }, handlers: RevocationHandlers{handler}, }, @@ -131,7 +147,7 @@ func TestNewRevocationRequest(t *testing.T) { store.EXPECT().GetClient(gomock.Any(), gomock.Eq("foo")).Return(client, nil) client.EXPECT().IsPublic().Return(true) hasher.EXPECT().Compare(gomock.Eq([]byte("foo")), gomock.Eq([]byte("bar"))).Return(nil) - handler.EXPECT().RevokeToken(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + handler.EXPECT().RevokeToken(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) }, handlers: RevocationHandlers{handler}, }, @@ -149,7 +165,7 @@ func TestNewRevocationRequest(t *testing.T) { store.EXPECT().GetClient(gomock.Any(), gomock.Eq("foo")).Return(client, nil) client.EXPECT().GetHashedSecret().Return([]byte("foo")) client.EXPECT().IsPublic().Return(false) - handler.EXPECT().RevokeToken(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + handler.EXPECT().RevokeToken(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) }, handlers: RevocationHandlers{handler}, }, @@ -168,25 +184,28 @@ func TestNewRevocationRequest(t *testing.T) { client.EXPECT().GetHashedSecret().Return([]byte("foo")) client.EXPECT().IsPublic().Return(false) hasher.EXPECT().Compare(gomock.Eq([]byte("foo")), gomock.Eq([]byte("bar"))).Return(nil) - handler.EXPECT().RevokeToken(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + handler.EXPECT().RevokeToken(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) }, handlers: RevocationHandlers{handler}, }, } { - r := &http.Request{ - Header: c.header, - PostForm: c.form, - Form: c.form, - Method: c.method, - } - c.mock() - ctx := NewContext() - fosite.RevocationHandlers = c.handlers - err := fosite.NewRevocationRequest(ctx, r) - assert.True(t, errors.Cause(err) == c.expectErr, "%d\nwant: %s \ngot: %s", k, c.expectErr, err) - if err != nil { - t.Logf("Error occured: %v", err) - } - t.Logf("Passed test case %d", k) + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + r := &http.Request{ + Header: c.header, + PostForm: c.form, + Form: c.form, + Method: c.method, + } + c.mock() + ctx := NewContext() + fosite.RevocationHandlers = c.handlers + err := fosite.NewRevocationRequest(ctx, r) + + if c.expectErr != nil { + assert.EqualError(t, err, c.expectErr.Error()) + } else { + assert.NoError(t, err) + } + }) } } diff --git a/scope_strategy.go b/scope_strategy.go index 92789cda..a8178e62 100644 --- a/scope_strategy.go +++ b/scope_strategy.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import "strings" diff --git a/scope_strategy_test.go b/scope_strategy_test.go index 21949578..512f1bb4 100644 --- a/scope_strategy_test.go +++ b/scope_strategy_test.go @@ -1,10 +1,25 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( "testing" - "github.com/stretchr/testify/assert" "strings" + + "github.com/stretchr/testify/assert" ) func TestHierarchicScopeStrategy(t *testing.T) { diff --git a/scripts/.gitattributes b/scripts/.gitattributes new file mode 100644 index 00000000..dfdb8b77 --- /dev/null +++ b/scripts/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/scripts/run-format.sh b/scripts/run-format.sh new file mode 100755 index 00000000..28c8a65a --- /dev/null +++ b/scripts/run-format.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -euo pipefail + +cd "$( dirname "${BASH_SOURCE[0]}" )/.." + +goimports -w $(go list -f {{.Dir}} ./... | grep -v vendor | grep -v fosite$) +goimports -w *.go diff --git a/scripts/test-format.sh b/scripts/test-format.sh new file mode 100755 index 00000000..0cc64896 --- /dev/null +++ b/scripts/test-format.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -euo pipefail + +cd "$( dirname "${BASH_SOURCE[0]}" )/.." + +toformat=$(goimports -l $(go list -f {{.Dir}} ./... | grep -v vendor | grep -v 'fosite$')) +[ -z "$toformat" ] && echo "All files are formatted correctly" +[ -n "$toformat" ] && echo "Please use \`goimports\` to format the following files:" && echo $toformat && exit 1 + +exit 0 diff --git a/session.go b/session.go index cc98d92f..1ff5a170 100644 --- a/session.go +++ b/session.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( @@ -11,7 +25,7 @@ import ( type Session interface { // SetExpiresAt sets the expiration time of a token. // - // session.SetExpiresAt(fosite.AccessToken, time.Now().Add(time.Hour)) + // session.SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(time.Hour)) SetExpiresAt(key TokenType, exp time.Time) // SetExpiresAt returns expiration time of a token if set, or time.IsZero() if not. diff --git a/session_test.go b/session_test.go index 635649a6..bf2d52f2 100644 --- a/session_test.go +++ b/session_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite import ( diff --git a/storage.go b/storage.go index a5d3d1ce..10cea84e 100644 --- a/storage.go +++ b/storage.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package fosite // Storage defines fosite's minimal storage interface. diff --git a/storage/memory.go b/storage/memory.go index 7b2ddd8b..5f9ed39f 100644 --- a/storage/memory.go +++ b/storage/memory.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package storage import ( diff --git a/token/hmac/bytes.go b/token/hmac/bytes.go index ad84599d..32b6917f 100644 --- a/token/hmac/bytes.go +++ b/token/hmac/bytes.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package hmac import ( diff --git a/token/hmac/bytes_test.go b/token/hmac/bytes_test.go index e19e59b7..f39c57cf 100644 --- a/token/hmac/bytes_test.go +++ b/token/hmac/bytes_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package hmac import ( @@ -8,7 +22,7 @@ import ( func TestRandomBytes(t *testing.T) { bytes, err := RandomBytes(128) - assert.Nil(t, err, "%s", err) + assert.NoError(t, err) assert.Len(t, bytes, 128) } @@ -17,7 +31,7 @@ func TestPseudoRandomness(t *testing.T) { results := map[string]bool{} for i := 0; i < runs; i++ { bytes, err := RandomBytes(128) - assert.Nil(t, err, "%s", err) + assert.NoError(t, err) _, ok := results[string(bytes)] assert.False(t, ok) diff --git a/token/hmac/hmacsha.go b/token/hmac/hmacsha.go index 7f44565e..9b3c1341 100644 --- a/token/hmac/hmacsha.go +++ b/token/hmac/hmacsha.go @@ -1,14 +1,28 @@ // Package hmac is the default implementation for generating and validating challenges. It uses HMAC-SHA256 to // generate and validate challenges. +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package hmac import ( - "crypto/hmac" - "crypto/sha256" "encoding/base64" "fmt" "strings" + "sync" + "github.com/gtank/cryptopasta" "github.com/ory/fosite" "github.com/pkg/errors" ) @@ -17,6 +31,7 @@ import ( type HMACStrategy struct { AuthCodeEntropy int GlobalSecret []byte + sync.Mutex } const ( @@ -32,10 +47,16 @@ var b64 = base64.URLEncoding.WithPadding(base64.NoPadding) // Generate generates a token and a matching signature or returns an error. // This method implements rfc6819 Section 5.1.4.2.2: Use High Entropy for Secrets. func (c *HMACStrategy) Generate() (string, string, error) { - if len(c.GlobalSecret) < minimumSecretLength/2 { - return "", "", errors.New("Secret is not strong enough") + c.Lock() + defer c.Unlock() + + if len(c.GlobalSecret) < minimumSecretLength { + return "", "", errors.Errorf("Secret for signing HMAC-SHA256 is expected to be 32 byte long, got %d byte", len(c.GlobalSecret)) } + var signingKey [32]byte + copy(signingKey[:], c.GlobalSecret) + if c.AuthCodeEntropy < minimumEntropy { c.AuthCodeEntropy = minimumEntropy } @@ -47,59 +68,49 @@ func (c *HMACStrategy) Generate() (string, string, error) { // constructed from a cryptographically strong random or pseudo-random // number sequence (see [RFC4086] for best current practice) generated // by the authorization server. - key, err := RandomBytes(c.AuthCodeEntropy) + tokenKey, err := RandomBytes(c.AuthCodeEntropy) if err != nil { return "", "", errors.WithStack(err) } - if len(key) < c.AuthCodeEntropy { - return "", "", errors.New("Could not read enough random data for key generation") - } - - useSecret := append([]byte{}, c.GlobalSecret...) - mac := hmac.New(sha256.New, useSecret) - _, err = mac.Write(key) - if err != nil { - return "", "", errors.WithStack(err) - } + signature := cryptopasta.GenerateHMAC(tokenKey, &signingKey) - signature := mac.Sum([]byte{}) encodedSignature := b64.EncodeToString(signature) - encodedToken := fmt.Sprintf("%s.%s", b64.EncodeToString(key), encodedSignature) + encodedToken := fmt.Sprintf("%s.%s", b64.EncodeToString(tokenKey), encodedSignature) return encodedToken, encodedSignature, nil } // Validate validates a token and returns its signature or an error if the token is not valid. func (c *HMACStrategy) Validate(token string) error { + if len(c.GlobalSecret) < minimumSecretLength { + return errors.Errorf("Secret for signing HMAC-SHA256 is expected to be 32 byte long, got %d byte", len(c.GlobalSecret)) + } + + var signingKey [32]byte + copy(signingKey[:], c.GlobalSecret) + split := strings.Split(token, ".") if len(split) != 2 { return errors.WithStack(fosite.ErrInvalidTokenFormat) } - key := split[0] - signature := split[1] - if key == "" || signature == "" { + tokenKey := split[0] + tokenSignature := split[1] + if tokenKey == "" || tokenSignature == "" { return errors.WithStack(fosite.ErrInvalidTokenFormat) } - decodedSignature, err := b64.DecodeString(signature) - if err != nil { - return errors.WithStack(err) - } - - decodedKey, err := b64.DecodeString(key) + decodedTokenSignature, err := b64.DecodeString(tokenSignature) if err != nil { return errors.WithStack(err) } - useSecret := append([]byte{}, c.GlobalSecret...) - mac := hmac.New(sha256.New, useSecret) - _, err = mac.Write(decodedKey) + decodedTokenKey, err := b64.DecodeString(tokenKey) if err != nil { return errors.WithStack(err) } - if !hmac.Equal(decodedSignature, mac.Sum([]byte{})) { + if !cryptopasta.CheckHMAC(decodedTokenKey, decodedTokenSignature, &signingKey) { // Hash is invalid return errors.WithStack(fosite.ErrTokenSignatureMismatch) } diff --git a/token/hmac/hmacsha_test.go b/token/hmac/hmacsha_test.go index 12e88ca1..b7e4a1cd 100644 --- a/token/hmac/hmacsha_test.go +++ b/token/hmac/hmacsha_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package hmac import ( @@ -10,37 +24,37 @@ import ( func TestGenerateFailsWithShortCredentials(t *testing.T) { cg := HMACStrategy{GlobalSecret: []byte("foo")} challenge, signature, err := cg.Generate() - require.NotNil(t, err, "%s", err) + require.Error(t, err) require.Empty(t, challenge) require.Empty(t, signature) } func TestGenerate(t *testing.T) { cg := HMACStrategy{ - GlobalSecret: []byte("12345678901234567890"), + GlobalSecret: []byte("1234567890123456789012345678901234567890"), } token, signature, err := cg.Generate() - require.Nil(t, err, "%s", err) + require.NoError(t, err) require.NotEmpty(t, token) require.NotEmpty(t, signature) t.Logf("Token: %s\n Signature: %s", token, signature) err = cg.Validate(token) - require.Nil(t, err, "%s", err) + require.NoError(t, err) validateSignature := cg.Signature(token) assert.Equal(t, signature, validateSignature) cg.GlobalSecret = []byte("baz") err = cg.Validate(token) - require.NotNil(t, err, "%s", err) + require.Error(t, err) } func TestValidateSignatureRejects(t *testing.T) { var err error cg := HMACStrategy{ - GlobalSecret: []byte("12345678901234567890"), + GlobalSecret: []byte("1234567890123456789012345678901234567890"), } for k, c := range []string{ "", @@ -50,7 +64,7 @@ func TestValidateSignatureRejects(t *testing.T) { ".foo", } { err = cg.Validate(c) - assert.NotNil(t, err, "%s", err) + assert.Error(t, err) t.Logf("Passed test case %d", k) } } diff --git a/token/jwt/claims.go b/token/jwt/claims.go index 4fc765e4..bb4d3797 100644 --- a/token/jwt/claims.go +++ b/token/jwt/claims.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package jwt import "time" @@ -35,9 +49,9 @@ func ToTime(i interface{}) time.Time { } if t, ok := i.(int64); ok { - return time.Unix(t, 0) + return time.Unix(t, 0).UTC() } else if t, ok := i.(float64); ok { - return time.Unix(int64(t), 0) + return time.Unix(int64(t), 0).UTC() } else if t, ok := i.(time.Time); ok { return t } diff --git a/token/jwt/claims_id_token.go b/token/jwt/claims_id_token.go index 1e2112c5..6f325050 100644 --- a/token/jwt/claims_id_token.go +++ b/token/jwt/claims_id_token.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package jwt import ( @@ -8,16 +22,18 @@ import ( // IDTokenClaims represent the claims used in open id connect requests type IDTokenClaims struct { - Issuer string - Subject string - Audience string - Nonce string - ExpiresAt time.Time - IssuedAt time.Time - AuthTime time.Time - AccessTokenHash string - CodeHash string - Extra map[string]interface{} + Issuer string + Subject string + Audience string + Nonce string + ExpiresAt time.Time + IssuedAt time.Time + RequestedAt time.Time + AuthTime time.Time + AccessTokenHash string + AuthenticationContextClassReference string + CodeHash string + Extra map[string]interface{} } // ToMap will transform the headers to a map structure @@ -40,8 +56,13 @@ func (c *IDTokenClaims) ToMap() map[string]interface{} { ret["auth_time"] = c.AuthTime.Unix() } + if len(c.AuthenticationContextClassReference) > 0 { + ret["acr"] = c.AuthenticationContextClassReference + } + ret["iat"] = float64(c.IssuedAt.Unix()) ret["exp"] = float64(c.ExpiresAt.Unix()) + ret["rat"] = float64(c.RequestedAt.Unix()) return ret } diff --git a/token/jwt/claims_id_token_test.go b/token/jwt/claims_id_token_test.go index 7381bdd7..5a9516d2 100644 --- a/token/jwt/claims_id_token_test.go +++ b/token/jwt/claims_id_token_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package jwt_test import ( @@ -10,13 +24,15 @@ import ( var idTokenClaims = &IDTokenClaims{ Subject: "peter", - IssuedAt: time.Now().Round(time.Second), + IssuedAt: time.Now().UTC().Round(time.Second), Issuer: "fosite", Audience: "tests", - ExpiresAt: time.Now().Add(time.Hour).Round(time.Second), - AuthTime: time.Now(), + ExpiresAt: time.Now().UTC().Add(time.Hour).Round(time.Second), + AuthTime: time.Now().UTC(), + RequestedAt: time.Now().UTC(), AccessTokenHash: "foobar", CodeHash: "barfoo", + AuthenticationContextClassReference: "acr", Extra: map[string]interface{}{ "foo": "bar", "baz": "bar", @@ -24,9 +40,9 @@ var idTokenClaims = &IDTokenClaims{ } func TestIDTokenAssert(t *testing.T) { - assert.Nil(t, (&IDTokenClaims{ExpiresAt: time.Now().Add(time.Hour)}). + assert.NoError(t, (&IDTokenClaims{ExpiresAt: time.Now().UTC().Add(time.Hour)}). ToMapClaims().Valid()) - assert.NotNil(t, (&IDTokenClaims{ExpiresAt: time.Now().Add(-time.Hour)}). + assert.Error(t, (&IDTokenClaims{ExpiresAt: time.Now().UTC().Add(-time.Hour)}). ToMapClaims().Valid()) } @@ -34,6 +50,7 @@ func TestIDTokenClaimsToMap(t *testing.T) { assert.Equal(t, map[string]interface{}{ "sub": idTokenClaims.Subject, "iat": float64(idTokenClaims.IssuedAt.Unix()), + "rat": float64(idTokenClaims.RequestedAt.Unix()), "iss": idTokenClaims.Issuer, "aud": idTokenClaims.Audience, "nonce": idTokenClaims.Nonce, @@ -43,5 +60,6 @@ func TestIDTokenClaimsToMap(t *testing.T) { "at_hash": idTokenClaims.AccessTokenHash, "c_hash": idTokenClaims.CodeHash, "auth_time": idTokenClaims.AuthTime.Unix(), + "acr": idTokenClaims.AuthenticationContextClassReference, }, idTokenClaims.ToMap()) } diff --git a/token/jwt/claims_jwt.go b/token/jwt/claims_jwt.go index e0518f6c..d30a2c32 100644 --- a/token/jwt/claims_jwt.go +++ b/token/jwt/claims_jwt.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package jwt import ( @@ -74,23 +88,23 @@ func (c *JWTClaims) FromMap(m map[string]interface{}) { case "iat": switch v.(type) { case float64: - c.IssuedAt = time.Unix(int64(v.(float64)), 0) + c.IssuedAt = time.Unix(int64(v.(float64)), 0).UTC() case int64: - c.IssuedAt = time.Unix(v.(int64), 0) + c.IssuedAt = time.Unix(v.(int64), 0).UTC() } case "nbf": switch v.(type) { case float64: - c.NotBefore = time.Unix(int64(v.(float64)), 0) + c.NotBefore = time.Unix(int64(v.(float64)), 0).UTC() case int64: - c.NotBefore = time.Unix(v.(int64), 0) + c.NotBefore = time.Unix(v.(int64), 0).UTC() } case "exp": switch v.(type) { case float64: - c.ExpiresAt = time.Unix(int64(v.(float64)), 0) + c.ExpiresAt = time.Unix(int64(v.(float64)), 0).UTC() case int64: - c.ExpiresAt = time.Unix(v.(int64), 0) + c.ExpiresAt = time.Unix(v.(int64), 0).UTC() } case "scp": switch v.(type) { diff --git a/token/jwt/claims_jwt_test.go b/token/jwt/claims_jwt_test.go index 3d703183..3d168fe4 100644 --- a/token/jwt/claims_jwt_test.go +++ b/token/jwt/claims_jwt_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package jwt_test import ( @@ -10,11 +24,11 @@ import ( var jwtClaims = &JWTClaims{ Subject: "peter", - IssuedAt: time.Now().Round(time.Second), + IssuedAt: time.Now().UTC().Round(time.Second), Issuer: "fosite", - NotBefore: time.Now().Round(time.Second), + NotBefore: time.Now().UTC().Round(time.Second), Audience: "tests", - ExpiresAt: time.Now().Add(time.Hour).Round(time.Second), + ExpiresAt: time.Now().UTC().Add(time.Hour).Round(time.Second), JTI: "abcdef", Scope: []string{"email", "offline"}, Extra: map[string]interface{}{ @@ -46,16 +60,16 @@ func TestClaimsToMapSetsID(t *testing.T) { } func TestAssert(t *testing.T) { - assert.Nil(t, (&JWTClaims{ExpiresAt: time.Now().Add(time.Hour)}). + assert.Nil(t, (&JWTClaims{ExpiresAt: time.Now().UTC().Add(time.Hour)}). ToMapClaims().Valid()) - assert.NotNil(t, (&JWTClaims{ExpiresAt: time.Now().Add(-2 * time.Hour)}). + assert.NotNil(t, (&JWTClaims{ExpiresAt: time.Now().UTC().Add(-2 * time.Hour)}). ToMapClaims().Valid()) - assert.NotNil(t, (&JWTClaims{NotBefore: time.Now().Add(time.Hour)}). + assert.NotNil(t, (&JWTClaims{NotBefore: time.Now().UTC().Add(time.Hour)}). ToMapClaims().Valid()) - assert.NotNil(t, (&JWTClaims{NotBefore: time.Now().Add(-time.Hour)}). + assert.NotNil(t, (&JWTClaims{NotBefore: time.Now().UTC().Add(-time.Hour)}). ToMapClaims().Valid()) - assert.Nil(t, (&JWTClaims{ExpiresAt: time.Now().Add(time.Hour), - NotBefore: time.Now().Add(-time.Hour)}).ToMapClaims().Valid()) + assert.Nil(t, (&JWTClaims{ExpiresAt: time.Now().UTC().Add(time.Hour), + NotBefore: time.Now().UTC().Add(-time.Hour)}).ToMapClaims().Valid()) } func TestClaimsToMap(t *testing.T) { diff --git a/token/jwt/claims_test.go b/token/jwt/claims_test.go index fbb8104c..304a480f 100644 --- a/token/jwt/claims_test.go +++ b/token/jwt/claims_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package jwt import ( @@ -18,7 +32,7 @@ func TestToTime(t *testing.T) { assert.Equal(t, time.Time{}, ToTime(nil)) assert.Equal(t, time.Time{}, ToTime("1234")) - now := time.Now().Round(time.Second) + now := time.Now().UTC().Round(time.Second) assert.Equal(t, now, ToTime(now)) assert.Equal(t, now, ToTime(now.Unix())) assert.Equal(t, now, ToTime(float64(now.Unix()))) diff --git a/token/jwt/header.go b/token/jwt/header.go index 5ae7ede2..577f06cd 100644 --- a/token/jwt/header.go +++ b/token/jwt/header.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package jwt import "github.com/dgrijalva/jwt-go" diff --git a/token/jwt/header_test.go b/token/jwt/header_test.go index 209bd7c6..48b92eb9 100644 --- a/token/jwt/header_test.go +++ b/token/jwt/header_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package jwt import ( diff --git a/token/jwt/jwt.go b/token/jwt/jwt.go index 319b9f5a..1399ebc2 100644 --- a/token/jwt/jwt.go +++ b/token/jwt/jwt.go @@ -1,5 +1,19 @@ // Package jwt is able to generate and validate json web tokens. // Follows https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32 +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package jwt import ( @@ -9,6 +23,7 @@ import ( "strings" "github.com/dgrijalva/jwt-go" + "github.com/ory/fosite" "github.com/pkg/errors" ) @@ -59,9 +74,9 @@ func (j *RS256JWTStrategy) Decode(token string) (*jwt.Token, error) { }) if err != nil { - return nil, errors.Wrap(err, "Couldn't parse token") + return nil, errors.WithStack(err) } else if !parsedToken.Valid { - return nil, errors.Errorf("Token is invalid") + return nil, errors.WithStack(fosite.ErrInactiveToken) } return parsedToken, err diff --git a/token/jwt/jwt_test.go b/token/jwt/jwt_test.go index f3feda08..29658d0b 100644 --- a/token/jwt/jwt_test.go +++ b/token/jwt/jwt_test.go @@ -1,3 +1,17 @@ +// Copyright © 2017 Aeneas Rekkas +// +// 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. + package jwt import ( @@ -23,7 +37,7 @@ func TestHash(t *testing.T) { } in := []byte("foo") out, err := j.Hash(in) - assert.Nil(t, err) + assert.NoError(t, err) assert.NotEqual(t, in, out) } @@ -56,7 +70,7 @@ func TestAssign(t *testing.T) { func TestGenerateJWT(t *testing.T) { claims := &JWTClaims{ - ExpiresAt: time.Now().Add(time.Hour), + ExpiresAt: time.Now().UTC().Add(time.Hour), } j := RS256JWTStrategy{ @@ -64,45 +78,45 @@ func TestGenerateJWT(t *testing.T) { } token, sig, err := j.Generate(claims.ToMapClaims(), header) - require.Nil(t, err, "%s", err) + require.NoError(t, err) require.NotNil(t, token) sig, err = j.Validate(token) - require.Nil(t, err, "%s", err) + require.NoError(t, err) sig, err = j.Validate(token + "." + "0123456789") - require.NotNil(t, err, "%s", err) + require.Error(t, err) partToken := strings.Split(token, ".")[2] sig, err = j.Validate(partToken) - require.NotNil(t, err, "%s", err) + require.Error(t, err) // Reset private key j.PrivateKey = internal.MustRSAKey() // Lets validate the exp claim claims = &JWTClaims{ - ExpiresAt: time.Now().Add(-time.Hour), + ExpiresAt: time.Now().UTC().Add(-time.Hour), } token, sig, err = j.Generate(claims.ToMapClaims(), header) - require.Nil(t, err, "%s", err) + require.NoError(t, err) require.NotNil(t, token) //t.Logf("%s.%s", token, sig) sig, err = j.Validate(token) - require.NotNil(t, err, "%s", err) + require.Error(t, err) // Lets validate the nbf claim claims = &JWTClaims{ - NotBefore: time.Now().Add(time.Hour), + NotBefore: time.Now().UTC().Add(time.Hour), } token, sig, err = j.Generate(claims.ToMapClaims(), header) - require.Nil(t, err, "%s", err) + require.NoError(t, err) require.NotNil(t, token) //t.Logf("%s.%s", token, sig) sig, err = j.Validate(token) - require.NotNil(t, err, "%s", err) + require.Error(t, err) require.Empty(t, sig, "%s", err) } @@ -120,7 +134,7 @@ func TestValidateSignatureRejectsJWT(t *testing.T) { ".foo", } { _, err = j.Validate(c) - assert.NotNil(t, err, "%s", err) + assert.Error(t, err) t.Logf("Passed test case %d", k) } }