From 8256356b9b4704c369b4d01498b5cd0b11fd1919 Mon Sep 17 00:00:00 2001 From: Aeneas Date: Tue, 25 Oct 2016 12:01:26 +0200 Subject: [PATCH] 0.6.0 (#293) * oauth2: scopes should be separated by %20 and not +, to ensure javascript compatibility - closes #277 * oauth2/introspect: make endpoint rfc7662 compatible - closes #289 * warden: make it clear that ladon.Request.Subject is not required or break bc and remove it - closes #270 * travis: execute gox build only when new commit is a new tag - closes #285 * docs: improve introduction (#267) * core: (health) monitoring endpoint - closes #216 * oauth2/introspect: make endpoint rfc7662 compatible - closes #289 * connections: remove connections API - closes #265 * oauth2: token revocation endpoint - closes #233 * vendor: update to fosite 0.5.0 * core: add sql support #292 * connections: remove connections API - closes #265 * all: coverage report is missing covered lines of nested packages - closes #296 * cmd: prettify the `hydra token user` output - closes #281 * travis: make it possible for travis-ci to build forked repos - closes #295 --- .gitignore | 3 +- .travis.yml | 14 +- CONTRIBUTING.md | 21 ++ Dockerfile-dangerous => Dockerfile-demo | 2 +- PATRONS.md | 13 + README.md | 113 ++---- client/client.go | 5 + client/client_test.go | 24 ++ client/handler.go | 14 +- client/manager_memory.go | 7 +- client/manager_rethinkdb.go | 7 +- client/manager_sql.go | 218 +++++++++++ client/manager_test.go | 239 ++++++++---- cmd/cli/handler.go | 18 +- cmd/cli/handler_client.go | 16 +- cmd/cli/handler_connection.go | 65 ---- cmd/cli/handler_policy.go | 2 +- cmd/cli/handler_warden.go | 10 +- cmd/clients_create.go | 1 + cmd/connect.go | 7 +- cmd/connections.go | 18 - cmd/connections_create.go | 22 -- cmd/connections_delete.go | 19 - cmd/host.go | 13 +- cmd/policies_resources_add.go | 2 +- cmd/root.go | 16 +- cmd/root_test.go | 18 +- cmd/server/handler.go | 37 +- cmd/server/handler_client_factory.go | 7 + cmd/server/handler_connection_factory.go | 43 --- cmd/server/handler_jwk_factory.go | 10 + cmd/server/handler_oauth2_factory.go | 43 ++- cmd/server/helper_cert.go | 2 +- cmd/server/helper_client.go | 2 +- cmd/server/helper_keys.go | 2 +- cmd/token_user.go | 23 +- {internal => compose}/firewall.go | 4 +- config/backend_connections.go | 39 +- config/config.go | 18 +- config/context.go | 4 +- connection/connection.go | 29 -- connection/connection_test.go | 23 -- connection/handler.go | 164 -------- connection/manager.go | 17 - connection/manager_http.go | 76 ---- connection/manager_memory.go | 71 ---- connection/manager_rethinkdb.go | 139 ------- connection/manager_test.go | 190 ---------- docker-compose.yml | 35 +- docs/README.md | 204 ++++++++-- docs/SUMMARY.md | 42 ++- docs/access-control.md | 83 +++- docs/access-control/introspection.md | 14 - docs/contribute.md | 7 + docs/demo.md | 88 ----- docs/faq/consistency.md | 5 - docs/faq/oauth2-error.md | 4 - docs/{basics => faq}/security.md | 0 docs/faq/when-use.md | 34 -- docs/install.md | 193 +++++++++- docs/jwk.md | 19 +- docs/oauth2.md | 201 +++++++++- docs/oauth2/basics.md | 48 --- docs/oauth2/clients/implicit-client.json | 15 - docs/oauth2/consent.md | 110 ------ docs/oauth2/openid.md | 21 -- docs/sdk/go.md | 32 +- docs/sso.md | 39 -- docs/tutorial.md | 59 +++ docs/what-good.md | 31 -- firewall/warden.go | 42 ++- glide.lock | 73 ++-- glide.yaml | 12 +- herodot/error.go | 20 +- herodot/json.go | 8 +- herodot/json_test.go | 10 +- internal/fosite_store_test.go | 241 ------------ jwk/aead_test.go | 15 +- jwk/generator_hs256.go | 2 +- jwk/handler.go | 21 +- jwk/manager_memory.go | 2 +- jwk/manager_rethinkdb.go | 2 +- jwk/manager_sql.go | 162 ++++++++ jwk/manager_test.go | 182 ++++++--- oauth2/consent_strategy.go | 6 +- {internal => oauth2}/fosite_store_memory.go | 50 ++- .../fosite_store_rethinkdb.go | 77 ++-- oauth2/fosite_store_sql.go | 259 +++++++++++++ oauth2/fosite_store_test.go | 356 ++++++++++++++++++ oauth2/handler.go | 55 +-- oauth2/handler_consent.go | 2 +- oauth2/handler_consent_test.go | 26 ++ oauth2/introspector.go | 4 +- oauth2/introspector_http.go | 27 +- oauth2/introspector_local.go | 45 --- oauth2/introspector_test.go | 94 ++--- oauth2/oauth2_auth_code_test.go | 2 +- oauth2/oauth2_test.go | 17 +- oauth2/revocator.go | 7 + oauth2/revocator_http.go | 44 +++ oauth2/revocator_test.go | 118 ++++++ oauth2/session.go | 6 +- pkg/fosite_storer.go | 10 + pkg/helper/dry.go | 2 +- pkg/retry.go | 2 +- pkg/superagent.go | 2 +- pkg/test_helpers.go | 8 +- policy/handler.go | 10 +- policy/manager_test.go | 4 +- sdk/client.go | 39 +- sdk/client_test.go | 15 + warden/handler.go | 52 +-- warden/warden_http.go | 33 +- warden/warden_local.go | 63 ++-- warden/warden_test.go | 103 +---- 115 files changed, 2965 insertions(+), 2499 deletions(-) rename Dockerfile-dangerous => Dockerfile-demo (73%) create mode 100644 PATRONS.md create mode 100644 client/client_test.go create mode 100644 client/manager_sql.go delete mode 100644 cmd/cli/handler_connection.go delete mode 100644 cmd/connections.go delete mode 100644 cmd/connections_create.go delete mode 100644 cmd/connections_delete.go delete mode 100644 cmd/server/handler_connection_factory.go rename {internal => compose}/firewall.go (94%) delete mode 100644 connection/connection.go delete mode 100644 connection/connection_test.go delete mode 100644 connection/handler.go delete mode 100644 connection/manager.go delete mode 100644 connection/manager_http.go delete mode 100644 connection/manager_memory.go delete mode 100644 connection/manager_rethinkdb.go delete mode 100644 connection/manager_test.go delete mode 100644 docs/access-control/introspection.md create mode 100644 docs/contribute.md delete mode 100644 docs/demo.md delete mode 100644 docs/faq/consistency.md delete mode 100644 docs/faq/oauth2-error.md rename docs/{basics => faq}/security.md (100%) delete mode 100644 docs/faq/when-use.md delete mode 100644 docs/oauth2/basics.md delete mode 100644 docs/oauth2/clients/implicit-client.json delete mode 100644 docs/oauth2/consent.md delete mode 100644 docs/oauth2/openid.md delete mode 100644 docs/sso.md create mode 100644 docs/tutorial.md delete mode 100644 docs/what-good.md delete mode 100644 internal/fosite_store_test.go create mode 100644 jwk/manager_sql.go rename {internal => oauth2}/fosite_store_memory.go (78%) rename {internal => oauth2}/fosite_store_rethinkdb.go (83%) create mode 100644 oauth2/fosite_store_sql.go create mode 100644 oauth2/fosite_store_test.go create mode 100644 oauth2/handler_consent_test.go delete mode 100644 oauth2/introspector_local.go create mode 100644 oauth2/revocator.go create mode 100644 oauth2/revocator_http.go create mode 100644 oauth2/revocator_test.go create mode 100644 sdk/client_test.go diff --git a/.gitignore b/.gitignore index 23707a1cf4e..08e747c17c8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ vendor/ cover.out output/ _book/ -dist/ \ No newline at end of file +dist/ +coverage.* \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 92f811e0f58..7481a461976 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,21 +9,22 @@ env: language: go go: - - 1.5 - - 1.6 - 1.7 +go_import_path: github.com/ory-am/hydra + install: - - go get github.com/mattn/goveralls golang.org/x/tools/cmd/cover github.com/pierrre/gotestcover github.com/Masterminds/glide github.com/mitchellh/gox github.com/tcnksm/ghr + - go get github.com/mattn/goveralls golang.org/x/tools/cmd/cover github.com/Masterminds/glide github.com/mitchellh/gox - git clone https://github.com/docker-library/official-images.git ~/official-images - glide install - go install github.com/ory-am/hydra script: - - gotestcover -coverprofile="cover.out" $(glide novendor) + - |- + touch ./coverage.tmp && echo 'mode: atomic' > coverage.txt && go list ./... | grep -v /vendor | xargs -n1 -I{} sh -c 'go test -covermode=atomic -coverprofile=coverage.tmp -coverpkg $(go list ./... | grep -v /vendor | tr "\n" ",") {} && tail -n +2 coverage.tmp >> coverage.txt' && rm coverage.tmp + - goveralls -coverprofile="coverage.txt" - go test -race $(go list ./... | grep -v /vendor | grep -v /cmd) - go test -v -bench=.* -run=none $(glide novendor) - - goveralls -coverprofile="cover.out" - docker build -t hydra-travis-ci . - docker run -d hydra-travis-ci - $GOPATH/bin/hydra host --dangerous-auto-logon & @@ -31,7 +32,8 @@ script: - $GOPATH/bin/hydra token client --skip-tls-verify after_success: - - if [ "${TRAVIS_TAG}" != "" ] && [ "${TRAVIS_GO_VERSION}" == "1.7" ]; then gox -ldflags "-X github.com/ory-am/hydra/cmd.Version=`git describe --tags` -X github.com/ory-am/hydra/cmd.BuildTime=`TZ=UTC date -u '+%Y-%m-%dT%H:%M:%SZ'` -X github.com/ory-am/hydra/cmd.GitHash=`git rev-parse HEAD`" -output "dist/{{.Dir}}-{{.OS}}-{{.Arch}}"; fi + - |- + [ "${TRAVIS_TAG}" != "" ] && [ "${TRAVIS_GO_VERSION}" == "1.7" ] && gox -ldflags "-X github.com/ory-am/hydra/cmd.Version=`git describe --tags` -X github.com/ory-am/hydra/cmd.BuildTime=`TZ=UTC date -u '+%Y-%m-%dT%H:%M:%SZ'` -X github.com/ory-am/hydra/cmd.GitHash=`git rev-parse HEAD`" -output "dist/{{.Dir}}-{{.OS}}-{{.Arch}}" deploy: provider: releases diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3243c251adc..0d1211b7a2f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,22 @@ # Contribution Guide + + +**Table of Contents** + +- [Introduction](#introduction) +- [Contributing Code](#contributing-code) +- [Disclosing vulnerabilities](#disclosing-vulnerabilities) +- [Code Style](#code-style) +- [Developer’s Certificate of Origin](#developer%E2%80%99s-certificate-of-origin) +- [Pull request procedure](#pull-request-procedure) +- [Communication](#communication) +- [Conduct](#conduct) + + + +## Introduction + We welcome and encourage community contributions to Hydra. Since the project is still unstable, there are specific priorities for development. Pull requests that do not address these priorities will not be accepted until Hydra is production ready. @@ -21,6 +38,10 @@ At least one review from a maintainer is required for all patches (even patches Reviewers should leave a "LGTM" comment once they are satisfied with the patch. If the patch was submitted by a maintainer with write access, the pull request should be merged by the submitter after review. +## Disclosing vulnerabilities + +Please disclose vulnerabilities exclusively to [hi@ory.am](mailto:hi@ory.am). Do not use GitHub issues. + ## Code Style Please follow these guidelines when formatting source code: diff --git a/Dockerfile-dangerous b/Dockerfile-demo similarity index 73% rename from Dockerfile-dangerous rename to Dockerfile-demo index 68c789cc6fe..b442b4d7e4f 100644 --- a/Dockerfile-dangerous +++ b/Dockerfile-demo @@ -7,6 +7,6 @@ RUN go get github.com/Masterminds/glide RUN glide install RUN go install github.com/ory-am/hydra -ENTRYPOINT /go/bin/hydra host --dangerous-auto-logon +ENTRYPOINT /go/bin/hydra host --dangerous-auto-logon --dangerous-force-http EXPOSE 4444 \ No newline at end of file diff --git a/PATRONS.md b/PATRONS.md new file mode 100644 index 00000000000..2b338e89127 --- /dev/null +++ b/PATRONS.md @@ -0,0 +1,13 @@ +# Patreon + +We are proud to be part of the Open Knowledge and Open Source movement. +We have been using Open Source Software throughout our career and want to contribute back. +We believe that developers and operators should not have to fiddle with hard to understand configuration files, +runtime and installation. Our vision is to enhance the developer and operator ecosystem with easy to use +and secure Open Source Software, and we need your help to achieve that! + +Support ORY's Open Source Software on [patreon](https://patreon.com/user?u=4298803)! + +## Patrons + +[Be the first!](https://patreon.com/user?u=4298803) \ No newline at end of file diff --git a/README.md b/README.md index ddad837511f..df8971c9559 100644 --- a/README.md +++ b/README.md @@ -3,35 +3,36 @@ [![Join the chat at https://gitter.im/ory-am/hydra](https://img.shields.io/badge/join-chat-00cc99.svg)](https://gitter.im/ory-am/hydra?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join mailinglist](https://img.shields.io/badge/join-mailinglist-00cc99.svg)](https://groups.google.com/forum/#!forum/ory-hydra/new) [![Join newsletter](https://img.shields.io/badge/join-newsletter-00cc99.svg)](http://eepurl.com/bKT3N9) -[![Follow newsletter](https://img.shields.io/badge/follow-twitter-00cc99.svg)](https://twitter.com/_aeneasr) +[![Follow twitter](https://img.shields.io/badge/follow-twitter-00cc99.svg)](https://twitter.com/_aeneasr) [![Follow GitHub](https://img.shields.io/badge/follow-github-00cc99.svg)](https://github.com/arekkas) [![Build Status](https://travis-ci.org/ory-am/hydra.svg?branch=master)](https://travis-ci.org/ory-am/hydra) [![Coverage Status](https://coveralls.io/repos/ory-am/hydra/badge.svg?branch=master&service=github)](https://coveralls.io/github/ory-am/hydra?branch=master) [![Code Climate](https://codeclimate.com/github/ory-am/hydra/badges/gpa.svg)](https://codeclimate.com/github/ory-am/hydra) [![Go Report Card](https://goreportcard.com/badge/github.com/ory-am/hydra)](https://goreportcard.com/report/github.com/ory-am/hydra) - +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/364/badge)](https://bestpractices.coreinfrastructure.org/projects/364) [![Docs Guide](https://img.shields.io/badge/docs-guide-blue.svg)](https://ory-am.gitbooks.io/hydra/content/) [![HTTP API Documentation](https://img.shields.io/badge/docs-http%20api-blue.svg)](http://docs.hdyra.apiary.io/) [![Code Documentation](https://img.shields.io/badge/docs-godoc-blue.svg)](https://godoc.org/github.com/ory-am/hydra) -Hydra is being developed by german-based company [Ory](https://ory.am). +[![Code Documentation](https://img.shields.io/badge/support-patreon-green.svg)](https://patreon.com/user?u=4298803) + +Hydra is a runnable server implementation of the OAuth2 2.0 authorization framework and the OpenID Connect Core 1.0. + +Hydra is being developed by german-based company [ORY](https://ory.am). Join our [newsletter](http://eepurl.com/bKT3N9) to stay on top of new developments. -We offer basic support requests on [Google Groups](https://groups.google.com/forum/#!forum/ory-hydra/new) and [Gitter](https://gitter.im/ory-am/hydra) -as well as [consulting](mailto:hi@ory.am) around integrating Hydra into -your particular environment and [premium support](mailto:hi@ory.am). +We answer basic support requests on [Google Groups](https://groups.google.com/forum/#!forum/ory-hydra/new) and [Gitter](https://gitter.im/ory-am/hydra) +and offer [premium services](http://www.ory.am/products/hydra) around Hydra. -Hydra uses the security first OAuth2 and OpenID Connect SDK [Fosite](https://github.com/ory-am/fosite) and [Ladon](https://github.com/ory-am/ladon) for policy-based access control. +Hydra uses the security first OAuth2 and OpenID Connect SDK [Fosite](https://github.com/ory-am/fosite) and +the access control SDK [Ladon](https://github.com/ory-am/ladon). **Table of Contents** - [What is Hydra?](#what-is-hydra) - - [What is Hydra / OAuth2 not?](#what-is-hydra--oauth2-not) - - [When does Hydra / OAuth2 make sense?](#when-does-hydra--oauth2-make-sense) -- [Feature Overview](#feature-overview) - [Quickstart](#quickstart) - [Installation](#installation) - [Download binaries](#download-binaries) @@ -46,82 +47,23 @@ Hydra uses the security first OAuth2 and OpenID Connect SDK [Fosite](https://git - [Command Line Documentation](#command-line-documentation) - [Develop](#develop) - [Third-party libraries and projects](#third-party-libraries-and-projects) -- [Hall of Fame](#hall-of-fame) ## What is Hydra? -At first, there was the monolith. The monolith worked well with the bespoke authentication module. -Then, the web evolved into an elastic cloud that serves thousands of different user agents -in every part of the world. - -Hydra is driven by the need for a **scalable, low-latency, in memory -Access Control, OAuth2, and OpenID Connect layer** that integrates with every identity provider you can imagine. - -* Hydra is built security first: architecture and work flows are designed to neutralize various common (OWASP TOP TEN) and uncommon attack vectors. [Learn more](https://ory-am.gitbooks.io/hydra/content/basics/security.html). -* Hydra can manage all of your access control needs, such as policy based access control and access token validation. [Learn more](https://ory-am.gitbooks.io/hydra/content/access-control.html). -* Hydra depends on an identity provider of your choosing, e.g. [authboss](https://github.com/go-authboss/authboss), and works with any identity provider that is able to read and issue JSON Web Tokens. [Learn more](https://ory-am.gitbooks.io/hydra/content/oauth2/consent.html). -* Hydra has nano-second latency on high profile endpoints, overwhelmingly efficient memory and CPU consumption and scales effortlessly. [Learn more](https://ory-am.gitbooks.io/hydra/content/basics/architecture.html). -* Hydra focuses on ease of use, integration, management and operation. [Get Hydra up and running in 5 Minutes](https://ory-am.gitbooks.io/hydra/content/demo.html). -* Hydra helps you manage [Social Login Connections](https://ory-am.gitbooks.io/hydra/content/sso.html) as well as [JSON Web Keys](https://ory-am.gitbooks.io/hydra/content/jwk.html) and is planned to help you manage User Groups and Two Factor Authentication as well. -* Hydra is available through [Docker](https://hub.docker.com/r/oryam/hydra/) and relies on RethinkDB for persistence. -Database drivers are extensible in case you want to use RabbitMQ, MySQL, MongoDB, or some other database instead. - -Hydra is built for high volume environments and is capable of serving tens of thousands of simultaneous requests per second per instance. Read [this issue](https://github.com/ory-am/hydra/issues/161) for information on reproducing these benchmarks yourself. - -### What is Hydra / OAuth2 not? - -I am new to all of this. When should I reconsider if using OAuth2 / Hydra is the right choice for me? - -1. Hydra is not something that manages user accounts. Hydra does not offer user registration, password reset, user -login, sending confirmation emails. This is what the *Identity Provider* ("login endpoint") is responsible for. -The communication between Hydra and the Identity Provider is called [*Consent Flow*](https://ory-am.gitbooks.io/hydra/content/oauth2/consent.html). -[Auth0.com](https://auth0.com) is an Identity Provider. We might implement this feature at some point and if, it is going to be a different product. -2. If you think running an OAuth2 Provider can solve your user authentication ("log a user in"), Hydra is probably not for you. OAuth2 is a delegation protocol: - - > The OAuth 2.0 authorization framework enables a third-party application *[think: a dropbox app that manages your dropbox photos]* - to obtain limited access to an HTTP service, either on behalf of *[do you allow "amazing photo app" to access all your photos?]* - a resource owner *[user]* by orchestrating an approval interaction *[consent flow]* between the resource owner and the - HTTP service, or by allowing the third-party application *[OAuth2 Client App]* to obtain access on its own behalf. - - **[IETF](https://tools.ietf.org/html/rfc6749)** -3. If you are building a simple service for 50-100 registered users, OAuth2 and Hydra will be overkill. -4. Hydra does not support the OAuth2 resource owner password credentials flow. -5. Hydra has no user interface. You must manage OAuth2 Clients and other things using the RESTful endpoints. -A user interface is scheduled to accompany the stable release. - -### When does Hydra / OAuth2 make sense? - -1. If you want third-party developers to access your APIs, Hydra is the perfect fit. This is what an OAuth2 Provider does. -2. If you want to become a Identity Provider, like Google, Facebook or Microsoft, OpenID Connect and thus Hydra is a perfect fit. -3. Running an OAuth2 Provider works great with browser, mobile and wearable apps, as you can avoid storing user -credentials on the device, phone or wearable and revoke access tokens, and thus access privileges, at any time. Adding -OAuth2 complexity to your environment when you never plan to do (1), -might not be worth it. Our advice: write a pros/cons list. -4. If you have a lot of services and want to limit automated access (think: cronjobs) for those services, -OAuth2 might make sense for you. Example: The comment service is not allowed to read user passwords when fetching -the latest user profile updates. - -## Feature Overview - -1. **Availability:** Hydra uses pub/sub to have the latest data available in memory. The in-memory architecture allows for heavy duty workloads. -2. **Scalability:** Hydra scales effortlessly on every platform you can imagine, including Heroku, Cloud Foundry, Docker, -Google Container Engine and many more. -3. **Integration:** Hydra wraps your existing stack like a blanket and keeps it safe. Hydra uses cryptographic tokens to authenticate users and request their consent, no APIs required. -The deprecated php-3.0 authentication service your intern wrote? It works with that too, don't worry. -We wrote an example with React to show you what this could look like: [React.js Identity Provider Example App](https://github.com/ory-am/hydra-idp-react). -4. **Security:** Hydra leverages the security first OAuth2 framework **[Fosite](https://github.com/ory-am/fosite)**, -encrypts important data at rest, and supports HTTP over TLS (https) out of the box. -5. **Ease of use:** Developers and operators are human. Therefore, Hydra is easy to install and manage. Hydra does not care if you use React, Angular, or Cocoa for your user interface. -To support you even further, there are APIs available for *cryptographic key management, social log on, policy based access control, policy management, and two factor authentication (tbd).* -Hydra is packaged using [Docker](https://hub.docker.com/r/oryam/hydra/). -6. **Open Source:** Hydra is licensed under Apache Version 2.0 -7. **Professional:** Hydra implements peer reviewed open standards published by [The Internet Engineering Task Force (IETF®)](https://www.ietf.org/) and the [OpenID Foundation](https://openid.net/) -and under supervision of the [LMU Teaching and Research Unit Programming and Modelling Languages](http://www.en.pms.ifi.lmu.de). No funny business. -8. **Real Time:** Operation is a lot easier with real time. There are no caches, - no invalidation strategies and no magic - just simple, cloud native pub-sub. Hydra leverages RethinkDB, so check out their real time database monitoring too! +Hydra is a server implementation of the OAuth 2.0 authorization framework and the OpenID Connect Core 1.0. Existing OAuth2 +implementations usually ship as libraries or SDKs such as [node-oauth2-server](https://github.com/oauthjs/node-oauth2-server) +or [fosite](https://github.com/ory-am/fosite/issues), or as fully featured identity solutions with user +management and user interfaces, such as [Dex](https://github.com/coreos/dex). -
+Implementing and using OAuth2 without understanding the whole specification is challenging and prone to errors, even when +SDKs are being used. The primary goal of Hydra is to make OAuth 2.0 and OpenID Connect 1.0 better accessible. + +Hydra implements the flows described in OAuth2 and OpenID Connect 1.0 without forcing you to use a "Hydra User Management" +or some template engine or a predefined front-end. Instead it relies on HTTP redirection and cryptographic methods +to verify user consent allowing you to use Hydra with any authentication endpoint, be it [authboss](https://github.com/go-authboss/authboss), +[auth0.com](https://auth0.com/) or your proprietary PHP authentication. ## Quickstart @@ -185,7 +127,7 @@ hydra ### 5 minutes tutorial: Run your very own OAuth2 environment The **[tutorial](https://ory-am.gitbooks.io/hydra/content/demo.html)** teaches you to set up Hydra, -a RethinkDB instance and an exemplary identity provider written in React using docker compose. +a Posgres instance and an exemplary identity provider written in React using docker compose. It will take you about 5 minutes to get complete the **[tutorial](https://ory-am.gitbooks.io/hydra/content/demo.html)**. OAuth2 Flow @@ -200,7 +142,7 @@ OAuth2 and OAuth2 related specifications are over 200 written pages. Implementin Even if you use a secure SDK (there are numerous SDKs not secure by design in the wild), messing up the implementation is a real threat - no matter how good you or your team is. To err is human. -An in-depth list of security features is listed [in the security guide](https://ory-am.gitbooks.io/hydra/content/basics/security.html). +An in-depth list of security features is listed [in the security guide](https://ory-am.gitbooks.io/hydra/content/faq/security.html). ## Reception @@ -261,10 +203,3 @@ DATABASE_URL=rethinkdb://localhost:28015/hydra go run main.go host ## Third-party libraries and projects * [Hydra middleware for Gin](https://github.com/janekolszak/gin-hydra) - -## Hall of Fame - -A list of extraordinary contributors and [bug hunters](https://github.com/ory-am/hydra/issues/84). - -* [Alexander Widerberg (leetal)](https://github.com/leetal) for implementing the prototype RethinkDB adapters. -* The active Community on Gitter. diff --git a/client/client.go b/client/client.go index 23ce612bb1f..5df25c81c76 100644 --- a/client/client.go +++ b/client/client.go @@ -20,6 +20,7 @@ type Client struct { ClientURI string `json:"client_uri" gorethink:"client_uri"` LogoURI string `json:"logo_uri" gorethink:"logo_uri"` Contacts []string `json:"contacts" gorethink:"contacts"` + Public bool `json:"public" gorethink:"public"` } func (c *Client) GetID() string { @@ -65,3 +66,7 @@ func (c *Client) GetResponseTypes() fosite.Arguments { func (c *Client) GetOwner() string { return c.Owner } + +func (c *Client) IsPublic() bool { + return c.Public +} diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 00000000000..c964a6e4cc7 --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,24 @@ +package client + +import ( + "github.com/ory-am/fosite" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestClient(t *testing.T) { + c := &Client{ + ID: "foo", + RedirectURIs: []string{"foo"}, + Scope: "foo bar", + } + + assert.EqualValues(t, c.RedirectURIs, c.GetRedirectURIs()) + assert.EqualValues(t, []byte(c.Secret), c.GetHashedSecret()) + assert.EqualValues(t, fosite.Arguments{"authorization_code"}, c.GetGrantTypes()) + assert.EqualValues(t, fosite.Arguments{"code"}, c.GetResponseTypes()) + assert.EqualValues(t, (c.Owner), c.GetOwner()) + assert.EqualValues(t, (c.Public), c.IsPublic()) + assert.Len(t, c.GetScopes(), 2) + assert.EqualValues(t, c.RedirectURIs, c.GetRedirectURIs()) +} diff --git a/client/handler.go b/client/handler.go index 076ddb61f57..ff492c253cb 100644 --- a/client/handler.go +++ b/client/handler.go @@ -5,12 +5,12 @@ import ( "fmt" "net/http" - "github.com/pkg/errors" "github.com/julienschmidt/httprouter" "github.com/ory-am/common/rand/sequence" "github.com/ory-am/hydra/firewall" "github.com/ory-am/hydra/herodot" "github.com/ory-am/ladon" + "github.com/pkg/errors" ) type Handler struct { @@ -46,10 +46,10 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request, _ httprouter.Pa return } - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ + if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: ClientsResource, Action: "create", - Context: ladon.Context{ + Context: map[string]interface{}{ "owner": c.Owner, }, }, Scope); err != nil { @@ -93,7 +93,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request, ps httprouter.P return } - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ + if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: ClientsResource, Action: "update", Context: ladon.Context{ @@ -120,7 +120,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request, ps httprouter.P func (h *Handler) GetAll(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { var ctx = herodot.NewContext() - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ + if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: ClientsResource, Action: "get", }, Scope); err != nil { @@ -152,7 +152,7 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request, ps httprouter.Para return } - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ + if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: fmt.Sprintf(ClientResource, id), Action: "get", Context: ladon.Context{ @@ -171,7 +171,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request, ps httprouter.P var ctx = herodot.NewContext() var id = ps.ByName("id") - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ + if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: fmt.Sprintf(ClientResource, id), Action: "delete", }, Scope); err != nil { diff --git a/client/manager_memory.go b/client/manager_memory.go index 83f3cc87d33..28b5ab49f61 100644 --- a/client/manager_memory.go +++ b/client/manager_memory.go @@ -3,17 +3,16 @@ package client import ( "sync" - "github.com/pkg/errors" + "github.com/imdario/mergo" "github.com/ory-am/fosite" - "github.com/ory-am/fosite/hash" "github.com/ory-am/hydra/pkg" "github.com/pborman/uuid" - "github.com/imdario/mergo" + "github.com/pkg/errors" ) type MemoryManager struct { Clients map[string]Client - Hasher hash.Hasher + Hasher fosite.Hasher sync.RWMutex } diff --git a/client/manager_rethinkdb.go b/client/manager_rethinkdb.go index ae296638a21..9b0fd3b3692 100644 --- a/client/manager_rethinkdb.go +++ b/client/manager_rethinkdb.go @@ -5,14 +5,13 @@ import ( "time" "github.com/Sirupsen/logrus" - "github.com/pkg/errors" + "github.com/imdario/mergo" "github.com/ory-am/fosite" - "github.com/ory-am/fosite/hash" "github.com/ory-am/hydra/pkg" "github.com/pborman/uuid" + "github.com/pkg/errors" "golang.org/x/net/context" r "gopkg.in/dancannon/gorethink.v2" - "github.com/imdario/mergo" ) type RethinkManager struct { @@ -21,7 +20,7 @@ type RethinkManager struct { sync.RWMutex Clients map[string]Client - Hasher hash.Hasher + Hasher fosite.Hasher } func (m *RethinkManager) GetConcreteClient(id string) (*Client, error) { diff --git a/client/manager_sql.go b/client/manager_sql.go new file mode 100644 index 00000000000..858e6c466fe --- /dev/null +++ b/client/manager_sql.go @@ -0,0 +1,218 @@ +package client + +import ( + "database/sql" + "fmt" + "github.com/imdario/mergo" + "github.com/jmoiron/sqlx" + "github.com/ory-am/fosite" + "github.com/ory-am/hydra/pkg" + "github.com/pborman/uuid" + "github.com/pkg/errors" + "strings" +) + +var sqlSchema = []string{ + `CREATE TABLE IF NOT EXISTS hydra_client ( + id varchar(255) NOT NULL PRIMARY KEY, + client_name text NOT NULL, + client_secret text NOT NULL, + redirect_uris text NOT NULL, + grant_types text NOT NULL, + response_types text NOT NULL, + scope text NOT NULL, + owner text NOT NULL, + policy_uri text NOT NULL, + tos_uri text NOT NULL, + client_uri text NOT NULL, + logo_uri text NOT NULL, + contacts text NOT NULL, + public boolean NOT NULL +)`, +} + +type SQLManager struct { + Hasher fosite.Hasher + DB *sqlx.DB +} + +type sqlData struct { + ID string `db:"id"` + Name string `db:"client_name"` + Secret string `db:"client_secret"` + RedirectURIs string `db:"redirect_uris"` + GrantTypes string `db:"grant_types"` + ResponseTypes string `db:"response_types"` + Scope string `db:"scope"` + Owner string `db:"owner"` + PolicyURI string `db:"policy_uri"` + TermsOfServiceURI string `db:"tos_uri"` + ClientURI string `db:"client_uri"` + LogoURI string `db:"logo_uri"` + Contacts string `db:"contacts"` + Public bool `db:"public"` +} + +var sqlParams = []string{ + "id", + "client_name", + "client_secret", + "redirect_uris", + "grant_types", + "response_types", + "scope", + "owner", + "policy_uri", + "tos_uri", + "client_uri", + "logo_uri", + "contacts", + "public", +} + +func sqlDataFromClient(d *Client) *sqlData { + return &sqlData{ + ID: d.ID, + Secret: d.Secret, + RedirectURIs: strings.Join(d.RedirectURIs, "|"), + GrantTypes: strings.Join(d.GrantTypes, "|"), + ResponseTypes: strings.Join(d.ResponseTypes, "|"), + Scope: d.Scope, + Owner: d.Owner, + PolicyURI: d.PolicyURI, + TermsOfServiceURI: d.TermsOfServiceURI, + ClientURI: d.ClientURI, + LogoURI: d.LogoURI, + Contacts: strings.Join(d.Contacts, "|"), + Public: d.Public, + } +} + +func (d *sqlData) ToClient() *Client { + return &Client{ + ID: d.ID, + Secret: d.Secret, + RedirectURIs: strings.Split(d.RedirectURIs, "|"), + GrantTypes: strings.Split(d.GrantTypes, "|"), + ResponseTypes: strings.Split(d.ResponseTypes, "|"), + Scope: d.Scope, + Owner: d.Owner, + PolicyURI: d.PolicyURI, + TermsOfServiceURI: d.TermsOfServiceURI, + ClientURI: d.ClientURI, + LogoURI: d.LogoURI, + Contacts: strings.Split(d.Contacts, "|"), + Public: d.Public, + } +} + +func (s *SQLManager) CreateSchemas() error { + for _, query := range sqlSchema { + if _, err := s.DB.Exec(query); err != nil { + return errors.Wrapf(err, "Could not create schema:\n%s", query) + } + } + return nil +} + +func (m *SQLManager) GetConcreteClient(id string) (*Client, error) { + var d sqlData + if err := m.DB.Get(&d, m.DB.Rebind("SELECT * FROM hydra_client WHERE id=?"), id); err == sql.ErrNoRows { + return nil, errors.Wrap(pkg.ErrNotFound, "") + } else if err != nil { + return nil, errors.Wrap(err, "") + } + + return d.ToClient(), nil +} + +func (m *SQLManager) GetClient(id string) (fosite.Client, error) { + return m.GetConcreteClient(id) +} + +func (m *SQLManager) UpdateClient(c *Client) error { + o, err := m.GetClient(c.ID) + if err != nil { + return err + } + + if c.Secret == "" { + c.Secret = string(o.GetHashedSecret()) + } else { + h, err := m.Hasher.Hash([]byte(c.Secret)) + if err != nil { + return errors.Wrap(err, "") + } + c.Secret = string(h) + } + if err := mergo.Merge(c, o); err != nil { + return errors.Wrap(err, "") + } + + s := sqlDataFromClient(c) + var query []string + for _, param := range sqlParams { + query = append(query, fmt.Sprintf("%s=:%s", param, param)) + } + + if _, err := m.DB.NamedExec(fmt.Sprintf(`UPDATE hydra_client SET %s WHERE id=:id`, strings.Join(query, ", ")), s); err != nil { + return errors.Wrap(err, "") + } + return nil +} + +func (m *SQLManager) Authenticate(id string, secret []byte) (*Client, error) { + c, err := m.GetConcreteClient(id) + if err != nil { + return nil, errors.Wrap(err, "") + } + + if err := m.Hasher.Compare(c.GetHashedSecret(), secret); err != nil { + return nil, errors.Wrap(err, "") + } + + return c, nil +} + +func (m *SQLManager) CreateClient(c *Client) error { + if c.ID == "" { + c.ID = uuid.New() + } + + h, err := m.Hasher.Hash([]byte(c.Secret)) + if err != nil { + return errors.Wrap(err, "") + } + c.Secret = string(h) + + data := sqlDataFromClient(c) + if _, err := m.DB.NamedExec(fmt.Sprintf( + "INSERT INTO hydra_client (%s) VALUES (%s)", + strings.Join(sqlParams, ", "), + ":"+strings.Join(sqlParams, ", :"), + ), data); err != nil { + return errors.Wrap(err, "") + } + return nil +} + +func (m *SQLManager) DeleteClient(id string) error { + if _, err := m.DB.Exec(m.DB.Rebind(`DELETE FROM hydra_client WHERE id=?`), id); err != nil { + return errors.Wrap(err, "") + } + return nil +} + +func (m *SQLManager) GetClients() (clients map[string]Client, err error) { + var d = []sqlData{} + clients = make(map[string]Client) + + if err := m.DB.Select(&d, "SELECT * FROM hydra_client"); err != nil { + return nil, errors.Wrap(err, "") + } + + for _, k := range d { + clients[k.ID] = *k.ToClient() + } + return clients, nil +} diff --git a/client/manager_test.go b/client/manager_test.go index 1bf38aa5502..d8ee08bf5b5 100644 --- a/client/manager_test.go +++ b/client/manager_test.go @@ -11,17 +11,19 @@ import ( "os" "time" + "fmt" + "github.com/jmoiron/sqlx" "github.com/julienschmidt/httprouter" "github.com/ory-am/dockertest" "github.com/ory-am/fosite" - "github.com/ory-am/fosite/hash" . "github.com/ory-am/hydra/client" + "github.com/ory-am/hydra/compose" "github.com/ory-am/hydra/herodot" - "github.com/ory-am/hydra/internal" "github.com/ory-am/hydra/pkg" "github.com/ory-am/ladon" "github.com/pborman/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/net/context" ) @@ -32,10 +34,10 @@ var ts *httptest.Server func init() { clientManagers["memory"] = &MemoryManager{ Clients: map[string]Client{}, - Hasher: &hash.BCrypt{}, + Hasher: &fosite.BCrypt{}, } - localWarden, httpClient := internal.NewFirewall("foo", "alice", fosite.Arguments{Scope}, &ladon.DefaultPolicy{ + localWarden, httpClient := compose.NewFirewall("foo", "alice", fosite.Arguments{Scope}, &ladon.DefaultPolicy{ ID: "1", Subjects: []string{"alice"}, Resources: []string{"rn:hydra:clients<.*>"}, @@ -46,7 +48,7 @@ func init() { s := &Handler{ Manager: &MemoryManager{ Clients: map[string]Client{}, - Hasher: &hash.BCrypt{}, + Hasher: &fosite.BCrypt{}, }, H: &herodot.JSON{}, W: localWarden, @@ -64,8 +66,77 @@ func init() { } var rethinkManager *RethinkManager +var containers = []dockertest.ContainerID{} func TestMain(m *testing.M) { + defer func() { + for _, c := range containers { + c.KillRemove() + } + }() + + connectToPG() + connectToRethinkDB() + connectToMySQL() + + os.Exit(m.Run()) +} + +func connectToMySQL() { + var db *sqlx.DB + c, err := dockertest.ConnectToMySQL(15, time.Second, func(url string) bool { + var err error + db, err = sqlx.Open("mysql", url) + if err != nil { + log.Printf("Got error in mysql connector: %s", err) + return false + } + return db.Ping() == nil + }) + + if err != nil { + log.Fatalf("Could not connect to database: %s", err) + } + + containers = append(containers, c) + s := &SQLManager{DB: db, Hasher: &fosite.BCrypt{WorkFactor: 4}} + + if err = s.CreateSchemas(); err != nil { + log.Fatalf("Could not create postgres schema: %v", err) + } + + clientManagers["mysql"] = s + containers = append(containers, c) +} + +func connectToPG() { + var db *sqlx.DB + c, err := dockertest.ConnectToPostgreSQL(15, time.Second, func(url string) bool { + var err error + db, err = sqlx.Open("postgres", url) + if err != nil { + log.Printf("Got error in postgres connector: %s", err) + return false + } + return db.Ping() == nil + }) + + if err != nil { + log.Fatalf("Could not connect to database: %s", err) + } + + containers = append(containers, c) + s := &SQLManager{DB: db, Hasher: &fosite.BCrypt{WorkFactor: 4}} + + if err = s.CreateSchemas(); err != nil { + log.Fatalf("Could not create postgres schema: %v", err) + } + + clientManagers["postgres"] = s + containers = append(containers, c) +} + +func connectToRethinkDB() { var session *r.Session var err error @@ -84,7 +155,7 @@ func TestMain(m *testing.M) { Session: session, Table: r.Table("hydra_clients"), Clients: make(map[string]Client), - Hasher: &hash.BCrypt{ + Hasher: &fosite.BCrypt{ // Low workfactor reduces test time WorkFactor: 4, }, @@ -93,23 +164,33 @@ func TestMain(m *testing.M) { time.Sleep(100 * time.Millisecond) return true }) - if session != nil { - defer session.Close() - } + if err != nil { log.Fatalf("Could not connect to database: %s", err) } - clientManagers["rethink"] = rethinkManager - retCode := m.Run() - c.KillRemove() - os.Exit(retCode) + containers = append(containers, c) + clientManagers["rethink"] = rethinkManager +} +func TestClientAutoGenerateKey(t *testing.T) { + for k, m := range clientManagers { + t.Run(fmt.Sprintf("case=%s", k), func(t *testing.T) { + c := &Client{ + Secret: "secret", + RedirectURIs: []string{"http://redirect"}, + TermsOfServiceURI: "foo", + } + assert.Nil(t, m.CreateClient(c)) + assert.NotEmpty(t, c.ID) + assert.Nil(t, m.DeleteClient(c.ID)) + }) + } } func TestAuthenticateClient(t *testing.T) { var mem = &MemoryManager{ Clients: map[string]Client{}, - Hasher: &hash.BCrypt{}, + Hasher: &fosite.BCrypt{}, } mem.CreateClient(&Client{ ID: "1234", @@ -181,12 +262,12 @@ func TestColdStartRethinkManager(t *testing.T) { time.Sleep(time.Second / 2) rethinkManager.Clients = make(map[string]Client) - assert.Nil(t, rethinkManager.ColdStart()) + require.Nil(t, rethinkManager.ColdStart()) c1, err := rethinkManager.GetClient("foo") - assert.Nil(t, err) + require.Nil(t, err) c2, err := rethinkManager.GetClient("bar") - assert.Nil(t, err) + require.Nil(t, err) assert.NotEqual(t, c1, c2) assert.Equal(t, "foo", c1.GetID()) @@ -196,67 +277,71 @@ func TestColdStartRethinkManager(t *testing.T) { func TestCreateGetDeleteClient(t *testing.T) { for k, m := range clientManagers { - _, err := m.GetClient("4321") - pkg.AssertError(t, true, err, "%s", k) - - c := &Client{ - ID: "1234", - Secret: "secret", - RedirectURIs: []string{"http://redirect"}, - TermsOfServiceURI: "foo", - } - err = m.CreateClient(c) - pkg.AssertError(t, false, err, "%s", k) - if err == nil { - compare(t, c, k) - } - - err = m.CreateClient(&Client{ - ID: "2-1234", - Secret: "secret", - RedirectURIs: []string{"http://redirect"}, - TermsOfServiceURI: "foo", + t.Run(fmt.Sprintf("case=%s", k), func(t *testing.T) { + _, err := m.GetClient("4321") + assert.NotNil(t, err) + + c := &Client{ + ID: "1234", + Secret: "secret", + RedirectURIs: []string{"http://redirect"}, + TermsOfServiceURI: "foo", + } + err = m.CreateClient(c) + assert.Nil(t, err) + if err == nil { + compare(t, c, k) + } + + err = m.CreateClient(&Client{ + ID: "2-1234", + Secret: "secret", + RedirectURIs: []string{"http://redirect"}, + TermsOfServiceURI: "foo", + }) + assert.Nil(t, err) + + // RethinkDB delay + time.Sleep(100 * time.Millisecond) + + d, err := m.GetClient("1234") + assert.Nil(t, err) + if err == nil { + compare(t, d, k) + } + + ds, err := m.GetClients() + assert.Nil(t, err) + assert.Len(t, ds, 2) + assert.NotEqual(t, ds["1234"].ID, ds["2-1234"].ID) + + err = m.UpdateClient(&Client{ + ID: "2-1234", + Secret: "secret-new", + TermsOfServiceURI: "bar", + }) + assert.Nil(t, err) + time.Sleep(100 * time.Millisecond) + + nc, err := m.GetConcreteClient("2-1234") + assert.Nil(t, err) + + if k != "http" { + // http always returns an empty secret + assert.NotEqual(t, d.GetHashedSecret(), nc.GetHashedSecret(), "%s", k) + } + assert.Equal(t, "bar", nc.TermsOfServiceURI, "%s", k) + assert.EqualValues(t, []string{"http://redirect"}, nc.GetRedirectURIs(), "%s", k) + + err = m.DeleteClient("1234") + assert.Nil(t, err) + + // RethinkDB delay + time.Sleep(100 * time.Millisecond) + + _, err = m.GetClient("1234") + assert.NotNil(t, err) }) - pkg.AssertError(t, false, err, "%s", k) - - // RethinkDB delay - time.Sleep(100 * time.Millisecond) - - d, err := m.GetClient("1234") - pkg.AssertError(t, false, err, "%s", k) - if err == nil { - compare(t, d, k) - } - - ds, err := m.GetClients() - pkg.AssertError(t, false, err, "%s", k) - assert.Len(t, ds, 2) - assert.NotEqual(t, ds["1234"].ID, ds["2-1234"].ID) - - err = m.UpdateClient(&Client{ - ID: "2-1234", - Secret: "secret-new", - TermsOfServiceURI: "bar", - }) - pkg.AssertError(t, false, err, "%s", k) - time.Sleep(100 * time.Millisecond) - - nc, err := m.GetConcreteClient("2-1234") - if k != "http" { - // http always returns an empty secret - assert.NotEqual(t, d.GetHashedSecret(), nc.GetHashedSecret(), "%s", k) - } - assert.Equal(t, "bar", nc.TermsOfServiceURI, "%s", k) - assert.EqualValues(t, []string{"http://redirect"}, nc.GetRedirectURIs(), "%s", k) - - err = m.DeleteClient("1234") - pkg.AssertError(t, false, err, "%s", k) - - // RethinkDB delay - time.Sleep(100 * time.Millisecond) - - _, err = m.GetClient("1234") - pkg.AssertError(t, true, err, "%s", k) } } diff --git a/cmd/cli/handler.go b/cmd/cli/handler.go index f65210413c3..a4c9887a425 100644 --- a/cmd/cli/handler.go +++ b/cmd/cli/handler.go @@ -5,19 +5,17 @@ import ( ) type Handler struct { - Clients *ClientHandler - Connections *ConnectionHandler - Policies *PolicyHandler - Keys *JWKHandler - Warden *WardenHandler + Clients *ClientHandler + Policies *PolicyHandler + Keys *JWKHandler + Warden *WardenHandler } func NewHandler(c *config.Config) *Handler { return &Handler{ - Clients: newClientHandler(c), - Connections: newConnectionHandler(c), - Policies: newPolicyHandler(c), - Keys: newJWKHandler(c), - Warden: newWardenHandler(c), + Clients: newClientHandler(c), + Policies: newPolicyHandler(c), + Keys: newJWKHandler(c), + Warden: newWardenHandler(c), } } diff --git a/cmd/cli/handler_client.go b/cmd/cli/handler_client.go index cb525d61c6e..31344cf75b4 100644 --- a/cmd/cli/handler_client.go +++ b/cmd/cli/handler_client.go @@ -36,17 +36,17 @@ func (h *ClientHandler) ImportClients(cmd *cobra.Command, args []string) { for _, path := range args { reader, err := os.Open(path) pkg.Must(err, "Could not open file %s: %s", path, err) - var client client.Client - err = json.NewDecoder(reader).Decode(&client) + var c client.Client + err = json.NewDecoder(reader).Decode(&c) pkg.Must(err, "Could not parse JSON: %s", err) - err = h.M.CreateClient(&client) + err = h.M.CreateClient(&c) if h.M.Dry { fmt.Printf("%s\n", err) continue } pkg.Must(err, "Could not create client: %s", err) - fmt.Printf("Imported client %s:%s from %s.\n", client.ID, client.Secret, path) + fmt.Printf("Imported client %s:%s from %s.\n", c.ID, c.Secret, path) } } @@ -63,11 +63,12 @@ func (h *ClientHandler) CreateClient(cmd *cobra.Command, args []string) { callbacks, _ := cmd.Flags().GetStringSlice("callbacks") name, _ := cmd.Flags().GetString("name") id, _ := cmd.Flags().GetString("id") + public, _ := cmd.Flags().GetBool("is-public") secret, err := pkg.GenerateSecret(26) pkg.Must(err, "Could not generate secret: %s", err) - client := &client.Client{ + cc := &client.Client{ ID: id, Secret: string(secret), ResponseTypes: responseTypes, @@ -75,15 +76,16 @@ func (h *ClientHandler) CreateClient(cmd *cobra.Command, args []string) { GrantTypes: grantTypes, RedirectURIs: callbacks, Name: name, + Public: public, } - err = h.M.CreateClient(client) + err = h.M.CreateClient(cc) if h.M.Dry { fmt.Printf("%s\n", err) return } pkg.Must(err, "Could not create client: %s", err) - fmt.Printf("Client ID: %s\n", client.ID) + fmt.Printf("Client ID: %s\n", cc.ID) fmt.Printf("Client Secret: %s\n", secret) } diff --git a/cmd/cli/handler_connection.go b/cmd/cli/handler_connection.go deleted file mode 100644 index 4370ecacdea..00000000000 --- a/cmd/cli/handler_connection.go +++ /dev/null @@ -1,65 +0,0 @@ -package cli - -import ( - "fmt" - - "github.com/ory-am/hydra/config" - "github.com/ory-am/hydra/connection" - "github.com/ory-am/hydra/pkg" - "github.com/pborman/uuid" - "github.com/spf13/cobra" -) - -type ConnectionHandler struct { - Config *config.Config - M *connection.HTTPManager -} - -func newConnectionHandler(c *config.Config) *ConnectionHandler { - return &ConnectionHandler{ - Config: c, - M: &connection.HTTPManager{}, - } -} - -func (h *ConnectionHandler) CreateConnection(cmd *cobra.Command, args []string) { - h.M.Dry, _ = cmd.Flags().GetBool("dry") - h.M.Client = h.Config.OAuth2Client(cmd) - h.M.Endpoint = h.Config.Resolve("/connections") - if len(args) != 3 { - fmt.Print(cmd.UsageString()) - return - } - - err := h.M.Create(&connection.Connection{ - ID: uuid.New(), - Provider: args[0], - LocalSubject: args[1], - RemoteSubject: args[2], - }) - if h.M.Dry { - fmt.Printf("%s\n", err) - return - } - pkg.Must(err, "Could not create connection: %s", err) -} - -func (h *ConnectionHandler) DeleteConnection(cmd *cobra.Command, args []string) { - h.M.Dry, _ = cmd.Flags().GetBool("dry") - h.M.Client = h.Config.OAuth2Client(cmd) - h.M.Endpoint = h.Config.Resolve("/connections") - if len(args) == 0 { - fmt.Print(cmd.UsageString()) - return - } - - for _, arg := range args { - err := h.M.Delete(arg) - if h.M.Dry { - fmt.Printf("%s\n", err) - continue - } - pkg.Must(err, "Could not delete connection: %s", err) - fmt.Printf("Connection %s deleted.\n", arg) - } -} diff --git a/cmd/cli/handler_policy.go b/cmd/cli/handler_policy.go index 86ec7f9fd1e..ed50be51ebd 100644 --- a/cmd/cli/handler_policy.go +++ b/cmd/cli/handler_policy.go @@ -229,4 +229,4 @@ func (h *PolicyHandler) DeletePolicy(cmd *cobra.Command, args []string) { pkg.Must(err, "Could not delete policy: %s", err) fmt.Printf("Connection %s deleted.\n", arg) } -} \ No newline at end of file +} diff --git a/cmd/cli/handler_warden.go b/cmd/cli/handler_warden.go index 034fb23b275..1064f52d7a0 100644 --- a/cmd/cli/handler_warden.go +++ b/cmd/cli/handler_warden.go @@ -5,21 +5,21 @@ import ( "fmt" "github.com/ory-am/hydra/config" + "github.com/ory-am/hydra/oauth2" "github.com/ory-am/hydra/pkg" - "github.com/ory-am/hydra/warden" "github.com/spf13/cobra" "golang.org/x/net/context" ) type WardenHandler struct { Config *config.Config - M *warden.HTTPWarden + M *oauth2.HTTPIntrospector } func newWardenHandler(c *config.Config) *WardenHandler { return &WardenHandler{ Config: c, - M: &warden.HTTPWarden{}, + M: &oauth2.HTTPIntrospector{}, } } @@ -34,11 +34,11 @@ func (h *WardenHandler) IsAuthorized(cmd *cobra.Command, args []string) { } scopes, _ := cmd.Flags().GetStringSlice("scopes") - res, err := h.M.TokenValid(context.Background(), args[0], scopes...) + res, err := h.M.IntrospectToken(context.Background(), args[0], scopes...) pkg.Must(err, "Could not validate token: %s", err) out, err := json.MarshalIndent(res, "", "\t") - pkg.Must(err, "Could not marshall keys: %s", err) + pkg.Must(err, "Could not prettify token: %s", err) fmt.Printf("%s\n", out) } diff --git a/cmd/clients_create.go b/cmd/clients_create.go index 9c9902cdd42..33ad29f4d5f 100644 --- a/cmd/clients_create.go +++ b/cmd/clients_create.go @@ -23,5 +23,6 @@ func init() { clientsCreateCmd.Flags().StringSliceP("grant-types", "g", []string{"authorization_code"}, "A list of allowed grant types") clientsCreateCmd.Flags().StringSliceP("response-types", "r", []string{"code"}, "A list of allowed response types") clientsCreateCmd.Flags().StringSliceP("allowed-scopes", "a", []string{""}, "A list of allowed scopes") + clientsCreateCmd.Flags().Bool("is-public", false, "Use this flag to create a public client") clientsCreateCmd.Flags().StringP("name", "n", "", "The client's name") } diff --git a/cmd/connect.go b/cmd/connect.go index d4a40b7e024..3fad26d3169 100644 --- a/cmd/connect.go +++ b/cmd/connect.go @@ -15,6 +15,7 @@ var connectCmd = &cobra.Command{ Use: "connect", Short: "Connect with a cluster", Run: func(cmd *cobra.Command, args []string) { + secret := "*********" fmt.Println("To keep the current value, press enter.") if u := input("Cluster URL [" + c.ClusterURL + "]: "); u != "" { @@ -23,16 +24,12 @@ var connectCmd = &cobra.Command{ if u := input("Client ID [" + c.ClientID + "]: "); u != "" { c.ClientID = u } - - secret := "*********" if c.ClientSecret == "" { secret = "empty" } - if u := input("Client Secret [" + secret + "]: "); u != "" { c.ClientSecret = u } - if err := c.Persist(); err != nil { log.Fatalf("Unable to save config file because %s.", err) } @@ -45,7 +42,7 @@ func input(message string) string { fmt.Print(message) s, err := reader.ReadString('\n') if err != nil { - fatal("Could not read user input because %s.", err) + fatal(fmt.Sprintf("Could not read user input because %s.", err)) } return strings.TrimSpace(s) } diff --git a/cmd/connections.go b/cmd/connections.go deleted file mode 100644 index 0437c0a348a..00000000000 --- a/cmd/connections.go +++ /dev/null @@ -1,18 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" -) - -// connectionsCmd represents the connections command -var connectionsCmd = &cobra.Command{ - Use: "connections", - Short: "Manage SSO connections", - Long: `With SSO connections, an identity can be associated with a social login provider like -Google, Twitter, or any other SSO provider.`, -} - -func init() { - RootCmd.AddCommand(connectionsCmd) - connectionsCmd.PersistentFlags().Bool("dry", false, "do not execute the command but show the corresponding curl command instead") -} diff --git a/cmd/connections_create.go b/cmd/connections_create.go deleted file mode 100644 index f2f9690ed93..00000000000 --- a/cmd/connections_create.go +++ /dev/null @@ -1,22 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" -) - -// connectionsCreate represents the create command -var connectionsCreate = &cobra.Command{ - Use: "create ", - Short: "Associate local identites with remote ones", - Long: `Use a user id from your database as the local argument. -The provider is the name of the SSO provider, e.g. "google", "twitter", "facebook". -The remote argument is the user's id from the SSO provider. - -Example: - create google peter@foobar.com googleid:jd92joafj`, - Run: cmdHandler.Connections.CreateConnection, -} - -func init() { - connectionsCmd.AddCommand(connectionsCreate) -} diff --git a/cmd/connections_delete.go b/cmd/connections_delete.go deleted file mode 100644 index 524dd45804f..00000000000 --- a/cmd/connections_delete.go +++ /dev/null @@ -1,19 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" -) - -// deleteCmd represents the delete command -var deleteCmd = &cobra.Command{ - Use: "delete [...]", - Short: "Remove a SSO connection", - Long: ` -Example: - hydra connections delete 4adb79ab-f89d-4445-ab01-ff670e51cefa`, - Run: cmdHandler.Connections.DeleteConnection, -} - -func init() { - connectionsCmd.AddCommand(deleteCmd) -} diff --git a/cmd/host.go b/cmd/host.go index ba2a840b47a..27dd9e9be36 100644 --- a/cmd/host.go +++ b/cmd/host.go @@ -23,7 +23,18 @@ CORE CONTROLS - DATABASE_URL: A URL to a persistent backend. Hydra supports various backends: - None: If DATABASE_URL is empty, all data will be lost when the command is killed. - - RethinkDB: If DATABASE_URL is a DSN starting with rethinkdb://, RethinkDB will be used as storage backend. + - Postgres: If DATABASE_URL is a DSN starting with postgres:// PostgreSQL will be used as storage backend. + Example: DATABASE_URL=rethinkdb://user:password@host:123/database + + If PostgreSQL is not serving TLS, append ?sslmode=disable to the url: + DATABASE_URL=rethinkdb://user:password@host:123/database?sslmode=disable + + - MySQL: If DATABASE_URL is a DSN starting with mysql:// MySQL will be used as storage backend. + Example: DATABASE_URL=mysql://user:password@tcp(host:123)/database?parseTime=true + + Be aware that the ?parseTime=true parameter is mandatory, or timestamps will not work. + + - RethinkDB: If DATABASE_URL is a DSN starting with rethinkdb:// RethinkDB will be used as storage backend. Example: DATABASE_URL=rethinkdb://user:password@host:123/database Additionally, these controls are available when using RethinkDB: diff --git a/cmd/policies_resources_add.go b/cmd/policies_resources_add.go index 94af214aed0..46c5b9607fd 100644 --- a/cmd/policies_resources_add.go +++ b/cmd/policies_resources_add.go @@ -11,7 +11,7 @@ var policyResourcesAddCmd = &cobra.Command{ Long: `You can use regular expressions in your matches. Encapsulate them in < >. Example: - hydra policies resources add my-policy some-item-123 some-item-<[234|345]>`, + hydra policies resources add my-policy some-item-123 some-item-<[234|345]>`, Run: cmdHandler.Policies.AddResourceToPolicy, } diff --git a/cmd/root.go b/cmd/root.go index 369526f2e12..d0f0504b7b1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -29,6 +29,7 @@ var RootCmd = &cobra.Command{ var cmdHandler = cli.NewHandler(c) +// Execute adds all child commands to the root command sets flags appropriately. // Execute adds all child commands to the root command sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { @@ -71,15 +72,28 @@ func initConfig() { viper.AutomaticEnv() // read in environment variables that match viper.BindEnv("HOST") + viper.SetDefault("HOST", "") + viper.BindEnv("CLIENT_ID") + viper.SetDefault("CLIENT_ID", "") + viper.BindEnv("CONSENT_URL") + viper.SetDefault("CONSENT_URL", "") + viper.BindEnv("DATABASE_URL") + viper.SetDefault("DATABASE_URL", "") + viper.BindEnv("SYSTEM_SECRET") + viper.SetDefault("SYSTEM_SECRET", "") + viper.BindEnv("CLIENT_SECRET") + viper.SetDefault("CLIENT_SECRET", "") + viper.BindEnv("HTTPS_ALLOW_TERMINATION_FROM") + viper.SetDefault("HTTPS_ALLOW_TERMINATION_FROM", "") viper.BindEnv("CLUSTER_URL") - viper.SetDefault("CLUSTER_URL", "https://localhost:4444") + viper.SetDefault("CLUSTER_URL", "") viper.BindEnv("PORT") viper.SetDefault("PORT", 4444) diff --git a/cmd/root_test.go b/cmd/root_test.go index 0565b26a247..5b73cd1c360 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -36,21 +36,27 @@ func TestExecute(t *testing.T) { }, }, {args: []string{"clients", "create", "--id", "foobarbaz"}}, - {args: []string{"clients", "create", "--id", "foobarbaz", "--dry"}}, + {args: []string{"clients", "create", "--id", "public-foo", "--is-public"}}, {args: []string{"clients", "delete", "foobarbaz"}}, {args: []string{"keys", "create", "foo", "-a", "HS256"}}, - {args: []string{"keys", "create", "foo", "-a", "HS256", "--dry"}}, {args: []string{"keys", "create", "foo", "-a", "HS256"}}, {args: []string{"keys", "get", "foo"}}, {args: []string{"keys", "delete", "foo"}}, - {args: []string{"connections", "create", "google", "localuser", "googleuser"}}, - {args: []string{"connections", "create", "google", "localuser", "googleuser", "--dry"}}, {args: []string{"token", "client"}}, - {args: []string{"policies", "create", "../dist/policies/noone-can-read-private-keys.json"}}, + {args: []string{"token", "user", "--no-open"}, wait: func() bool { + time.Sleep(time.Millisecond * 10) + return false + }}, {args: []string{"policies", "create", "-i", "foobar", "-s", "peter", "max", "-r", "blog", "users", "-a", "post", "ban", "--allow"}}, - {args: []string{"policies", "create", "-i", "foobar", "-s", "peter", "max", "-r", "blog", "users", "-a", "post", "ban", "--allow", "--dry"}}, + {args: []string{"policies", "actions", "add", "foobar", "update|create"}}, + {args: []string{"policies", "actions", "delete", "foobar", "update|create"}}, + {args: []string{"policies", "resources", "add", "foobar", "printer"}}, + {args: []string{"policies", "resources", "delete", "foobar", "printer"}}, + {args: []string{"policies", "subjects", "add", "foobar", "ken", "tracy"}}, + {args: []string{"policies", "subjects", "delete", "foobar", "ken", "tracy"}}, {args: []string{"policies", "get", "foobar"}}, {args: []string{"policies", "delete", "foobar"}}, + {args: []string{"version"}}, } { c.args = append(c.args, []string{"--skip-tls-verify", "--config", path}...) RootCmd.SetArgs(c.args) diff --git a/cmd/server/handler.go b/cmd/server/handler.go index 5bfe5412068..a69904e8e26 100644 --- a/cmd/server/handler.go +++ b/cmd/server/handler.go @@ -5,13 +5,12 @@ import ( "net/http" "time" + "fmt" "github.com/Sirupsen/logrus" - "github.com/pkg/errors" "github.com/julienschmidt/httprouter" "github.com/meatballhat/negroni-logrus" "github.com/ory-am/hydra/client" "github.com/ory-am/hydra/config" - "github.com/ory-am/hydra/connection" "github.com/ory-am/hydra/herodot" "github.com/ory-am/hydra/jwk" "github.com/ory-am/hydra/oauth2" @@ -19,6 +18,7 @@ import ( "github.com/ory-am/hydra/policy" "github.com/ory-am/hydra/warden" "github.com/ory-am/ladon" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/urfave/negroni" "golang.org/x/net/context" @@ -29,6 +29,19 @@ func RunHost(c *config.Config) func(cmd *cobra.Command, args []string) { router := httprouter.New() serverHandler := &Handler{Config: c} serverHandler.registerRoutes(router) + c.ForceHTTP, _ = cmd.Flags().GetBool("dangerous-force-http") + + if c.ClusterURL == "" { + proto := "https" + if c.ForceHTTP { + proto = "http" + } + host := "localhost" + if c.BindHost != "" { + host = c.BindHost + } + c.ClusterURL = fmt.Sprintf("%s://%s:%d", proto, host, c.BindPort) + } if ok, _ := cmd.Flags().GetBool("dangerous-auto-logon"); ok { logrus.Warnln("Do not use flag --dangerous-auto-logon in production.") @@ -55,7 +68,7 @@ func RunHost(c *config.Config) func(cmd *cobra.Command, args []string) { var err error logrus.Infof("Setting up http server on %s", c.GetAddress()) - if ok, _ := cmd.Flags().GetBool("dangerous-force-http"); ok { + if c.ForceHTTP { logrus.Warnln("HTTPS disabled. Never do this in production.") err = srv.ListenAndServe() } else if c.AllowTLSTermination != "" { @@ -69,13 +82,12 @@ func RunHost(c *config.Config) func(cmd *cobra.Command, args []string) { } type Handler struct { - Clients *client.Handler - Connections *connection.Handler - Keys *jwk.Handler - OAuth2 *oauth2.Handler - Policy *policy.Handler - Warden *warden.WardenHandler - Config *config.Config + Clients *client.Handler + Keys *jwk.Handler + OAuth2 *oauth2.Handler + Policy *policy.Handler + Warden *warden.WardenHandler + Config *config.Config } func (h *Handler) registerRoutes(router *httprouter.Router) { @@ -101,11 +113,14 @@ func (h *Handler) registerRoutes(router *httprouter.Router) { // Set up handlers h.Clients = newClientHandler(c, router, clientsManager) h.Keys = newJWKHandler(c, router) - h.Connections = newConnectionHandler(c, router) h.Policy = newPolicyHandler(c, router) h.OAuth2 = newOAuth2Handler(c, router, ctx.KeyManager, oauth2Provider) h.Warden = warden.NewHandler(c, router) + router.GET("/health", func(rw http.ResponseWriter, r *http.Request, _ httprouter.Params) { + rw.WriteHeader(http.StatusNoContent) + }) + // Create root account if new install createRS256KeysIfNotExist(c, oauth2.ConsentEndpointKey, "private", "sig") createRS256KeysIfNotExist(c, oauth2.ConsentChallengeKey, "private", "sig") diff --git a/cmd/server/handler_client_factory.go b/cmd/server/handler_client_factory.go index 49c95d4978c..d4b88204a6b 100644 --- a/cmd/server/handler_client_factory.go +++ b/cmd/server/handler_client_factory.go @@ -19,6 +19,13 @@ func newClientManager(c *config.Config) client.Manager { Clients: map[string]client.Client{}, Hasher: ctx.Hasher, } + case *config.SQLConnection: + m := &client.SQLManager{ + DB: con.GetDatabase(), + Hasher: ctx.Hasher, + } + m.CreateSchemas() + return m case *config.RethinkDBConnection: con.CreateTableIfNotExists("hydra_clients") m := &client.RethinkManager{ diff --git a/cmd/server/handler_connection_factory.go b/cmd/server/handler_connection_factory.go deleted file mode 100644 index adaa23beb24..00000000000 --- a/cmd/server/handler_connection_factory.go +++ /dev/null @@ -1,43 +0,0 @@ -package server - -import ( - "github.com/Sirupsen/logrus" - "github.com/julienschmidt/httprouter" - "github.com/ory-am/hydra/config" - "github.com/ory-am/hydra/connection" - "github.com/ory-am/hydra/herodot" - "golang.org/x/net/context" - r "gopkg.in/dancannon/gorethink.v2" -) - -func newConnectionHandler(c *config.Config, router *httprouter.Router) *connection.Handler { - ctx := c.Context() - - h := &connection.Handler{} - h.H = &herodot.JSON{} - h.W = ctx.Warden - h.SetRoutes(router) - - switch con := ctx.Connection.(type) { - case *config.MemoryConnection: - h.Manager = connection.NewMemoryManager() - break - case *config.RethinkDBConnection: - con.CreateTableIfNotExists("hydra_connections") - m := &connection.RethinkManager{ - Session: con.GetSession(), - Table: r.Table("hydra_connections"), - Connections: make(map[string]connection.Connection), - } - if err := m.ColdStart(); err != nil { - logrus.Fatalf("Could not fetch initial state: %s", err) - } - m.Watch(context.Background()) - h.Manager = m - break - default: - panic("Unknown connection type.") - } - - return h -} diff --git a/cmd/server/handler_jwk_factory.go b/cmd/server/handler_jwk_factory.go index 8ab89facfd8..ad5e7fe2fc2 100644 --- a/cmd/server/handler_jwk_factory.go +++ b/cmd/server/handler_jwk_factory.go @@ -18,6 +18,16 @@ func injectJWKManager(c *config.Config) { case *config.MemoryConnection: ctx.KeyManager = &jwk.MemoryManager{} break + case *config.SQLConnection: + m := &jwk.SQLManager{ + DB: con.GetDatabase(), + Cipher: &jwk.AEAD{ + Key: c.GetSystemSecret(), + }, + } + m.CreateSchemas() + ctx.KeyManager = m + break case *config.RethinkDBConnection: con.CreateTableIfNotExists("hydra_json_web_keys") m := &jwk.RethinkManager{ diff --git a/cmd/server/handler_oauth2_factory.go b/cmd/server/handler_oauth2_factory.go index 9ef3f4ef6e1..4a6d4dcfe9c 100644 --- a/cmd/server/handler_oauth2_factory.go +++ b/cmd/server/handler_oauth2_factory.go @@ -5,17 +5,16 @@ import ( "net/url" "github.com/Sirupsen/logrus" - "github.com/pkg/errors" "github.com/julienschmidt/httprouter" "github.com/ory-am/fosite" "github.com/ory-am/fosite/compose" "github.com/ory-am/hydra/client" "github.com/ory-am/hydra/config" "github.com/ory-am/hydra/herodot" - "github.com/ory-am/hydra/internal" "github.com/ory-am/hydra/jwk" "github.com/ory-am/hydra/oauth2" "github.com/ory-am/hydra/pkg" + "github.com/pkg/errors" "golang.org/x/net/context" r "gopkg.in/dancannon/gorethink.v2" ) @@ -26,34 +25,39 @@ func injectFositeStore(c *config.Config, clients client.Manager) { switch con := ctx.Connection.(type) { case *config.MemoryConnection: - store = &internal.FositeMemoryStore{ + store = &oauth2.FositeMemoryStore{ Manager: clients, AuthorizeCodes: make(map[string]fosite.Requester), IDSessions: make(map[string]fosite.Requester), AccessTokens: make(map[string]fosite.Requester), - Implicit: make(map[string]fosite.Requester), RefreshTokens: make(map[string]fosite.Requester), } break + case *config.SQLConnection: + m := &oauth2.FositeSQLStore{ + DB: con.GetDatabase(), + Manager: clients, + } + m.CreateSchemas() + store = m + break case *config.RethinkDBConnection: con.CreateTableIfNotExists("hydra_oauth2_authorize_code") con.CreateTableIfNotExists("hydra_oauth2_id_sessions") con.CreateTableIfNotExists("hydra_oauth2_access_token") con.CreateTableIfNotExists("hydra_oauth2_implicit") con.CreateTableIfNotExists("hydra_oauth2_refresh_token") - m := &internal.FositeRehinkDBStore{ + m := &oauth2.FositeRehinkDBStore{ Session: con.GetSession(), Manager: clients, AuthorizeCodesTable: r.Table("hydra_oauth2_authorize_code"), IDSessionsTable: r.Table("hydra_oauth2_id_sessions"), AccessTokensTable: r.Table("hydra_oauth2_access_token"), - ImplicitTable: r.Table("hydra_oauth2_implicit"), RefreshTokensTable: r.Table("hydra_oauth2_refresh_token"), - AuthorizeCodes: make(internal.RDBItems), - IDSessions: make(internal.RDBItems), - AccessTokens: make(internal.RDBItems), - Implicit: make(internal.RDBItems), - RefreshTokens: make(internal.RDBItems), + AuthorizeCodes: make(oauth2.RDBItems), + IDSessions: make(oauth2.RDBItems), + AccessTokens: make(oauth2.RDBItems), + RefreshTokens: make(oauth2.RDBItems), } if err := m.ColdStart(); err != nil { logrus.Fatalf("Could not fetch initial state: %s", err) @@ -104,9 +108,11 @@ func newOAuth2Provider(c *config.Config, km jwk.Manager) fosite.OAuth2Provider { compose.OAuth2AuthorizeImplicitFactory, compose.OAuth2ClientCredentialsGrantFactory, compose.OAuth2RefreshTokenGrantFactory, - compose.OpenIDConnectExplicit, - compose.OpenIDConnectHybrid, - compose.OpenIDConnectImplicit, + compose.OpenIDConnectExplicitFactory, + compose.OpenIDConnectHybridFactory, + compose.OpenIDConnectImplicitFactory, + compose.OAuth2TokenRevocationFactory, + compose.OAuth2TokenIntrospectionFactory, ) } @@ -125,7 +131,6 @@ func newOAuth2Handler(c *config.Config, router *httprouter.Router, km jwk.Manage consentURL, err := url.Parse(c.ConsentURL) pkg.Must(err, "Could not parse consent url %s.", c.ConsentURL) - ctx := c.Context() handler := &oauth2.Handler{ ForcedHTTP: c.ForceHTTP, OAuth2: o, @@ -136,13 +141,7 @@ func newOAuth2Handler(c *config.Config, router *httprouter.Router, km jwk.Manage DefaultIDTokenLifespan: c.GetIDTokenLifespan(), }, ConsentURL: *consentURL, - Introspector: &oauth2.LocalIntrospector{ - OAuth2: o, - AccessTokenLifespan: c.GetAccessTokenLifespan(), - Issuer: c.Issuer, - }, - Firewall: ctx.Warden, - H: &herodot.JSON{}, + H: &herodot.JSON{}, } handler.SetRoutes(router) diff --git a/cmd/server/helper_cert.go b/cmd/server/helper_cert.go index d8a2cfd4d91..16500ae0a0c 100644 --- a/cmd/server/helper_cert.go +++ b/cmd/server/helper_cert.go @@ -10,10 +10,10 @@ import ( "time" "github.com/Sirupsen/logrus" - "github.com/pkg/errors" "github.com/ory-am/hydra/config" "github.com/ory-am/hydra/jwk" "github.com/ory-am/hydra/pkg" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/square/go-jose" diff --git a/cmd/server/helper_client.go b/cmd/server/helper_client.go index f38ea79cc17..48cd4290df9 100644 --- a/cmd/server/helper_client.go +++ b/cmd/server/helper_client.go @@ -65,6 +65,6 @@ func (h *Handler) createRootIfNewInstall(c *config.Config) { if forceRoot == "" { logrus.Infof("client_id: %s", root.GetID()) logrus.Infof("client_secret: %s", string(secret)) - logrus.Warn("WARNING: YOU MUST delete this client once in production, as credentials may have been leaked logfiles.") + logrus.Warn("WARNING: YOU MUST delete this client once in production, as credentials may have been leaked in your logfiles.") } } diff --git a/cmd/server/helper_keys.go b/cmd/server/helper_keys.go index 9184a0f0fbb..f95f64bbe02 100644 --- a/cmd/server/helper_keys.go +++ b/cmd/server/helper_keys.go @@ -5,10 +5,10 @@ import ( "crypto/rsa" "github.com/Sirupsen/logrus" - "github.com/pkg/errors" "github.com/ory-am/hydra/config" "github.com/ory-am/hydra/jwk" "github.com/ory-am/hydra/pkg" + "github.com/pkg/errors" ) func createRS256KeysIfNotExist(c *config.Config, set, kid, use string) { diff --git a/cmd/token_user.go b/cmd/token_user.go index 830ff7c74e5..d8567381b58 100644 --- a/cmd/token_user.go +++ b/cmd/token_user.go @@ -52,10 +52,10 @@ var tokenUserCmd = &cobra.Command{ if ok, _ := cmd.Flags().GetBool("no-open"); !ok { webbrowser.Open(location) } - fmt.Printf("If your browser does not open automatically, navigate to: %s\n", location) fmt.Println("Setting up callback listener on http://localhost:4445/callback") fmt.Println("Press ctrl + c on Linux / Windows or cmd + c on OSX to end the process.") + fmt.Printf("If your browser does not open automatically, navigate to:\n\n\t%s\n\n", location) srv := &graceful.Server{ Timeout: 2 * time.Second, @@ -87,19 +87,24 @@ var tokenUserCmd = &cobra.Command{ token, err := conf.Exchange(ctx, code) pkg.Must(err, "Could not exchange code for token: %s", err) - fmt.Printf("Access Token: %s\n", token.AccessToken) - fmt.Printf("Refresh Token: %s\n", token.RefreshToken) - fmt.Printf("Expires in: %s\n", token.Expiry) + fmt.Printf("Access Token:\n\t%s\n", token.AccessToken) + fmt.Printf("Refresh Token:\n\t%s\n\n", token.RefreshToken) + fmt.Printf("Expires in:\n\t%s\n\n", token.Expiry) - w.Write([]byte(fmt.Sprintf("Access Token: %s\n", token.AccessToken))) - w.Write([]byte(fmt.Sprintf("Refresh Token: %s\n", token.RefreshToken))) - w.Write([]byte(fmt.Sprintf("Expires in: %s\n", token.Expiry))) + w.Write([]byte(fmt.Sprintf(` + +
    +
  • Access Token: %s
  • +
  • Refresh Token: %s
  • +
  • Expires in: %s
  • +`, token.AccessToken, token.RefreshToken, token.Expiry))) idt := token.Extra("id_token") if idt != nil { - w.Write([]byte(fmt.Sprintf("ID Token: %s\n", idt))) - fmt.Printf("ID Token: %s\n", idt) + w.Write([]byte(fmt.Sprintf(`
  • ID Token: %s
  • `, idt))) + fmt.Printf("ID Token:\n\t%s\n\n", idt) } + w.Write([]byte("
")) }) srv.Server.Handler = r srv.ListenAndServe() diff --git a/internal/firewall.go b/compose/firewall.go similarity index 94% rename from internal/firewall.go rename to compose/firewall.go index e776c46a71a..8e95fb807a8 100644 --- a/internal/firewall.go +++ b/compose/firewall.go @@ -1,4 +1,4 @@ -package internal +package compose import ( "net/http" @@ -35,7 +35,7 @@ func NewFirewall(issuer string, subject string, scopes fosite.Arguments, p ...la Warden: ladonWarden, OAuth2: &fosite.Fosite{ Store: fositeStore, - TokenValidators: fosite.TokenValidators{ + TokenIntrospectionHandlers: fosite.TokenIntrospectionHandlers{ &foauth2.CoreValidator{ CoreStrategy: pkg.HMACStrategy, CoreStorage: fositeStore, diff --git a/config/backend_connections.go b/config/backend_connections.go index 44c55631cc3..0432e102d59 100644 --- a/config/backend_connections.go +++ b/config/backend_connections.go @@ -9,14 +9,51 @@ import ( "time" "github.com/Sirupsen/logrus" - "github.com/pkg/errors" + _ "github.com/go-sql-driver/mysql" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" "github.com/ory-am/hydra/pkg" + "github.com/pkg/errors" "github.com/spf13/viper" r "gopkg.in/dancannon/gorethink.v2" + "strings" ) type MemoryConnection struct{} +type SQLConnection struct { + db *sqlx.DB + URL *url.URL +} + +func (c *SQLConnection) GetDatabase() *sqlx.DB { + if c.db != nil { + return c.db + } + + var err error + if err = pkg.Retry(time.Second*15, time.Minute*2, func() error { + logrus.Infof("Connecting with %s", c.URL.String()) + u := c.URL.String() + if c.URL.Scheme == "mysql" { + u = strings.Replace(u, "mysql://", "", -1) + } + + if c.db, err = sqlx.Open(c.URL.Scheme, u); err != nil { + return errors.Errorf("Could not connect to SQL: %s", err) + } else if err := c.db.Ping(); err != nil { + return errors.Errorf("Could not connect to SQL: %s", err) + } + + logrus.Infof("Connected to SQL!") + return nil + }); err != nil { + logrus.Fatalf("Could not connect to SQL: %s", err) + } + + return c.db +} + type RethinkDBConnection struct { session *r.Session URL *url.URL diff --git a/config/config.go b/config/config.go index 254e27592a4..f7fddce43eb 100644 --- a/config/config.go +++ b/config/config.go @@ -13,12 +13,12 @@ import ( "time" "github.com/Sirupsen/logrus" - "github.com/pkg/errors" + "github.com/ory-am/fosite" foauth2 "github.com/ory-am/fosite/handler/oauth2" - "github.com/ory-am/fosite/hash" "github.com/ory-am/fosite/token/hmac" "github.com/ory-am/hydra/pkg" "github.com/ory-am/ladon" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/net/context" @@ -146,8 +146,13 @@ func (c *Config) Context() *Context { case "rethinkdb": connection = &RethinkDBConnection{URL: u} break + case "postgres": + fallthrough + case "mysql": + connection = &SQLConnection{URL: u} + break default: - logrus.Fatalf("Unkown DSN in DATABASE_URL: %s", c.DatabaseURL) + logrus.Fatalf("Unkown DSN %s in DATABASE_URL: %s", u.Scheme, c.DatabaseURL) } } @@ -157,6 +162,11 @@ func (c *Config) Context() *Context { logrus.Printf("DATABASE_URL not set, connecting to ephermal in-memory database.") manager = ladon.NewMemoryManager() break + case *SQLConnection: + m := ladon.NewSQLManager(con.GetDatabase(), nil) + m.CreateSchemas() + manager = m + break case *RethinkDBConnection: logrus.Printf("DATABASE_URL set, connecting to RethinkDB.") con.CreateTableIfNotExists("hydra_policies") @@ -176,7 +186,7 @@ func (c *Config) Context() *Context { c.context = &Context{ Connection: connection, - Hasher: &hash.BCrypt{ + Hasher: &fosite.BCrypt{ WorkFactor: c.BCryptWorkFactor, }, LadonManager: manager, diff --git a/config/context.go b/config/context.go index e93e2dce092..90ccbe5fa26 100644 --- a/config/context.go +++ b/config/context.go @@ -1,8 +1,8 @@ package config import ( + "github.com/ory-am/fosite" "github.com/ory-am/fosite/handler/oauth2" - "github.com/ory-am/fosite/hash" "github.com/ory-am/hydra/firewall" "github.com/ory-am/hydra/jwk" "github.com/ory-am/hydra/pkg" @@ -12,7 +12,7 @@ import ( type Context struct { Connection interface{} - Hasher hash.Hasher + Hasher fosite.Hasher Warden firewall.Firewall LadonManager ladon.Manager FositeStrategy oauth2.CoreStrategy diff --git a/connection/connection.go b/connection/connection.go deleted file mode 100644 index d592aac9820..00000000000 --- a/connection/connection.go +++ /dev/null @@ -1,29 +0,0 @@ -package connection - -// Connection connects an subject S with a token T issued by provider P -type Connection struct { - ID string `json:"id,omitempty" gorethink:"id"` - Provider string `json:"provider" valid:"required" gorethink:"provider"` - LocalSubject string `json:"localSubject" valid:"required" gorethink:"localsubject"` - RemoteSubject string `json:"remoteSubject" valid:"required" gorethink:"remotesubject"` -} - -// GetID returns the connection's unique identifier. -func (c *Connection) GetID() string { - return c.ID -} - -// GetProvider returns the connection's provider, for example "Google". -func (c *Connection) GetProvider() string { - return c.Provider -} - -// GetLocalSubject returns the connection's local subject, for example "peter". -func (c *Connection) GetLocalSubject() string { - return c.LocalSubject -} - -// GetRemoteSubject returns the connection's remote subject, for example "peter@gmail.com". -func (c *Connection) GetRemoteSubject() string { - return c.RemoteSubject -} diff --git a/connection/connection_test.go b/connection/connection_test.go deleted file mode 100644 index 8f194fc6132..00000000000 --- a/connection/connection_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package connection_test - -import ( - "testing" - - "github.com/ory-am/hydra/connection" - "github.com/pborman/uuid" - "github.com/stretchr/testify/assert" -) - -func TestConnection(t *testing.T) { - c := &connection.Connection{ - ID: uuid.New(), - LocalSubject: "peter", - RemoteSubject: "peter@gmail.com", - Provider: "google", - } - - assert.Equal(t, c.ID, c.GetID()) - assert.Equal(t, c.Provider, c.GetProvider()) - assert.Equal(t, c.LocalSubject, c.GetLocalSubject()) - assert.Equal(t, c.RemoteSubject, c.GetRemoteSubject()) -} diff --git a/connection/handler.go b/connection/handler.go deleted file mode 100644 index bf1ba7bd7d5..00000000000 --- a/connection/handler.go +++ /dev/null @@ -1,164 +0,0 @@ -package connection - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/asaskevich/govalidator" - "github.com/pkg/errors" - "github.com/julienschmidt/httprouter" - "github.com/ory-am/hydra/firewall" - "github.com/ory-am/hydra/herodot" - "github.com/ory-am/ladon" - "github.com/pborman/uuid" - "golang.org/x/net/context" -) - -const ( - connectionsResource = "rn:hydra:connections" - connectionResource = "rn:hydra:connections:%s" - scope = "hydra.connections" -) - -type Handler struct { - Manager Manager - H herodot.Herodot - W firewall.Firewall -} - -func (h *Handler) SetRoutes(r *httprouter.Router) { - r.GET("/connections", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - if r.URL.Query().Get("local_subject") != "" { - h.FindLocal(w, r, ps) - return - } - - if r.URL.Query().Get("remote_subject") != "" && r.URL.Query().Get("provider") != "" { - h.FindRemote(w, r, ps) - return - } - - var ctx = context.Background() - h.H.WriteErrorCode(ctx, w, r, http.StatusBadRequest, errors.New("Pass either [local_subject] or [remote_subject, provider] as query to this request")) - }) - - r.POST("/connections", h.Create) - r.GET("/connections/:id", h.Get) - r.DELETE("/connections/:id", h.Delete) -} - -func (h *Handler) Create(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - var conn Connection - var ctx = context.Background() - - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ - Resource: connectionsResource, - Action: "create", - }, scope); err != nil { - h.H.WriteError(ctx, w, r, err) - return - } - - if err := json.NewDecoder(r.Body).Decode(&conn); err != nil { - h.H.WriteErrorCode(ctx, w, r, http.StatusBadRequest, err) - return - } - - if v, err := govalidator.ValidateStruct(conn); err != nil { - h.H.WriteErrorCode(ctx, w, r, http.StatusBadRequest, err) - return - } else if !v { - h.H.WriteErrorCode(ctx, w, r, http.StatusBadRequest, errors.New("Payload did not validate.")) - return - } - - conn.ID = uuid.New() - if err := h.Manager.Create(&conn); err != nil { - h.H.WriteError(ctx, w, r, err) - return - } - - h.H.WriteCreated(ctx, w, r, "/oauth2/connections/"+conn.ID, &conn) -} - -func (h *Handler) FindLocal(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - var ctx = context.Background() - - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ - Resource: connectionsResource, - Action: "find", - }, scope); err != nil { - h.H.WriteError(ctx, w, r, err) - return - } - - conns, err := h.Manager.FindAllByLocalSubject(r.URL.Query().Get("local_subject")) - if err != nil { - h.H.WriteError(ctx, w, r, err) - return - } - - h.H.Write(ctx, w, r, conns) -} - -func (h *Handler) FindRemote(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - var ctx = context.Background() - - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ - Resource: connectionsResource, - Action: "find", - }, scope); err != nil { - h.H.WriteError(ctx, w, r, err) - return - } - - conns, err := h.Manager.FindByRemoteSubject(r.URL.Query().Get("provider"), r.URL.Query().Get("remote_subject")) - if err != nil { - h.H.WriteError(ctx, w, r, err) - return - } - - h.H.Write(ctx, w, r, conns) -} - -func (h *Handler) Get(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - var ctx = context.Background() - var id = ps.ByName("id") - - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ - Resource: fmt.Sprintf(connectionResource, id), - Action: "get", - }, scope); err != nil { - h.H.WriteError(ctx, w, r, err) - return - } - - conn, err := h.Manager.Get(id) - if err != nil { - h.H.WriteError(ctx, w, r, err) - return - } - - h.H.Write(ctx, w, r, conn) -} - -func (h *Handler) Delete(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - var ctx = context.Background() - var id = ps.ByName("id") - - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ - Resource: fmt.Sprintf(connectionResource, id), - Action: "delete", - }, scope); err != nil { - h.H.WriteError(ctx, w, r, err) - return - } - - if err := h.Manager.Delete(ps.ByName("id")); err != nil { - h.H.WriteError(ctx, w, r, err) - return - } - - w.WriteHeader(http.StatusNoContent) -} diff --git a/connection/manager.go b/connection/manager.go deleted file mode 100644 index 61948017667..00000000000 --- a/connection/manager.go +++ /dev/null @@ -1,17 +0,0 @@ -package connection - -// Storage defines an interface for storing connections. -type Manager interface { - // Create a new connection. - Create(c *Connection) error - - // Delete an existing connection. - Delete(id string) error - - // Get an existing connection. - Get(id string) (*Connection, error) - - FindAllByLocalSubject(subject string) ([]Connection, error) - - FindByRemoteSubject(provider, subject string) (*Connection, error) -} diff --git a/connection/manager_http.go b/connection/manager_http.go deleted file mode 100644 index 97a137cd16b..00000000000 --- a/connection/manager_http.go +++ /dev/null @@ -1,76 +0,0 @@ -package connection - -import ( - "net/http" - "net/url" - - "github.com/ory-am/hydra/pkg" -) - -type HTTPManager struct { - Endpoint *url.URL - Client *http.Client - Dry bool -} - -func (m *HTTPManager) Create(connection *Connection) error { - var r = pkg.NewSuperAgent(m.Endpoint.String()) - r.Client = m.Client - r.Dry = m.Dry - return r.Create(connection) -} - -func (m *HTTPManager) Get(id string) (*Connection, error) { - var connection Connection - var r = pkg.NewSuperAgent(pkg.JoinURL(m.Endpoint, id).String()) - r.Client = m.Client - r.Dry = m.Dry - if err := r.Get(&connection); err != nil { - return nil, err - } - - return &connection, nil -} - -func (m *HTTPManager) Delete(id string) error { - var r = pkg.NewSuperAgent(pkg.JoinURL(m.Endpoint, id).String()) - r.Client = m.Client - r.Dry = m.Dry - return r.Delete() -} - -func (m *HTTPManager) FindAllByLocalSubject(subject string) ([]Connection, error) { - var connection []Connection - var u = pkg.CopyURL(m.Endpoint) - var q = u.Query() - - q.Add("local_subject", subject) - u.RawQuery = q.Encode() - - var r = pkg.NewSuperAgent(u.String()) - r.Client = m.Client - r.Dry = m.Dry - if err := r.Get(&connection); err != nil { - return nil, err - } - - return connection, nil -} - -func (m *HTTPManager) FindByRemoteSubject(provider, subject string) (*Connection, error) { - var connection Connection - var u = pkg.CopyURL(m.Endpoint) - var q = u.Query() - q.Add("remote_subject", subject) - q.Add("provider", provider) - u.RawQuery = q.Encode() - - var r = pkg.NewSuperAgent(u.String()) - r.Client = m.Client - r.Dry = m.Dry - if err := r.Get(&connection); err != nil { - return nil, err - } - - return &connection, nil -} diff --git a/connection/manager_memory.go b/connection/manager_memory.go deleted file mode 100644 index f1a9ed28e91..00000000000 --- a/connection/manager_memory.go +++ /dev/null @@ -1,71 +0,0 @@ -package connection - -import ( - "sync" - - "github.com/pkg/errors" - "github.com/ory-am/hydra/pkg" -) - -type MemoryManager struct { - Connections map[string]Connection - sync.RWMutex -} - -func NewMemoryManager() *MemoryManager { - return &MemoryManager{ - Connections: make(map[string]Connection), - } -} - -func (m *MemoryManager) Create(c *Connection) error { - m.Lock() - defer m.Unlock() - - m.Connections[c.GetID()] = *c - return nil -} - -func (m *MemoryManager) Delete(id string) error { - m.Lock() - defer m.Unlock() - - delete(m.Connections, id) - return nil -} - -func (m *MemoryManager) Get(id string) (*Connection, error) { - m.RLock() - defer m.RUnlock() - - c, ok := m.Connections[id] - if !ok { - return nil, errors.Wrap(pkg.ErrNotFound, "") - } - return &c, nil -} - -func (m *MemoryManager) FindAllByLocalSubject(subject string) ([]Connection, error) { - m.RLock() - defer m.RUnlock() - - var cs []Connection - for _, c := range m.Connections { - if c.GetLocalSubject() == subject { - cs = append(cs, c) - } - } - return cs, nil -} - -func (m *MemoryManager) FindByRemoteSubject(provider, subject string) (*Connection, error) { - m.RLock() - defer m.RUnlock() - - for _, c := range m.Connections { - if c.GetProvider() == provider && c.GetRemoteSubject() == subject { - return &c, nil - } - } - return nil, errors.Wrap(pkg.ErrNotFound, "") -} diff --git a/connection/manager_rethinkdb.go b/connection/manager_rethinkdb.go deleted file mode 100644 index 3635fa2331a..00000000000 --- a/connection/manager_rethinkdb.go +++ /dev/null @@ -1,139 +0,0 @@ -package connection - -import ( - "sync" - - r "gopkg.in/dancannon/gorethink.v2" - - "time" - - "github.com/Sirupsen/logrus" - "github.com/pkg/errors" - "github.com/ory-am/hydra/pkg" - "golang.org/x/net/context" -) - -type RethinkManager struct { - Session *r.Session - Table r.Term - - Connections map[string]Connection - - sync.RWMutex -} - -func (m *RethinkManager) Create(c *Connection) error { - if err := m.publishCreate(c); err != nil { - return err - } - return nil -} - -func (m *RethinkManager) Delete(id string) error { - if err := m.publishDelete(id); err != nil { - return err - } - - return nil -} - -func (m *RethinkManager) Get(id string) (*Connection, error) { - m.RLock() - defer m.RUnlock() - - c, ok := m.Connections[id] - if !ok { - return nil, errors.Wrap(pkg.ErrNotFound, "") - } - return &c, nil -} - -func (m *RethinkManager) FindAllByLocalSubject(subject string) ([]Connection, error) { - m.RLock() - defer m.RUnlock() - - var cs []Connection - for _, c := range m.Connections { - if c.GetLocalSubject() == subject { - cs = append(cs, c) - } - } - return cs, nil -} - -func (m *RethinkManager) FindByRemoteSubject(provider, subject string) (*Connection, error) { - m.RLock() - defer m.RUnlock() - - for _, c := range m.Connections { - if c.GetProvider() == provider && c.GetRemoteSubject() == subject { - return &c, nil - } - } - return nil, errors.Wrap(pkg.ErrNotFound, "") -} - -func (m *RethinkManager) ColdStart() error { - m.Connections = map[string]Connection{} - clients, err := m.Table.Run(m.Session) - if err != nil { - return errors.Wrap(err, "") - } - - var connection Connection - m.Lock() - defer m.Unlock() - for clients.Next(&connection) { - m.Connections[connection.ID] = connection - } - - return nil -} - -func (m *RethinkManager) publishCreate(c *Connection) error { - if err := m.Table.Insert(c).Exec(m.Session); err != nil { - return errors.Wrap(err, "") - } - return nil -} - -func (m *RethinkManager) publishDelete(id string) error { - if err := m.Table.Get(id).Delete().Exec(m.Session); err != nil { - return errors.Wrap(err, "") - } - return nil -} - -func (m *RethinkManager) Watch(ctx context.Context) { - go pkg.Retry(time.Second*15, time.Minute, func() error { - connections, err := m.Table.Changes().Run(m.Session) - if err != nil { - return errors.Wrap(err, "") - } - defer connections.Close() - - var update map[string]*Connection - for connections.Next(&update) { - logrus.Debug("Received update in social connection manager.") - newVal := update["new_val"] - oldVal := update["old_val"] - m.Lock() - if newVal == nil && oldVal != nil { - delete(m.Connections, oldVal.GetID()) - } else if newVal != nil && oldVal != nil { - delete(m.Connections, oldVal.GetID()) - m.Connections[newVal.GetID()] = *newVal - } else { - m.Connections[newVal.GetID()] = *newVal - } - m.Unlock() - } - - if connections.Err() != nil { - err = errors.Wrap(connections.Err(), "") - pkg.LogError(err) - return err - } - return nil - }) -} diff --git a/connection/manager_test.go b/connection/manager_test.go deleted file mode 100644 index 217341487e4..00000000000 --- a/connection/manager_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package connection - -import ( - "testing" - - "net/http/httptest" - "net/url" - - "log" - "os" - "time" - - "github.com/julienschmidt/httprouter" - "github.com/ory-am/dockertest" - "github.com/ory-am/fosite" - "github.com/ory-am/hydra/herodot" - "github.com/ory-am/hydra/internal" - "github.com/ory-am/hydra/pkg" - "github.com/ory-am/ladon" - "github.com/pborman/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/net/context" - r "gopkg.in/dancannon/gorethink.v2" -) - -var connections = map[string]*Connection{ - "a": { - ID: uuid.New(), - LocalSubject: "peter", - RemoteSubject: "peterson", - Provider: "google", - }, - "b": { - ID: uuid.New(), - LocalSubject: "peter", - RemoteSubject: "dudeguy", - Provider: "amazon", - }, -} - -var managers = map[string]Manager{ - "memory": NewMemoryManager(), -} - -var ts *httptest.Server - -func init() { - localWarden, httpClient := internal.NewFirewall("hydra", "alice", fosite.Arguments{scope}, - &ladon.DefaultPolicy{ - ID: "1", - Subjects: []string{"alice"}, - Resources: []string{"rn:hydra:connections<.*>"}, - Actions: []string{"create", "get", "delete", "find"}, - Effect: ladon.AllowAccess, - }, - ) - - s := &Handler{ - Manager: &MemoryManager{Connections: map[string]Connection{}}, - H: &herodot.JSON{}, - W: localWarden, - } - - r := httprouter.New() - s.SetRoutes(r) - ts = httptest.NewServer(r) - - u, _ := url.Parse(ts.URL + "/connections") - managers["http"] = &HTTPManager{ - Client: httpClient, - Endpoint: u, - } -} - -var rethinkManager *RethinkManager - -func TestMain(m *testing.M) { - var session *r.Session - var err error - - c, err := dockertest.ConnectToRethinkDB(20, time.Second, func(url string) bool { - if session, err = r.Connect(r.ConnectOpts{Address: url, Database: "hydra"}); err != nil { - return false - } else if _, err = r.DBCreate("hydra").RunWrite(session); err != nil { - log.Printf("Database exists: %s", err) - return false - } else if _, err = r.TableCreate("hydra_clients").RunWrite(session); err != nil { - log.Printf("Could not create table: %s", err) - return false - } - - rethinkManager = &RethinkManager{ - Session: session, - Table: r.Table("hydra_clients"), - Connections: make(map[string]Connection), - } - rethinkManager.Watch(context.Background()) - time.Sleep(500 * time.Millisecond) - return true - }) - if session != nil { - defer session.Close() - } - if err != nil { - log.Fatalf("Could not connect to database: %s", err) - } - managers["rethink"] = rethinkManager - - retCode := m.Run() - c.KillRemove() - os.Exit(retCode) -} - -func BenchmarkRethinkGet(b *testing.B) { - b.StopTimer() - m := rethinkManager - var err error - err = m.Create(&Connection{ - ID: "someid", - LocalSubject: "peter", - RemoteSubject: "peterson", - Provider: "google", - }) - if err != nil { - b.Fatalf("%s", err) - } - time.Sleep(100 * time.Millisecond) - - b.StartTimer() - for i := 0; i < b.N; i++ { - _, _ = m.Get("someid") - } -} - -func TestColdStart(t *testing.T) { - pkg.AssertError(t, false, rethinkManager.Create(&Connection{ID: "foo"})) - pkg.AssertError(t, false, rethinkManager.Create(&Connection{ID: "bar"})) - - time.Sleep(time.Second / 2) - rethinkManager.Connections = map[string]Connection{} - pkg.AssertError(t, false, rethinkManager.ColdStart()) - - c1, err := rethinkManager.Get("foo") - pkg.AssertError(t, false, err) - c2, err := rethinkManager.Get("bar") - pkg.AssertError(t, false, err) - - assert.NotEqual(t, c1, c2) - assert.Equal(t, "foo", c1.ID) -} - -func TestCreateGetFindDelete(t *testing.T) { - for m, store := range managers { - _, err := store.Get("asdf") - pkg.RequireError(t, true, err) - - for _, c := range connections { - err = store.Create(c) - pkg.RequireError(t, false, err) - } - - time.Sleep(200 * time.Millisecond) - - for _, c := range connections { - res, err := store.Get(c.GetID()) - pkg.RequireError(t, false, err) - require.Equal(t, c, res) - - cs, err := store.FindAllByLocalSubject("peter") - pkg.RequireError(t, false, err) - assert.Len(t, cs, 2, "%s", m) - //require.NotEqual(t, cs[1], cs[0]) - - res, err = store.FindByRemoteSubject("google", "peterson") - pkg.RequireError(t, false, err, m) - require.Equal(t, connections["a"], res) - } - - for _, c := range connections { - err = store.Delete(c.GetID()) - pkg.RequireError(t, false, err) - - time.Sleep(100 * time.Millisecond) - - _, err = store.Get(c.GetID()) - pkg.RequireError(t, true, err) - } - } -} diff --git a/docker-compose.yml b/docker-compose.yml index ad0e26e0a7e..ab9431f9a31 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,39 +5,48 @@ services: hydra: build: context: . - dockerfile: Dockerfile-dangerous + dockerfile: Dockerfile-demo + links: + - postgresd:postgresd + - mysqld:mysqld volumes: - hydravolume:/root - links: - - rethinkdb:database ports: - "4444:4444" - "4445:4445" environment: + - LOG_LEVEL=${LOG_LEVEL} - SYSTEM_SECRET=${SYSTEM_SECRET} - CONSENT_URL=http://${DOCKER_IP}:3000 - - DATABASE_URL=rethinkdb://database:28015/hydra + - DATABASE_URL=postgres://postgres:secret@postgresd:5432/postgres?sslmode=disable +# Uncomment the following line to use mysql instead. +# - DATABASE_URL=mysql://root:secret@tcp(mysqld:3306)/mysql?parseTime=true + - FORCE_ROOT_CLIENT_CREDENTIALS=admin:demo-password restart: unless-stopped consent: environment: - - HYDRA_URL=https://hydra:4444 + - HYDRA_URL=http://hydra:4444 + - HYDRA_CLIENT_ID=admin + - HYDRA_CLIENT_SECRET=demo-password - NODE_TLS_REJECT_UNAUTHORIZED=0 image: oryam/hydra-idp-react links: - hydra - volumes: - - hydravolume:/root ports: - "3000:3000" restart: unless-stopped - rethinkdb: - image: rethinkdb - ports: - - "8080:8080" - - "28015:28015" - - "29015:29015" + postgresd: + image: postgres:9.6 + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=secret + + mysqld: + image: mysql:5.7 + environment: + - MYSQL_ROOT_PASSWORD=secret volumes: hydravolume: diff --git a/docs/README.md b/docs/README.md index f719bac1a3a..42941504b38 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,189 @@ -# What is [Hydra](https://github.com/ory-am/hydra)? +# Introduction -At first, there was the monolith. The monolith worked well with the bespoke authentication module. -Then, the web evolved into an elastic cloud that serves thousands of different user agents -in every part of the world. +Welcome to the Hydra documentation. This documentation will -Hydra is driven by the need for a **scalable in memory -OAuth2 and OpenID Connect** layer, that integrates with every Identity Provider you can imagine. +1. teach you what OAuth2 and OpenID Connect are and how Hydra fits in the picture. +2. help you run a Hydra installation on your system using Docker. +3. teach you how to install, configure, run and use Hydra. +3. teach you how to hack and contribute back to Hydra. -Hydra is available through [Docker](https://hub.docker.com/r/oryam/hydra/) and at [GitHub](https://github.com/ory-am/hydra). +Let us begin with the first part, understanding what OAuth2 and OpenID Connect are. -### Feature Overview +## Introduction to OAuth 2.0 and OpenID Connect -1. **Availability:** Hydra uses pub/sub to have the latest data available in memory. The in-memory architecture allows for heavy duty workloads. -2. **Scalability:** Hydra scales effortlessly on every platform you can imagine, including Heroku, Cloud Foundry, Docker, +This section will give you some ideas of what OAuth 2.0 and OpenID Connect 1.0 are for. If you +already know what OAuth2 and OpenID Connect are and how they works, you can skip to the next [Section](#introduction-to-hydra). +This section will not explain how the various flows of OAuth2 work and how they look like. We strongly recommend +to read the following articles: + +* [DigitalOcean: An Introduction to OAuth 2](https://www.digitalocean.com/community/tutorials/an-introduction-to-oauth-2) +* [Aaron Parecki: OAuth2 Simplified](https://aaronparecki.com/2012/07/29/2/oauth2-simplified) +* [Zapier: Chapter 5: Authentication, Part 2](https://zapier.com/learn/apis/chapter-5-authentication-part-2/) + +### What is OAuth 2.0? + +[The OAuth 2.0 authorization framework](https://tools.ietf.org/html/rfc6749) is a memo in the +[Request for Comments](https://www.ietf.org/rfc.html) document series published by the +IETF Internet Engineering Task Force (IETF). Memos in the Requests for Comments (RFC) document series +contain technical and organizational notes about the Internet. They cover many aspects of computer +networking, including protocols, procedures, programs, and concepts [...]. + +The OAuth 2.0 authorization framework enables a third-party +application to obtain limited access to an HTTP service, either on +behalf of a resource owner by orchestrating an approval interaction +between the resource owner and the HTTP service, or by allowing the +third-party application to obtain access on its own behalf. + +In the traditional client-server authentication model, the client +requests an access-restricted resource (protected resource) on the +server by authenticating with the server using the resource owner's +credentials. In order to provide third-party applications access to +restricted resources, the resource owner shares its credentials with +the third party. This creates several problems and limitations. + +OAuth addresses these issues by introducing an authorization layer +and separating the role of the client from that of the resource +owner. In OAuth, the client requests access to resources controlled +by the resource owner and hosted by the resource server, and is +issued a different set of credentials than those of the resource +owner. + +Instead of using the resource owner's credentials to access protected +resources, the client obtains an access token -- a string denoting a +specific scope, lifetime, and other access attributes. Access tokens +are issued to third-party clients by an authorization server with the +approval of the resource owner. The client uses the access token to +access the protected resources hosted by the resource server. + +Source: [IETF RFC 6749](https://tools.ietf.org/html/rfc6749) + +### OAuth 2.0 Example + +An end-user (resource owner) can grant a printing +service (client) access to her protected photos stored at a photo- +sharing service (resource server), without sharing her username and +password with the printing service. Instead, she authenticates +directly with a server trusted by the photo-sharing service +(authorization server), which issues the printing service delegation- +specific credentials (access token). + +Source: [IETF RFC 6749](https://tools.ietf.org/html/rfc6749) + +### What is OpenID Connect 1.0? + +OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol. +It enables Clients to verify the identity of the End-User based on the authentication performed +by an Authorization Server, as well as to obtain basic profile information about the End-User in +an interoperable and REST-like manner. + +As background, the OAuth 2.0 Authorization Framework and OAuth 2.0 Bearer Token +Usage specifications provide a general framework for third-party +applications to obtain and use limited access to HTTP resources. +They define mechanisms to obtain and use Access Tokens to access resources +but do not define standard methods to provide identity information. +Notably, without profiling OAuth 2.0, it is incapable of providing information +about the authentication of an End-User. + +OpenID Connect implements authentication as an extension to the OAuth 2.0 authorization process. + +Source [OpenID Connect Core 1.0](openid.net/specs/openid-connect-core-1_0.html) + + +**OpenID Connect 1.0** is a simple identity layer on top of the OAuth 2.0 protocol. +It allows Clients to verify the identity of the End-User based on the authentication performed +by an Authorization Server, as well as to obtain basic profile information about the End-User in an +interoperable and REST-like manner. + +OpenID Connect allows clients of all types, including Web-based, mobile, and JavaScript clients, +to request and receive information about authenticated sessions and end-users. The specification +suite is extensible, allowing participants to use optional features such as encryption of identity data, +discovery of OpenID Providers, and session management, when it makes sense for them. + +There are different work flows for OpenID Connect 1.0, we recommend checking out the OpenID Connect sandbox at +[openidconnect.net](https://openidconnect.net/). + +## Introduction to Hydra + +Hydra is a server implementation of the OAuth 2.0 authorization framework and the OpenID Connect Core 1.0. Existing OAuth2 +implementations usually ship as libraries or SDKs such as [node-oauth2-server](https://github.com/oauthjs/node-oauth2-server) +or [fosite](https://github.com/ory-am/fosite/issues), or as fully featured identity solutions with user +management and user interfaces, such as [Dex](https://github.com/coreos/dex) or [Okta](https://www.okta.com/). + +Implementing and using OAuth2 without understanding the whole specification is challenging and prone to errors, even when +SDKs are being used. The primary goal of Hydra is to make OAuth 2.0 and OpenID Connect 1.0 less painful to set up and easier to use. + +Hydra implements the flows described in OAuth2 and OpenID Connect 1.0 without forcing you to use a "Hydra User Management" +or some template engine or a predefined front-end. Instead it relies on HTTP redirection and cryptographic methods +to verify user consent allowing you to use Hydra with any authentication endpoint, be it [authboss](https://github.com/go-authboss/authboss), +[auth0.com](https://auth0.com/) or your proprietary PHP authentication. + +Hydra incorporates best practices in the area of the web service technology: + +1. Hydra ships as a single binary for all popular platforms including Linux, OSX and Windows, without any additional +dependencies. For further simplicity, Hydra is available as a [Docker Image](https://hub.docker.com/r/oryam/hydra/). +2. Hydra is built security first: architecture and work flows are designed to neutralize various common (OWASP TOP TEN) +and uncommon attack vectors. [Learn more](https://ory-am.gitbooks.io/hydra/content/basics/security.html). +3. Hydra has a low CPU and memory footprint, short start up times and a CLI with developers in mind. +4. Additionally, Hydra is capable of sophisticated access control, suitable for distributed systems or large organization. [Learn more](https://ory-am.gitbooks.io/hydra/content/access-control.html). +5. Hydra scales effortlessly up and down on every platform imaginable, including Heroku, Cloud Foundry, Docker, Google Container Engine and many more. -3. **Integration:** Hydra wraps your existing stack like a blanket and keeps it safe. Hydra uses cryptographic tokens for authenticate users and request their consent, no APIs required. -The deprecated php-3.0 authentication service your intern wrote? It works with that too, don't worry. -We wrote an example with React to show you how this could look like: [React.js Identity Provider Example App](https://github.com/ory-am/hydra-idp-react). -4. **Security:** Hydra leverages the security first OAuth2 framework **[Fosite](https://github.com/ory-am/fosite)**, -encrypts important data at rest, and supports HTTP over TLS (https) out of the box. -5. **Ease of use:** Developers and Operators are human. Therefore, Hydra is easy to install and manage. Hydra does not care if you use React, Angular, or Cocoa for your user interface. -To support you even further, there are APIs available for *cryptographic key management, social log on, policy based access control, policy management, and two factor authentication (tbd)* -Hydra is packaged using [Docker](https://hub.docker.com/r/oryam/hydra/). -6. **Open Source:** Hydra is licensed Apache Version 2.0 -7. **Professional:** Hydra implements peer reviewed open standards published by [The Internet Engineering Task Force (IETF®)](https://www.ietf.org/) and the [OpenID Foundation](https://openid.net/) -and under supervision of the [LMU Teaching and Research Unit Programming and Modelling Languages](http://www.en.pms.ifi.lmu.de). No funny business. -8. **Real Time:** Operation is a lot easier with real time monitoring. Because Hydra leverages RethinkDB, you get real time monitoring for free. +Hydra has a limitations too: + +1. Hydra is not something that manages user accounts. Hydra does not offer user registration, password reset, user +login, sending confirmation emails. This is what the *Identity Provider* is responsible for. +The communication between Hydra and the Identity Provider is called [*Consent Flow*](https://ory-am.gitbooks.io/hydra/content/oauth2/consent.html). +2. If you are building a simple service for 50-100 registered users, OAuth2 and Hydra will probably be too sophisticated. +3. Hydra does currently not support the OAuth2 resource owner password credentials flow. This will change in the future and is tracked +as issue [#214](https://github.com/ory-am/hydra/issues/214). +4. Hydra has no management frontend. You must manage OAuth2 Clients and other things using the RESTful endpoints or +the command line interface. We are open to having an official Hydra Management frontend. + +OAuth2 is used in many areas, for various purposes and supported by all well known programming languages, but it is important +to understand what the vision of OAuth2 is. This non-exclusive list might help you decide, if OAuth 2.0 and Hydra are +the right fit for you. + +1. If you want to allow third-party developers accessing your APIs now or in the future, Hydra is the perfect fit. This is what an OAuth2 Provider does. +2. If you want to become a Identity Provider, like Google, Facebook or Microsoft, OpenID Connect and thus Hydra is a perfect fit. +3. Running an OAuth2 Provider works great with browser, mobile and wearable apps, as you can avoid storing user +credentials on the device, phone or wearable and revoke access tokens, and thus access privileges, at any time. Adding +OAuth2 complexity to your environment when you never plan to do (1), +might not be worth it. Our advice: write a pros/cons list. +4. If you have a lot of services and want to limit automated access (think: cronjobs) for those services, +OAuth2 might make sense for you. Example: The comment service is not allowed to read user passwords when fetching +the latest user profile updates. + +# OAuth 2.0 Case Study + +OAuth2 and OpenID Connect are tricky to understand. It is important to understand that OAuth2 is +a delegation protocol. It makes sense to use Hydra in new and existing projects. A use case covering an existing project +explains how one would use Hydra in a new one as well. So let's look at a use case! + +Let's assume we are running a ToDo List App (todo24.com). ToDo24 has a login endpoint (todo24.com/login). +The login endpoint is written in node and uses MongoDB to store user information (email + password + settings). Of course, +todo24 has other services as well: list management (todo24.com/lists/manage: close, create, move), item management (todo24.com/lists/items/manage: mark solved, add), and so on. +You are using cookies to see which user is performing the request. + +Now you decide to use OAuth2 on top of your current infrastructure. There are many reasons to do this: +* You want to open up your APIs to third-party developers. Their apps will be using OAuth2 Access Tokens to access a user's to do list. +* You want a mobile client. Because you can not store secrets on devices (they can be reverse engineered and stolen), you use OAuth2 Access Tokens instead. +* You have Cross Origin Requests. Making cookies work with Cross Origin Requests weakens or even disables important anti-CSRF measures. +* You want to write an in-browser client. This is the same case as in a mobile client (you can't store secrets in a browser). + +These are only a couple of reasons to use OAuth2. You might decide to use OAuth2 as your single source of authorization, thus maintaining +only one authorization protocol and being able to open up to third party devs in no time. With OpenID Connect, you are able to delegate authentication as well as authorization! + +Your decision is final. You want to use OAuth2 and you want Hydra to do the job. You install Hydra in your cluster using docker. +Next, you set up some exemplary OAuth2 clients. Clients can act on their own, but most of the time they need to access a user's todo lists. +To do so, the client initiates an OAuth2 request. This is where [Hydra's authentication flow](https://ory-am.gitbooks.io/hydra/content/oauth2.html#authentication-flow) comes in to play. +Before Hydra can issue an access token, we need to know WHICH user is giving consent. To do so, Hydra redirects the user agent (e.g. browser, mobile device) +to the login endpoint alongside with a challenge that contains an expiry time and other information. The login endpoint (todo24.com/login) authenticates the +user as usual, e.g. by username & password, session cookie or other means. Upon successful authentication, the login endpoint asks for the user's consent: +*"Do you want to grant MyCoolAnalyticsApp read & write access to all your todo lists? [Yes] [No]"*. Once the user clicks *Yes* and gives consent, +the login endpoint redirects back to hydra and appends something called a *consent token*. The consent token is a cryptographically signed +string that contains information about the user, specifically the user's unique id. Hydra validates the signature's trustworthiness +and issues an OAuth2 access token and optionally a refresh or OpenID token. + +Every time a request containing an access token hits a resource server (todo24.com/lists/manage), you make a request to Hydra asking who the token's +subject (the user who authorized the client to create a token on its behalf) is and whether the token is valid or not. You may optionally +ask if the token has permission to perform a certain action. \ No newline at end of file diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index b9cca8a8df2..7a266d26d1b 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,28 +1,29 @@ # Summary * [Introduction](README.md) -* [What's it good for?](what-good.md) -* [Basics](basics.md) - * [Architecture](basics/architecture.md) - * [Security](basics/security.md) - * [Interoperability](basics/interoperability.md) -* [5 Minutes Tutorial](demo.md) -* [Installation](install.md) -* Core Capabilities + * [Introduction to OAuth 2.0 and OpenID Connect](README.md#introduction-to-oauth-20-and-openid-connect) + * [Introduction to Hydra](README.md#introduction-to-hydra) + * [OAuth2 Case Study](README.md#oauth-20-case-study) +* [5 Minute Tutorial](tutorial.md) +* [Using Hydra](install.md) + * [Installing Hydra](install.md#installing-hydra) + * [Configuring Hydra](install.md#configuring-hydra) + * [Running Hydra](install.md#running-hydra) +* Understanding Hydra * [OAuth2 & OpenID Connect](oauth2.md) - * [OAuth2 Basics](oauth2/basics.md) - * [OpenID Connect Basics](oauth2/openid.md) - * [Consent Flow](oauth2/consent.md) + * [Overview](oauth2.md#overview) + * [Confirming User Consent: Consent App Flow](oauth2.md#consent-app-flow) + * [Validating Tokens: OAuth2 Token Introspection](oauth2.md#oauth2-token-introspection) * [JSON Web Keys](jwk.md) - * [Access Control](access-control.md) - * [Policy Introduction](access-control/policies.md) - * [The Warden](access-control/warden.md) - * [OAuth2 Token Introspection](access-control/introspection.md) - * [Manage Social Logins](sso.md) - * [SDK](sdk.md) - * [Go SDK](sdk/go.md) + * [Access Control Policies](access-control.md) + * [Introduction](access-control.md#introduction-to-access-control-policies) + * [The Warden](access-control.md#warden) +* [Client Libraries](sdk.md) + * [Hydra SDK for Go](sdk/go.md) +* [Contribute](contribute.md) + * [Architecture and Design](contribute.md) + * [Running Tests](contribute.md) * [FAQ](faq.md) - * [What does *"eventually consistent"* mean?](faq/consistency.md) * [Where is the HTTP API Documentation?](faq/http-api.md) * [How can I disable HTTPS for testing?](faq/disable-https.md) * [How can I import TLS certificates?](faq/https-tls-import.md) @@ -32,4 +33,5 @@ * [Why isn't the redirect url working?](faq/redirect-uri.md) * [How can I import a custom CA for RethinkDB?](faq/rethink-ca.md) * [How do I know if OAuth2 / Hydra is the right choice for me?](faq/when-use.md) - * [Operational considerations?](faq/operations.md) + * [Operational considerations](faq/operations.md) + * [How Secure is Hydra?](faq/security.md) diff --git a/docs/access-control.md b/docs/access-control.md index b3fb7f40809..fc91995e9bc 100644 --- a/docs/access-control.md +++ b/docs/access-control.md @@ -1,22 +1,75 @@ -# Access Control +# Access Control Policies -Hydra offers various access control methods. Resource providers (e.g. photo/user/asset/balance/... service) use +Besides OAuth2 Token Introspection, Hydra offers Access Control Policies using +the [Ladon](https://github.com/ory-am/ladon) framework. Access Control Policies are used by Hydra internally and exposed +via various HTTP APIs. -1. **Warden Token Validation** to validate access tokens -2. **Warden Access Control with Access Tokens** to validate access tokens and decide -if the token's subject is allowed to perform the request -3. **Warden Access Control without Access Tokens** to decide if any subject is allowed -to perform a request +## Introduction to Access Control Policies -whereas third party apps (think of a facebook app) use +Hydra's Access Control is able to answer the question: -1. **OAuth2 Token Introspection** to validate access tokens. +> **Who** is **able** to do **what** on **something** given some **context** -There are two common ways to solve access control in a distributed environment (e.g. microservices). +* **Who** An arbitrary unique subject name, for example "ken" or "printer-service.mydomain.com". +* **Able**: The effect which can be either "allow" or "deny". +* **What**: An arbitrary action name, for example "delete", "create" or "scoped:action:something". +* **Something**: An arbitrary unique resource name, for example "something", "resources.articles.1234" or some uniform + resource name like "urn:isbn:3827370191". +* **Context**: The current context containing information about the environment such as the IP Address, + request date, the resource owner name, the department ken is working in or any other information you want to pass along. + (optional) -1. Your services are behind a gateway (e.g. access control, rate limiting, and load balancer) -that does the access control for them. This is known as a "trusted network/subnet". -2. Clients (e.g. Browser) talk to your services -directly. The services are responsible for checking access privileges themselves. +To decide what the answer is, Hydra uses policy documents which can be represented as JSON. Values `actions`, `subjects` +and `resources` can use regular expressions by encapsulating the expression in `<>`, for example `<.*>`. -In both cases, you would use on of the warden endpoints. +```json +{ + "description": "One policy to rule them all.", + "subjects": ["users:<[peter|ken]>", "users:maria", "groups:admins"], + "actions" : ["delete", "<[create|update]>"], + "effect": "allow", + "resources": [ + "resources:articles:<.*>", + "resources:printer" + ], + "conditions": { + "remoteIP": { + "type": "CIDRCondition", + "options": { + "cidr": "192.168.0.1/16" + } + } + } +} +``` + +Now, Hydra is able to answer access requests like the following one: + +```json +{ + "subject": "users:peter", + "action" : "delete", + "resource": "resource:articles:ladon-introduction", + "context": { + "remoteIP": "192.168.0.5" + } +} +``` + +In this case, the access request will be allowed: + +1. `users:peter` matches `"subjects": ["users:<[peter|ken]>", "users:maria", "groups:admins"]`, as would `users:ken`, `users:maria` and `group:admins`. +2. `delete` matches `"actions" : ["delete", "<[create|update]>"]` as would `update` and `create` +3. `resource:articles:ladon-introduction` matches `"resources": ["resources:articles:<.*>", "resources:printer"],` +4. `"remoteIP": "192.168.0.5"` matches the [`CIDRCondition`](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) +condition that was configured for the field `remoteIP`. + +## Access Control Decisions: The Warden + +The warden is a HTTP API allowing you to perform these access requests. +The warden knows two endpoints: + +* `/warden/allowed`: Check if a subject is allowed to do something. +* `/warden/token/allowed`: Check if the subject of a token is allowed to do something. + +Both endpoints use policies to compute the result and are documented in the [HTTP API Documentation](http://docs.hdyra.apiary.io/#reference/warden:-access-control). \ No newline at end of file diff --git a/docs/access-control/introspection.md b/docs/access-control/introspection.md deleted file mode 100644 index 831361f541c..00000000000 --- a/docs/access-control/introspection.md +++ /dev/null @@ -1,14 +0,0 @@ -# OAuth2 Token Introspection - -OAuth2 Token Introspection is an [IETF](https://tools.ietf.org/html/rfc7662) standard. -It defines a method for a protected resource to query -an OAuth 2.0 authorization server to determine the active state of an -OAuth 2.0 token and to determine meta-information about this token. -OAuth 2.0 deployments can use this method to convey information about -the authorization context of the token from the authorization server -to the protected resource. - -In order to make a successful Token Introspection request, the audience of the access token you are introspecting -*must* match the subject of the access token you are using to access the introspection endpoint. - -The Token Introspection endpoint is documented in more detail [here](http://docs.hdyra.apiary.io/#reference/oauth2/oauth2-token-introspection). \ No newline at end of file diff --git a/docs/contribute.md b/docs/contribute.md new file mode 100644 index 00000000000..ae1b2bd0c51 --- /dev/null +++ b/docs/contribute.md @@ -0,0 +1,7 @@ +# Contribute + +This section is work in progress. + +## Architecture and Design + +## Running Tests \ No newline at end of file diff --git a/docs/demo.md b/docs/demo.md deleted file mode 100644 index 451122a6dfe..00000000000 --- a/docs/demo.md +++ /dev/null @@ -1,88 +0,0 @@ -### 5 minutes tutorial: Run your very own OAuth2 environment - -In this example, you will set up Hydra, a RethinkDB instance and an exemplary identity provider written in React using docker compose. It will take you about 5 minutes to get complete this tutorial. - -OAuth2 Flow - -Running the example - -Install the [CLI and Docker](https://github.com/ory-am/hydra#installation). Make sure you install Docker Compose as well. - -We will use a dummy password as the system secret: `SYSTEM_SECRET=passwordtutorialpasswordtutorial`. Use a very secure secret in production. - -``` -$ go get github.com/ory-am/hydra -$ cd $GOPATH/src/github.com/ory-am/hydra -$ SYSTEM_SECRET=passwordtutorial DOCKER_IP=localhost docker-compose up --build -Starting hydra_rethinkdb_1 -[...] -mhydra | mtime="2016-05-17T18:09:28Z" level=warning msg="Generated system secret: MnjFP5eLIr60h?hLI1h-!<4(TlWjAHX7" -[...] -mhydra | mtime="2016-05-17T18:09:29Z" level=warning msg="client_id: d9227bd5-5d47-4557-957d-2fd3bee11035" -mhydra | mtime="2016-05-17T18:09:29Z" level=warning msg="client_secret: ,IvxGt02uNjv1ur9" -[...] -``` - -You now have a running hydra docker container! Additionally, a RethinkDB image was deployed as well as a consent app. - -Hydra can be managed using the Hydra Command Line Interface (CLI) client. This client has to log on before it is allowed to do anything. When Hydra host process detects a new installation, a new temporary root client is created and its credentials are printed to the container logs. - -``` -mhydra | mtime="2016-05-17T18:09:29Z" level=warning msg="client_id: d9227bd5-5d47-4557-957d-2fd3bee11035" -mhydra | mtime="2016-05-17T18:09:29Z" level=warning msg="client_secret: ,IvxGt02uNjv1ur9" -``` - -The system secret is a global secret assigned to every hydra instance. It is used to encrypt data at rest. You can -set the system secret through the `$SYSTEM_SECRET` environment variable. When no secret is set, hydra generates one: - -``` -time="2016-05-15T14:56:34Z" level=warning msg="Generated system secret: (.UL_&77zy8/v9Tp7.ggn>EE&rhnOzdt1 -``` - -**Important note:** if no certificate is provided, Hydra uses self-signed TLS certificates for HTTPS. This should -never be done in production. To skip the TLS verification step on the client, provide the `--skip-tls-verify` flag. The tutorial is using self-signed TLS certificates and you must use the `--skip-tls-verify` tag everywhere. - -Now, let us issue an access token for your OAuth2 client! - -``` -$ hydra token client --skip-tls-verify -JLbnRS9GQmzUBT4x7ESNw0kj2wc0ffbMwOv3QQZW4eI.qkP-IQXn6guoFew8TvaMFUD-SnAyT8GmWuqGi3wuWXg -``` - -Let's try this with the authorize code grant! - -``` -$ hydra token user --skip-tls-verify -If your browser does not open automatically, navigate to: https://192.168.99.100:4444/oauth2/... -Setting up callback listener on http://localhost:4445/callback -Press ctrl + c on Linux / Windows or cmd + c on OSX to end the process. -``` - -Great! You installed hydra, connected the CLI, created a client and completed two authentication flows! diff --git a/docs/faq/consistency.md b/docs/faq/consistency.md deleted file mode 100644 index e8e6bbd41ef..00000000000 --- a/docs/faq/consistency.md +++ /dev/null @@ -1,5 +0,0 @@ -# Eventually consistent - -Using hydra with RethinkDB implies eventual consistency on all endpoints, except `/oauth2/auth` and `/oauth2/token`. -Eventual consistent data is usually not immediately available. This is dependent on the network latency between Hydra -and RethinkDB. \ No newline at end of file diff --git a/docs/faq/oauth2-error.md b/docs/faq/oauth2-error.md deleted file mode 100644 index ae51b974882..00000000000 --- a/docs/faq/oauth2-error.md +++ /dev/null @@ -1,4 +0,0 @@ -# What will happen if an error occurs during an OAuth2 flow? - -The user agent will either, according to spec, be redirected to the OAuth2 client who initiated the request, if possible. If not, the user agent will be redirected to the identity provider -endpoint and an `error` and `error_description` query parameter will be appended to it's URL. diff --git a/docs/basics/security.md b/docs/faq/security.md similarity index 100% rename from docs/basics/security.md rename to docs/faq/security.md diff --git a/docs/faq/when-use.md b/docs/faq/when-use.md deleted file mode 100644 index 30c62b53cb0..00000000000 --- a/docs/faq/when-use.md +++ /dev/null @@ -1,34 +0,0 @@ -# How do I know if OAuth2 / Hydra is the right choice for me? - -OAuth2 and OpenID Connect are tricky to understand. It is important to understand that OAuth2 is -a delegation protocol. It makes sense to use Hydra in new and existing projects. A use case covering an existing project -explains how one would use Hydra in a new one as well. So let's look at a use case! - -Let's assume we are running a ToDo List App (todo24.com). ToDo24 has a login endpoint (todo24.com/login). -The login endpoint is written in node and uses MongoDB to store user information (email + password + settings). Of course, -todo24 has other services as well: list management (todo24.com/lists/manage: close, create, move), item management (todo24.com/lists/items/manage: mark solved, add), and so on. -You are using cookies to see which user is performing the request. - -Now you decide to use OAuth2 on top of your current infrastructure. There are many reasons to do this: -* You want to open up your APIs to third-party developers. Their apps will be using OAuth2 Access Tokens to access a user's to do list. -* You want a mobile client. Because you can not store secrets on devices (they can be reverse engineered and stolen), you use OAuth2 Access Tokens instead. -* You have Cross Origin Requests. Making cookies work with Cross Origin Requests weakens or even disables important anti-CSRF measures. -* You want to write an in-browser client. This is the same case as in a mobile client (you can't store secrets in a browser). - -These are only a couple of reasons to use OAuth2. You might decide to use OAuth2 as your single source of authorization, thus maintaining -only one authorization protocol and being able to open up to third party devs in no time. With OpenID Connect, you are able to delegate authentication as well as authorization! - -Your decision is final. You want to use OAuth2 and you want Hydra to do the job. You install Hydra in your cluster using docker. -Next, you set up some exemplary OAuth2 clients. Clients can act on their own, but most of the time they need to access a user's todo lists. -To do so, the client initiates an OAuth2 request. This is where [Hydra's authentication flow](https://ory-am.gitbooks.io/hydra/content/oauth2.html#authentication-flow) comes in to play. -Before Hydra can issue an access token, we need to know WHICH user is giving consent. To do so, Hydra redirects the user agent (e.g. browser, mobile device) -to the login endpoint alongside with a challenge that contains an expiry time and other information. The login endpoint (todo24.com/login) authenticates the -user as usual, e.g. by username & password, session cookie or other means. Upon successful authentication, the login endpoint asks for the user's consent: -*"Do you want to grant MyCoolAnalyticsApp read & write access to all your todo lists? [Yes] [No]"*. Once the user clicks *Yes* and gives consent, -the login endpoint redirects back to hydra and appends something called a *consent token*. The consent token is a cryptographically signed -string that contains information about the user, specifically the user's unique id. Hydra validates the signature's trustworthiness -and issues an OAuth2 access token and optionally a refresh or OpenID token. - -Every time a request containing an access token hits a resource server (todo24.com/lists/manage), you make a request to Hydra asking who the token's -subject (the user who authorized the client to create a token on its behalf) is and whether the token is valid or not. You may optionally -ask if the token has permission to perform a certain action. \ No newline at end of file diff --git a/docs/install.md b/docs/install.md index 13e96b93561..fa6f19cca74 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,33 +1,35 @@ -# Installation +# Installing, Configuring and Running Hydra -There are various ways of installing hydra on your system. +Before starting with this section, please check out the [tutorial](./demo.md). It will teach you the most important flows +and settings for Hydra. -## Download binaries +## Installing Hydra -The client and server **binaries are downloadable at [releases](https://github.com/ory-am/hydra/releases)**. -There is currently no installer available. You have to add the hydra binary to the PATH environment variable yourself or put -the binary in a location that is already in your path (`/usr/bin`, ...). -If you do not understand what that all of this means, ask in our [chat channel](https://gitter.im/ory-am/hydra). We are happy to help. +You can install Hydra using multiple methods. -## Using Docker +### Using Docker -**Starting the host** is easiest with docker. The host process handles HTTP requests and is backed by a database. +Installing, configuring and running Hydra is easiest with docker. The host process +handles HTTP requests and is backed by a database. Read how to install docker on [Linux](https://docs.docker.com/linux/), [OSX](https://docs.docker.com/mac/) or [Windows](https://docs.docker.com/windows/). Hydra is available on [Docker Hub](https://hub.docker.com/r/oryam/hydra/). -You can use Hydra without a database, but be aware that restarting, scaling -or stopping the container will **lose all data**: +In this minimalistic example, we will use Hydra without a database. Bee aware that restarting, scaling +or stopping the container will **lose all the data**. ``` $ docker run -d -p 4444:4444 oryam/hydra --name my-hydra ec91228cb105db315553499c81918258f52cee9636ea2a4821bdb8226872f54b ``` -**Using the client command line interface** can be achieve by ssh'ing into the hydra container +Now, you should be able to open [https://localhost:4444](https://localhost:4444). If asked, accept the self signed +certificate in your browser. + +**Using the client command line interface** can be achieved by ssh'ing into the hydra container and execute the hydra command from there: ``` -$ docker exec -i -t /bin/bash +$ docker exec -i -t /bin/bash # e.g. docker exec -i -t ec91228 /bin/bash root@ec91228cb105:/go/src/github.com/ory-am/hydra# hydra @@ -36,16 +38,167 @@ Hydra is a twelve factor OAuth2 and OpenID Connect provider [...] ``` -## Building from source +### Download Binaries + +The client and server **binaries are downloadable at the [releases tab](https://github.com/ory-am/hydra/releases)**. +There is currently no installer available. You have to add the hydra binary to the PATH environment variable yourself or put +the binary in a location that is already in your path (`/usr/bin`, ...). +If you do not understand what that all of this means, ask in our [chat channel](https://gitter.im/ory-am/hydra). We are happy to help. + +Once installed, you should be able to run: + +``` +$ hydra help +Hydra is a cloud native high throughput OAuth2 and OpenID Connect provider + +Usage: + hydra [command] + +Available Commands: + clients Manage OAuth2 clients +... +``` + +### Build from source If you wish to compile hydra yourself, you need to install and set up [Go 1.5+](https://golang.org/) and add `$GOPATH/bin` to your `$PATH`. To do so, run the following commands in a shell (bash, sh, cmd.exe, ...): ``` -go get github.com/ory-am/hydra -go get github.com/Masterminds/glide -cd $GOPATH/src/github.com/ory-am/hydra -glide install -go install github.com/ory-am/hydra -hydra +$ go get github.com/ory-am/hydra +$ go get github.com/Masterminds/glide +$ cd $GOPATH/src/github.com/ory-am/hydra +$ glide install +$ go install github.com/ory-am/hydra +$ hydra +Hydra is a cloud native high throughput OAuth2 and OpenID Connect provider + +Usage: + hydra [command] + +Available Commands: + clients Manage OAuth2 clients +... +``` + +## Configuring Hydra + +Running the default Hydra environment is as easy as: + +``` +$ hydra host +time="2016-10-13T10:04:01+02:00" level=info msg="DATABASE_URL not set, connecting to ephermal in-memory database." +time="2016-10-13T10:04:01+02:00" level=warning msg="Expected system secret to be at least 32 characters long, got 0 characters." +time="2016-10-13T10:04:01+02:00" level=info msg="Generating a random system secret..." +[...] +``` + +Hydra relies on a third party for storing data, such as Postgres or MySQL (officially supported) and RethinkDB +(community supported). If no storage is set, data will be written to memory and is lost when the process is killed. +The `hydra help host` command will give you an insight into the different configuration settings. The following section +might be outdated and is only for demonstration purposes, please run the `hydra help host` command on your local +machine to get the latest documentation: + +``` +$ hydra help host +Starts all HTTP/2 APIs and connects to a database backend. + +This command exposes a variety of controls via environment variables. You can +set environments using "export KEY=VALUE" (Linux/macOS) or "set KEY=VALUE" (Windows). On Linux, +you can also set environments by prepending key value pairs: "KEY=VALUE KEY2=VALUE2 hydra" + +All possible controls are listed below. The host process additionally exposes a few flags, which are listed below +the controls section. + +CORE CONTROLS +============= + +- DATABASE_URL: A URL to a persistent backend. Hydra supports various backends: +[...] +``` + +It is quite common to run hydra with the following options: + +``` +$ export DATABASE_URL=postgres://foo:bar@localhost/hydra +$ export SYSTEM_SECRET=some-very-random-secret-$§123 +$ hydra host ``` + +If you want to check out hydra locally, we recommend setting these options to ease things up. ` --dangerous-auto-logon` +will write the administrator's credentials directly to `~/.hydra.yml`, no `hydra connect` required. The section option +`--dangerous-force-http` disables https and serves Hydra over http instead: + +``` +$ hydra host --dangerous-auto-logon --dangerous-force-http +``` + +## Running Hydra + +On first run, Hydra initializes various settings: + +``` +$ hydra host +[...] +mtime="2016-05-17T18:09:28Z" level=warning msg="Generated system secret: MnjFP5eLIr60h?hLI1h-!<4(TlWjAHX7" +[...] +time="2016-10-25T09:58:54+02:00" level=info msg="Key pair for signing hydra.openid.id-token is missing. Creating new one." +time="2016-10-25T09:58:56+02:00" level=info msg="Key pair for signing hydra.consent.response is missing. Creating new one." +time="2016-10-25T09:59:02+02:00" level=info msg="Key pair for signing hydra.consent.challenge is missing. Creating new one." +[...] +time="2016-10-25T09:59:04+02:00" level=warning msg="No clients were found. Creating a temporary root client..." +mtime="2016-05-17T18:09:29Z" level=warning msg="client_id: d9227bd5-5d47-4557-957d-2fd3bee11035" +mtime="2016-05-17T18:09:29Z" level=warning msg="client_secret: ,IvxGt02uNjv1ur9" +[...] +time="2016-10-25T09:59:04+02:00" level=warning msg="No TLS Key / Certificate for HTTPS found. Generating self-signed certificate." +``` + +1. If no system secret was given, a random one is generated +2. Cryptographic keys for JWT signing are being generated +3. If the OAuth 2.0 Client database table is empty, a new root client with random credentials is created. Root clients +have access to all APIs, OAuth 2.0 flows and are allowed to do everything. If the `FORCE_ROOT_CLIENT_CREDENTIALS` environment +is set, those credentials will be used instead. +4. A self signed certificate for serving HTTP over TLS is created. + +Hydra can be managed using the Hydra Command Line Interface (CLI) client. This client has to log on before it is +allowed to do anything. When Hydra host process detects a new installation, a new temporary root client is +created and its credentials are printed to the container logs. + +``` +mhydra | mtime="2016-05-17T18:09:29Z" level=warning msg="client_id: d9227bd5-5d47-4557-957d-2fd3bee11035" +mhydra | mtime="2016-05-17T18:09:29Z" level=warning msg="client_secret: ,IvxGt02uNjv1ur9" +``` + +The system secret is a global secret assigned to every hydra instance. It is used to encrypt data at rest. You can +set the system secret through the `SYSTEM_SECRET` environment variable. When no secret is set, hydra generates one: + +``` +time="2016-05-15T14:56:34Z" level=warning msg="Generated system secret: (.UL_&77zy8/v9Tp7.ggn>EE&rhnOzdt1 +``` + +Now, let us issue an access token for your OAuth2 client! + +``` +$ hydra token client +JLbnRS9GQmzUBT4x7ESNw0kj2wc0ffbMwOv3QQZW4eI.qkP-IQXn6guoFew8TvaMFUD-SnAyT8GmWuqGi3wuWXg +``` + +Great! You installed hydra, connected the CLI, created a client and completed two authentication flows! diff --git a/docs/jwk.md b/docs/jwk.md index 78ad8209470..f3356087c66 100644 --- a/docs/jwk.md +++ b/docs/jwk.md @@ -1,6 +1,7 @@ # JSON Web Keys (JWK) -A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data structure that represents a cryptographic key and is specified at [IETF RFC7517](https://tools.ietf.org/html/rfc7517). If you've heard of PEM files... +A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data structure that represents a cryptographic key and is +specified at [IETF RFC7517](https://tools.ietf.org/html/rfc7517). If you've heard of PEM files... ``` -----BEGIN ENCRYPTED PRIVATE KEY----- @@ -34,16 +35,16 @@ GEs= ``` Hydra offers an API for generating and managing JWKs, the [JSON Web Keys API](http://docs.hdyra.apiary.io/#reference/json-web-keys-jwk). -When using persistent storage backends, the keys are encrypted at rest using AES256-GCM and the **system secret**. -The **system secret** is generated by default and overridden by the environment variable **SYSTEM_SECRET**. +When using persistent storage backends, the keys are encrypted at rest using AES256-GCM and *the system secret*. +The system secret is generated by default and overridden by the environment variable `SYSTEM_SECRET`. -JWKs are well supported amongst all languages. An HTTPS API takes away the pain of managing -certificates and keeps them in a safe place. **When transporting private keys over the network you -MUST encrypt ALL related traffic.** +JWKs are well supported amongst all languages. This endpoint helps you managing +certificates, private, public and symmetric keys. It is important to never transport keys over insecure channels such as http. -Please read the [API Documentation](http://docs.hdyra.apiary.io/#reference/json-web-keys-jwk) for API details. +The [JWK REST API Documentation](http://docs.hdyra.apiary.io/#reference/json-web-keys-jwk) will give you details on the +various endpoints. -## What JWK sets exists by default? +## Auto-generated JWKs Hydra generates a couple of JSON Web Keys in order to operate correctly: @@ -57,5 +58,3 @@ Hydra generates a couple of JSON Web Keys in order to operate correctly: * `http://localhost:4444/keys/hydra.consent.response/public`: The public key which you can use to validate the consent response. * `http://localhost:4444/keys/hydra.consent.response/private`: The private key used for signing the consent response. * `http://localhost:4444/keys/https-tls`: A RSA public/private key pair and a certificate for signing HTTP over TLS. - -You can read and update those keys using the [HTTP REST API](http://docs.hdyra.apiary.io/#reference/json-web-keys-jwk). \ No newline at end of file diff --git a/docs/oauth2.md b/docs/oauth2.md index 1712dfe10d8..0b73fb18955 100644 --- a/docs/oauth2.md +++ b/docs/oauth2.md @@ -1,4 +1,199 @@ -# OAuth2 +# OAuth 2.0 & OpenID Connect -This section covers some basic OAuth2 and OpenID Connect concepts and shows you how to integrate Hydra with your authentication -flow. +If you are new to OAuth2, please read the [Introduction to OAuth 2.0 and OpenID Connect](README.md#introduction-to-oauth-20-and-openid-connect) +first. + +## Overview + +This section defines a glossary, provides additional information on OpenID Connect and introduces OAuth 2.0 Clients. + +### Glossary + +1. **The resource owner** is the user who authorizes an application to access their account. The application's access to +the user's account is limited to the "scope" of the authorization granted (e.g. read or write access). +2. **Authorization Server (Hydra)** verifies the identity of the user and issues access tokens to the *client application*. +3. **Client** is the *application* that wants to access the user's account. Before it may do so, it must be authorized +by the user. +4. **Identity Provider** contains a log in user interface and a database of all your users. To integrate Hydra, +you must modify the Identity Provider. It mus be able to generate consent tokens and ask for the user's consent. +5. **User Agent** is usually the resource owner's browser. +6. **Consent App** is an app (e.g. NodeJS) that is able to receive consent challenges and create consent tokens. +It must verify the identity of the user that is giving the consent. This can be achieved using Cookie Auth, +HTTP Basic Auth, Login HTML Form, or any other mean of authentication. Upon authentication, the user must be asked +if he consents to allowing the client access to his resources. + +Examples: +1. Peter wants to give MyPhotoBook access to his Dropbox. Peter is the resource owner. +2. The Authorization Server (Hydra) is responsible for managing the access request fom MyPhotoBook. Hydra handles +the communication between the resource owner, the consent endpoint and the client. Hydra is the authorization server. +In this case, Dropbox would be the one who uses Hydra. +3. MyPhotoBook is the client and was issued an id and a password by Hydra. MyPhotoBook uses these credentials +to talk with Hydra. +4. Dropbox has a database and a frontend that allow their users to log in, using their username and password. +This is what an Identity Provider does. +5. The User Agent is Peter's FireFox. +6. The Consent App is a frontend app that asks the user if he is willing to give MyPhotoBook access to his pictures stored +on Dropbox. It is responsible to tell Hydra if the user accepted or rejected the request by MyPhotoBook. The Consent App +uses the Identity Provider to authenticate peter, for example by using cookies or presenting a user/password login view. + +### OpenID Connect 1.0 + +If you are new to OpenID Connect, please read the [Introduction to OAuth 2.0 and OpenID Connect](README.md#introduction-to-oauth-20-and-openid-connect) +first. + +Hydra uses the [JSON Web Key Manager](./jwk.md) to retrieve the +key pair `hydra.openid.id-token` for signing ID tokens. You can use that endpoint to retrieve the public key for verification, +has Hydra is not supporting OpenID Connect Discovery yet. + +### OAuth 2.0 Clients + +You can manage *OAuth 2.0 clients* using the cli or the HTTP REST API. + +* **CLI:** `hydra clients -h` +* **REST:** Read the [API Docs](http://docs.hdyra.apiary.io/#reference/oauth2-clients) + +## Consent App Flow + +Hydra does not include user authentication and things like lost password, user registration or user activation. +The consent app flow is used to let Hydra identify who resource owner is. In abstract, the consent flow looks like this: + +![](../images/consent.png) + +1. A *client* application (app in browser in laptop) requests an access token from a resource owner: +`https://hydra.myapp.com/oauth2/auth?client_id=c3b49cf0-88e4-4faa-9489-28d5b8957858&response_type=code&scope=core+hydra&state=vboeidlizlxrywkwlsgeggff&nonce=tedgziijemvninkuotcuuiof`. +2. Hydra generates a consent challenge and forwards the *user agent* (browser in laptop) to the *consent endpoint*: +`https://login.myapp.com/?challenge=eyJhbGciOiJSUzI1N...`. +3. The *consent endpoint* verifies the resource owner's identity (e.g. cookie, username/password login form, ...). +The consent challenge is then decoded and the information extracted. It is used to show the consent screen: `Do you want to grant _my cool app_ access to all your private data? [Yes] [No]` +4. When consent is given, the *consent endpoint* generates a consent response token and redirects the user +agent (browser in laptop) back to hydra: +`https://hydra.myapp.com/oauth2/auth?client_id=c3b49cf0-88e4-4faa-9489-28d5b8957858&response_type=code&scope=core+hydra&state=vboeidlizlxrywkwlsgeggff&nonce=tedgziijemvninkuotcuuiof&consent=eyJhbGciOiJSU...`. +5. Hydra validates the consent response token and issues the access token to the *user agent*. + +### Consent App Flow Example + +In this section we assume that hydra runs on `https://192.168.99.100:4444` and our +consent app on `https:/192.168.99.100:3000`. + +All user-based OAuth2 requests, including the OpenID Connect workflow, begin at the `/oauth2/auth` endpoint. +For example: `https://192.168.99.100:4444/oauth2/auth?client_id=c3b49cf0-88e4-4faa-9489-28d5b8957858&response_type=code&scope=core+hydra&state=wewuphkgywhtldsmainefkyx&nonce=uqfjjzftqpjccdvxltaposri` + +If the request includes a valid redirect uri and a valid client id, hydra redirects the user to then consent url. +The consent url can be set using the `CONSENT_URL` environment variable. + +Let's set the `CONSENT_URL` to `https:/192.168.99.100:3000/consent`, where a NodeJS application +is running (the *consent app*). Next, Hydra appends a consent challenge to the consent url and redirects the user to it. +For example: `http://192.168.99.100:3000/consent/?challenge=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJjM2I0OWNmMC04OGU0LTRmYWEtOTQ4OS0yOGQ1Yjg5NTc4NTgiLCJleHAiOjE0NjQ1MTU0ODIsImp0aSI6IjNmYWRlN2NjLTdlYTItNGViMi05MGI1LWY5OTUwNTI4MzgyOSIsInJlZGlyIjoiaHR0cHM6Ly8xOTIuMTY4Ljk5LjEwMDo0NDQ0L29hdXRoMi9hdXRoP2NsaWVudF9pZD1jM2I0OWNmMC04OGU0LTRmYWEtOTQ4OS0yOGQ1Yjg5NTc4NThcdTAwMjZyZXNwb25zZV90eXBlPWNvZGVcdTAwMjZzY29wZT1jb3JlK2h5ZHJhXHUwMDI2c3RhdGU9d2V3dXBoa2d5d2h0bGRzbWFpbmVma3l4XHUwMDI2bm9uY2U9dXFmamp6ZnRxcGpjY2R2eGx0YXBvc3JpIiwic2NwIjpbImNvcmUiLCJoeWRyYSJdfQ.KpLBotIEE4izVSAjLOeCCfm_wYZ7UWSCA81akr6Ci1yycKs8e_bhBYdSThy8JW3bAvofNcZ0v48ov9KxZVegWm8GuNbBEcNvKeiyW_8PiJXWE92YsMv-tDIL3VFPOp0469FmDLsSg5ohsFj5S89FzykNYfVxLPBAFcAS_JElWbo` + +The consent challenge is a signed RSA-SHA 256 (RS256) [JSON Web Token](https://tools.ietf.org/html/rfc7519) and contains +the following claims: + + +``` +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJjM2I0OWNmMC04OGU0LTRmYWEtOTQ4OS0yOGQ1Yjg5NTc4NTgiLCJleHAiOjE0NjQ1MTUwOTksImp0aSI6IjY0YzRmNzllLWUwMTYtNDViOC04YzBlLWQ5NmM2NzFjMWU4YSIsInJlZGlyIjoiaHR0cHM6Ly8xOTIuMTY4Ljk5LjEwMDo0NDQ0L29hdXRoMi9hdXRoP2NsaWVudF9pZD1jM2I0OWNmMC04OGU0LTRmYWEtOTQ4OS0yOGQ1Yjg5NTc4NThcdTAwMjZyZXNwb25zZV90eXBlPWNvZGVcdTAwMjZzY29wZT1jb3JlK2h5ZHJhXHUwMDI2c3RhdGU9bXlobnhxbXd6aHRleWN3ZW92Ymxzd3dqXHUwMDI2bm9uY2U9Z21tc3V2dHNidG9ldW1lb2hlc3p0c2hnIiwic2NwIjpbImNvcmUiLCJoeWRyYSJdfQ.v4K1-AuT5Uwu1DRNvdf7SwjjPT8KO97thRYa3pDWzjBLyjkCNvgp0P5V0oA3XqRutoFpYx4AtQyz0bY7n3XcPE7ZQ2nBWTBnZ04GzWbxcJNFhBvgc_jiQBECebdxN29kgxHoU0frtVDcz6Uur468nBa9D_BDBpN-KgEBsI5Hjhc + +{ + "aud": "c3b49cf0-88e4-4faa-9489-28d5b8957858", + "exp": 1464515099, + "jti": "64c4f79e-e016-45b8-8c0e-d96c671c1e8a", + "redir": "https://192.168.99.100:4444/oauth2/auth?client_id=c3b49cf0-88e4-4faa-9489-28d5b8957858&response_type=code&scope=core+hydra&state=myhnxqmwzhteycweovblswwj&nonce=gmmsuvtsbtoeumeohesztshg", + "scp": [ + "core", + "hydra" + ] +} +``` + +The challenge claims are: +* **jti:** A unique id. +* **scp:** The requested scopes, e.g. `["blog.readall", "blog.writeall"]` +* **aud:** The client id that initiated the request. You can fetch client data using the [OAuth2 Client API](http://docs.hdyra.apiary.io/#reference/oauth2/manage-the-oauth2-client-collection). +* **exp:** The challenge's expiry date. Consent endpoints must not accept challenges that have expired. +* **redir:** Where the consent endpoint should redirect the user agent to, once consent is given. + +Hydra signs the consent response token with a key called `hydra.consent.challenge`. +The public key can be looked up via the [Key Manager](https://ory-am.gitbooks.io/hydra/content/jwk.html): + +``` +https://192.168.99.100:4444/keys/hydra.consent.challenge/public +``` + +Next, the consent-app must check if the user is authenticated. This can be done by e.g. using a session cookie. +If the user is not authenticate, he must be challenged to provide valid credentials through e.g. a HTML form. +The consent-app could use LDAP, MySQL, RethinkDB or any other backend to store and verify the credentials. + +Upon user authentication, the consent-app must ask for the user's consent. This could look like: + +> _That super useful service app_ would like to: +> * Know who you are +> * View your extended profile info +> * Get read access to all your cloud pictures +> +> [Deny] - [Allow] + +If the user clicks *Allow*, the consent-app redirects him back to the *redir* claim value. The consent-app appends +a signed consent response token to the URL: + +``` +https://192.168.99.100:4444/oauth2/auth?client_id=c3b49cf0-88e4-4faa-9489-28d5b8957858&response_type=code&scope=core+hydra&state=myhnxqmwzhteycweovblswwj&nonce=gmmsuvtsbtoeumeohesztshg&consent=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJjM2I0OWNmMC04OGU0LTRmYWEtOTQ4OS0yOGQ1Yjg5NTc4NTgiLCJleHAiOjE0NjQ1MTUwOTksInNjcCI6WyJjb3JlIiwiaHlkcmEiXSwic3ViIjoiam9obi5kb2VAbWUuY29tIiwiaWF0IjoxNDY0NTExNTE1fQ.tX5TKdP9hHCgPbqBzKIYMjJVwqOdxf5ACScmQ6t20Qteo8AYEfavGwq8KxRF1Oz_otcQDdZY--jcl1caom0yT2eTvj1d9E2Hs7eXmYuW_xF9pTpmDwJnrcOlONFKsNZN97n41qprzMrsX5ez0T5AcopGwpPMxKhwGDSXq9CQgQU +``` + +The consent response token is a RSA-SHA 256 (RS256) signed [JSON Web Token](https://tools.ietf.org/html/rfc7519) +that contains the following claims: + +``` +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJjM2I0OWNmMC04OGU0LTRmYWEtOTQ4OS0yOGQ1Yjg5NTc4NTgiLCJleHAiOjE0NjQ1MTUwOTksInNjcCI6WyJjb3JlIiwiaHlkcmEiXSwic3ViIjoiam9obi5kb2VAbWUuY29tIiwiaWF0IjoxNDY0NTExNTE1fQ.tX5TKdP9hHCgPbqBzKIYMjJVwqOdxf5ACScmQ6t20Qteo8AYEfavGwq8KxRF1Oz_otcQDdZY--jcl1caom0yT2eTvj1d9E2Hs7eXmYuW_xF9pTpmDwJnrcOlONFKsNZN97n41qprzMrsX5ez0T5AcopGwpPMxKhwGDSXq9CQgQU + +{ + "aud": "c3b49cf0-88e4-4faa-9489-28d5b8957858", + "exp": 1464515099, + "scp": [ + "core", + "hydra" + ], + "sub": "john.doe@me.com", + "iat": 1464511515, + "id_ext": { "foo": "bar" }, + "at_ext": { "baz": true } +} +``` + +The consent claims are: +* **jti:** A unique id. +* **scp:** The scopes the user opted in to *grant* access to, e.g. only `["blog.readall"]`. +* **aud:** The client id that initiated the OAuth2 request. You can fetch +client data using the [OAuth2 Client API](http://docs.hdyra.apiary.io/#reference/oauth2/manage-the-oauth2-client-collection). +* **exp:** The expiry date of this token. Use very short lifespans (< 5 min). +* **iat:** The tokens issuance time. +* **id_ext:** If set, pass this extra data to the id token. This data is not available at OAuth2 Token Introspection + nor at the warden endpoints. *(optional)* +* **at_ext:** If set, pass this extra data to the access token session. You can retrieve the data +by using OAuth2 Token Introspection or the warden endpoints. *(optional)* + +Hydra validates the consent response token with consent-app's public key. The public +key must be stored in the [JSON Web Key Manager](./jwk.md) +at `https://localhost:4444/keys/hydra.consent.response/public` + +If you want, you can use the Key Manager to store and retrieve private keys as well. When Hydra boots for the first time, +a private/public `hydra.consent.response` keypair is created. +You can that keypair to sign consent response tokens. The private key is available at +`https://localhost:4444/keys/asymmetric/hydra.consent.response/private`. + +### Error Handling during Consent App Flow + +Hydra follows the OAuth 2.0 error response specifications. Some errors however must be handled by the consent app. +In the case of such an error, the user agent will be redirected to the consent app +endpoint and an `error` and `error_description` query parameter will be appended to the URL. + +# OAuth2 Token Introspection + +OAuth2 Token Introspection is an [IETF](https://tools.ietf.org/html/rfc7662) standard. +It defines a method for a protected resource to query +an OAuth 2.0 authorization server to determine the active state of an +OAuth 2.0 token and to determine meta-information about this token. +OAuth 2.0 deployments can use this method to convey information about +the authorization context of the token from the authorization server +to the protected resource. + +The Token Introspection endpoint is documented in the +[API Docs](http://docs.hdyra.apiary.io/#reference/oauth2/oauth2-token-introspection). \ No newline at end of file diff --git a/docs/oauth2/basics.md b/docs/oauth2/basics.md deleted file mode 100644 index bcb86123663..00000000000 --- a/docs/oauth2/basics.md +++ /dev/null @@ -1,48 +0,0 @@ -# OAuth2 Basics - -[This introduction was taken from the Digital Ocean Blog.](https://www.digitalocean.com/community/tutorials/an-introduction-to-oauth-2) - -**OAuth 2** is an authorization framework that enables applications to obtain limited access to user accounts on an -HTTP service, such as Facebook, GitHub, and Hydra. It works by delegating user authentication to the service that hosts -the user account, and authorizing third-party applications to access the user account. OAuth 2 provides authorization -flows for web and desktop applications, and mobile devices. - -![](../images/abstract_flow.png) - -Here is a more detailed explanation of the steps in the diagram: - -* The application requests authorization to access service resources from the user -* If the user authorized the request, the application receives an authorization grant -* The application requests an access token from the authorization server (API) by presenting authentication of its own -identity, and the authorization grant. -* If the application identity is authenticated and the authorization grant is valid, the authorization server (API) -issues an access token to the application. Authorization is complete. -* The application requests the resource from the resource server (API) and presents the access token for authentication. -* If the access token is valid, the resource server (API) serves the resource to the application. - -The actual flow of this process will differ depending on the authorization grant type in use, but this is the general idea. - -Read more on OAuth2 on [the Digital Ocean Blog](https://www.digitalocean.com/community/tutorials/an-introduction-to-oauth-2). -We also recommend reading [API Security: Deep Dive into OAuth and OpenID Connect](http://nordicapis.com/api-security-oauth-openid-connect-depth/). - -**Glossary** -* **The resource owner** is the user who authorizes an application to access their account. The application's access to -the user's account is limited to the "scope" of the authorization granted (e.g. read or write access). -* **Authorization Server (Hydra)** verifies the identity of the user and issues access tokens to the *client application*. -* **Client** is the *application* that wants to access the user's account. Before it may do so, it must be authorized -by the user. -* **Identity Provider** contains a log in user interface and a database of all your users. To integrate Hydra, -you must modify the Identity Provider. It mus be able to generate consent tokens and ask for the user's consent. -* **User Agent** is usually the resource owner's browser. -* **Consent Endpoint** is an app (e.g. NodeJS) that is able to receive consent challenges and create consent tokens. -It must verify the identity of the user that is giving the consent. This can be achieved using Cookie Auth, -HTTP Basic Auth, Login HTML Form, or any other mean of authentication. Upon authentication, the user must be asked -if he consents to allowing the client access to his resources. - -## OAuth2 Clients - -We already covered some basic OAuth2 concepts [in the Introduction](introduction.html). -You can manage *clients* using the cli or the HTTP REST API. - -* **CLI:** `hydra clients -h` -* **REST:** Read the [API Docs](http://docs.hdyra.apiary.io/#reference/oauth2-clients) diff --git a/docs/oauth2/clients/implicit-client.json b/docs/oauth2/clients/implicit-client.json deleted file mode 100644 index 6ee53a2e615..00000000000 --- a/docs/oauth2/clients/implicit-client.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "implicit-example-client", - "scope": "hydra.keys.get openid", - "redirect_uris": [ - "https://localhost/callback" - ], - "grant_types": [ - "implicit" - ], - "response_types": [ - "code", - "token", - "id_token" - ] -} \ No newline at end of file diff --git a/docs/oauth2/consent.md b/docs/oauth2/consent.md deleted file mode 100644 index 4fae1509a71..00000000000 --- a/docs/oauth2/consent.md +++ /dev/null @@ -1,110 +0,0 @@ -# Consent Flow - -Hydra does not ship user authentication. This is something you will have to solve yourself. Usually when you are looking -at using OAuth2 for your app, you already have user authentication anyways. - -In abstract, a consent flow looks like this: - -![](../images/consent.png) - -1. A *client* application (app in browser in laptop) requests an access token from a resource owner: `https://hydra.myapp.com/oauth2/auth?client_id=c3b49cf0-88e4-4faa-9489-28d5b8957858&response_type=code&scope=core+hydra&state=vboeidlizlxrywkwlsgeggff&nonce=tedgziijemvninkuotcuuiof`. -2. Hydra generates a consent challenge and forwards the *user agent* (browser in laptop) to the *consent endpoint*: `https://login.myapp.com/?challenge=eyJhbGciOiJSUzI1N...`. -3. The *consent endpoint* verifies the resource owner's identity (e.g. cookie, username/password login form, ...). The consent challenge is then decoded and the information extracted. It is used to show the consent screen: `Do you want to grant _my cool app_ access to all your private data? [Yes] [No]` -4. When consent is given, the *consent endpoint* generates a consent response token and redirects the user agent (browser in laptop) back to hydra: `https://hydra.myapp.com/oauth2/auth?client_id=c3b49cf0-88e4-4faa-9489-28d5b8957858&response_type=code&scope=core+hydra&state=vboeidlizlxrywkwlsgeggff&nonce=tedgziijemvninkuotcuuiof&consent=eyJhbGciOiJSU...`. -5. Hydra validates the consent response token and issues the access token to the *user agent*. - -## Detailed Example - -In this section we assume that hydra runs on `https://192.168.99.100:4444` and our consent app on `https:/192.168.99.100:3000`. - -All user-based OAuth2 requests, including the OpenID Connect workflow, begin at the `/oauth2/auth` endpoint. For example: `https://192.168.99.100:4444/oauth2/auth?client_id=c3b49cf0-88e4-4faa-9489-28d5b8957858&response_type=code&scope=core+hydra&state=wewuphkgywhtldsmainefkyx&nonce=uqfjjzftqpjccdvxltaposri` - -If the request includes a valid redirect uri and a valid client id, hydra redirects the user to then consent url. The consent url can be set using the `$CONSENT_URL` environment variable. - -Let's set the `$CONSENT_URL` to `https:/192.168.99.100:3000/consent`, where a NodeJS application is running (the *consent app*). Next, Hydra appends a consent challenge to the consent url and redirects the user to it. For example: `http://192.168.99.100:3000/consent/?challenge=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJjM2I0OWNmMC04OGU0LTRmYWEtOTQ4OS0yOGQ1Yjg5NTc4NTgiLCJleHAiOjE0NjQ1MTU0ODIsImp0aSI6IjNmYWRlN2NjLTdlYTItNGViMi05MGI1LWY5OTUwNTI4MzgyOSIsInJlZGlyIjoiaHR0cHM6Ly8xOTIuMTY4Ljk5LjEwMDo0NDQ0L29hdXRoMi9hdXRoP2NsaWVudF9pZD1jM2I0OWNmMC04OGU0LTRmYWEtOTQ4OS0yOGQ1Yjg5NTc4NThcdTAwMjZyZXNwb25zZV90eXBlPWNvZGVcdTAwMjZzY29wZT1jb3JlK2h5ZHJhXHUwMDI2c3RhdGU9d2V3dXBoa2d5d2h0bGRzbWFpbmVma3l4XHUwMDI2bm9uY2U9dXFmamp6ZnRxcGpjY2R2eGx0YXBvc3JpIiwic2NwIjpbImNvcmUiLCJoeWRyYSJdfQ.KpLBotIEE4izVSAjLOeCCfm_wYZ7UWSCA81akr6Ci1yycKs8e_bhBYdSThy8JW3bAvofNcZ0v48ov9KxZVegWm8GuNbBEcNvKeiyW_8PiJXWE92YsMv-tDIL3VFPOp0469FmDLsSg5ohsFj5S89FzykNYfVxLPBAFcAS_JElWbo` - -The consent challenge is a signed RSA-SHA 256 (RS256) [JSON Web Token](https://tools.ietf.org/html/rfc7519) and contains the following claims: - - -``` -eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJjM2I0OWNmMC04OGU0LTRmYWEtOTQ4OS0yOGQ1Yjg5NTc4NTgiLCJleHAiOjE0NjQ1MTUwOTksImp0aSI6IjY0YzRmNzllLWUwMTYtNDViOC04YzBlLWQ5NmM2NzFjMWU4YSIsInJlZGlyIjoiaHR0cHM6Ly8xOTIuMTY4Ljk5LjEwMDo0NDQ0L29hdXRoMi9hdXRoP2NsaWVudF9pZD1jM2I0OWNmMC04OGU0LTRmYWEtOTQ4OS0yOGQ1Yjg5NTc4NThcdTAwMjZyZXNwb25zZV90eXBlPWNvZGVcdTAwMjZzY29wZT1jb3JlK2h5ZHJhXHUwMDI2c3RhdGU9bXlobnhxbXd6aHRleWN3ZW92Ymxzd3dqXHUwMDI2bm9uY2U9Z21tc3V2dHNidG9ldW1lb2hlc3p0c2hnIiwic2NwIjpbImNvcmUiLCJoeWRyYSJdfQ.v4K1-AuT5Uwu1DRNvdf7SwjjPT8KO97thRYa3pDWzjBLyjkCNvgp0P5V0oA3XqRutoFpYx4AtQyz0bY7n3XcPE7ZQ2nBWTBnZ04GzWbxcJNFhBvgc_jiQBECebdxN29kgxHoU0frtVDcz6Uur468nBa9D_BDBpN-KgEBsI5Hjhc - -{ - "aud": "c3b49cf0-88e4-4faa-9489-28d5b8957858", - "exp": 1464515099, - "jti": "64c4f79e-e016-45b8-8c0e-d96c671c1e8a", - "redir": "https://192.168.99.100:4444/oauth2/auth?client_id=c3b49cf0-88e4-4faa-9489-28d5b8957858&response_type=code&scope=core+hydra&state=myhnxqmwzhteycweovblswwj&nonce=gmmsuvtsbtoeumeohesztshg", - "scp": [ - "core", - "hydra" - ] -} -``` - -The challenge claims are: -* **jti:** A unique id. -* **scp:** The requested scopes, e.g. `["blog.readall", "blog.writeall"]` -* **aud:** The client id that initiated the request. You can fetch client data using the [OAuth2 Client API](http://docs.hdyra.apiary.io/#reference/oauth2/manage-the-oauth2-client-collection). -* **exp:** The challenge's expiry date. Consent endpoints must not accept challenges that have expired. -* **redir:** Where the consent endpoint should redirect the user agent to, once consent is given. - -Hydra signs the consent response token with a key called `hydra.consent.challenge`. -The public key can be looked up via the [Key Manager](https://ory-am.gitbooks.io/hydra/content/jwk.html): - -``` -https://192.168.99.100:4444/keys/hydra.consent.challenge/public -``` - -Next, the consent-app must check if the user is authenticated. This can be done by e.g. using a session cookie. -If the user is not authenticate, he must be challenged to provide valid credentials through e.g. a HTML form. -The consent-app could use LDAP, MySQL, RethinkDB or any other backend to store and verify the credentials. - -Upon user authentication, the consent-app must ask for the user's consent. This could look like: - -> _That super useful service app_ would like to: -> * Know who you are -> * View your extended profile info -> * Get read access to all your cloud pictures -> -> [Deny] - [Allow] - -If the user clicks *Allow*, the consent-app redirects him back to the *redir* claim value. The consent-app appends -a signed consent response token to the URL: - -``` -https://192.168.99.100:4444/oauth2/auth?client_id=c3b49cf0-88e4-4faa-9489-28d5b8957858&response_type=code&scope=core+hydra&state=myhnxqmwzhteycweovblswwj&nonce=gmmsuvtsbtoeumeohesztshg&consent=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJjM2I0OWNmMC04OGU0LTRmYWEtOTQ4OS0yOGQ1Yjg5NTc4NTgiLCJleHAiOjE0NjQ1MTUwOTksInNjcCI6WyJjb3JlIiwiaHlkcmEiXSwic3ViIjoiam9obi5kb2VAbWUuY29tIiwiaWF0IjoxNDY0NTExNTE1fQ.tX5TKdP9hHCgPbqBzKIYMjJVwqOdxf5ACScmQ6t20Qteo8AYEfavGwq8KxRF1Oz_otcQDdZY--jcl1caom0yT2eTvj1d9E2Hs7eXmYuW_xF9pTpmDwJnrcOlONFKsNZN97n41qprzMrsX5ez0T5AcopGwpPMxKhwGDSXq9CQgQU -``` - -The consent response token is a RSA-SHA 256 (RS256) signed [JSON Web Token](https://tools.ietf.org/html/rfc7519) -that contains the following claims: - -``` -eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJjM2I0OWNmMC04OGU0LTRmYWEtOTQ4OS0yOGQ1Yjg5NTc4NTgiLCJleHAiOjE0NjQ1MTUwOTksInNjcCI6WyJjb3JlIiwiaHlkcmEiXSwic3ViIjoiam9obi5kb2VAbWUuY29tIiwiaWF0IjoxNDY0NTExNTE1fQ.tX5TKdP9hHCgPbqBzKIYMjJVwqOdxf5ACScmQ6t20Qteo8AYEfavGwq8KxRF1Oz_otcQDdZY--jcl1caom0yT2eTvj1d9E2Hs7eXmYuW_xF9pTpmDwJnrcOlONFKsNZN97n41qprzMrsX5ez0T5AcopGwpPMxKhwGDSXq9CQgQU - -{ - "aud": "c3b49cf0-88e4-4faa-9489-28d5b8957858", - "exp": 1464515099, - "scp": [ - "core", - "hydra" - ], - "sub": "john.doe@me.com", - "iat": 1464511515, - "id_ext": { "foo": "bar" }, - "at_ext": { "baz": true } -} -``` - -The consent claims are: -* **jti:** A unique id. -* **scp:** The scopes the user opted in to *grant* access to, e.g. only `["blog.readall"]`. -* **aud:** The client id that initiated the OAuth2 request. You can fetch client data using the [OAuth2 Client API](http://docs.hdyra.apiary.io/#reference/oauth2/manage-the-oauth2-client-collection). -* **exp:** The expiry date of this token. Use very short lifespans (< 5 min). -* **iat:** The tokens issuance time. -* **id_ext:** If set, pass this extra data to the id token *(optional)* -* **at_ext:** If set, pass this extra data to the access token session. You can retrieve the data by using the warden endpoints *(optional)*. - -Hydra validates the consent response token with consent-app's public key. The public key must be stored in the (https://ory-am.gitbooks.io/hydra/content/key_manager.html) at `https://localhost:4444/keys/hydra.consent.response/public` - -If you want, you can use the Key Manager to store and retrieve private keys as well. When Hydra boots for the first time, a private/public `hydra.consent.response` keypair is created. -You can that keypair to sign consent response tokens. The private key is available at `https://localhost:4444/keys/asymmetric/hydra.consent.response/private`. diff --git a/docs/oauth2/openid.md b/docs/oauth2/openid.md deleted file mode 100644 index 0a67f268924..00000000000 --- a/docs/oauth2/openid.md +++ /dev/null @@ -1,21 +0,0 @@ -# OpenID Connect 1.0 - -**OpenID Connect 1.0** is a simple identity layer on top of the OAuth 2.0 protocol. -It allows Clients to verify the identity of the End-User based on the authentication performed -by an Authorization Server, as well as to obtain basic profile information about the End-User in an -interoperable and REST-like manner. - -OpenID Connect allows clients of all types, including Web-based, mobile, and JavaScript clients, -to request and receive information about authenticated sessions and end-users. The specification -suite is extensible, allowing participants to use optional features such as encryption of identity data, -discovery of OpenID Providers, and session management, when it makes sense for them. - -There are different work flows for OpenID Connect 1.0, we recommend checking out the OpenID Connect sandbox at -[openidconnect.net](https://openidconnect.net/). - -In a nutshell, add `openid` to the OAuth2 scope when making an OAuth2 Authorize Code request. -You will receive an `id_token` alongside the `access_token` when making the code exchange. - - -Hydra uses the [JSON Web Key Manager](https://ory-am.gitbooks.io/hydra/content/key_manager.html) to retrieve the -key pair `hydra.openid.id-token` for signing ID tokens. You can use that endpoint to retrieve the public key for verification. diff --git a/docs/sdk/go.md b/docs/sdk/go.md index 00218e354ce..ade5448e9f3 100644 --- a/docs/sdk/go.md +++ b/docs/sdk/go.md @@ -35,29 +35,6 @@ var err = hydra.Client.DeleteClient(newClient.ID) var clients, err = hydra.Client.GetClients() ``` -Manage SSO Connections using [`ory-am/hydra/connection.HTTPManager`](connection/manager_http.go): -```go -import "github.com/ory-am/hydra/connection" - -// Create a new connection -var sso = connection.Connection{ - Provider: "login.google.com", - LocalSubject: "bob", - RemoteSubject: "googleSubjectID", -} -var err = hydra.SSO.Create(&sso) - -// Retrieve newly created connection -var result, err := hydra.SSO.Get(sso.ID) - -// Delete connection -var err = hydra.SSO.Delete(sso.ID) - -// Find a connection by subject -var ssoConns, err = hydra.SSO.FindAllByLocalSubject("bob") -var ssoConns, err = hydra.SSO.FindByRemoteSubject("login.google.com", "googleSubjectID") -``` - Manage policies using [`ory-am/hydra/policy.HTTPManager`](policy/manager_http.go): ```go import "github.com/ory-am/ladon" @@ -110,7 +87,7 @@ func anyHttpHandler(w http.ResponseWriter, r *http.Request) { fmt.Sprintf("%s", ctx.Subject) // Check if a token is valid and the token's subject fulfills the policy based access request. - ctx, err := hydra.Warden.TokenAllowed(context.Background(), "access-token", &ladon.Request{ + ctx, err := hydra.Warden.TokenAllowed(context.Background(), "access-token", &firewall.TokenAccessRequest{ Resource: "matrix", Action: "create", Context: ladon.Context{}, @@ -124,3 +101,10 @@ Perform Token Introspection as specified in [IETF RFC 7662](https://tools.ietf.o ```go var ctx, err = hydra.Introspector.IntrospectToken(context.Background(), "access-token") ``` + + +Perform Token Revocation as specified in [IETF RFC 7009](https://tools.ietf.org/html/rfc7009): + +```go +var ctx, err = hydra.Revocator.RevokeToken(context.Background(), "access-token") +``` \ No newline at end of file diff --git a/docs/sso.md b/docs/sso.md deleted file mode 100644 index 7ee94ec3c56..00000000000 --- a/docs/sso.md +++ /dev/null @@ -1,39 +0,0 @@ -# Social Login Management - -> Social login, also known as social sign-in, is a form of single sign-on using existing login information from a social -networking service such as Facebook, Twitter or Google+ to sign into a third party website instead of creating -a new login account specifically for that website. It is designed to simplify logins for end users as well as -provide more and more reliable demographic information to web developers. *- [Source: Wikipedia](https://en.wikipedia.org/wiki/Social_login)* - -It is important to note, that Hydra supports you in managing Social Login capabilities, -but does not handle Social Login itself. - -## Exemplary Social Login Journey - -The log in screen - -![](images/social-login-example.jpg) - -Logging in with Google Account -![](images/google.png) - -User authorizes access -![](images/google2.png) - -![](images/social-login-example.jpg) - -Login completed -![](images/login-success-a.gif) - -## In The Background - -Depending on the third party's APIs you complete the sign in request with OAuth 1.0, -OAuth2, OpenID Connect, or some other flow. In any case, you will receive (e.g. /userinfo, id token, ...) -a user id from that service, e.g. `googleuser:u398fjka8f2hj28g`. We call this value the **remote subject**, -the login provider (e.g. Google) **provider**, and the users stored in your private MySQL/LDAP/... -database **local subjects**. - -You can pass the provider and the remote subject values to the -[Social Login API](http://docs.hdyra.apiary.io/#reference/social-login-management) and look up if one of your local -subjects is linked to that third party account. If there is a match you can use the local subject value -to identify and authenticate the user. If there is no match, you will probably send him to your sign up page. \ No newline at end of file diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 00000000000..e06dcf652d3 --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,59 @@ +### 5 Minute Tutorial + +In this example, you will set up Hydra, a Postgres instance and an exemplary identity provider written in React using docker compose. It will take you about 5 minutes to get complete this tutorial. + +OAuth2 Flow + +Running the example + +Install the [CLI and Docker](https://github.com/ory-am/hydra#installation). Make sure you install Docker Compose as well. + +We will use a dummy password as the system secret: `SYSTEM_SECRET=passwordtutorialpasswordtutorial`. +Use a very secure secret in production. + +``` +$ go get github.com/ory-am/hydra +$ cd $GOPATH/src/github.com/ory-am/hydra +$ SYSTEM_SECRET=passwordtutorial DOCKER_IP=localhost docker-compose up --build +Starting hydra_mysqld_1 +Starting hydra_postgresd_1 +Starting hydra_hydra_1 + +[...] +``` + +You now have a running hydra docker container! Additionally, a Postgres image was deployed as well as a consent app. +Next, let us manage the host process. You can use the Hydra CLI by ssh'ing to the docker container: + +``` +$ docker exec -i -t hydra_hydra_1 /bin/bash +root@b4403bb4147f:/go/src/github.com/ory-am/hydra# +``` + +Let's start by creating a new client: + +``` +$ hydra clients create +Client ID: c003830f-a090-4721-9463-92424270ce91 +Client Secret: Z2pJ0>Tp7.ggn>EE&rhnOzdt1 +``` + +Now, let us issue an access token for your OAuth2 client! + +``` +$ hydra token client +JLbnRS9GQmzUBT4x7ESNw0kj2wc0ffbMwOv3QQZW4eI.qkP-IQXn6guoFew8TvaMFUD-SnAyT8GmWuqGi3wuWXg +``` + +Let's try this with the authorize code grant! + +``` +$ hydra token user +Setting up callback listener on http://localhost:4445/callback +Press ctrl + c on Linux / Windows or cmd + c on OSX to end the process. +If your browser does not open automatically, navigate to: + + https://192.168.99.100:4444/oauth2/... +``` + +Great! You installed hydra, connected the CLI, created a client and completed two authentication flows! diff --git a/docs/what-good.md b/docs/what-good.md deleted file mode 100644 index 974ca4c7053..00000000000 --- a/docs/what-good.md +++ /dev/null @@ -1,31 +0,0 @@ -# What's it good for? - -If you are new to OAuth2, this section is for you. To understand what OAuth2 and Hydra are, we will look at what they -**are not good for** first. - -1. Hydra is not something that manages user accounts. Hydra does not offer user registration, password reset, user -login, sending confirmation emails. This is what the *Identity Provider* ("login endpoint") is responsible for. -The communication between Hydra and the Identity Provider is called [*Consent Flow*](https://ory-am.gitbooks.io/hydra/content/oauth2/consent.html). -[Auth0.com](https://auth0.com) is an Identity Provider. We might implement this feature at some point and if, it is going to be a different product. -2. If you think running an OAuth2 Provider can solve your user authentication ("log a user in"), Hydra is probably not for you. OAuth2 is a delegation protocol: - - > The OAuth 2.0 authorization framework enables a third-party application *[think: a dropbox app that manages your dropbox photos]* - to obtain limited access to an HTTP service, either on behalf of *[do you allow "amazing photo app" to access all your photos?]* - a resource owner *[user]* by orchestrating an approval interaction *[consent flow]* between the resource owner and the - HTTP service, or by allowing the third-party application *[OAuth2 Client App]* to obtain access on its own behalf. \- *[IETF rfc6749](https://tools.ietf.org/html/rfc6749)* -3. If you are building a simple service for 50-100 registered users, OAuth2 and Hydra will be overkill. -4. Hydra does not support the OAuth2 resource owner password credentials flow. -5. Hydra has no user interface. You must manage OAuth2 Clients and other things using the RESTful endpoints. -A user interface is scheduled to accompany the stable release. - -We use the following non-exclusive list to help people decide, if **OAuth2 is the right fit** for them. - -1. If you want third-party developers to access your APIs, Hydra is the perfect fit. This is what an OAuth2 Provider does. -2. If you want to become a Identity Provider, like Google, Facebook or Microsoft, OpenID Connect and thus Hydra is a perfect fit. -3. Running an OAuth2 Provider works great with browser, mobile and wearable apps, as you can avoid storing user -credentials on the device, phone or wearable and revoke access tokens, and thus access privileges, at any time. Adding -OAuth2 complexity to your environment when you never plan to do (1), -might not be worth it. Our advice: write a pros/cons list. -4. If you have a lot of services and want to limit automated access (think: cronjobs) for those services, -OAuth2 might make sense for you. Example: The comment service is not allowed to read user passwords when fetching -the latest user profile updates. \ No newline at end of file diff --git a/firewall/warden.go b/firewall/warden.go index 1c22507ab71..59e9c7019e6 100644 --- a/firewall/warden.go +++ b/firewall/warden.go @@ -5,7 +5,6 @@ import ( "net/http" "time" - "github.com/ory-am/ladon" "golang.org/x/net/context" ) @@ -34,18 +33,37 @@ type Context struct { Extra map[string]interface{} `json:"ext"` } +// AccessRequest is the warden's request object. +type AccessRequest struct { + // Resource is the resource that access is requested to. + Resource string `json:"resource"` + + // Action is the action that is requested on the resource. + Action string `json:"action"` + + // Subejct is the subject that is requesting access. + Subject string `json:"subject"` + + // Context is the request's environmental context. + Context map[string]interface{} `json:"context"` +} + +type TokenAccessRequest struct { + // Resource is the resource that access is requested to. + Resource string `json:"resource"` + + // Action is the action that is requested on the resource. + Action string `json:"action"` + + // Context is the request's environmental context. + Context map[string]interface{} `json:"context"` +} + // Firewall offers various validation strategies for access tokens. type Firewall interface { - // TokenValid checks if the given token is valid and if the requested scopes are satisfied. Returns - // a context if the token is valid and an error if not. - // - // ctx, err := firewall.TokenValid(context.Background(), "access-token", "photos", "files") - // fmt.Sprintf("%s", ctx.Subject) - TokenValid(ctx context.Context, token string, scopes ...string) (*Context, error) - // IsAllowed uses policies to return nil if the access request can be fulfilled or an error if not. // - // ctx, err := firewall.IsAllowed(context.Background(), &ladon.Request{ + // ctx, err := firewall.IsAllowed(context.Background(), &AccessRequest{ // Subject: "alice", // Resource: "matrix", // Action: "create", @@ -53,18 +71,18 @@ type Firewall interface { // }, "photos", "files") // // fmt.Sprintf("%s", ctx.Subject) - IsAllowed(ctx context.Context, accessRequest *ladon.Request) error + IsAllowed(ctx context.Context, accessRequest *AccessRequest) error // TokenAllowed uses policies and a token to return a context and no error if the access request can be fulfilled or an error if not. // - // ctx, err := firewall.TokenAllowed(context.Background(), "access-token", &ladon.Request{ + // ctx, err := firewall.TokenAllowed(context.Background(), "access-token", &TokenAccessRequest{ // Resource: "matrix", // Action: "create", // Context: ladon.Context{}, // }, "photos", "files") // // fmt.Sprintf("%s", ctx.Subject) - TokenAllowed(ctx context.Context, token string, accessRequest *ladon.Request, scopes ...string) (*Context, error) + TokenAllowed(ctx context.Context, token string, accessRequest *TokenAccessRequest, scopes ...string) (*Context, error) // TokenFromRequest returns an access token from the HTTP Authorization header. // diff --git a/glide.lock b/glide.lock index 4c6ffd1ada5..0e996b2a1db 100644 --- a/glide.lock +++ b/glide.lock @@ -1,12 +1,10 @@ -hash: 83ebe5a2aa0e2e818b556ef9dad206d0685000271c269876310d45d0e2bf6e30 -updated: 2016-10-06T13:24:49.885386519+02:00 +hash: 18b2d7631e4e950d4e18b6ec76906081980e532850d32c19e414259b8293fe9a +updated: 2016-10-21T22:08:32.7305955+02:00 imports: - name: github.com/asaskevich/govalidator - version: 7664702784775e51966f0885f5cd27435916517b -- name: github.com/BurntSushi/toml - version: 99064174e013895bbd9b025c31100bd1d9b590ca + version: 7b3beb6df3c42abd3509abfc3bcacc0fbfb7c877 - name: github.com/cenk/backoff - version: cdf48bbc1eb78d1349cbda326a4a037f7ba565c6 + version: 8edc80b07f38c27352fb186d971c628a6c32552b - name: github.com/davecgh/go-spew version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 subpackages: @@ -14,9 +12,11 @@ imports: - name: github.com/dgrijalva/jwt-go version: d2709f9f1f31ebcda9651b03077758c1f3a0018c - name: github.com/fsnotify/fsnotify - version: a8a77c9133d2d6fd8334f3260d06f60e8d80a5fb + version: bd2828f9f176e52d7222e565abb2d338d3f3c103 - name: github.com/go-errors/errors version: a41850380601eeb43f4350f7d17c6bbd8944aaf8 +- name: github.com/go-sql-driver/mysql + version: 0b58b37b664c21f3010e836f1b931e1d0b0b0685 - name: github.com/golang/protobuf version: c3cefd437628a0b7d31b34fe44b3a7a540e98527 subpackages: @@ -24,7 +24,7 @@ imports: - name: github.com/hailocab/go-hostpool version: e80d13ce29ede4452c43dea11e79b9bc8a15b478 - name: github.com/hashicorp/hcl - version: d8c773c4cba11b11539e3d45f93daeaa5dcf1fa1 + version: 6f5bfed9a0a22222fbe4e731ae3481730ba41e93 subpackages: - hcl/ast - hcl/parser @@ -38,16 +38,24 @@ imports: version: 50d4dbd4eb0e84778abe37cefef140271d96fade - name: github.com/inconshreveable/mousetrap version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 +- name: github.com/jmoiron/sqlx + version: 05b81a7d5d38058e42148dc01f17daf4acba4640 + subpackages: + - reflectx - name: github.com/julienschmidt/httprouter version: 8c199fb6259ffc1af525cc3ad52ee60ba8359669 - name: github.com/kr/fs version: 2788f0dbd16903de03cb8186e5c7d97b69ad387b +- name: github.com/lib/pq + version: ae8357db35d721c58dcdc911318b55bef6b1b001 + subpackages: + - oid - name: github.com/magiconair/properties - version: b3f6dd549956e8a61ea4a686a1c02a33d5bdda4b + version: 0723e352fa358f9322c938cc2dadda874e9151a9 - name: github.com/meatballhat/negroni-logrus version: 7c570a907cfc69cdc004ad506c6f5e234815b936 - name: github.com/mitchellh/mapstructure - version: ca63d7c062ee3c9f34db231e352b60012b4fd0c1 + version: a6ef2f080c66d0a2e94e97cf74f80f772855da63 - name: github.com/moul/http2curl version: b1479103caacaa39319f75e7f57fc545287fca0d - name: github.com/oleiade/reflections @@ -60,28 +68,30 @@ imports: - pkg - rand/sequence - name: github.com/ory-am/fosite - version: bef61973fdee1a18aedba4e42a1d8977c3f8cc1c + version: eb9077f6608d776ae50eb2ad4205705bad6ee0eb subpackages: - compose - fosite-example/pkg - handler/oauth2 - handler/openid - hash - - rand + - storage - token/hmac - token/jwt - name: github.com/ory-am/ladon - version: 67845728bf072d2b3f050cb415ece9a54ec6a546 -- name: github.com/parnurzeal/gorequest - version: 2aea80ce763523ecc6452e61c3727ae9595a5809 + version: e877152062165a6855d97bbde868572b07a03be4 - name: github.com/pborman/uuid version: a97ce2ca70fa5a848076093f05e639a89ca34d06 +- name: github.com/pelletier/go-buffruneio + version: df1e16fde7fc330a0ca68167c23bf7ed6ac31d6d +- name: github.com/pelletier/go-toml + version: 45932ad32dfdd20826f5671da37a5f3ce9f26a8d - name: github.com/pkg/errors - version: 17b591df37844cde689f4d5813e5cea0927d8dd2 + version: 645ef00459ed84a119197bfb8d8205042c6df63d - name: github.com/pkg/profile version: 1c16f117a3ab788fdf0e334e623b8bccf5679866 - name: github.com/pkg/sftp - version: a71e8f580e3b622ebff585309160b1cc549ef4d2 + version: 4d0e916071f68db74f8a73926335f809396d6b42 - name: github.com/pmezard/go-difflib version: d8ed2627bdf02c080bf22230dbb337003b7aba2d subpackages: @@ -89,22 +99,22 @@ imports: - name: github.com/Sirupsen/logrus version: 4b6ea7319e214d98c938f12692336f7ca9348d6b - name: github.com/spf13/afero - version: cc9c21814bb945440253108c4d3c65c85aac3c68 + version: 52e4a6cfac46163658bd4f123c49b6ee7dc75f78 subpackages: - mem - sftp - name: github.com/spf13/cast - version: e31f36ffc91a2ba9ddb72a4b6a607ff9b3d3cb63 + version: 2580bc98dc0e62908119e4737030cc2fdfc45e4c - name: github.com/spf13/cobra - version: 7c674d9e72017ed25f6d2b5e497a1368086b6a6f + version: 856b96dcb49d6427babe192998a35190a12c2230 - name: github.com/spf13/jwalterweatherman version: 33c24e77fb80341fe7130ee7c594256ff08ccc46 - name: github.com/spf13/pflag - version: 6454a84b6da0ea8b628d5d8a26759f62c6c161b4 + version: bf8481a6aebc13a8aab52e699ffe2e79771f5a3f - name: github.com/spf13/viper - version: 654fc7bb54d0c138ef80405ff577391f79c0c32d + version: 50515b700e02658272117a72bd641b6b7f1222e5 - name: github.com/square/go-jose - version: 139276ceb5afbf13e636c44e9382f0ca75c12ba3 + version: aa2e30fdd1fe9dd3394119af66451ae790d50e0d subpackages: - json - name: github.com/stretchr/testify @@ -127,10 +137,9 @@ imports: - pbkdf2 - ssh - name: golang.org/x/net - version: f315505cf3349909cdf013ea56690da34e96a451 + version: 075e191f18186a8ff2becaf64478e30f4545cdad subpackages: - context - - publicsuffix - name: golang.org/x/oauth2 version: 04e1573abc896e70388bd387a69753c378d46466 subpackages: @@ -141,7 +150,7 @@ imports: subpackages: - unix - name: golang.org/x/text - version: 2910a502d2bf9e43193af9d68ca516529614eed3 + version: fa5033c827cad7080e8e7047a0091945b0e1f031 subpackages: - transform - unicode/norm @@ -164,24 +173,18 @@ imports: - name: gopkg.in/fatih/pool.v2 version: 20a0a429c5f93de45c90f5f09ea297c25e0929b3 - name: gopkg.in/square/go-jose.v1 - version: a3927f83df1b1516f9e9dec71839c93e6bcf1db0 + version: aa2e30fdd1fe9dd3394119af66451ae790d50e0d subpackages: - cipher - json - name: gopkg.in/tylerb/graceful.v1 version: 50a48b6e73fcc75b45e22c05b79629a67c79e938 - name: gopkg.in/yaml.v2 - version: e4d366fc3c7938e2958e662b4258c7a89e1f0e3e + version: a5b47d31c556af34a302ce5d659e6fea44d90de0 testImports: -- name: github.com/go-sql-driver/mysql - version: 0b58b37b664c21f3010e836f1b931e1d0b0b0685 - name: github.com/gorilla/context version: 1ea25387ff6f684839d82767c1733ff4d4d15d0a - name: github.com/gorilla/mux version: 0eeaf8392f5b04950925b8a69fe70f110fa7cbfc -- name: github.com/lib/pq - version: 80f8150043c80fb52dee6bc863a709cdac7ec8f8 - subpackages: - - oid - name: github.com/ory-am/dockertest - version: 1b35e25f4895dff0155ac7b67f69f9aa3a275a76 + version: edb9d9444e6868c1b1e279098b628e7b8e6f0714 diff --git a/glide.yaml b/glide.yaml index 247aae84e26..63e88905d43 100644 --- a/glide.yaml +++ b/glide.yaml @@ -3,7 +3,7 @@ import: - package: github.com/Sirupsen/logrus version: ~0.10.0 - package: github.com/asaskevich/govalidator - version: ~4.0.0 + version: ~5.0.0 - package: gopkg.in/dancannon/gorethink.v2 version: ~2.1.3 - package: github.com/go-errors/errors @@ -18,7 +18,7 @@ import: - package: github.com/dgrijalva/jwt-go version: ~3.0.0 - package: github.com/ory-am/fosite - version: ~0.3.5 + version: ~0.5.0 subpackages: - compose - fosite-example/pkg @@ -28,17 +28,17 @@ import: - token/hmac - token/jwt - package: github.com/ory-am/ladon - version: ~0.2.0 + version: ~0.3.0 - package: github.com/pborman/uuid version: ~1.0.0 - package: github.com/pkg/errors - version: ~0.7.0 + version: ~0.8.0 - package: github.com/pkg/profile version: ~1.2.0 - package: github.com/spf13/cobra - package: github.com/spf13/viper - package: github.com/square/go-jose - version: ~1.0.3 + version: ~1.1.0 subpackages: - json - package: github.com/stretchr/testify @@ -63,4 +63,4 @@ testImport: - package: github.com/gorilla/mux version: ~1.1.0 - package: github.com/ory-am/dockertest - version: ~2.2.2 + version: ~2.2.3 diff --git a/herodot/error.go b/herodot/error.go index 7d8e8167b82..09d66527fff 100644 --- a/herodot/error.go +++ b/herodot/error.go @@ -3,16 +3,16 @@ package herodot import ( "net/http" + "fmt" + "github.com/Sirupsen/logrus" "github.com/ory-am/fosite" "github.com/pkg/errors" "reflect" - "github.com/Sirupsen/logrus" - "fmt" ) type Error struct { - OriginalError error `json:"-"` - StatusCode int `json:"code"` + OriginalError error `json:"-"` + StatusCode int `json:"code"` Description string `json:"description,omitempty"` Name string `json:"name"` } @@ -38,17 +38,17 @@ func ToError(err error) *Error { } else if rfcErr := fosite.ErrorToRFC6749Error(err); rfcErr.Name != fosite.UnknownErrorName { return &Error{ OriginalError: err, - StatusCode: rfcErr.StatusCode, - Description: rfcErr.Description, - Name: rfcErr.Name, + StatusCode: rfcErr.StatusCode, + Description: rfcErr.Description, + Name: rfcErr.Name, } } return &Error{ OriginalError: err, - Description: fmt.Sprintf("Could not unwrap error of type %s", reflect.TypeOf(err)), - Name: "internal-error", - StatusCode: http.StatusInternalServerError, + Description: fmt.Sprintf("Could not unwrap error of type %s", reflect.TypeOf(err)), + Name: "internal-error", + StatusCode: http.StatusInternalServerError, } } diff --git a/herodot/json.go b/herodot/json.go index c1d83fe172f..725abff0956 100644 --- a/herodot/json.go +++ b/herodot/json.go @@ -4,14 +4,14 @@ import ( "encoding/json" "net/http" + "github.com/Sirupsen/logrus" "github.com/pborman/uuid" "golang.org/x/net/context" - "github.com/Sirupsen/logrus" ) type jsonError struct { RequestID string `json:"request"` - Message string `json:"message"` + Message string `json:"message"` *Error } @@ -64,7 +64,7 @@ func (h *JSON) WriteErrorCode(ctx context.Context, w http.ResponseWriter, r *htt je.StatusCode = code h.WriteCode(ctx, w, r, je.StatusCode, &jsonError{ RequestID: id, - Error: ToError(err), - Message: err.Error(), + Error: ToError(err), + Message: err.Error(), }) } diff --git a/herodot/json_test.go b/herodot/json_test.go index 6e2cd04c262..2c7b8bec1ea 100644 --- a/herodot/json_test.go +++ b/herodot/json_test.go @@ -6,8 +6,8 @@ import ( "net/http/httptest" "testing" - "github.com/pkg/errors" "github.com/gorilla/mux" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/net/context" @@ -15,10 +15,10 @@ import ( var ( exampleError = &Error{ - Name: "not-found", - OriginalError: errors.New("Not found"), - StatusCode: http.StatusNotFound, - Description: "test", + Name: "not-found", + OriginalError: errors.New("Not found"), + StatusCode: http.StatusNotFound, + Description: "test", } ) diff --git a/internal/fosite_store_test.go b/internal/fosite_store_test.go deleted file mode 100644 index d8caca53843..00000000000 --- a/internal/fosite_store_test.go +++ /dev/null @@ -1,241 +0,0 @@ -package internal - -import ( - "net/url" - "os" - "testing" - "time" - - "github.com/Sirupsen/logrus" - c "github.com/ory-am/common/pkg" - "github.com/ory-am/dockertest" - "github.com/ory-am/fosite" - "github.com/ory-am/hydra/client" - "github.com/ory-am/hydra/pkg" - "github.com/pborman/uuid" - "github.com/stretchr/testify/assert" - "golang.org/x/net/context" - r "gopkg.in/dancannon/gorethink.v2" -) - -var rethinkManager *FositeRehinkDBStore - -var clientManagers = map[string]pkg.FositeStorer{} - -func init() { - clientManagers["memory"] = &FositeMemoryStore{ - AuthorizeCodes: make(map[string]fosite.Requester), - IDSessions: make(map[string]fosite.Requester), - AccessTokens: make(map[string]fosite.Requester), - Implicit: make(map[string]fosite.Requester), - RefreshTokens: make(map[string]fosite.Requester), - } - -} - -func TestMain(m *testing.M) { - var session *r.Session - var err error - - c, err := dockertest.ConnectToRethinkDB(20, time.Millisecond*500, func(url string) bool { - if session, err = r.Connect(r.ConnectOpts{Address: url, Database: "hydra"}); err != nil { - return false - } else if _, err = r.DBCreate("hydra").RunWrite(session); err != nil { - logrus.Printf("Database exists: %s", err) - return false - } else if _, err = r.TableCreate("hydra_authorize_code").RunWrite(session); err != nil { - logrus.Printf("Could not create table: %s", err) - return false - } else if _, err = r.TableCreate("hydra_id_sessions").RunWrite(session); err != nil { - logrus.Printf("Could not create table: %s", err) - return false - } else if _, err = r.TableCreate("hydra_access_token").RunWrite(session); err != nil { - logrus.Printf("Could not create table: %s", err) - return false - } else if _, err = r.TableCreate("hydra_implicit").RunWrite(session); err != nil { - logrus.Printf("Could not create table: %s", err) - return false - } else if _, err = r.TableCreate("hydra_refresh_token").RunWrite(session); err != nil { - logrus.Printf("Could not create table: %s", err) - return false - } - - rethinkManager = &FositeRehinkDBStore{ - Session: session, - AuthorizeCodesTable: r.Table("hydra_authorize_code"), - IDSessionsTable: r.Table("hydra_id_sessions"), - AccessTokensTable: r.Table("hydra_access_token"), - ImplicitTable: r.Table("hydra_implicit"), - RefreshTokensTable: r.Table("hydra_refresh_token"), - AuthorizeCodes: make(RDBItems), - IDSessions: make(RDBItems), - AccessTokens: make(RDBItems), - Implicit: make(RDBItems), - RefreshTokens: make(RDBItems), - } - rethinkManager.Watch(context.Background()) - time.Sleep(500 * time.Millisecond) - return true - }) - if session != nil { - defer session.Close() - } - if err != nil { - logrus.Fatalf("Could not connect to database: %s", err) - } - clientManagers["rethink"] = rethinkManager - - retCode := m.Run() - c.KillRemove() - os.Exit(retCode) -} - -type testSession struct { - Foo string `json:"foo" gorethink:"foo"` -} - -var defaultRequest = fosite.Request{ - RequestedAt: time.Now().Round(time.Second), - Client: &client.Client{ID: "foobar"}, - Scopes: fosite.Arguments{"fa", "ba"}, - GrantedScopes: fosite.Arguments{"fa", "ba"}, - Form: url.Values{"foo": []string{"bar", "baz"}}, - Session: &testSession{Foo: "bar"}, -} - -func TestColdStartRethinkManager(t *testing.T) { - ctx := context.Background() - m := rethinkManager - id := uuid.New() - - err := m.CreateAuthorizeCodeSession(ctx, id, &defaultRequest) - pkg.AssertError(t, false, err) - err = m.CreateAccessTokenSession(ctx, "12345", &fosite.Request{ - RequestedAt: time.Now().Round(time.Second), - Client: &client.Client{ID: "baz"}, - }) - pkg.AssertError(t, false, err) - - err = m.CreateAccessTokenSession(ctx, id, &defaultRequest) - pkg.AssertError(t, false, err) - - _, err = m.GetAuthorizeCodeSession(ctx, id, &testSession{}) - pkg.AssertError(t, false, err) - _, err = m.GetAccessTokenSession(ctx, id, &testSession{}) - pkg.AssertError(t, false, err) - - delete(rethinkManager.AuthorizeCodes, id) - delete(rethinkManager.AccessTokens, id) - delete(rethinkManager.AccessTokens, "12345") - - _, err = m.GetAuthorizeCodeSession(ctx, id, &testSession{}) - pkg.AssertError(t, true, err) - _, err = m.GetAccessTokenSession(ctx, id, &testSession{}) - pkg.AssertError(t, true, err) - - err = rethinkManager.ColdStart() - pkg.AssertError(t, false, err) - - _, err = m.GetAuthorizeCodeSession(ctx, id, &testSession{}) - pkg.AssertError(t, false, err) - - s1, err := m.GetAccessTokenSession(ctx, id, &testSession{}) - pkg.AssertError(t, false, err) - s2, err := m.GetAccessTokenSession(ctx, "12345", &testSession{}) - pkg.AssertError(t, false, err) - assert.NotEqual(t, s1, s2) -} - -func TestCreateGetDeleteAuthorizeCodes(t *testing.T) { - ctx := context.Background() - for k, m := range clientManagers { - _, err := m.GetAuthorizeCodeSession(ctx, "4321", &testSession{}) - pkg.AssertError(t, true, err, "%s", k) - - err = m.CreateAuthorizeCodeSession(ctx, "4321", &defaultRequest) - pkg.AssertError(t, false, err, "%s", k) - - res, err := m.GetAuthorizeCodeSession(ctx, "4321", &testSession{}) - pkg.RequireError(t, false, err, "%s", k) - c.AssertObjectKeysEqual(t, &defaultRequest, res, "Scopes", "GrantedScopes", "Form", "Session") - - err = m.DeleteAuthorizeCodeSession(ctx, "4321") - pkg.AssertError(t, false, err, "%s", k) - - time.Sleep(100 * time.Millisecond) - - _, err = m.GetAuthorizeCodeSession(ctx, "4321", &testSession{}) - pkg.AssertError(t, true, err, "%s", k) - } -} - -func TestCreateGetDeleteAccessTokenSession(t *testing.T) { - ctx := context.Background() - for k, m := range clientManagers { - _, err := m.GetAccessTokenSession(ctx, "4321", &testSession{}) - pkg.AssertError(t, true, err, "%s", k) - - err = m.CreateAccessTokenSession(ctx, "4321", &defaultRequest) - pkg.AssertError(t, false, err, "%s", k) - - res, err := m.GetAccessTokenSession(ctx, "4321", &testSession{}) - pkg.RequireError(t, false, err, "%s", k) - c.AssertObjectKeysEqual(t, &defaultRequest, res, "Scopes", "GrantedScopes", "Form", "Session") - - err = m.DeleteAccessTokenSession(ctx, "4321") - pkg.AssertError(t, false, err, "%s", k) - - time.Sleep(100 * time.Millisecond) - - _, err = m.GetAccessTokenSession(ctx, "4321", &testSession{}) - pkg.AssertError(t, true, err, "%s", k) - } -} - -func TestCreateGetDeleteOpenIDConnectSession(t *testing.T) { - ctx := context.Background() - for k, m := range clientManagers { - _, err := m.GetOpenIDConnectSession(ctx, "4321", &fosite.Request{}) - pkg.AssertError(t, true, err, "%s", k) - - err = m.CreateOpenIDConnectSession(ctx, "4321", &defaultRequest) - pkg.AssertError(t, false, err, "%s", k) - - res, err := m.GetOpenIDConnectSession(ctx, "4321", &fosite.Request{ - Session: &testSession{}, - }) - pkg.RequireError(t, false, err, "%s", k) - c.AssertObjectKeysEqual(t, &defaultRequest, res, "Scopes", "GrantedScopes", "Form", "Session") - - err = m.DeleteOpenIDConnectSession(ctx, "4321") - pkg.AssertError(t, false, err, "%s", k) - - time.Sleep(100 * time.Millisecond) - - _, err = m.GetOpenIDConnectSession(ctx, "4321", &fosite.Request{}) - pkg.AssertError(t, true, err, "%s", k) - } -} - -func TestCreateGetDeleteRefreshTokenSession(t *testing.T) { - ctx := context.Background() - for k, m := range clientManagers { - _, err := m.GetRefreshTokenSession(ctx, "4321", &testSession{}) - pkg.AssertError(t, true, err, "%s", k) - - err = m.CreateRefreshTokenSession(ctx, "4321", &defaultRequest) - pkg.AssertError(t, false, err, "%s", k) - - res, err := m.GetRefreshTokenSession(ctx, "4321", &testSession{}) - pkg.RequireError(t, false, err, "%s", k) - c.AssertObjectKeysEqual(t, &defaultRequest, res, "Scopes", "GrantedScopes", "Form", "Session") - - err = m.DeleteRefreshTokenSession(ctx, "4321") - pkg.AssertError(t, false, err, "%s", k) - - time.Sleep(100 * time.Millisecond) - - _, err = m.GetRefreshTokenSession(ctx, "4321", &testSession{}) - pkg.AssertError(t, true, err, "%s", k) - } -} diff --git a/jwk/aead_test.go b/jwk/aead_test.go index e45588680df..4ec58de0560 100644 --- a/jwk/aead_test.go +++ b/jwk/aead_test.go @@ -3,14 +3,25 @@ package jwk import ( "testing" - "github.com/ory-am/fosite/rand" + "crypto/rand" "github.com/ory-am/hydra/pkg" "github.com/pborman/uuid" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" + "io" ) +// RandomBytes returns n random bytes by reading from crypto/rand.Reader +func randomBytes(n int) ([]byte, error) { + bytes := make([]byte, n) + if _, err := io.ReadFull(rand.Reader, bytes); err != nil { + return []byte{}, errors.Wrap(err, "") + } + return bytes, nil +} + func TestAEAD(t *testing.T) { - key, err := rand.RandomBytes(32) + key, err := randomBytes(32) pkg.AssertError(t, false, err) a := &AEAD{ diff --git a/jwk/generator_hs256.go b/jwk/generator_hs256.go index eb1c6ec5894..878a61e086e 100644 --- a/jwk/generator_hs256.go +++ b/jwk/generator_hs256.go @@ -3,8 +3,8 @@ package jwk import ( "crypto/x509" - "github.com/pkg/errors" "github.com/ory-am/common/rand/sequence" + "github.com/pkg/errors" "github.com/square/go-jose" ) diff --git a/jwk/handler.go b/jwk/handler.go index a4463645e06..a91ce75504b 100644 --- a/jwk/handler.go +++ b/jwk/handler.go @@ -5,11 +5,10 @@ import ( "fmt" "net/http" - "github.com/pkg/errors" "github.com/julienschmidt/httprouter" "github.com/ory-am/hydra/firewall" "github.com/ory-am/hydra/herodot" - "github.com/ory-am/ladon" + "github.com/pkg/errors" "github.com/square/go-jose" "golang.org/x/net/context" ) @@ -60,7 +59,7 @@ func (h *Handler) DeleteKey(w http.ResponseWriter, r *http.Request, ps httproute var setName = ps.ByName("set") var keyName = ps.ByName("key") - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ + if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: "rn:hydra:keys:" + setName + ":" + keyName, Action: "delete", }, "hydra.keys.delete"); err != nil { @@ -80,7 +79,7 @@ func (h *Handler) DeleteKeySet(w http.ResponseWriter, r *http.Request, ps httpro var ctx = context.Background() var setName = ps.ByName("set") - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ + if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: "rn:hydra:keys:" + setName, Action: "delete", }, "hydra.keys.delete"); err != nil { @@ -101,7 +100,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request, ps httprouter.P var keyRequest createRequest var set = ps.ByName("set") - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ + if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: "rn:hydra:keys:" + set, Action: "create", }, "hydra.keys.create"); err != nil { @@ -139,7 +138,7 @@ func (h *Handler) UpdateKeySet(w http.ResponseWriter, r *http.Request, ps httpro var keySet = new(jose.JsonWebKeySet) var set = ps.ByName("set") - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ + if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: "rn:hydra:keys:" + set, Action: "update", }, "hydra.keys.update"); err != nil { @@ -178,7 +177,7 @@ func (h *Handler) UpdateKey(w http.ResponseWriter, r *http.Request, ps httproute return } - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ + if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: "rn:hydra:keys:" + set + ":" + key.KeyID, Action: "update", }, "hydra.keys.update"); err != nil { @@ -199,13 +198,13 @@ func (h *Handler) GetKey(w http.ResponseWriter, r *http.Request, ps httprouter.P var setName = ps.ByName("set") var keyName = ps.ByName("key") - if err := h.W.IsAllowed(ctx, &ladon.Request{ - Subject: "", + if err := h.W.IsAllowed(ctx, &firewall.AccessRequest{ + Subject: "", Resource: "rn:hydra:keys:" + setName + ":" + keyName, Action: "get", }); err == nil { // Allow unauthorized requests to access this resource if it is enabled by policies - } else if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ + } else if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: "rn:hydra:keys:" + setName + ":" + keyName, Action: "get", }, "hydra.keys.get"); err != nil { @@ -233,7 +232,7 @@ func (h *Handler) GetKeySet(w http.ResponseWriter, r *http.Request, ps httproute } for _, key := range keys.Keys { - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ + if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: "rn:hydra:keys:" + setName + ":" + key.KeyID, Action: "get", }, "hydra.keys.get"); err != nil { diff --git a/jwk/manager_memory.go b/jwk/manager_memory.go index 629d5ba46a6..6e0b3acf8d7 100644 --- a/jwk/manager_memory.go +++ b/jwk/manager_memory.go @@ -3,8 +3,8 @@ package jwk import ( "sync" - "github.com/pkg/errors" "github.com/ory-am/hydra/pkg" + "github.com/pkg/errors" "github.com/square/go-jose" ) diff --git a/jwk/manager_rethinkdb.go b/jwk/manager_rethinkdb.go index 2f678cb76a8..7158874873e 100644 --- a/jwk/manager_rethinkdb.go +++ b/jwk/manager_rethinkdb.go @@ -10,8 +10,8 @@ import ( "fmt" "github.com/Sirupsen/logrus" - "github.com/pkg/errors" "github.com/ory-am/hydra/pkg" + "github.com/pkg/errors" "github.com/square/go-jose" "golang.org/x/net/context" r "gopkg.in/dancannon/gorethink.v2" diff --git a/jwk/manager_sql.go b/jwk/manager_sql.go new file mode 100644 index 00000000000..6ab87809e34 --- /dev/null +++ b/jwk/manager_sql.go @@ -0,0 +1,162 @@ +package jwk + +import ( + "database/sql" + "encoding/json" + "github.com/jmoiron/sqlx" + "github.com/ory-am/hydra/pkg" + "github.com/pkg/errors" + "github.com/square/go-jose" +) + +type SQLManager struct { + DB *sqlx.DB + Cipher *AEAD +} + +var sqlSchema = []string{ + `CREATE TABLE IF NOT EXISTS hydra_jwk ( + sid varchar(255) NOT NULL, + kid varchar(255) NOT NULL, + version int NOT NULL DEFAULT 0, + keydata text NOT NULL, + PRIMARY KEY (sid, kid) +)`, +} + +type sqlData struct { + Set string `db:"sid"` + KID string `db:"kid"` + Version int `db:"version"` + Key string `db:"keydata"` +} + +func (s *SQLManager) CreateSchemas() error { + for _, query := range sqlSchema { + if _, err := s.DB.Exec(query); err != nil { + return errors.Wrapf(err, "Could not create schema:\n%s", query) + } + } + return nil +} + +func (m *SQLManager) AddKey(set string, key *jose.JsonWebKey) error { + out, err := json.Marshal(key) + if err != nil { + return errors.Wrap(err, "") + } + + encrypted, err := m.Cipher.Encrypt(out) + if err != nil { + return errors.Wrap(err, "") + } + + if _, err = m.DB.NamedExec(`INSERT INTO hydra_jwk (sid, kid, version, keydata) VALUES (:sid, :kid, :version, :keydata)`, &sqlData{ + Set: set, + KID: key.KeyID, + Version: 0, + Key: encrypted, + }); err != nil { + return errors.Wrap(err, "") + } + return nil +} + +func (m *SQLManager) AddKeySet(set string, keys *jose.JsonWebKeySet) error { + tx, err := m.DB.Beginx() + if err != nil { + return errors.Wrap(err, "") + } + + for _, key := range keys.Keys { + out, err := json.Marshal(key) + if err != nil { + return errors.Wrap(err, "") + } + + encrypted, err := m.Cipher.Encrypt(out) + if err != nil { + return errors.Wrap(err, "") + } + + if _, err = tx.NamedExec(`INSERT INTO hydra_jwk (sid, kid, version, keydata) VALUES (:sid, :kid, :version, :keydata)`, &sqlData{ + Set: set, + KID: key.KeyID, + Version: 0, + Key: encrypted, + }); err != nil { + return errors.Wrap(err, "") + } + } + + if err := tx.Commit(); err != nil { + return errors.Wrap(err, "") + } + return nil +} + +func (m *SQLManager) GetKey(set, kid string) (*jose.JsonWebKeySet, error) { + var d sqlData + if err := m.DB.Get(&d, m.DB.Rebind("SELECT * FROM hydra_jwk WHERE sid=? AND kid=?"), set, kid); err == sql.ErrNoRows { + return nil, errors.Wrap(pkg.ErrNotFound, "") + } else if err != nil { + return nil, errors.Wrap(err, "") + } + + key, err := m.Cipher.Decrypt(d.Key) + if err != nil { + return nil, errors.Wrap(err, "") + } + + var c jose.JsonWebKey + if err := json.Unmarshal(key, &c); err != nil { + return nil, errors.Wrap(err, "") + } + + return &jose.JsonWebKeySet{ + Keys: []jose.JsonWebKey{c}, + }, nil +} + +func (m *SQLManager) GetKeySet(set string) (*jose.JsonWebKeySet, error) { + var ds []sqlData + if err := m.DB.Select(&ds, m.DB.Rebind("SELECT * FROM hydra_jwk WHERE sid=?"), set); err == sql.ErrNoRows { + return nil, errors.Wrap(pkg.ErrNotFound, "") + } else if err != nil { + return nil, errors.Wrap(err, "") + } + + if len(ds) == 0 { + return nil, errors.Wrap(pkg.ErrNotFound, "") + } + + keys := &jose.JsonWebKeySet{Keys: []jose.JsonWebKey{}} + for _, d := range ds { + key, err := m.Cipher.Decrypt(d.Key) + if err != nil { + return nil, errors.Wrap(err, "") + } + + var c jose.JsonWebKey + if err := json.Unmarshal(key, &c); err != nil { + return nil, errors.Wrap(err, "") + } + keys.Keys = append(keys.Keys, c) + } + + return keys, nil +} + +func (m *SQLManager) DeleteKey(set, kid string) error { + if _, err := m.DB.Exec(m.DB.Rebind(`DELETE FROM hydra_jwk WHERE sid=? AND kid=?`), set, kid); err != nil { + return errors.Wrap(err, "") + } + return nil +} + +func (m *SQLManager) DeleteKeySet(set string) error { + if _, err := m.DB.Exec(m.DB.Rebind(`DELETE FROM hydra_jwk WHERE sid=?`), set); err != nil { + return errors.Wrap(err, "") + } + return nil +} diff --git a/jwk/manager_test.go b/jwk/manager_test.go index 0ecb3297e9c..d88ea743438 100644 --- a/jwk/manager_test.go +++ b/jwk/manager_test.go @@ -8,8 +8,8 @@ import ( "github.com/julienschmidt/httprouter" "github.com/ory-am/dockertest" "github.com/ory-am/fosite" + "github.com/ory-am/hydra/compose" "github.com/ory-am/hydra/herodot" - "github.com/ory-am/hydra/internal" . "github.com/ory-am/hydra/jwk" "github.com/ory-am/hydra/pkg" "github.com/ory-am/ladon" @@ -20,9 +20,13 @@ import ( "os" "time" - "github.com/ory-am/fosite/rand" + "crypto/rand" + "fmt" + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" "github.com/square/go-jose" "golang.org/x/net/context" + "io" "net/http" ) @@ -34,7 +38,7 @@ var ts *httptest.Server var httpManager *HTTPManager func init() { - localWarden, httpClient := internal.NewFirewall( + localWarden, httpClient := compose.NewFirewall( "tests", "alice", fosite.Arguments{ @@ -73,7 +77,60 @@ func init() { var rethinkManager = new(RethinkManager) +func randomBytes(n int) ([]byte, error) { + bytes := make([]byte, n) + if _, err := io.ReadFull(rand.Reader, bytes); err != nil { + return []byte{}, errors.Wrap(err, "") + } + return bytes, nil +} + +var encryptionKey, _ = randomBytes(32) + +var containers = []dockertest.ContainerID{} + func TestMain(m *testing.M) { + defer func() { + for _, c := range containers { + c.KillRemove() + } + }() + + connectToMySQL() + connectToRethinkDB() + connectToPG() + + os.Exit(m.Run()) +} + +func connectToPG() { + var db *sqlx.DB + c, err := dockertest.ConnectToPostgreSQL(15, time.Second, func(url string) bool { + var err error + db, err = sqlx.Open("postgres", url) + if err != nil { + log.Printf("Got error in postgres connector: %s", err) + return false + } + return db.Ping() == nil + }) + + if err != nil { + log.Fatalf("Could not connect to database: %s", err) + } + + containers = append(containers, c) + s := &SQLManager{DB: db, Cipher: &AEAD{Key: encryptionKey}} + + if err = s.CreateSchemas(); err != nil { + log.Fatalf("Could not create postgres schema: %v", err) + } + + managers["postgres"] = s + containers = append(containers, c) +} + +func connectToRethinkDB() { var session *r.Session var err error @@ -88,34 +145,51 @@ func TestMain(m *testing.M) { return false } - key, err := rand.RandomBytes(32) - if err != nil { - log.Printf("Could not watch: %s", err) - return false - } rethinkManager = &RethinkManager{ Keys: map[string]jose.JsonWebKeySet{}, Session: session, Table: r.Table("hydra_keys"), Cipher: &AEAD{ - Key: key, + Key: encryptionKey, }, } rethinkManager.Watch(context.Background()) time.Sleep(100 * time.Millisecond) return true }) - if session != nil { - defer session.Close() - } if err != nil { log.Fatalf("Could not connect to database: %s", err) } + + containers = append(containers, c) managers["rethink"] = rethinkManager +} - retCode := m.Run() - c.KillRemove() - os.Exit(retCode) +func connectToMySQL() { + var db *sqlx.DB + c, err := dockertest.ConnectToMySQL(15, time.Second, func(url string) bool { + var err error + db, err = sqlx.Open("mysql", url) + if err != nil { + log.Printf("Got error in mysql connector: %s", err) + return false + } + return db.Ping() == nil + }) + + if err != nil { + log.Fatalf("Could not connect to database: %s", err) + } + + containers = append(containers, c) + s := &SQLManager{DB: db, Cipher: &AEAD{Key: encryptionKey}} + + if err = s.CreateSchemas(); err != nil { + log.Fatalf("Could not create postgres schema: %v", err) + } + + managers["mysql"] = s + containers = append(containers, c) } func BenchmarkRethinkGet(b *testing.B) { @@ -191,40 +265,40 @@ func TestManagerKey(t *testing.T) { pub := ks.Key("public") for name, m := range managers { - t.Logf("Running test %s", name) - - _, err := m.GetKey("faz", "baz") - pkg.AssertError(t, true, err, name) + t.Run(fmt.Sprintf("case=%s", name), func(t *testing.T) { + _, err := m.GetKey("faz", "baz") + assert.NotNil(t, err) - err = m.AddKey("faz", First(priv)) - pkg.AssertError(t, false, err, name) + err = m.AddKey("faz", First(priv)) + assert.Nil(t, err) - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - got, err := m.GetKey("faz", "private") - pkg.RequireError(t, false, err, name) - assert.Equal(t, priv, got.Keys, "%s", name) + got, err := m.GetKey("faz", "private") + assert.Nil(t, err) + assert.Equal(t, priv, got.Keys, "%s", name) - err = m.AddKey("faz", First(pub)) - pkg.AssertError(t, false, err, name) + err = m.AddKey("faz", First(pub)) + assert.Nil(t, err) - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - got, err = m.GetKey("faz", "private") - pkg.RequireError(t, false, err, name) - assert.Equal(t, priv, got.Keys, "%s", name) + got, err = m.GetKey("faz", "private") + assert.Nil(t, err) + assert.Equal(t, priv, got.Keys, "%s", name) - got, err = m.GetKey("faz", "public") - pkg.RequireError(t, false, err, name) - assert.Equal(t, pub, got.Keys, "%s", name) + got, err = m.GetKey("faz", "public") + assert.Nil(t, err) + assert.Equal(t, pub, got.Keys, "%s", name) - err = m.DeleteKey("faz", "public") - pkg.AssertError(t, false, err, name) + err = m.DeleteKey("faz", "public") + assert.Nil(t, err) - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - ks, err = m.GetKey("faz", "public") - pkg.AssertError(t, true, err, name) + ks, err = m.GetKey("faz", "public") + assert.NotNil(t, err) + }) } err := managers["http"].AddKey("nonono", First(priv)) @@ -236,26 +310,28 @@ func TestManagerKeySet(t *testing.T) { ks.Key("private") for name, m := range managers { - _, err := m.GetKeySet("foo") - pkg.AssertError(t, true, err, name) + t.Run(fmt.Sprintf("case=%s", name), func(t *testing.T) { + _, err := m.GetKeySet("foo") + pkg.AssertError(t, true, err, name) - err = m.AddKeySet("bar", ks) - pkg.AssertError(t, false, err, name) + err = m.AddKeySet("bar", ks) + assert.Nil(t, err) - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - got, err := m.GetKeySet("bar") - pkg.RequireError(t, false, err, name) - assert.Equal(t, ks.Key("public"), got.Key("public"), name) - assert.Equal(t, ks.Key("private"), got.Key("private"), name) + got, err := m.GetKeySet("bar") + assert.Nil(t, err) + assert.Equal(t, ks.Key("public"), got.Key("public"), name) + assert.Equal(t, ks.Key("private"), got.Key("private"), name) - err = m.DeleteKeySet("bar") - pkg.AssertError(t, false, err, name) + err = m.DeleteKeySet("bar") + assert.Nil(t, err) - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - _, err = m.GetKeySet("bar") - pkg.AssertError(t, true, err, name) + _, err = m.GetKeySet("bar") + assert.NotNil(t, err) + }) } err := managers["http"].AddKeySet("nonono", ks) diff --git a/oauth2/consent_strategy.go b/oauth2/consent_strategy.go index 9f0ecf2c6d8..0e565f3834c 100644 --- a/oauth2/consent_strategy.go +++ b/oauth2/consent_strategy.go @@ -1,17 +1,17 @@ package oauth2 import ( + "crypto/rsa" "fmt" "time" - "crypto/rsa" "github.com/dgrijalva/jwt-go" - "github.com/pkg/errors" "github.com/ory-am/fosite" "github.com/ory-am/fosite/handler/openid" ejwt "github.com/ory-am/fosite/token/jwt" "github.com/ory-am/hydra/jwk" "github.com/pborman/uuid" + "github.com/pkg/errors" ) const ( @@ -77,7 +77,6 @@ func (s *DefaultConsentStrategy) ValidateResponse(a fosite.AuthorizeRequester, t } return &Session{ - Subject: subject, DefaultSession: &openid.DefaultSession{ Claims: &ejwt.IDTokenClaims{ Audience: a.GetClient().GetID(), @@ -88,6 +87,7 @@ func (s *DefaultConsentStrategy) ValidateResponse(a fosite.AuthorizeRequester, t Extra: idExt, }, Headers: &ejwt.Headers{}, + Subject: subject, }, Extra: atExt, }, err diff --git a/internal/fosite_store_memory.go b/oauth2/fosite_store_memory.go similarity index 78% rename from internal/fosite_store_memory.go rename to oauth2/fosite_store_memory.go index 92f87750636..fad57e74e44 100644 --- a/internal/fosite_store_memory.go +++ b/oauth2/fosite_store_memory.go @@ -1,12 +1,12 @@ -package internal +package oauth2 import ( "sync" "github.com/ory-am/fosite" "github.com/ory-am/hydra/client" - "golang.org/x/net/context" "github.com/pkg/errors" + "golang.org/x/net/context" ) type FositeMemoryStore struct { @@ -15,7 +15,6 @@ type FositeMemoryStore struct { AuthorizeCodes map[string]fosite.Requester IDSessions map[string]fosite.Requester AccessTokens map[string]fosite.Requester - Implicit map[string]fosite.Requester RefreshTokens map[string]fosite.Requester sync.RWMutex @@ -52,7 +51,7 @@ func (s *FositeMemoryStore) CreateAuthorizeCodeSession(_ context.Context, code s return nil } -func (s *FositeMemoryStore) GetAuthorizeCodeSession(_ context.Context, code string, _ interface{}) (fosite.Requester, error) { +func (s *FositeMemoryStore) GetAuthorizeCodeSession(_ context.Context, code string, _ fosite.Session) (fosite.Requester, error) { s.RLock() defer s.RUnlock() rel, ok := s.AuthorizeCodes[code] @@ -76,7 +75,7 @@ func (s *FositeMemoryStore) CreateAccessTokenSession(_ context.Context, signatur return nil } -func (s *FositeMemoryStore) GetAccessTokenSession(_ context.Context, signature string, _ interface{}) (fosite.Requester, error) { +func (s *FositeMemoryStore) GetAccessTokenSession(_ context.Context, signature string, _ fosite.Session) (fosite.Requester, error) { s.RLock() defer s.RUnlock() rel, ok := s.AccessTokens[signature] @@ -100,7 +99,7 @@ func (s *FositeMemoryStore) CreateRefreshTokenSession(_ context.Context, signatu return nil } -func (s *FositeMemoryStore) GetRefreshTokenSession(_ context.Context, signature string, _ interface{}) (fosite.Requester, error) { +func (s *FositeMemoryStore) GetRefreshTokenSession(_ context.Context, signature string, _ fosite.Session) (fosite.Requester, error) { s.RLock() defer s.RUnlock() rel, ok := s.RefreshTokens[signature] @@ -117,11 +116,8 @@ func (s *FositeMemoryStore) DeleteRefreshTokenSession(_ context.Context, signatu return nil } -func (s *FositeMemoryStore) CreateImplicitAccessTokenSession(_ context.Context, code string, req fosite.Requester) error { - s.Lock() - defer s.Unlock() - s.Implicit[code] = req - return nil +func (s *FositeMemoryStore) CreateImplicitAccessTokenSession(ctx context.Context, code string, req fosite.Requester) error { + return s.CreateAccessTokenSession(ctx, code, req) } func (s *FositeMemoryStore) PersistAuthorizeCodeGrantSession(ctx context.Context, authorizeCode, accessSignature, refreshSignature string, request fosite.Requester) error { @@ -153,3 +149,35 @@ func (s *FositeMemoryStore) PersistRefreshTokenGrantSession(ctx context.Context, return nil } + +func (s *FositeMemoryStore) RevokeRefreshToken(ctx context.Context, id string) error { + var found bool + for sig, token := range s.RefreshTokens { + if token.GetID() == id { + if err := s.DeleteRefreshTokenSession(ctx, sig); err != nil { + return err + } + found = true + } + } + if !found { + return errors.New("Not found") + } + return nil +} + +func (s *FositeMemoryStore) RevokeAccessToken(ctx context.Context, id string) error { + var found bool + for sig, token := range s.AccessTokens { + if token.GetID() == id { + if err := s.DeleteAccessTokenSession(ctx, sig); err != nil { + return err + } + found = true + } + } + if !found { + return errors.New("Not found") + } + return nil +} diff --git a/internal/fosite_store_rethinkdb.go b/oauth2/fosite_store_rethinkdb.go similarity index 83% rename from internal/fosite_store_rethinkdb.go rename to oauth2/fosite_store_rethinkdb.go index 53c867b8dc1..e8ff9428de3 100644 --- a/internal/fosite_store_rethinkdb.go +++ b/oauth2/fosite_store_rethinkdb.go @@ -1,4 +1,4 @@ -package internal +package oauth2 import ( "encoding/json" @@ -7,10 +7,10 @@ import ( "time" "github.com/Sirupsen/logrus" - "github.com/pkg/errors" "github.com/ory-am/fosite" "github.com/ory-am/hydra/client" "github.com/ory-am/hydra/pkg" + "github.com/pkg/errors" "golang.org/x/net/context" r "gopkg.in/dancannon/gorethink.v2" ) @@ -24,7 +24,6 @@ type FositeRehinkDBStore struct { AuthorizeCodesTable r.Term IDSessionsTable r.Term AccessTokensTable r.Term - ImplicitTable r.Term RefreshTokensTable r.Term ClientsTable r.Term @@ -33,12 +32,12 @@ type FositeRehinkDBStore struct { AuthorizeCodes RDBItems IDSessions RDBItems AccessTokens RDBItems - Implicit RDBItems RefreshTokens RDBItems } type RdbSchema struct { ID string `json:"id" gorethink:"id"` + RequestID string `json:"requestId" gorethink:"requestId"` RequestedAt time.Time `json:"requestedAt" gorethink:"requestedAt"` Client *client.Client `json:"client" gorethink:"client"` Scopes fosite.Arguments `json:"scopes" gorethink:"scopes"` @@ -47,7 +46,7 @@ type RdbSchema struct { Session json.RawMessage `json:"session" gorethink:"session"` } -func requestFromRDB(s *RdbSchema, proto interface{}) (*fosite.Request, error) { +func requestFromRDB(s *RdbSchema, proto fosite.Session) (*fosite.Request, error) { if proto != nil { if err := json.Unmarshal(s.Session, proto); err != nil { return nil, errors.Wrap(err, "") @@ -55,6 +54,7 @@ func requestFromRDB(s *RdbSchema, proto interface{}) (*fosite.Request, error) { } d := new(fosite.Request) + d.ID = s.RequestID d.RequestedAt = s.RequestedAt d.Client = s.Client d.Scopes = s.Scopes @@ -71,8 +71,6 @@ func (m *FositeRehinkDBStore) ColdStart() error { return err } else if err := m.IDSessions.coldStart(m.Session, &m.RWMutex, m.IDSessionsTable); err != nil { return err - } else if err := m.Implicit.coldStart(m.Session, &m.RWMutex, m.ImplicitTable); err != nil { - return err } else if err := m.RefreshTokens.coldStart(m.Session, &m.RWMutex, m.RefreshTokensTable); err != nil { return err } @@ -87,6 +85,7 @@ func (s *FositeRehinkDBStore) publishInsert(table r.Term, id string, requester f if _, err := table.Insert(&RdbSchema{ ID: id, + RequestID: requester.GetID(), RequestedAt: requester.GetRequestedAt(), Client: requester.GetClient().(*client.Client), Scopes: requester.GetRequestedScopes(), @@ -106,19 +105,23 @@ func (s *FositeRehinkDBStore) publishDelete(table r.Term, id string) error { return nil } -func waitFor(i RDBItems, id string) error { +func (s *FositeRehinkDBStore) waitFor(i RDBItems, id string) error { c := make(chan bool) go func() { loopWait := time.Millisecond + s.RLock() _, ok := i[id] + s.RUnlock() for !ok { time.Sleep(loopWait) loopWait = loopWait * time.Duration(int64(2)) if loopWait > time.Second { loopWait = time.Second } + s.RLock() _, ok = i[id] + s.RUnlock() } c <- true @@ -136,7 +139,7 @@ func (s *FositeRehinkDBStore) CreateOpenIDConnectSession(_ context.Context, auth if err := s.publishInsert(s.IDSessionsTable, authorizeCode, requester); err != nil { return err } - return waitFor(s.IDSessions, authorizeCode) + return s.waitFor(s.IDSessions, authorizeCode) } func (s *FositeRehinkDBStore) GetOpenIDConnectSession(_ context.Context, authorizeCode string, requester fosite.Requester) (fosite.Requester, error) { @@ -157,10 +160,10 @@ func (s *FositeRehinkDBStore) CreateAuthorizeCodeSession(_ context.Context, code if err := s.publishInsert(s.AuthorizeCodesTable, code, requester); err != nil { return err } - return waitFor(s.AuthorizeCodes, code) + return s.waitFor(s.AuthorizeCodes, code) } -func (s *FositeRehinkDBStore) GetAuthorizeCodeSession(_ context.Context, code string, sess interface{}) (fosite.Requester, error) { +func (s *FositeRehinkDBStore) GetAuthorizeCodeSession(_ context.Context, code string, sess fosite.Session) (fosite.Requester, error) { s.RLock() defer s.RUnlock() rel, ok := s.AuthorizeCodes[code] @@ -179,10 +182,10 @@ func (s *FositeRehinkDBStore) CreateAccessTokenSession(_ context.Context, signat if err := s.publishInsert(s.AccessTokensTable, signature, requester); err != nil { return err } - return waitFor(s.AccessTokens, signature) + return s.waitFor(s.AccessTokens, signature) } -func (s *FositeRehinkDBStore) GetAccessTokenSession(_ context.Context, signature string, sess interface{}) (fosite.Requester, error) { +func (s *FositeRehinkDBStore) GetAccessTokenSession(_ context.Context, signature string, sess fosite.Session) (fosite.Requester, error) { s.RLock() defer s.RUnlock() rel, ok := s.AccessTokens[signature] @@ -201,10 +204,10 @@ func (s *FositeRehinkDBStore) CreateRefreshTokenSession(_ context.Context, signa if err := s.publishInsert(s.RefreshTokensTable, signature, requester); err != nil { return err } - return waitFor(s.RefreshTokens, signature) + return s.waitFor(s.RefreshTokens, signature) } -func (s *FositeRehinkDBStore) GetRefreshTokenSession(_ context.Context, signature string, sess interface{}) (fosite.Requester, error) { +func (s *FositeRehinkDBStore) GetRefreshTokenSession(_ context.Context, signature string, sess fosite.Session) (fosite.Requester, error) { s.RLock() defer s.RUnlock() rel, ok := s.RefreshTokens[signature] @@ -219,11 +222,8 @@ func (s *FositeRehinkDBStore) DeleteRefreshTokenSession(_ context.Context, signa return s.publishDelete(s.RefreshTokensTable, signature) } -func (s *FositeRehinkDBStore) CreateImplicitAccessTokenSession(_ context.Context, code string, req fosite.Requester) error { - if err := s.publishInsert(s.ImplicitTable, code, req); err != nil { - return err - } - return waitFor(s.Implicit, code) +func (s *FositeRehinkDBStore) CreateImplicitAccessTokenSession(ctx context.Context, code string, req fosite.Requester) error { + return s.CreateAccessTokenSession(ctx, code, req) } func (s *FositeRehinkDBStore) PersistAuthorizeCodeGrantSession(ctx context.Context, authorizeCode, accessSignature, refreshSignature string, request fosite.Requester) error { @@ -260,7 +260,6 @@ func (m *FositeRehinkDBStore) Watch(ctx context.Context) { m.AccessTokens.watch(ctx, m.Session, &m.RWMutex, m.AccessTokensTable) m.AuthorizeCodes.watch(ctx, m.Session, &m.RWMutex, m.AuthorizeCodesTable) m.IDSessions.watch(ctx, m.Session, &m.RWMutex, m.IDSessionsTable) - m.Implicit.watch(ctx, m.Session, &m.RWMutex, m.ImplicitTable) m.RefreshTokens.watch(ctx, m.Session, &m.RWMutex, m.RefreshTokensTable) } @@ -286,7 +285,9 @@ func (items RDBItems) coldStart(sess *r.Session, lock *sync.RWMutex, table r.Ter func (items RDBItems) watch(ctx context.Context, sess *r.Session, lock *sync.RWMutex, table r.Term) { go pkg.Retry(time.Second*15, time.Minute, func() error { + lock.Lock() changes, err := table.Changes().Run(sess) + lock.Unlock() if err != nil { return errors.Wrap(err, "") } @@ -294,10 +295,10 @@ func (items RDBItems) watch(ctx context.Context, sess *r.Session, lock *sync.RWM var update = map[string]*RdbSchema{} for changes.Next(&update) { + lock.Lock() logrus.Debugln("Received update from RethinkDB Cluster in OAuth2 manager.") newVal := update["new_val"] oldVal := update["old_val"] - lock.Lock() if newVal == nil && oldVal != nil { delete(items, oldVal.ID) } else if newVal != nil && oldVal != nil { @@ -316,3 +317,35 @@ func (items RDBItems) watch(ctx context.Context, sess *r.Session, lock *sync.RWM return nil }) } + +func (s *FositeRehinkDBStore) RevokeRefreshToken(ctx context.Context, id string) error { + var found bool + for sig, token := range s.RefreshTokens { + if token.RequestID == id { + if err := s.DeleteRefreshTokenSession(ctx, sig); err != nil { + return err + } + found = true + } + } + if !found { + return errors.New("Not found") + } + return nil +} + +func (s *FositeRehinkDBStore) RevokeAccessToken(ctx context.Context, id string) error { + var found bool + for sig, token := range s.AccessTokens { + if token.RequestID == id { + if err := s.DeleteAccessTokenSession(ctx, sig); err != nil { + return err + } + found = true + } + } + if !found { + return errors.New("Not found") + } + return nil +} diff --git a/oauth2/fosite_store_sql.go b/oauth2/fosite_store_sql.go new file mode 100644 index 00000000000..834ec22ab96 --- /dev/null +++ b/oauth2/fosite_store_sql.go @@ -0,0 +1,259 @@ +package oauth2 + +import ( + "database/sql" + "encoding/json" + "fmt" + "github.com/jmoiron/sqlx" + "github.com/ory-am/fosite" + "github.com/ory-am/hydra/client" + "github.com/pkg/errors" + "golang.org/x/net/context" + "net/url" + "strings" + "time" +) + +type FositeSQLStore struct { + client.Manager + DB *sqlx.DB +} + +func sqlTemplate(table string) string { + return fmt.Sprintf(`CREATE TABLE IF NOT EXISTS hydra_oauth2_%s ( + signature varchar(255) NOT NULL PRIMARY KEY, + request_id varchar(255) NOT NULL, + requested_at timestamp NOT NULL DEFAULT now(), + client_id text NOT NULL, + scope text NOT NULL, + granted_scope text NOT NULL, + form_data text NOT NULL, + session_data text NOT NULL +)`, table) +} + +const ( + sqlTableOpenID = "oidc" + sqlTableAccess = "access" + sqlTableRefresh = "refresh" + sqlTableCode = "code" +) + +var sqlSchema = []string{ + sqlTemplate(sqlTableAccess), + sqlTemplate(sqlTableRefresh), + sqlTemplate(sqlTableCode), + sqlTemplate(sqlTableOpenID), +} + +var sqlParams = []string{ + "signature", + "request_id", + "requested_at", + "client_id", + "scope", + "granted_scope", + "form_data", + "session_data", +} + +type sqlData struct { + Signature string `db:"signature"` + Request string `db:"request_id"` + RequestedAt time.Time `db:"requested_at"` + Client string `db:"client_id"` + Scopes string `db:"scope"` + GrantedScopes string `db:"granted_scope"` + Form string `db:"form_data"` + Session []byte `db:"session_data"` +} + +func sqlSchemaFromRequest(signature string, r fosite.Requester) (*sqlData, error) { + session, err := json.Marshal(r.GetSession()) + if err != nil { + return nil, errors.Wrap(err, "") + } + + return &sqlData{ + Request: r.GetID(), + Signature: signature, + RequestedAt: r.GetRequestedAt(), + Client: r.GetClient().GetID(), + Scopes: strings.Join([]string(r.GetRequestedScopes()), "|"), + GrantedScopes: strings.Join([]string(r.GetGrantedScopes()), "|"), + Form: r.GetRequestForm().Encode(), + Session: session, + }, nil +} + +func (s *sqlData) ToRequest(session fosite.Session, cm client.Manager) (*fosite.Request, error) { + if session != nil { + if err := json.Unmarshal(s.Session, session); err != nil { + return nil, errors.Wrap(err, "") + } + } + + c, err := cm.GetClient(s.Client) + if err != nil { + return nil, err + } + + val, err := url.ParseQuery(s.Form) + if err != nil { + return nil, errors.Wrap(err, "") + } + + return &fosite.Request{ + ID: s.Request, + RequestedAt: s.RequestedAt, + Client: c, + Scopes: fosite.Arguments(strings.Split(s.Scopes, "|")), + GrantedScopes: fosite.Arguments(strings.Split(s.GrantedScopes, "|")), + Form: val, + Session: session, + }, nil +} + +func (s *FositeSQLStore) createSession(signature string, requester fosite.Requester, table string) error { + data, err := sqlSchemaFromRequest(signature, requester) + if err != nil { + return err + } + + query := fmt.Sprintf( + "INSERT INTO hydra_oauth2_%s (%s) VALUES (%s)", + table, + strings.Join(sqlParams, ", "), + ":"+strings.Join(sqlParams, ", :"), + ) + if _, err := s.DB.NamedExec(query, data); err != nil { + return errors.Wrap(err, "") + } + return nil +} + +func (s *FositeSQLStore) findSessionBySignature(signature string, session fosite.Session, table string) (fosite.Requester, error) { + var d sqlData + if err := s.DB.Get(&d, s.DB.Rebind(fmt.Sprintf("SELECT * FROM hydra_oauth2_%s WHERE signature=?", table)), signature); err == sql.ErrNoRows { + return nil, errors.Wrap(fosite.ErrNotFound, "") + } else if err != nil { + return nil, errors.Wrap(err, "") + } + + return d.ToRequest(session, s.Manager) +} + +func (s *FositeSQLStore) deleteSession(signature string, table string) error { + if _, err := s.DB.Exec(s.DB.Rebind(fmt.Sprintf("DELETE FROM hydra_oauth2_%s WHERE signature=?", table)), signature); err != nil { + return errors.Wrap(err, "") + } + return nil +} + +func (s *FositeSQLStore) CreateSchemas() error { + for _, query := range sqlSchema { + if _, err := s.DB.Exec(query); err != nil { + return errors.Wrapf(err, "Could not create schema:\n%s", query) + } + } + return nil +} + +func (s *FositeSQLStore) CreateOpenIDConnectSession(_ context.Context, signature string, requester fosite.Requester) error { + return s.createSession(signature, requester, sqlTableOpenID) +} + +func (s *FositeSQLStore) GetOpenIDConnectSession(_ context.Context, signature string, requester fosite.Requester) (fosite.Requester, error) { + return s.findSessionBySignature(signature, requester.GetSession(), sqlTableOpenID) +} + +func (s *FositeSQLStore) DeleteOpenIDConnectSession(_ context.Context, signature string) error { + return s.deleteSession(signature, sqlTableOpenID) +} + +func (s *FositeSQLStore) CreateAuthorizeCodeSession(_ context.Context, signature string, requester fosite.Requester) error { + return s.createSession(signature, requester, sqlTableCode) +} + +func (s *FositeSQLStore) GetAuthorizeCodeSession(_ context.Context, signature string, session fosite.Session) (fosite.Requester, error) { + return s.findSessionBySignature(signature, session, sqlTableCode) +} + +func (s *FositeSQLStore) DeleteAuthorizeCodeSession(_ context.Context, signature string) error { + return s.deleteSession(signature, sqlTableCode) +} + +func (s *FositeSQLStore) CreateAccessTokenSession(_ context.Context, signature string, requester fosite.Requester) error { + return s.createSession(signature, requester, sqlTableAccess) +} + +func (s *FositeSQLStore) GetAccessTokenSession(_ context.Context, signature string, session fosite.Session) (fosite.Requester, error) { + return s.findSessionBySignature(signature, session, sqlTableAccess) +} + +func (s *FositeSQLStore) DeleteAccessTokenSession(_ context.Context, signature string) error { + return s.deleteSession(signature, sqlTableAccess) +} + +func (s *FositeSQLStore) CreateRefreshTokenSession(_ context.Context, signature string, requester fosite.Requester) error { + return s.createSession(signature, requester, sqlTableRefresh) +} + +func (s *FositeSQLStore) GetRefreshTokenSession(_ context.Context, signature string, session fosite.Session) (fosite.Requester, error) { + return s.findSessionBySignature(signature, session, sqlTableRefresh) +} + +func (s *FositeSQLStore) DeleteRefreshTokenSession(_ context.Context, signature string) error { + return s.deleteSession(signature, sqlTableRefresh) +} + +func (s *FositeSQLStore) CreateImplicitAccessTokenSession(ctx context.Context, signature string, requester fosite.Requester) error { + return s.CreateAccessTokenSession(ctx, signature, requester) +} + +func (s *FositeSQLStore) PersistAuthorizeCodeGrantSession(ctx context.Context, authorizeCode, accessSignature, refreshSignature string, request fosite.Requester) error { + if err := s.DeleteAuthorizeCodeSession(ctx, authorizeCode); err != nil { + return err + } else if err := s.CreateAccessTokenSession(ctx, accessSignature, request); err != nil { + return err + } + + if refreshSignature == "" { + return nil + } + + if err := s.CreateRefreshTokenSession(ctx, refreshSignature, request); err != nil { + return err + } + + return nil +} + +func (s *FositeSQLStore) PersistRefreshTokenGrantSession(ctx context.Context, originalRefreshSignature, accessSignature, refreshSignature string, request fosite.Requester) error { + if err := s.DeleteRefreshTokenSession(ctx, originalRefreshSignature); err != nil { + return err + } else if err := s.CreateAccessTokenSession(ctx, accessSignature, request); err != nil { + return err + } else if err := s.CreateRefreshTokenSession(ctx, refreshSignature, request); err != nil { + return err + } + + return nil +} + +func (s *FositeSQLStore) RevokeRefreshToken(ctx context.Context, id string) error { + return s.revokeSession(id, sqlTableRefresh) +} + +func (s *FositeSQLStore) RevokeAccessToken(ctx context.Context, id string) error { + return s.revokeSession(id, sqlTableAccess) +} + +func (s *FositeSQLStore) revokeSession(id string, table string) error { + if _, err := s.DB.Exec(s.DB.Rebind(fmt.Sprintf("DELETE FROM hydra_oauth2_%s WHERE request_id=?", table)), id); err == sql.ErrNoRows { + return errors.Wrap(fosite.ErrNotFound, "") + } else if err != nil { + return errors.Wrap(err, "") + } + return nil +} diff --git a/oauth2/fosite_store_test.go b/oauth2/fosite_store_test.go new file mode 100644 index 00000000000..91b93fb90fb --- /dev/null +++ b/oauth2/fosite_store_test.go @@ -0,0 +1,356 @@ +package oauth2 + +import ( + "net/url" + "os" + "testing" + "time" + + "fmt" + "github.com/Sirupsen/logrus" + "github.com/jmoiron/sqlx" + c "github.com/ory-am/common/pkg" + "github.com/ory-am/dockertest" + "github.com/ory-am/fosite" + "github.com/ory-am/hydra/client" + "github.com/ory-am/hydra/pkg" + "github.com/pborman/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + r "gopkg.in/dancannon/gorethink.v2" +) + +var rethinkManager *FositeRehinkDBStore +var containers = []dockertest.ContainerID{} +var clientManagers = map[string]pkg.FositeStorer{} +var clientManager = &client.MemoryManager{ + Clients: map[string]client.Client{"foobar": {ID: "foobar"}}, + Hasher: &fosite.BCrypt{}, +} + +func init() { + clientManagers["memory"] = &FositeMemoryStore{ + AuthorizeCodes: make(map[string]fosite.Requester), + IDSessions: make(map[string]fosite.Requester), + AccessTokens: make(map[string]fosite.Requester), + RefreshTokens: make(map[string]fosite.Requester), + } +} + +func TestMain(m *testing.M) { + defer func() { + for _, c := range containers { + c.KillRemove() + } + }() + connectToMySQL() + connectToPG() + connectToRethink() + os.Exit(m.Run()) +} + +func connectToMySQL() { + var db *sqlx.DB + cn, err := dockertest.ConnectToMySQL(15, time.Second, func(url string) bool { + var err error + db, err = sqlx.Open("mysql", url) + if err != nil { + logrus.Printf("Got error in mysql connector: %s", err) + return false + } + return db.Ping() == nil + }) + + if err != nil { + logrus.Fatalf("Could not connect to database: %s", err) + } + + containers = append(containers, cn) + s := &FositeSQLStore{DB: db, Manager: clientManager} + if err = s.CreateSchemas(); err != nil { + logrus.Fatalf("Could not create postgres schema: %v", err) + } + + clientManagers["mysql"] = s +} + +func connectToPG() { + var db *sqlx.DB + cn, err := dockertest.ConnectToPostgreSQL(15, time.Second, func(url string) bool { + var err error + db, err = sqlx.Open("postgres", url) + if err != nil { + logrus.Printf("Got error in postgres connector: %s", err) + return false + } + return db.Ping() == nil + }) + + if err != nil { + logrus.Fatalf("Could not connect to database: %s", err) + } + + containers = append(containers, cn) + s := &FositeSQLStore{DB: db, Manager: clientManager} + if err = s.CreateSchemas(); err != nil { + logrus.Fatalf("Could not create postgres schema: %v", err) + } + + clientManagers["postgres"] = s +} + +func connectToRethink() { + var session *r.Session + var err error + + cn, err := dockertest.ConnectToRethinkDB(20, time.Millisecond*500, func(url string) bool { + if session, err = r.Connect(r.ConnectOpts{Address: url, Database: "hydra"}); err != nil { + return false + } else if _, err = r.DBCreate("hydra").RunWrite(session); err != nil { + logrus.Printf("Database exists: %s", err) + return false + } else if _, err = r.TableCreate("hydra_authorize_code").RunWrite(session); err != nil { + logrus.Printf("Could not create table: %s", err) + return false + } else if _, err = r.TableCreate("hydra_id_sessions").RunWrite(session); err != nil { + logrus.Printf("Could not create table: %s", err) + return false + } else if _, err = r.TableCreate("hydra_access_token").RunWrite(session); err != nil { + logrus.Printf("Could not create table: %s", err) + return false + } else if _, err = r.TableCreate("hydra_refresh_token").RunWrite(session); err != nil { + logrus.Printf("Could not create table: %s", err) + return false + } + + rethinkManager = &FositeRehinkDBStore{ + Session: session, + AuthorizeCodesTable: r.Table("hydra_authorize_code"), + IDSessionsTable: r.Table("hydra_id_sessions"), + AccessTokensTable: r.Table("hydra_access_token"), + RefreshTokensTable: r.Table("hydra_refresh_token"), + AuthorizeCodes: make(RDBItems), + IDSessions: make(RDBItems), + AccessTokens: make(RDBItems), + RefreshTokens: make(RDBItems), + } + rethinkManager.Watch(context.Background()) + time.Sleep(500 * time.Millisecond) + return true + }) + + if err != nil { + logrus.Fatalf("Could not connect to database: %s", err) + } + clientManagers["rethink"] = rethinkManager + containers = append(containers, cn) +} + +var defaultRequest = fosite.Request{ + RequestedAt: time.Now().Round(time.Second), + Client: &client.Client{ID: "foobar"}, + Scopes: fosite.Arguments{"fa", "ba"}, + GrantedScopes: fosite.Arguments{"fa", "ba"}, + Form: url.Values{"foo": []string{"bar", "baz"}}, + Session: &fosite.DefaultSession{Subject: "bar"}, +} + +func TestColdStartRethinkManager(t *testing.T) { + ctx := context.Background() + m := rethinkManager + id := uuid.New() + + err := m.CreateAuthorizeCodeSession(ctx, id, &defaultRequest) + pkg.AssertError(t, false, err) + err = m.CreateAccessTokenSession(ctx, "12345", &fosite.Request{ + RequestedAt: time.Now().Round(time.Second), + Client: &client.Client{ID: "baz"}, + }) + pkg.AssertError(t, false, err) + + err = m.CreateAccessTokenSession(ctx, id, &defaultRequest) + pkg.AssertError(t, false, err) + + _, err = m.GetAuthorizeCodeSession(ctx, id, &fosite.DefaultSession{}) + pkg.AssertError(t, false, err) + _, err = m.GetAccessTokenSession(ctx, id, &fosite.DefaultSession{}) + pkg.AssertError(t, false, err) + + delete(rethinkManager.AuthorizeCodes, id) + delete(rethinkManager.AccessTokens, id) + delete(rethinkManager.AccessTokens, "12345") + + _, err = m.GetAuthorizeCodeSession(ctx, id, &fosite.DefaultSession{}) + pkg.AssertError(t, true, err) + _, err = m.GetAccessTokenSession(ctx, id, &fosite.DefaultSession{}) + pkg.AssertError(t, true, err) + + err = rethinkManager.ColdStart() + pkg.AssertError(t, false, err) + + _, err = m.GetAuthorizeCodeSession(ctx, id, &fosite.DefaultSession{}) + pkg.AssertError(t, false, err) + + s1, err := m.GetAccessTokenSession(ctx, id, &fosite.DefaultSession{}) + pkg.AssertError(t, false, err) + s2, err := m.GetAccessTokenSession(ctx, "12345", &fosite.DefaultSession{}) + pkg.AssertError(t, false, err) + assert.NotEqual(t, s1, s2) +} + +func TestCreateImplicitAccessTokenSession(t *testing.T) { + ctx := context.Background() + for k, m := range clientManagers { + t.Run(fmt.Sprintf("case=%s", k), func(t *testing.T) { + + _, err := m.GetAccessTokenSession(ctx, "implicit-4321", &fosite.DefaultSession{}) + assert.NotNil(t, err) + + err = m.CreateImplicitAccessTokenSession(ctx, "implicit-4321", &defaultRequest) + assert.Nil(t, err) + + res, err := m.GetAccessTokenSession(ctx, "implicit-4321", &fosite.DefaultSession{}) + require.Nil(t, err) + c.AssertObjectKeysEqual(t, &defaultRequest, res, "Scopes", "GrantedScopes", "Form", "Session") + + err = m.DeleteAccessTokenSession(ctx, "implicit-4321") + assert.Nil(t, err) + + time.Sleep(100 * time.Millisecond) + + _, err = m.GetAccessTokenSession(ctx, "implicit-4321", &fosite.DefaultSession{}) + assert.NotNil(t, err) + }) + } +} +func TestCreateGetDeleteAuthorizeCodes(t *testing.T) { + ctx := context.Background() + for k, m := range clientManagers { + t.Run(fmt.Sprintf("case=%s", k), func(t *testing.T) { + _, err := m.GetAuthorizeCodeSession(ctx, "4321", &fosite.DefaultSession{}) + assert.NotNil(t, err) + + err = m.CreateAuthorizeCodeSession(ctx, "4321", &defaultRequest) + require.Nil(t, err) + + res, err := m.GetAuthorizeCodeSession(ctx, "4321", &fosite.DefaultSession{}) + require.Nil(t, err) + c.AssertObjectKeysEqual(t, &defaultRequest, res, "Scopes", "GrantedScopes", "Form", "Session") + + err = m.DeleteAuthorizeCodeSession(ctx, "4321") + require.Nil(t, err) + + time.Sleep(100 * time.Millisecond) + + _, err = m.GetAuthorizeCodeSession(ctx, "4321", &fosite.DefaultSession{}) + assert.NotNil(t, err) + }) + } +} + +func TestCreateGetDeleteAccessTokenSession(t *testing.T) { + ctx := context.Background() + for k, m := range clientManagers { + t.Run(fmt.Sprintf("case=%s", k), func(t *testing.T) { + _, err := m.GetAccessTokenSession(ctx, "4321", &fosite.DefaultSession{}) + assert.NotNil(t, err) + + err = m.CreateAccessTokenSession(ctx, "4321", &defaultRequest) + require.Nil(t, err) + + res, err := m.GetAccessTokenSession(ctx, "4321", &fosite.DefaultSession{}) + require.Nil(t, err) + c.AssertObjectKeysEqual(t, &defaultRequest, res, "Scopes", "GrantedScopes", "Form", "Session") + + err = m.DeleteAccessTokenSession(ctx, "4321") + require.Nil(t, err) + + time.Sleep(100 * time.Millisecond) + + _, err = m.GetAccessTokenSession(ctx, "4321", &fosite.DefaultSession{}) + assert.NotNil(t, err) + }) + } +} + +func TestCreateGetDeleteOpenIDConnectSession(t *testing.T) { + ctx := context.Background() + for k, m := range clientManagers { + t.Run(fmt.Sprintf("case=%s", k), func(t *testing.T) { + _, err := m.GetOpenIDConnectSession(ctx, "4321", &fosite.Request{}) + assert.NotNil(t, err) + + err = m.CreateOpenIDConnectSession(ctx, "4321", &defaultRequest) + require.Nil(t, err) + + res, err := m.GetOpenIDConnectSession(ctx, "4321", &fosite.Request{Session: &fosite.DefaultSession{}}) + require.Nil(t, err) + c.AssertObjectKeysEqual(t, &defaultRequest, res, "Scopes", "GrantedScopes", "Form", "Session") + + err = m.DeleteOpenIDConnectSession(ctx, "4321") + require.Nil(t, err) + + time.Sleep(100 * time.Millisecond) + + _, err = m.GetOpenIDConnectSession(ctx, "4321", &fosite.Request{}) + assert.NotNil(t, err) + }) + } +} + +func TestCreateGetDeleteRefreshTokenSession(t *testing.T) { + ctx := context.Background() + for k, m := range clientManagers { + t.Run(fmt.Sprintf("case=%s", k), func(t *testing.T) { + _, err := m.GetRefreshTokenSession(ctx, "4321", &fosite.DefaultSession{}) + assert.NotNil(t, err) + + err = m.CreateRefreshTokenSession(ctx, "4321", &defaultRequest) + require.Nil(t, err) + + res, err := m.GetRefreshTokenSession(ctx, "4321", &fosite.DefaultSession{}) + require.Nil(t, err) + c.AssertObjectKeysEqual(t, &defaultRequest, res, "Scopes", "GrantedScopes", "Form", "Session") + + err = m.DeleteRefreshTokenSession(ctx, "4321") + require.Nil(t, err) + + time.Sleep(100 * time.Millisecond) + + _, err = m.GetRefreshTokenSession(ctx, "4321", &fosite.DefaultSession{}) + assert.NotNil(t, err) + }) + } +} + +func TestRevokeRefreshToken(t *testing.T) { + ctx := context.Background() + id := uuid.New() + for k, m := range clientManagers { + t.Run(fmt.Sprintf("case=%s", k), func(t *testing.T) { + _, err := m.GetRefreshTokenSession(ctx, "1111", &fosite.DefaultSession{}) + assert.NotNil(t, err) + + err = m.CreateRefreshTokenSession(ctx, "1111", &fosite.Request{ID: id, Client: &client.Client{ID: "foobar"}, RequestedAt: time.Now().Round(time.Second)}) + require.Nil(t, err) + + err = m.CreateRefreshTokenSession(ctx, "1122", &fosite.Request{ID: id, Client: &client.Client{ID: "foobar"}, RequestedAt: time.Now().Round(time.Second)}) + require.Nil(t, err) + + _, err = m.GetRefreshTokenSession(ctx, "1111", &fosite.DefaultSession{}) + require.Nil(t, err) + + err = m.RevokeRefreshToken(ctx, id) + require.Nil(t, err) + + time.Sleep(100 * time.Millisecond) + + _, err = m.GetRefreshTokenSession(ctx, "1111", &fosite.DefaultSession{}) + assert.NotNil(t, err) + + _, err = m.GetRefreshTokenSession(ctx, "1122", &fosite.DefaultSession{}) + assert.NotNil(t, err) + }) + } +} diff --git a/oauth2/handler.go b/oauth2/handler.go index d68c39d0506..847376d6c8b 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -4,12 +4,13 @@ import ( "net/http" "net/url" - "github.com/pkg/errors" + "encoding/json" "github.com/julienschmidt/httprouter" "github.com/ory-am/fosite" - "github.com/ory-am/hydra/firewall" "github.com/ory-am/hydra/herodot" "github.com/ory-am/hydra/pkg" + "github.com/pkg/errors" + "strings" ) const ( @@ -21,15 +22,14 @@ const ( // IntrospectPath points to the OAuth2 introspection endpoint. IntrospectPath = "/oauth2/introspect" + RevocationPath = "/oauth2/revoke" ) type Handler struct { OAuth2 fosite.OAuth2Provider Consent ConsentStrategy - Introspector Introspector - Firewall firewall.Firewall - H herodot.Herodot + H herodot.Herodot ForcedHTTP bool ConsentURL url.URL @@ -40,34 +40,47 @@ func (h *Handler) SetRoutes(r *httprouter.Router) { r.GET(AuthPath, h.AuthHandler) r.POST(AuthPath, h.AuthHandler) r.GET(ConsentPath, h.DefaultConsentHandler) - r.POST(IntrospectPath, h.Introspect) + r.POST(IntrospectPath, h.IntrospectHandler) + r.POST(RevocationPath, h.RevocationHandler) } -func (h *Handler) Introspect(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - var inactive = map[string]bool{"active": false} +func (h *Handler) RevocationHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + var ctx = fosite.NewContext() - ctx := herodot.NewContext() - clientCtx, err := h.Firewall.TokenValid(ctx, h.Firewall.TokenFromRequest(r)) + err := h.OAuth2.NewRevocationRequest(ctx, r) if err != nil { - h.H.WriteError(ctx, w, r, err) - return + pkg.LogError(err) } - if err := r.ParseForm(); err != nil { - h.H.WriteError(ctx, w, r, err) - return - } + h.OAuth2.WriteRevocationResponse(w, err) +} - auth, err := h.Introspector.IntrospectToken(ctx, r.PostForm.Get("token")) +func (h *Handler) IntrospectHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + var session = NewSession("") + var ctx = fosite.NewContext() + resp, err := h.OAuth2.NewIntrospectionRequest(ctx, r, session) if err != nil { - h.H.Write(ctx, w, r, &inactive) + pkg.LogError(err) + h.OAuth2.WriteIntrospectionError(w, err) return - } else if clientCtx.Subject != auth.Audience { - h.H.Write(ctx, w, r, &inactive) + } + + if !resp.IsActive() { + _ = json.NewEncoder(w).Encode(&Introspection{Active: false}) return } - h.H.Write(ctx, w, r, auth) + _ = json.NewEncoder(w).Encode(&Introspection{ + Active: true, + ClientID: resp.GetAccessRequester().GetClient().GetID(), + Scope: strings.Join(resp.GetAccessRequester().GetGrantedScopes(), " "), + ExpiresAt: resp.GetAccessRequester().GetSession().GetExpiresAt(fosite.AccessToken).Unix(), + IssuedAt: resp.GetAccessRequester().GetRequestedAt().Unix(), + Subject: resp.GetAccessRequester().GetSession().GetSubject(), + Username: resp.GetAccessRequester().GetSession().GetUsername(), + Extra: resp.GetAccessRequester().GetSession().(*Session).Extra, + Audience: resp.GetAccessRequester().GetClient().GetID(), + }) } func (h *Handler) TokenHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { diff --git a/oauth2/handler_consent.go b/oauth2/handler_consent.go index 65e446f2699..6d47ec2220d 100644 --- a/oauth2/handler_consent.go +++ b/oauth2/handler_consent.go @@ -17,7 +17,7 @@ func (o *Handler) DefaultConsentHandler(w http.ResponseWriter, r *http.Request,

- It looks like you forgot to set the consent endpoint url, which can be set using the CONSENT_ENDPOINT + It looks like you forgot to set the consent endpoint url, which can be set using the CONSENT_URL environment variable.

diff --git a/oauth2/handler_consent_test.go b/oauth2/handler_consent_test.go new file mode 100644 index 00000000000..390ba11c3bb --- /dev/null +++ b/oauth2/handler_consent_test.go @@ -0,0 +1,26 @@ +package oauth2 + +import ( + "net/http/httptest" + "net/http" + "io/ioutil" + "github.com/julienschmidt/httprouter" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestHandlerConsent(t *testing.T) { + h := new(Handler) + r := httprouter.New() + h.SetRoutes(r) + ts := httptest.NewServer(r) + + res, err := http.Get(ts.URL + "/oauth2/consent") + assert.Nil(t, err) + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + assert.Nil(t, err) + + assert.NotEmpty(t, body) +} diff --git a/oauth2/introspector.go b/oauth2/introspector.go index 5f11e10e965..125018659e6 100644 --- a/oauth2/introspector.go +++ b/oauth2/introspector.go @@ -45,7 +45,7 @@ type Introspection struct { // Username is a human-readable identifier for the resource owner who // authorized this token. - Username int64 `json:"username,omitempty"` + Username string `json:"username,omitempty"` // Audience is a service-specific string identifier or list of string // identifiers representing the intended audience for this token. @@ -67,5 +67,5 @@ type Introspector interface { // ctx, err := introspector.IntrospectToken(context.Background(), introspector.TokenFromRequest(r), "photos", "files") // fmt.Sprintf("%s", ctx.Subject) // } - IntrospectToken(ctx context.Context, token string) (*Introspection, error) + IntrospectToken(ctx context.Context, token string, scopes ...string) (*Introspection, error) } diff --git a/oauth2/introspector_http.go b/oauth2/introspector_http.go index 03442b025a2..1357bde01a6 100644 --- a/oauth2/introspector_http.go +++ b/oauth2/introspector_http.go @@ -3,8 +3,8 @@ package oauth2 import ( "bytes" "encoding/json" - "github.com/pkg/errors" "github.com/ory-am/fosite" + "github.com/pkg/errors" "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" @@ -12,6 +12,7 @@ import ( "net/http" "net/url" "strconv" + "strings" ) type HTTPIntrospector struct { @@ -20,23 +21,25 @@ type HTTPIntrospector struct { Endpoint *url.URL } -func (this *HTTPIntrospector) TokenFromRequest(r *http.Request) string { +func (i *HTTPIntrospector) TokenFromRequest(r *http.Request) string { return fosite.AccessTokenFromRequest(r) } -func (this *HTTPIntrospector) SetClient(c *clientcredentials.Config) { - this.Client = c.Client(oauth2.NoContext) +func (i *HTTPIntrospector) SetClient(c *clientcredentials.Config) { + i.Client = c.Client(oauth2.NoContext) } // IntrospectToken is capable of introspecting tokens according to https://tools.ietf.org/html/rfc7662 // // The HTTP API is documented at http://docs.hdyra.apiary.io/#reference/oauth2/oauth2-token-introspection -func (this *HTTPIntrospector) IntrospectToken(ctx context.Context, token string) (*Introspection, error) { - var resp = new(Introspection) - var ep = *this.Endpoint +func (i *HTTPIntrospector) IntrospectToken(ctx context.Context, token string, scopes ...string) (*Introspection, error) { + var resp = &Introspection{ + Extra: make(map[string]interface{}), + } + var ep = *i.Endpoint ep.Path = IntrospectPath - data := url.Values{"token": []string{token}} + data := url.Values{"token": []string{token}, "scope": []string{strings.Join(scopes, " ")}} hreq, err := http.NewRequest("POST", ep.String(), bytes.NewBufferString(data.Encode())) if err != nil { return nil, errors.Wrap(err, "") @@ -44,21 +47,19 @@ func (this *HTTPIntrospector) IntrospectToken(ctx context.Context, token string) hreq.Header.Add("Content-Type", "application/x-www-form-urlencoded") hreq.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) - hres, err := this.Client.Do(hreq) + hres, err := i.Client.Do(hreq) if err != nil { return nil, errors.Wrap(err, "") } defer hres.Body.Close() + body, _ := ioutil.ReadAll(hres.Body) if hres.StatusCode < 200 || hres.StatusCode >= 300 { - body, _ := ioutil.ReadAll(hres.Body) return nil, errors.Errorf("Expected 2xx status code but got %d.\n%s", hres.StatusCode, body) - } else if err := json.NewDecoder(hres.Body).Decode(resp); err != nil { - body, _ := ioutil.ReadAll(hres.Body) + } else if err := json.Unmarshal(body, resp); err != nil { return nil, errors.Errorf("%s: %s", err, body) } else if !resp.Active { return nil, errors.New("Token is malformed, expired or otherwise invalid") } - return resp, nil } diff --git a/oauth2/introspector_local.go b/oauth2/introspector_local.go deleted file mode 100644 index 8e5527a6001..00000000000 --- a/oauth2/introspector_local.go +++ /dev/null @@ -1,45 +0,0 @@ -package oauth2 - -import ( - "github.com/Sirupsen/logrus" - "github.com/ory-am/fosite" - "golang.org/x/net/context" - "net/http" - "strings" - "time" -) - -type LocalIntrospector struct { - OAuth2 fosite.OAuth2Provider - - AccessTokenLifespan time.Duration - Issuer string -} - -func (w *LocalIntrospector) TokenFromRequest(r *http.Request) string { - return fosite.AccessTokenFromRequest(r) -} - -func (w *LocalIntrospector) IntrospectToken(ctx context.Context, token string) (*Introspection, error) { - var session = new(Session) - var auth, err = w.OAuth2.ValidateToken(ctx, token, fosite.AccessToken, session) - if err != nil { - logrus.WithError(err).Infof("Token introspection failed") - return &Introspection{ - Active: false, - }, err - } - - session = auth.GetSession().(*Session) - return &Introspection{ - Active: true, - Subject: session.Subject, - Audience: auth.GetClient().GetID(), - Scope: strings.Join(auth.GetGrantedScopes(), " "), - Issuer: w.Issuer, - IssuedAt: auth.GetRequestedAt().Unix(), - NotBefore: auth.GetRequestedAt().Unix(), - ExpiresAt: session.AccessTokenExpiresAt(auth.GetRequestedAt().Add(w.AccessTokenLifespan)).Unix(), - Extra: session.Extra, - }, nil -} diff --git a/oauth2/introspector_test.go b/oauth2/introspector_test.go index 1305ee9964d..63cb9d31fa6 100644 --- a/oauth2/introspector_test.go +++ b/oauth2/introspector_test.go @@ -6,15 +6,15 @@ import ( "testing" "time" + "fmt" "github.com/Sirupsen/logrus" "github.com/julienschmidt/httprouter" "github.com/ory-am/fosite" - foauth2 "github.com/ory-am/fosite/handler/oauth2" + "github.com/ory-am/fosite/compose" + "github.com/ory-am/fosite/storage" "github.com/ory-am/hydra/herodot" "github.com/ory-am/hydra/oauth2" "github.com/ory-am/hydra/pkg" - "github.com/ory-am/hydra/warden" - "github.com/ory-am/ladon" "github.com/stretchr/testify/assert" "golang.org/x/net/context" goauth2 "golang.org/x/oauth2" @@ -24,55 +24,27 @@ var ( introspectors = make(map[string]oauth2.Introspector) now = time.Now().Round(time.Second) tokens = pkg.Tokens(3) - fositeStore = pkg.FositeStore() + fositeStore = storage.NewExampleStore() ) -var ladonWarden = pkg.LadonWarden(map[string]ladon.Policy{ - "1": &ladon.DefaultPolicy{ - ID: "1", - Subjects: []string{"alice"}, - Resources: []string{"matrix", "rn:hydra:token<.*>"}, - Actions: []string{"create", "decide"}, - Effect: ladon.AllowAccess, - }, - "2": &ladon.DefaultPolicy{ - ID: "2", - Subjects: []string{"siri"}, - Resources: []string{"<.*>"}, - Actions: []string{"decide"}, - Effect: ladon.AllowAccess, - }, -}) - -var localWarden = &warden.LocalWarden{ - Warden: ladonWarden, - OAuth2: &fosite.Fosite{ - Store: fositeStore, - TokenValidators: fosite.TokenValidators{ - 0: &foauth2.CoreValidator{ - CoreStrategy: pkg.HMACStrategy, - CoreStorage: fositeStore, - ScopeStrategy: fosite.HierarchicScopeStrategy, - }, - }, - ScopeStrategy: fosite.HierarchicScopeStrategy, - }, - Issuer: "tests", - AccessTokenLifespan: time.Hour, -} - func init() { - introspectors["local"] = &oauth2.LocalIntrospector{ - OAuth2: localWarden.OAuth2, - Issuer: "tests", - AccessTokenLifespan: time.Hour, - } - + introspectors = make(map[string]oauth2.Introspector) + now = time.Now().Round(time.Second) + tokens = pkg.Tokens(3) + fositeStore = storage.NewExampleStore() r := httprouter.New() serv := &oauth2.Handler{ - Firewall: localWarden, - H: &herodot.JSON{}, - Introspector: introspectors["local"], + OAuth2: compose.Compose( + fc, + fositeStore, + &compose.CommonStrategy{ + CoreStrategy: compose.NewOAuth2HMACStrategy(fc, []byte("1234567890123456789012345678901234567890")), + OpenIDConnectTokenStrategy: compose.NewOpenIDConnectStrategy(pkg.MustRSAKey()), + }, + compose.OAuth2AuthorizeExplicitFactory, + compose.OAuth2TokenIntrospectionFactory, + ), + H: &herodot.JSON{}, } serv.SetRoutes(r) ts = httptest.NewServer(r) @@ -81,6 +53,7 @@ func init() { ar.GrantedScopes = fosite.Arguments{"core"} ar.RequestedAt = now ar.Client = &fosite.DefaultClient{ID: "siri"} + ar.Session.SetExpiresAt(fosite.AccessToken, now.Add(time.Hour)) ar.Session.(*oauth2.Session).Extra = map[string]interface{}{"foo": "bar"} fositeStore.CreateAccessTokenSession(nil, tokens[0][0], ar) @@ -88,15 +61,16 @@ func init() { ar2.GrantedScopes = fosite.Arguments{"core"} ar2.RequestedAt = now ar2.Session.(*oauth2.Session).Extra = map[string]interface{}{"foo": "bar"} + ar2.Session.SetExpiresAt(fosite.AccessToken, now.Add(time.Hour)) ar2.Client = &fosite.DefaultClient{ID: "siri"} fositeStore.CreateAccessTokenSession(nil, tokens[1][0], ar2) ar3 := fosite.NewAccessRequest(oauth2.NewSession("siri")) ar3.GrantedScopes = fosite.Arguments{"core"} ar3.RequestedAt = now - ar2.Session.(*oauth2.Session).Extra = map[string]interface{}{"foo": "bar"} + ar3.Session.(*oauth2.Session).Extra = map[string]interface{}{"foo": "bar"} ar3.Client = &fosite.DefaultClient{ID: "doesnt-exist"} - ar3.Session.(*oauth2.Session).AccessTokenExpiry = time.Now().Add(-time.Hour) + ar3.Session.SetExpiresAt(fosite.AccessToken, now.Add(-time.Hour)) fositeStore.CreateAccessTokenSession(nil, tokens[2][0], ar3) conf := &goauth2.Config{ @@ -112,14 +86,14 @@ func init() { Endpoint: ep, Client: conf.Client(goauth2.NoContext, &goauth2.Token{ AccessToken: tokens[1][1], - Expiry: time.Now().Add(time.Hour), + Expiry: now.Add(time.Hour), TokenType: "bearer", }), } } func TestIntrospect(t *testing.T) { - for _, w := range introspectors { + for k, w := range introspectors { for _, c := range []struct { token string expectErr bool @@ -135,6 +109,10 @@ func TestIntrospect(t *testing.T) { }, { token: tokens[1][1], + expectErr: true, + }, + { + token: tokens[0][1], expectErr: false, }, { @@ -142,18 +120,20 @@ func TestIntrospect(t *testing.T) { expectErr: false, assert: func(c *oauth2.Introspection) { assert.Equal(t, "alice", c.Subject) - assert.Equal(t, "tests", c.Issuer) + //assert.Equal(t, "tests", c.Issuer) assert.Equal(t, now.Add(time.Hour).Unix(), c.ExpiresAt, "expires at") assert.Equal(t, now.Unix(), c.IssuedAt, "issued at") assert.Equal(t, map[string]interface{}{"foo": "bar"}, c.Extra) }, }, } { - ctx, err := w.IntrospectToken(context.Background(), c.token) - pkg.AssertError(t, c.expectErr, err) - if err == nil && c.assert != nil { - c.assert(ctx) - } + t.Run(fmt.Sprintf("case=%s", k), func(t *testing.T) { + ctx, err := w.IntrospectToken(context.Background(), c.token) + pkg.AssertError(t, c.expectErr, err) + if err == nil && c.assert != nil { + c.assert(ctx) + } + }) } } } diff --git a/oauth2/oauth2_auth_code_test.go b/oauth2/oauth2_auth_code_test.go index 9be516aeb70..5c78f3c6955 100644 --- a/oauth2/oauth2_auth_code_test.go +++ b/oauth2/oauth2_auth_code_test.go @@ -7,13 +7,13 @@ import ( "time" "github.com/dgrijalva/jwt-go" - "github.com/pkg/errors" "github.com/julienschmidt/httprouter" ejwt "github.com/ory-am/fosite/token/jwt" "github.com/ory-am/hydra/jwk" . "github.com/ory-am/hydra/oauth2" "github.com/ory-am/hydra/pkg" "github.com/pborman/uuid" + "github.com/pkg/errors" "github.com/stretchr/testify/require" "golang.org/x/oauth2" ) diff --git a/oauth2/oauth2_test.go b/oauth2/oauth2_test.go index 64bac9d2984..d409794dba0 100644 --- a/oauth2/oauth2_test.go +++ b/oauth2/oauth2_test.go @@ -7,23 +7,21 @@ import ( "time" "github.com/dgrijalva/jwt-go" - "github.com/pkg/errors" "github.com/julienschmidt/httprouter" "github.com/ory-am/fosite" "github.com/ory-am/fosite/compose" - "github.com/ory-am/fosite/hash" hc "github.com/ory-am/hydra/client" - "github.com/ory-am/hydra/internal" "github.com/ory-am/hydra/jwk" . "github.com/ory-am/hydra/oauth2" "github.com/ory-am/hydra/pkg" + "github.com/pkg/errors" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" ) -var hasher = &hash.BCrypt{} +var hasher = &fosite.BCrypt{} -var store = &internal.FositeMemoryStore{ +var store = &FositeMemoryStore{ Manager: &hc.MemoryManager{ Clients: map[string]hc.Client{}, Hasher: hasher, @@ -31,7 +29,6 @@ var store = &internal.FositeMemoryStore{ AuthorizeCodes: make(map[string]fosite.Requester), IDSessions: make(map[string]fosite.Requester), AccessTokens: make(map[string]fosite.Requester), - Implicit: make(map[string]fosite.Requester), RefreshTokens: make(map[string]fosite.Requester), } @@ -51,9 +48,11 @@ var handler = &Handler{ compose.OAuth2AuthorizeImplicitFactory, compose.OAuth2ClientCredentialsGrantFactory, compose.OAuth2RefreshTokenGrantFactory, - compose.OpenIDConnectExplicit, - compose.OpenIDConnectHybrid, - compose.OpenIDConnectImplicit, + compose.OpenIDConnectExplicitFactory, + compose.OpenIDConnectHybridFactory, + compose.OpenIDConnectImplicitFactory, + compose.OAuth2TokenRevocationFactory, + compose.OAuth2TokenIntrospectionFactory, ), Consent: &DefaultConsentStrategy{ Issuer: "http://hydra.localhost", diff --git a/oauth2/revocator.go b/oauth2/revocator.go new file mode 100644 index 00000000000..521d9ecdd82 --- /dev/null +++ b/oauth2/revocator.go @@ -0,0 +1,7 @@ +package oauth2 + +import "golang.org/x/net/context" + +type Revocator interface { + RevokeToken(ctx context.Context, token string) error +} diff --git a/oauth2/revocator_http.go b/oauth2/revocator_http.go new file mode 100644 index 00000000000..3aebf334958 --- /dev/null +++ b/oauth2/revocator_http.go @@ -0,0 +1,44 @@ +package oauth2 + +import ( + "bytes" + "github.com/pkg/errors" + "golang.org/x/net/context" + "golang.org/x/oauth2/clientcredentials" + "io/ioutil" + "net/http" + "net/url" + "strconv" +) + +type HTTPRecovator struct { + Config *clientcredentials.Config + Dry bool + Endpoint *url.URL +} + +func (r *HTTPRecovator) RevokeToken(ctx context.Context, token string) error { + var ep = *r.Endpoint + ep.Path = RevocationPath + + data := url.Values{"token": []string{token}} + hreq, err := http.NewRequest("POST", ep.String(), bytes.NewBufferString(data.Encode())) + if err != nil { + return errors.Wrap(err, "") + } + + hreq.Header.Add("Content-Type", "application/x-www-form-urlencoded") + hreq.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) + hreq.SetBasicAuth(r.Config.ClientID, r.Config.ClientSecret) + hres, err := http.DefaultClient.Do(hreq) + if err != nil { + return errors.Wrap(err, "") + } + defer hres.Body.Close() + + body, _ := ioutil.ReadAll(hres.Body) + if hres.StatusCode < 200 || hres.StatusCode >= 300 { + return errors.Errorf("Expected 2xx status code but got %d.\n%s", hres.StatusCode, body) + } + return nil +} diff --git a/oauth2/revocator_test.go b/oauth2/revocator_test.go new file mode 100644 index 00000000000..81ea5237db6 --- /dev/null +++ b/oauth2/revocator_test.go @@ -0,0 +1,118 @@ +package oauth2_test + +import ( + "net/http/httptest" + "net/url" + "testing" + "time" + + "fmt" + "github.com/Sirupsen/logrus" + "github.com/julienschmidt/httprouter" + "github.com/ory-am/fosite" + "github.com/ory-am/fosite/compose" + "github.com/ory-am/fosite/storage" + "github.com/ory-am/hydra/herodot" + "github.com/ory-am/hydra/oauth2" + "github.com/ory-am/hydra/pkg" + "golang.org/x/net/context" + "golang.org/x/oauth2/clientcredentials" +) + +var ( + revocators = make(map[string]oauth2.Revocator) + nowRecovator = time.Now().Round(time.Second) + tokensRecovator = pkg.Tokens(3) + fositeStoreRecovator = storage.NewExampleStore() +) + +func init() { + + r := httprouter.New() + serv := &oauth2.Handler{ + OAuth2: compose.Compose( + fc, + fositeStoreRecovator, + &compose.CommonStrategy{ + CoreStrategy: compose.NewOAuth2HMACStrategy(fc, []byte("1234567890123456789012345678901234567890")), + OpenIDConnectTokenStrategy: compose.NewOpenIDConnectStrategy(pkg.MustRSAKey()), + }, + compose.OAuth2TokenIntrospectionFactory, + compose.OAuth2TokenRevocationFactory, + ), + H: &herodot.JSON{}, + } + serv.SetRoutes(r) + ts = httptest.NewServer(r) + + ar := fosite.NewAccessRequest(oauth2.NewSession("alice")) + ar.GrantedScopes = fosite.Arguments{"core"} + ar.RequestedAt = nowRecovator + ar.Client = &fosite.DefaultClient{ID: "siri"} + ar.Session.SetExpiresAt(fosite.AccessToken, nowRecovator.Add(time.Hour)) + ar.Session.(*oauth2.Session).Extra = map[string]interface{}{"foo": "bar"} + fositeStoreRecovator.CreateAccessTokenSession(nil, tokensRecovator[0][0], ar) + + ar2 := fosite.NewAccessRequest(oauth2.NewSession("siri")) + ar2.GrantedScopes = fosite.Arguments{"core"} + ar2.RequestedAt = nowRecovator + ar2.Session.(*oauth2.Session).Extra = map[string]interface{}{"foo": "bar"} + ar2.Session.SetExpiresAt(fosite.AccessToken, nowRecovator.Add(time.Hour)) + ar2.Client = &fosite.DefaultClient{ID: "siri"} + fositeStoreRecovator.CreateAccessTokenSession(nil, tokensRecovator[1][0], ar2) + + ar3 := fosite.NewAccessRequest(oauth2.NewSession("siri")) + ar3.GrantedScopes = fosite.Arguments{"core"} + ar3.RequestedAt = nowRecovator + ar3.Session.(*oauth2.Session).Extra = map[string]interface{}{"foo": "bar"} + ar3.Client = &fosite.DefaultClient{ID: "doesnt-exist"} + ar3.Session.SetExpiresAt(fosite.AccessToken, nowRecovator.Add(-time.Hour)) + fositeStoreRecovator.CreateAccessTokenSession(nil, tokensRecovator[2][0], ar3) + + ep, err := url.Parse(ts.URL) + if err != nil { + logrus.Fatalf("%s", err) + } + revocators["http"] = &oauth2.HTTPRecovator{ + Endpoint: ep, + Config: &clientcredentials.Config{ + ClientID: "my-client", + ClientSecret: "foobar", + }, + } +} + +func TestRevoke(t *testing.T) { + for k, w := range revocators { + for _, c := range []struct { + token string + expectErr bool + }{ + { + token: "invalid", + expectErr: false, + }, + { + token: tokensRecovator[0][1], + expectErr: false, + }, + { + token: tokensRecovator[0][1], + expectErr: false, + }, + { + token: tokensRecovator[2][1], + expectErr: false, + }, + { + token: tokensRecovator[1][1], + expectErr: false, + }, + } { + t.Run(fmt.Sprintf("case=%s", k), func(t *testing.T) { + err := w.RevokeToken(context.Background(), c.token) + pkg.AssertError(t, c.expectErr, err) + }) + } + } +} diff --git a/oauth2/session.go b/oauth2/session.go index c180c7e8069..397a4760a00 100644 --- a/oauth2/session.go +++ b/oauth2/session.go @@ -1,25 +1,21 @@ package oauth2 import ( - "github.com/ory-am/fosite/handler/oauth2" "github.com/ory-am/fosite/handler/openid" "github.com/ory-am/fosite/token/jwt" ) type Session struct { - Subject string `json:"sub"` *openid.DefaultSession `json:"idToken"` - *oauth2.HMACSession `json:"session"` Extra map[string]interface{} `json:"extra"` } func NewSession(subject string) *Session { return &Session{ - Subject: subject, DefaultSession: &openid.DefaultSession{ Claims: new(jwt.IDTokenClaims), Headers: new(jwt.Headers), + Subject: subject, }, - HMACSession: new(oauth2.HMACSession), } } diff --git a/pkg/fosite_storer.go b/pkg/fosite_storer.go index 2cbf9746705..a80c1202f74 100644 --- a/pkg/fosite_storer.go +++ b/pkg/fosite_storer.go @@ -4,6 +4,7 @@ import ( "github.com/ory-am/fosite" "github.com/ory-am/fosite/handler/oauth2" "github.com/ory-am/fosite/handler/openid" + "golang.org/x/net/context" ) type FositeStorer interface { @@ -13,4 +14,13 @@ type FositeStorer interface { oauth2.RefreshTokenGrantStorage oauth2.ImplicitGrantStorage openid.OpenIDConnectRequestStorage + + RevokeRefreshToken(ctx context.Context, requestID string) error + + // RevokeAccessToken revokes an access token as specified in: + // https://tools.ietf.org/html/rfc7009#section-2.1 + // If the token passed to the request + // is an access token, the server MAY revoke the respective refresh + // token as well. + RevokeAccessToken(ctx context.Context, requestID string) error } diff --git a/pkg/helper/dry.go b/pkg/helper/dry.go index 9aa6aad2e2c..54f710d9821 100644 --- a/pkg/helper/dry.go +++ b/pkg/helper/dry.go @@ -3,8 +3,8 @@ package helper import ( "net/http" - "github.com/pkg/errors" "github.com/moul/http2curl" + "github.com/pkg/errors" ) func DoDryRequest(dry bool, req *http.Request) error { diff --git a/pkg/retry.go b/pkg/retry.go index 65f28891494..25947bf520c 100644 --- a/pkg/retry.go +++ b/pkg/retry.go @@ -10,7 +10,7 @@ import ( func Retry(maxWait time.Duration, failAfter time.Duration, f func() error) (err error) { var lastStart time.Time err = errors.New("Did not connect.") - loopWait := time.Millisecond * 500 + loopWait := time.Millisecond * 100 retryStart := time.Now() for retryStart.Add(failAfter).After(time.Now()) { lastStart = time.Now() diff --git a/pkg/superagent.go b/pkg/superagent.go index 6528b0bf150..5e3393f3380 100644 --- a/pkg/superagent.go +++ b/pkg/superagent.go @@ -6,8 +6,8 @@ import ( "io/ioutil" "net/http" - "github.com/pkg/errors" "github.com/ory-am/hydra/pkg/helper" + "github.com/pkg/errors" ) type SuperAgent struct { diff --git a/pkg/test_helpers.go b/pkg/test_helpers.go index 98f320701a5..ac5f464e399 100644 --- a/pkg/test_helpers.go +++ b/pkg/test_helpers.go @@ -4,11 +4,11 @@ import ( "testing" "time" - "github.com/pkg/errors" - "github.com/ory-am/fosite/fosite-example/pkg" "github.com/ory-am/fosite/handler/oauth2" + "github.com/ory-am/fosite/storage" "github.com/ory-am/fosite/token/hmac" "github.com/ory-am/ladon" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -53,8 +53,8 @@ func LadonWarden(ps map[string]ladon.Policy) ladon.Warden { } } -func FositeStore() *pkg.Store { - return pkg.NewStore() +func FositeStore() *storage.MemoryStore { + return storage.NewMemoryStore() } func Tokens(length int) (res [][]string) { diff --git a/policy/handler.go b/policy/handler.go index 6aa9a37e1c3..e1d2e1c4697 100644 --- a/policy/handler.go +++ b/policy/handler.go @@ -5,12 +5,12 @@ import ( "fmt" "net/http" - "github.com/pkg/errors" "github.com/julienschmidt/httprouter" "github.com/ory-am/hydra/firewall" "github.com/ory-am/hydra/herodot" "github.com/ory-am/ladon" "github.com/pborman/uuid" + "github.com/pkg/errors" ) const ( @@ -40,7 +40,7 @@ func (h *Handler) Find(w http.ResponseWriter, r *http.Request, _ httprouter.Para h.H.WriteErrorCode(ctx, w, r, http.StatusBadRequest, errors.New("Missing query parameter subject")) } - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ + if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: policyResource, Action: "find", }, scope); err != nil { @@ -62,7 +62,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request, _ httprouter.Pa } ctx := herodot.NewContext() - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ + if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: policyResource, Action: "create", }, scope); err != nil { @@ -89,7 +89,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request, _ httprouter.Pa func (h *Handler) Get(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { ctx := herodot.NewContext() - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ + if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: fmt.Sprintf(policiesResource, ps.ByName("id")), Action: "get", }, scope); err != nil { @@ -109,7 +109,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request, ps httprouter.P ctx := herodot.NewContext() id := ps.ByName("id") - if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{ + if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: fmt.Sprintf(policiesResource, id), Action: "get", }, scope); err != nil { diff --git a/policy/manager_test.go b/policy/manager_test.go index 64aab83ef90..cf697cbaa5a 100644 --- a/policy/manager_test.go +++ b/policy/manager_test.go @@ -9,8 +9,8 @@ import ( "github.com/julienschmidt/httprouter" "github.com/ory-am/fosite" + "github.com/ory-am/hydra/compose" "github.com/ory-am/hydra/herodot" - "github.com/ory-am/hydra/internal" "github.com/ory-am/hydra/pkg" "github.com/ory-am/ladon" "github.com/pborman/uuid" @@ -21,7 +21,7 @@ import ( var managers = map[string]ladon.Manager{} func init() { - localWarden, httpClient := internal.NewFirewall("hydra", "alice", fosite.Arguments{scope}, + localWarden, httpClient := compose.NewFirewall("hydra", "alice", fosite.Arguments{scope}, &ladon.DefaultPolicy{ ID: "1", Subjects: []string{"alice"}, diff --git a/sdk/client.go b/sdk/client.go index 2da6142af4c..bdf54eded27 100644 --- a/sdk/client.go +++ b/sdk/client.go @@ -7,7 +7,6 @@ import ( "os" "github.com/ory-am/hydra/client" - "github.com/ory-am/hydra/connection" "github.com/ory-am/hydra/jwk" hoauth2 "github.com/ory-am/hydra/oauth2" "github.com/ory-am/hydra/pkg" @@ -31,30 +30,30 @@ var defaultOptions = []option{ // Client offers easy use of all HTTP clients. type Client struct { // Clients offers OAuth2 Client management capabilities. - Clients *client.HTTPManager - - // SocialConnections offers Social Login management capabilities. - SocialConnections *connection.HTTPManager + Clients *client.HTTPManager // JSONWebKeys offers JSON Web Key management capabilities. - JSONWebKeys *jwk.HTTPManager + JSONWebKeys *jwk.HTTPManager // Policies offers Access Policy management capabilities. - Policies *policy.HTTPManager + Policies *policy.HTTPManager // Warden offers Access Token and Access Request validation strategies (for first-party resource servers). - Warden *warden.HTTPWarden + Warden *warden.HTTPWarden // Introspection offers Access Token and Access Request introspection strategies (according to RFC 7662). - Introspection *hoauth2.HTTPIntrospector - - http *http.Client - clusterURL *url.URL - clientID string - clientSecret string - skipTLSVerify bool - scopes []string - credentials clientcredentials.Config + Introspection *hoauth2.HTTPIntrospector + + // Revocation offers OAuth2 Token Revocation. + Revocator *hoauth2.HTTPRecovator + + http *http.Client + clusterURL *url.URL + clientID string + clientSecret string + skipTLSVerify bool + scopes []string + credentials clientcredentials.Config } // Connect instantiates a new client to communicate with Hydra. @@ -114,9 +113,9 @@ func Connect(opts ...option) (*Client, error) { Client: c.http, } - c.SocialConnections = &connection.HTTPManager{ - Endpoint: pkg.JoinURL(c.clusterURL, "/connections"), - Client: c.http, + c.Revocator = &hoauth2.HTTPRecovator{ + Endpoint: pkg.JoinURL(c.clusterURL, hoauth2.RevocationPath), + Config: &c.credentials, } c.Introspection = &hoauth2.HTTPIntrospector{ diff --git a/sdk/client_test.go b/sdk/client_test.go new file mode 100644 index 00000000000..7f091c8f11c --- /dev/null +++ b/sdk/client_test.go @@ -0,0 +1,15 @@ +package sdk + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestConnect(t *testing.T) { + var _, err = Connect( + ClientID("client-id"), + ClientSecret("client-secret"), + ClusterURL("https://localhost:4444"), + ) + assert.NotNil(t, err) +} diff --git a/warden/handler.go b/warden/handler.go index 7ad62dd6a12..42fedea82c1 100644 --- a/warden/handler.go +++ b/warden/handler.go @@ -5,18 +5,14 @@ import ( "net/http" "strings" - "github.com/pkg/errors" "github.com/julienschmidt/httprouter" "github.com/ory-am/hydra/config" "github.com/ory-am/hydra/firewall" "github.com/ory-am/hydra/herodot" - "github.com/ory-am/ladon" + "github.com/pkg/errors" ) const ( - // TokenValidHandlerPath points to the token validation endpoint. - TokenValidHandlerPath = "/warden/token/valid" - // TokenAllowedHandlerPath points to the token access request validation endpoint. TokenAllowedHandlerPath = "/warden/token/allowed" @@ -30,7 +26,7 @@ type wardenAuthorizedRequest struct { } type wardenAccessRequest struct { - *ladon.Request + *firewall.TokenAccessRequest *wardenAuthorizedRequest } @@ -61,47 +57,13 @@ func NewHandler(c *config.Config, router *httprouter.Router) *WardenHandler { } func (h *WardenHandler) SetRoutes(r *httprouter.Router) { - r.POST(TokenValidHandlerPath, h.TokenValid) r.POST(TokenAllowedHandlerPath, h.TokenAllowed) r.POST(AllowedHandlerPath, h.Allowed) } -func (h *WardenHandler) TokenValid(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - ctx := herodot.NewContext() - _, err := h.Warden.TokenAllowed(ctx, h.Warden.TokenFromRequest(r), &ladon.Request{ - Resource: "rn:hydra:warden:token:valid", - Action: "decide", - }, "hydra.warden") - if err != nil { - h.H.WriteError(ctx, w, r, err) - return - } - - var ar wardenAuthorizedRequest - if err := json.NewDecoder(r.Body).Decode(&ar); err != nil { - h.H.WriteError(ctx, w, r, err) - return - } - defer r.Body.Close() - - authContext, err := h.Warden.TokenValid(ctx, ar.Token, ar.Scopes...) - if err != nil { - h.H.Write(ctx, w, r, &invalid) - return - } - - h.H.Write(ctx, w, r, struct { - *firewall.Context - Valid bool `json:"valid"` - }{ - Context: authContext, - Valid: true, - }) -} - func (h *WardenHandler) Allowed(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { var ctx = herodot.NewContext() - if _, err := h.Warden.TokenAllowed(ctx, h.Warden.TokenFromRequest(r), &ladon.Request{ + if _, err := h.Warden.TokenAllowed(ctx, h.Warden.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: "rn:hydra:warden:allowed", Action: "decide", }, "hydra.warden"); err != nil { @@ -109,7 +71,7 @@ func (h *WardenHandler) Allowed(w http.ResponseWriter, r *http.Request, _ httpro return } - var access = new(ladon.Request) + var access = new(firewall.AccessRequest) if err := json.NewDecoder(r.Body).Decode(access); err != nil { h.H.WriteError(ctx, w, r, errors.Wrap(err, "")) return @@ -128,7 +90,7 @@ func (h *WardenHandler) Allowed(w http.ResponseWriter, r *http.Request, _ httpro func (h *WardenHandler) TokenAllowed(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { ctx := herodot.NewContext() - _, err := h.Warden.TokenAllowed(ctx, h.Warden.TokenFromRequest(r), &ladon.Request{ + _, err := h.Warden.TokenAllowed(ctx, h.Warden.TokenFromRequest(r), &firewall.TokenAccessRequest{ Resource: "rn:hydra:warden:token:allowed", Action: "decide", }, "hydra.warden") @@ -138,7 +100,7 @@ func (h *WardenHandler) TokenAllowed(w http.ResponseWriter, r *http.Request, _ h } var ar = wardenAccessRequest{ - Request: new(ladon.Request), + TokenAccessRequest: new(firewall.TokenAccessRequest), wardenAuthorizedRequest: new(wardenAuthorizedRequest), } if err := json.NewDecoder(r.Body).Decode(&ar); err != nil { @@ -147,7 +109,7 @@ func (h *WardenHandler) TokenAllowed(w http.ResponseWriter, r *http.Request, _ h } defer r.Body.Close() - authContext, err := h.Warden.TokenAllowed(ctx, ar.Token, ar.Request, ar.Scopes...) + authContext, err := h.Warden.TokenAllowed(ctx, ar.Token, ar.TokenAccessRequest, ar.Scopes...) if err != nil { h.H.Write(ctx, w, r, ¬Allowed) return diff --git a/warden/warden_http.go b/warden/warden_http.go index 591e1412757..5ca03e99011 100644 --- a/warden/warden_http.go +++ b/warden/warden_http.go @@ -4,11 +4,10 @@ import ( "net/http" "net/url" - "github.com/pkg/errors" "github.com/ory-am/fosite" "github.com/ory-am/hydra/firewall" "github.com/ory-am/hydra/pkg" - "github.com/ory-am/ladon" + "github.com/pkg/errors" "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" @@ -32,7 +31,7 @@ func (w *HTTPWarden) SetClient(c *clientcredentials.Config) { // This endpoint requires a token, a scope, a resource name, an action name and a context. // // The HTTP API is documented at http://docs.hdyra.apiary.io/#reference/warden:-access-control-for-resource-providers/check-if-an-access-tokens-subject-is-allowed-to-do-something -func (w *HTTPWarden) TokenAllowed(ctx context.Context, token string, a *ladon.Request, scopes ...string) (*firewall.Context, error) { +func (w *HTTPWarden) TokenAllowed(ctx context.Context, token string, a *firewall.TokenAccessRequest, scopes ...string) (*firewall.Context, error) { var resp = struct { *firewall.Context Allowed bool `json:"allowed"` @@ -46,7 +45,7 @@ func (w *HTTPWarden) TokenAllowed(ctx context.Context, token string, a *ladon.Re Token: token, Scopes: scopes, }, - Request: a, + TokenAccessRequest: a, }, &resp); err != nil { return nil, err } else if !resp.Allowed { @@ -59,7 +58,7 @@ func (w *HTTPWarden) TokenAllowed(ctx context.Context, token string, a *ladon.Re // IsAllowed checks if an arbitrary subject is allowed to perform an action on a resource. // // The HTTP API is documented at http://docs.hdyra.apiary.io/#reference/warden:-access-control-for-resource-providers/check-if-a-subject-is-allowed-to-do-something -func (w *HTTPWarden) IsAllowed(ctx context.Context, a *ladon.Request) error { +func (w *HTTPWarden) IsAllowed(ctx context.Context, a *firewall.AccessRequest) error { var allowed = struct { Allowed bool `json:"allowed"` }{} @@ -75,27 +74,3 @@ func (w *HTTPWarden) IsAllowed(ctx context.Context, a *ladon.Request) error { return nil } - -// TokenValid checks if an access token is valid. You must provide a token and a scope. -// -// The HTTP API is documented at http://docs.hdyra.apiary.io/#reference/warden:-access-control-for-resource-providers/check-if-an-access-token-is-valid -func (w *HTTPWarden) TokenValid(ctx context.Context, token string, scopes ...string) (*firewall.Context, error) { - var resp = struct { - *firewall.Context - Valid bool `json:"valid"` - }{} - - var ep = *w.Endpoint - ep.Path = TokenValidHandlerPath - agent := &pkg.SuperAgent{URL: ep.String(), Client: w.Client} - if err := agent.POST(&wardenAuthorizedRequest{ - Token: token, - Scopes: scopes, - }, &resp); err != nil { - return nil, err - } else if !resp.Valid { - return nil, errors.New("Token is not valid") - } - - return resp.Context, nil -} diff --git a/warden/warden_local.go b/warden/warden_local.go index a8f4ad4332b..b7e244fa87f 100644 --- a/warden/warden_local.go +++ b/warden/warden_local.go @@ -5,7 +5,6 @@ import ( "time" "github.com/Sirupsen/logrus" - "github.com/pkg/errors" "github.com/ory-am/fosite" "github.com/ory-am/hydra/firewall" "github.com/ory-am/hydra/oauth2" @@ -25,8 +24,13 @@ func (w *LocalWarden) TokenFromRequest(r *http.Request) string { return fosite.AccessTokenFromRequest(r) } -func (w *LocalWarden) IsAllowed(ctx context.Context, a *ladon.Request) error { - if err := w.Warden.IsAllowed(a); err != nil { +func (w *LocalWarden) IsAllowed(ctx context.Context, a *firewall.AccessRequest) error { + if err := w.Warden.IsAllowed(&ladon.Request{ + Resource: a.Resource, + Action: a.Action, + Subject: a.Subject, + Context: a.Context, + }); err != nil { logrus.WithFields(logrus.Fields{ "subject": a.Subject, "request": a, @@ -38,12 +42,12 @@ func (w *LocalWarden) IsAllowed(ctx context.Context, a *ladon.Request) error { return nil } -func (w *LocalWarden) TokenAllowed(ctx context.Context, token string, a *ladon.Request, scopes ...string) (*firewall.Context, error) { - var session = new(oauth2.Session) - var auth, err = w.OAuth2.ValidateToken(ctx, token, fosite.AccessToken, session, scopes...) +func (w *LocalWarden) TokenAllowed(ctx context.Context, token string, a *firewall.TokenAccessRequest, scopes ...string) (*firewall.Context, error) { + var session = oauth2.NewSession("") + var auth, err = w.OAuth2.IntrospectToken(ctx, token, fosite.AccessToken, session, scopes...) if err != nil { logrus.WithFields(logrus.Fields{ - "subject": a.Subject, + "subject": session.Subject, "request": a, "reason": "Token is expired, malformed or missing", }).WithError(err).Infof("Access denied") @@ -53,46 +57,21 @@ func (w *LocalWarden) TokenAllowed(ctx context.Context, token string, a *ladon.R return w.sessionAllowed(ctx, a, scopes, auth, session) } -func (w *LocalWarden) TokenValid(ctx context.Context, token string, scopes ...string) (*firewall.Context, error) { - var session = new(oauth2.Session) - var oauthRequest = fosite.NewAccessRequest(session) - - var auth, err = w.OAuth2.ValidateToken(ctx, token, fosite.AccessToken, session, scopes...) - if err != nil { - logrus.WithFields(logrus.Fields{ - "scopes": scopes, - "subject": session.Subject, - "audience": oauthRequest.GetClient().GetID(), - "reason": "Token is expired, malformed or missing", - }).WithError(err).Infof("Access denied") - return nil, err - } - - return w.newContext(auth), nil -} - -func (w *LocalWarden) sessionAllowed(ctx context.Context, a *ladon.Request, scopes []string, oauthRequest fosite.AccessRequester, session *oauth2.Session) (*firewall.Context, error) { +func (w *LocalWarden) sessionAllowed(ctx context.Context, a *firewall.TokenAccessRequest, scopes []string, oauthRequest fosite.AccessRequester, session *oauth2.Session) (*firewall.Context, error) { session = oauthRequest.GetSession().(*oauth2.Session) - if a.Subject != "" && a.Subject != session.Subject { - err := errors.Errorf("Expected subject to be %s but got %s", session.Subject, a.Subject) - logrus.WithFields(logrus.Fields{ - "scopes": scopes, - "subject": a.Subject, - "audience": oauthRequest.GetClient().GetID(), - "request": a, - "reason": "Request subject and token subject do not match", - }).WithError(err).Infof("Access denied") - return nil, err - } - a.Subject = session.Subject - if err := w.Warden.IsAllowed(a); err != nil { + if err := w.Warden.IsAllowed(&ladon.Request{ + Resource: a.Resource, + Action: a.Action, + Subject: session.Subject, + Context: a.Context, + }); err != nil { logrus.WithFields(logrus.Fields{ "scopes": scopes, - "subject": a.Subject, + "subject": session.Subject, "audience": oauthRequest.GetClient().GetID(), "request": a, - "reason": "The policy decision point denied the request", + "reason": "The policy decision point denied the request", }).WithError(err).Infof("Access denied") return nil, err } @@ -108,7 +87,7 @@ func (w *LocalWarden) newContext(oauthRequest fosite.AccessRequester) *firewall. Issuer: w.Issuer, Audience: oauthRequest.GetClient().GetID(), IssuedAt: oauthRequest.GetRequestedAt(), - ExpiresAt: session.AccessTokenExpiresAt(oauthRequest.GetRequestedAt().Add(w.AccessTokenLifespan)), + ExpiresAt: session.GetExpiresAt(fosite.AccessToken), Extra: session.Extra, } diff --git a/warden/warden_test.go b/warden/warden_test.go index f5d5d989576..484ee2831f3 100644 --- a/warden/warden_test.go +++ b/warden/warden_test.go @@ -53,7 +53,7 @@ func init() { Warden: ladonWarden, OAuth2: &fosite.Fosite{ Store: fositeStore, - TokenValidators: fosite.TokenValidators{ + TokenIntrospectionHandlers: fosite.TokenIntrospectionHandlers{ &foauth2.CoreValidator{ CoreStrategy: pkg.HMACStrategy, CoreStorage: fositeStore, @@ -83,19 +83,21 @@ func init() { ar.GrantedScopes = fosite.Arguments{"core", "hydra.warden"} ar.RequestedAt = now ar.Client = &fosite.DefaultClient{ID: "siri"} + ar.Session.SetExpiresAt(fosite.AccessToken, time.Now().Add(time.Hour).Round(time.Second)) fositeStore.CreateAccessTokenSession(nil, tokens[0][0], ar) ar2 := fosite.NewAccessRequest(oauth2.NewSession("siri")) ar2.GrantedScopes = fosite.Arguments{"core", "hydra.warden"} ar2.RequestedAt = now ar2.Client = &fosite.DefaultClient{ID: "bob"} + ar2.Session.SetExpiresAt(fosite.AccessToken, time.Now().Add(time.Hour).Round(time.Second)) fositeStore.CreateAccessTokenSession(nil, tokens[1][0], ar2) ar3 := fosite.NewAccessRequest(oauth2.NewSession("siri")) ar3.GrantedScopes = fosite.Arguments{"core", "hydra.warden"} ar3.RequestedAt = now ar3.Client = &fosite.DefaultClient{ID: "doesnt-exist"} - ar3.Session.(*oauth2.Session).AccessTokenExpiry = time.Now().Add(-time.Hour) + ar3.Session.SetExpiresAt(fosite.AccessToken, time.Now().Add(-time.Hour).Round(time.Second)) fositeStore.CreateAccessTokenSession(nil, tokens[2][0], ar3) conf := &coauth2.Config{ @@ -116,45 +118,32 @@ func TestActionAllowed(t *testing.T) { for n, w := range wardens { for k, c := range []struct { token string - req *ladon.Request + req *firewall.TokenAccessRequest scopes []string expectErr bool assert func(*firewall.Context) }{ { token: "invalid", - req: &ladon.Request{}, + req: &firewall.TokenAccessRequest{}, scopes: []string{}, expectErr: true, }, { - token: "invalid", - req: &ladon.Request{ - Subject: "mallet", - }, - scopes: []string{}, - expectErr: true, - }, - { - token: tokens[0][1], - req: &ladon.Request{ - Subject: "mallet", - }, + token: tokens[0][1], + req: &firewall.TokenAccessRequest{}, scopes: []string{"core"}, expectErr: true, }, { - token: tokens[0][1], - req: &ladon.Request{ - Subject: "alice", - }, + token: tokens[0][1], + req: &firewall.TokenAccessRequest{}, scopes: []string{"foo"}, expectErr: true, }, { token: tokens[0][1], - req: &ladon.Request{ - Subject: "alice", + req: &firewall.TokenAccessRequest{ Resource: "matrix", Action: "create", Context: ladon.Context{}, @@ -164,8 +153,7 @@ func TestActionAllowed(t *testing.T) { }, { token: tokens[0][1], - req: &ladon.Request{ - Subject: "alice", + req: &firewall.TokenAccessRequest{ Resource: "matrix", Action: "delete", Context: ladon.Context{}, @@ -175,8 +163,7 @@ func TestActionAllowed(t *testing.T) { }, { token: tokens[0][1], - req: &ladon.Request{ - Subject: "alice", + req: &firewall.TokenAccessRequest{ Resource: "matrix", Action: "create", Context: ladon.Context{}, @@ -186,8 +173,7 @@ func TestActionAllowed(t *testing.T) { }, { token: tokens[0][1], - req: &ladon.Request{ - Subject: "alice", + req: &firewall.TokenAccessRequest{ Resource: "matrix", Action: "create", Context: ladon.Context{}, @@ -215,12 +201,12 @@ func TestActionAllowed(t *testing.T) { func TestAllowed(t *testing.T) { for n, w := range wardens { for k, c := range []struct { - req *ladon.Request + req *firewall.AccessRequest expectErr bool assert func(*firewall.Context) }{ { - req: &ladon.Request{ + req: &firewall.AccessRequest{ Subject: "alice", Resource: "other-thing", Action: "create", @@ -229,7 +215,7 @@ func TestAllowed(t *testing.T) { expectErr: true, }, { - req: &ladon.Request{ + req: &firewall.AccessRequest{ Subject: "alice", Resource: "matrix", Action: "delete", @@ -238,7 +224,7 @@ func TestAllowed(t *testing.T) { expectErr: true, }, { - req: &ladon.Request{ + req: &firewall.AccessRequest{ Subject: "alice", Resource: "matrix", Action: "create", @@ -255,56 +241,3 @@ func TestAllowed(t *testing.T) { } } - -func TestTokenValid(t *testing.T) { - for n, w := range wardens { - for k, c := range []struct { - token string - scopes []string - expectErr bool - assert func(*firewall.Context) - }{ - { - token: "invalid", - expectErr: true, - }, - { - token: "invalid", - expectErr: true, - }, - { - token: tokens[0][1], - scopes: []string{"foo"}, - expectErr: true, - }, - { - token: tokens[1][1], - scopes: []string{"illegal"}, - expectErr: true, - }, - { - token: tokens[1][1], - scopes: []string{"core"}, - expectErr: false, - assert: func(c *firewall.Context) { - assert.Equal(t, "bob", c.Audience) - assert.Equal(t, "siri", c.Subject) - assert.Equal(t, "tests", c.Issuer) - assert.Equal(t, now.Add(time.Hour), c.ExpiresAt, "expires at", n) - assert.Equal(t, now, c.IssuedAt, "issued at", n) - }, - }, - { - token: tokens[2][1], - scopes: []string{"core"}, - expectErr: true, - }, - } { - ctx, err := w.TokenValid(context.Background(), c.token, c.scopes...) - pkg.AssertError(t, c.expectErr, err, "ActionAllowed case", n, k) - if err == nil && c.assert != nil { - c.assert(ctx) - } - } - } -}