From a92c75531cf5bb89524cd719c9bc2c98fe709c62 Mon Sep 17 00:00:00 2001 From: Aeneas Date: Sat, 6 Aug 2016 14:10:43 +0200 Subject: [PATCH] all: composable factories, better token validation, better scope handling and simplify structure * readme: add gitter chat badge closes #67 * handler: flatten packages closes #70 * openid: don't autogrant openid scope - closes #68 * all: clean up scopes / arguments - closes #66 * all: composable factories - closes #64 * all: refactor token validation - closes #63 * all: remove mandatory scope - closes #62 --- HISTORY.md | 21 + README.md | 297 +++++++--- access_request_handler.go | 8 +- access_request_handler_test.go | 24 +- authorize_request_handler.go | 8 +- authorize_request_handler_test.go | 19 +- authorize_request_test.go | 6 +- client.go | 54 +- client_test.go | 29 +- compose/compose.go | 79 +++ compose/compose_oauth2.go | 119 ++++ compose/compose_openid.go | 70 +++ compose/compose_strategy.go | 41 ++ compose/config.go | 49 ++ errors.go | 2 +- fosite-example/main.go | 511 ++++-------------- fosite-example/pkg/handler_app_callback.go | 83 +++ fosite-example/pkg/handler_home.go | 40 ++ .../pkg/handler_oauth2_client_flow.go | 22 + .../pkg/handler_oauth2_owner_flow.go | 41 ++ fosite-example/{store => pkg}/store.go | 27 + fosite.go | 59 +- fosite_test.go | 29 +- generate-mocks.sh | 30 +- handler/core/client/storage.go | 7 - handler/core/validator.go | 40 -- .../flow_authorize_code_auth.go} | 41 +- .../flow_authorize_code_auth_test.go} | 7 +- .../flow_authorize_code_storage.go} | 5 +- .../flow_authorize_code_token.go} | 14 +- .../flow_authorize_code_token_test.go} | 12 +- .../flow_authorize_implicit.go} | 35 +- .../flow_authorize_implicit_storage.go} | 2 +- .../flow_authorize_implicit_test.go} | 6 +- .../client.go => oauth2/flow_client.go} | 16 +- .../oauth2/flow_client_credentials_storage.go | 5 + .../flow_client_credentials_test.go} | 22 +- .../refresh.go => oauth2/flow_refresh.go} | 13 +- .../flow_refresh_storage.go} | 5 +- .../flow_refresh_test.go} | 6 +- .../flow_resource_owner.go} | 20 +- .../flow_resource_owner_storage.go} | 5 +- .../flow_resource_owner_test.go} | 12 +- handler/{core => oauth2}/helper.go | 6 +- handler/{core => oauth2}/helper_test.go | 3 +- handler/{core => oauth2}/storage.go | 2 +- handler/{core => oauth2}/strategy.go | 2 +- .../hmacsha.go => oauth2/strategy_hmacsha.go} | 2 +- .../strategy_hmacsha_session.go} | 2 +- .../strategy_hmacsha_test.go} | 2 +- .../jwt.go => oauth2/strategy_jwt.go} | 2 +- .../strategy_jwt_session.go} | 2 +- .../strategy_jwt_test.go} | 2 +- handler/oauth2/validator.go | 75 +++ handler/{core => oauth2}/validator_test.go | 23 +- handler/oidc/explicit/explicit_token.go | 41 -- handler/oidc/hybrid/hybrid.go | 99 ---- handler/{oidc => openid}/errors.go | 2 +- .../flow_explicit_auth.go} | 15 +- .../flow_explicit_auth_test.go} | 12 +- handler/openid/flow_explicit_token.go | 40 ++ .../flow_explicit_token_test.go} | 16 +- handler/openid/flow_hybrid.go | 105 ++++ .../flow_hybrid_test.go} | 45 +- .../implicit.go => openid/flow_implicit.go} | 34 +- .../flow_implicit_test.go} | 39 +- handler/{oidc => openid}/helper.go | 11 +- handler/{oidc => openid}/helper_test.go | 11 +- handler/{oidc => openid}/storage.go | 2 +- handler/{oidc => openid}/strategy.go | 2 +- .../default.go => openid/strategy_jwt.go} | 4 +- .../strategy_jwt_test.go} | 12 +- integration/authorize_code_grant_test.go | 49 +- integration/authorize_implicit_grant_test.go | 29 +- integration/basic_test.go | 24 - integration/client_credentials_grant_test.go | 64 +-- integration/helper_endpoints_test.go | 13 +- integration/helper_setup_test.go | 42 +- integration/oidc_explicit_test.go | 53 +- integration/oidc_implicit_hybrid_test.go | 64 +-- integration/refresh_token_grant_test.go | 53 +- ...e_owner_password_credentials_grant_test.go | 72 +-- internal/access_request.go | 24 +- internal/authorize_request.go | 40 +- internal/client.go | 20 +- internal/core_storage.go | 124 +++++ internal/core_strategy.go | 127 +++++ ...ner_storage.go => oauth2_owner_storage.go} | 2 +- ...h_storage.go => oauth2_refresh_storage.go} | 2 +- ..._storage.go => openid_id_token_storage.go} | 2 +- internal/request.go | 24 +- internal/validator.go | 32 +- oauth2.go | 32 +- request.go | 23 +- scope.go | 8 - scope_strategy.go | 36 ++ scope_strategy_test.go | 33 ++ scope_test.go | 15 - validator.go | 32 +- validator_test.go | 39 +- 100 files changed, 2038 insertions(+), 1594 deletions(-) create mode 100644 HISTORY.md create mode 100644 compose/compose.go create mode 100644 compose/compose_oauth2.go create mode 100644 compose/compose_openid.go create mode 100644 compose/compose_strategy.go create mode 100644 compose/config.go create mode 100644 fosite-example/pkg/handler_app_callback.go create mode 100644 fosite-example/pkg/handler_home.go create mode 100644 fosite-example/pkg/handler_oauth2_client_flow.go create mode 100644 fosite-example/pkg/handler_oauth2_owner_flow.go rename fosite-example/{store => pkg}/store.go (80%) delete mode 100644 handler/core/client/storage.go delete mode 100644 handler/core/validator.go rename handler/{core/explicit/explicit_auth.go => oauth2/flow_authorize_code_auth.go} (52%) rename handler/{core/explicit/explicit_auth_test.go => oauth2/flow_authorize_code_auth_test.go} (93%) rename handler/{core/explicit/storage.go => oauth2/flow_authorize_code_storage.go} (75%) rename handler/{core/explicit/explicit_token.go => oauth2/flow_authorize_code_token.go} (85%) rename handler/{core/explicit/explicit_token_test.go => oauth2/flow_authorize_code_token_test.go} (94%) rename handler/{core/implicit/implicit.go => oauth2/flow_authorize_implicit.go} (63%) rename handler/{core/implicit/storage.go => oauth2/flow_authorize_implicit_storage.go} (92%) rename handler/{core/implicit/implicit_test.go => oauth2/flow_authorize_implicit_test.go} (94%) rename handler/{core/client/client.go => oauth2/flow_client.go} (78%) create mode 100644 handler/oauth2/flow_client_credentials_storage.go rename handler/{core/client/client_test.go => oauth2/flow_client_credentials_test.go} (85%) rename handler/{core/refresh/refresh.go => oauth2/flow_refresh.go} (90%) rename handler/{core/refresh/storage.go => oauth2/flow_refresh_storage.go} (76%) rename handler/{core/refresh/refresh_test.go => oauth2/flow_refresh_test.go} (97%) rename handler/{core/owner/owner.go => oauth2/flow_resource_owner.go} (78%) rename handler/{core/owner/storage.go => oauth2/flow_resource_owner_storage.go} (68%) rename handler/{core/owner/owner_test.go => oauth2/flow_resource_owner_test.go} (92%) rename handler/{core => oauth2}/helper.go (83%) rename handler/{core => oauth2}/helper_test.go (96%) rename handler/{core => oauth2}/storage.go (98%) rename handler/{core => oauth2}/strategy.go (98%) rename handler/{core/strategy/hmacsha.go => oauth2/strategy_hmacsha.go} (99%) rename handler/{core/strategy/hmacsha_session.go => oauth2/strategy_hmacsha_session.go} (98%) rename handler/{core/strategy/hmacsha_test.go => oauth2/strategy_hmacsha_test.go} (99%) rename handler/{core/strategy/jwt.go => oauth2/strategy_jwt.go} (99%) rename handler/{core/strategy/jwt_session.go => oauth2/strategy_jwt_session.go} (97%) rename handler/{core/strategy/jwt_test.go => oauth2/strategy_jwt_test.go} (99%) create mode 100644 handler/oauth2/validator.go rename handler/{core => oauth2}/validator_test.go (76%) delete mode 100644 handler/oidc/explicit/explicit_token.go delete mode 100644 handler/oidc/hybrid/hybrid.go rename handler/{oidc => openid}/errors.go (86%) rename handler/{oidc/explicit/explicit_auth.go => openid/flow_explicit_auth.go} (57%) rename handler/{oidc/explicit/explicit_auth_test.go => openid/flow_explicit_auth_test.go} (87%) create mode 100644 handler/openid/flow_explicit_token.go rename handler/{oidc/explicit/explicit_token_test.go => openid/flow_explicit_token_test.go} (88%) create mode 100644 handler/openid/flow_hybrid.go rename handler/{oidc/hybrid/hybrid_test.go => openid/flow_hybrid_test.go} (74%) rename handler/{oidc/implicit/implicit.go => openid/flow_implicit.go} (55%) rename handler/{oidc/implicit/implicit_test.go => openid/flow_implicit_test.go} (80%) rename handler/{oidc => openid}/helper.go (71%) rename handler/{oidc => openid}/helper_test.go (90%) rename handler/{oidc => openid}/storage.go (98%) rename handler/{oidc => openid}/strategy.go (94%) rename handler/{oidc/strategy/default.go => openid/strategy_jwt.go} (96%) rename handler/{oidc/strategy/default_test.go => openid/strategy_jwt_test.go} (90%) delete mode 100644 integration/basic_test.go create mode 100644 internal/core_storage.go create mode 100644 internal/core_strategy.go rename internal/{core_owner_storage.go => oauth2_owner_storage.go} (96%) rename internal/{core_refresh_storage.go => oauth2_refresh_storage.go} (96%) rename internal/{oidc_id_token_storage.go => openid_id_token_storage.go} (96%) delete mode 100644 scope.go create mode 100644 scope_strategy.go create mode 100644 scope_strategy_test.go delete mode 100644 scope_test.go diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 000000000..7a874978e --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,21 @@ +This is a list of breaking changes. As long as `1.0.0` is not released, breaking changes will be addressed as minor version +bumps (`0.1.0` -> `0.2.0`). + +## 0.2.0 + +Breaking changes: + +* Token validation refactored: `ValidateRequestAuthorization` is now `Validate` and does not require a http request +but instead a token and a token hint. A token can be anything, including authorization codes, refresh tokens, +id tokens, ... +* Remove mandatory scope: The mandatory scope (`fosite`) has been removed as it has proven impractical. +* Allowed OAuth2 Client scopes are now being set with `scope` instead of `granted_scopes` when using the DefaultClient. +* There is now a scope matching strategy that can be replaced. +* OAuth2 Client scopes are now checked on every grant type. +* Handler subpackages such as `core/client` or `oidc/explicit` have been merged and moved one level up +* `handler/oidc` is now `handler/openid` +* `handler/core` is now `handler/oauth2` + +## 0.1.0 + +Initial release \ No newline at end of file diff --git a/README.md b/README.md index e562bf1a4..b51678ef3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # ![Fosite security first OAuth2 framework](fosite.png) +[![Build Status](https://travis-ci.org/ory-am/fosite.svg?branch=master)](https://travis-ci.org/ory-am/fosite?branch=master) +[![Coverage Status](https://coveralls.io/repos/ory-am/fosite/badge.svg?branch=master&service=github&foo)](https://coveralls.io/github/ory-am/fosite?branch=master) +[![Go Report Card](https://goreportcard.com/badge/ory-am/fosite)](https://goreportcard.com/report/ory-am/fosite) + +[![Join the chat at https://gitter.im/ory-am/fosite](https://badges.gitter.im/ory-am/fosite.svg)](https://gitter.im/ory-am/fosite) + **The security first OAuth2 & OpenID Connect framework for [Google's Go Language](https://golang.org).** Built simple, powerful and extensible. This library implements peer-reviewed [IETF RFC6749](https://tools.ietf.org/html/rfc6749), counterfeits weaknesses covered in peer-reviewed [IETF RFC6819](https://tools.ietf.org/html/rfc6819) and countermeasures various database @@ -7,36 +13,36 @@ attack scenarios, keeping your application safe when that hacker penetrates or l implemented according to [OpenID Connect Core 1.0 incorporating errata set 1](openid.net/specs/openid-connect-core-1_0.html) and includes all flows: code, implicit, hybrid. - -[![Build Status](https://travis-ci.org/ory-am/fosite.svg?branch=master)](https://travis-ci.org/ory-am/fosite?branch=master) -[![Coverage Status](https://coveralls.io/repos/ory-am/fosite/badge.svg?branch=master&service=github&foo)](https://coveralls.io/github/ory-am/fosite?branch=master) -[![Go Report Card](https://goreportcard.com/badge/ory-am/fosite)](https://goreportcard.com/report/ory-am/fosite) - -During development, we reviewed the following open specifications: -* [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) +This library considered and implemented: * [The OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749) * [OAuth 2.0 Multiple Response Type Encoding Practices](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html) -* [OAuth 2.0 Threat Model and Security Considerations](https://tools.ietf.org/html/rfc6819) +* [OAuth 2.0 Threat Model and Security Considerations](https://tools.ietf.org/html/rfc6819) (partially) +* [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) (partially) + +OAuth2 and OpenID Connect are difficult protocols. If you want quick wins, we strongly encourage you to look at [Hydra](https://github.com/ory-am/hydra). +Hydra is a secure, high performance, cloud native OAuth2 and OpenID Connect service that integrates with every authentication method +imaginable and is built on top of Fosite. **Table of Contents** -- [Motivation](#motivation) -- [API Stability](#api-stability) -- [Example](#example) -- [A word on quality](#a-word-on-quality) -- [A word on security](#a-word-on-security) -- [A word on extensibility](#a-word-on-extensibility) -- [Usage](#usage) + - [Motivation](#motivation) + - [API Stability](#api-stability) + - [Example](#example) + - [A word on quality](#a-word-on-quality) + - [A word on security](#a-word-on-security) + - [A word on extensibility](#a-word-on-extensibility) - [Installation](#installation) -- [Examples](#examples) - - [Exemplary Storage Implementation](#exemplary-storage-implementation) - - [Extensible handlers](#extensible-handlers) -- [Develop fosite](#develop-fosite) - - [Refresh mock objects](#refresh-mock-objects) -- [Known Limitations and Issues](#known-limitations-and-issues) -- [Hall of Fame](#hall-of-fame) + - [Documentation](#documentation) + - [Scopes](#scopes) + - [Quickstart](#quickstart) + - [Code Examples](#code-examples) + - [Exemplary Storage Implementation](#exemplary-storage-implementation) + - [Extensible handlers](#extensible-handlers) + - [Contribute](#contribute) + - [Refresh mock objects](#refresh-mock-objects) + - [Hall of Fame](#hall-of-fame) @@ -115,9 +121,6 @@ Additionally, we added these safeguards: using a global secret. This is what a token can look like: `/tgBeUhWlAT8tM8Bhmnx+Amf8rOYOUhrDi3pGzmjP7c=.BiV/Yhma+5moTP46anxMT6cWW8gz5R5vpC9RbpwSDdM=` -* **Enforging scopes:** By default, you always need to include the `fosite` scope or fosite will not execute the request - properly. Obviously, you can change the scope to `basic` or `core` but be aware that you should use scopes if you use - OAuth2. Sections below [Section 5](https://tools.ietf.org/html/rfc6819#section-5) that are not covered in the list above should be reviewed by you. If you think that a specific section should be something @@ -135,11 +138,7 @@ have been validated against the OAuth2 specs beforehand. You can easily extend Fosite's capabilities. For example, if you want to provide OpenID Connect on top of your OAuth2 stack, that's no problem. Or custom assertions, what ever you like and as long as it is secure. ;) -## Usage - -There is an API documentation available at [godoc.org/ory-am/fosite](https://godoc.org/github.com/ory-am/fosite). - -### Installation +## Installation You will need [Go](https://golang.org) installed on your machine and it is required that you have set up your GOPATH environment variable. @@ -148,84 +147,206 @@ GOPATH environment variable. go get -d github.com/ory-am/fosite ``` -We recommend to use [Glide](https://github.com/Masterminds/glide) or [Godep](https://github.com/tools/godep), because -there might be breaking changes in the future. +We recommend to use [Glide](https://github.com/Masterminds/glide) or [Godep](https://github.com/tools/godep) to +mitigate compatibility breaks that come with new api versions. -## Examples +## Documentation -Take a look at these real-life implementations: -* [Integration tests](integration/) -* [Request validation](integration/helper_endpoints_test.go) (check `func tokenInfoHandler`) -* [Fully functional example app with all OpenID Connect and OAuth2 flows enabled](fosite-example/main.go) +There is an API documentation available at [godoc.org/ory-am/fosite](https://godoc.org/github.com/ory-am/fosite). -### Exemplary Storage Implementation +### Scopes -Fosite does not ship a storage implementation yet. To get fosite running, you need to implement `github.com/ory-am/fosite.Storage`. -Additionally, most of the token / authorize endpoint handlers require a dedicated store implementation as well. You could however use one struct -to implement all the signatures. +Before we skip ahead, it is important to note how scopes are being used in fosite per default. +The general pattern can be abstracted as `...`. Every level ("subset") +contains all sublevels, too. For example: -You can find a working store implementation at [fosite-example/internal/store.go](fosite-example/internal/store.go). +* `blogposts` grants scopes `blogposts.create`, `blogposts.delete`, `blockposts.read`, `blohposts.own.read`. +* `blogposts.create` does not grant `blogposts.delete` nor `blogposts.read` nor `blogposts.own.read`. +* `blogposts.own.read` does not grant `blogposts.create`. +* `blogposts.own` grants `blogposts.own.read` but not `blogposts.create`. -### Extensible handlers +You can replace the default scope strategy if you need a custom one by implementing `fosite.ScopeStrategy`. + +### Quickstart -You can replace the Token and Authorize endpoint logic by modifying `Fosite.TokenEndpointHandlers` and -`Fosite.AuthorizeEndpointHandlers`. +Instantiating fosite by hand can be painful. Therefore we created a few convenience helpers available through the [compose package](/compose). +It is strongly encouraged to use these well tested composers. -Let's take the explicit authorize handler. He is responsible for handling the -[authorize code workflow](https://aaronparecki.com/articles/2012/07/29/1/oauth2-simplified#web-server-apps). +In this very basic example, we will instantiate fosite with all OpenID Connect and OAuth2 handlers enabled. Please refer +to the [example app](fosite-example/main.go) for more details. -If you want to enable the handler able to handle this workflow, you can do this: +This little code snippet sets up a full-blown OAuth2 and OpenID Connect example. ```go -var accessTokenLifespan = time.Hour -var authorizeCodeLifespan = time.Minute * 10 - -var hmacStrategy = &strategy.HMACSHAStrategy{ - Enigma: &enigma.Enigma{ - GlobalSecret: []byte("some-super-cool-secret-that-nobody-knows"), - }, - AccessTokenLifespan: accessTokenLifespan, - AuthorizeCodeLifespan: authorizeCodeLifespan, +import "github.com/ory-am/fosite" +import "github.com/ory-am/fosite/compose" +import "github.com/ory-am/fosite/fosite-example/store" + +// This is the exemplary storage that contains: +// * an OAuth2 Client with id "my-client" and secret "foobar" capable of all oauth2 and open id connect grant and response types. +// * a User for the resource owner password credentials grant type with usename "peter" and password "secret". +// +// You will most likely replace this with your own logic once you set up a real world application. +var storage = store.NewExampleStore() + +// This secret is being used to sign access and refresh tokens as well as authorize codes. +var secret = []byte{"my super secret password"} + +// check the api docs of compose.Config for further configuration options +var config = compose.Config { + AccessTokenLifespan: time.Minute * 30, + // ... } -// var store = ... +var oauth2Provider = compose.ComposeAllEnabled(config *Config, storage, secret, privateKey) + +// The session will be persisted by the store and made available when e.g. validating tokens or handling token endpoint requests. +// The default OAuth2 and OpenID Connect handlers require the session to implement a few methods. Apart from that, the +// session struct can be anything you want it to be. +type session struct { + UserID string + Foobar int + + // this is used in the OAuth2 handlers. You can set per-session expiry times here, for example. + *oauth2Strat.HMACSession + + // this is used by the OpenID connect handlers. You can set custom id token headers and claims. + *oidcStrat.DefaultSession +} -var f = NewFosite(store) +// The authorize endpoint is usually at "https://mydomain.com/oauth2/auth". +func authorizeHandlerFunc(rw http.ResponseWriter, req *http.Request) { + // This context will be passed to all methods. It doesn't fulfill a real purpose in the standard library but could be used + // to abort database lookups or similar things. + ctx := NewContext() + + // Let's create an AuthorizeRequest object! + // It will analyze the request and extract important information like scopes, response type and others. + ar, err := oauth2.NewAuthorizeRequest(ctx, req) + if err != nil { + oauth2.WriteAuthorizeError(rw, ar, err) + return + } + + // Normally, this would be the place where you would check if the user is logged in and gives his consent. + // We're simplifying things and just checking if the request includes a valid username and password + if req.Form.Get("username") != "peter" { + rw.Header().Set("Content-Type", "text/html; charset=utf-8") + rw.Write([]byte(`

Login page

`)) + rw.Write([]byte(` +

Howdy! This is the log in page. For this example, it is enough to supply the username.

+
+ try peter
+ +
+ `)) + return + } + + // Now that the user is authorized, we set up a session. When validating / looking up tokens, we additionally get + // the session. You can store anything you want in it. + mySessionData := &session{ + UserID: req.Form.Get("username") + } + + // It's also wise to check the requested scopes, e.g.: + // if authorizeRequest.GetScopes().Has("admin") { + // http.Error(rw, "you're not allowed to do that", http.StatusForbidden) + // return + // } + + // Now we need to get a response. This is the place where the AuthorizeEndpointHandlers kick in and start processing the request. + // NewAuthorizeResponse is capable of running multiple response type handlers which in turn enables this library + // to support open id connect. + response, err := oauth2.NewAuthorizeResponse(ctx, req, ar, mySessionData) + if err != nil { + oauth2.WriteAuthorizeError(rw, ar, err) + return + } + + // Awesome, now we redirect back to the client redirect uri and pass along an authorize code + oauth2.WriteAuthorizeResponse(rw, ar, response) +} -// Let's enable the explicit authorize code grant! -explicitHandler := &explicit.AuthorizeExplicitGrantTypeHandler{ - AccessTokenStrategy: hmacStrategy, - RefreshTokenStrategy: hmacStrategy, - AuthorizeCodeStrategy: hmacStrategy, - Store: store, - AuthCodeLifespan: authorizeCodeLifespan, - AccessTokenLifespan: accessTokenLifespan, +// The token endpoint is usually at "https://mydomain.com/oauth2/token" +func tokenHandlerFunc(rw http.ResponseWriter, req *http.Request) { + ctx := NewContext() + mySessionData := &session{} + + // This will create an access request object and iterate through the registered TokenEndpointHandlers to validate the request. + accessRequest, err := oauth2.NewAccessRequest(ctx, req, mySessionData) + if err != nil { + oauth2.WriteAccessError(rw, accessRequest, err) + return + } + + if mySessionData.UserID == "super-admin-guy" { + // do something... + } + + // Next we create a response for the access request. Again, we iterate through the TokenEndpointHandlers + // and aggregate the result in response. + response, err := oauth2.NewAccessResponse(ctx, req, accessRequest) + if err != nil { + oauth2.WriteAccessError(rw, accessRequest, err) + return + } + + // All done, send the response. + oauth2.WriteAccessResponse(rw, accessRequest, response) + + // The client has a valid access token now } -// Please note that order matters! -f.AuthorizeEndpointHandlers.Append(explicitHandler) -f.TokenEndpointHandlers.Append(explicitHandler) +func someResourceProviderHandlerFunc(rw http.ResponseWriter, req *http.Request) { + ctx := NewContext() + mySessionData := &session{} + requiredScope := "blogposts.create" + + ar, err := oauth2.ValidateToken(ctx, fosite.AccessTokenFromRequest(req), mySessionData, requiredScope) + if err != nil { + // ... + } + + // if now error occurred the token + scope is valid and you have access to: + // ar.GetClient().GetID(), ar.GetGrantedScopes(), ar.GetScopes(), mySessionData.UserID, ar.GetRequestedAt(), ... +} ``` -As you probably noticed, there are two types of handlers, one for the [authorization */auth* endpoint](https://tools.ietf.org/html/rfc6749#section-3.1) and one for the [token -*/token* endpoint](https://tools.ietf.org/html/rfc6749#section-3.2). The `AuthorizeExplicitEndpointHandler` implements -API requirements for both endpoints, while, for example, the `AuthorizeImplicitEndpointHandler` only implements -the `AuthorizeEndpointHandler` API. +### Code Examples + +Fosite provides integration tests as well as a http server example: -You can find a complete list of handlers inside the [handler directory](handler). A short list is documented here: +* Fosite ships with an example app that runs in your browser: [Example app](fosite-example/main.go). +* If you want to check out how to enable specific handlers, check out the [integration tests](integration/). -* `github.com/ory-am/fosite/handler/core/explicit.AuthorizeExplicitEndpointHandler` implements the - [Authorization Code Grant](https://tools.ietf.org/html/rfc6749#section-4.1) -* `github.com/ory-am/fosite/handler/core/implicit.AuthorizeImplicitEndpointHandler` implements the - [Implicit Grant](https://tools.ietf.org/html/rfc6749#section-4.2) -* `github.com/ory-am/fosite/handler/core/token/owner.TokenROPasswordCredentialsEndpointHandler` implements the - [Resource Owner Password Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.3) -* `github.com/ory-am/fosite/handler/core/token/client.TokenClientCredentialsEndpointHandler` implements the - [Client Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.4) +If you have working examples yourself, please share them with us! -There are also [OpenID Connect Handlers available](handler/oidc). +### Exemplary Storage Implementation -## Develop fosite +Fosite does not ship a storage implementation. This is intended, because requirements vary with every environment. +You can find a reference implementation at [fosite-example/internal/store.go](fosite-example/internal/store.go). +This storage fulfills requirements from all OAuth2 and OpenID Conenct handlers. + +### Extensible handlers + +OAuth2 is a framework. Fosite mimics this behaviour by enabling you to replace existing or create new OAuth2 handlers. +Of course, fosite ships handlers for all OAuth2 and OpenID Connect flows. + +* **[Fosite OAuth2 Core Handlers](handler/oauth2)** implement the [Client Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.4), + [Resource Owner Password Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.3), + [Implicit Grant](https://tools.ietf.org/html/rfc6749#section-4.2), + [Authorization Code Grant](https://tools.ietf.org/html/rfc6749#section-4.1), + [Refresh Token Grant](https://tools.ietf.org/html/rfc6749#section-6) +* **[Fosite OpenID Connect Handlers](handler/openid)** implement the + [Authentication using the Authorization Code Flow](http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth), + [Authentication using the Implicit Flow](http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth), + [Authentication using the Hybrid Flow](http://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth) + + +This section is missing documentation and we welcome any contributions in that direction. + +## Contribute You need git and golang installed on your system. @@ -238,16 +359,12 @@ go test ./... ``` Simple, right? Now you are ready to go! Make sure to run `go test ./...` often, detecting problems with your code -rather sooner than later. +rather sooner than later. Please read [CONTRIBUTE.md] before creating pull requests and issues. ### Refresh mock objects Run `./generate-mocks.sh` in fosite's root directory or run the contents of [generate-mocks.sh] in a shell. -## Known Limitations and Issues - -* Validator handler receives empty Request object - ## Hall of Fame This place is reserved for the fearless bug hunters, reviewers and contributors (alphabetical order). diff --git a/access_request_handler.go b/access_request_handler.go index 203841b88..00351042c 100644 --- a/access_request_handler.go +++ b/access_request_handler.go @@ -47,7 +47,7 @@ func (f *Fosite) NewAccessRequest(ctx context.Context, r *http.Request, session return accessRequest, errors.New("Session must not be nil") } - accessRequest.Scopes = removeEmpty(strings.Split(r.PostForm.Get("scope"), " ")) + 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") @@ -83,11 +83,5 @@ func (f *Fosite) NewAccessRequest(ctx context.Context, r *http.Request, session if !found { return nil, errors.Wrap(ErrInvalidRequest, "") } - - if !accessRequest.GetScopes().Has(f.GetMandatoryScope()) { - return accessRequest, errors.Wrap(ErrInvalidScope, "") - } - - accessRequest.GrantScope(f.GetMandatoryScope()) return accessRequest, nil } diff --git a/access_request_handler_test.go b/access_request_handler_test.go index 62df3c033..9b7b0aead 100644 --- a/access_request_handler_test.go +++ b/access_request_handler_test.go @@ -13,7 +13,6 @@ import ( "github.com/ory-am/fosite/internal" "github.com/pkg/errors" "github.com/stretchr/testify/assert" - "golang.org/x/net/context" ) func TestNewAccessRequest(t *testing.T) { @@ -140,28 +139,7 @@ func TestNewAccessRequest(t *testing.T) { store.EXPECT().GetClient(gomock.Eq("foo")).Return(client, nil) client.EXPECT().GetHashedSecret().Return([]byte("foo")) hasher.EXPECT().Compare(gomock.Eq([]byte("foo")), gomock.Eq([]byte("bar"))).Return(nil) - handler.EXPECT().HandleTokenEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(_ context.Context, _ *http.Request, a AccessRequester) { - a.SetScopes([]string{"asdfasdf"}) - }).Return(nil) - }, - handlers: TokenEndpointHandlers{handler}, - expectErr: ErrInvalidScope, - }, - { - header: http.Header{ - "Authorization": {basicAuth("foo", "bar")}, - }, - method: "POST", - form: url.Values{ - "grant_type": {"foo"}, - }, - mock: func() { - store.EXPECT().GetClient(gomock.Eq("foo")).Return(client, nil) - client.EXPECT().GetHashedSecret().Return([]byte("foo")) - hasher.EXPECT().Compare(gomock.Eq([]byte("foo")), gomock.Eq([]byte("bar"))).Return(nil) - handler.EXPECT().HandleTokenEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(_ context.Context, _ *http.Request, a AccessRequester) { - a.SetScopes([]string{DefaultMandatoryScope}) - }).Return(nil) + handler.EXPECT().HandleTokenEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) }, handlers: TokenEndpointHandlers{handler}, expect: &AccessRequest{ diff --git a/authorize_request_handler.go b/authorize_request_handler.go index 3279af81f..5295f31ac 100644 --- a/authorize_request_handler.go +++ b/authorize_request_handler.go @@ -62,12 +62,6 @@ func (c *Fosite) NewAuthorizeRequest(ctx context.Context, r *http.Request) (Auth request.State = state // Remove empty items from arrays - request.Scopes = removeEmpty(strings.Split(r.Form.Get("scope"), " ")) - - if !request.Scopes.Has(c.GetMandatoryScope()) { - return request, errors.Wrap(ErrInvalidScope, "mandatory scope is missing") - } - request.GrantScope(c.GetMandatoryScope()) - + request.SetRequestedScopes(removeEmpty(strings.Split(r.Form.Get("scope"), " "))) return request, nil } diff --git a/authorize_request_handler_test.go b/authorize_request_handler_test.go index 55bb3cd4c..55bf47beb 100644 --- a/authorize_request_handler_test.go +++ b/authorize_request_handler_test.go @@ -133,21 +133,6 @@ func TestNewAuthorizeRequest(t *testing.T) { }, }, /* success case */ - { - desc: "should pass", - conf: &Fosite{Store: store}, - query: url.Values{ - "redirect_uri": {"https://foo.bar/cb"}, - "client_id": {"1234"}, - "response_type": {"code"}, - "state": {"strong-state"}, - "scope": {"foo bar"}, - }, - mock: func() { - store.EXPECT().GetClient("1234").Return(&DefaultClient{RedirectURIs: []string{"https://foo.bar/cb"}}, nil) - }, - expectedError: ErrInvalidScope, - }, { desc: "should pass", conf: &Fosite{Store: store}, @@ -156,7 +141,7 @@ func TestNewAuthorizeRequest(t *testing.T) { "client_id": {"1234"}, "response_type": {"code token"}, "state": {"strong-state"}, - "scope": {DefaultMandatoryScope + " foo bar"}, + "scope": {"foo bar"}, }, mock: func() { store.EXPECT().GetClient("1234").Return(&DefaultClient{RedirectURIs: []string{"https://foo.bar/cb"}}, nil) @@ -167,7 +152,7 @@ func TestNewAuthorizeRequest(t *testing.T) { State: "strong-state", Request: Request{ Client: &DefaultClient{RedirectURIs: []string{"https://foo.bar/cb"}}, - Scopes: []string{DefaultMandatoryScope, "foo", "bar"}, + Scopes: []string{"foo", "bar"}, }, }, }, diff --git a/authorize_request_test.go b/authorize_request_test.go index 5e6e51d17..9c18dcf91 100644 --- a/authorize_request_test.go +++ b/authorize_request_test.go @@ -91,15 +91,15 @@ func TestAuthorizeRequest(t *testing.T) { assert.Equal(t, c.ar.RedirectURI, c.ar.GetRedirectURI(), "%d", k) assert.Equal(t, c.ar.RequestedAt, c.ar.GetRequestedAt(), "%d", k) assert.Equal(t, c.ar.ResponseTypes, c.ar.GetResponseTypes(), "%d", k) - assert.Equal(t, c.ar.Scopes, c.ar.GetScopes(), "%d", k) + assert.Equal(t, c.ar.Scopes, c.ar.GetRequestedScopes(), "%d", k) assert.Equal(t, c.ar.State, c.ar.GetState(), "%d", k) assert.Equal(t, c.isRedirValid, c.ar.IsRedirectURIValid(), "%d", k) c.ar.GrantScope("foo") c.ar.SetSession(&struct{}{}) - c.ar.SetScopes([]string{"foo"}) + c.ar.SetRequestedScopes([]string{"foo"}) assert.True(t, c.ar.GetGrantedScopes().Has("foo")) - assert.True(t, c.ar.GetScopes().Has("foo")) + assert.True(t, c.ar.GetRequestedScopes().Has("foo")) assert.Equal(t, &struct{}{}, c.ar.GetSession()) } } diff --git a/client.go b/client.go index 46d295904..c875e3c57 100644 --- a/client.go +++ b/client.go @@ -1,13 +1,5 @@ package fosite -import "strings" - -// Scopes is a list of scopes. -type Scopes interface { - // Fulfill returns true if requestScope is fulfilled by the scope list. - Grant(requestScope string) bool -} - // Client represents a client or an app. type Client interface { // GetID returns the client ID. @@ -28,8 +20,8 @@ type Client interface { // Returns the client's owner. GetOwner() string - // Returns the scopes this client was granted. - GetGrantedScopes() Scopes + // Returns the scopes this client is allowed to request. + GetScopes() Arguments } // DefaultClient is a simple default implementation of the Client interface. @@ -40,7 +32,7 @@ type DefaultClient struct { RedirectURIs []string `json:"redirect_uris" gorethink:"redirect_uris"` GrantTypes []string `json:"grant_types" gorethink:"grant_types"` ResponseTypes []string `json:"response_types" gorethink:"response_types"` - GrantedScopes []string `json:"granted_scopes" gorethink:"granted_scopes"` + Scopes []string `json:"scopes" gorethink:"scopes"` Owner string `json:"owner" gorethink:"owner"` PolicyURI string `json:"policy_uri" gorethink:"policy_uri"` TermsOfServiceURI string `json:"tos_uri" gorethink:"tos_uri"` @@ -49,40 +41,6 @@ type DefaultClient struct { Contacts []string `json:"contacts" gorethink:"contacts"` } -type DefaultScopes struct { - Scopes []string -} - -func (s *DefaultScopes) Grant(requestScope string) bool { - for _, scope := range s.Scopes { - // foo == foo -> true - if scope == requestScope { - return true - } - - // picture.read > picture -> false (scope picture includes read, write, ...) - if len(scope) > len(requestScope) { - continue - } - - needles := strings.Split(requestScope, ".") - haystack := strings.Split(scope, ".") - haystackLen := len(haystack) - 1 - for k, needle := range needles { - if haystackLen < k { - return true - } - - current := haystack[k] - if current != needle { - continue - } - } - } - - return false -} - func (c *DefaultClient) GetID() string { return c.ID } @@ -95,10 +53,8 @@ func (c *DefaultClient) GetHashedSecret() []byte { return c.Secret } -func (c *DefaultClient) GetGrantedScopes() Scopes { - return &DefaultScopes{ - Scopes: c.GrantedScopes, - } +func (c *DefaultClient) GetScopes() Arguments { + return c.Scopes } func (c *DefaultClient) GetGrantTypes() Arguments { diff --git a/client_test.go b/client_test.go index 6037a9e1a..671b2924e 100644 --- a/client_test.go +++ b/client_test.go @@ -13,41 +13,18 @@ func TestDefaultClient(t *testing.T) { RedirectURIs: []string{"foo", "bar"}, ResponseTypes: []string{"foo", "bar"}, GrantTypes: []string{"foo", "bar"}, + Scopes: []string{"fooscope"}, } + assert.Equal(t, sc.ID, sc.GetID()) assert.Equal(t, sc.RedirectURIs, sc.GetRedirectURIs()) assert.Equal(t, sc.Secret, sc.GetHashedSecret()) assert.EqualValues(t, sc.ResponseTypes, sc.GetResponseTypes()) assert.EqualValues(t, sc.GrantTypes, sc.GetGrantTypes()) - - assert.False(t, sc.GetGrantedScopes().Grant("foo.bar.baz")) - assert.False(t, sc.GetGrantedScopes().Grant("foo.bar")) - assert.False(t, sc.GetGrantedScopes().Grant("foo")) - - sc.GrantedScopes = []string{"foo.bar", "bar.baz", "baz.baz.1", "baz.baz.2", "baz.baz.3", "baz.baz.baz"} - assert.True(t, sc.GetGrantedScopes().Grant("foo.bar.baz")) - assert.True(t, sc.GetGrantedScopes().Grant("baz.baz.baz")) - assert.True(t, sc.GetGrantedScopes().Grant("foo.bar")) - assert.False(t, sc.GetGrantedScopes().Grant("foo")) - - assert.True(t, sc.GetGrantedScopes().Grant("bar.baz")) - assert.True(t, sc.GetGrantedScopes().Grant("bar.baz.zad")) - assert.False(t, sc.GetGrantedScopes().Grant("bar")) - - assert.False(t, sc.GetGrantedScopes().Grant("baz")) - - sc.GrantedScopes = []string{"fosite.keys.create", "fosite.keys.get", "fosite.keys.delete", "fosite.keys.update"} - assert.True(t, sc.GetGrantedScopes().Grant("fosite.keys.delete")) - assert.True(t, sc.GetGrantedScopes().Grant("fosite.keys.get")) - assert.True(t, sc.GetGrantedScopes().Grant("fosite.keys.get")) - assert.True(t, sc.GetGrantedScopes().Grant("fosite.keys.update")) + assert.EqualValues(t, sc.Scopes, sc.GetScopes()) sc.GrantTypes = []string{} sc.ResponseTypes = []string{} assert.Equal(t, "code", sc.GetResponseTypes()[0]) assert.Equal(t, "authorization_code", sc.GetGrantTypes()[0]) } - -func TestDefaultScope(t *testing.T) { - -} diff --git a/compose/compose.go b/compose/compose.go new file mode 100644 index 000000000..2f9ff005c --- /dev/null +++ b/compose/compose.go @@ -0,0 +1,79 @@ +package compose + +import ( + "crypto/rsa" + + "github.com/Sirupsen/logrus" + "github.com/ory-am/fosite" + "github.com/ory-am/fosite/hash" +) + +type handler func(config *Config, storage interface{}, strategy interface{}) interface{} + +// Compose takes a config, a storage, a strategy and handlers to instantiate an OAuth2Provider: +// +// import "github.com/ory-am/fosite/compose" +// +// // var storage = new(MyFositeStorage) +// var config = Config { +// AccessTokenLifespan: time.Minute * 30, +// // check Config for further configuration options +// } +// +// var strategy = NewOAuth2HMACStrategy(config) +// +// var oauth2Provider = Compose( +// config, +// storage, +// strategy, +// NewOAuth2AuthorizeExplicitHandler, +// OAuth2ClientCredentialsGrantFactory, +// // for a complete list refer to the docs of this package +// ) +func Compose(config *Config, storage interface{}, strategy interface{}, handlers ...handler) fosite.OAuth2Provider { + f := &fosite.Fosite{ + Store: storage.(fosite.Storage), + AuthorizeEndpointHandlers: fosite.AuthorizeEndpointHandlers{}, + TokenEndpointHandlers: fosite.TokenEndpointHandlers{}, + TokenValidators: fosite.TokenValidators{}, + Hasher: &hash.BCrypt{WorkFactor: config.GetHashCost()}, + Logger: &logrus.Logger{}, + ScopeStrategy: fosite.HierarchicScopeStrategy, + } + + for _, h := range handlers { + res := h(config, storage, strategy) + if ah, ok := res.(fosite.AuthorizeEndpointHandler); ok { + f.AuthorizeEndpointHandlers.Append(ah) + } + if th, ok := res.(fosite.TokenEndpointHandler); ok { + f.TokenEndpointHandlers.Append(th) + } + if tv, ok := res.(fosite.TokenValidator); ok { + f.TokenValidators.Append(tv) + } + } + + return f +} + +// ComposeAllEnabled returns a fosite instance with all OAuth2 and OpenID Connect handlers enabled. +func ComposeAllEnabled(config *Config, storage interface{}, secret []byte, key *rsa.PrivateKey) fosite.OAuth2Provider { + return Compose( + config, + storage, + &CommonStrategy{ + CoreStrategy: NewOAuth2HMACStrategy(config, secret), + OpenIDConnectTokenStrategy: NewOpenIDConnectStrategy(key), + }, + OAuth2AuthorizeExplicitFactory, + OAuth2AuthorizeImplicitFactory, + OAuth2ClientCredentialsGrantFactory, + OAuth2RefreshTokenGrantFactory, + OAuth2ResourceOwnerPasswordCredentialsFactory, + + OpenIDConnectExplicit, + OpenIDConnectImplicit, + OpenIDConnectHybrid, + ) +} diff --git a/compose/compose_oauth2.go b/compose/compose_oauth2.go new file mode 100644 index 000000000..09cc526b7 --- /dev/null +++ b/compose/compose_oauth2.go @@ -0,0 +1,119 @@ +package compose + +import ( + "github.com/ory-am/fosite" + "github.com/ory-am/fosite/handler/oauth2" +) + +// OAuth2AuthorizeExplicitFactory creates an OAuth2 authorize code grant ("authorize explicit flow") handler and registers +// an access token, refresh token and authorize code validator. +func OAuth2AuthorizeExplicitFactory(config *Config, storage interface{}, strategy interface{}) interface{} { + return &struct { + *oauth2.AuthorizeExplicitGrantHandler + *oauth2.CoreValidator + }{ + AuthorizeExplicitGrantHandler: &oauth2.AuthorizeExplicitGrantHandler{ + AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), + RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), + AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy), + AuthorizeCodeGrantStorage: storage.(oauth2.AuthorizeCodeGrantStorage), + AuthCodeLifespan: config.GetAuthorizeCodeLifespan(), + AccessTokenLifespan: config.GetAccessTokenLifespan(), + ScopeStrategy: fosite.HierarchicScopeStrategy, + }, + CoreValidator: &oauth2.CoreValidator{ + CoreStrategy: strategy.(oauth2.CoreStrategy), + CoreStorage: storage.(oauth2.CoreStorage), + ScopeStrategy: fosite.HierarchicScopeStrategy, + }, + } +} + +// OAuth2ClientCredentialsGrantFactory creates an OAuth2 client credentials grant handler and registers +// an access token, refresh token and authorize code validator. +func OAuth2ClientCredentialsGrantFactory(config *Config, storage interface{}, strategy interface{}) interface{} { + return &struct { + *oauth2.ClientCredentialsGrantHandler + *oauth2.CoreValidator + }{ + ClientCredentialsGrantHandler: &oauth2.ClientCredentialsGrantHandler{ + HandleHelper: &oauth2.HandleHelper{ + AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), + AccessTokenStorage: storage.(oauth2.AccessTokenStorage), + AccessTokenLifespan: config.GetAccessTokenLifespan(), + }, + ScopeStrategy: fosite.HierarchicScopeStrategy, + }, + CoreValidator: &oauth2.CoreValidator{ + CoreStrategy: strategy.(oauth2.CoreStrategy), + CoreStorage: storage.(oauth2.CoreStorage), + ScopeStrategy: fosite.HierarchicScopeStrategy, + }, + } +} + +// OAuth2RefreshTokenGrantFactory creates an OAuth2 refresh grant handler and registers +// an access token, refresh token and authorize code validator. +func OAuth2RefreshTokenGrantFactory(config *Config, storage interface{}, strategy interface{}) interface{} { + return &struct { + *oauth2.RefreshTokenGrantHandler + *oauth2.CoreValidator + }{ + RefreshTokenGrantHandler: &oauth2.RefreshTokenGrantHandler{ + AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), + RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), + RefreshTokenGrantStorage: storage.(oauth2.RefreshTokenGrantStorage), + AccessTokenLifespan: config.GetAccessTokenLifespan(), + }, + CoreValidator: &oauth2.CoreValidator{ + CoreStrategy: strategy.(oauth2.CoreStrategy), + CoreStorage: storage.(oauth2.CoreStorage), + ScopeStrategy: fosite.HierarchicScopeStrategy, + }, + } +} + +// OAuth2AuthorizeImplicitFactory creates an OAuth2 implicit grant ("authorize implicit flow") handler and registers +// an access token, refresh token and authorize code validator. +func OAuth2AuthorizeImplicitFactory(config *Config, storage interface{}, strategy interface{}) interface{} { + return &struct { + *oauth2.AuthorizeImplicitGrantTypeHandler + *oauth2.CoreValidator + }{ + AuthorizeImplicitGrantTypeHandler: &oauth2.AuthorizeImplicitGrantTypeHandler{ + AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), + AccessTokenStorage: storage.(oauth2.AccessTokenStorage), + AccessTokenLifespan: config.GetAccessTokenLifespan(), + ScopeStrategy: fosite.HierarchicScopeStrategy, + }, + CoreValidator: &oauth2.CoreValidator{ + CoreStrategy: strategy.(oauth2.CoreStrategy), + CoreStorage: storage.(oauth2.CoreStorage), + ScopeStrategy: fosite.HierarchicScopeStrategy, + }, + } +} + +// OAuth2ResourceOwnerPasswordCredentialsFactory creates an OAuth2 resource owner password credentials grant handler and registers +// an access token, refresh token and authorize code validator. +func OAuth2ResourceOwnerPasswordCredentialsFactory(config *Config, storage interface{}, strategy interface{}) interface{} { + return &struct { + *oauth2.ResourceOwnerPasswordCredentialsGrantHandler + *oauth2.CoreValidator + }{ + ResourceOwnerPasswordCredentialsGrantHandler: &oauth2.ResourceOwnerPasswordCredentialsGrantHandler{ + ResourceOwnerPasswordCredentialsGrantStorage: storage.(oauth2.ResourceOwnerPasswordCredentialsGrantStorage), + HandleHelper: &oauth2.HandleHelper{ + AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), + AccessTokenStorage: storage.(oauth2.AccessTokenStorage), + AccessTokenLifespan: config.GetAccessTokenLifespan(), + }, + ScopeStrategy: fosite.HierarchicScopeStrategy, + }, + CoreValidator: &oauth2.CoreValidator{ + CoreStrategy: strategy.(oauth2.CoreStrategy), + CoreStorage: storage.(oauth2.CoreStorage), + ScopeStrategy: fosite.HierarchicScopeStrategy, + }, + } +} diff --git a/compose/compose_openid.go b/compose/compose_openid.go new file mode 100644 index 000000000..3816d3960 --- /dev/null +++ b/compose/compose_openid.go @@ -0,0 +1,70 @@ +package compose + +import ( + "github.com/ory-am/fosite" + "github.com/ory-am/fosite/handler/oauth2" + "github.com/ory-am/fosite/handler/openid" +) + +// OpenIDConnectExplicit creates an OpenID Connect explicit ("authorize code flow") grant handler. You must add this handler +// *after* you have added an OAuth2 authorize code handler! +func OpenIDConnectExplicit(config *Config, storage interface{}, strategy interface{}) interface{} { + return &struct { + *openid.OpenIDConnectExplicitHandler + }{ + OpenIDConnectExplicitHandler: &openid.OpenIDConnectExplicitHandler{ + OpenIDConnectRequestStorage: storage.(openid.OpenIDConnectRequestStorage), + IDTokenHandleHelper: &openid.IDTokenHandleHelper{ + IDTokenStrategy: strategy.(openid.OpenIDConnectTokenStrategy), + }, + }, + } +} + +// OpenIDConnectImplicit creates an OpenID Connect implicit ("implicit flow") grant handler. You must add this handler +// *after* you have added an OAuth2 authorize implicit handler! +func OpenIDConnectImplicit(config *Config, storage interface{}, strategy interface{}) interface{} { + return &struct { + *openid.OpenIDConnectImplicitHandler + }{ + OpenIDConnectImplicitHandler: &openid.OpenIDConnectImplicitHandler{ + AuthorizeImplicitGrantTypeHandler: &oauth2.AuthorizeImplicitGrantTypeHandler{ + AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), + AccessTokenStorage: storage.(oauth2.AccessTokenStorage), + AccessTokenLifespan: config.GetAccessTokenLifespan(), + }, + ScopeStrategy: fosite.HierarchicScopeStrategy, + IDTokenHandleHelper: &openid.IDTokenHandleHelper{ + IDTokenStrategy: strategy.(openid.OpenIDConnectTokenStrategy), + }, + }, + } +} + +// OpenIDConnectHybrid creates an OpenID Connect hybrid grant handler. You must add this handler +// *after* you have added an OAuth2 authorize code and implicit authorize handler! +func OpenIDConnectHybrid(config *Config, storage interface{}, strategy interface{}) interface{} { + return &struct { + *openid.OpenIDConnectHybridHandler + }{ + OpenIDConnectHybridHandler: &openid.OpenIDConnectHybridHandler{ + AuthorizeExplicitGrantHandler: &oauth2.AuthorizeExplicitGrantHandler{ + AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), + RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), + AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy), + AuthorizeCodeGrantStorage: storage.(oauth2.AuthorizeCodeGrantStorage), + AuthCodeLifespan: config.GetAuthorizeCodeLifespan(), + AccessTokenLifespan: config.GetAccessTokenLifespan(), + }, + ScopeStrategy: fosite.HierarchicScopeStrategy, + AuthorizeImplicitGrantTypeHandler: &oauth2.AuthorizeImplicitGrantTypeHandler{ + AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), + AccessTokenStorage: storage.(oauth2.AccessTokenStorage), + AccessTokenLifespan: config.GetAccessTokenLifespan(), + }, + IDTokenHandleHelper: &openid.IDTokenHandleHelper{ + IDTokenStrategy: strategy.(openid.OpenIDConnectTokenStrategy), + }, + }, + } +} diff --git a/compose/compose_strategy.go b/compose/compose_strategy.go new file mode 100644 index 000000000..946371b99 --- /dev/null +++ b/compose/compose_strategy.go @@ -0,0 +1,41 @@ +package compose + +import ( + "crypto/rsa" + + "github.com/ory-am/fosite/handler/oauth2" + "github.com/ory-am/fosite/handler/openid" + "github.com/ory-am/fosite/token/hmac" + "github.com/ory-am/fosite/token/jwt" +) + +type CommonStrategy struct { + oauth2.CoreStrategy + openid.OpenIDConnectTokenStrategy +} + +func NewOAuth2HMACStrategy(config *Config, secret []byte) *oauth2.HMACSHAStrategy { + return &oauth2.HMACSHAStrategy{ + Enigma: &hmac.HMACStrategy{ + GlobalSecret: secret, + }, + AccessTokenLifespan: config.GetAccessTokenLifespan(), + AuthorizeCodeLifespan: config.GetAuthorizeCodeLifespan(), + } +} + +func NewOAuth2JWTStrategy(key *rsa.PrivateKey) *oauth2.RS256JWTStrategy { + return &oauth2.RS256JWTStrategy{ + RS256JWTStrategy: &jwt.RS256JWTStrategy{ + PrivateKey: key, + }, + } +} + +func NewOpenIDConnectStrategy(key *rsa.PrivateKey) *openid.DefaultStrategy { + return &openid.DefaultStrategy{ + RS256JWTStrategy: &jwt.RS256JWTStrategy{ + PrivateKey: key, + }, + } +} diff --git a/compose/config.go b/compose/config.go new file mode 100644 index 000000000..d015ad16c --- /dev/null +++ b/compose/config.go @@ -0,0 +1,49 @@ +package compose + +import "time" + +type Config struct { + // AccessTokenLifespan sets how long an access token is going to be valid. Defaults to one hour. + AccessTokenLifespan time.Duration + + // AuthorizeCodeLifespan sets how long an authorize code is going to be valid. Defaults to fifteen minutes. + AuthorizeCodeLifespan time.Duration + + // IDTokenLifespan sets how long an id token is going to be valid. Defaults to one hour. + IDTokenLifespan time.Duration + + // HashCost sets the cost of the password hashing cost. Defaults to 12. + HashCost int +} + +// GetAuthorizeCodeLifespan returns how long an authorize code should be valid. Defaults to one fifteen minutes. +func (c *Config) GetAuthorizeCodeLifespan() time.Duration { + if c.AuthorizeCodeLifespan == 0 { + return time.Minute * 15 + } + return c.AuthorizeCodeLifespan +} + +// GeIDTokenLifespan returns how long an id token should be valid. Defaults to one hour. +func (c *Config) GetIDTokenLifespan() time.Duration { + if c.IDTokenLifespan == 0 { + return time.Hour + } + return c.IDTokenLifespan +} + +// GetAccessTokenLifespan returns how long a refresh token should be valid. Defaults to one hour. +func (c *Config) GetAccessTokenLifespan() time.Duration { + if c.AccessTokenLifespan == 0 { + return time.Hour + } + return c.AccessTokenLifespan +} + +// GetAccessTokenLifespan returns how long a refresh token should be valid. Defaults to one hour. +func (c *Config) GetHashCost() int { + if c.HashCost == 0 { + return 12 + } + return c.HashCost +} diff --git a/errors.go b/errors.go index ff7d97e37..aa9d77c97 100644 --- a/errors.go +++ b/errors.go @@ -21,7 +21,7 @@ var ( 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 a misconfiguration") + 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)") ) diff --git a/fosite-example/main.go b/fosite-example/main.go index 49fdd24d0..b3c3e32ba 100644 --- a/fosite-example/main.go +++ b/fosite-example/main.go @@ -1,73 +1,64 @@ package main import ( + "crypto/rand" + "crypto/rsa" "fmt" "log" "net/http" - "net/url" - "reflect" - "time" - "os/exec" + "time" - "crypto/rand" - "crypto/rsa" - - . "github.com/ory-am/fosite" - exampleStore "github.com/ory-am/fosite/fosite-example/store" - "github.com/ory-am/fosite/handler/core" - coreclient "github.com/ory-am/fosite/handler/core/client" - "github.com/ory-am/fosite/handler/core/explicit" - "github.com/ory-am/fosite/handler/core/implicit" - "github.com/ory-am/fosite/handler/core/owner" - "github.com/ory-am/fosite/handler/core/refresh" - "github.com/ory-am/fosite/handler/core/strategy" - "github.com/ory-am/fosite/handler/oidc" - oidcexplicit "github.com/ory-am/fosite/handler/oidc/explicit" - "github.com/ory-am/fosite/handler/oidc/hybrid" - oidcimplicit "github.com/ory-am/fosite/handler/oidc/implicit" - oidcstrategy "github.com/ory-am/fosite/handler/oidc/strategy" - "github.com/ory-am/fosite/token/hmac" + "github.com/ory-am/fosite" + "github.com/ory-am/fosite/compose" + helpers "github.com/ory-am/fosite/fosite-example/pkg" + core "github.com/ory-am/fosite/handler/oauth2" + "github.com/ory-am/fosite/handler/openid" "github.com/ory-am/fosite/token/jwt" - "github.com/parnurzeal/gorequest" "github.com/pkg/errors" goauth "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" ) // This is an exemplary storage instance. We will add a client and a user to it so we can use these later on. -var store = &exampleStore.Store{ - IDSessions: make(map[string]Requester), - Clients: map[string]*DefaultClient{ - "my-client": { - ID: "my-client", - Secret: []byte(`$2a$10$IxMdI6d.LIRZPpSfEwNoeu4rY3FhDREsxFJXikcgdRRAStxUlsuEO`), // = "foobar" - RedirectURIs: []string{"http://localhost:3846/callback"}, - ResponseTypes: []string{"id_token", "code", "token"}, - GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"}, - GrantedScopes: []string{"fosite", "offline"}, - }, - }, - Users: map[string]exampleStore.UserRelation{ - "peter": { - // This store simply checks for equality, a real storage implementation would obviously use - // a hashing algorithm for encrypting the user password. - Username: "peter", - Password: "foobar", - }, - }, - AuthorizeCodes: map[string]Requester{}, - Implicit: map[string]Requester{}, - AccessTokens: map[string]Requester{}, - RefreshTokens: map[string]Requester{}, +var store = helpers.NewExampleStore() + +var config = new(compose.Config) + +// Because we are using oauth2 and open connect id, we use this little helper to combine the two in one +// variable. +var strat = compose.CommonStrategy{ + // alternatively you could use OAuth2Strategy: compose.NewOAuth2JWTStrategy(mustRSAKey()) + CoreStrategy: compose.NewOAuth2HMACStrategy(config, []byte("some-super-cool-secret-that-nobody-knows")), + + // open id connect strategy + OpenIDConnectTokenStrategy: compose.NewOpenIDConnectStrategy(mustRSAKey()), } +var oauth2 = compose.Compose( + config, + store, + strat, + + // enabled handlers + compose.OAuth2AuthorizeExplicitFactory, + compose.OAuth2AuthorizeImplicitFactory, + compose.OAuth2ClientCredentialsGrantFactory, + compose.OAuth2RefreshTokenGrantFactory, + compose.OAuth2ResourceOwnerPasswordCredentialsFactory, + + // be aware that open id connect factories need to be added after oauth2 factories to work properly. + compose.OpenIDConnectExplicit, + compose.OpenIDConnectImplicit, + compose.OpenIDConnectHybrid, +) + // A valid oauth2 client (check the store) that additionally requests an OpenID Connect id token var clientConf = goauth.Config{ ClientID: "my-client", ClientSecret: "foobar", RedirectURL: "http://localhost:3846/callback", - Scopes: []string{"fosite", "openid", "offline"}, + Scopes: []string{"photos", "openid", "offline"}, Endpoint: goauth.Endpoint{ TokenURL: "http://localhost:3846/token", AuthURL: "http://localhost:3846/auth", @@ -82,210 +73,47 @@ var appClientConf = clientcredentials.Config{ TokenURL: "http://localhost:3846/token", } -// You can decide if you want to use HMAC or JWT or another strategy for generating authorize codes and access / refresh tokens -var hmacStrategy = &strategy.HMACSHAStrategy{ - Enigma: &hmac.HMACStrategy{ - GlobalSecret: []byte("some-super-cool-secret-that-nobody-knows"), - }, - AccessTokenLifespan: time.Hour, - AuthorizeCodeLifespan: time.Hour, -} - -// You can decide if you want to use HMAC or JWT or another strategy for generating authorize codes and access / refresh tokens -// The JWT strategy is mandatory for issuing ID Tokens (OpenID Connect) -// -// NOTE One thing to keep in mind is that the power of JWT does not mean anything -// when used as an authorize token, since the authorize token really just should -// be a random string that is hard to guess. -var jwtStrategy = &strategy.RS256JWTStrategy{ - RS256JWTStrategy: &jwt.RS256JWTStrategy{ - PrivateKey: mustRSAKey(), - }, -} - -// This strategy is used for issuing OpenID Conenct id tokens -var idtokenStrategy = &oidcstrategy.DefaultStrategy{ - RS256JWTStrategy: &jwt.RS256JWTStrategy{ - PrivateKey: mustRSAKey(), - }, -} - -// Change below to change the signing method (hmacStrategy or jwtStrategy) -var selectedStrategy = hmacStrategy - // A session is passed from the `/auth` to the `/token` endpoint. You probably want to store data like: "Who made the request", // "What organization does that person belong to" and so on. // For our use case, the session will meet the requirements imposed by JWT access tokens, HMAC access tokens and OpenID Connect // ID Tokens plus a custom field type session struct { User string - *strategy.JWTSession - *oidcstrategy.DefaultSession -} - -type stackTracer interface { - StackTrace() errors.StackTrace -} - -// newSession is a helper function for creating a new session -func newSession(user string) *session { - return &session{ - User: user, - JWTSession: &strategy.JWTSession{ - JWTClaims: &jwt.JWTClaims{ - Issuer: "https://fosite.my-application.com", - Subject: user, - Audience: "https://my-client.my-application.com", - ExpiresAt: time.Now().Add(time.Hour * 6), - IssuedAt: time.Now(), - }, - JWTHeader: &jwt.Headers{ - Extra: make(map[string]interface{}), - }, - }, - DefaultSession: &oidcstrategy.DefaultSession{ - Claims: &jwt.IDTokenClaims{ - Issuer: "https://fosite.my-application.com", - Subject: user, - Audience: "https://my-client.my-application.com", - ExpiresAt: time.Now().Add(time.Hour * 6), - IssuedAt: time.Now(), - }, - Headers: &jwt.Headers{ - Extra: make(map[string]interface{}), - }, - HMACSession: &strategy.HMACSession{}, - }, - } -} - -// fositeFactory creates a new Fosite instance with all features enabled -func fositeFactory() OAuth2Provider { - // Instantiate a new fosite instance - f := NewFosite(store) - - // Set the default access token lifespan to one hour - accessTokenLifespan := time.Hour - - // Most handlers are composable. This little helper is used by some of the handlers below. - oauth2HandleHelper := &core.HandleHelper{ - AccessTokenStrategy: selectedStrategy, - AccessTokenStorage: store, - AccessTokenLifespan: accessTokenLifespan, - } - - // This handler is responsible for the authorization code grant flow - explicitHandler := &explicit.AuthorizeExplicitGrantTypeHandler{ - AccessTokenStrategy: selectedStrategy, - RefreshTokenStrategy: selectedStrategy, - AuthorizeCodeStrategy: selectedStrategy, - AuthorizeCodeGrantStorage: store, - AuthCodeLifespan: time.Minute * 10, - AccessTokenLifespan: accessTokenLifespan, - } - // In order to "activate" the handler, we need to add it to fosite - f.AuthorizeEndpointHandlers.Append(explicitHandler) - - // Because this handler both handles `/auth` and `/token` endpoint requests, we need to add him to - // both registries. - f.TokenEndpointHandlers.Append(explicitHandler) - - // This handler is responsible for the implicit flow. The implicit flow does not return an authorize code - // but instead returns the access token directly via an url fragment. - implicitHandler := &implicit.AuthorizeImplicitGrantTypeHandler{ - AccessTokenStrategy: selectedStrategy, - AccessTokenStorage: store, - AccessTokenLifespan: accessTokenLifespan, - } - f.AuthorizeEndpointHandlers.Append(implicitHandler) - - // This handler is responsible for the client credentials flow. This flow is used when you want to - // authorize a client instead of an user. - clientHandler := &coreclient.ClientCredentialsGrantHandler{ - HandleHelper: oauth2HandleHelper, - } - f.TokenEndpointHandlers.Append(clientHandler) - - // This handler is responsible for the resource owner password credentials grant. In general, this - // is a flow which should not be used but could be useful in legacy environments. It uses a - // user's credentials (username, password) to issue an access token. - ownerHandler := &owner.ResourceOwnerPasswordCredentialsGrantHandler{ - HandleHelper: oauth2HandleHelper, - ResourceOwnerPasswordCredentialsGrantStorage: store, - } - f.TokenEndpointHandlers.Append(ownerHandler) - - // This handler is responsible for the refresh token grant. This type is used when you want to exchange - // a refresh token for a new refresh token and a new access token. - refreshHandler := &refresh.RefreshTokenGrantHandler{ - AccessTokenStrategy: selectedStrategy, - RefreshTokenStrategy: selectedStrategy, - RefreshTokenGrantStorage: store, - AccessTokenLifespan: accessTokenLifespan, - } - f.TokenEndpointHandlers.Append(refreshHandler) - - // This helper is similar to oauth2HandleHelper but for OpenID Connect handlers. - oidcHelper := &oidc.IDTokenHandleHelper{IDTokenStrategy: idtokenStrategy} - - // The OpenID Connect Authorize Code Flow. - oidcExplicit := &oidcexplicit.OpenIDConnectExplicitHandler{ - OpenIDConnectRequestStorage: store, - IDTokenHandleHelper: oidcHelper, - } - f.AuthorizeEndpointHandlers.Append(oidcExplicit) - // Because this handler both handles `/auth` and `/token` endpoint requests, we need to add him to - // both registries. - f.TokenEndpointHandlers.Append(oidcExplicit) - - // The OpenID Connect Implicit Flow. - oidcImplicit := &oidcimplicit.OpenIDConnectImplicitHandler{ - IDTokenHandleHelper: oidcHelper, - AuthorizeImplicitGrantTypeHandler: implicitHandler, - } - f.AuthorizeEndpointHandlers.Append(oidcImplicit) - - // The OpenID Connect Hybrid Flow. - oidcHybrid := &hybrid.OpenIDConnectHybridHandler{ - IDTokenHandleHelper: oidcHelper, - AuthorizeExplicitGrantTypeHandler: explicitHandler, - AuthorizeImplicitGrantTypeHandler: implicitHandler, - } - f.AuthorizeEndpointHandlers.Append(oidcHybrid) - - // Add a request validator for Access Tokens to fosite - f.AuthorizedRequestValidators.Append(&core.CoreValidator{ - AccessTokenStrategy: hmacStrategy, - AccessTokenStorage: store, - }) - - return f + *core.HMACSession + *core.JWTSession + *openid.DefaultSession } -// This is our fosite instance -var oauth2 = fositeFactory() - func main() { - // Set up some endpoints. You could also use gorilla/mux or any other router. - + // Set up oauth2 endpoints. You could also use gorilla/mux or any other router. http.HandleFunc("/auth", authEndpoint) http.HandleFunc("/token", tokenEndpoint) - http.HandleFunc("/", homeHandler) - http.HandleFunc("/callback", callbackHandler) - http.HandleFunc("/client", clientEndpoint) - http.HandleFunc("/owner", ownerEndpoint) + // some helper handlers for easily creating access tokens etc + // show some links on the index + http.HandleFunc("/", helpers.HomeHandler(clientConf)) + + // complete a client credentials flow + http.HandleFunc("/client", helpers.ClientEndpoint(appClientConf)) + + // complete a resource owner password credentials flow + http.HandleFunc("/owner", helpers.OwnerHandler(clientConf)) + + // validate tokens http.HandleFunc("/protected-api", validateEndpoint) - fmt.Printf("Please open your webbrowser at http://localhost:3846") + // the oauth2 callback endpoint + http.HandleFunc("/callback", helpers.CallbackHandler(clientConf)) + + fmt.Println("Please open your webbrowser at http://localhost:3846") _ = exec.Command("open", "http://localhost:3846").Run() log.Fatal(http.ListenAndServe(":3846", nil)) } func tokenEndpoint(rw http.ResponseWriter, req *http.Request) { // This context will be passed to all methods. - ctx := NewContext() + ctx := fosite.NewContext() // Create an empty session object which will be passed to the request handlers mySessionData := newSession("") @@ -320,7 +148,7 @@ func tokenEndpoint(rw http.ResponseWriter, req *http.Request) { func authEndpoint(rw http.ResponseWriter, req *http.Request) { // This context will be passed to all methods. - ctx := NewContext() + ctx := fosite.NewContext() // Let's create an AuthorizeRequest object! // It will analyze the request and extract important information like scopes, response type and others. @@ -332,24 +160,34 @@ func authEndpoint(rw http.ResponseWriter, req *http.Request) { } // You have now access to authorizeRequest, Code ResponseTypes, Scopes ... + var requestedScopes string + for _, this := range ar.GetRequestedScopes() { + requestedScopes += fmt.Sprintf(`
  • %s
  • `, this, this) + } + // Normally, this would be the place where you would check if the user is logged in and gives his consent. // We're simplifying things and just checking if the request includes a valid username and password - if req.Form.Get("username") != "peter" { + req.ParseForm() + if req.PostForm.Get("username") != "peter" { rw.Header().Set("Content-Type", "text/html; charset=utf-8") rw.Write([]byte(`

    Login page

    `)) - rw.Write([]byte(` + rw.Write([]byte(fmt.Sprintf(`

    Howdy! This is the log in page. For this example, it is enough to supply the username.

    +

    + By logging in, you consent to grant these scopes: +

      %s
    +

    try peter
    - `)) + `, requestedScopes))) return } - // we allow issuing of refresh tokens per default - if ar.GetScopes().Has("offline") { - ar.GrantScope("offline") + // let's see what scopes the user gave consent to + for _, scope := range req.PostForm["scopes"] { + ar.GrantScope(scope) } // Now that the user is authorized, we set up a session: @@ -396,11 +234,10 @@ func authEndpoint(rw http.ResponseWriter, req *http.Request) { } func validateEndpoint(rw http.ResponseWriter, req *http.Request) { - ctx := NewContext() - req.Header.Add("Authorization", "bearer "+req.URL.Query().Get("token")) + ctx := fosite.NewContext() mySessionData := newSession("peter") - ar, err := oauth2.ValidateRequestAuthorization(ctx, req, mySessionData, "fosite") + ar, err := oauth2.ValidateToken(ctx, req.URL.Query().Get("token"), fosite.AccessToken, mySessionData) if err != nil { fmt.Fprintf(rw, "

    An error occurred!

    %s", err.Error()) return @@ -411,167 +248,57 @@ func validateEndpoint(rw http.ResponseWriter, req *http.Request) {
  • Client: %s
  • Granted scopes: %v
  • Requested scopes: %v
  • -
  • Session data: %v, %v
  • +
  • Session data: %v
  • Requested at: %s
  • -`, ar.GetClient().GetID(), ar.GetGrantedScopes(), ar.GetScopes(), mySessionData, mySessionData.HMACSession, ar.GetRequestedAt()) +`, ar.GetClient().GetID(), ar.GetGrantedScopes(), ar.GetRequestedScopes(), mySessionData, ar.GetRequestedAt()) } -// ***************************************************************************** -// some views for easier navigation -// ***************************************************************************** - -func homeHandler(rw http.ResponseWriter, req *http.Request) { - rw.Write([]byte(fmt.Sprintf(` -

    You can obtain an access token using various methods

    - `, - clientConf.AuthCodeURL("some-random-state-foobar")+"&nonce=some-random-nonce", - "http://localhost:3846/auth?client_id=my-client&redirect_uri=http%3A%2F%2Flocalhost%3A3846%2Fcallback&response_type=token%20id_token&scope=fosite%20openid&state=some-random-state-foobar&nonce=some-random-nonce", - clientConf.AuthCodeURL("some-random-state-foobar")+"&nonce=some-random-nonce", - "/auth?client_id=my-client&scope=fosite&response_type=123&redirect_uri=http://localhost:3846/callback", - ))) -} - -func callbackHandler(rw http.ResponseWriter, req *http.Request) { - rw.Write([]byte(`

    Callback site

    Go back`)) - rw.Header().Set("Content-Type", "text/html; charset=utf-8") - if req.URL.Query().Get("error") != "" { - rw.Write([]byte(fmt.Sprintf(`

    Error!

    - Error: %s
    - Description: %s
    -
    `, - req.URL.Query().Get("error"), - req.URL.Query().Get("error_description"), - ))) - return - } - - if req.URL.Query().Get("refresh") != "" { - _, body, errs := gorequest.New().Post(clientConf.Endpoint.TokenURL).SetBasicAuth(clientConf.ClientID, clientConf.ClientSecret).SendString(url.Values{ - "grant_type": {"refresh_token"}, - "refresh_token": {req.URL.Query().Get("refresh")}, - "scope": {"fosite"}, - }.Encode()).End() - if len(errs) > 0 { - rw.Write([]byte(fmt.Sprintf(`

    Could not refresh token %s

    `, errs))) - return - } - rw.Write([]byte(fmt.Sprintf(`

    Got a response from the refresh grant:
    %s

    `, body))) - return - } +// a few simple helpers - if req.URL.Query().Get("code") == "" { - rw.Write([]byte(fmt.Sprintf(`

    Could not find the authorize code. If you've used the implicit grant, check the - browser location bar for the - access token (the server side does not have access to url fragments) -

    `, - ))) - return - } - - rw.Write([]byte(fmt.Sprintf(`

    Amazing! You just got an authorize code!:
    %s

    -

    Click here to return to the front page

    `, - req.URL.Query().Get("code"), - ))) - - token, err := clientConf.Exchange(goauth.NoContext, req.URL.Query().Get("code")) - if err != nil { - rw.Write([]byte(fmt.Sprintf(`

    I tried to exchange the authorize code for an access token but it did not work but got error: %s

    `, err.Error()))) - return - } - - rw.Write([]byte(fmt.Sprintf(`

    Cool! You are now a proud token owner.
    -

    `, - "/protected-api?token="+token.AccessToken, - token.AccessToken, - "?refresh="+url.QueryEscape(token.RefreshToken), - token.RefreshToken, - token, - ))) -} - -func clientEndpoint(rw http.ResponseWriter, req *http.Request) { - rw.Write([]byte(fmt.Sprintf(`

    Client Credentials Grant

    `))) - token, err := appClientConf.Token(goauth.NoContext) - if err != nil { - rw.Write([]byte(fmt.Sprintf(`

    I tried to get a token but received an error: %s

    `, err.Error()))) - return - } - rw.Write([]byte(fmt.Sprintf(`

    Awesome, you just received an access token!

    %s

    more info:

    %s

    `, token.AccessToken, token))) - rw.Write([]byte(`

    Go back

    `)) -} - -func ownerEndpoint(rw http.ResponseWriter, req *http.Request) { - rw.Write([]byte(fmt.Sprintf(`

    Resource Owner Password Credentials Grant

    `))) - req.ParseForm() - if req.Form.Get("username") == "" || req.Form.Get("password") == "" { - rw.Write([]byte(`
    -
      -
    • - try peter -
    • -
    • - try foobar
      -
    • -
    • - -
    • -
    -
    `)) - rw.Write([]byte(`

    Go back

    `)) - return - } - - token, err := clientConf.PasswordCredentialsToken(goauth.NoContext, req.Form.Get("username"), req.Form.Get("password")) +func mustRSAKey() *rsa.PrivateKey { + key, err := rsa.GenerateKey(rand.Reader, 1024) if err != nil { - rw.Write([]byte(fmt.Sprintf(`

    I tried to get a token but received an error: %s

    `, err.Error()))) - rw.Write([]byte(`

    Go back

    `)) - return + panic(err) } - rw.Write([]byte(fmt.Sprintf(`

    Awesome, you just received an access token!

    %s

    more info:

    %s

    `, token.AccessToken, token))) - rw.Write([]byte(`

    Go back

    `)) + return key } -func typeof(v interface{}) string { - return reflect.TypeOf(v).String() +type stackTracer interface { + StackTrace() errors.StackTrace } -func mustRSAKey() *rsa.PrivateKey { - key, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - panic(err) +// newSession is a helper function for creating a new session +func newSession(user string) *session { + return &session{ + User: user, + HMACSession: &core.HMACSession{ + AccessTokenExpiry: time.Now().Add(time.Minute * 30), + }, + // The JWTSession will not be used unless the OAuth2 JWT strategy is being used instead of HMAC + JWTSession: &core.JWTSession{ + JWTClaims: &jwt.JWTClaims{ + Issuer: "https://fosite.my-application.com", + Subject: user, + Audience: "https://my-client.my-application.com", + ExpiresAt: time.Now().Add(time.Hour * 6), + IssuedAt: time.Now(), + }, + JWTHeader: &jwt.Headers{ + Extra: make(map[string]interface{}), + }, + }, + DefaultSession: &openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Issuer: "https://fosite.my-application.com", + Subject: user, + Audience: "https://my-client.my-application.com", + ExpiresAt: time.Now().Add(time.Hour * 6), + IssuedAt: time.Now(), + }, + Headers: &jwt.Headers{ + Extra: make(map[string]interface{}), + }, + }, } - return key } diff --git a/fosite-example/pkg/handler_app_callback.go b/fosite-example/pkg/handler_app_callback.go new file mode 100644 index 000000000..3c5c9b33b --- /dev/null +++ b/fosite-example/pkg/handler_app_callback.go @@ -0,0 +1,83 @@ +package store + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/parnurzeal/gorequest" + "golang.org/x/oauth2" +) + +func CallbackHandler(c oauth2.Config) func(rw http.ResponseWriter, req *http.Request) { + return func(rw http.ResponseWriter, req *http.Request) { + rw.Write([]byte(`

    Callback site

    Go back`)) + rw.Header().Set("Content-Type", "text/html; charset=utf-8") + if req.URL.Query().Get("error") != "" { + rw.Write([]byte(fmt.Sprintf(`

    Error!

    + Error: %s
    + Description: %s
    +
    `, + req.URL.Query().Get("error"), + req.URL.Query().Get("error_description"), + ))) + return + } + + if req.URL.Query().Get("refresh") != "" { + _, body, errs := gorequest.New().Post(c.Endpoint.TokenURL).SetBasicAuth(c.ClientID, c.ClientSecret).SendString(url.Values{ + "grant_type": {"refresh_token"}, + "refresh_token": {req.URL.Query().Get("refresh")}, + "scope": {"fosite"}, + }.Encode()).End() + if len(errs) > 0 { + rw.Write([]byte(fmt.Sprintf(`

    Could not refresh token %s

    `, errs))) + return + } + rw.Write([]byte(fmt.Sprintf(`

    Got a response from the refresh grant:
    %s

    `, body))) + return + } + + if req.URL.Query().Get("code") == "" { + rw.Write([]byte(fmt.Sprintln(`

    Could not find the authorize code. If you've used the implicit grant, check the + browser location bar for the + access token (the server side does not have access to url fragments) +

    `, + ))) + return + } + + rw.Write([]byte(fmt.Sprintf(`

    Amazing! You just got an authorize code!:
    %s

    +

    Click here to return to the front page

    `, + req.URL.Query().Get("code"), + ))) + + token, err := c.Exchange(oauth2.NoContext, req.URL.Query().Get("code")) + if err != nil { + rw.Write([]byte(fmt.Sprintf(`

    I tried to exchange the authorize code for an access token but it did not work but got error: %s

    `, err.Error()))) + return + } + + rw.Write([]byte(fmt.Sprintf(`

    Cool! You are now a proud token owner.
    +

    `, + "/protected-api?token="+token.AccessToken, + token.AccessToken, + "?refresh="+url.QueryEscape(token.RefreshToken), + token.RefreshToken, + token, + ))) + } +} diff --git a/fosite-example/pkg/handler_home.go b/fosite-example/pkg/handler_home.go new file mode 100644 index 000000000..502e7b1e7 --- /dev/null +++ b/fosite-example/pkg/handler_home.go @@ -0,0 +1,40 @@ +package store + +import ( + "fmt" + "net/http" + + "golang.org/x/oauth2" +) + +func HomeHandler(c oauth2.Config) func(rw http.ResponseWriter, req *http.Request) { + return func(rw http.ResponseWriter, req *http.Request) { + rw.Write([]byte(fmt.Sprintf(` +

    You can obtain an access token using various methods

    + `, + c.AuthCodeURL("some-random-state-foobar")+"&nonce=some-random-nonce", + "http://localhost:3846/auth?client_id=my-client&redirect_uri=http%3A%2F%2Flocalhost%3A3846%2Fcallback&response_type=token%20id_token&scope=fosite%20openid&state=some-random-state-foobar&nonce=some-random-nonce", + c.AuthCodeURL("some-random-state-foobar")+"&nonce=some-random-nonce", + "/auth?client_id=my-client&scope=fosite&response_type=123&redirect_uri=http://localhost:3846/callback", + ))) + } +} diff --git a/fosite-example/pkg/handler_oauth2_client_flow.go b/fosite-example/pkg/handler_oauth2_client_flow.go new file mode 100644 index 000000000..5c35c6e9d --- /dev/null +++ b/fosite-example/pkg/handler_oauth2_client_flow.go @@ -0,0 +1,22 @@ +package store + +import ( + "fmt" + "net/http" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" +) + +func ClientEndpoint(c clientcredentials.Config) func(rw http.ResponseWriter, req *http.Request) { + return func(rw http.ResponseWriter, req *http.Request) { + rw.Write([]byte("

    Client Credentials Grant

    ")) + token, err := c.Token(oauth2.NoContext) + if err != nil { + rw.Write([]byte(fmt.Sprintf(`

    I tried to get a token but received an error: %s

    `, err.Error()))) + return + } + rw.Write([]byte(fmt.Sprintf(`

    Awesome, you just received an access token!

    %s

    more info:

    %s

    `, token.AccessToken, token))) + rw.Write([]byte(`

    Go back

    `)) + } +} diff --git a/fosite-example/pkg/handler_oauth2_owner_flow.go b/fosite-example/pkg/handler_oauth2_owner_flow.go new file mode 100644 index 000000000..4a2520704 --- /dev/null +++ b/fosite-example/pkg/handler_oauth2_owner_flow.go @@ -0,0 +1,41 @@ +package store + +import ( + "fmt" + "net/http" + + "golang.org/x/oauth2" +) + +func OwnerHandler(c oauth2.Config) func(rw http.ResponseWriter, req *http.Request) { + return func(rw http.ResponseWriter, req *http.Request) { + rw.Write([]byte("

    Resource Owner Password Credentials Grant

    ")) + req.ParseForm() + if req.Form.Get("username") == "" || req.Form.Get("password") == "" { + rw.Write([]byte(`
    +
      +
    • + try "peter" +
    • +
    • + try "secret"
      +
    • +
    • + +
    • +
    +
    `)) + rw.Write([]byte(`

    Go back

    `)) + return + } + + token, err := c.PasswordCredentialsToken(oauth2.NoContext, req.Form.Get("username"), req.Form.Get("password")) + if err != nil { + rw.Write([]byte(fmt.Sprintf(`

    I tried to get a token but received an error: %s

    `, err.Error()))) + rw.Write([]byte(`

    Go back

    `)) + return + } + rw.Write([]byte(fmt.Sprintf(`

    Awesome, you just received an access token!

    %s

    more info:

    %s

    `, token.AccessToken, token))) + rw.Write([]byte(`

    Go back

    `)) + } +} diff --git a/fosite-example/store/store.go b/fosite-example/pkg/store.go similarity index 80% rename from fosite-example/store/store.go rename to fosite-example/pkg/store.go index 6727e3aa8..35ca1f9c5 100644 --- a/fosite-example/store/store.go +++ b/fosite-example/pkg/store.go @@ -31,7 +31,34 @@ func NewStore() *Store { RefreshTokens: make(map[string]fosite.Requester), Users: make(map[string]UserRelation), } +} +func NewExampleStore() *Store { + return &Store{ + IDSessions: make(map[string]fosite.Requester), + Clients: map[string]*fosite.DefaultClient{ + "my-client": { + ID: "my-client", + Secret: []byte(`$2a$10$IxMdI6d.LIRZPpSfEwNoeu4rY3FhDREsxFJXikcgdRRAStxUlsuEO`), // = "foobar" + RedirectURIs: []string{"http://localhost:3846/callback"}, + ResponseTypes: []string{"id_token", "code", "token"}, + GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"}, + Scopes: []string{"fosite", "openid", "photos", "offline"}, + }, + }, + Users: map[string]UserRelation{ + "peter": { + // This store simply checks for equality, a real storage implementation would obviously use + // a hashing algorithm for encrypting the user password. + Username: "peter", + Password: "secret", + }, + }, + AuthorizeCodes: map[string]fosite.Requester{}, + Implicit: map[string]fosite.Requester{}, + AccessTokens: map[string]fosite.Requester{}, + RefreshTokens: map[string]fosite.Requester{}, + } } func (s *Store) CreateOpenIDConnectSession(_ context.Context, authorizeCode string, requester fosite.Requester) error { diff --git a/fosite.go b/fosite.go index 62f64972e..ae7fbef65 100644 --- a/fosite.go +++ b/fosite.go @@ -1,6 +1,8 @@ package fosite import ( + "reflect" + "github.com/Sirupsen/logrus" "github.com/ory-am/fosite/hash" ) @@ -8,47 +10,52 @@ import ( // AuthorizeEndpointHandlers is a list of AuthorizeEndpointHandler type AuthorizeEndpointHandlers []AuthorizeEndpointHandler -// Add adds an AuthorizeEndpointHandler to this list +// Add adds an AuthorizeEndpointHandler to this list. Ignores duplicates based on reflect.TypeOf. func (a *AuthorizeEndpointHandlers) Append(h AuthorizeEndpointHandler) { + for _, this := range *a { + if reflect.TypeOf(this) == reflect.TypeOf(h) { + return + } + } + *a = append(*a, h) } // TokenEndpointHandlers is a list of TokenEndpointHandler type TokenEndpointHandlers []TokenEndpointHandler -// Add adds an TokenEndpointHandler to this list +// Add adds an TokenEndpointHandler to this list. Ignores duplicates based on reflect.TypeOf. func (t *TokenEndpointHandlers) Append(h TokenEndpointHandler) { - *t = append(*t, h) -} - -// AuthorizedRequestValidators is a list of AuthorizedRequestValidator -type AuthorizedRequestValidators []AuthorizedRequestValidator + for _, this := range *t { + if reflect.TypeOf(this) == reflect.TypeOf(h) { + return + } + } -// Add adds an AccessTokenValidator to this list -func (t *AuthorizedRequestValidators) Append(h AuthorizedRequestValidator) { *t = append(*t, h) } -// NewFosite returns a new OAuth2Provider implementation -func NewFosite(store Storage) *Fosite { - return &Fosite{ - Store: store, - MandatoryScope: DefaultMandatoryScope, - AuthorizeEndpointHandlers: AuthorizeEndpointHandlers{}, - TokenEndpointHandlers: TokenEndpointHandlers{}, - AuthorizedRequestValidators: AuthorizedRequestValidators{}, - Hasher: &hash.BCrypt{WorkFactor: 12}, - Logger: &logrus.Logger{}, +// TokenValidators is a list of TokenValidator +type TokenValidators []TokenValidator + +// Add adds an AccessTokenValidator to this list. Ignores duplicates based on reflect.TypeOf. +func (t *TokenValidators) Append(h TokenValidator) { + for _, this := range *t { + if reflect.TypeOf(this) == reflect.TypeOf(h) { + return + } } + + *t = append(*t, h) } // Fosite implements OAuth2Provider. type Fosite struct { - MandatoryScope string - Store Storage - AuthorizeEndpointHandlers AuthorizeEndpointHandlers - TokenEndpointHandlers TokenEndpointHandlers - AuthorizedRequestValidators AuthorizedRequestValidators - Hasher hash.Hasher - Logger logrus.StdLogger + Store Storage + AuthorizeEndpointHandlers AuthorizeEndpointHandlers + TokenEndpointHandlers TokenEndpointHandlers + TokenValidators TokenValidators + Hasher hash.Hasher + Logger logrus.StdLogger + ScopeStrategy ScopeStrategy } diff --git a/fosite_test.go b/fosite_test.go index 8210fd7d9..60971c63d 100644 --- a/fosite_test.go +++ b/fosite_test.go @@ -4,41 +4,40 @@ import ( "testing" . "github.com/ory-am/fosite" - "github.com/ory-am/fosite/fosite-example/store" - "github.com/ory-am/fosite/handler/core" - "github.com/ory-am/fosite/handler/core/explicit" + "github.com/ory-am/fosite/handler/oauth2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestNewFosite(t *testing.T) { - f := NewFosite(store.NewStore()) - assert.NotNil(t, f.Store) - assert.NotNil(t, f.AuthorizedRequestValidators) - assert.NotNil(t, f.TokenEndpointHandlers) - assert.NotNil(t, f.AuthorizedRequestValidators) -} - func TestAuthorizeEndpointHandlers(t *testing.T) { - h := &explicit.AuthorizeExplicitGrantTypeHandler{} + h := &oauth2.AuthorizeExplicitGrantHandler{} hs := AuthorizeEndpointHandlers{} hs.Append(h) + hs.Append(h) + hs.Append(&oauth2.AuthorizeExplicitGrantHandler{}) assert.Len(t, hs, 1) assert.Equal(t, hs[0], h) } func TestTokenEndpointHandlers(t *testing.T) { - h := &explicit.AuthorizeExplicitGrantTypeHandler{} + h := &oauth2.AuthorizeExplicitGrantHandler{} hs := TokenEndpointHandlers{} hs.Append(h) + hs.Append(h) + // do some crazy type things and make sure dupe detection works + var f interface{} = &oauth2.AuthorizeExplicitGrantHandler{} + hs.Append(&oauth2.AuthorizeExplicitGrantHandler{}) + hs.Append(f.(TokenEndpointHandler)) require.Len(t, hs, 1) assert.Equal(t, hs[0], h) } func TestAuthorizedRequestValidators(t *testing.T) { - h := &core.CoreValidator{} - hs := AuthorizedRequestValidators{} + h := &oauth2.CoreValidator{} + hs := TokenValidators{} + hs.Append(h) hs.Append(h) + hs.Append(&oauth2.CoreValidator{}) require.Len(t, hs, 1) assert.Equal(t, hs[0], h) } diff --git a/generate-mocks.sh b/generate-mocks.sh index 1d274480c..70741ecd2 100755 --- a/generate-mocks.sh +++ b/generate-mocks.sh @@ -1,22 +1,24 @@ #!/bin/bash mockgen -package internal -destination internal/storage.go github.com/ory-am/fosite Storage -mockgen -package internal -destination internal/authorize_code_storage.go github.com/ory-am/fosite/handler/core AuthorizeCodeStorage -mockgen -package internal -destination internal/access_token_storage.go github.com/ory-am/fosite/handler/core AccessTokenStorage -mockgen -package internal -destination internal/refresh_token_strategy.go github.com/ory-am/fosite/handler/core RefreshTokenStorage -mockgen -package internal -destination internal/core_client_storage.go github.com/ory-am/fosite/handler/core/client ClientCredentialsGrantStorage -mockgen -package internal -destination internal/core_explicit_storage.go github.com/ory-am/fosite/handler/core/explicit AuthorizeCodeGrantStorage -mockgen -package internal -destination internal/core_implicit_storage.go github.com/ory-am/fosite/handler/core/implicit ImplicitGrantStorage -mockgen -package internal -destination internal/core_owner_storage.go github.com/ory-am/fosite/handler/core/owner ResourceOwnerPasswordCredentialsGrantStorage -mockgen -package internal -destination internal/core_refresh_storage.go github.com/ory-am/fosite/handler/core/refresh RefreshTokenGrantStorage -mockgen -package internal -destination internal/oidc_id_token_storage.go github.com/ory-am/fosite/handler/oidc OpenIDConnectRequestStorage -mockgen -package internal -destination internal/access_token_strategy.go github.com/ory-am/fosite/handler/core AccessTokenStrategy -mockgen -package internal -destination internal/refresh_token_strategy.go github.com/ory-am/fosite/handler/core RefreshTokenStrategy -mockgen -package internal -destination internal/authorize_code_strategy.go github.com/ory-am/fosite/handler/core AuthorizeCodeStrategy -mockgen -package internal -destination internal/id_token_strategy.go github.com/ory-am/fosite/handler/oidc OpenIDConnectTokenStrategy +mockgen -package internal -destination internal/oauth2_storage.go github.com/ory-am/fosite/handler/oauth2 CoreStorage +mockgen -package internal -destination internal/oauth2_strategy.go github.com/ory-am/fosite/handler/oauth2 CoreStrategy +mockgen -package internal -destination internal/authorize_code_storage.go github.com/ory-am/fosite/handler/oauth2 AuthorizeCodeStorage +mockgen -package internal -destination internal/access_token_storage.go github.com/ory-am/fosite/handler/oauth2 AccessTokenStorage +mockgen -package internal -destination internal/refresh_token_strategy.go github.com/ory-am/fosite/handler/oauth2 RefreshTokenStorage +mockgen -package internal -destination internal/oauth2_client_storage.go github.com/ory-am/fosite/handler/oauth2 ClientCredentialsGrantStorage +mockgen -package internal -destination internal/oauth2_explicit_storage.go github.com/ory-am/fosite/handler/oauth2 AuthorizeCodeGrantStorage +mockgen -package internal -destination internal/oauth2_implicit_storage.go github.com/ory-am/fosite/handler/oauth2 ImplicitGrantStorage +mockgen -package internal -destination internal/oauth2_owner_storage.go github.com/ory-am/fosite/handler/oauth2 ResourceOwnerPasswordCredentialsGrantStorage +mockgen -package internal -destination internal/oauth2_refresh_storage.go github.com/ory-am/fosite/handler/oauth2 RefreshTokenGrantStorage +mockgen -package internal -destination internal/openid_id_token_storage.go github.com/ory-am/fosite/handler/openid OpenIDConnectRequestStorage +mockgen -package internal -destination internal/access_token_strategy.go github.com/ory-am/fosite/handler/oauth2 AccessTokenStrategy +mockgen -package internal -destination internal/refresh_token_strategy.go github.com/ory-am/fosite/handler/oauth2 RefreshTokenStrategy +mockgen -package internal -destination internal/authorize_code_strategy.go github.com/ory-am/fosite/handler/oauth2 AuthorizeCodeStrategy +mockgen -package internal -destination internal/id_token_strategy.go github.com/ory-am/fosite/handler/openid OpenIDConnectTokenStrategy mockgen -package internal -destination internal/authorize_handler.go github.com/ory-am/fosite AuthorizeEndpointHandler mockgen -package internal -destination internal/token_handler.go github.com/ory-am/fosite TokenEndpointHandler -mockgen -package internal -destination internal/validator.go github.com/ory-am/fosite AuthorizedRequestValidator +mockgen -package internal -destination internal/validator.go github.com/ory-am/fosite TokenValidator mockgen -package internal -destination internal/client.go github.com/ory-am/fosite Client mockgen -package internal -destination internal/request.go github.com/ory-am/fosite Requester mockgen -package internal -destination internal/access_request.go github.com/ory-am/fosite AccessRequester diff --git a/handler/core/client/storage.go b/handler/core/client/storage.go deleted file mode 100644 index caa844265..000000000 --- a/handler/core/client/storage.go +++ /dev/null @@ -1,7 +0,0 @@ -package client - -import "github.com/ory-am/fosite/handler/core" - -type ClientCredentialsGrantStorage interface { - core.AccessTokenStorage -} diff --git a/handler/core/validator.go b/handler/core/validator.go deleted file mode 100644 index 4cbb57599..000000000 --- a/handler/core/validator.go +++ /dev/null @@ -1,40 +0,0 @@ -package core - -import ( - "net/http" - "strings" - - "github.com/ory-am/fosite" - "github.com/pkg/errors" - "golang.org/x/net/context" -) - -type CoreValidator struct { - AccessTokenStrategy - AccessTokenStorage -} - -func (c *CoreValidator) ValidateRequest(ctx context.Context, req *http.Request, accessRequest fosite.AccessRequester) error { - auth := req.Header.Get("Authorization") - split := strings.SplitN(auth, " ", 2) - if len(split) != 2 || !strings.EqualFold(split[0], "bearer") { - return errors.Wrap(fosite.ErrUnknownRequest, "") - } - - return c.ValidateToken(ctx, accessRequest, split[1]) -} - -func (c *CoreValidator) ValidateToken(ctx context.Context, accessRequest fosite.AccessRequester, token string) error { - sig := c.AccessTokenStrategy.AccessTokenSignature(token) - or, err := c.AccessTokenStorage.GetAccessTokenSession(ctx, sig, accessRequest.GetSession()) - if err != nil { - return errors.Wrap(fosite.ErrRequestUnauthorized, err.Error()) - } - - if err := c.AccessTokenStrategy.ValidateAccessToken(ctx, or, token); err != nil { - return errors.Wrap(fosite.ErrRequestUnauthorized, err.Error()) - } - - accessRequest.Merge(or) - return nil -} diff --git a/handler/core/explicit/explicit_auth.go b/handler/oauth2/flow_authorize_code_auth.go similarity index 52% rename from handler/core/explicit/explicit_auth.go rename to handler/oauth2/flow_authorize_code_auth.go index ccee3c1b4..6618d157f 100644 --- a/handler/core/explicit/explicit_auth.go +++ b/handler/oauth2/flow_authorize_code_auth.go @@ -1,25 +1,23 @@ -package explicit +package oauth2 import ( "net/http" + "strings" "time" - "strings" + "fmt" - . "github.com/ory-am/fosite" - "github.com/ory-am/fosite/handler/core" + "github.com/ory-am/fosite" "github.com/pkg/errors" "golang.org/x/net/context" ) -const authCodeDefaultLifespan = time.Hour / 2 - // AuthorizeExplicitGrantTypeHandler is a response handler for the Authorize Code grant using the explicit grant type // as defined in https://tools.ietf.org/html/rfc6749#section-4.1 -type AuthorizeExplicitGrantTypeHandler struct { - AccessTokenStrategy core.AccessTokenStrategy - RefreshTokenStrategy core.RefreshTokenStrategy - AuthorizeCodeStrategy core.AuthorizeCodeStrategy +type AuthorizeExplicitGrantHandler struct { + AccessTokenStrategy AccessTokenStrategy + RefreshTokenStrategy RefreshTokenStrategy + AuthorizeCodeStrategy AuthorizeCodeStrategy // AuthorizeCodeGrantStorage is used to persist session data across requests. AuthorizeCodeGrantStorage AuthorizeCodeGrantStorage @@ -29,33 +27,42 @@ type AuthorizeExplicitGrantTypeHandler struct { // AccessTokenLifespan defines the lifetime of an access token. AccessTokenLifespan time.Duration + + ScopeStrategy fosite.ScopeStrategy } -func (c *AuthorizeExplicitGrantTypeHandler) HandleAuthorizeEndpointRequest(ctx context.Context, req *http.Request, ar AuthorizeRequester, resp AuthorizeResponder) error { +func (c *AuthorizeExplicitGrantHandler) HandleAuthorizeEndpointRequest(ctx context.Context, req *http.Request, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { // This let's us define multiple response types, for example open id connect's id_token if !ar.GetResponseTypes().Exact("code") { return nil } if !ar.GetClient().GetResponseTypes().Has("code") { - return errors.Wrap(ErrInvalidGrant, "") + return errors.Wrap(fosite.ErrInvalidGrant, "") + } + + if !fosite.IsRedirectURISecure(ar.GetRedirectURI()) { + return errors.Wrap(fosite.ErrInvalidRequest, "") } - if !IsRedirectURISecure(ar.GetRedirectURI()) { - return errors.Wrap(ErrInvalidRequest, "") + 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 c.IssueAuthorizeCode(ctx, req, ar, resp) } -func (c *AuthorizeExplicitGrantTypeHandler) IssueAuthorizeCode(ctx context.Context, req *http.Request, ar AuthorizeRequester, resp AuthorizeResponder) error { +func (c *AuthorizeExplicitGrantHandler) IssueAuthorizeCode(ctx context.Context, req *http.Request, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { code, signature, err := c.AuthorizeCodeStrategy.GenerateAuthorizeCode(ctx, ar) if err != nil { - return errors.Wrap(ErrServerError, err.Error()) + return errors.Wrap(fosite.ErrServerError, err.Error()) } if err := c.AuthorizeCodeGrantStorage.CreateAuthorizeCodeSession(ctx, signature, ar); err != nil { - return errors.Wrap(ErrServerError, err.Error()) + return errors.Wrap(fosite.ErrServerError, err.Error()) } resp.AddQuery("code", code) diff --git a/handler/core/explicit/explicit_auth_test.go b/handler/oauth2/flow_authorize_code_auth_test.go similarity index 93% rename from handler/core/explicit/explicit_auth_test.go rename to handler/oauth2/flow_authorize_code_auth_test.go index 4e4dad7d3..172036884 100644 --- a/handler/core/explicit/explicit_auth_test.go +++ b/handler/oauth2/flow_authorize_code_auth_test.go @@ -1,4 +1,4 @@ -package explicit +package oauth2 import ( "net/http" @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestHandleAuthorizeEndpointRequest(t *testing.T) { +func TestAuthorizeCode_HandleAuthorizeEndpointRequest(t *testing.T) { ctrl := gomock.NewController(t) store := internal.NewMockAuthorizeCodeGrantStorage(ctrl) chgen := internal.NewMockAuthorizeCodeStrategy(ctrl) @@ -24,9 +24,10 @@ func TestHandleAuthorizeEndpointRequest(t *testing.T) { areq := fosite.NewAuthorizeRequest() httpreq := &http.Request{Form: url.Values{}} - h := AuthorizeExplicitGrantTypeHandler{ + h := AuthorizeExplicitGrantHandler{ AuthorizeCodeGrantStorage: store, AuthorizeCodeStrategy: chgen, + ScopeStrategy: fosite.HierarchicScopeStrategy, } for k, c := range []struct { description string diff --git a/handler/core/explicit/storage.go b/handler/oauth2/flow_authorize_code_storage.go similarity index 75% rename from handler/core/explicit/storage.go rename to handler/oauth2/flow_authorize_code_storage.go index 8eb5b9ba8..40a1ff939 100644 --- a/handler/core/explicit/storage.go +++ b/handler/oauth2/flow_authorize_code_storage.go @@ -1,13 +1,12 @@ -package explicit +package oauth2 import ( "github.com/ory-am/fosite" - "github.com/ory-am/fosite/handler/core" "golang.org/x/net/context" ) type AuthorizeCodeGrantStorage interface { - core.AuthorizeCodeStorage + AuthorizeCodeStorage PersistAuthorizeCodeGrantSession(ctx context.Context, authorizeCode, accessSignature, refreshSignature string, request fosite.Requester) error } diff --git a/handler/core/explicit/explicit_token.go b/handler/oauth2/flow_authorize_code_token.go similarity index 85% rename from handler/core/explicit/explicit_token.go rename to handler/oauth2/flow_authorize_code_token.go index fb73bfbc8..73e36ae18 100644 --- a/handler/core/explicit/explicit_token.go +++ b/handler/oauth2/flow_authorize_code_token.go @@ -1,4 +1,4 @@ -package explicit +package oauth2 import ( "net/http" @@ -11,7 +11,7 @@ import ( // HandleTokenEndpointRequest implements // * https://tools.ietf.org/html/rfc6749#section-4.1.3 (everything) -func (c *AuthorizeExplicitGrantTypeHandler) HandleTokenEndpointRequest(ctx context.Context, r *http.Request, request fosite.AccessRequester) error { +func (c *AuthorizeExplicitGrantHandler) HandleTokenEndpointRequest(ctx context.Context, r *http.Request, request fosite.AccessRequester) error { // grant_type REQUIRED. // Value MUST be set to "authorization_code". if !request.GetGrantTypes().Exact("authorization_code") { @@ -19,7 +19,7 @@ func (c *AuthorizeExplicitGrantTypeHandler) HandleTokenEndpointRequest(ctx conte } if !request.GetClient().GetGrantTypes().Has("authorization_code") { - return errors.Wrap(fosite.ErrInvalidGrant, "") + return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use grant type authorization_code") } code := r.PostForm.Get("code") @@ -37,13 +37,13 @@ func (c *AuthorizeExplicitGrantTypeHandler) HandleTokenEndpointRequest(ctx conte } // Override scopes - request.SetScopes(authorizeRequest.GetScopes()) + request.SetRequestedScopes(authorizeRequest.GetRequestedScopes()) // The authorization server MUST ensure that the authorization code was issued to the authenticated // 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(fosite.ErrInvalidRequest, "") + return errors.Wrap(fosite.ErrInvalidRequest, "Client ID mismatch") } // ensure that the "redirect_uri" parameter is present if the @@ -52,7 +52,7 @@ func (c *AuthorizeExplicitGrantTypeHandler) HandleTokenEndpointRequest(ctx conte // their values are identical. forcedRedirectURI := authorizeRequest.GetRequestForm().Get("redirect_uri") if forcedRedirectURI != "" && forcedRedirectURI != r.PostForm.Get("redirect_uri") { - return errors.Wrap(fosite.ErrInvalidRequest, "") + return errors.Wrap(fosite.ErrInvalidRequest, "Redirect URI mismatch") } // Checking of POST client_id skipped, because: @@ -64,7 +64,7 @@ func (c *AuthorizeExplicitGrantTypeHandler) HandleTokenEndpointRequest(ctx conte return nil } -func (c *AuthorizeExplicitGrantTypeHandler) PopulateTokenEndpointResponse(ctx context.Context, req *http.Request, requester fosite.AccessRequester, responder fosite.AccessResponder) error { +func (c *AuthorizeExplicitGrantHandler) PopulateTokenEndpointResponse(ctx context.Context, req *http.Request, requester fosite.AccessRequester, responder fosite.AccessResponder) error { // grant_type REQUIRED. // Value MUST be set to "authorization_code". if !requester.GetGrantTypes().Exact("authorization_code") { diff --git a/handler/core/explicit/explicit_token_test.go b/handler/oauth2/flow_authorize_code_token_test.go similarity index 94% rename from handler/core/explicit/explicit_token_test.go rename to handler/oauth2/flow_authorize_code_token_test.go index 13d562c53..e81e02627 100644 --- a/handler/core/explicit/explicit_token_test.go +++ b/handler/oauth2/flow_authorize_code_token_test.go @@ -1,4 +1,4 @@ -package explicit +package oauth2 import ( "net/http" @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestPopulateTokenEndpointResponse(t *testing.T) { +func TestAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) { ctrl := gomock.NewController(t) store := internal.NewMockAuthorizeCodeGrantStorage(ctrl) ach := internal.NewMockAccessTokenStrategy(ctrl) @@ -27,11 +27,12 @@ func TestPopulateTokenEndpointResponse(t *testing.T) { httpreq := &http.Request{PostForm: url.Values{}} authreq := fosite.NewAuthorizeRequest() - h := AuthorizeExplicitGrantTypeHandler{ + h := AuthorizeExplicitGrantHandler{ AuthorizeCodeGrantStorage: store, AuthorizeCodeStrategy: auch, AccessTokenStrategy: ach, RefreshTokenStrategy: rch, + ScopeStrategy: fosite.HierarchicScopeStrategy, } for k, c := range []struct { description string @@ -112,7 +113,7 @@ func TestPopulateTokenEndpointResponse(t *testing.T) { } } -func TestHandleTokenEndpointRequest(t *testing.T) { +func TestAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) { ctrl := gomock.NewController(t) store := internal.NewMockAuthorizeCodeGrantStorage(ctrl) ach := internal.NewMockAuthorizeCodeStrategy(ctrl) @@ -122,9 +123,10 @@ func TestHandleTokenEndpointRequest(t *testing.T) { areq := fosite.NewAccessRequest(nil) httpreq := &http.Request{PostForm: url.Values{}} - h := AuthorizeExplicitGrantTypeHandler{ + h := AuthorizeExplicitGrantHandler{ AuthorizeCodeGrantStorage: store, AuthorizeCodeStrategy: ach, + ScopeStrategy: fosite.HierarchicScopeStrategy, } for k, c := range []struct { description string diff --git a/handler/core/implicit/implicit.go b/handler/oauth2/flow_authorize_implicit.go similarity index 63% rename from handler/core/implicit/implicit.go rename to handler/oauth2/flow_authorize_implicit.go index 79d73021c..f1ed5ff78 100644 --- a/handler/core/implicit/implicit.go +++ b/handler/oauth2/flow_authorize_implicit.go @@ -1,14 +1,14 @@ -package implicit +package oauth2 import ( "net/http" - "time" - "strconv" "strings" + "time" + + "fmt" - . "github.com/ory-am/fosite" - "github.com/ory-am/fosite/handler/core" + "github.com/ory-am/fosite" "github.com/pkg/errors" "golang.org/x/net/context" ) @@ -16,27 +16,36 @@ import ( // AuthorizeImplicitGrantTypeHandler is a response handler for the Authorize Code grant using the implicit grant type // as defined in https://tools.ietf.org/html/rfc6749#section-4.2 type AuthorizeImplicitGrantTypeHandler struct { - AccessTokenStrategy core.AccessTokenStrategy + AccessTokenStrategy AccessTokenStrategy // ImplicitGrantStorage is used to persist session data across requests. - AccessTokenStorage core.AccessTokenStorage + AccessTokenStorage AccessTokenStorage // AccessTokenLifespan defines the lifetime of an access token. AccessTokenLifespan time.Duration + + ScopeStrategy fosite.ScopeStrategy } -func (c *AuthorizeImplicitGrantTypeHandler) HandleAuthorizeEndpointRequest(ctx context.Context, req *http.Request, ar AuthorizeRequester, resp AuthorizeResponder) error { +func (c *AuthorizeImplicitGrantTypeHandler) HandleAuthorizeEndpointRequest(ctx context.Context, req *http.Request, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { // This let's us define multiple response types, for example open id connect's id_token if !ar.GetResponseTypes().Exact("token") { return nil } if !ar.GetClient().GetResponseTypes().Has("token") { - return errors.Wrap(ErrInvalidGrant, "") + return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use response type token") } if !ar.GetClient().GetGrantTypes().Has("implicit") { - return errors.Wrap(ErrInvalidGrant, "") + return errors.Wrap(fosite.ErrInvalidGrant, "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)) + } } // there is no need to check for https, because implicit flow does not require https @@ -45,13 +54,13 @@ func (c *AuthorizeImplicitGrantTypeHandler) HandleAuthorizeEndpointRequest(ctx c return c.IssueImplicitAccessToken(ctx, req, ar, resp) } -func (c *AuthorizeImplicitGrantTypeHandler) IssueImplicitAccessToken(ctx context.Context, req *http.Request, ar AuthorizeRequester, resp AuthorizeResponder) error { +func (c *AuthorizeImplicitGrantTypeHandler) IssueImplicitAccessToken(ctx context.Context, req *http.Request, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { // Generate the code token, signature, err := c.AccessTokenStrategy.GenerateAccessToken(ctx, ar) if err != nil { - return errors.Wrap(ErrServerError, err.Error()) + return errors.Wrap(fosite.ErrServerError, err.Error()) } else if err := c.AccessTokenStorage.CreateAccessTokenSession(ctx, signature, ar); err != nil { - return errors.Wrap(ErrServerError, err.Error()) + return errors.Wrap(fosite.ErrServerError, err.Error()) } resp.AddFragment("access_token", token) diff --git a/handler/core/implicit/storage.go b/handler/oauth2/flow_authorize_implicit_storage.go similarity index 92% rename from handler/core/implicit/storage.go rename to handler/oauth2/flow_authorize_implicit_storage.go index fe5c7754c..e16203ae2 100644 --- a/handler/core/implicit/storage.go +++ b/handler/oauth2/flow_authorize_implicit_storage.go @@ -1,4 +1,4 @@ -package implicit +package oauth2 import ( "github.com/ory-am/fosite" diff --git a/handler/core/implicit/implicit_test.go b/handler/oauth2/flow_authorize_implicit_test.go similarity index 94% rename from handler/core/implicit/implicit_test.go rename to handler/oauth2/flow_authorize_implicit_test.go index 6fd9a69a6..01db058cd 100644 --- a/handler/core/implicit/implicit_test.go +++ b/handler/oauth2/flow_authorize_implicit_test.go @@ -1,4 +1,4 @@ -package implicit_test +package oauth2 import ( "net/http" @@ -9,13 +9,12 @@ import ( "github.com/golang/mock/gomock" "github.com/ory-am/fosite" - . "github.com/ory-am/fosite/handler/core/implicit" "github.com/ory-am/fosite/internal" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) -func TestAuthorizeImplicitEndpointHandler(t *testing.T) { +func TestAuthorizeImplicit_EndpointHandler(t *testing.T) { ctrl := gomock.NewController(t) store := internal.NewMockAccessTokenStorage(ctrl) chgen := internal.NewMockAccessTokenStrategy(ctrl) @@ -29,6 +28,7 @@ func TestAuthorizeImplicitEndpointHandler(t *testing.T) { AccessTokenStorage: store, AccessTokenStrategy: chgen, AccessTokenLifespan: time.Hour, + ScopeStrategy: fosite.HierarchicScopeStrategy, } for k, c := range []struct { description string diff --git a/handler/core/client/client.go b/handler/oauth2/flow_client.go similarity index 78% rename from handler/core/client/client.go rename to handler/oauth2/flow_client.go index 8c4731639..a89aa3c1f 100644 --- a/handler/core/client/client.go +++ b/handler/oauth2/flow_client.go @@ -1,16 +1,18 @@ -package client +package oauth2 import ( "net/http" + "fmt" + "github.com/ory-am/fosite" - "github.com/ory-am/fosite/handler/core" "github.com/pkg/errors" "golang.org/x/net/context" ) type ClientCredentialsGrantHandler struct { - *core.HandleHelper + *HandleHelper + ScopeStrategy fosite.ScopeStrategy } // ValidateTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.4.2 @@ -22,9 +24,9 @@ func (c *ClientCredentialsGrantHandler) HandleTokenEndpointRequest(_ context.Con } client := request.GetClient() - for _, scope := range request.GetScopes() { - if client.GetGrantedScopes().Grant(scope) { - request.GrantScope(scope) + 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)) } } @@ -43,7 +45,7 @@ func (c *ClientCredentialsGrantHandler) PopulateTokenEndpointResponse(ctx contex } if !request.GetClient().GetGrantTypes().Has("client_credentials") { - return errors.Wrap(fosite.ErrInvalidGrant, "") + return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use grant type client_credentials") } return c.IssueAccessToken(ctx, r, request, response) diff --git a/handler/oauth2/flow_client_credentials_storage.go b/handler/oauth2/flow_client_credentials_storage.go new file mode 100644 index 000000000..0ef5da746 --- /dev/null +++ b/handler/oauth2/flow_client_credentials_storage.go @@ -0,0 +1,5 @@ +package oauth2 + +type ClientCredentialsGrantStorage interface { + AccessTokenStorage +} diff --git a/handler/core/client/client_test.go b/handler/oauth2/flow_client_credentials_test.go similarity index 85% rename from handler/core/client/client_test.go rename to handler/oauth2/flow_client_credentials_test.go index d128c3395..2fc63189f 100644 --- a/handler/core/client/client_test.go +++ b/handler/oauth2/flow_client_credentials_test.go @@ -1,4 +1,4 @@ -package client +package oauth2 import ( "net/http" @@ -10,11 +10,9 @@ import ( "github.com/ory-am/fosite/internal" "github.com/pkg/errors" "github.com/stretchr/testify/assert" - - "github.com/ory-am/fosite/handler/core" ) -func TestHandleTokenEndpointRequest(t *testing.T) { +func TestClientCredentials_HandleTokenEndpointRequest(t *testing.T) { ctrl := gomock.NewController(t) store := internal.NewMockClientCredentialsGrantStorage(ctrl) chgen := internal.NewMockAccessTokenStrategy(ctrl) @@ -22,11 +20,12 @@ func TestHandleTokenEndpointRequest(t *testing.T) { defer ctrl.Finish() h := ClientCredentialsGrantHandler{ - &core.HandleHelper{ + HandleHelper: &HandleHelper{ AccessTokenStorage: store, AccessTokenStrategy: chgen, AccessTokenLifespan: time.Hour, }, + ScopeStrategy: fosite.HierarchicScopeStrategy, } for k, c := range []struct { description string @@ -45,12 +44,10 @@ func TestHandleTokenEndpointRequest(t *testing.T) { description: "should pass", mock: func() { areq.EXPECT().GetGrantTypes().Return(fosite.Arguments{"client_credentials"}) - areq.EXPECT().GetScopes().Return([]string{"foo", "bar", "baz.bar"}) - areq.EXPECT().GrantScope("foo") - areq.EXPECT().GrantScope("baz.bar") + areq.EXPECT().GetRequestedScopes().Return([]string{"foo", "bar", "baz.bar"}) areq.EXPECT().GetClient().Return(&fosite.DefaultClient{ - GrantTypes: fosite.Arguments{"client_credentials"}, - GrantedScopes: []string{"foo", "baz"}, + GrantTypes: fosite.Arguments{"client_credentials"}, + Scopes: []string{"foo", "bar", "baz"}, }) }, }, @@ -62,7 +59,7 @@ func TestHandleTokenEndpointRequest(t *testing.T) { } } -func TestPopulateTokenEndpointResponse(t *testing.T) { +func TestClientCredentials_PopulateTokenEndpointResponse(t *testing.T) { ctrl := gomock.NewController(t) store := internal.NewMockClientCredentialsGrantStorage(ctrl) chgen := internal.NewMockAccessTokenStrategy(ctrl) @@ -71,11 +68,12 @@ func TestPopulateTokenEndpointResponse(t *testing.T) { defer ctrl.Finish() h := ClientCredentialsGrantHandler{ - &core.HandleHelper{ + HandleHelper: &HandleHelper{ AccessTokenStorage: store, AccessTokenStrategy: chgen, AccessTokenLifespan: time.Hour, }, + ScopeStrategy: fosite.HierarchicScopeStrategy, } for k, c := range []struct { description string diff --git a/handler/core/refresh/refresh.go b/handler/oauth2/flow_refresh.go similarity index 90% rename from handler/core/refresh/refresh.go rename to handler/oauth2/flow_refresh.go index 5c3a4ac41..edb0292f0 100644 --- a/handler/core/refresh/refresh.go +++ b/handler/oauth2/flow_refresh.go @@ -1,19 +1,18 @@ -package refresh +package oauth2 import ( "net/http" "time" "github.com/ory-am/fosite" - "github.com/ory-am/fosite/handler/core" "github.com/pkg/errors" "golang.org/x/net/context" ) type RefreshTokenGrantHandler struct { - AccessTokenStrategy core.AccessTokenStrategy + AccessTokenStrategy AccessTokenStrategy - RefreshTokenStrategy core.RefreshTokenStrategy + RefreshTokenStrategy RefreshTokenStrategy // RefreshTokenGrantStorage is used to persist session data across requests. RefreshTokenGrantStorage RefreshTokenGrantStorage @@ -31,7 +30,7 @@ func (c *RefreshTokenGrantHandler) HandleTokenEndpointRequest(ctx context.Contex } if !request.GetClient().GetGrantTypes().Has("refresh_token") { - return errors.Wrap(fosite.ErrInvalidGrant, "") + return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use grant type refresh_token") } refresh := req.PostForm.Get("refresh_token") @@ -48,14 +47,14 @@ func (c *RefreshTokenGrantHandler) HandleTokenEndpointRequest(ctx context.Contex return errors.Wrap(fosite.ErrInvalidRequest, err.Error()) } - request.SetScopes(accessRequest.GetScopes()) + request.SetRequestedScopes(accessRequest.GetRequestedScopes()) for _, scope := range accessRequest.GetGrantedScopes() { request.GrantScope(scope) } // The authorization server MUST ... and ensure that the refresh token was issued to the authenticated client if accessRequest.GetClient().GetID() != request.GetClient().GetID() { - return errors.Wrap(fosite.ErrInvalidRequest, "") + return errors.Wrap(fosite.ErrInvalidRequest, "Client ID mismatch") } return nil } diff --git a/handler/core/refresh/storage.go b/handler/oauth2/flow_refresh_storage.go similarity index 76% rename from handler/core/refresh/storage.go rename to handler/oauth2/flow_refresh_storage.go index bfe2bbc8f..23b938604 100644 --- a/handler/core/refresh/storage.go +++ b/handler/oauth2/flow_refresh_storage.go @@ -1,12 +1,11 @@ -package refresh +package oauth2 import ( "github.com/ory-am/fosite" - "github.com/ory-am/fosite/handler/core" "golang.org/x/net/context" ) type RefreshTokenGrantStorage interface { - core.RefreshTokenStorage + RefreshTokenStorage PersistRefreshTokenGrantSession(ctx context.Context, requestRefreshSignature, accessSignature, refreshSignature string, request fosite.Requester) error } diff --git a/handler/core/refresh/refresh_test.go b/handler/oauth2/flow_refresh_test.go similarity index 97% rename from handler/core/refresh/refresh_test.go rename to handler/oauth2/flow_refresh_test.go index 176d9856e..3a9ac3709 100644 --- a/handler/core/refresh/refresh_test.go +++ b/handler/oauth2/flow_refresh_test.go @@ -1,4 +1,4 @@ -package refresh +package oauth2 import ( "net/http" @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestHandleTokenEndpointRequest(t *testing.T) { +func TestRefreshFlow_HandleTokenEndpointRequest(t *testing.T) { ctrl := gomock.NewController(t) store := internal.NewMockRefreshTokenGrantStorage(ctrl) chgen := internal.NewMockRefreshTokenStrategy(ctrl) @@ -84,7 +84,7 @@ func TestHandleTokenEndpointRequest(t *testing.T) { } } -func TestPopulateTokenEndpointResponse(t *testing.T) { +func TestRefreshFlow_PopulateTokenEndpointResponse(t *testing.T) { ctrl := gomock.NewController(t) store := internal.NewMockRefreshTokenGrantStorage(ctrl) rcts := internal.NewMockRefreshTokenStrategy(ctrl) diff --git a/handler/core/owner/owner.go b/handler/oauth2/flow_resource_owner.go similarity index 78% rename from handler/core/owner/owner.go rename to handler/oauth2/flow_resource_owner.go index fcd0be300..577e50287 100644 --- a/handler/core/owner/owner.go +++ b/handler/oauth2/flow_resource_owner.go @@ -1,10 +1,11 @@ -package owner +package oauth2 import ( "net/http" + "fmt" + "github.com/ory-am/fosite" - "github.com/ory-am/fosite/handler/core" "github.com/pkg/errors" "golang.org/x/net/context" ) @@ -13,7 +14,9 @@ type ResourceOwnerPasswordCredentialsGrantHandler struct { // ResourceOwnerPasswordCredentialsGrantStorage is used to persist session data across requests. ResourceOwnerPasswordCredentialsGrantStorage ResourceOwnerPasswordCredentialsGrantStorage - *core.HandleHelper + ScopeStrategy fosite.ScopeStrategy + + *HandleHelper } // HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2 @@ -25,19 +28,26 @@ func (c *ResourceOwnerPasswordCredentialsGrantHandler) HandleTokenEndpointReques } if !request.GetClient().GetGrantTypes().Has("password") { - return errors.Wrap(fosite.ErrInvalidGrant, "") + return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use grant type password") } username := req.PostForm.Get("username") password := req.PostForm.Get("password") if username == "" || password == "" { - return errors.Wrap(fosite.ErrInvalidRequest, "") + return errors.Wrap(fosite.ErrInvalidRequest, "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()) } else if err != nil { return errors.Wrap(fosite.ErrServerError, 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)) + } + } + // Credentials must not be passed around, potentially leaking to the database! delete(request.GetRequestForm(), "password") return nil diff --git a/handler/core/owner/storage.go b/handler/oauth2/flow_resource_owner_storage.go similarity index 68% rename from handler/core/owner/storage.go rename to handler/oauth2/flow_resource_owner_storage.go index 8c595fb59..adc01e16e 100644 --- a/handler/core/owner/storage.go +++ b/handler/oauth2/flow_resource_owner_storage.go @@ -1,11 +1,10 @@ -package owner +package oauth2 import ( - "github.com/ory-am/fosite/handler/core" "golang.org/x/net/context" ) type ResourceOwnerPasswordCredentialsGrantStorage interface { Authenticate(ctx context.Context, name string, secret string) error - core.AccessTokenStorage + AccessTokenStorage } diff --git a/handler/core/owner/owner_test.go b/handler/oauth2/flow_resource_owner_test.go similarity index 92% rename from handler/core/owner/owner_test.go rename to handler/oauth2/flow_resource_owner_test.go index dcd42d093..71dfa49b3 100644 --- a/handler/core/owner/owner_test.go +++ b/handler/oauth2/flow_resource_owner_test.go @@ -1,4 +1,4 @@ -package owner +package oauth2 import ( "net/http" @@ -8,13 +8,12 @@ import ( "github.com/golang/mock/gomock" "github.com/ory-am/fosite" - "github.com/ory-am/fosite/handler/core" "github.com/ory-am/fosite/internal" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) -func TestHandleTokenEndpointRequest(t *testing.T) { +func TestResourceOwnerFlow_HandleTokenEndpointRequest(t *testing.T) { ctrl := gomock.NewController(t) store := internal.NewMockResourceOwnerPasswordCredentialsGrantStorage(ctrl) defer ctrl.Finish() @@ -24,10 +23,11 @@ func TestHandleTokenEndpointRequest(t *testing.T) { h := ResourceOwnerPasswordCredentialsGrantHandler{ ResourceOwnerPasswordCredentialsGrantStorage: store, - HandleHelper: &core.HandleHelper{ + HandleHelper: &HandleHelper{ AccessTokenStorage: store, AccessTokenLifespan: time.Hour, }, + ScopeStrategy: fosite.HierarchicScopeStrategy, } for k, c := range []struct { description string @@ -73,7 +73,7 @@ func TestHandleTokenEndpointRequest(t *testing.T) { } } -func TestPopulateTokenEndpointResponse(t *testing.T) { +func TestResourceOwnerFlow_PopulateTokenEndpointResponse(t *testing.T) { ctrl := gomock.NewController(t) store := internal.NewMockResourceOwnerPasswordCredentialsGrantStorage(ctrl) chgen := internal.NewMockAccessTokenStrategy(ctrl) @@ -85,7 +85,7 @@ func TestPopulateTokenEndpointResponse(t *testing.T) { h := ResourceOwnerPasswordCredentialsGrantHandler{ ResourceOwnerPasswordCredentialsGrantStorage: store, - HandleHelper: &core.HandleHelper{ + HandleHelper: &HandleHelper{ AccessTokenStorage: store, AccessTokenStrategy: chgen, AccessTokenLifespan: time.Hour, diff --git a/handler/core/helper.go b/handler/oauth2/helper.go similarity index 83% rename from handler/core/helper.go rename to handler/oauth2/helper.go index 0f7474f21..9c126e1f7 100644 --- a/handler/core/helper.go +++ b/handler/oauth2/helper.go @@ -1,10 +1,10 @@ -package core +package oauth2 import ( "net/http" "time" - . "github.com/ory-am/fosite" + "github.com/ory-am/fosite" "golang.org/x/net/context" ) @@ -14,7 +14,7 @@ type HandleHelper struct { AccessTokenLifespan time.Duration } -func (h *HandleHelper) IssueAccessToken(ctx context.Context, req *http.Request, requester AccessRequester, responder AccessResponder) error { +func (h *HandleHelper) IssueAccessToken(ctx context.Context, req *http.Request, requester fosite.AccessRequester, responder fosite.AccessResponder) error { token, signature, err := h.AccessTokenStrategy.GenerateAccessToken(ctx, requester) if err != nil { return err diff --git a/handler/core/helper_test.go b/handler/oauth2/helper_test.go similarity index 96% rename from handler/core/helper_test.go rename to handler/oauth2/helper_test.go index 4e065b5d5..20dcad215 100644 --- a/handler/core/helper_test.go +++ b/handler/oauth2/helper_test.go @@ -1,4 +1,4 @@ -package core_test +package oauth2 import ( "net/http" @@ -7,7 +7,6 @@ import ( "github.com/golang/mock/gomock" "github.com/ory-am/fosite" - . "github.com/ory-am/fosite/handler/core" "github.com/ory-am/fosite/internal" "github.com/pkg/errors" "github.com/stretchr/testify/assert" diff --git a/handler/core/storage.go b/handler/oauth2/storage.go similarity index 98% rename from handler/core/storage.go rename to handler/oauth2/storage.go index 77980c22a..49e2c677b 100644 --- a/handler/core/storage.go +++ b/handler/oauth2/storage.go @@ -1,4 +1,4 @@ -package core +package oauth2 import ( "github.com/ory-am/fosite" diff --git a/handler/core/strategy.go b/handler/oauth2/strategy.go similarity index 98% rename from handler/core/strategy.go rename to handler/oauth2/strategy.go index a78ee69a4..0822aac1a 100644 --- a/handler/core/strategy.go +++ b/handler/oauth2/strategy.go @@ -1,4 +1,4 @@ -package core +package oauth2 import ( "github.com/ory-am/fosite" diff --git a/handler/core/strategy/hmacsha.go b/handler/oauth2/strategy_hmacsha.go similarity index 99% rename from handler/core/strategy/hmacsha.go rename to handler/oauth2/strategy_hmacsha.go index f9cb04a3c..9769fc588 100644 --- a/handler/core/strategy/hmacsha.go +++ b/handler/oauth2/strategy_hmacsha.go @@ -1,4 +1,4 @@ -package strategy +package oauth2 import ( "reflect" diff --git a/handler/core/strategy/hmacsha_session.go b/handler/oauth2/strategy_hmacsha_session.go similarity index 98% rename from handler/core/strategy/hmacsha_session.go rename to handler/oauth2/strategy_hmacsha_session.go index 10e79d5ab..ded6ba219 100644 --- a/handler/core/strategy/hmacsha_session.go +++ b/handler/oauth2/strategy_hmacsha_session.go @@ -1,4 +1,4 @@ -package strategy +package oauth2 import ( "time" diff --git a/handler/core/strategy/hmacsha_test.go b/handler/oauth2/strategy_hmacsha_test.go similarity index 99% rename from handler/core/strategy/hmacsha_test.go rename to handler/oauth2/strategy_hmacsha_test.go index 639003fa8..64080a75a 100644 --- a/handler/core/strategy/hmacsha_test.go +++ b/handler/oauth2/strategy_hmacsha_test.go @@ -1,4 +1,4 @@ -package strategy +package oauth2 import ( "strings" diff --git a/handler/core/strategy/jwt.go b/handler/oauth2/strategy_jwt.go similarity index 99% rename from handler/core/strategy/jwt.go rename to handler/oauth2/strategy_jwt.go index 2979f1c55..21ea6cab1 100644 --- a/handler/core/strategy/jwt.go +++ b/handler/oauth2/strategy_jwt.go @@ -1,4 +1,4 @@ -package strategy +package oauth2 import ( "strings" diff --git a/handler/core/strategy/jwt_session.go b/handler/oauth2/strategy_jwt_session.go similarity index 97% rename from handler/core/strategy/jwt_session.go rename to handler/oauth2/strategy_jwt_session.go index df95eea97..c4c35aec8 100644 --- a/handler/core/strategy/jwt_session.go +++ b/handler/oauth2/strategy_jwt_session.go @@ -1,4 +1,4 @@ -package strategy +package oauth2 import ( "github.com/ory-am/fosite/token/jwt" diff --git a/handler/core/strategy/jwt_test.go b/handler/oauth2/strategy_jwt_test.go similarity index 99% rename from handler/core/strategy/jwt_test.go rename to handler/oauth2/strategy_jwt_test.go index 1f89e7b40..3c4d40b8e 100644 --- a/handler/core/strategy/jwt_test.go +++ b/handler/oauth2/strategy_jwt_test.go @@ -1,4 +1,4 @@ -package strategy +package oauth2 import ( "strings" diff --git a/handler/oauth2/validator.go b/handler/oauth2/validator.go new file mode 100644 index 000000000..27b70290f --- /dev/null +++ b/handler/oauth2/validator.go @@ -0,0 +1,75 @@ +package oauth2 + +import ( + "fmt" + + "github.com/ory-am/fosite" + "github.com/pkg/errors" + "golang.org/x/net/context" +) + +type CoreValidator struct { + CoreStrategy + CoreStorage + ScopeStrategy fosite.ScopeStrategy +} + +func (c *CoreValidator) ValidateToken(ctx context.Context, token string, tokenType fosite.TokenType, accessRequest fosite.AccessRequester, scopes []string) error { + switch tokenType { + case fosite.AccessToken: + return c.validateAccessToken(ctx, token, accessRequest, scopes) + case fosite.RefreshToken: + return c.validateRefreshToken(ctx, token, accessRequest) + case fosite.AuthorizeCode: + return c.validateAuthorizeCode(ctx, token, accessRequest) + default: + return errors.Wrap(fosite.ErrUnknownRequest, "") + } +} + +func (c *CoreValidator) validateAccessToken(ctx context.Context, token string, accessRequest fosite.AccessRequester, scopes []string) error { + sig := c.CoreStrategy.AccessTokenSignature(token) + or, err := c.CoreStorage.GetAccessTokenSession(ctx, sig, accessRequest.GetSession()) + if err != nil { + fmt.Printf("%s", err) + return errors.Wrap(fosite.ErrRequestUnauthorized, err.Error()) + } else if err := c.CoreStrategy.ValidateAccessToken(ctx, or, token); err != nil { + fmt.Printf("%s", err) + return errors.Wrap(fosite.ErrRequestUnauthorized, err.Error()) + } + + for _, scope := range scopes { + if !c.ScopeStrategy(or.GetGrantedScopes(), scope) { + return errors.Wrap(fosite.ErrInvalidScope, "") + } + } + + accessRequest.Merge(or) + return nil +} + +func (c *CoreValidator) validateRefreshToken(ctx context.Context, token string, accessRequest fosite.AccessRequester) error { + sig := c.CoreStrategy.AccessTokenSignature(token) + if or, err := c.CoreStorage.GetAccessTokenSession(ctx, sig, accessRequest.GetSession()); err != nil { + return errors.Wrap(fosite.ErrRequestUnauthorized, err.Error()) + } else if err := c.CoreStrategy.ValidateAccessToken(ctx, or, token); err != nil { + return errors.Wrap(fosite.ErrRequestUnauthorized, err.Error()) + } else { + accessRequest.Merge(or) + } + + return nil +} + +func (c *CoreValidator) validateAuthorizeCode(ctx context.Context, token string, accessRequest fosite.AccessRequester) error { + sig := c.CoreStrategy.AccessTokenSignature(token) + if or, err := c.CoreStorage.GetAccessTokenSession(ctx, sig, accessRequest.GetSession()); err != nil { + return errors.Wrap(fosite.ErrRequestUnauthorized, err.Error()) + } else if err := c.CoreStrategy.ValidateAccessToken(ctx, or, token); err != nil { + return errors.Wrap(fosite.ErrRequestUnauthorized, err.Error()) + } else { + accessRequest.Merge(or) + } + + return nil +} diff --git a/handler/core/validator_test.go b/handler/oauth2/validator_test.go similarity index 76% rename from handler/core/validator_test.go rename to handler/oauth2/validator_test.go index b5b1574fd..ba14972ee 100644 --- a/handler/core/validator_test.go +++ b/handler/oauth2/validator_test.go @@ -1,4 +1,4 @@ -package core +package oauth2 import ( "net/http" @@ -11,16 +11,16 @@ import ( "github.com/stretchr/testify/assert" ) -func TestValidateRequest(t *testing.T) { +func TestValidateToken(t *testing.T) { ctrl := gomock.NewController(t) - store := internal.NewMockAccessTokenStorage(ctrl) - chgen := internal.NewMockAccessTokenStrategy(ctrl) + store := internal.NewMockCoreStorage(ctrl) + chgen := internal.NewMockCoreStrategy(ctrl) areq := fosite.NewAccessRequest(nil) defer ctrl.Finish() v := &CoreValidator{ - AccessTokenStrategy: chgen, - AccessTokenStorage: store, + CoreStrategy: chgen, + CoreStorage: store, } httpreq := &http.Request{Header: http.Header{}} @@ -29,17 +29,14 @@ func TestValidateRequest(t *testing.T) { setup func() expectErr error }{ - { - description: "should fail because no authorization header set", - expectErr: fosite.ErrUnknownRequest, - setup: func() {}, - }, { description: "should fail because no bearer token set", - expectErr: fosite.ErrUnknownRequest, setup: func() { httpreq.Header.Set("Authorization", "bearer") + chgen.EXPECT().AccessTokenSignature("").Return("") + store.EXPECT().GetAccessTokenSession(nil, "", nil).Return(nil, errors.New("")) }, + expectErr: fosite.ErrRequestUnauthorized, }, { description: "should fail because retrieval fails", @@ -66,7 +63,7 @@ func TestValidateRequest(t *testing.T) { }, } { c.setup() - err := v.ValidateRequest(nil, httpreq, areq) + err := v.ValidateToken(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) } diff --git a/handler/oidc/explicit/explicit_token.go b/handler/oidc/explicit/explicit_token.go deleted file mode 100644 index 4da81fb70..000000000 --- a/handler/oidc/explicit/explicit_token.go +++ /dev/null @@ -1,41 +0,0 @@ -package explicit - -import ( - "net/http" - - . "github.com/ory-am/fosite" - "github.com/ory-am/fosite/handler/oidc" - "github.com/pkg/errors" - "golang.org/x/net/context" -) - -func (c *OpenIDConnectExplicitHandler) HandleTokenEndpointRequest(ctx context.Context, r *http.Request, request AccessRequester) error { - return ErrUnknownRequest -} - -func (c *OpenIDConnectExplicitHandler) PopulateTokenEndpointResponse(ctx context.Context, req *http.Request, requester AccessRequester, responder AccessResponder) error { - if !requester.GetGrantTypes().Exact("authorization_code") { - return errors.Wrap(ErrUnknownRequest, "") - } - - authorize, err := c.OpenIDConnectRequestStorage.GetOpenIDConnectSession(ctx, requester.GetRequestForm().Get("code"), requester) - if err == oidc.ErrNoSessionFound { - return errors.Wrap(ErrUnknownRequest, err.Error()) - } else if err != nil { - return errors.Wrap(ErrServerError, err.Error()) - } - - if !authorize.GetScopes().Has("openid") { - return errors.Wrap(ErrUnknownRequest, "") - } - - if !requester.GetClient().GetGrantTypes().Has("authorization_code") { - return errors.Wrap(ErrInvalidGrant, "") - } - - if !requester.GetClient().GetResponseTypes().Has("id_token") { - return errors.Wrap(ErrInvalidGrant, "") - } - - return c.IssueExplicitIDToken(ctx, req, authorize, responder) -} diff --git a/handler/oidc/hybrid/hybrid.go b/handler/oidc/hybrid/hybrid.go deleted file mode 100644 index 3e1abb299..000000000 --- a/handler/oidc/hybrid/hybrid.go +++ /dev/null @@ -1,99 +0,0 @@ -package hybrid - -import ( - "net/http" - - . "github.com/ory-am/fosite" - "github.com/ory-am/fosite/handler/core/explicit" - "github.com/ory-am/fosite/handler/core/implicit" - "github.com/ory-am/fosite/handler/oidc" - "github.com/ory-am/fosite/handler/oidc/strategy" - "github.com/ory-am/fosite/token/jwt" - "github.com/pkg/errors" - "golang.org/x/net/context" -) - -type OpenIDConnectHybridHandler struct { - *implicit.AuthorizeImplicitGrantTypeHandler - *explicit.AuthorizeExplicitGrantTypeHandler - *oidc.IDTokenHandleHelper - - Enigma *jwt.RS256JWTStrategy -} - -func (c *OpenIDConnectHybridHandler) HandleAuthorizeEndpointRequest(ctx context.Context, req *http.Request, ar AuthorizeRequester, resp AuthorizeResponder) error { - if len(ar.GetResponseTypes()) < 2 { - return nil - } - - if !(ar.GetResponseTypes().Matches("token", "id_token", "code") || ar.GetResponseTypes().Matches("token", "code")) { - return nil - } - - if !ar.GetClient().GetResponseTypes().Has("token", "code") { - return errors.Wrap(ErrInvalidGrant, "") - } else if ar.GetResponseTypes().Matches("id_token") && !ar.GetClient().GetResponseTypes().Has("id_token") { - return errors.Wrap(ErrInvalidGrant, "") - } - - sess, ok := ar.GetSession().(strategy.Session) - if !ok { - return errors.Wrap(oidc.ErrInvalidSession, "") - } - - claims := sess.IDTokenClaims() - - if ar.GetResponseTypes().Has("code") { - if !ar.GetClient().GetGrantTypes().Has("authorization_code") { - return errors.Wrap(ErrInvalidGrant, "") - } - - code, signature, err := c.AuthorizeCodeStrategy.GenerateAuthorizeCode(ctx, ar) - if err != nil { - return errors.Wrap(ErrServerError, err.Error()) - } else if err := c.AuthorizeCodeGrantStorage.CreateAuthorizeCodeSession(ctx, signature, ar); err != nil { - return errors.Wrap(ErrServerError, err.Error()) - } - - resp.AddFragment("code", code) - resp.AddFragment("state", ar.GetState()) - ar.SetResponseTypeHandled("code") - - hash, err := c.Enigma.Hash([]byte(resp.GetFragment().Get("code"))) - if err != nil { - return err - } - claims.CodeHash = hash[:c.Enigma.GetSigningMethodLength()/2] - } - - if ar.GetResponseTypes().Has("token") { - if !ar.GetClient().GetGrantTypes().Has("implicit") { - return errors.Wrap(ErrInvalidGrant, "") - } else if err := c.IssueImplicitAccessToken(ctx, req, ar, resp); err != nil { - return errors.Wrap(err, err.Error()) - } - ar.SetResponseTypeHandled("token") - - hash, err := c.Enigma.Hash([]byte(resp.GetFragment().Get("access_token"))) - if err != nil { - return err - } - claims.AccessTokenHash = hash[:c.Enigma.GetSigningMethodLength()/2] - } - - if !ar.GetScopes().Has("openid") { - return nil - } - - if err := c.IssueImplicitIDToken(ctx, req, ar, resp); err != nil { - return errors.Wrap(err, err.Error()) - } else if err := c.IssueImplicitIDToken(ctx, req, ar, resp); err != nil { - return errors.Wrap(err, err.Error()) - } - - // there is no need to check for https, because implicit flow does not require https - // https://tools.ietf.org/html/rfc6819#section-4.4.2 - - ar.SetResponseTypeHandled("id_token") - return nil -} diff --git a/handler/oidc/errors.go b/handler/openid/errors.go similarity index 86% rename from handler/oidc/errors.go rename to handler/openid/errors.go index f9ee1abe9..1245f2b5c 100644 --- a/handler/oidc/errors.go +++ b/handler/openid/errors.go @@ -1,4 +1,4 @@ -package oidc +package openid import "github.com/pkg/errors" diff --git a/handler/oidc/explicit/explicit_auth.go b/handler/openid/flow_explicit_auth.go similarity index 57% rename from handler/oidc/explicit/explicit_auth.go rename to handler/openid/flow_explicit_auth.go index 4a68f977f..42d928379 100644 --- a/handler/oidc/explicit/explicit_auth.go +++ b/handler/openid/flow_explicit_auth.go @@ -1,10 +1,9 @@ -package explicit +package openid import ( "net/http" - . "github.com/ory-am/fosite" - . "github.com/ory-am/fosite/handler/oidc" + "github.com/ory-am/fosite" "github.com/pkg/errors" "golang.org/x/net/context" ) @@ -16,21 +15,21 @@ type OpenIDConnectExplicitHandler struct { *IDTokenHandleHelper } -func (c *OpenIDConnectExplicitHandler) HandleAuthorizeEndpointRequest(ctx context.Context, req *http.Request, ar AuthorizeRequester, resp AuthorizeResponder) error { - if !(ar.GetScopes().Has("openid") && ar.GetResponseTypes().Exact("code")) { +func (c *OpenIDConnectExplicitHandler) HandleAuthorizeEndpointRequest(ctx context.Context, req *http.Request, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { + if !(ar.GetGrantedScopes().Has("openid") && ar.GetResponseTypes().Exact("code")) { return nil } if !ar.GetClient().GetResponseTypes().Has("id_token", "code") { - return errors.Wrap(ErrInvalidClient, "client is not allowed to use rseponse type id_token and code") + return errors.Wrap(fosite.ErrInvalidRequest, "The client is not allowed to use response type id_token and code") } if len(resp.GetCode()) == 0 { - return errors.Wrap(ErrMisconfiguration, "code is not set") + return errors.Wrap(fosite.ErrMisconfiguration, "Authorization code has not been issued yet") } if err := c.OpenIDConnectRequestStorage.CreateOpenIDConnectSession(ctx, resp.GetCode(), ar); err != nil { - return errors.Wrap(ErrServerError, err.Error()) + return errors.Wrap(fosite.ErrServerError, err.Error()) } // there is no need to check for https, because it has already been checked by core.explicit diff --git a/handler/oidc/explicit/explicit_auth_test.go b/handler/openid/flow_explicit_auth_test.go similarity index 87% rename from handler/oidc/explicit/explicit_auth_test.go rename to handler/openid/flow_explicit_auth_test.go index 0249f3a51..cf5651d07 100644 --- a/handler/oidc/explicit/explicit_auth_test.go +++ b/handler/openid/flow_explicit_auth_test.go @@ -1,4 +1,4 @@ -package explicit +package openid import ( "net/http" @@ -7,21 +7,19 @@ import ( "github.com/golang/mock/gomock" "github.com/ory-am/fosite" - "github.com/ory-am/fosite/handler/oidc" - "github.com/ory-am/fosite/handler/oidc/strategy" "github.com/ory-am/fosite/internal" "github.com/ory-am/fosite/token/jwt" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) -var j = &strategy.DefaultStrategy{ +var j = &DefaultStrategy{ RS256JWTStrategy: &jwt.RS256JWTStrategy{ PrivateKey: internal.MustRSAKey(), }, } -func TestHandleAuthorizeEndpointRequest(t *testing.T) { +func TestExplicit_HandleAuthorizeEndpointRequest(t *testing.T) { ctrl := gomock.NewController(t) store := internal.NewMockOpenIDConnectRequestStorage(ctrl) aresp := internal.NewMockAuthorizeResponder(ctrl) @@ -32,7 +30,7 @@ func TestHandleAuthorizeEndpointRequest(t *testing.T) { h := &OpenIDConnectExplicitHandler{ OpenIDConnectRequestStorage: store, - IDTokenHandleHelper: &oidc.IDTokenHandleHelper{ + IDTokenHandleHelper: &IDTokenHandleHelper{ IDTokenStrategy: j, }, } @@ -60,7 +58,7 @@ func TestHandleAuthorizeEndpointRequest(t *testing.T) { { description: "should fail because no code set", setup: func() { - areq.Scopes = fosite.Arguments{"openid"} + areq.GrantedScopes = fosite.Arguments{"openid"} areq.Form.Set("nonce", "11111111111111111111111111111") aresp.EXPECT().GetCode().Return("") }, diff --git a/handler/openid/flow_explicit_token.go b/handler/openid/flow_explicit_token.go new file mode 100644 index 000000000..863c916d7 --- /dev/null +++ b/handler/openid/flow_explicit_token.go @@ -0,0 +1,40 @@ +package openid + +import ( + "net/http" + + "github.com/ory-am/fosite" + "github.com/pkg/errors" + "golang.org/x/net/context" +) + +func (c *OpenIDConnectExplicitHandler) HandleTokenEndpointRequest(ctx context.Context, r *http.Request, request fosite.AccessRequester) error { + return fosite.ErrUnknownRequest +} + +func (c *OpenIDConnectExplicitHandler) PopulateTokenEndpointResponse(ctx context.Context, req *http.Request, requester fosite.AccessRequester, responder fosite.AccessResponder) error { + if !requester.GetGrantTypes().Exact("authorization_code") { + return errors.Wrap(fosite.ErrUnknownRequest, "") + } + + authorize, err := c.OpenIDConnectRequestStorage.GetOpenIDConnectSession(ctx, requester.GetRequestForm().Get("code"), requester) + if err == ErrNoSessionFound { + return errors.Wrap(fosite.ErrUnknownRequest, err.Error()) + } else if err != nil { + return errors.Wrap(fosite.ErrServerError, 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") + } + + if !requester.GetClient().GetGrantTypes().Has("authorization_code") { + return errors.Wrap(fosite.ErrInvalidGrant, "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 c.IssueExplicitIDToken(ctx, req, authorize, responder) +} diff --git a/handler/oidc/explicit/explicit_token_test.go b/handler/openid/flow_explicit_token_test.go similarity index 88% rename from handler/oidc/explicit/explicit_token_test.go rename to handler/openid/flow_explicit_token_test.go index 25004892a..ca04d9156 100644 --- a/handler/oidc/explicit/explicit_token_test.go +++ b/handler/openid/flow_explicit_token_test.go @@ -1,4 +1,4 @@ -package explicit +package openid import ( "net/http" @@ -7,8 +7,6 @@ import ( "github.com/golang/mock/gomock" "github.com/ory-am/fosite" - "github.com/ory-am/fosite/handler/oidc" - "github.com/ory-am/fosite/handler/oidc/strategy" "github.com/ory-am/fosite/internal" "github.com/ory-am/fosite/token/jwt" "github.com/pkg/errors" @@ -24,12 +22,12 @@ func TestHandleTokenEndpointRequest(t *testing.T) { assert.True(t, errors.Cause(h.HandleTokenEndpointRequest(nil, nil, areq)) == fosite.ErrUnknownRequest) } -func TestPopulateTokenEndpointResponse(t *testing.T) { +func TestExplicit_PopulateTokenEndpointResponse(t *testing.T) { ctrl := gomock.NewController(t) store := internal.NewMockOpenIDConnectRequestStorage(ctrl) defer ctrl.Finish() - session := &strategy.DefaultSession{ + session := &DefaultSession{ Claims: &jwt.IDTokenClaims{ Subject: "peter", }, @@ -41,7 +39,7 @@ func TestPopulateTokenEndpointResponse(t *testing.T) { h := &OpenIDConnectExplicitHandler{ OpenIDConnectRequestStorage: store, - IDTokenHandleHelper: &oidc.IDTokenHandleHelper{ + IDTokenHandleHelper: &IDTokenHandleHelper{ IDTokenStrategy: j, }, } @@ -64,7 +62,7 @@ func TestPopulateTokenEndpointResponse(t *testing.T) { ResponseTypes: fosite.Arguments{"id_token"}, } areq.Form.Set("code", "foobar") - store.EXPECT().GetOpenIDConnectSession(nil, "foobar", areq).Return(nil, oidc.ErrNoSessionFound) + store.EXPECT().GetOpenIDConnectSession(nil, "foobar", areq).Return(nil, ErrNoSessionFound) }, expectErr: fosite.ErrUnknownRequest, }, @@ -82,14 +80,14 @@ func TestPopulateTokenEndpointResponse(t *testing.T) { areq.GrantTypes = fosite.Arguments{"authorization_code"} store.EXPECT().GetOpenIDConnectSession(nil, "foobar", areq).Return(fosite.NewAuthorizeRequest(), nil) }, - expectErr: fosite.ErrUnknownRequest, + expectErr: fosite.ErrMisconfiguration, }, { description: "should pass", setup: func() { r := fosite.NewAuthorizeRequest() r.Session = areq.Session - r.Scopes = fosite.Arguments{"openid"} + r.GrantedScopes = fosite.Arguments{"openid"} r.Form.Set("nonce", "1111111111111111") store.EXPECT().GetOpenIDConnectSession(nil, gomock.Any(), areq).AnyTimes().Return(r, nil) }, diff --git a/handler/openid/flow_hybrid.go b/handler/openid/flow_hybrid.go new file mode 100644 index 000000000..3ea50e805 --- /dev/null +++ b/handler/openid/flow_hybrid.go @@ -0,0 +1,105 @@ +package openid + +import ( + "net/http" + + "fmt" + + "github.com/ory-am/fosite" + "github.com/ory-am/fosite/handler/oauth2" + "github.com/ory-am/fosite/token/jwt" + "github.com/pkg/errors" + "golang.org/x/net/context" +) + +type OpenIDConnectHybridHandler struct { + AuthorizeImplicitGrantTypeHandler *oauth2.AuthorizeImplicitGrantTypeHandler + AuthorizeExplicitGrantHandler *oauth2.AuthorizeExplicitGrantHandler + IDTokenHandleHelper *IDTokenHandleHelper + ScopeStrategy fosite.ScopeStrategy + + Enigma *jwt.RS256JWTStrategy +} + +func (c *OpenIDConnectHybridHandler) HandleAuthorizeEndpointRequest(ctx context.Context, req *http.Request, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { + if len(ar.GetResponseTypes()) < 2 { + return nil + } + + if !(ar.GetResponseTypes().Matches("token", "id_token", "code") || ar.GetResponseTypes().Matches("token", "code")) { + return nil + } + + if !ar.GetClient().GetResponseTypes().Has("token", "code") { + return errors.Wrap(fosite.ErrInvalidGrant, "The client is not allowed to use the code and token 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") + } + + sess, ok := ar.GetSession().(Session) + if !ok { + return errors.Wrap(ErrInvalidSession, "") + } + + 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)) + } + } + + 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") + } + + code, signature, err := c.AuthorizeExplicitGrantHandler.AuthorizeCodeStrategy.GenerateAuthorizeCode(ctx, ar) + if err != nil { + return errors.Wrap(fosite.ErrServerError, err.Error()) + } else if err := c.AuthorizeExplicitGrantHandler.AuthorizeCodeGrantStorage.CreateAuthorizeCodeSession(ctx, signature, ar); err != nil { + return errors.Wrap(fosite.ErrServerError, err.Error()) + } + + resp.AddFragment("code", code) + resp.AddFragment("state", ar.GetState()) + ar.SetResponseTypeHandled("code") + + hash, err := c.Enigma.Hash([]byte(resp.GetFragment().Get("code"))) + if err != nil { + return err + } + claims.CodeHash = hash[:c.Enigma.GetSigningMethodLength()/2] + } + + 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") + } else if err := c.AuthorizeImplicitGrantTypeHandler.IssueImplicitAccessToken(ctx, req, ar, resp); err != nil { + return errors.Wrap(err, err.Error()) + } + ar.SetResponseTypeHandled("token") + + hash, err := c.Enigma.Hash([]byte(resp.GetFragment().Get("access_token"))) + if err != nil { + return err + } + claims.AccessTokenHash = hash[:c.Enigma.GetSigningMethodLength()/2] + } + + if !ar.GetGrantedScopes().Has("openid") { + return nil + } + + if err := c.IDTokenHandleHelper.IssueImplicitIDToken(ctx, req, ar, resp); err != nil { + return errors.Wrap(err, err.Error()) + } else if err := c.IDTokenHandleHelper.IssueImplicitIDToken(ctx, req, ar, resp); err != nil { + return errors.Wrap(err, err.Error()) + } + + // there is no need to check for https, because implicit flow does not require https + // https://tools.ietf.org/html/rfc6819#section-4.4.2 + + ar.SetResponseTypeHandled("id_token") + return nil +} diff --git a/handler/oidc/hybrid/hybrid_test.go b/handler/openid/flow_hybrid_test.go similarity index 74% rename from handler/oidc/hybrid/hybrid_test.go rename to handler/openid/flow_hybrid_test.go index 2fdf12dc5..852d730bf 100644 --- a/handler/oidc/hybrid/hybrid_test.go +++ b/handler/openid/flow_hybrid_test.go @@ -1,4 +1,4 @@ -package hybrid +package openid import ( "net/http" @@ -8,12 +8,8 @@ import ( "github.com/golang/mock/gomock" "github.com/ory-am/fosite" - "github.com/ory-am/fosite/fosite-example/store" - "github.com/ory-am/fosite/handler/core/explicit" - "github.com/ory-am/fosite/handler/core/implicit" - oauthStrat "github.com/ory-am/fosite/handler/core/strategy" - "github.com/ory-am/fosite/handler/oidc" - "github.com/ory-am/fosite/handler/oidc/strategy" + store "github.com/ory-am/fosite/fosite-example/pkg" + "github.com/ory-am/fosite/handler/oauth2" "github.com/ory-am/fosite/internal" "github.com/ory-am/fosite/token/hmac" "github.com/ory-am/fosite/token/jwt" @@ -21,13 +17,13 @@ import ( "github.com/stretchr/testify/assert" ) -var idStrategy = &strategy.DefaultStrategy{ +var idStrategy = &DefaultStrategy{ RS256JWTStrategy: &jwt.RS256JWTStrategy{ PrivateKey: internal.MustRSAKey(), }, } -var hmacStrategy = &oauthStrat.HMACSHAStrategy{ +var hmacStrategy = &oauth2.HMACSHAStrategy{ Enigma: &hmac.HMACStrategy{ GlobalSecret: []byte("some-super-cool-secret-that-nobody-knows"), }, @@ -36,7 +32,7 @@ var hmacStrategy = &oauthStrat.HMACSHAStrategy{ type defaultSession struct { Claims *jwt.IDTokenClaims Headers *jwt.Headers - *oauthStrat.HMACSession + *oauth2.HMACSession } func (s *defaultSession) IDTokenHeaders() *jwt.Headers { @@ -53,7 +49,7 @@ func (s *defaultSession) IDTokenClaims() *jwt.IDTokenClaims { return s.Claims } -func TestHandleAuthorizeEndpointRequest(t *testing.T) { +func TestHybrid_HandleAuthorizeEndpointRequest(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -61,21 +57,22 @@ func TestHandleAuthorizeEndpointRequest(t *testing.T) { areq := fosite.NewAuthorizeRequest() httpreq := &http.Request{Form: url.Values{}} h := OpenIDConnectHybridHandler{ - AuthorizeExplicitGrantTypeHandler: &explicit.AuthorizeExplicitGrantTypeHandler{ + AuthorizeExplicitGrantHandler: &oauth2.AuthorizeExplicitGrantHandler{ AuthorizeCodeStrategy: hmacStrategy, AccessTokenLifespan: time.Hour, AuthCodeLifespan: time.Hour, AccessTokenStrategy: hmacStrategy, AuthorizeCodeGrantStorage: store.NewStore(), }, - AuthorizeImplicitGrantTypeHandler: &implicit.AuthorizeImplicitGrantTypeHandler{ + AuthorizeImplicitGrantTypeHandler: &oauth2.AuthorizeImplicitGrantTypeHandler{ AccessTokenLifespan: time.Hour, AccessTokenStrategy: hmacStrategy, AccessTokenStorage: store.NewStore(), }, - IDTokenHandleHelper: &oidc.IDTokenHandleHelper{ + IDTokenHandleHelper: &IDTokenHandleHelper{ IDTokenStrategy: idStrategy, }, + ScopeStrategy: fosite.HierarchicScopeStrategy, } for k, c := range []struct { description string @@ -100,10 +97,11 @@ func TestHandleAuthorizeEndpointRequest(t *testing.T) { areq.Client = &fosite.DefaultClient{ GrantTypes: fosite.Arguments{"authorization_code", "implicit"}, ResponseTypes: fosite.Arguments{"token", "code", "id_token"}, + Scopes: []string{"openid"}, } - areq.Scopes = fosite.Arguments{"openid"} + areq.GrantedScopes = fosite.Arguments{"openid"} }, - expectErr: oidc.ErrInvalidSession, + expectErr: ErrInvalidSession, }, { description: "should fail because client missing response types", @@ -112,13 +110,14 @@ func TestHandleAuthorizeEndpointRequest(t *testing.T) { areq.Client = &fosite.DefaultClient{ GrantTypes: fosite.Arguments{"implicit"}, ResponseTypes: fosite.Arguments{"token", "code", "id_token"}, + Scopes: []string{"openid"}, } areq.Session = &defaultSession{ Claims: &jwt.IDTokenClaims{ Subject: "peter", }, Headers: &jwt.Headers{}, - HMACSession: &oauthStrat.HMACSession{}, + HMACSession: &oauth2.HMACSession{}, } }, expectErr: fosite.ErrInvalidGrant, @@ -129,6 +128,7 @@ func TestHandleAuthorizeEndpointRequest(t *testing.T) { areq.Client = &fosite.DefaultClient{ GrantTypes: fosite.Arguments{"authorization_code", "implicit"}, ResponseTypes: fosite.Arguments{"token", "code", "id_token"}, + Scopes: []string{"openid"}, } }, expectErr: fosite.ErrInsufficientEntropy, @@ -137,20 +137,11 @@ func TestHandleAuthorizeEndpointRequest(t *testing.T) { description: "should fail because nonce was not set", setup: func() { areq.Form.Add("nonce", "some-foobar-nonce-win") - areq.Client = &fosite.DefaultClient{ - GrantTypes: fosite.Arguments{"authorization_code", "implicit"}, - ResponseTypes: fosite.Arguments{"token", "code", "id_token"}, - } }, }, { description: "should pass", - setup: func() { - areq.Client = &fosite.DefaultClient{ - GrantTypes: fosite.Arguments{"authorization_code", "implicit"}, - ResponseTypes: fosite.Arguments{"token", "code", "id_token"}, - } - }, + setup: func() {}, check: func() { assert.NotEmpty(t, aresp.GetFragment().Get("id_token")) assert.NotEmpty(t, aresp.GetFragment().Get("code")) diff --git a/handler/oidc/implicit/implicit.go b/handler/openid/flow_implicit.go similarity index 55% rename from handler/oidc/implicit/implicit.go rename to handler/openid/flow_implicit.go index 6a3f3905b..a7974dca3 100644 --- a/handler/oidc/implicit/implicit.go +++ b/handler/openid/flow_implicit.go @@ -1,42 +1,50 @@ -package implicit +package openid import ( "net/http" - . "github.com/ory-am/fosite" - "github.com/ory-am/fosite/handler/core/implicit" - . "github.com/ory-am/fosite/handler/oidc" - "github.com/ory-am/fosite/handler/oidc/strategy" + "fmt" + + "github.com/ory-am/fosite" + "github.com/ory-am/fosite/handler/oauth2" "github.com/ory-am/fosite/token/jwt" "github.com/pkg/errors" "golang.org/x/net/context" ) type OpenIDConnectImplicitHandler struct { - AuthorizeImplicitGrantTypeHandler *implicit.AuthorizeImplicitGrantTypeHandler + AuthorizeImplicitGrantTypeHandler *oauth2.AuthorizeImplicitGrantTypeHandler *IDTokenHandleHelper + ScopeStrategy fosite.ScopeStrategy RS256JWTStrategy *jwt.RS256JWTStrategy } -func (c *OpenIDConnectImplicitHandler) HandleAuthorizeEndpointRequest(ctx context.Context, req *http.Request, ar AuthorizeRequester, resp AuthorizeResponder) error { - if !(ar.GetScopes().Has("openid") && (ar.GetResponseTypes().Has("token", "id_token") || ar.GetResponseTypes().Exact("id_token"))) { +func (c *OpenIDConnectImplicitHandler) HandleAuthorizeEndpointRequest(ctx context.Context, req *http.Request, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error { + if !(ar.GetGrantedScopes().Has("openid") && (ar.GetResponseTypes().Has("token", "id_token") || ar.GetResponseTypes().Exact("id_token"))) { return nil } if !ar.GetClient().GetGrantTypes().Has("implicit") { - return errors.Wrap(ErrInvalidGrant, "") + return errors.Wrap(fosite.ErrInvalidGrant, "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(ErrInvalidGrant, "") + return errors.Wrap(fosite.ErrInvalidGrant, "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(ErrInvalidGrant, "") + return errors.Wrap(fosite.ErrInvalidGrant, "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)) + } } - sess, ok := ar.GetSession().(strategy.Session) + sess, ok := ar.GetSession().(Session) if !ok { - return ErrInvalidSession + return errors.Wrap(ErrInvalidSession, "") } claims := sess.IDTokenClaims() diff --git a/handler/oidc/implicit/implicit_test.go b/handler/openid/flow_implicit_test.go similarity index 80% rename from handler/oidc/implicit/implicit_test.go rename to handler/openid/flow_implicit_test.go index bb11c460f..b3132f524 100644 --- a/handler/oidc/implicit/implicit_test.go +++ b/handler/openid/flow_implicit_test.go @@ -1,4 +1,4 @@ -package implicit +package openid import ( "net/http" @@ -8,31 +8,14 @@ import ( "github.com/golang/mock/gomock" "github.com/ory-am/fosite" - "github.com/ory-am/fosite/fosite-example/store" - "github.com/ory-am/fosite/handler/core/implicit" - oauthStrat "github.com/ory-am/fosite/handler/core/strategy" - "github.com/ory-am/fosite/handler/oidc" - "github.com/ory-am/fosite/handler/oidc/strategy" - "github.com/ory-am/fosite/internal" - "github.com/ory-am/fosite/token/hmac" + store "github.com/ory-am/fosite/fosite-example/pkg" + "github.com/ory-am/fosite/handler/oauth2" "github.com/ory-am/fosite/token/jwt" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) -var idStrategy = &strategy.DefaultStrategy{ - RS256JWTStrategy: &jwt.RS256JWTStrategy{ - PrivateKey: internal.MustRSAKey(), - }, -} - -var hmacStrategy = &oauthStrat.HMACSHAStrategy{ - Enigma: &hmac.HMACStrategy{ - GlobalSecret: []byte("some-super-cool-secret-that-nobody-knows"), - }, -} - -func TestHandleAuthorizeEndpointRequest(t *testing.T) { +func TestImplicit_HandleAuthorizeEndpointRequest(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -41,14 +24,15 @@ func TestHandleAuthorizeEndpointRequest(t *testing.T) { httpreq := &http.Request{Form: url.Values{}} h := OpenIDConnectImplicitHandler{ - AuthorizeImplicitGrantTypeHandler: &implicit.AuthorizeImplicitGrantTypeHandler{ + AuthorizeImplicitGrantTypeHandler: &oauth2.AuthorizeImplicitGrantTypeHandler{ AccessTokenLifespan: time.Hour, AccessTokenStrategy: hmacStrategy, AccessTokenStorage: store.NewStore(), }, - IDTokenHandleHelper: &oidc.IDTokenHandleHelper{ + IDTokenHandleHelper: &IDTokenHandleHelper{ IDTokenStrategy: idStrategy, }, + ScopeStrategy: fosite.HierarchicScopeStrategy, } for k, c := range []struct { description string @@ -76,7 +60,7 @@ func TestHandleAuthorizeEndpointRequest(t *testing.T) { description: "should not do anything because request requirements are not met", setup: func() { areq.ResponseTypes = fosite.Arguments{} - areq.Scopes = fosite.Arguments{"openid"} + areq.GrantedScopes = fosite.Arguments{"openid"} }, }, { @@ -87,6 +71,7 @@ func TestHandleAuthorizeEndpointRequest(t *testing.T) { areq.Client = &fosite.DefaultClient{ GrantTypes: fosite.Arguments{}, ResponseTypes: fosite.Arguments{}, + Scopes: []string{"openid", "fosite"}, } }, expectErr: fosite.ErrInvalidGrant, @@ -99,6 +84,7 @@ func TestHandleAuthorizeEndpointRequest(t *testing.T) { areq.Client = &fosite.DefaultClient{ GrantTypes: fosite.Arguments{"implicit"}, ResponseTypes: fosite.Arguments{}, + Scopes: []string{"openid", "fosite"}, } }, expectErr: fosite.ErrInvalidGrant, @@ -111,14 +97,15 @@ func TestHandleAuthorizeEndpointRequest(t *testing.T) { areq.Client = &fosite.DefaultClient{ GrantTypes: fosite.Arguments{"implicit"}, ResponseTypes: fosite.Arguments{"token", "id_token"}, + Scopes: []string{"openid", "fosite"}, } }, - expectErr: oidc.ErrInvalidSession, + expectErr: ErrInvalidSession, }, { description: "should fail because nonce not set", setup: func() { - areq.Session = &strategy.DefaultSession{ + areq.Session = &DefaultSession{ Claims: &jwt.IDTokenClaims{ Subject: "peter", }, diff --git a/handler/oidc/helper.go b/handler/openid/helper.go similarity index 71% rename from handler/oidc/helper.go rename to handler/openid/helper.go index 34a34dde4..e1abb4c46 100644 --- a/handler/oidc/helper.go +++ b/handler/openid/helper.go @@ -1,9 +1,9 @@ -package oidc +package openid import ( "net/http" - . "github.com/ory-am/fosite" + "github.com/ory-am/fosite" "golang.org/x/net/context" ) @@ -11,17 +11,16 @@ type IDTokenHandleHelper struct { IDTokenStrategy OpenIDConnectTokenStrategy } -func (i *IDTokenHandleHelper) generateIDToken(ctx context.Context, netr *http.Request, fosr Requester) (token string, err error) { +func (i *IDTokenHandleHelper) generateIDToken(ctx context.Context, netr *http.Request, fosr fosite.Requester) (token string, err error) { token, err = i.IDTokenStrategy.GenerateIDToken(ctx, netr, fosr) if err != nil { return "", err } - fosr.GrantScope("openid") return token, nil } -func (i *IDTokenHandleHelper) IssueImplicitIDToken(ctx context.Context, req *http.Request, ar Requester, resp AuthorizeResponder) error { +func (i *IDTokenHandleHelper) IssueImplicitIDToken(ctx context.Context, req *http.Request, ar fosite.Requester, resp fosite.AuthorizeResponder) error { token, err := i.generateIDToken(ctx, req, ar) if err != nil { return err @@ -31,7 +30,7 @@ func (i *IDTokenHandleHelper) IssueImplicitIDToken(ctx context.Context, req *htt return nil } -func (i *IDTokenHandleHelper) IssueExplicitIDToken(ctx context.Context, req *http.Request, ar Requester, resp AccessResponder) error { +func (i *IDTokenHandleHelper) IssueExplicitIDToken(ctx context.Context, req *http.Request, ar fosite.Requester, resp fosite.AccessResponder) error { token, err := i.generateIDToken(ctx, req, ar) if err != nil { return err diff --git a/handler/oidc/helper_test.go b/handler/openid/helper_test.go similarity index 90% rename from handler/oidc/helper_test.go rename to handler/openid/helper_test.go index 48befb5a9..707eed1fb 100644 --- a/handler/oidc/helper_test.go +++ b/handler/openid/helper_test.go @@ -1,4 +1,4 @@ -package oidc +package openid import ( "net/http" @@ -7,14 +7,13 @@ import ( "github.com/golang/mock/gomock" "github.com/ory-am/fosite" - "github.com/ory-am/fosite/handler/oidc/strategy" "github.com/ory-am/fosite/internal" "github.com/ory-am/fosite/token/jwt" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) -var strat = &strategy.DefaultStrategy{ +var strat = &DefaultStrategy{ RS256JWTStrategy: &jwt.RS256JWTStrategy{ PrivateKey: internal.MustRSAKey(), }, @@ -29,7 +28,7 @@ func TestGenerateIDToken(t *testing.T) { httpreq := &http.Request{Form: url.Values{}} ar := fosite.NewAccessRequest(nil) - sess := &strategy.DefaultSession{ + sess := &DefaultSession{ Claims: &jwt.IDTokenClaims{ Subject: "peter", }, @@ -77,7 +76,7 @@ func TestIssueExplicitToken(t *testing.T) { httpreq := &http.Request{} ar := fosite.NewAuthorizeRequest() ar.Form = url.Values{"nonce": {"111111111111"}} - ar.SetSession(&strategy.DefaultSession{Claims: &jwt.IDTokenClaims{ + ar.SetSession(&DefaultSession{Claims: &jwt.IDTokenClaims{ Subject: "peter", }, Headers: &jwt.Headers{}}) @@ -95,7 +94,7 @@ func TestIssueImplicitToken(t *testing.T) { httpreq := &http.Request{} ar := fosite.NewAuthorizeRequest() ar.Form = url.Values{"nonce": {"111111111111"}} - ar.SetSession(&strategy.DefaultSession{Claims: &jwt.IDTokenClaims{ + ar.SetSession(&DefaultSession{Claims: &jwt.IDTokenClaims{ Subject: "peter", }, Headers: &jwt.Headers{}}) diff --git a/handler/oidc/storage.go b/handler/openid/storage.go similarity index 98% rename from handler/oidc/storage.go rename to handler/openid/storage.go index 71445e33c..09f2cb6cc 100644 --- a/handler/oidc/storage.go +++ b/handler/openid/storage.go @@ -1,4 +1,4 @@ -package oidc +package openid import ( "github.com/ory-am/fosite" diff --git a/handler/oidc/strategy.go b/handler/openid/strategy.go similarity index 94% rename from handler/oidc/strategy.go rename to handler/openid/strategy.go index 73d086b5e..a8b61ec03 100644 --- a/handler/oidc/strategy.go +++ b/handler/openid/strategy.go @@ -1,4 +1,4 @@ -package oidc +package openid import ( "net/http" diff --git a/handler/oidc/strategy/default.go b/handler/openid/strategy_jwt.go similarity index 96% rename from handler/oidc/strategy/default.go rename to handler/openid/strategy_jwt.go index cb58b0f5a..99ef5c972 100644 --- a/handler/oidc/strategy/default.go +++ b/handler/openid/strategy_jwt.go @@ -1,4 +1,4 @@ -package strategy +package openid import ( "net/http" @@ -6,7 +6,6 @@ import ( "time" "github.com/ory-am/fosite" - "github.com/ory-am/fosite/handler/core/strategy" "github.com/ory-am/fosite/token/jwt" "github.com/pkg/errors" "golang.org/x/net/context" @@ -23,7 +22,6 @@ type Session interface { type DefaultSession struct { Claims *jwt.IDTokenClaims Headers *jwt.Headers - *strategy.HMACSession } func (s *DefaultSession) IDTokenHeaders() *jwt.Headers { diff --git a/handler/oidc/strategy/default_test.go b/handler/openid/strategy_jwt_test.go similarity index 90% rename from handler/oidc/strategy/default_test.go rename to handler/openid/strategy_jwt_test.go index ce17d758a..5cdee3fa9 100644 --- a/handler/oidc/strategy/default_test.go +++ b/handler/openid/strategy_jwt_test.go @@ -1,23 +1,15 @@ -package strategy +package openid import ( "testing" - "time" "github.com/ory-am/fosite" - "github.com/ory-am/fosite/internal" "github.com/ory-am/fosite/token/jwt" "github.com/stretchr/testify/assert" ) -var j = &DefaultStrategy{ - RS256JWTStrategy: &jwt.RS256JWTStrategy{ - PrivateKey: internal.MustRSAKey(), - }, -} - -func TestGenerateIDToken(t *testing.T) { +func TestJWTStrategy_GenerateIDToken(t *testing.T) { var req *fosite.AccessRequest for k, c := range []struct { setup func() diff --git a/integration/authorize_code_grant_test.go b/integration/authorize_code_grant_test.go index e26abc0a3..7efca21d4 100644 --- a/integration/authorize_code_grant_test.go +++ b/integration/authorize_code_grant_test.go @@ -4,18 +4,16 @@ import ( "testing" "net/http" - "time" - "github.com/ory-am/fosite/handler/core" - "github.com/ory-am/fosite/handler/core/explicit" - hst "github.com/ory-am/fosite/handler/core/strategy" + "github.com/ory-am/fosite/compose" + "github.com/ory-am/fosite/handler/oauth2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/oauth2" + goauth "golang.org/x/oauth2" ) -func TestAuthorizeCodeGrant(t *testing.T) { - for _, strategy := range []core.AccessTokenStrategy{ +func TestAuthorizeCodeFlow(t *testing.T) { + for _, strategy := range []oauth2.AccessTokenStrategy{ hmacStrategy, } { runAuthorizeCodeGrantTest(t, strategy) @@ -23,9 +21,9 @@ func TestAuthorizeCodeGrant(t *testing.T) { } func runAuthorizeCodeGrantTest(t *testing.T, strategy interface{}) { - f := newFosite() + f := compose.Compose(new(compose.Config), fositeStore, strategy, compose.OAuth2AuthorizeExplicitFactory) ts := mockServer(t, f, &mySessionData{ - HMACSession: new(hst.HMACSession), + HMACSession: new(oauth2.HMACSession), }) defer ts.Close() @@ -38,38 +36,11 @@ func runAuthorizeCodeGrantTest(t *testing.T, strategy interface{}) { setup func() authStatusCode int }{ - { - description: "should fail because handler not registered", - setup: func() { - oauthClient.ClientID = "1234" - }, - authStatusCode: http.StatusBadRequest, - }, - { - description: "should fail (and redirect) because handler not registered", - setup: func() { - oauthClient = newOAuth2Client(ts) - }, - authStatusCode: http.StatusNotAcceptable, - }, { description: "should pass", setup: func() { + oauthClient = newOAuth2Client(ts) state = "12345678901234567890" - handler := &explicit.AuthorizeExplicitGrantTypeHandler{ - AccessTokenStrategy: strategy.(core.AccessTokenStrategy), - RefreshTokenStrategy: strategy.(core.RefreshTokenStrategy), - AuthorizeCodeStrategy: strategy.(core.AuthorizeCodeStrategy), - AuthorizeCodeGrantStorage: fositeStore, - AuthCodeLifespan: time.Minute, - AccessTokenLifespan: time.Hour, - } - f.AuthorizeEndpointHandlers.Append(handler) - f.TokenEndpointHandlers.Append(handler) - f.AuthorizedRequestValidators.Append(&core.CoreValidator{ - AccessTokenStrategy: strategy.(core.AccessTokenStrategy), - AccessTokenStorage: fositeStore, - }) }, authStatusCode: http.StatusOK, }, @@ -81,11 +52,11 @@ func runAuthorizeCodeGrantTest(t *testing.T, strategy interface{}) { 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")) + 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) - httpClient := oauthClient.Client(oauth2.NoContext, token) + 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) diff --git a/integration/authorize_implicit_grant_test.go b/integration/authorize_implicit_grant_test.go index 1d2528239..f5ff7bc48 100644 --- a/integration/authorize_implicit_grant_test.go +++ b/integration/authorize_implicit_grant_test.go @@ -8,17 +8,16 @@ import ( "testing" "time" - "github.com/ory-am/fosite/handler/core" - "github.com/ory-am/fosite/handler/core/implicit" - hst "github.com/ory-am/fosite/handler/core/strategy" + "github.com/ory-am/fosite/compose" + "github.com/ory-am/fosite/handler/oauth2" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/oauth2" + goauth "golang.org/x/oauth2" ) -func TestAuthorizeImplicitGrant(t *testing.T) { - for _, strategy := range []core.AccessTokenStrategy{ +func TestAuthorizeImplicitFlow(t *testing.T) { + for _, strategy := range []oauth2.AccessTokenStrategy{ hmacStrategy, } { runTestAuthorizeImplicitGrant(t, strategy) @@ -26,9 +25,9 @@ func TestAuthorizeImplicitGrant(t *testing.T) { } func runTestAuthorizeImplicitGrant(t *testing.T, strategy interface{}) { - f := newFosite() + f := compose.Compose(new(compose.Config), fositeStore, strategy, compose.OAuth2AuthorizeImplicitFactory) ts := mockServer(t, f, &mySessionData{ - HMACSession: new(hst.HMACSession), + HMACSession: new(oauth2.HMACSession), }) defer ts.Close() @@ -45,16 +44,6 @@ func runTestAuthorizeImplicitGrant(t *testing.T, strategy interface{}) { description: "should pass", setup: func() { state = "12345678901234567890" - handler := &implicit.AuthorizeImplicitGrantTypeHandler{ - AccessTokenStrategy: strategy.(core.AccessTokenStrategy), - AccessTokenStorage: fositeStore, - AccessTokenLifespan: time.Hour, - } - f.AuthorizeEndpointHandlers.Append(handler) - f.AuthorizedRequestValidators.Append(&core.CoreValidator{ - AccessTokenStrategy: strategy.(core.AccessTokenStrategy), - AccessTokenStorage: fositeStore, - }) }, authStatusCode: http.StatusOK, }, @@ -77,14 +66,14 @@ func runTestAuthorizeImplicitGrant(t *testing.T, strategy interface{}) { require.Nil(t, err) expires, err := strconv.Atoi(fragment.Get("expires_in")) require.Nil(t, err) - token := &oauth2.Token{ + 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), } - httpClient := oauthClient.Client(oauth2.NoContext, token) + 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) diff --git a/integration/basic_test.go b/integration/basic_test.go deleted file mode 100644 index 3690374eb..000000000 --- a/integration/basic_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package integration_test - -import ( - "net/http" - "testing" - - hst "github.com/ory-am/fosite/handler/core/strategy" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestBasic(t *testing.T) { - f := newFosite() - ts := mockServer(t, f, &mySessionData{ - HMACSession: new(hst.HMACSession), - }) - defer ts.Close() - - client := newOAuth2Client(ts) - resp, err := http.Get(client.AuthCodeURL("")) - defer resp.Body.Close() - require.Nil(t, err) - assert.Equal(t, http.StatusBadRequest, resp.StatusCode) -} diff --git a/integration/client_credentials_grant_test.go b/integration/client_credentials_grant_test.go index 050681324..4ddfe90fe 100644 --- a/integration/client_credentials_grant_test.go +++ b/integration/client_credentials_grant_test.go @@ -3,87 +3,43 @@ package integration_test import ( "testing" - "github.com/ory-am/fosite/handler/core" - "github.com/ory-am/fosite/handler/core/owner" - hst "github.com/ory-am/fosite/handler/core/strategy" + "github.com/ory-am/fosite/compose" + "github.com/ory-am/fosite/handler/oauth2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/oauth2" + goauth "golang.org/x/oauth2" ) -func TestClientCredentialsGrabt(t *testing.T) { - for _, strategy := range []core.AccessTokenStrategy{ +func TestClientCredentialsFlow(t *testing.T) { + for _, strategy := range []oauth2.AccessTokenStrategy{ hmacStrategy, } { runClientCredentialsGrantTest(t, strategy) } } -func runClientCredentialsGrantTest(t *testing.T, strategy core.AccessTokenStrategy) { - f := newFosite() +func runClientCredentialsGrantTest(t *testing.T, strategy oauth2.AccessTokenStrategy) { + f := compose.Compose(new(compose.Config), fositeStore, strategy, compose.OAuth2ClientCredentialsGrantFactory) ts := mockServer(t, f, &mySessionData{ - HMACSession: new(hst.HMACSession), + HMACSession: new(oauth2.HMACSession), }) defer ts.Close() - oauthClient := newOAuth2Client(ts) - var username string - var password string + oauthClient := newOAuth2AppClient(ts) for k, c := range []struct { description string setup func() err bool }{ - { - description: "should fail because handler not registered", - setup: func() {}, - err: true, - }, - { - description: "should fail because unknown client", - setup: func() { - f.TokenEndpointHandlers.Append(&owner.ResourceOwnerPasswordCredentialsGrantHandler{ - HandleHelper: &core.HandleHelper{ - AccessTokenStrategy: strategy, - AccessTokenStorage: fositeStore, - AccessTokenLifespan: accessTokenLifespan, - }, - ResourceOwnerPasswordCredentialsGrantStorage: fositeStore, - }) - f.AuthorizedRequestValidators.Append(&core.CoreValidator{ - AccessTokenStrategy: strategy.(core.AccessTokenStrategy), - AccessTokenStorage: fositeStore, - }) - }, - err: true, - }, - { - description: "should fail because user does not exist", - setup: func() { - username = "not-existent" - password = "wrong" - }, - err: true, - }, - { - description: "should fail because wrong credentials", - setup: func() { - username = "peter" - password = "wrong" - }, - err: true, - }, { description: "should pass", setup: func() { - username = "peter" - password = "foobar" }, }, } { c.setup() - token, err := oauthClient.PasswordCredentialsToken(oauth2.NoContext, username, password) + token, err := oauthClient.Token(goauth.NoContext) require.Equal(t, c.err, err != nil, "(%d) %s\n%s\n%s", k, c.description, c.err, err) if !c.err { assert.NotEmpty(t, token.AccessToken, "(%d) %s\n%s", k, c.description, token) diff --git a/integration/helper_endpoints_test.go b/integration/helper_endpoints_test.go index c9507bef3..a2884afe6 100644 --- a/integration/helper_endpoints_test.go +++ b/integration/helper_endpoints_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/ory-am/fosite" - hst "github.com/ory-am/fosite/handler/core/strategy" + foauth "github.com/ory-am/fosite/handler/oauth2" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) @@ -16,13 +16,13 @@ type stackTracer interface { type mySessionData struct { Foo string - *hst.HMACSession + *foauth.HMACSession } func tokenInfoHandler(t *testing.T, oauth2 fosite.OAuth2Provider, session interface{}) func(rw http.ResponseWriter, req *http.Request) { return func(rw http.ResponseWriter, req *http.Request) { ctx := fosite.NewContext() - if _, err := oauth2.ValidateRequestAuthorization(ctx, req, session); err != nil { + if _, err := oauth2.ValidateToken(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()) @@ -47,9 +47,12 @@ func authEndpointHandler(t *testing.T, oauth2 fosite.OAuth2Provider, session int return } - if ar.GetScopes().Has("offline") { + if ar.GetRequestedScopes().Has("offline") { ar.GrantScope("offline") } + if ar.GetRequestedScopes().Has("openid") { + ar.GrantScope("openid") + } // Normally, this would be the place where you would check if the user is logged in and gives his consent. // For this test, let's assume that the user exists, is logged in, and gives his consent... @@ -92,7 +95,7 @@ func tokenEndpointHandler(t *testing.T, oauth2 fosite.OAuth2Provider) func(rw ht ctx := fosite.NewContext() accessRequest, err := oauth2.NewAccessRequest(ctx, req, &mySessionData{ - HMACSession: &hst.HMACSession{}, + HMACSession: &foauth.HMACSession{}, }) if err != nil { t.Logf("Access request failed because %s.", err.Error()) diff --git a/integration/helper_setup_test.go b/integration/helper_setup_test.go index 8fcd12992..364d3e5ba 100644 --- a/integration/helper_setup_test.go +++ b/integration/helper_setup_test.go @@ -7,13 +7,11 @@ import ( "github.com/gorilla/mux" "github.com/ory-am/fosite" - "github.com/ory-am/fosite/fosite-example/store" - "github.com/ory-am/fosite/handler/core/strategy" - idstrat "github.com/ory-am/fosite/handler/oidc/strategy" - "github.com/ory-am/fosite/internal" + store "github.com/ory-am/fosite/fosite-example/pkg" + "github.com/ory-am/fosite/handler/oauth2" + "github.com/ory-am/fosite/handler/openid" "github.com/ory-am/fosite/token/hmac" - "github.com/ory-am/fosite/token/jwt" - "golang.org/x/oauth2" + goauth "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" ) @@ -25,13 +23,13 @@ var fositeStore = &store.Store{ RedirectURIs: []string{"http://localhost:3846/callback"}, ResponseTypes: []string{"id_token", "code", "token"}, GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"}, - GrantedScopes: []string{"fosite", "offline"}, + Scopes: []string{"fosite", "offline", "openid"}, }, }, Users: map[string]store.UserRelation{ "peter": { Username: "peter", - Password: "foobar", + Password: "secret", }, }, AuthorizeCodes: map[string]fosite.Requester{}, @@ -41,21 +39,22 @@ var fositeStore = &store.Store{ IDSessions: map[string]fosite.Requester{}, } -var accessTokenLifespan = time.Hour - -var refreshTokenLifespan = time.Hour +type defaultSession struct { + *openid.DefaultSession + *oauth2.HMACSession +} -var idTokenLifespan = time.Hour +var accessTokenLifespan = time.Hour var authCodeLifespan = time.Minute -func newOAuth2Client(ts *httptest.Server) *oauth2.Config { - return &oauth2.Config{ +func newOAuth2Client(ts *httptest.Server) *goauth.Config { + return &goauth.Config{ ClientID: "my-client", ClientSecret: "foobar", RedirectURL: ts.URL + "/callback", Scopes: []string{"fosite"}, - Endpoint: oauth2.Endpoint{ + Endpoint: goauth.Endpoint{ TokenURL: ts.URL + "/token", AuthURL: ts.URL + "/auth", }, @@ -71,13 +70,7 @@ func newOAuth2AppClient(ts *httptest.Server) *clientcredentials.Config { } } -var idTokenStrategy = &idstrat.DefaultStrategy{ - RS256JWTStrategy: &jwt.RS256JWTStrategy{ - PrivateKey: internal.MustRSAKey(), - }, -} - -var hmacStrategy = &strategy.HMACSHAStrategy{ +var hmacStrategy = &oauth2.HMACSHAStrategy{ Enigma: &hmac.HMACStrategy{ GlobalSecret: []byte("some-super-cool-secret-that-nobody-knows"), }, @@ -85,11 +78,6 @@ var hmacStrategy = &strategy.HMACSHAStrategy{ AuthorizeCodeLifespan: authCodeLifespan, } -func newFosite() *fosite.Fosite { - f := fosite.NewFosite(fositeStore) - return f -} - func mockServer(t *testing.T, f fosite.OAuth2Provider, session interface{}) *httptest.Server { router := mux.NewRouter() router.HandleFunc("/auth", authEndpointHandler(t, f, session)) diff --git a/integration/oidc_explicit_test.go b/integration/oidc_explicit_test.go index badeb098b..eb17c5cb4 100644 --- a/integration/oidc_explicit_test.go +++ b/integration/oidc_explicit_test.go @@ -3,31 +3,30 @@ package integration_test import ( "net/http" "testing" - "time" - "github.com/ory-am/fosite/handler/core" - "github.com/ory-am/fosite/handler/core/explicit" - "github.com/ory-am/fosite/handler/oidc" - oidcexp "github.com/ory-am/fosite/handler/oidc/explicit" - "github.com/ory-am/fosite/handler/oidc/strategy" + "fmt" + + "github.com/ory-am/fosite/compose" + "github.com/ory-am/fosite/handler/openid" + "github.com/ory-am/fosite/internal" "github.com/ory-am/fosite/token/jwt" "github.com/stretchr/testify/require" "golang.org/x/oauth2" ) -func TestOpenIDConnectExplicit(t *testing.T) { - session := &strategy.DefaultSession{ - Claims: &jwt.IDTokenClaims{ - Subject: "peter", +func TestOpenIDConnectExplicitFlow(t *testing.T) { + session := &defaultSession{ + DefaultSession: &openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "peter", + }, + Headers: &jwt.Headers{}, }, - Headers: &jwt.Headers{}, } - f := newFosite() + f := compose.ComposeAllEnabled(new(compose.Config), fositeStore, []byte("some-secret-thats-random"), internal.MustRSAKey()) ts := mockServer(t, f, session) defer ts.Close() - - strategy := hmacStrategy oauthClient := newOAuth2Client(ts) fositeStore.Clients["my-client"].RedirectURIs[0] = ts.URL + "/callback" @@ -41,30 +40,7 @@ func TestOpenIDConnectExplicit(t *testing.T) { description: "should pass", setup: func() { state = "12345678901234567890" - oauthClient.Scopes = []string{"fosite", "openid"} - handler := &explicit.AuthorizeExplicitGrantTypeHandler{ - AccessTokenStrategy: strategy, - RefreshTokenStrategy: strategy, - AuthorizeCodeStrategy: strategy, - AuthorizeCodeGrantStorage: fositeStore, - AuthCodeLifespan: time.Minute, - AccessTokenLifespan: time.Hour, - } - f.AuthorizeEndpointHandlers.Append(handler) - f.TokenEndpointHandlers.Append(handler) - - idcHandler := &oidcexp.OpenIDConnectExplicitHandler{ - OpenIDConnectRequestStorage: fositeStore, - IDTokenHandleHelper: &oidc.IDTokenHandleHelper{ - IDTokenStrategy: idTokenStrategy, - }, - } - f.AuthorizeEndpointHandlers.Append(idcHandler) - f.TokenEndpointHandlers.Append(idcHandler) - f.AuthorizedRequestValidators.Append(&core.CoreValidator{ - AccessTokenStrategy: hmacStrategy, - AccessTokenStorage: fositeStore, - }) + oauthClient.Scopes = []string{"openid"} }, authStatusCode: http.StatusOK, }, @@ -77,6 +53,7 @@ func TestOpenIDConnectExplicit(t *testing.T) { 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) diff --git a/integration/oidc_implicit_hybrid_test.go b/integration/oidc_implicit_hybrid_test.go index ffcb01108..0817566f8 100644 --- a/integration/oidc_implicit_hybrid_test.go +++ b/integration/oidc_implicit_hybrid_test.go @@ -8,71 +8,33 @@ import ( "testing" "time" - "github.com/ory-am/fosite/handler/core/explicit" - "github.com/ory-am/fosite/handler/core/implicit" - "github.com/ory-am/fosite/handler/oidc" - "github.com/ory-am/fosite/handler/oidc/hybrid" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" - "github.com/ory-am/fosite/handler/core" - idimplicit "github.com/ory-am/fosite/handler/oidc/implicit" - "github.com/ory-am/fosite/handler/oidc/strategy" + "github.com/ory-am/fosite/compose" + "github.com/ory-am/fosite/handler/openid" + "github.com/ory-am/fosite/internal" "github.com/ory-am/fosite/token/jwt" ) -func TestOIDCImplicitGrants(t *testing.T) { - session := &strategy.DefaultSession{ - Claims: &jwt.IDTokenClaims{ - Subject: "peter", +func TestOIDCImplicitFlow(t *testing.T) { + session := &defaultSession{ + DefaultSession: &openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "peter", + }, + Headers: &jwt.Headers{}, }, - Headers: &jwt.Headers{}, } - f := newFosite() + 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" - explicitHandler := &explicit.AuthorizeExplicitGrantTypeHandler{ - AccessTokenStrategy: hmacStrategy, - RefreshTokenStrategy: hmacStrategy, - AuthorizeCodeStrategy: hmacStrategy, - AuthorizeCodeGrantStorage: fositeStore, - AuthCodeLifespan: time.Minute, - AccessTokenLifespan: time.Hour, - } - f.AuthorizeEndpointHandlers.Append(explicitHandler) - f.TokenEndpointHandlers.Append(explicitHandler) - - implicitHandler := &implicit.AuthorizeImplicitGrantTypeHandler{ - AccessTokenStrategy: hmacStrategy, - AccessTokenStorage: fositeStore, - AccessTokenLifespan: time.Hour, - } - f.AuthorizeEndpointHandlers.Append(implicitHandler) - - f.AuthorizeEndpointHandlers.Append(&idimplicit.OpenIDConnectImplicitHandler{ - AuthorizeImplicitGrantTypeHandler: implicitHandler, - IDTokenHandleHelper: &oidc.IDTokenHandleHelper{ - IDTokenStrategy: idTokenStrategy, - }, - }) - f.AuthorizeEndpointHandlers.Append(&hybrid.OpenIDConnectHybridHandler{ - AuthorizeImplicitGrantTypeHandler: implicitHandler, - AuthorizeExplicitGrantTypeHandler: explicitHandler, - IDTokenHandleHelper: &oidc.IDTokenHandleHelper{ - IDTokenStrategy: idTokenStrategy, - }, - }) - f.AuthorizedRequestValidators.Append(&core.CoreValidator{ - AccessTokenStrategy: hmacStrategy, - AccessTokenStorage: fositeStore, - }) - var state = "12345678901234567890" for k, c := range []struct { responseType string @@ -86,7 +48,7 @@ func TestOIDCImplicitGrants(t *testing.T) { description: "should pass without id token", responseType: "token", setup: func() { - oauthClient.Scopes = []string{f.MandatoryScope} + oauthClient.Scopes = []string{"fosite"} }, }, { @@ -95,7 +57,7 @@ func TestOIDCImplicitGrants(t *testing.T) { nonce: "1111111111111111", description: "should pass id token (id_token token)", setup: func() { - oauthClient.Scopes = []string{f.MandatoryScope, "openid"} + oauthClient.Scopes = []string{"fosite", "openid"} }, hasToken: true, }, diff --git a/integration/refresh_token_grant_test.go b/integration/refresh_token_grant_test.go index 6ae325b1c..49802002e 100644 --- a/integration/refresh_token_grant_test.go +++ b/integration/refresh_token_grant_test.go @@ -6,17 +6,15 @@ import ( "net/http" "time" - "github.com/ory-am/fosite/handler/core" - "github.com/ory-am/fosite/handler/core/explicit" - "github.com/ory-am/fosite/handler/core/refresh" - hst "github.com/ory-am/fosite/handler/core/strategy" + "github.com/ory-am/fosite/compose" + hst "github.com/ory-am/fosite/handler/oauth2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/oauth2" + oauth2 "golang.org/x/oauth2" ) -func TestRefreshTokenGrant(t *testing.T) { - for _, strategy := range []core.AccessTokenStrategy{ +func TestRefreshTokenFlow(t *testing.T) { + for _, strategy := range []hst.AccessTokenStrategy{ hmacStrategy, } { runRefreshTokenGrantTest(t, strategy) @@ -24,54 +22,31 @@ func TestRefreshTokenGrant(t *testing.T) { } func runRefreshTokenGrantTest(t *testing.T, strategy interface{}) { - f := newFosite() + f := compose.Compose( + new(compose.Config), + fositeStore, + strategy, + compose.OAuth2AuthorizeExplicitFactory, + compose.OAuth2RefreshTokenGrantFactory, + ) ts := mockServer(t, f, &mySessionData{ HMACSession: new(hst.HMACSession), }) defer ts.Close() oauthClient := newOAuth2Client(ts) - fositeStore.Clients["my-client"].RedirectURIs[0] = ts.URL + "/callback" - - handler := &explicit.AuthorizeExplicitGrantTypeHandler{ - AccessTokenStrategy: strategy.(core.AccessTokenStrategy), - RefreshTokenStrategy: strategy.(core.RefreshTokenStrategy), - AuthorizeCodeStrategy: strategy.(core.AuthorizeCodeStrategy), - AuthorizeCodeGrantStorage: fositeStore, - AuthCodeLifespan: time.Minute, - AccessTokenLifespan: time.Second, - } - f.AuthorizeEndpointHandlers.Append(handler) - f.TokenEndpointHandlers.Append(handler) - f.AuthorizedRequestValidators.Append(&core.CoreValidator{ - AccessTokenStrategy: strategy.(core.AccessTokenStrategy), - AccessTokenStorage: fositeStore, - }) - state := "1234567890" + fositeStore.Clients["my-client"].RedirectURIs[0] = ts.URL + "/callback" for k, c := range []struct { description string setup func() pass bool }{ { - description: "should fail because handler not registered", + description: "should fail because scope missing", setup: func() {}, pass: false, }, - { - description: "should fail because scope missing", - setup: func() { - handler := &refresh.RefreshTokenGrantHandler{ - AccessTokenStrategy: strategy.(core.AccessTokenStrategy), - RefreshTokenStrategy: strategy.(core.RefreshTokenStrategy), - RefreshTokenGrantStorage: fositeStore, - AccessTokenLifespan: time.Second, - } - f.TokenEndpointHandlers.Append(handler) - }, - pass: false, - }, { description: "should pass", setup: func() { diff --git a/integration/resource_owner_password_credentials_grant_test.go b/integration/resource_owner_password_credentials_grant_test.go index 390e2e158..84ccbdf63 100644 --- a/integration/resource_owner_password_credentials_grant_test.go +++ b/integration/resource_owner_password_credentials_grant_test.go @@ -3,99 +3,53 @@ package integration_test import ( "testing" - "github.com/ory-am/fosite/handler/core" - "github.com/ory-am/fosite/handler/core/client" - hst "github.com/ory-am/fosite/handler/core/strategy" + "github.com/ory-am/fosite/compose" + hst "github.com/ory-am/fosite/handler/oauth2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" - "golang.org/x/oauth2/clientcredentials" ) -func TestResourceOwnerPasswordCredentialsGrant(t *testing.T) { - for _, strategy := range []core.AccessTokenStrategy{ +func TestResourceOwnerPasswordCredentialsFlow(t *testing.T) { + for _, strategy := range []hst.AccessTokenStrategy{ hmacStrategy, } { runResourceOwnerPasswordCredentialsGrantTest(t, strategy) } } -func runResourceOwnerPasswordCredentialsGrantTest(t *testing.T, strategy core.AccessTokenStrategy) { - f := newFosite() +func runResourceOwnerPasswordCredentialsGrantTest(t *testing.T, strategy hst.AccessTokenStrategy) { + f := compose.Compose(new(compose.Config), fositeStore, strategy, compose.OAuth2ResourceOwnerPasswordCredentialsFactory) ts := mockServer(t, f, &mySessionData{ HMACSession: new(hst.HMACSession), }) defer ts.Close() - oauthClient := newOAuth2AppClient(ts) + var username, password string + oauthClient := newOAuth2Client(ts) for k, c := range []struct { description string setup func() err bool }{ { - description: "should fail because handler not registered", - setup: func() {}, - err: true, - }, - { - description: "should fail because unknown client", - setup: func() { - f.TokenEndpointHandlers.Append(&client.ClientCredentialsGrantHandler{ - HandleHelper: &core.HandleHelper{ - AccessTokenStrategy: strategy, - AccessTokenStorage: fositeStore, - AccessTokenLifespan: accessTokenLifespan, - }, - }) - f.AuthorizedRequestValidators.Append(&core.CoreValidator{ - AccessTokenStrategy: strategy.(core.AccessTokenStrategy), - AccessTokenStorage: fositeStore, - }) - - oauthClient = &clientcredentials.Config{ - ClientID: "my-client-wrong", - ClientSecret: "foobar", - Scopes: []string{"fosite"}, - TokenURL: ts.URL + "/token", - } - }, - err: true, - }, - { - description: "should fail because unknown client", - setup: func() { - oauthClient = &clientcredentials.Config{ - ClientID: "my-client", - ClientSecret: "foobar-wrong", - Scopes: []string{"fosite"}, - TokenURL: ts.URL + "/token", - } - }, - err: true, - }, - { - description: "should fail because unknown client", + description: "should fail because invalid password", setup: func() { - oauthClient = &clientcredentials.Config{ - ClientID: "my-client", - ClientSecret: "foobar", - Scopes: []string{""}, - TokenURL: ts.URL + "/token", - } + username = "peter" + password = "something-wrong" }, err: true, }, { description: "should pass", setup: func() { - oauthClient = newOAuth2AppClient(ts) + password = "secret" }, }, } { c.setup() - token, err := oauthClient.Token(oauth2.NoContext) + token, err := oauthClient.PasswordCredentialsToken(oauth2.NoContext, username, password) require.Equal(t, c.err, err != nil, "(%d) %s\n%s\n%s", k, c.description, c.err, err) if !c.err { assert.NotEmpty(t, token.AccessToken, "(%d) %s\n%s", k, c.description, token) diff --git a/internal/access_request.go b/internal/access_request.go index 173c0d0ed..e3396eab8 100644 --- a/internal/access_request.go +++ b/internal/access_request.go @@ -32,6 +32,14 @@ func (_m *MockAccessRequester) EXPECT() *_MockAccessRequesterRecorder { return _m.recorder } +func (_m *MockAccessRequester) AppendRequestedScope(_param0 string) { + _m.ctrl.Call(_m, "AppendRequestedScope", _param0) +} + +func (_mr *_MockAccessRequesterRecorder) AppendRequestedScope(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "AppendRequestedScope", arg0) +} + func (_m *MockAccessRequester) GetClient() fosite.Client { ret := _m.ctrl.Call(_m, "GetClient") ret0, _ := ret[0].(fosite.Client) @@ -82,14 +90,14 @@ func (_mr *_MockAccessRequesterRecorder) GetRequestedAt() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GetRequestedAt") } -func (_m *MockAccessRequester) GetScopes() fosite.Arguments { - ret := _m.ctrl.Call(_m, "GetScopes") +func (_m *MockAccessRequester) GetRequestedScopes() fosite.Arguments { + ret := _m.ctrl.Call(_m, "GetRequestedScopes") ret0, _ := ret[0].(fosite.Arguments) return ret0 } -func (_mr *_MockAccessRequesterRecorder) GetScopes() *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "GetScopes") +func (_mr *_MockAccessRequesterRecorder) GetRequestedScopes() *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "GetRequestedScopes") } func (_m *MockAccessRequester) GetSession() interface{} { @@ -118,12 +126,12 @@ func (_mr *_MockAccessRequesterRecorder) Merge(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Merge", arg0) } -func (_m *MockAccessRequester) SetScopes(_param0 fosite.Arguments) { - _m.ctrl.Call(_m, "SetScopes", _param0) +func (_m *MockAccessRequester) SetRequestedScopes(_param0 fosite.Arguments) { + _m.ctrl.Call(_m, "SetRequestedScopes", _param0) } -func (_mr *_MockAccessRequesterRecorder) SetScopes(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "SetScopes", arg0) +func (_mr *_MockAccessRequesterRecorder) SetRequestedScopes(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "SetRequestedScopes", arg0) } func (_m *MockAccessRequester) SetSession(_param0 interface{}) { diff --git a/internal/authorize_request.go b/internal/authorize_request.go index 9138cf731..d02c7fe3b 100644 --- a/internal/authorize_request.go +++ b/internal/authorize_request.go @@ -32,6 +32,14 @@ func (_m *MockAuthorizeRequester) EXPECT() *_MockAuthorizeRequesterRecorder { return _m.recorder } +func (_m *MockAuthorizeRequester) AppendRequestedScope(_param0 string) { + _m.ctrl.Call(_m, "AppendRequestedScope", _param0) +} + +func (_mr *_MockAuthorizeRequesterRecorder) AppendRequestedScope(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "AppendRequestedScope", arg0) +} + func (_m *MockAuthorizeRequester) DidHandleAllResponseTypes() bool { ret := _m.ctrl.Call(_m, "DidHandleAllResponseTypes") ret0, _ := ret[0].(bool) @@ -92,24 +100,24 @@ func (_mr *_MockAuthorizeRequesterRecorder) GetRequestedAt() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GetRequestedAt") } -func (_m *MockAuthorizeRequester) GetResponseTypes() fosite.Arguments { - ret := _m.ctrl.Call(_m, "GetResponseTypes") +func (_m *MockAuthorizeRequester) GetRequestedScopes() fosite.Arguments { + ret := _m.ctrl.Call(_m, "GetRequestedScopes") ret0, _ := ret[0].(fosite.Arguments) return ret0 } -func (_mr *_MockAuthorizeRequesterRecorder) GetResponseTypes() *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "GetResponseTypes") +func (_mr *_MockAuthorizeRequesterRecorder) GetRequestedScopes() *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "GetRequestedScopes") } -func (_m *MockAuthorizeRequester) GetScopes() fosite.Arguments { - ret := _m.ctrl.Call(_m, "GetScopes") +func (_m *MockAuthorizeRequester) GetResponseTypes() fosite.Arguments { + ret := _m.ctrl.Call(_m, "GetResponseTypes") ret0, _ := ret[0].(fosite.Arguments) return ret0 } -func (_mr *_MockAuthorizeRequesterRecorder) GetScopes() *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "GetScopes") +func (_mr *_MockAuthorizeRequesterRecorder) GetResponseTypes() *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "GetResponseTypes") } func (_m *MockAuthorizeRequester) GetSession() interface{} { @@ -158,20 +166,20 @@ func (_mr *_MockAuthorizeRequesterRecorder) Merge(arg0 interface{}) *gomock.Call return _mr.mock.ctrl.RecordCall(_mr.mock, "Merge", arg0) } -func (_m *MockAuthorizeRequester) SetResponseTypeHandled(_param0 string) { - _m.ctrl.Call(_m, "SetResponseTypeHandled", _param0) +func (_m *MockAuthorizeRequester) SetRequestedScopes(_param0 fosite.Arguments) { + _m.ctrl.Call(_m, "SetRequestedScopes", _param0) } -func (_mr *_MockAuthorizeRequesterRecorder) SetResponseTypeHandled(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "SetResponseTypeHandled", arg0) +func (_mr *_MockAuthorizeRequesterRecorder) SetRequestedScopes(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "SetRequestedScopes", arg0) } -func (_m *MockAuthorizeRequester) SetScopes(_param0 fosite.Arguments) { - _m.ctrl.Call(_m, "SetScopes", _param0) +func (_m *MockAuthorizeRequester) SetResponseTypeHandled(_param0 string) { + _m.ctrl.Call(_m, "SetResponseTypeHandled", _param0) } -func (_mr *_MockAuthorizeRequesterRecorder) SetScopes(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "SetScopes", arg0) +func (_mr *_MockAuthorizeRequesterRecorder) SetResponseTypeHandled(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "SetResponseTypeHandled", arg0) } func (_m *MockAuthorizeRequester) SetSession(_param0 interface{}) { diff --git a/internal/client.go b/internal/client.go index c26f7ecf3..724eb77f4 100644 --- a/internal/client.go +++ b/internal/client.go @@ -39,16 +39,6 @@ func (_mr *_MockClientRecorder) GetGrantTypes() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GetGrantTypes") } -func (_m *MockClient) GetGrantedScopes() fosite.Scopes { - ret := _m.ctrl.Call(_m, "GetGrantedScopes") - ret0, _ := ret[0].(fosite.Scopes) - return ret0 -} - -func (_mr *_MockClientRecorder) GetGrantedScopes() *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "GetGrantedScopes") -} - func (_m *MockClient) GetHashedSecret() []byte { ret := _m.ctrl.Call(_m, "GetHashedSecret") ret0, _ := ret[0].([]byte) @@ -98,3 +88,13 @@ func (_m *MockClient) GetResponseTypes() fosite.Arguments { func (_mr *_MockClientRecorder) GetResponseTypes() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GetResponseTypes") } + +func (_m *MockClient) GetScopes() fosite.Arguments { + ret := _m.ctrl.Call(_m, "GetScopes") + ret0, _ := ret[0].(fosite.Arguments) + return ret0 +} + +func (_mr *_MockClientRecorder) GetScopes() *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "GetScopes") +} diff --git a/internal/core_storage.go b/internal/core_storage.go new file mode 100644 index 000000000..81769b2cf --- /dev/null +++ b/internal/core_storage.go @@ -0,0 +1,124 @@ +// Automatically generated by MockGen. DO NOT EDIT! +// Source: github.com/ory-am/fosite/handler/core (interfaces: CoreStorage) + +package internal + +import ( + gomock "github.com/golang/mock/gomock" + fosite "github.com/ory-am/fosite" + context "golang.org/x/net/context" +) + +// Mock of CoreStorage interface +type MockCoreStorage struct { + ctrl *gomock.Controller + recorder *_MockCoreStorageRecorder +} + +// Recorder for MockCoreStorage (not exported) +type _MockCoreStorageRecorder struct { + mock *MockCoreStorage +} + +func NewMockCoreStorage(ctrl *gomock.Controller) *MockCoreStorage { + mock := &MockCoreStorage{ctrl: ctrl} + mock.recorder = &_MockCoreStorageRecorder{mock} + return mock +} + +func (_m *MockCoreStorage) EXPECT() *_MockCoreStorageRecorder { + return _m.recorder +} + +func (_m *MockCoreStorage) CreateAccessTokenSession(_param0 context.Context, _param1 string, _param2 fosite.Requester) error { + ret := _m.ctrl.Call(_m, "CreateAccessTokenSession", _param0, _param1, _param2) + ret0, _ := ret[0].(error) + return ret0 +} + +func (_mr *_MockCoreStorageRecorder) CreateAccessTokenSession(arg0, arg1, arg2 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "CreateAccessTokenSession", arg0, arg1, arg2) +} + +func (_m *MockCoreStorage) CreateAuthorizeCodeSession(_param0 context.Context, _param1 string, _param2 fosite.Requester) error { + ret := _m.ctrl.Call(_m, "CreateAuthorizeCodeSession", _param0, _param1, _param2) + ret0, _ := ret[0].(error) + return ret0 +} + +func (_mr *_MockCoreStorageRecorder) CreateAuthorizeCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "CreateAuthorizeCodeSession", arg0, arg1, arg2) +} + +func (_m *MockCoreStorage) CreateRefreshTokenSession(_param0 context.Context, _param1 string, _param2 fosite.Requester) error { + ret := _m.ctrl.Call(_m, "CreateRefreshTokenSession", _param0, _param1, _param2) + ret0, _ := ret[0].(error) + return ret0 +} + +func (_mr *_MockCoreStorageRecorder) CreateRefreshTokenSession(arg0, arg1, arg2 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "CreateRefreshTokenSession", arg0, arg1, arg2) +} + +func (_m *MockCoreStorage) DeleteAccessTokenSession(_param0 context.Context, _param1 string) error { + ret := _m.ctrl.Call(_m, "DeleteAccessTokenSession", _param0, _param1) + ret0, _ := ret[0].(error) + return ret0 +} + +func (_mr *_MockCoreStorageRecorder) DeleteAccessTokenSession(arg0, arg1 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "DeleteAccessTokenSession", arg0, arg1) +} + +func (_m *MockCoreStorage) DeleteAuthorizeCodeSession(_param0 context.Context, _param1 string) error { + ret := _m.ctrl.Call(_m, "DeleteAuthorizeCodeSession", _param0, _param1) + ret0, _ := ret[0].(error) + return ret0 +} + +func (_mr *_MockCoreStorageRecorder) DeleteAuthorizeCodeSession(arg0, arg1 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "DeleteAuthorizeCodeSession", arg0, arg1) +} + +func (_m *MockCoreStorage) DeleteRefreshTokenSession(_param0 context.Context, _param1 string) error { + ret := _m.ctrl.Call(_m, "DeleteRefreshTokenSession", _param0, _param1) + ret0, _ := ret[0].(error) + return ret0 +} + +func (_mr *_MockCoreStorageRecorder) DeleteRefreshTokenSession(arg0, arg1 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "DeleteRefreshTokenSession", arg0, arg1) +} + +func (_m *MockCoreStorage) GetAccessTokenSession(_param0 context.Context, _param1 string, _param2 interface{}) (fosite.Requester, error) { + ret := _m.ctrl.Call(_m, "GetAccessTokenSession", _param0, _param1, _param2) + ret0, _ := ret[0].(fosite.Requester) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +func (_mr *_MockCoreStorageRecorder) GetAccessTokenSession(arg0, arg1, arg2 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "GetAccessTokenSession", arg0, arg1, arg2) +} + +func (_m *MockCoreStorage) GetAuthorizeCodeSession(_param0 context.Context, _param1 string, _param2 interface{}) (fosite.Requester, error) { + ret := _m.ctrl.Call(_m, "GetAuthorizeCodeSession", _param0, _param1, _param2) + ret0, _ := ret[0].(fosite.Requester) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +func (_mr *_MockCoreStorageRecorder) GetAuthorizeCodeSession(arg0, arg1, arg2 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "GetAuthorizeCodeSession", arg0, arg1, arg2) +} + +func (_m *MockCoreStorage) GetRefreshTokenSession(_param0 context.Context, _param1 string, _param2 interface{}) (fosite.Requester, error) { + ret := _m.ctrl.Call(_m, "GetRefreshTokenSession", _param0, _param1, _param2) + ret0, _ := ret[0].(fosite.Requester) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +func (_mr *_MockCoreStorageRecorder) GetRefreshTokenSession(arg0, arg1, arg2 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "GetRefreshTokenSession", arg0, arg1, arg2) +} diff --git a/internal/core_strategy.go b/internal/core_strategy.go new file mode 100644 index 000000000..870dd77df --- /dev/null +++ b/internal/core_strategy.go @@ -0,0 +1,127 @@ +// Automatically generated by MockGen. DO NOT EDIT! +// Source: github.com/ory-am/fosite/handler/core (interfaces: CoreStrategy) + +package internal + +import ( + gomock "github.com/golang/mock/gomock" + fosite "github.com/ory-am/fosite" + context "golang.org/x/net/context" +) + +// Mock of CoreStrategy interface +type MockCoreStrategy struct { + ctrl *gomock.Controller + recorder *_MockCoreStrategyRecorder +} + +// Recorder for MockCoreStrategy (not exported) +type _MockCoreStrategyRecorder struct { + mock *MockCoreStrategy +} + +func NewMockCoreStrategy(ctrl *gomock.Controller) *MockCoreStrategy { + mock := &MockCoreStrategy{ctrl: ctrl} + mock.recorder = &_MockCoreStrategyRecorder{mock} + return mock +} + +func (_m *MockCoreStrategy) EXPECT() *_MockCoreStrategyRecorder { + return _m.recorder +} + +func (_m *MockCoreStrategy) AccessTokenSignature(_param0 string) string { + ret := _m.ctrl.Call(_m, "AccessTokenSignature", _param0) + ret0, _ := ret[0].(string) + return ret0 +} + +func (_mr *_MockCoreStrategyRecorder) AccessTokenSignature(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "AccessTokenSignature", arg0) +} + +func (_m *MockCoreStrategy) AuthorizeCodeSignature(_param0 string) string { + ret := _m.ctrl.Call(_m, "AuthorizeCodeSignature", _param0) + ret0, _ := ret[0].(string) + return ret0 +} + +func (_mr *_MockCoreStrategyRecorder) AuthorizeCodeSignature(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "AuthorizeCodeSignature", arg0) +} + +func (_m *MockCoreStrategy) GenerateAccessToken(_param0 context.Context, _param1 fosite.Requester) (string, string, error) { + ret := _m.ctrl.Call(_m, "GenerateAccessToken", _param0, _param1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +func (_mr *_MockCoreStrategyRecorder) GenerateAccessToken(arg0, arg1 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "GenerateAccessToken", arg0, arg1) +} + +func (_m *MockCoreStrategy) GenerateAuthorizeCode(_param0 context.Context, _param1 fosite.Requester) (string, string, error) { + ret := _m.ctrl.Call(_m, "GenerateAuthorizeCode", _param0, _param1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +func (_mr *_MockCoreStrategyRecorder) GenerateAuthorizeCode(arg0, arg1 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "GenerateAuthorizeCode", arg0, arg1) +} + +func (_m *MockCoreStrategy) GenerateRefreshToken(_param0 context.Context, _param1 fosite.Requester) (string, string, error) { + ret := _m.ctrl.Call(_m, "GenerateRefreshToken", _param0, _param1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +func (_mr *_MockCoreStrategyRecorder) GenerateRefreshToken(arg0, arg1 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "GenerateRefreshToken", arg0, arg1) +} + +func (_m *MockCoreStrategy) RefreshTokenSignature(_param0 string) string { + ret := _m.ctrl.Call(_m, "RefreshTokenSignature", _param0) + ret0, _ := ret[0].(string) + return ret0 +} + +func (_mr *_MockCoreStrategyRecorder) RefreshTokenSignature(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "RefreshTokenSignature", arg0) +} + +func (_m *MockCoreStrategy) ValidateAccessToken(_param0 context.Context, _param1 fosite.Requester, _param2 string) error { + ret := _m.ctrl.Call(_m, "ValidateAccessToken", _param0, _param1, _param2) + ret0, _ := ret[0].(error) + return ret0 +} + +func (_mr *_MockCoreStrategyRecorder) ValidateAccessToken(arg0, arg1, arg2 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "ValidateAccessToken", arg0, arg1, arg2) +} + +func (_m *MockCoreStrategy) ValidateAuthorizeCode(_param0 context.Context, _param1 fosite.Requester, _param2 string) error { + ret := _m.ctrl.Call(_m, "ValidateAuthorizeCode", _param0, _param1, _param2) + ret0, _ := ret[0].(error) + return ret0 +} + +func (_mr *_MockCoreStrategyRecorder) ValidateAuthorizeCode(arg0, arg1, arg2 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "ValidateAuthorizeCode", arg0, arg1, arg2) +} + +func (_m *MockCoreStrategy) ValidateRefreshToken(_param0 context.Context, _param1 fosite.Requester, _param2 string) error { + ret := _m.ctrl.Call(_m, "ValidateRefreshToken", _param0, _param1, _param2) + ret0, _ := ret[0].(error) + return ret0 +} + +func (_mr *_MockCoreStrategyRecorder) ValidateRefreshToken(arg0, arg1, arg2 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "ValidateRefreshToken", arg0, arg1, arg2) +} diff --git a/internal/core_owner_storage.go b/internal/oauth2_owner_storage.go similarity index 96% rename from internal/core_owner_storage.go rename to internal/oauth2_owner_storage.go index f2e7bfed7..3413b698c 100644 --- a/internal/core_owner_storage.go +++ b/internal/oauth2_owner_storage.go @@ -1,5 +1,5 @@ // Automatically generated by MockGen. DO NOT EDIT! -// Source: github.com/ory-am/fosite/handler/core/owner (interfaces: ResourceOwnerPasswordCredentialsGrantStorage) +// Source: github.com/ory-am/fosite/handler/oauth2 (interfaces: ResourceOwnerPasswordCredentialsGrantStorage) package internal diff --git a/internal/core_refresh_storage.go b/internal/oauth2_refresh_storage.go similarity index 96% rename from internal/core_refresh_storage.go rename to internal/oauth2_refresh_storage.go index 144eb0a08..2228abaa3 100644 --- a/internal/core_refresh_storage.go +++ b/internal/oauth2_refresh_storage.go @@ -1,5 +1,5 @@ // Automatically generated by MockGen. DO NOT EDIT! -// Source: github.com/ory-am/fosite/handler/core/refresh (interfaces: RefreshTokenGrantStorage) +// Source: github.com/ory-am/fosite/handler/oauth2 (interfaces: RefreshTokenGrantStorage) package internal diff --git a/internal/oidc_id_token_storage.go b/internal/openid_id_token_storage.go similarity index 96% rename from internal/oidc_id_token_storage.go rename to internal/openid_id_token_storage.go index 5eeb28987..5ff8a745b 100644 --- a/internal/oidc_id_token_storage.go +++ b/internal/openid_id_token_storage.go @@ -1,5 +1,5 @@ // Automatically generated by MockGen. DO NOT EDIT! -// Source: github.com/ory-am/fosite/handler/oidc (interfaces: OpenIDConnectRequestStorage) +// Source: github.com/ory-am/fosite/handler/openid (interfaces: OpenIDConnectRequestStorage) package internal diff --git a/internal/request.go b/internal/request.go index 1d13bc71e..6f6f2883c 100644 --- a/internal/request.go +++ b/internal/request.go @@ -32,6 +32,14 @@ func (_m *MockRequester) EXPECT() *_MockRequesterRecorder { return _m.recorder } +func (_m *MockRequester) AppendRequestedScope(_param0 string) { + _m.ctrl.Call(_m, "AppendRequestedScope", _param0) +} + +func (_mr *_MockRequesterRecorder) AppendRequestedScope(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "AppendRequestedScope", arg0) +} + func (_m *MockRequester) GetClient() fosite.Client { ret := _m.ctrl.Call(_m, "GetClient") ret0, _ := ret[0].(fosite.Client) @@ -72,14 +80,14 @@ func (_mr *_MockRequesterRecorder) GetRequestedAt() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GetRequestedAt") } -func (_m *MockRequester) GetScopes() fosite.Arguments { - ret := _m.ctrl.Call(_m, "GetScopes") +func (_m *MockRequester) GetRequestedScopes() fosite.Arguments { + ret := _m.ctrl.Call(_m, "GetRequestedScopes") ret0, _ := ret[0].(fosite.Arguments) return ret0 } -func (_mr *_MockRequesterRecorder) GetScopes() *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "GetScopes") +func (_mr *_MockRequesterRecorder) GetRequestedScopes() *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "GetRequestedScopes") } func (_m *MockRequester) GetSession() interface{} { @@ -108,12 +116,12 @@ func (_mr *_MockRequesterRecorder) Merge(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Merge", arg0) } -func (_m *MockRequester) SetScopes(_param0 fosite.Arguments) { - _m.ctrl.Call(_m, "SetScopes", _param0) +func (_m *MockRequester) SetRequestedScopes(_param0 fosite.Arguments) { + _m.ctrl.Call(_m, "SetRequestedScopes", _param0) } -func (_mr *_MockRequesterRecorder) SetScopes(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "SetScopes", arg0) +func (_mr *_MockRequesterRecorder) SetRequestedScopes(arg0 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "SetRequestedScopes", arg0) } func (_m *MockRequester) SetSession(_param0 interface{}) { diff --git a/internal/validator.go b/internal/validator.go index 9a28e06d9..50fce88bf 100644 --- a/internal/validator.go +++ b/internal/validator.go @@ -1,43 +1,41 @@ // Automatically generated by MockGen. DO NOT EDIT! -// Source: github.com/ory-am/fosite (interfaces: AuthorizedRequestValidator) +// Source: github.com/ory-am/fosite (interfaces: TokenValidator) package internal import ( - http "net/http" - gomock "github.com/golang/mock/gomock" fosite "github.com/ory-am/fosite" context "golang.org/x/net/context" ) -// Mock of AuthorizedRequestValidator interface -type MockAuthorizedRequestValidator struct { +// Mock of TokenValidator interface +type MockTokenValidator struct { ctrl *gomock.Controller - recorder *_MockAuthorizedRequestValidatorRecorder + recorder *_MockTokenValidatorRecorder } -// Recorder for MockAuthorizedRequestValidator (not exported) -type _MockAuthorizedRequestValidatorRecorder struct { - mock *MockAuthorizedRequestValidator +// Recorder for MockTokenValidator (not exported) +type _MockTokenValidatorRecorder struct { + mock *MockTokenValidator } -func NewMockAuthorizedRequestValidator(ctrl *gomock.Controller) *MockAuthorizedRequestValidator { - mock := &MockAuthorizedRequestValidator{ctrl: ctrl} - mock.recorder = &_MockAuthorizedRequestValidatorRecorder{mock} +func NewMockTokenValidator(ctrl *gomock.Controller) *MockTokenValidator { + mock := &MockTokenValidator{ctrl: ctrl} + mock.recorder = &_MockTokenValidatorRecorder{mock} return mock } -func (_m *MockAuthorizedRequestValidator) EXPECT() *_MockAuthorizedRequestValidatorRecorder { +func (_m *MockTokenValidator) EXPECT() *_MockTokenValidatorRecorder { return _m.recorder } -func (_m *MockAuthorizedRequestValidator) ValidateRequest(_param0 context.Context, _param1 *http.Request, _param2 fosite.AccessRequester) error { - ret := _m.ctrl.Call(_m, "ValidateRequest", _param0, _param1, _param2) +func (_m *MockTokenValidator) ValidateToken(_param0 context.Context, _param1 string, _param2 fosite.TokenType, _param3 fosite.AccessRequester, _param4 []string) error { + ret := _m.ctrl.Call(_m, "ValidateToken", _param0, _param1, _param2, _param3, _param4) ret0, _ := ret[0].(error) return ret0 } -func (_mr *_MockAuthorizedRequestValidatorRecorder) ValidateRequest(arg0, arg1, arg2 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "ValidateRequest", arg0, arg1, arg2) +func (_mr *_MockTokenValidatorRecorder) ValidateToken(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "ValidateToken", arg0, arg1, arg2, arg3, arg4) } diff --git a/oauth2.go b/oauth2.go index 3061661b1..08dd4d20a 100644 --- a/oauth2.go +++ b/oauth2.go @@ -8,10 +8,17 @@ import ( "golang.org/x/net/context" ) -var DefaultMandatoryScope = "fosite" - const MinParameterEntropy = 8 +type TokenType string + +const ( + AccessToken TokenType = "access_token" + RefreshToken TokenType = "refresh_token" + AuthorizeCode TokenType = "authorize_code" + IDToken TokenType = "id_token" +) + // OAuth2Provider is an interface that enables you to write OAuth2 handlers with only a few lines of code. // Check fosite.Fosite for an implementation of this interface. type OAuth2Provider interface { @@ -104,13 +111,9 @@ type OAuth2Provider interface { // https://tools.ietf.org/html/rfc6749#section-5.1 WriteAccessResponse(rw http.ResponseWriter, requester AccessRequester, responder AccessResponder) - // ValidateRequestAuthorization returns nil if the http request contains a valid access token or an error if not. - // If the token is valid, ValidateRequestAuthorization will return the access request object. - ValidateRequestAuthorization(ctx context.Context, req *http.Request, session interface{}, scope ...string) (AccessRequester, error) - - // GetMandatoryScope returns the mandatory scope. Fosite enforces the usage of at least one scope. Returns a - // default value if no scope was set. - GetMandatoryScope() string + // ValidateToken validates a token. Popular validators include authorize code, id token, access token and refresh token. + // Returns an error if validation failed. + ValidateToken(ctx context.Context, token string, tokenType TokenType, session interface{}, scope ...string) (AccessRequester, error) } // Requester is an abstract interface for handling requests in Fosite. @@ -121,11 +124,14 @@ type Requester interface { // GetClient returns the requests client. GetClient() (client Client) - // GetScopes returns the request's scopes. - GetScopes() (scopes Arguments) + // GetRequestedScopes returns the request's scopes. + GetRequestedScopes() (scopes Arguments) - // SetScopes sets the request's scopes. - SetScopes(scopes Arguments) + // SetRequestedScopes sets the request's scopes. + SetRequestedScopes(scopes Arguments) + + // AppendRequestedScope appends a scope to the request. + AppendRequestedScope(scope string) // GetGrantScopes returns all granted scopes. GetGrantedScopes() (grantedScopes Arguments) diff --git a/request.go b/request.go index 1114f030e..4f2040cc4 100644 --- a/request.go +++ b/request.go @@ -37,12 +37,23 @@ func (a *Request) GetClient() Client { return a.Client } -func (a *Request) GetScopes() Arguments { +func (a *Request) GetRequestedScopes() Arguments { return a.Scopes } -func (a *Request) SetScopes(s Arguments) { - a.Scopes = s +func (a *Request) SetRequestedScopes(s Arguments) { + for _, scope := range s { + a.AppendRequestedScope(scope) + } +} + +func (a *Request) AppendRequestedScope(scope string) { + for _, has := range a.Scopes { + if scope == has { + return + } + } + a.Scopes = append(a.Scopes, scope) } func (a *Request) GetGrantedScopes() Arguments { @@ -67,11 +78,11 @@ func (a *Request) GetSession() interface{} { } func (a *Request) Merge(request Requester) { - for _, scope := range request.GetScopes() { - a.Scopes = append(a.Scopes, scope) + for _, scope := range request.GetRequestedScopes() { + a.AppendRequestedScope(scope) } for _, scope := range request.GetGrantedScopes() { - a.GrantedScopes = append(a.GrantedScopes, scope) + a.GrantScope(scope) } a.RequestedAt = request.GetRequestedAt() a.Client = request.GetClient() diff --git a/scope.go b/scope.go deleted file mode 100644 index aa2118fe5..000000000 --- a/scope.go +++ /dev/null @@ -1,8 +0,0 @@ -package fosite - -func (f *Fosite) GetMandatoryScope() string { - if f.MandatoryScope == "" { - return DefaultMandatoryScope - } - return f.MandatoryScope -} diff --git a/scope_strategy.go b/scope_strategy.go new file mode 100644 index 000000000..915d635c3 --- /dev/null +++ b/scope_strategy.go @@ -0,0 +1,36 @@ +package fosite + +import "strings" + +// ScopeStrategy is a strategy for matching scopes. +type ScopeStrategy func(haystack []string, needle string) bool + +func HierarchicScopeStrategy(haystack []string, needle string) bool { + for _, this := range haystack { + // foo == foo -> true + if this == needle { + return true + } + + // picture.read > picture -> false (scope picture includes read, write, ...) + if len(this) > len(needle) { + continue + } + + needles := strings.Split(needle, ".") + haystack := strings.Split(this, ".") + haystackLen := len(haystack) - 1 + for k, needle := range needles { + if haystackLen < k { + return true + } + + current := haystack[k] + if current != needle { + continue + } + } + } + + return false +} diff --git a/scope_strategy_test.go b/scope_strategy_test.go new file mode 100644 index 000000000..576ae408e --- /dev/null +++ b/scope_strategy_test.go @@ -0,0 +1,33 @@ +package fosite + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHierarchicScopeStrategy(t *testing.T) { + var strategy ScopeStrategy = HierarchicScopeStrategy + var scopes = []string{} + + assert.False(t, strategy(scopes, "foo.bar.baz")) + assert.False(t, strategy(scopes, "foo.bar")) + assert.False(t, strategy(scopes, "foo")) + + scopes = []string{"foo.bar", "bar.baz", "baz.baz.1", "baz.baz.2", "baz.baz.3", "baz.baz.baz"} + assert.True(t, strategy(scopes, "foo.bar.baz")) + assert.True(t, strategy(scopes, "baz.baz.baz")) + assert.True(t, strategy(scopes, "foo.bar")) + assert.False(t, strategy(scopes, "foo")) + + assert.True(t, strategy(scopes, "bar.baz")) + assert.True(t, strategy(scopes, "bar.baz.zad")) + assert.False(t, strategy(scopes, "bar")) + assert.False(t, strategy(scopes, "baz")) + + scopes = []string{"fosite.keys.create", "fosite.keys.get", "fosite.keys.delete", "fosite.keys.update"} + assert.True(t, strategy(scopes, "fosite.keys.delete")) + assert.True(t, strategy(scopes, "fosite.keys.get")) + assert.True(t, strategy(scopes, "fosite.keys.get")) + assert.True(t, strategy(scopes, "fosite.keys.update")) +} diff --git a/scope_test.go b/scope_test.go deleted file mode 100644 index 8a32a55aa..000000000 --- a/scope_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package fosite - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetRequiredScope(t *testing.T) { - f := Fosite{MandatoryScope: ""} - assert.Equal(t, DefaultMandatoryScope, f.GetMandatoryScope()) - - f.MandatoryScope = "foo" - assert.Equal(t, "foo", f.GetMandatoryScope()) -} diff --git a/validator.go b/validator.go index 1efd7b507..ca969a43d 100644 --- a/validator.go +++ b/validator.go @@ -2,23 +2,35 @@ package fosite import ( "net/http" + "strings" "github.com/pkg/errors" "golang.org/x/net/context" ) -type AuthorizedRequestValidator interface { - ValidateRequest(ctx context.Context, req *http.Request, accessRequest AccessRequester) error +type TokenValidator interface { + ValidateToken(ctx context.Context, token string, tokenType TokenType, accessRequest AccessRequester, scopes []string) error } -func (f *Fosite) ValidateRequestAuthorization(ctx context.Context, req *http.Request, session interface{}, scopes ...string) (AccessRequester, error) { +func AccessTokenFromRequest(req *http.Request) string { + auth := req.Header.Get("Authorization") + split := strings.SplitN(auth, " ", 2) + if len(split) != 2 || !strings.EqualFold(split[0], "bearer") { + return "" + } + + return split[1] +} + +func (f *Fosite) ValidateToken(ctx context.Context, token string, tokenType TokenType, session interface{}, scopes ...string) (AccessRequester, error) { var found bool = false + ar := NewAccessRequest(session) - for _, validator := range f.AuthorizedRequestValidators { - if err := errors.Cause(validator.ValidateRequest(ctx, req, ar)); err == ErrUnknownRequest { + for _, validator := range f.TokenValidators { + if err := errors.Cause(validator.ValidateToken(ctx, token, tokenType, ar, scopes)); err == ErrUnknownRequest { // Nothing to do } else if err != nil { - return nil, errors.Wrap(err, "") + return nil, errors.Wrap(err, "An error occurred") } else { found = true } @@ -28,13 +40,5 @@ func (f *Fosite) ValidateRequestAuthorization(ctx context.Context, req *http.Req return nil, errors.Wrap(ErrRequestUnauthorized, "") } - if len(scopes) == 0 { - scopes = append(scopes, f.GetMandatoryScope()) - } - - if !ar.GetGrantedScopes().Has(scopes...) { - return nil, errors.Wrap(ErrRequestForbidden, "one or more scopes missing") - } - return ar, nil } diff --git a/validator_test.go b/validator_test.go index dd91d41c0..90c369e05 100644 --- a/validator_test.go +++ b/validator_test.go @@ -7,20 +7,26 @@ import ( "github.com/golang/mock/gomock" . "github.com/ory-am/fosite" - "github.com/ory-am/fosite/fosite-example/store" + "github.com/ory-am/fosite/compose" + store "github.com/ory-am/fosite/fosite-example/pkg" "github.com/ory-am/fosite/internal" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "golang.org/x/net/context" ) -func TestValidateRequestAuthorization(t *testing.T) { +func TestValidate(t *testing.T) { ctrl := gomock.NewController(t) - validator := internal.NewMockAuthorizedRequestValidator(ctrl) + validator := internal.NewMockTokenValidator(ctrl) defer ctrl.Finish() - f := NewFosite(store.NewStore()) - httpreq := &http.Request{Form: url.Values{}} + f := compose.ComposeAllEnabled(new(compose.Config), store.NewStore(), []byte{}, nil).(*Fosite) + httpreq := &http.Request{ + Header: http.Header{ + "Authorization": []string{"bearer some-token"}, + }, + Form: url.Values{}, + } for k, c := range []struct { description string @@ -39,8 +45,8 @@ func TestValidateRequestAuthorization(t *testing.T) { description: "should fail", scopes: []string{"foo"}, setup: func() { - f.AuthorizedRequestValidators = AuthorizedRequestValidators{validator} - validator.EXPECT().ValidateRequest(nil, httpreq, gomock.Any()).Return(ErrUnknownRequest) + f.TokenValidators = TokenValidators{validator} + validator.EXPECT().ValidateToken(nil, "some-token", gomock.Any(), gomock.Any(), gomock.Any()).Return(ErrUnknownRequest) }, expectErr: ErrRequestUnauthorized, }, @@ -48,24 +54,15 @@ func TestValidateRequestAuthorization(t *testing.T) { description: "should fail", scopes: []string{"foo"}, setup: func() { - validator.EXPECT().ValidateRequest(nil, httpreq, gomock.Any()).Return(ErrInvalidClient) + validator.EXPECT().ValidateToken(nil, "some-token", gomock.Any(), gomock.Any(), gomock.Any()).Return(ErrInvalidClient) }, expectErr: ErrInvalidClient, }, - { - description: "should fail", - setup: func() { - validator.EXPECT().ValidateRequest(nil, httpreq, gomock.Any()).Do(func(ctx context.Context, req *http.Request, accessRequest AccessRequester) { - accessRequest.(*AccessRequest).GrantedScopes = []string{"bar"} - }).Return(nil) - }, - expectErr: ErrRequestForbidden, - }, { description: "should pass", setup: func() { - validator.EXPECT().ValidateRequest(nil, httpreq, gomock.Any()).Do(func(ctx context.Context, req *http.Request, accessRequest AccessRequester) { - accessRequest.(*AccessRequest).GrantedScopes = []string{f.MandatoryScope} + validator.EXPECT().ValidateToken(nil, "some-token", gomock.Any(), gomock.Any(), gomock.Any()).Do(func(ctx context.Context, _ string, _ TokenType, accessRequest AccessRequester, _ []string) { + accessRequest.(*AccessRequest).GrantedScopes = []string{"bar"} }).Return(nil) }, }, @@ -73,14 +70,14 @@ func TestValidateRequestAuthorization(t *testing.T) { description: "should pass", scopes: []string{"bar"}, setup: func() { - validator.EXPECT().ValidateRequest(nil, httpreq, gomock.Any()).Do(func(ctx context.Context, req *http.Request, accessRequest AccessRequester) { + validator.EXPECT().ValidateToken(nil, "some-token", gomock.Any(), gomock.Any(), gomock.Any()).Do(func(ctx context.Context, _ string, _ TokenType, accessRequest AccessRequester, _ []string) { accessRequest.(*AccessRequest).GrantedScopes = []string{"bar"} }).Return(nil) }, }, } { c.setup() - _, err := f.ValidateRequestAuthorization(nil, httpreq, nil, c.scopes...) + _, err := f.ValidateToken(nil, AccessTokenFromRequest(httpreq), 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) }