` is created using HMAC-SHA256
+ and a global secret. This is what a token can look like: `/tgBeUhWlAT8tM8Bhmnx+Amf8rOYOUhrDi3pGzmjP7c=.BiV/Yhma+5moTP46anxMT6cWW8gz5R5vpC9RbpwSDdM=`
+ * **Enforcing scopes:** By default, you always need to include the `core` scope or Hydra will not execute the request.
+* Hydra uses [Ladon](https://github.com/ory-am/ladon) for policy management and access control. Ladon's API is minimalistic
+and well tested.
+* Hydra encrypts symmetric and asymmetric keys at rest using AES-GCM 256bit.
+* Hydra does not store tokens, only their signatures. An attacker gaining database access is neither able to steal tokens nor
+to issue new ones.
+* Hydra has automated unit and integration tests.
+* Hydra does not use hacks. We would rather rewrite the whole thing instead of introducing a hack.
+* APIs are uniform, well documented and secured using the warden's access control infrastructure.
+* Hydra is open source and can be reviewed by anyone.
+* Hydra is designed by a [security enthusiast](https://github.com/arekkas), who has written and participated in numerous auth* projects.
+
+Additionally to the claims above, Hydra has received a lot of positive feedback. Let's see what the community is saying:
+
+> Nice! Lowering barriers to the use of technologies like these is important.
+
+[Pyxl101](https://news.ycombinator.com/item?id=11798641)
+
+> OAuth is a framework not a protocol. The security it provides can vary greatly between implementations.
+Fosite (which is what this is based on) is a very good implementation from a security perspective: https://github.com/ory-am/fosite#a-word-on-security
+
+[abritishguy](https://news.ycombinator.com/item?id=11800515)
+
+> [...] Thanks for releasing this by the way, looks really well engineered. [...]
+
+[olalonde](https://news.ycombinator.com/item?id=11798831)
+
## Documentation
### Guide
@@ -423,7 +352,59 @@ DATABASE_URL=rethinkdb://$(docker-machine ip default):28015/hydra go run main.go
## FAQ
-### I'm having trouble with the redirect URI.
+### What is OAuth2 and what is OpenID Connect?
+
+* For OAuth2 explanation, I recommend reading the [Dropbox OAuth2 Guide](https://www.dropbox.com/developers/reference/oauth-guide)
+* For OpenID, I recommend reading [OpenID Connect explained](http://connect2id.com/learn/openid-connect)
+
+### Should I use OAuth2 tokens for authentication?
+
+OAuth2 tokens are like money. It allows you to buy stuff, but the cashier does not really care if the money is
+yours or if you stole it, as long as it's valid money. Depending on what you understand as authentication, this is a yes and no answer:
+
+* **Yes:** You can use access tokens to find out which user ("subject") is performing an action in a resource provider (blog article service, shopping basket, ...).
+Coming back to the money example: *You*, the subject, receives a cappuccino from the vendor (resource provider) in exchange for money (access token).
+* **No:** Never use access tokens for logging people in, for example `http://myapp.com/login?access_token=...`.
+Coming back to the money example: The police officer ("authentication server") will not accept money ("access token") as a proof of identity ("it's really you"). Unless he is corrupt ("vulnerable"), of course.
+
+In the second example ("authentication server"), you must use OpenID Connect ID Tokens.
+
+### Can I use Hydra in my new or existing app?
+
+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.
+
+### I'm having trouble with the redirect URI
Hydra enforces HTTPS for all hosts except localhost. Also make sure that the path is an exact match. `http://localhost:123/`
is not the same as `http://localhost:123`.
@@ -454,7 +435,7 @@ Or by specifying the following flags:
### I want to disable HTTPS for testing
-You can do so by running `hydra host --force-dangerous-http`.
+You can do so by running `hydra host --dangerous-force-http`.
### Can I set the log level to warn, error, debug, ...?
@@ -479,6 +460,17 @@ or via command line flag:
--rethink-tls-cert-path string Path to the certificate file to connect to rethinkdb over TLS (https). You can set RETHINK_TLS_CERT_PATH or RETHINK_TLS_CERT instead.
```
+### 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.
+
+### 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.
+
### Is there a client library / SDK?
Yes, for Go! It is available at `github.com/ory-am/hydra/sdk`.
@@ -585,7 +577,7 @@ Validate requests with the Warden, uses [`ory-am/hydra/warden.HTTPWarden`](warde
import "github.com/ory-am/ladon"
// Check if action is allowed
-hydra.Warden.HTTPActionAllowed(ctx, req, &ladon.Request{
+hydra.Warden.HTTPRequestAllowed(ctx, req, &ladon.Request{
Resource: "urn:media:images",
Action: "get",
Subject: "bob",
diff --git a/client/client.go b/client/client.go
index ec1cfcbadf1..15b7286aca3 100644
--- a/client/client.go
+++ b/client/client.go
@@ -1,6 +1,9 @@
package client
-import "github.com/ory-am/fosite"
+import (
+ "github.com/ory-am/fosite"
+ "strings"
+)
type Client struct {
ID string `json:"id" gorethink:"id"`
@@ -9,7 +12,7 @@ type Client struct {
RedirectURIs []string `json:"redirect_uris" gorethink:"redirect_uris"`
GrantTypes []string `json:"grant_types" gorethink:"grant_types"`
ResponseTypes []string `json:"response_types" gorethink:"response_types"`
- GrantedScopes []string `json:"granted_scopes" gorethink:"granted_scopes"`
+ Scopes string `json:"scopes" gorethink:"scopes"`
Owner string `json:"owner" gorethink:"owner"`
PolicyURI string `json:"policy_uri" gorethink:"policy_uri"`
TermsOfServiceURI string `json:"tos_uri" gorethink:"tos_uri"`
@@ -30,10 +33,8 @@ func (c *Client) GetHashedSecret() []byte {
return []byte(c.Secret)
}
-func (c *Client) GetGrantedScopes() fosite.Scopes {
- return &fosite.DefaultScopes{
- Scopes: c.GrantedScopes,
- }
+func (c *Client) GetScopes() fosite.Arguments {
+ return fosite.Arguments(strings.Split(c.Scopes, " "))
}
func (c *Client) GetGrantTypes() fosite.Arguments {
diff --git a/client/handler.go b/client/handler.go
index 6ada5eff9a8..15c7294f0d3 100644
--- a/client/handler.go
+++ b/client/handler.go
@@ -45,7 +45,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request, _ httprouter.Pa
return
}
- if _, err := h.W.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: ClientsResource,
Action: "create",
Context: ladon.Context{
@@ -80,7 +80,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request, _ httprouter.Pa
func (h *Handler) GetAll(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
var ctx = herodot.NewContext()
- if _, err := h.W.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: ClientsResource,
Action: "get",
}, Scope); err != nil {
@@ -106,13 +106,13 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request, ps httprouter.Para
var ctx = herodot.NewContext()
var id = ps.ByName("id")
- c, err := h.Manager.GetClient(id)
+ c, err := h.Manager.GetConcreteClient(id)
if err != nil {
h.H.WriteError(ctx, w, r, err)
return
}
- if _, err := h.W.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: fmt.Sprintf(ClientResource, id),
Action: "get",
Context: ladon.Context{
@@ -123,7 +123,7 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request, ps httprouter.Para
return
}
- c.(*Client).Secret = ""
+ c.Secret = ""
h.H.Write(ctx, w, r, c)
}
@@ -131,7 +131,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.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: fmt.Sprintf(ClientResource, id),
Action: "delete",
}, Scope); err != nil {
diff --git a/client/manager.go b/client/manager.go
index d38fbcae395..3e14bcd3bad 100644
--- a/client/manager.go
+++ b/client/manager.go
@@ -18,4 +18,6 @@ type Storage interface {
DeleteClient(id string) error
GetClients() (map[string]Client, error)
+
+ GetConcreteClient(id string) (*Client, error)
}
diff --git a/client/manager_http.go b/client/manager_http.go
index e1675c75059..d1d67b31f9b 100644
--- a/client/manager_http.go
+++ b/client/manager_http.go
@@ -14,7 +14,7 @@ type HTTPManager struct {
Dry bool
}
-func (m *HTTPManager) GetClient(id string) (fosite.Client, error) {
+func (m *HTTPManager) GetConcreteClient(id string) (*Client, error) {
var c Client
var r = pkg.NewSuperAgent(pkg.JoinURL(m.Endpoint, id).String())
r.Client = m.Client
@@ -26,6 +26,10 @@ func (m *HTTPManager) GetClient(id string) (fosite.Client, error) {
return &c, nil
}
+func (m *HTTPManager) GetClient(id string) (fosite.Client, error) {
+ return m.GetConcreteClient(id)
+}
+
func (m *HTTPManager) CreateClient(c *Client) error {
var r = pkg.NewSuperAgent(m.Endpoint.String())
r.Client = m.Client
diff --git a/client/manager_memory.go b/client/manager_memory.go
index 015f122f1cb..3ea968856a5 100644
--- a/client/manager_memory.go
+++ b/client/manager_memory.go
@@ -16,7 +16,7 @@ type MemoryManager struct {
sync.RWMutex
}
-func (m *MemoryManager) GetClient(id string) (fosite.Client, error) {
+func (m *MemoryManager) GetConcreteClient(id string) (*Client, error) {
m.RLock()
defer m.RUnlock()
@@ -27,6 +27,10 @@ func (m *MemoryManager) GetClient(id string) (fosite.Client, error) {
return &c, nil
}
+func (m *MemoryManager) GetClient(id string) (fosite.Client, error) {
+ return m.GetConcreteClient(id)
+}
+
func (m *MemoryManager) Authenticate(id string, secret []byte) (*Client, error) {
m.RLock()
defer m.RUnlock()
diff --git a/client/manager_rethinkdb.go b/client/manager_rethinkdb.go
index 9276e34b865..c3e6e29b43b 100644
--- a/client/manager_rethinkdb.go
+++ b/client/manager_rethinkdb.go
@@ -23,7 +23,7 @@ type RethinkManager struct {
Hasher hash.Hasher
}
-func (m *RethinkManager) GetClient(id string) (fosite.Client, error) {
+func (m *RethinkManager) GetConcreteClient(id string) (*Client, error) {
m.RLock()
defer m.RUnlock()
@@ -34,6 +34,10 @@ func (m *RethinkManager) GetClient(id string) (fosite.Client, error) {
return &c, nil
}
+func (m *RethinkManager) GetClient(id string) (fosite.Client, error) {
+ return m.GetConcreteClient(id)
+}
+
func (m *RethinkManager) Authenticate(id string, secret []byte) (*Client, error) {
m.RLock()
defer m.RUnlock()
diff --git a/client/manager_test.go b/client/manager_test.go
index fa94ca3552a..cf70e061841 100644
--- a/client/manager_test.go
+++ b/client/manager_test.go
@@ -12,6 +12,7 @@ import (
"time"
"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"
@@ -22,7 +23,6 @@ import (
"github.com/pborman/uuid"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
- "gopkg.in/ory-am/dockertest.v2"
)
var clientManagers = map[string]Storage{}
diff --git a/cmd/cli/handler_client.go b/cmd/cli/handler_client.go
index 063b7c9cc83..48a532ae89e 100644
--- a/cmd/cli/handler_client.go
+++ b/cmd/cli/handler_client.go
@@ -9,6 +9,7 @@ import (
"github.com/ory-am/hydra/config"
"github.com/ory-am/hydra/pkg"
"github.com/spf13/cobra"
+ "strings"
)
type ClientHandler struct {
@@ -24,7 +25,6 @@ func newClientHandler(c *config.Config) *ClientHandler {
}
func (h *ClientHandler) ImportClients(cmd *cobra.Command, args []string) {
- h.M.Dry = *h.Config.Dry
h.M.Endpoint = h.Config.Resolve("/clients")
h.M.Client = h.Config.OAuth2Client(cmd)
if len(args) == 0 {
@@ -52,7 +52,7 @@ func (h *ClientHandler) ImportClients(cmd *cobra.Command, args []string) {
func (h *ClientHandler) CreateClient(cmd *cobra.Command, args []string) {
var err error
- h.M.Dry = *h.Config.Dry
+ h.M.Dry, _ = cmd.Flags().GetBool("dry")
h.M.Endpoint = h.Config.Resolve("/clients")
h.M.Client = h.Config.OAuth2Client(cmd)
@@ -70,7 +70,7 @@ func (h *ClientHandler) CreateClient(cmd *cobra.Command, args []string) {
ID: id,
Secret: string(secret),
ResponseTypes: responseTypes,
- GrantedScopes: allowedScopes,
+ Scopes: strings.Join(allowedScopes, " "),
GrantTypes: grantTypes,
RedirectURIs: callbacks,
Name: name,
@@ -87,7 +87,6 @@ func (h *ClientHandler) CreateClient(cmd *cobra.Command, args []string) {
}
func (h *ClientHandler) DeleteClient(cmd *cobra.Command, args []string) {
- h.M.Dry = *h.Config.Dry
h.M.Endpoint = h.Config.Resolve("/clients")
h.M.Client = h.Config.OAuth2Client(cmd)
if len(args) == 0 {
diff --git a/cmd/cli/handler_connection.go b/cmd/cli/handler_connection.go
index 6113b330819..4370ecacdea 100644
--- a/cmd/cli/handler_connection.go
+++ b/cmd/cli/handler_connection.go
@@ -23,7 +23,7 @@ func newConnectionHandler(c *config.Config) *ConnectionHandler {
}
func (h *ConnectionHandler) CreateConnection(cmd *cobra.Command, args []string) {
- h.M.Dry = *h.Config.Dry
+ 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 {
@@ -45,7 +45,7 @@ func (h *ConnectionHandler) CreateConnection(cmd *cobra.Command, args []string)
}
func (h *ConnectionHandler) DeleteConnection(cmd *cobra.Command, args []string) {
- h.M.Dry = *h.Config.Dry
+ 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 {
diff --git a/cmd/cli/handler_jwk.go b/cmd/cli/handler_jwk.go
index 5ef53698c86..805fe3bbc5c 100644
--- a/cmd/cli/handler_jwk.go
+++ b/cmd/cli/handler_jwk.go
@@ -23,7 +23,7 @@ func newJWKHandler(c *config.Config) *JWKHandler {
}
func (h *JWKHandler) CreateKeys(cmd *cobra.Command, args []string) {
- h.M.Dry = *h.Config.Dry
+ h.M.Dry, _ = cmd.Flags().GetBool("dry")
h.M.Endpoint = h.Config.Resolve("/keys")
h.M.Client = h.Config.OAuth2Client(cmd)
if len(args) == 0 {
@@ -46,7 +46,7 @@ func (h *JWKHandler) CreateKeys(cmd *cobra.Command, args []string) {
}
func (h *JWKHandler) GetKeys(cmd *cobra.Command, args []string) {
- h.M.Dry = *h.Config.Dry
+ h.M.Dry, _ = cmd.Flags().GetBool("dry")
h.M.Endpoint = h.Config.Resolve("/keys")
h.M.Client = h.Config.OAuth2Client(cmd)
if len(args) == 0 {
@@ -68,6 +68,7 @@ func (h *JWKHandler) GetKeys(cmd *cobra.Command, args []string) {
}
func (h *JWKHandler) DeleteKeys(cmd *cobra.Command, args []string) {
+ h.M.Dry, _ = cmd.Flags().GetBool("dry")
h.M.Endpoint = h.Config.Resolve("/keys")
h.M.Client = h.Config.OAuth2Client(cmd)
if len(args) == 0 {
diff --git a/cmd/cli/handler_policy.go b/cmd/cli/handler_policy.go
index 04407f31f0c..a8e1cfba717 100644
--- a/cmd/cli/handler_policy.go
+++ b/cmd/cli/handler_policy.go
@@ -25,7 +25,7 @@ func newPolicyHandler(c *config.Config) *PolicyHandler {
}
func (h *PolicyHandler) CreatePolicy(cmd *cobra.Command, args []string) {
- h.M.Dry = *h.Config.Dry
+ h.M.Dry, _ = cmd.Flags().GetBool("dry")
h.M.Endpoint = h.Config.Resolve("/policies")
h.M.Client = h.Config.OAuth2Client(cmd)
@@ -80,7 +80,7 @@ func (h *PolicyHandler) CreatePolicy(cmd *cobra.Command, args []string) {
}
func (h *PolicyHandler) AddResourceToPolicy(cmd *cobra.Command, args []string) {
- h.M.Dry = *h.Config.Dry
+ h.M.Dry, _ = cmd.Flags().GetBool("dry")
h.M.Endpoint = h.Config.Resolve("/policies")
h.M.Client = h.Config.OAuth2Client(cmd)
@@ -112,12 +112,11 @@ func (h *PolicyHandler) AddResourceToPolicy(cmd *cobra.Command, args []string) {
}
func (h *PolicyHandler) RemoveResourceFromPolicy(cmd *cobra.Command, args []string) {
- h.M.Dry = *h.Config.Dry
fmt.Println("Not yet implemented.")
}
func (h *PolicyHandler) AddSubjectToPolicy(cmd *cobra.Command, args []string) {
- h.M.Dry = *h.Config.Dry
+ h.M.Dry, _ = cmd.Flags().GetBool("dry")
h.M.Endpoint = h.Config.Resolve("/policies")
h.M.Client = h.Config.OAuth2Client(cmd)
@@ -149,12 +148,11 @@ func (h *PolicyHandler) AddSubjectToPolicy(cmd *cobra.Command, args []string) {
}
func (h *PolicyHandler) RemoveSubjectFromPolicy(cmd *cobra.Command, args []string) {
- h.M.Dry = *h.Config.Dry
fmt.Println("Not yet implemented.")
}
func (h *PolicyHandler) AddActionToPolicy(cmd *cobra.Command, args []string) {
- h.M.Dry = *h.Config.Dry
+ h.M.Dry, _ = cmd.Flags().GetBool("dry")
h.M.Endpoint = h.Config.Resolve("/policies")
h.M.Client = h.Config.OAuth2Client(cmd)
@@ -186,12 +184,11 @@ func (h *PolicyHandler) AddActionToPolicy(cmd *cobra.Command, args []string) {
}
func (h *PolicyHandler) RemoveActionFromPolicy(cmd *cobra.Command, args []string) {
- h.M.Dry = *h.Config.Dry
fmt.Println("Not yet implemented.")
}
func (h *PolicyHandler) GetPolicy(cmd *cobra.Command, args []string) {
- h.M.Dry = *h.Config.Dry
+ h.M.Dry, _ = cmd.Flags().GetBool("dry")
h.M.Endpoint = h.Config.Resolve("/policies")
h.M.Client = h.Config.OAuth2Client(cmd)
@@ -214,7 +211,7 @@ func (h *PolicyHandler) GetPolicy(cmd *cobra.Command, args []string) {
}
func (h *PolicyHandler) DeletePolicy(cmd *cobra.Command, args []string) {
- h.M.Dry = *h.Config.Dry
+ h.M.Dry, _ = cmd.Flags().GetBool("dry")
h.M.Endpoint = h.Config.Resolve("/policies")
h.M.Client = h.Config.OAuth2Client(cmd)
diff --git a/cmd/cli/handler_warden.go b/cmd/cli/handler_warden.go
index 097c0ae1a03..f16f7dcfc19 100644
--- a/cmd/cli/handler_warden.go
+++ b/cmd/cli/handler_warden.go
@@ -24,6 +24,7 @@ func newWardenHandler(c *config.Config) *WardenHandler {
}
func (h *WardenHandler) IsAuthorized(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")
@@ -33,11 +34,7 @@ func (h *WardenHandler) IsAuthorized(cmd *cobra.Command, args []string) {
}
scopes, _ := cmd.Flags().GetStringSlice("scopes")
- if len(scopes) == 0 {
- scopes = []string{"core"}
- }
-
- res, err := h.M.Authorized(context.Background(), args[0], scopes...)
+ res, err := h.M.InspectToken(context.Background(), args[0], scopes...)
pkg.Must(err, "Could not validate token: %s", err)
out, err := json.MarshalIndent(res, "", "\t")
diff --git a/cmd/clients.go b/cmd/clients.go
index 24e5e6a4df2..adbb3ab12a2 100644
--- a/cmd/clients.go
+++ b/cmd/clients.go
@@ -26,12 +26,9 @@ var clientsCmd = &cobra.Command{
}
func init() {
- var dry bool
- c.Dry = &dry
-
RootCmd.AddCommand(clientsCmd)
+ clientsCmd.PersistentFlags().Bool("dry", false, "do not execute the command but show the corresponding curl command instead")
- clientsCmd.PersistentFlags().BoolVar(c.Dry, "dry", false, "do not execute the command but show the corresponding curl command instead")
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
@@ -41,5 +38,4 @@ func init() {
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// clientsCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
-
}
diff --git a/cmd/clients_create.go b/cmd/clients_create.go
index 8c4a57f7a7f..9c9902cdd42 100644
--- a/cmd/clients_create.go
+++ b/cmd/clients_create.go
@@ -22,6 +22,6 @@ func init() {
clientsCreateCmd.Flags().StringSliceP("callbacks", "c", []string{}, "REQUIRED list of allowed callback URLs")
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{"core"}, "A list of allowed scopes")
+ clientsCreateCmd.Flags().StringSliceP("allowed-scopes", "a", []string{""}, "A list of allowed scopes")
clientsCreateCmd.Flags().StringP("name", "n", "", "The client's name")
}
diff --git a/cmd/connect.go b/cmd/connect.go
index d8b7bd03787..d4a40b7e024 100644
--- a/cmd/connect.go
+++ b/cmd/connect.go
@@ -15,13 +15,21 @@ var connectCmd = &cobra.Command{
Use: "connect",
Short: "Connect with a cluster",
Run: func(cmd *cobra.Command, args []string) {
+ fmt.Println("To keep the current value, press enter.")
+
if u := input("Cluster URL [" + c.ClusterURL + "]: "); u != "" {
c.ClusterURL = u
}
- if u := input("Client ID: "); u != "" {
+ if u := input("Client ID [" + c.ClientID + "]: "); u != "" {
c.ClientID = u
}
- if u := input("Client Secret: "); u != "" {
+
+ secret := "*********"
+ if c.ClientSecret == "" {
+ secret = "empty"
+ }
+
+ if u := input("Client Secret [" + secret + "]: "); u != "" {
c.ClientSecret = u
}
@@ -43,9 +51,5 @@ func input(message string) string {
}
func init() {
- var dry bool
- c.Dry = &dry
-
RootCmd.AddCommand(connectCmd)
- connectCmd.PersistentFlags().BoolVar(c.Dry, "dry", false, "do not execute the command but show the corresponding curl command instead")
}
diff --git a/cmd/connections.go b/cmd/connections.go
index c4d9e15b3d8..0437c0a348a 100644
--- a/cmd/connections.go
+++ b/cmd/connections.go
@@ -13,9 +13,6 @@ Google, Twitter, or any other SSO provider.`,
}
func init() {
- var dry bool
- c.Dry = &dry
-
RootCmd.AddCommand(connectionsCmd)
- connectionsCmd.PersistentFlags().BoolVar(c.Dry, "dry", false, "do not execute the command but show the corresponding curl command instead")
+ connectionsCmd.PersistentFlags().Bool("dry", false, "do not execute the command but show the corresponding curl command instead")
}
diff --git a/cmd/host.go b/cmd/host.go
index bd82a8f4d45..26da1bbadef 100644
--- a/cmd/host.go
+++ b/cmd/host.go
@@ -1,59 +1,110 @@
package cmd
import (
- "crypto/tls"
- "net/http"
-
- "crypto/ecdsa"
- "crypto/rand"
- "crypto/rsa"
- "crypto/x509"
- "crypto/x509/pkix"
- "encoding/pem"
- "math/big"
- "time"
-
- "github.com/Sirupsen/logrus"
- "github.com/go-errors/errors"
- "github.com/julienschmidt/httprouter"
"github.com/ory-am/hydra/cmd/server"
- "github.com/ory-am/hydra/jwk"
- "github.com/ory-am/hydra/pkg"
"github.com/spf13/cobra"
- "github.com/spf13/viper"
- "github.com/square/go-jose"
-)
-
-const (
- TLSKeyName = "hydra.tls"
)
// hostCmd represents the host command
var hostCmd = &cobra.Command{
Use: "host",
Short: "Start the HTTP/2 host service",
- Long: `Starts all HTTP/2 APIs and connects to a backend.
+ Long: `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"
-This command supports the following environment variables:
+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:
- 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.
+ Example: DATABASE_URL=rethinkdb://user:password@host:123/database
+
+ Additionally, these controls are available when using RethinkDB:
+ - RETHINK_TLS_CERT_PATH: The path to the TLS certificate (pem encoded) used to connect to rethinkdb.
+ Example: RETHINK_TLS_CERT_PATH=~/rethink.pem
+
+ - RETHINK_TLS_CERT: A pem encoded TLS certificate passed as string. Can be used instead of RETHINK_TLS_CERT_PATH.
+ Example: RETHINK_TLS_CERT_PATH="-----BEGIN CERTIFICATE-----\nMIIDZTCCAk2gAwIBAgIEV5xOtDANBgkqhkiG9w0BAQ0FADA0MTIwMAYDVQQDDClP..."
- SYSTEM_SECRET: A secret that is at least 16 characters long. If none is provided, one will be generated. They key
is used to encrypt sensitive data using AES-GCM (256 bit) and validate HMAC signatures.
+ Example: SYSTEM_SECRET=jf89-jgklAS9gk3rkAF90dfsk
+
+- FORCE_ROOT_CLIENT_CREDENTIALS: On first start up, Hydra generates a root client with random id and secret. Use
+ this environment variable in the form of "FORCE_ROOT_CLIENT_CREDENTIALS=id:secret" to set
+ the client id and secret yourself.
+ Example: FORCE_ROOT_CLIENT_CREDENTIALS=admin:kf0AKfm12fas3F-.f
+
+- PORT: The port hydra should listen on.
+ Defaults to PORT=4444
+
+- HOST: The port hydra should listen on.
+ Example: PORT=localhost
+
+- BCRYPT_COST: Set the bcrypt hashing cost. This is a trade off between
+ security and performance. Range is 4 =< x =< 31.
+ Defaults to BCRYPT_COST=10
+
+
+OAUTH2 CONTROLS
+===============
+
+- CONSENT_URL: The uri of the consent endpoint.
+ Example: CONSENT_URL=https://id.myapp.com/consent
+
+- ISSUER: The issuer is used for identification in all OAuth2 tokens.
+ Defaults to ISSUER=hydra.localhost
+
+- AUTH_CODE_LIFESPAN: Lifespan of OAuth2 authorize codes. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
+ Defaults to AUTH_CODE_LIFESPAN=10m
+
+- ID_TOKEN_LIFESPAN: Lifespan of OpenID Connect ID Tokens. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
+ Defaults to AUTH_CODE_LIFESPAN=1h
+
+- ACCESS_TOKEN_LIFESPAN: Lifespan of OAuth2 access tokens. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
+ Defaults to AUTH_CODE_LIFESPAN=1h
+
+- CHALLENGE_TOKEN_LIFESPAN: Lifespan of OAuth2 consent tokens. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
+ Defaults to AUTH_CODE_LIFESPAN=10m
+
+
+HTTPS CONTROLS
+==============
+
+- HTTPS_ALLOW_TERMINATION_FROM: Whitelist one or multiple CIDR address ranges and allow them to terminate TLS connections.
+ Be aware that the X-Forwarded-Proto header must be set and must never be modifiable by anyone but
+ your proxy / gateway / load balancer. Supports ipv4 and ipv6.
+ Hydra serves http instead of https when this option is set.
+ Example: HTTPS_ALLOW_TERMINATION_FROM=127.0.0.1/32,192.168.178.0/24,2620:0:2d0:200::7/32
- HTTPS_TLS_CERT_PATH: The path to the TLS certificate (pem encoded).
+ Example: HTTPS_TLS_CERT_PATH=~/cert.pem
+
- HTTPS_TLS_KEY_PATH: The path to the TLS private key (pem encoded).
+ Example: HTTPS_TLS_KEY_PATH=~/key.pem
+
- HTTPS_TLS_CERT: A pem encoded TLS certificate passed as string. Can be used instead of HTTPS_TLS_CERT_PATH.
+ Example: HTTPS_TLS_CERT="-----BEGIN CERTIFICATE-----\nMIIDZTCCAk2gAwIBAgIEV5xOtDANBgkqhkiG9w0BAQ0FADA0MTIwMAYDVQQDDClP..."
+
- HTTPS_TLS_KEY: A pem encoded TLS key passed as string. Can be used instead of HTTPS_TLS_KEY_PATH.
+ Example: HTTPS_TLS_KEY="-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDg..."
-- RETHINK_TLS_CERT_PATH: The path to the TLS certificate (pem encoded) used to connect to rethinkdb.
-- RETHINK_TLS_CERT: A pem encoded TLS certificate passed as string. Can be used instead of RETHINK_TLS_CERT_PATH.
-- HYDRA_PROFILING: Set "HYDRA_PROFILING=1" to enable profiling.
+DEBUG CONTROLS
+==============
+
+- HYDRA_PROFILING: Set "HYDRA_PROFILING=cpu" to enable cpu profiling and "HYDRA_PROFILING=memory" to enable memory profiling.
+ It is not possible to do both at the same time.
+ Example: HYDRA_PROFILING=cpu
`,
- Run: runHostCmd,
+ Run: server.RunHost(c),
}
func init() {
@@ -67,179 +118,9 @@ func init() {
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
- hostCmd.Flags().Bool("force-dangerous-http", false, "Disable HTTP/2 over TLS (HTTPS) and serve HTTP instead. Never use this in production.")
+ hostCmd.Flags().BoolVar(&c.ForceHTTP, "dangerous-force-http", false, "Disable HTTP/2 over TLS (HTTPS) and serve HTTP instead. Never use this in production.")
hostCmd.Flags().Bool("dangerous-auto-logon", false, "Stores the root credentials in ~/.hydra.yml. Do not use in production.")
hostCmd.Flags().String("https-tls-key-path", "", "Path to the key file for HTTP/2 over TLS (https). You can set HTTPS_TLS_KEY_PATH or HTTPS_TLS_KEY instead.")
hostCmd.Flags().String("https-tls-cert-path", "", "Path to the certificate file for HTTP/2 over TLS (https). You can set HTTPS_TLS_CERT_PATH or HTTPS_TLS_CERT instead.")
hostCmd.Flags().String("rethink-tls-cert-path", "", "Path to the certificate file to connect to rethinkdb over TLS (https). You can set RETHINK_TLS_CERT_PATH or RETHINK_TLS_CERT instead.")
}
-
-func runHostCmd(cmd *cobra.Command, args []string) {
- router := httprouter.New()
- serverHandler := &server.Handler{}
- serverHandler.Start(c, router)
-
- if ok, _ := cmd.Flags().GetBool("dangerous-auto-logon"); ok {
- logrus.Warnln("Do not use flag --dangerous-auto-logon in production.")
- err := c.Persist()
- pkg.Must(err, "Could not write configuration file: %s", err)
- }
-
- http.Handle("/", router)
-
- var srv = http.Server{
- Addr: c.GetAddress(),
- TLSConfig: &tls.Config{
- Certificates: []tls.Certificate{
- getOrCreateTLSCertificate(cmd),
- },
- },
- }
-
- var err error
- logrus.Infof("Starting server on %s", c.GetAddress())
- if ok, _ := cmd.Flags().GetBool("force-dangerous-http"); ok {
- logrus.Warnln("HTTPS disabled. Never do this in production.")
- err = srv.ListenAndServe()
- } else {
- err = srv.ListenAndServeTLS("", "")
- }
- pkg.Must(err, "Could not start server: %s %s.", err)
-}
-
-func loadCertificateFromFile(cmd *cobra.Command) *tls.Certificate {
- keyPath := viper.GetString("HTTPS_TLS_KEY_PATH")
- certPath := viper.GetString("HTTPS_TLS_CERT_PATH")
- if kp, _ := cmd.Flags().GetString("https-tls-key-path"); kp != "" {
- keyPath = kp
- } else if cp, _ := cmd.Flags().GetString("https-tls-cert-path"); cp != "" {
- certPath = cp
- } else if keyPath == "" || certPath == "" {
- return nil
- }
-
- cert, err := tls.LoadX509KeyPair(certPath, keyPath)
- if err != nil {
- logrus.Warn("Could not load x509 key pair: %s", cert)
- return nil
- }
- return &cert
-}
-
-func loadCertificateFromEnv(cmd *cobra.Command) *tls.Certificate {
- keyString := viper.GetString("HTTPS_TLS_KEY")
- certString := viper.GetString("HTTPS_TLS_CERT")
- if keyString == "" || certString == "" {
- return nil
- }
-
- var cert tls.Certificate
- var err error
- if cert, err = tls.X509KeyPair([]byte(certString), []byte(keyString)); err != nil {
- logrus.Warn("Could not parse x509 key pair from env: %s", cert)
- return nil
- }
-
- return &cert
-}
-
-func getOrCreateTLSCertificate(cmd *cobra.Command) tls.Certificate {
- if cert := loadCertificateFromFile(cmd); cert != nil {
- return *cert
- } else if cert := loadCertificateFromEnv(cmd); cert != nil {
- return *cert
- }
-
- ctx := c.Context()
- keys, err := ctx.KeyManager.GetKey(TLSKeyName, "private")
- if errors.Is(err, pkg.ErrNotFound) {
- logrus.Warn("Key for TLS not found. Creating new one.")
-
- keys, err = new(jwk.ECDSA256Generator).Generate("")
- pkg.Must(err, "Could not generate key: %s", err)
-
- cert, err := createSelfSignedCertificate(jwk.First(keys.Key("private")).Key)
- pkg.Must(err, "Could not create X509 PEM Key Pair: %s", err)
-
- private := jwk.First(keys.Key("private"))
- private.Certificates = []*x509.Certificate{cert}
- keys = &jose.JsonWebKeySet{
- Keys: []jose.JsonWebKey{
- *private,
- *jwk.First(keys.Key("public")),
- },
- }
-
- err = ctx.KeyManager.AddKeySet(TLSKeyName, keys)
- pkg.Must(err, "Could not persist key: %s", err)
- } else {
- pkg.Must(err, "Could not retrieve key: %s", err)
- }
-
- private := jwk.First(keys.Key("private"))
- block, err := jwk.PEMBlockForKey(private.Key)
- if err != nil {
- pkg.Must(err, "Could not encode key to PEM: %s", err)
- }
-
- if len(private.Certificates) == 0 {
- logrus.Fatal("TLS certificate chain can not be empty")
- }
-
- pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: private.Certificates[0].Raw})
- pemKey := pem.EncodeToMemory(block)
- cert, err := tls.X509KeyPair(pemCert, pemKey)
- pkg.Must(err, "Could not decode certificate: %s", err)
-
- return cert
-}
-
-func createSelfSignedCertificate(key interface{}) (cert *x509.Certificate, err error) {
- serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
- serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
- if err != nil {
- return cert, errors.Errorf("Failed to generate serial number: %s", err)
- }
-
- certificate := &x509.Certificate{
- SerialNumber: serialNumber,
- Subject: pkix.Name{
- Organization: []string{"Hydra"},
- CommonName: "Hydra",
- },
- Issuer: pkix.Name{
- Organization: []string{"Hydra"},
- CommonName: "Hydra",
- },
- NotBefore: time.Now(),
- NotAfter: time.Now().Add(time.Hour * 24 * 7),
- KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
- ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
- BasicConstraintsValid: true,
- }
-
- certificate.IsCA = true
- certificate.KeyUsage |= x509.KeyUsageCertSign
- certificate.DNSNames = append(certificate.DNSNames, "localhost")
- der, err := x509.CreateCertificate(rand.Reader, certificate, certificate, publicKey(key), key)
- if err != nil {
- return cert, errors.Errorf("Failed to create certificate: %s", err)
- }
-
- cert, err = x509.ParseCertificate(der)
- if err != nil {
- return cert, errors.Errorf("Failed to encode private key: %s", err)
- }
- return cert, nil
-}
-
-func publicKey(key interface{}) interface{} {
- switch k := key.(type) {
- case *rsa.PrivateKey:
- return &k.PublicKey
- case *ecdsa.PrivateKey:
- return &k.PublicKey
- default:
- return nil
- }
-}
diff --git a/cmd/keys.go b/cmd/keys.go
index 7214d51e9b4..f880ca626bb 100644
--- a/cmd/keys.go
+++ b/cmd/keys.go
@@ -25,11 +25,8 @@ var keysCmd = &cobra.Command{
}
func init() {
- var dry bool
- c.Dry = &dry
-
RootCmd.AddCommand(keysCmd)
- keysCmd.PersistentFlags().BoolVar(c.Dry, "dry", false, "do not execute the command but show the corresponding curl command instead")
+ keysCmd.PersistentFlags().Bool("dry", false, "do not execute the command but show the corresponding curl command instead")
// Here you will define your flags and configuration settings.
diff --git a/cmd/policies.go b/cmd/policies.go
index 4300284175f..90c3745b389 100644
--- a/cmd/policies.go
+++ b/cmd/policies.go
@@ -11,9 +11,6 @@ var policiesCmd = &cobra.Command{
}
func init() {
- var dry bool
- c.Dry = &dry
-
RootCmd.AddCommand(policiesCmd)
- policiesCmd.PersistentFlags().BoolVar(c.Dry, "dry", false, "do not execute the command but show the corresponding curl command instead")
+ policiesCmd.PersistentFlags().Bool("dry", false, "do not execute the command but show the corresponding curl command instead")
}
diff --git a/cmd/root.go b/cmd/root.go
index b6600c1bd9b..369526f2e12 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -7,7 +7,6 @@ import (
"path/filepath"
"runtime"
"strings"
- "sync"
"github.com/ory-am/hydra/cmd/cli"
"github.com/ory-am/hydra/config"
@@ -22,14 +21,13 @@ var c = new(config.Config)
// This represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "hydra",
- Short: "Hydra is a twelve factor OAuth2 and OpenID Connect provider",
+ Short: "Hydra is a cloud native high throughput OAuth2 and OpenID Connect provider",
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
var cmdHandler = cli.NewHandler(c)
-var mutex = &sync.RWMutex{}
// 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.
@@ -57,56 +55,62 @@ func init() {
// initConfig reads in config file and ENV variables if set.
func initConfig() {
- mutex.Lock()
if cfgFile != "" {
// enable ability to specify config file via flag
viper.SetConfigFile(cfgFile)
- }
+ } else {
+ path := absPathify("$HOME")
+ if _, err := os.Stat(filepath.Join(path, ".hydra.yml")); err != nil {
+ _, _ = os.Create(filepath.Join(path, ".hydra.yml"))
+ }
- path := absPathify("$HOME")
- if _, err := os.Stat(filepath.Join(path, ".hydra.yml")); err != nil {
- _, _ = os.Create(filepath.Join(path, ".hydra.yml"))
+ viper.SetConfigType("yaml")
+ viper.SetConfigName(".hydra") // name of config file (without extension)
+ viper.AddConfigPath("$HOME") // adding home directory as first search path
}
+ viper.AutomaticEnv() // read in environment variables that match
- viper.SetConfigType("yaml")
- viper.SetConfigName(".hydra") // name of config file (without extension)
- viper.AddConfigPath("$HOME") // adding home directory as first search path
- viper.AutomaticEnv() // read in environment variables that match
+ viper.BindEnv("HOST")
+ viper.BindEnv("CLIENT_ID")
+ viper.BindEnv("CONSENT_URL")
+ viper.BindEnv("DATABASE_URL")
+ viper.BindEnv("SYSTEM_SECRET")
+ viper.BindEnv("CLIENT_SECRET")
+ viper.BindEnv("HTTPS_ALLOW_TERMINATION_FROM")
- // If a config file is found, read it in.
- if err := viper.ReadInConfig(); err != nil {
- fmt.Printf(`Config file not found because "%s"`, err)
- fmt.Println("")
- }
+ viper.BindEnv("CLUSTER_URL")
+ viper.SetDefault("CLUSTER_URL", "https://localhost:4444")
- if err := viper.Unmarshal(c); err != nil {
- fatal("Could not read config because %s.", err)
- }
+ viper.BindEnv("PORT")
+ viper.SetDefault("PORT", 4444)
- if consentURL, ok := viper.Get("CONSENT_URL").(string); ok {
- c.ConsentURL = consentURL
- }
+ viper.BindEnv("ISSUER")
+ viper.SetDefault("ISSUER", "hydra.localhost")
- if clientID, ok := viper.Get("CLIENT_ID").(string); ok {
- c.ClientID = clientID
- }
+ viper.BindEnv("BCRYPT_COST")
+ viper.SetDefault("BCRYPT_COST", 10)
- if systemSecret, ok := viper.Get("SYSTEM_SECRET").(string); ok {
- c.SystemSecret = []byte(systemSecret)
- }
+ viper.BindEnv("ACCESS_TOKEN_LIFESPAN")
+ viper.SetDefault("ACCESS_TOKEN_LIFESPAN", "1h")
- if clientSecret, ok := viper.Get("CLIENT_SECRET").(string); ok {
- c.ClientSecret = clientSecret
- }
+ viper.BindEnv("ID_TOKEN_LIFESPAN")
+ viper.SetDefault("ID_TOKEN_LIFESPAN", "1h")
+
+ viper.BindEnv("AUTH_CODE_LIFESPAN")
+ viper.SetDefault("AUTH_CODE_LIFESPAN", "10m")
+
+ viper.BindEnv("CHALLENGE_TOKEN_LIFESPAN")
+ viper.SetDefault("CHALLENGE_TOKEN_LIFESPAN", "10m")
- if databaseURL, ok := viper.Get("DATABASE_URL").(string); ok {
- c.DatabaseURL = databaseURL
+ // If a config file is found, read it in.
+ if err := viper.ReadInConfig(); err != nil {
+ fmt.Printf(`Config file not found because "%s"`, err)
+ fmt.Println("")
}
- if c.ClusterURL == "" {
- fmt.Printf("Pointing cluster at %s\n", c.GetClusterURL())
+ if err := viper.Unmarshal(c); err != nil {
+ fatal(fmt.Sprintf("Could not read config because %s.", err))
}
- mutex.Unlock()
}
func absPathify(inPath string) string {
diff --git a/cmd/root_test.go b/cmd/root_test.go
index 9b9d07e294f..c4ce3053d18 100644
--- a/cmd/root_test.go
+++ b/cmd/root_test.go
@@ -1,13 +1,12 @@
package cmd
import (
+ "fmt"
"os"
"path/filepath"
"testing"
"time"
- "fmt"
-
"github.com/pborman/uuid"
"github.com/stretchr/testify/assert"
)
@@ -23,7 +22,6 @@ func TestExecute(t *testing.T) {
for _, c := range []struct {
args []string
- timeout time.Duration
wait func() bool
expectErr bool
}{
@@ -31,13 +29,12 @@ func TestExecute(t *testing.T) {
args: []string{"host", "--dangerous-auto-logon"},
wait: func() bool {
_, err := os.Stat(path)
+ if err != nil {
+ t.Logf("Could not stat path %s because %s", path, err)
+ }
return err != nil
},
},
- {
- args: []string{"token", "user", "--no-open"},
- timeout: time.Second,
- },
{args: []string{"clients", "create", "--id", "foobarbaz"}},
{args: []string{"clients", "create", "--id", "foobarbaz", "--dry"}},
{args: []string{"clients", "delete", "foobarbaz"}},
@@ -49,6 +46,7 @@ func TestExecute(t *testing.T) {
{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{"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", "get", "foobar"}},
@@ -58,20 +56,23 @@ func TestExecute(t *testing.T) {
RootCmd.SetArgs(c.args)
t.Logf("Running command: %s", c.args)
- if c.wait != nil || c.timeout > 0 {
+ if c.wait != nil {
go func() {
assert.Nil(t, RootCmd.Execute())
}()
}
if c.wait != nil {
+ var count = 0
for c.wait() {
- time.Sleep(time.Millisecond * 500)
+ t.Logf("Config file has not been found yet, retrying attempt #%d...", count)
+ count++
+ if count > 30 {
+ t.FailNow()
+ }
+ time.Sleep(time.Second * 4)
}
- } else if c.timeout > 0 {
- time.Sleep(c.timeout)
} else {
-
assert.Equal(t, c.expectErr, RootCmd.Execute() != nil)
}
}
diff --git a/cmd/server/handler.go b/cmd/server/handler.go
index c4a728bad3a..e6c9c7d7c29 100644
--- a/cmd/server/handler.go
+++ b/cmd/server/handler.go
@@ -1,21 +1,73 @@
package server
import (
+ "crypto/tls"
+ "net/http"
+ "time"
+
"github.com/Sirupsen/logrus"
"github.com/go-errors/errors"
"github.com/julienschmidt/httprouter"
- "github.com/ory-am/fosite/handler/core"
+ "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"
"github.com/ory-am/hydra/pkg"
"github.com/ory-am/hydra/policy"
"github.com/ory-am/hydra/warden"
"github.com/ory-am/ladon"
+ "github.com/spf13/cobra"
+ "github.com/urfave/negroni"
+ "golang.org/x/net/context"
)
+func RunHost(c *config.Config) func(cmd *cobra.Command, args []string) {
+ return func(cmd *cobra.Command, args []string) {
+ router := httprouter.New()
+ serverHandler := &Handler{Config: c}
+ serverHandler.registerRoutes(router)
+
+ if ok, _ := cmd.Flags().GetBool("dangerous-auto-logon"); ok {
+ logrus.Warnln("Do not use flag --dangerous-auto-logon in production.")
+ err := c.Persist()
+ pkg.Must(err, "Could not write configuration file: %s", err)
+ }
+
+ n := negroni.New()
+ n.Use(negronilogrus.NewMiddleware())
+ n.UseFunc(serverHandler.rejectInsecureRequests)
+ n.UseHandler(router)
+
+ var srv = http.Server{
+ Addr: c.GetAddress(),
+ Handler: n,
+ TLSConfig: &tls.Config{
+ Certificates: []tls.Certificate{
+ getOrCreateTLSCertificate(cmd, c),
+ },
+ },
+ ReadTimeout: time.Second * 5,
+ WriteTimeout: time.Second * 10,
+ }
+
+ var err error
+ logrus.Infof("Setting up http server on %s", c.GetAddress())
+ if ok, _ := cmd.Flags().GetBool("dangerous-force-http"); ok {
+ logrus.Warnln("HTTPS disabled. Never do this in production.")
+ err = srv.ListenAndServe()
+ } else if c.AllowTLSTermination != "" {
+ logrus.Infoln("TLS termination enabled, disabling https.")
+ err = srv.ListenAndServe()
+ } else {
+ err = srv.ListenAndServeTLS("", "")
+ }
+ pkg.Must(err, "Could not start server: %s %s.", err)
+ }
+}
+
type Handler struct {
Clients *client.Handler
Connections *connection.Handler
@@ -23,22 +75,25 @@ type Handler struct {
OAuth2 *oauth2.Handler
Policy *policy.Handler
Warden *warden.WardenHandler
+ Config *config.Config
}
-func (h *Handler) Start(c *config.Config, router *httprouter.Router) {
+func (h *Handler) registerRoutes(router *httprouter.Router) {
+ c := h.Config
ctx := c.Context()
- // Set up warden
+ // Set up dependencies
+ injectJWKManager(c)
clientsManager := newClientManager(c)
injectFositeStore(c, clientsManager)
+ oauth2Provider := newOAuth2Provider(c, ctx.KeyManager)
+
+ // set up warden
ctx.Warden = &warden.LocalWarden{
Warden: &ladon.Ladon{
Manager: ctx.LadonManager,
},
- TokenValidator: &core.CoreValidator{
- AccessTokenStrategy: ctx.FositeStrategy,
- AccessTokenStorage: ctx.FositeStore,
- },
+ OAuth2: oauth2Provider,
Issuer: c.Issuer,
AccessTokenLifespan: c.GetAccessTokenLifespan(),
}
@@ -48,7 +103,7 @@ func (h *Handler) Start(c *config.Config, router *httprouter.Router) {
h.Keys = newJWKHandler(c, router)
h.Connections = newConnectionHandler(c, router)
h.Policy = newPolicyHandler(c, router)
- h.OAuth2 = newOAuth2Handler(c, router, h.Keys.Manager)
+ h.OAuth2 = newOAuth2Handler(c, router, ctx.KeyManager, oauth2Provider)
h.Warden = warden.NewHandler(c, router)
// Create root account if new install
@@ -58,62 +113,19 @@ func (h *Handler) Start(c *config.Config, router *httprouter.Router) {
h.createRootIfNewInstall(c)
}
-func (h *Handler) createRS256KeysIfNotExist(c *config.Config, set, lookup string) {
- ctx := c.Context()
- generator := jwk.RS256Generator{}
-
- if _, err := ctx.KeyManager.GetKey(set, lookup); errors.Is(err, pkg.ErrNotFound) {
- logrus.Warnf("Key pair for signing %s is missing. Creating new one.", set)
-
- keys, err := generator.Generate("")
- pkg.Must(err, "Could not generate %s key: %s", set, err)
-
- err = ctx.KeyManager.AddKeySet(set, keys)
- pkg.Must(err, "Could not persist %s key: %s", set, err)
- }
-}
-
-func (h *Handler) createRootIfNewInstall(c *config.Config) {
- ctx := c.Context()
-
- clients, err := h.Clients.Manager.GetClients()
- pkg.Must(err, "Could not fetch client list: %s", err)
- if len(clients) != 0 {
+func (h *Handler) rejectInsecureRequests(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
+ if r.TLS != nil || h.Config.ForceHTTP {
+ next.ServeHTTP(rw, r)
return
}
- rs, err := pkg.GenerateSecret(16)
- pkg.Must(err, "Could notgenerate secret because %s", err)
- secret := string(rs)
-
- logrus.Warn("No clients were found. Creating a temporary root client...")
- root := &client.Client{
- Name: "This temporary client is generated by hydra and is granted all of hydra's administrative privileges. It must be removed when everything is set up.",
- ResponseTypes: []string{"id_token", "code", "token"},
- GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"},
- GrantedScopes: []string{"hydra", "core", "openid", "offline"},
- RedirectURIs: []string{"http://localhost:4445/callback"},
- Secret: secret,
+ if err := h.Config.DoesRequestSatisfyTermination(r); err == nil {
+ next.ServeHTTP(rw, r)
+ return
+ } else {
+ logrus.WithError(err).Warnln("Could not serve http connection")
}
- err = h.Clients.Manager.CreateClient(root)
- pkg.Must(err, "Could not create temporary root because %s", err)
- err = ctx.LadonManager.Create(&ladon.DefaultPolicy{
- Description: "This is a policy created by hydra and issued to the first client. It grants all of hydra's administrative privileges to the client and enables the client_credentials response type.",
- Subjects: []string{root.GetID()},
- Effect: ladon.AllowAccess,
- Resources: []string{"rn:hydra:<.*>"},
- Actions: []string{"<.*>"},
- })
- pkg.Must(err, "Could not create admin policy because %s", err)
-
- c.Lock()
- c.ClientID = root.ID
- c.ClientSecret = string(secret)
- c.Unlock()
-
- logrus.Warn("Temporary root client created.")
- logrus.Warnf("client_id: %s", root.GetID())
- logrus.Warnf("client_secret: %s", string(secret))
- logrus.Warn("The root client must be removed in production. The root's credentials could be accidentally logged.")
+ ans := new(herodot.JSON)
+ ans.WriteErrorCode(context.Background(), rw, r, http.StatusBadGateway, errors.New("Can not serve request over insecure http"))
}
diff --git a/cmd/server/handler_jwk_factory.go b/cmd/server/handler_jwk_factory.go
index d5679bee050..8ab89facfd8 100644
--- a/cmd/server/handler_jwk_factory.go
+++ b/cmd/server/handler_jwk_factory.go
@@ -11,13 +11,8 @@ import (
r "gopkg.in/dancannon/gorethink.v2"
)
-func newJWKHandler(c *config.Config, router *httprouter.Router) *jwk.Handler {
+func injectJWKManager(c *config.Config) {
ctx := c.Context()
- h := &jwk.Handler{
- H: &herodot.JSON{},
- W: ctx.Warden,
- }
- h.SetRoutes(router)
switch con := ctx.Connection.(type) {
case *config.MemoryConnection:
@@ -42,7 +37,15 @@ func newJWKHandler(c *config.Config, router *httprouter.Router) *jwk.Handler {
default:
logrus.Fatalf("Unknown connection type.")
}
+}
- h.Manager = ctx.KeyManager
+func newJWKHandler(c *config.Config, router *httprouter.Router) *jwk.Handler {
+ ctx := c.Context()
+ h := &jwk.Handler{
+ H: &herodot.JSON{},
+ W: ctx.Warden,
+ Manager: ctx.KeyManager,
+ }
+ h.SetRoutes(router)
return h
}
diff --git a/cmd/server/handler_oauth2_factory.go b/cmd/server/handler_oauth2_factory.go
index 077759357d9..66ce26349d8 100644
--- a/cmd/server/handler_oauth2_factory.go
+++ b/cmd/server/handler_oauth2_factory.go
@@ -1,26 +1,14 @@
package server
import (
+ "fmt"
"net/url"
- "time"
-
"github.com/Sirupsen/logrus"
"github.com/go-errors/errors"
"github.com/julienschmidt/httprouter"
"github.com/ory-am/fosite"
- "github.com/ory-am/fosite/handler/core"
- oc "github.com/ory-am/fosite/handler/core/client"
- "github.com/ory-am/fosite/handler/core/explicit"
- "github.com/ory-am/fosite/handler/core/implicit"
- "github.com/ory-am/fosite/handler/core/refresh"
- "github.com/ory-am/fosite/handler/oidc"
- oe "github.com/ory-am/fosite/handler/oidc/explicit"
- "github.com/ory-am/fosite/handler/oidc/hybrid"
- oi "github.com/ory-am/fosite/handler/oidc/implicit"
- os "github.com/ory-am/fosite/handler/oidc/strategy"
- "github.com/ory-am/fosite/hash"
- "github.com/ory-am/fosite/token/jwt"
+ "github.com/ory-am/fosite/compose"
"github.com/ory-am/hydra/client"
"github.com/ory-am/hydra/config"
"github.com/ory-am/hydra/internal"
@@ -79,7 +67,7 @@ func injectFositeStore(c *config.Config, clients client.Manager) {
ctx.FositeStore = store
}
-func newOAuth2Handler(c *config.Config, router *httprouter.Router, km jwk.Manager) *oauth2.Handler {
+func newOAuth2Provider(c *config.Config, km jwk.Manager) fosite.OAuth2Provider {
var ctx = c.Context()
var store = ctx.FositeStore
@@ -89,96 +77,59 @@ func newOAuth2Handler(c *config.Config, router *httprouter.Router, km jwk.Manage
keys, err = new(jwk.RS256Generator).Generate("")
pkg.Must(err, "Could not generate signing key for OpenID Connect")
km.AddKeySet(oauth2.OpenIDConnectKeyName, keys)
- logrus.Warnln("Keypair generated.")
+ logrus.Infoln("Keypair generated.")
logrus.Warnln("WARNING: Automated key creation causes low entropy. Replace the keys as soon as possible.")
} else {
pkg.Must(err, "Could not fetch signing key for OpenID Connect")
}
rsaKey := jwk.MustRSAPrivate(jwk.First(keys.Keys))
-
- idStrategy := &os.DefaultStrategy{
- RS256JWTStrategy: &jwt.RS256JWTStrategy{
- PrivateKey: rsaKey,
- },
- }
-
- oauth2HandleHelper := &core.HandleHelper{
- AccessTokenStrategy: ctx.FositeStrategy,
- AccessTokenStorage: store,
- AccessTokenLifespan: c.GetAccessTokenLifespan(),
- }
-
- oidcHelper := &oidc.IDTokenHandleHelper{IDTokenStrategy: idStrategy}
-
- explicitHandler := &explicit.AuthorizeExplicitGrantTypeHandler{
- AccessTokenStrategy: ctx.FositeStrategy,
- RefreshTokenStrategy: ctx.FositeStrategy,
- AuthorizeCodeStrategy: ctx.FositeStrategy,
- AuthorizeCodeGrantStorage: store,
- AuthCodeLifespan: c.GetAuthCodeLifespan(),
- AccessTokenLifespan: c.GetAccessTokenLifespan(),
- }
-
- // The OpenID Connect Authorize Code Flow.
- oidcExplicit := &oe.OpenIDConnectExplicitHandler{
- OpenIDConnectRequestStorage: store,
- IDTokenHandleHelper: oidcHelper,
+ fc := &compose.Config{
+ AccessTokenLifespan: c.GetAccessTokenLifespan(),
+ AuthorizeCodeLifespan: c.GetAuthCodeLifespan(),
+ IDTokenLifespan: c.GetIDTokenLifespan(),
+ HashCost: c.BCryptWorkFactor,
}
+ return compose.Compose(
+ fc,
+ store,
+ &compose.CommonStrategy{
+ CoreStrategy: compose.NewOAuth2HMACStrategy(fc, c.GetSystemSecret()),
+ OpenIDConnectTokenStrategy: compose.NewOpenIDConnectStrategy(rsaKey),
+ },
+ compose.OAuth2AuthorizeExplicitFactory,
+ compose.OAuth2AuthorizeImplicitFactory,
+ compose.OAuth2ClientCredentialsGrantFactory,
+ compose.OAuth2RefreshTokenGrantFactory,
+ compose.OpenIDConnectExplicit,
+ compose.OpenIDConnectHybrid,
+ compose.OpenIDConnectImplicit,
+ )
+}
- implicitHandler := &implicit.AuthorizeImplicitGrantTypeHandler{
- AccessTokenStrategy: ctx.FositeStrategy,
- AccessTokenStorage: store,
- AccessTokenLifespan: c.GetAccessTokenLifespan(),
+func newOAuth2Handler(c *config.Config, router *httprouter.Router, km jwk.Manager, o fosite.OAuth2Provider) *oauth2.Handler {
+ if c.ConsentURL == "" {
+ proto := "https"
+ if c.ForceHTTP {
+ proto = "http"
+ }
+ host := "localhost"
+ if c.BindHost != "" {
+ host = c.BindHost
+ }
+ c.ConsentURL = fmt.Sprintf("%s://%s:%d/oauth2/consent", proto, host, c.BindPort)
}
-
consentURL, err := url.Parse(c.ConsentURL)
- pkg.Must(err, "Could not parse consent url.")
+ pkg.Must(err, "Could not parse consent url %s.", c.ConsentURL)
handler := &oauth2.Handler{
- OAuth2: &fosite.Fosite{
- Store: store,
- MandatoryScope: "core",
- AuthorizeEndpointHandlers: fosite.AuthorizeEndpointHandlers{
- explicitHandler,
- implicitHandler,
- oidcExplicit,
- &oi.OpenIDConnectImplicitHandler{
- IDTokenHandleHelper: oidcHelper,
- AuthorizeImplicitGrantTypeHandler: implicitHandler,
- },
- &hybrid.OpenIDConnectHybridHandler{
- IDTokenHandleHelper: oidcHelper,
- AuthorizeExplicitGrantTypeHandler: explicitHandler,
- AuthorizeImplicitGrantTypeHandler: implicitHandler,
- },
- },
- TokenEndpointHandlers: fosite.TokenEndpointHandlers{
- explicitHandler,
- oidcExplicit,
- &refresh.RefreshTokenGrantHandler{
- AccessTokenStrategy: ctx.FositeStrategy,
- RefreshTokenStrategy: ctx.FositeStrategy,
- RefreshTokenGrantStorage: store,
- AccessTokenLifespan: c.GetAccessTokenLifespan(),
- },
- &oc.ClientCredentialsGrantHandler{
- HandleHelper: oauth2HandleHelper,
- },
- },
- AuthorizedRequestValidators: fosite.AuthorizedRequestValidators{
- &core.CoreValidator{
- AccessTokenStrategy: ctx.FositeStrategy,
- AccessTokenStorage: store,
- },
- },
- Hasher: &hash.BCrypt{},
- },
+ ForcedHTTP: c.ForceHTTP,
+ OAuth2: o,
Consent: &oauth2.DefaultConsentStrategy{
Issuer: c.Issuer,
KeyManager: km,
- DefaultChallengeLifespan: time.Hour,
- DefaultIDTokenLifespan: time.Hour * 24,
+ DefaultChallengeLifespan: c.GetChallengeTokenLifespan(),
+ DefaultIDTokenLifespan: c.GetIDTokenLifespan(),
},
ConsentURL: *consentURL,
}
diff --git a/cmd/server/handler_test.go b/cmd/server/handler_test.go
index ac3b0189d2e..3c1b9014da2 100644
--- a/cmd/server/handler_test.go
+++ b/cmd/server/handler_test.go
@@ -9,6 +9,8 @@ import (
func TestStart(t *testing.T) {
router := httprouter.New()
- h := &Handler{}
- h.Start(&config.Config{}, router)
+ h := &Handler{
+ Config: &config.Config{},
+ }
+ h.registerRoutes(router)
}
diff --git a/cmd/server/helper_cert.go b/cmd/server/helper_cert.go
new file mode 100644
index 00000000000..3d637ed96ba
--- /dev/null
+++ b/cmd/server/helper_cert.go
@@ -0,0 +1,150 @@
+package server
+
+import (
+ "crypto/rand"
+ "crypto/tls"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "math/big"
+ "time"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/go-errors/errors"
+ "github.com/ory-am/hydra/config"
+ "github.com/ory-am/hydra/jwk"
+ "github.com/ory-am/hydra/pkg"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+ "github.com/square/go-jose"
+)
+
+const (
+ tlsKeyName = "hydra.tls"
+)
+
+func loadCertificateFromFile(cmd *cobra.Command) *tls.Certificate {
+ keyPath := viper.GetString("HTTPS_TLS_KEY_PATH")
+ certPath := viper.GetString("HTTPS_TLS_CERT_PATH")
+ if kp, _ := cmd.Flags().GetString("https-tls-key-path"); kp != "" {
+ keyPath = kp
+ } else if cp, _ := cmd.Flags().GetString("https-tls-cert-path"); cp != "" {
+ certPath = cp
+ } else if keyPath == "" || certPath == "" {
+ return nil
+ }
+
+ cert, err := tls.LoadX509KeyPair(certPath, keyPath)
+ if err != nil {
+ logrus.Warn("Could not load x509 key pair: %s", cert)
+ return nil
+ }
+ return &cert
+}
+
+func loadCertificateFromEnv() *tls.Certificate {
+ keyString := viper.GetString("HTTPS_TLS_KEY")
+ certString := viper.GetString("HTTPS_TLS_CERT")
+ if keyString == "" || certString == "" {
+ return nil
+ }
+
+ var cert tls.Certificate
+ var err error
+ if cert, err = tls.X509KeyPair([]byte(certString), []byte(keyString)); err != nil {
+ logrus.Warn("Could not parse x509 key pair from env: %s", cert)
+ return nil
+ }
+
+ return &cert
+}
+
+func getOrCreateTLSCertificate(cmd *cobra.Command, c *config.Config) tls.Certificate {
+ if cert := loadCertificateFromFile(cmd); cert != nil {
+ return *cert
+ } else if cert := loadCertificateFromEnv(); cert != nil {
+ return *cert
+ }
+
+ ctx := c.Context()
+ keys, err := ctx.KeyManager.GetKey(tlsKeyName, "private")
+ if errors.Is(err, pkg.ErrNotFound) {
+ logrus.Warn("No TLS Key / Certificate for HTTPS found. Generating self-signed certificate.")
+
+ keys, err = new(jwk.ECDSA256Generator).Generate("")
+ pkg.Must(err, "Could not generate key: %s", err)
+
+ cert, err := createSelfSignedCertificate(jwk.First(keys.Key("private")).Key)
+ pkg.Must(err, "Could not create X509 PEM Key Pair: %s", err)
+
+ private := jwk.First(keys.Key("private"))
+ private.Certificates = []*x509.Certificate{cert}
+ keys = &jose.JsonWebKeySet{
+ Keys: []jose.JsonWebKey{
+ *private,
+ *jwk.First(keys.Key("public")),
+ },
+ }
+
+ err = ctx.KeyManager.AddKeySet(tlsKeyName, keys)
+ pkg.Must(err, "Could not persist key: %s", err)
+ } else {
+ pkg.Must(err, "Could not retrieve key: %s", err)
+ }
+
+ private := jwk.First(keys.Key("private"))
+ block, err := jwk.PEMBlockForKey(private.Key)
+ if err != nil {
+ pkg.Must(err, "Could not encode key to PEM: %s", err)
+ }
+
+ if len(private.Certificates) == 0 {
+ logrus.Fatal("TLS certificate chain can not be empty")
+ }
+
+ pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: private.Certificates[0].Raw})
+ pemKey := pem.EncodeToMemory(block)
+ cert, err := tls.X509KeyPair(pemCert, pemKey)
+ pkg.Must(err, "Could not decode certificate: %s", err)
+
+ return cert
+}
+
+func createSelfSignedCertificate(key interface{}) (cert *x509.Certificate, err error) {
+ serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+ serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+ if err != nil {
+ return cert, errors.Errorf("Failed to generate serial number: %s", err)
+ }
+
+ certificate := &x509.Certificate{
+ SerialNumber: serialNumber,
+ Subject: pkix.Name{
+ Organization: []string{"Hydra"},
+ CommonName: "Hydra",
+ },
+ Issuer: pkix.Name{
+ Organization: []string{"Hydra"},
+ CommonName: "Hydra",
+ },
+ NotBefore: time.Now(),
+ NotAfter: time.Now().Add(time.Hour * 24 * 7),
+ KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+ BasicConstraintsValid: true,
+ }
+
+ certificate.IsCA = true
+ certificate.KeyUsage |= x509.KeyUsageCertSign
+ certificate.DNSNames = append(certificate.DNSNames, "localhost")
+ der, err := x509.CreateCertificate(rand.Reader, certificate, certificate, publicKey(key), key)
+ if err != nil {
+ return cert, errors.Errorf("Failed to create certificate: %s", err)
+ }
+
+ cert, err = x509.ParseCertificate(der)
+ if err != nil {
+ return cert, errors.Errorf("Failed to encode private key: %s", err)
+ }
+ return cert, nil
+}
diff --git a/cmd/server/helper_client.go b/cmd/server/helper_client.go
new file mode 100644
index 00000000000..2c0f8c9c9eb
--- /dev/null
+++ b/cmd/server/helper_client.go
@@ -0,0 +1,70 @@
+package server
+
+import (
+ "os"
+ "strings"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/ory-am/hydra/client"
+ "github.com/ory-am/hydra/config"
+ "github.com/ory-am/hydra/pkg"
+ "github.com/ory-am/ladon"
+)
+
+func (h *Handler) createRootIfNewInstall(c *config.Config) {
+ ctx := c.Context()
+
+ clients, err := h.Clients.Manager.GetClients()
+ pkg.Must(err, "Could not fetch client list: %s", err)
+ if len(clients) != 0 {
+ return
+ }
+
+ rs, err := pkg.GenerateSecret(16)
+ pkg.Must(err, "Could notgenerate secret because %s", err)
+ secret := string(rs)
+
+ id := ""
+ forceRoot := os.Getenv("FORCE_ROOT_CLIENT_CREDENTIALS")
+ if forceRoot != "" {
+ credentials := strings.Split(forceRoot, ":")
+ if len(credentials) == 2 {
+ id = credentials[0]
+ secret = credentials[1]
+ } else {
+ logrus.Warnln("You passed malformed root client credentials, falling back to random values.")
+ }
+ }
+
+ logrus.Warn("No clients were found. Creating a temporary root client...")
+ root := &client.Client{
+ ID: id,
+ Name: "This temporary client is generated by hydra and is granted all of hydra's administrative privileges. It must be removed when everything is set up.",
+ ResponseTypes: []string{"id_token", "code", "token"},
+ GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"},
+ Scopes: "hydra openid offline",
+ RedirectURIs: []string{"http://localhost:4445/callback"},
+ Secret: secret,
+ }
+
+ err = h.Clients.Manager.CreateClient(root)
+ pkg.Must(err, "Could not create temporary root because %s", err)
+ err = ctx.LadonManager.Create(&ladon.DefaultPolicy{
+ Description: "This is a policy created by hydra and issued to the first client. It grants all of hydra's administrative privileges to the client and enables the client_credentials response type.",
+ Subjects: []string{root.GetID()},
+ Effect: ladon.AllowAccess,
+ Resources: []string{"rn:hydra:<.*>"},
+ Actions: []string{"<.*>"},
+ })
+ pkg.Must(err, "Could not create admin policy because %s", err)
+
+ c.ClientID = root.ID
+ c.ClientSecret = string(secret)
+
+ logrus.Infoln("Temporary root client created.")
+ if forceRoot == "" {
+ logrus.Infoln("client_id: %s", root.GetID())
+ logrus.Infoln("client_secret: %s", string(secret))
+ logrus.Warn("WARNING: YOU MUST delete this client once in production, as credentials may have been leaked logfiles.")
+ }
+}
diff --git a/cmd/server/helper_keys.go b/cmd/server/helper_keys.go
new file mode 100644
index 00000000000..62299a46a86
--- /dev/null
+++ b/cmd/server/helper_keys.go
@@ -0,0 +1,38 @@
+package server
+
+import (
+ "crypto/ecdsa"
+ "crypto/rsa"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/go-errors/errors"
+ "github.com/ory-am/hydra/config"
+ "github.com/ory-am/hydra/jwk"
+ "github.com/ory-am/hydra/pkg"
+)
+
+func (h *Handler) createRS256KeysIfNotExist(c *config.Config, set, lookup string) {
+ ctx := c.Context()
+ generator := jwk.RS256Generator{}
+
+ if _, err := ctx.KeyManager.GetKey(set, lookup); errors.Is(err, pkg.ErrNotFound) {
+ logrus.Infof("Key pair for signing %s is missing. Creating new one.", set)
+
+ keys, err := generator.Generate("")
+ pkg.Must(err, "Could not generate %s key: %s", set, err)
+
+ err = ctx.KeyManager.AddKeySet(set, keys)
+ pkg.Must(err, "Could not persist %s key: %s", set, err)
+ }
+}
+
+func publicKey(key interface{}) interface{} {
+ switch k := key.(type) {
+ case *rsa.PrivateKey:
+ return &k.PublicKey
+ case *ecdsa.PrivateKey:
+ return &k.PublicKey
+ default:
+ return nil
+ }
+}
diff --git a/cmd/token_self.go b/cmd/token_self.go
index 15ff17d2f8e..aa55b113543 100644
--- a/cmd/token_self.go
+++ b/cmd/token_self.go
@@ -32,7 +32,6 @@ var tokenSelfCmd = &cobra.Command{
ClientSecret: c.ClientSecret,
TokenURL: pkg.JoinURLStrings(c.ClusterURL, "/oauth2/token"),
Scopes: []string{
- "core",
"hydra",
},
}
diff --git a/cmd/token_user.go b/cmd/token_user.go
index 4fec15eef84..d833d3ab655 100644
--- a/cmd/token_user.go
+++ b/cmd/token_user.go
@@ -74,7 +74,7 @@ var tokenUserCmd = &cobra.Command{
}
if r.URL.Query().Get("state") != string(state) {
- message := fmt.Sprintf("States do not match. Expected %s but got %s", string(state), r.URL.Query().Get("state"))
+ message := fmt.Sprintf("States do not match. Expected %s, got %s", string(state), r.URL.Query().Get("state"))
fmt.Println(message)
w.WriteHeader(http.StatusInternalServerError)
@@ -107,5 +107,5 @@ var tokenUserCmd = &cobra.Command{
func init() {
tokenCmd.AddCommand(tokenUserCmd)
tokenUserCmd.Flags().Bool("no-open", false, "Do not open a browser window with the authorize url")
- tokenUserCmd.Flags().StringSlice("scopes", []string{"core", "hydra", "offline", "openid"}, "Ask for specific scopes")
+ tokenUserCmd.Flags().StringSlice("scopes", []string{"hydra", "offline", "openid"}, "Ask for specific scopes")
}
diff --git a/cmd/token_validate.go b/cmd/token_validate.go
index 65d96d1e4cc..ed34f30e243 100644
--- a/cmd/token_validate.go
+++ b/cmd/token_validate.go
@@ -13,5 +13,6 @@ var tokenValidatorCmd = &cobra.Command{
func init() {
tokenCmd.AddCommand(tokenValidatorCmd)
- tokenValidatorCmd.Flags().StringSlice("scopes", []string{"core"}, "Additionally check if scope was granted")
+ tokenValidatorCmd.Flags().StringSlice("scopes", []string{""}, "Additionally check if scope was granted")
+ tokenValidatorCmd.Flags().Bool("dry", false, "do not execute the command but show the corresponding curl command instead")
}
diff --git a/config/backend_connections.go b/config/backend_connections.go
index 40825f75aa1..c17be4fda38 100644
--- a/config/backend_connections.go
+++ b/config/backend_connections.go
@@ -40,7 +40,7 @@ func (c *RethinkDBConnection) GetSession() *r.Session {
}
if err := pkg.Retry(time.Second*15, time.Minute*2, func() error {
- logrus.Infof("Connecting with RethinkDB: %s (%s) (%s)", c.URL.String(), c.URL.Host, database)
+ logrus.Infof("Connecting with RethinkDB: %s@%s/%s", username, c.URL.Host, database)
options := r.ConnectOpts{
Address: c.URL.Host,
diff --git a/config/config.go b/config/config.go
index e6b6ce35ba0..e5745ce57ae 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1,21 +1,20 @@
package config
import (
+ "crypto/sha256"
"crypto/tls"
"fmt"
"io/ioutil"
+ "net"
"net/http"
"net/url"
"os"
- "strconv"
- "sync"
+ "strings"
"time"
- "crypto/sha256"
-
"github.com/Sirupsen/logrus"
"github.com/go-errors/errors"
- "github.com/ory-am/fosite/handler/core/strategy"
+ 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"
@@ -30,85 +29,107 @@ import (
)
type Config struct {
- BindPort int `mapstructure:"port" yaml:"port,omitempty"`
-
- BindHost string `mapstructure:"host" yaml:"host,omitempty"`
-
- Issuer string `mapstructure:"issuer" yaml:"issuer,omitempty"`
-
- SystemSecret []byte `mapstructure:"system_secret" yaml:"-"`
-
- DatabaseURL string `mapstructure:"database_url" yaml:"database_url,omitempty"`
-
- ConsentURL string `mapstructure:"consent_url" yaml:"consent_url,omitempty"`
-
- ClusterURL string `mapstructure:"cluster_url" yaml:"cluster_url,omitempty"`
-
- ClientID string `mapstructure:"client_id" yaml:"client_id,omitempty"`
-
- ClientSecret string `mapstructure:"client_secret" yaml:"client_secret,omitempty"`
+ // These are used by client commands
+ ClusterURL string `mapstructure:"CLUSTER_URL" yaml:"cluster_url"`
+ ClientID string `mapstructure:"CLIENT_ID" yaml:"client_id,omitempty"`
+ ClientSecret string `mapstructure:"CLIENT_SECRET" yaml:"client_secret,omitempty"`
+
+ // These are used by the host command
+ BindPort int `mapstructure:"PORT" yaml:"-"`
+ BindHost string `mapstructure:"HOST" yaml:"-"`
+ Issuer string `mapstructure:"ISSUER" yaml:"-"`
+ SystemSecret string `mapstructure:"SYSTEM_SECRET" yaml:"-"`
+ DatabaseURL string `mapstructure:"DATABASE_URL" yaml:"-"`
+ ConsentURL string `mapstructure:"CONSENT_URL" yaml:"-"`
+ AllowTLSTermination string `mapstructure:"HTTPS_ALLOW_TERMINATION_FROM" yaml:"-"`
+ BCryptWorkFactor int `mapstructure:"BCRYPT_COST" yaml:"-"`
+ AccessTokenLifespan string `mapstructure:"ACCESS_TOKEN_LIFESPAN" yaml:"-"`
+ AuthCodeLifespan string `mapstructure:"AUTH_CODE_LIFESPAN" yaml:"-"`
+ IDTokenLifespan string `mapstructure:"ID_TOKEN_LIFESPAN" yaml:"-"`
+ ChallengeTokenLifespan string `mapstructure:"CHALLENGE_TOKEN_LIFESPAN" yaml:"-"`
+ ForceHTTP bool `yaml:"-"`
+
+ cluster *url.URL `yaml:"-"`
+ oauth2Client *http.Client `yaml:"-"`
+ context *Context `yaml:"-"`
+}
- ForceHTTP bool `mapstructure:"foolishly_force_http" yaml:"-"`
+func matchesRange(r *http.Request, ranges []string) error {
+ ip, _, err := net.SplitHostPort(r.RemoteAddr)
+ if err != nil {
+ return errors.New(err)
+ }
- Dry *bool `mapstructure:"-" yaml:"-"`
+ for _, rn := range ranges {
+ _, cidr, err := net.ParseCIDR(rn)
+ if err != nil {
+ return errors.New(err)
+ }
+ addr := net.ParseIP(ip)
+ if cidr.Contains(addr) {
+ return nil
+ }
+ }
+ return errors.New("Remote address does not match any cidr ranges")
+}
- AccessTokenLifespan time.Duration
- AuthCodeLifespan time.Duration
+func (c *Config) DoesRequestSatisfyTermination(r *http.Request) error {
+ if c.AllowTLSTermination == "" {
+ return errors.New("TLS termination is not enabled")
+ }
- cluster *url.URL
+ ranges := strings.Split(c.AllowTLSTermination, ",")
+ if err := matchesRange(r, ranges); err != nil {
+ return err
+ }
- oauth2Client *http.Client
+ proto := r.Header.Get("X-Forwarded-Proto")
+ if proto == "" {
+ return errors.New("X-Forwarded-Proto header is missing")
+ } else if proto != "https" {
+ return errors.Errorf("Expected X-Forwarded-Proto header to be https, got %s", proto)
+ }
- context *Context
+ return nil
+}
- sync.Mutex
+func (c *Config) GetChallengeTokenLifespan() time.Duration {
+ d, err := time.ParseDuration(c.ChallengeTokenLifespan)
+ if err != nil {
+ logrus.Warnf("Could not parse challenge token lifespan value (%s). Defaulting to 10m", c.AccessTokenLifespan)
+ return time.Minute * 10
+ }
+ return d
}
func (c *Config) GetAccessTokenLifespan() time.Duration {
- if c.AuthCodeLifespan == 0 {
+ d, err := time.ParseDuration(c.AccessTokenLifespan)
+ if err != nil {
+ logrus.Warnf("Could not parse access token lifespan value (%s). Defaulting to 1h", c.AccessTokenLifespan)
return time.Hour
}
- return c.AccessTokenLifespan
+ return d
}
func (c *Config) GetAuthCodeLifespan() time.Duration {
- if c.AuthCodeLifespan == 0 {
+ d, err := time.ParseDuration(c.AuthCodeLifespan)
+ if err != nil {
+ logrus.Warnf("Could not parse auth code lifespan value (%s). Defaulting to 10m", c.AuthCodeLifespan)
return time.Minute * 10
}
- return c.AuthCodeLifespan
+ return d
}
-func (c *Config) GetClusterURL() string {
- c.Lock()
- defer c.Unlock()
-
- if c.ClusterURL == "" {
- bindHost := c.BindHost
- if bindHost == "" {
- bindHost = "localhost"
- }
-
- schema := "https"
- if c.ForceHTTP {
- schema = "http"
- }
-
- port := strconv.Itoa(c.BindPort)
- if c.BindPort == 0 {
- port = "4444"
- }
-
- c.ClusterURL = schema + "://" + bindHost + ":" + port
+func (c *Config) GetIDTokenLifespan() time.Duration {
+ d, err := time.ParseDuration(c.IDTokenLifespan)
+ if err != nil {
+ logrus.Warnf("Could not parse id token lifespan value (%s). Defaulting to 1h", c.IDTokenLifespan)
+ return time.Hour
}
-
- return c.ClusterURL
+ return d
}
func (c *Config) Context() *Context {
- secret := c.GetSystemSecret()
- c.Lock()
- defer c.Unlock()
-
if c.context != nil {
return c.context
}
@@ -155,12 +176,12 @@ func (c *Config) Context() *Context {
c.context = &Context{
Connection: connection,
Hasher: &hash.BCrypt{
- WorkFactor: 11,
+ WorkFactor: c.BCryptWorkFactor,
},
LadonManager: manager,
- FositeStrategy: &strategy.HMACSHAStrategy{
+ FositeStrategy: &foauth2.HMACSHAStrategy{
Enigma: &hmac.HMACStrategy{
- GlobalSecret: secret,
+ GlobalSecret: c.GetSystemSecret(),
},
AccessTokenLifespan: c.GetAccessTokenLifespan(),
AuthorizeCodeLifespan: c.GetAuthCodeLifespan(),
@@ -171,9 +192,6 @@ func (c *Config) Context() *Context {
}
func (c *Config) Resolve(join ...string) *url.URL {
- c.Lock()
- defer c.Unlock()
-
if c.cluster == nil {
cluster, err := url.Parse(c.ClusterURL)
c.cluster = cluster
@@ -188,9 +206,6 @@ func (c *Config) Resolve(join ...string) *url.URL {
}
func (c *Config) OAuth2Client(cmd *cobra.Command) *http.Client {
- c.Lock()
- defer c.Unlock()
-
if c.oauth2Client != nil {
return c.oauth2Client
}
@@ -199,10 +214,7 @@ func (c *Config) OAuth2Client(cmd *cobra.Command) *http.Client {
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
TokenURL: pkg.JoinURLStrings(c.ClusterURL, "/oauth2/token"),
- Scopes: []string{
- "core",
- "hydra",
- },
+ Scopes: []string{"hydra"},
}
ctx := context.Background()
@@ -216,9 +228,8 @@ func (c *Config) OAuth2Client(cmd *cobra.Command) *http.Client {
_, err := oauthConfig.Token(ctx)
if err != nil {
fmt.Printf("Could not authenticate, because: %s\n", err)
- fmt.Println("Did you forget to log on? Run `hydra connect`.")
- fmt.Println("Did you run Hydra without a valid TLS certificate? Make sure to use the `--skip-tls-verify` flag.")
- fmt.Println("Did you know you can skip `hydra connect` when running `hydra host --dangerous-auto-logon`? DO NOT use this flag in production!")
+ fmt.Println("This can have multiple reasons, like a wrong cluster or wrong credentials. To resolve this, run `hydra connect`.")
+ fmt.Println("You can disable TLS verification using the `--skip-tls-verify` flag.")
os.Exit(1)
}
@@ -227,59 +238,41 @@ func (c *Config) OAuth2Client(cmd *cobra.Command) *http.Client {
}
func (c *Config) GetSystemSecret() []byte {
- c.Lock()
- defer c.Unlock()
-
- if len(c.SystemSecret) >= 16 {
- hash := sha256.Sum256(c.SystemSecret)
- c.SystemSecret = hash[:]
- return c.SystemSecret
+ var secret = []byte(c.SystemSecret)
+ if len(secret) >= 16 {
+ hash := sha256.Sum256(secret)
+ secret = hash[:]
+ c.SystemSecret = string(secret)
+ return secret
}
- logrus.Warnf("Expected system secret to be at least %d characters long but only got %d characters.", 32, len(c.SystemSecret))
- logrus.Warnln("Generating a random system secret...")
+ logrus.Warnf("Expected system secret to be at least %d characters long, got %d characters.", 32, len(c.SystemSecret))
+ logrus.Infoln("Generating a random system secret...")
var err error
- c.SystemSecret, err = pkg.GenerateSecret(32)
+ secret, err = pkg.GenerateSecret(32)
pkg.Must(err, "Could not generate global secret: %s", err)
- logrus.Warnf("Generated system secret: %s", c.SystemSecret)
- logrus.Warnln("Do not auto-generate system secrets in production.")
- hash := sha256.Sum256(c.SystemSecret)
- c.SystemSecret = hash[:]
- return c.SystemSecret
+ logrus.Infof("Generated system secret: %s", secret)
+ hash := sha256.Sum256(secret)
+ secret = hash[:]
+ c.SystemSecret = string(secret)
+ logrus.Warnln("WARNING: DO NOT generate system secrets in production. The secret will be leaked to the logs.")
+ return secret
}
func (c *Config) GetAddress() string {
- c.Lock()
- defer c.Unlock()
-
- if c.BindPort == 0 {
- c.BindPort = 4444
- }
return fmt.Sprintf("%s:%d", c.BindHost, c.BindPort)
}
-func (c *Config) GetIssuer() string {
- c.Lock()
- defer c.Unlock()
-
- if c.Issuer == "" {
- c.Issuer = "hydra"
- }
- return c.Issuer
-}
-
func (c *Config) Persist() error {
- _ = c.GetIssuer()
- _ = c.GetAddress()
- _ = c.GetClusterURL()
-
out, err := yaml.Marshal(c)
if err != nil {
return errors.New(err)
}
+ logrus.Infof("Persisting config in file %s", viper.ConfigFileUsed())
if err := ioutil.WriteFile(viper.ConfigFileUsed(), out, 0700); err != nil {
return errors.Errorf(`Could not write to "%s" because: %s`, viper.ConfigFileUsed(), err)
}
+
return nil
}
diff --git a/config/context.go b/config/context.go
index efb036cd4bd..e93e2dce092 100644
--- a/config/context.go
+++ b/config/context.go
@@ -1,7 +1,7 @@
package config
import (
- "github.com/ory-am/fosite/handler/core"
+ "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"
@@ -15,7 +15,7 @@ type Context struct {
Hasher hash.Hasher
Warden firewall.Firewall
LadonManager ladon.Manager
- FositeStrategy core.CoreStrategy
+ FositeStrategy oauth2.CoreStrategy
FositeStore pkg.FositeStorer
KeyManager jwk.Manager
}
diff --git a/connection/handler.go b/connection/handler.go
index 15ad63f5488..d486ef58b1e 100644
--- a/connection/handler.go
+++ b/connection/handler.go
@@ -52,7 +52,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request, ps httprouter.P
var conn Connection
var ctx = context.Background()
- if _, err := h.W.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: connectionsResource,
Action: "create",
}, scope); err != nil {
@@ -85,7 +85,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request, ps httprouter.P
func (h *Handler) FindLocal(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
var ctx = context.Background()
- if _, err := h.W.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: connectionsResource,
Action: "find",
}, scope); err != nil {
@@ -105,7 +105,7 @@ func (h *Handler) FindLocal(w http.ResponseWriter, r *http.Request, ps httproute
func (h *Handler) FindRemote(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
var ctx = context.Background()
- if _, err := h.W.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: connectionsResource,
Action: "find",
}, scope); err != nil {
@@ -126,7 +126,7 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request, ps httprouter.Para
var ctx = context.Background()
var id = ps.ByName("id")
- if _, err := h.W.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: fmt.Sprintf(connectionResource, id),
Action: "get",
}, scope); err != nil {
@@ -147,7 +147,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request, ps httprouter.P
var ctx = context.Background()
var id = ps.ByName("id")
- if _, err := h.W.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: fmt.Sprintf(connectionResource, id),
Action: "delete",
}, scope); err != nil {
diff --git a/connection/manager_test.go b/connection/manager_test.go
index de45c84f8c4..d95ce8c6868 100644
--- a/connection/manager_test.go
+++ b/connection/manager_test.go
@@ -11,6 +11,7 @@ import (
"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"
@@ -21,7 +22,6 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
r "gopkg.in/dancannon/gorethink.v2"
- "gopkg.in/ory-am/dockertest.v2"
)
var connections = map[string]*Connection{
diff --git a/doc.go b/doc.go
new file mode 100644
index 00000000000..0bd3c58d177
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,9 @@
+// Package hydra is an api-only cloud native OAuth2 and OpenID Connect provider that integrates with existing authentication mechanisms:
+//
+// 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 available through Docker 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 throughput environments. Check out the below siege benchmark on a Macbook Pro Late 2013, connected to RethinkDB validating access tokens.
+//
+// The official repository is located at https://github.com/ory-am/hydra
+package main
\ No newline at end of file
diff --git a/firewall/warden.go b/firewall/warden.go
index bb12049c026..b4d242b4c3e 100644
--- a/firewall/warden.go
+++ b/firewall/warden.go
@@ -1,3 +1,4 @@
+// Package firewall defines an API for validating access requests.
package firewall
import (
@@ -8,19 +9,50 @@ import (
"golang.org/x/net/context"
)
+// Context contains an access token's session data
type Context struct {
- Subject string `json:"sub"`
- GrantedScopes []string `json:"scopes"`
- Issuer string `json:"iss"`
- Audience string `json:"aud"`
- IssuedAt time.Time `json:"iat"`
- ExpiresAt time.Time `json:"exp"`
+ Subject string `json:"sub"`
+ GrantedScopes []string `json:"scopes"`
+ Issuer string `json:"iss"`
+ Audience string `json:"aud"`
+ IssuedAt time.Time `json:"iat"`
+ ExpiresAt time.Time `json:"exp"`
+ Extra map[string]interface{} `json:"ext"`
}
+// Firewall offers various validation strategies for access tokens.
type Firewall interface {
- Authorized(ctx context.Context, token string, scopes ...string) (*Context, error)
- HTTPAuthorized(ctx context.Context, r *http.Request, scopes ...string) (*Context, error)
+ Introspector
- ActionAllowed(ctx context.Context, token string, accessRequest *ladon.Request, scopes ...string) (*Context, error)
- HTTPActionAllowed(ctx context.Context, r *http.Request, accessRequest *ladon.Request, scopes ...string) (*Context, error)
+ // InspectToken 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.
+ InspectToken(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.
+ IsAllowed(ctx context.Context, accessRequest *ladon.Request) 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.
+ TokenAllowed(ctx context.Context, token string, accessRequest *ladon.Request, scopes ...string) (*Context, error)
+
+ // TokenFromRequest returns an access token from the HTTP Authorization header.
+ TokenFromRequest(r *http.Request) string
+}
+
+// Introspection contains an access token's session data as specified by IETF RFC 7662.
+type Introspection struct {
+ Active bool `json:"active"`
+ Scope string `json:"scope,omitempty"`
+ ClientID string `json:"client_id,omitempty"`
+ Subject string `json:"sub,omitempty"`
+ ExpiresAt int64 `json:"exp,omitempty"`
+ IssuedAt int64 `json:"iat,omitempty"`
+ NotBefore int64 `json:"nbf,omitempty"`
+ Username int64 `json:"username,omitempty"`
+ Audience string `json:"aud,omitempty"`
+ Issuer string `json:"iss,omitempty"`
+}
+
+// Introspector is capable of introspecting an access token according to IETF RFC 7662.
+type Introspector interface {
+ IntrospectToken(ctx context.Context, token string) (*Introspection, error)
}
diff --git a/glide.lock b/glide.lock
index 544672880fe..e070d55a8a6 100644
--- a/glide.lock
+++ b/glide.lock
@@ -1,24 +1,24 @@
-hash: 175acff756341dd7fa75a3e1e2e1a042a957a1b2482f017ae0bb38a2e8b9c625
-updated: 2016-07-24T16:54:50.0589572+02:00
+hash: ebc1878cf8d6949bda749aa41fd3f5272edcdee5e7dcae7ca2bfce4f6af02511
+updated: 2016-08-09T10:22:22.712279877+02:00
imports:
- name: github.com/asaskevich/govalidator
- version: 593d64559f7600f29581a3ee42177f5dbded27a9
+ version: 7664702784775e51966f0885f5cd27435916517b
- name: github.com/BurntSushi/toml
version: 99064174e013895bbd9b025c31100bd1d9b590ca
- name: github.com/cenk/backoff
version: cdf48bbc1eb78d1349cbda326a4a037f7ba565c6
- name: github.com/davecgh/go-spew
- version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
+ version: 2df174808ee097f90d259e432cc04442cf60be21
subpackages:
- spew
- name: github.com/dgrijalva/jwt-go
- version: 01aeca54ebda6e0fbfafd0a524d234159c05ec20
+ version: 268038b363c7a8d7306b8e35bf77a1fde4b0c402
- name: github.com/fsnotify/fsnotify
version: a8a77c9133d2d6fd8334f3260d06f60e8d80a5fb
- name: github.com/go-errors/errors
version: a41850380601eeb43f4350f7d17c6bbd8944aaf8
- name: github.com/golang/protobuf
- version: 874264fbbb43f4d91e999fecb4b40143ed611400
+ version: c3cefd437628a0b7d31b34fe44b3a7a540e98527
subpackages:
- proto
- name: github.com/hailocab/go-hostpool
@@ -37,88 +37,100 @@ imports:
- name: github.com/inconshreveable/mousetrap
version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
- name: github.com/julienschmidt/httprouter
- version: fb79d6a91d3e4a9ecb6d945b218d78fc0d9b1939
+ version: 8c199fb6259ffc1af525cc3ad52ee60ba8359669
+- name: github.com/kr/fs
+ version: 2788f0dbd16903de03cb8186e5c7d97b69ad387b
- name: github.com/magiconair/properties
- version: af14024f63beeb153d0048591b39c5788f21cc24
+ version: b3f6dd549956e8a61ea4a686a1c02a33d5bdda4b
+- name: github.com/meatballhat/negroni-logrus
+ version: 7c570a907cfc69cdc004ad506c6f5e234815b936
- name: github.com/mitchellh/mapstructure
- version: 21a35fb16463dfb7c8eee579c65d995d95e64d1e
+ version: ca63d7c062ee3c9f34db231e352b60012b4fd0c1
- name: github.com/moul/http2curl
version: b1479103caacaa39319f75e7f57fc545287fca0d
- name: github.com/oleiade/reflections
- version: 632977f98cd34d217c4b57d0840ec188b3d3dcaf
+ version: ec27669d960a245738b87ffa688dac28fa288c33
- name: github.com/ory-am/common
- version: 930cc805232909c38f2e68310b1e21f71b056d59
+ version: d93c852f2d09c219fd058756caf67bbdf8cf4be4
subpackages:
- pkg
- rand/sequence
- compiler
+ - env
- name: github.com/ory-am/fosite
- version: 4d0a5450dd3b44e44f5169f90b3591566a6eef1d
- subpackages:
- - fosite-example/store
- - handler/core
- - handler/core/client
- - handler/core/explicit
- - handler/core/implicit
- - handler/core/refresh
- - handler/core/strategy
- - handler/oidc
- - handler/oidc/explicit
- - handler/oidc/hybrid
- - handler/oidc/implicit
- - handler/oidc/strategy
+ version: 66b53a903c03950ac5180dc30c3f69e477344205
+ subpackages:
+ - compose
+ - fosite-example/pkg
+ - handler/oauth2
+ - handler/openid
- hash
- token/hmac
- token/jwt
- rand
- name: github.com/ory-am/ladon
- version: 940d26e2cc46679ee472225e84ca353ff0fdb43e
+ version: 67845728bf072d2b3f050cb415ece9a54ec6a546
+- name: github.com/parnurzeal/gorequest
+ version: b64673b971a1742b8ba91f228f1c029632d4b686
- name: github.com/pborman/uuid
- version: c55201b036063326c5b1b89ccfe45a184973d073
+ version: a97ce2ca70fa5a848076093f05e639a89ca34d06
- name: github.com/pkg/errors
- version: 1d2e60385a13aaa66134984235061c2f9302520e
+ version: 01fa4104b9c248c8945d14d9f128454d5b28d595
- name: github.com/pkg/profile
- version: 7b053ad66e2a49baca9cc97b982dcea0e182bda4
+ version: 1c16f117a3ab788fdf0e334e623b8bccf5679866
+- name: github.com/pkg/sftp
+ version: a71e8f580e3b622ebff585309160b1cc549ef4d2
- name: github.com/pmezard/go-difflib
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
subpackages:
- difflib
- name: github.com/Sirupsen/logrus
- version: a283a10442df8dc09befd873fab202bf8a253d6a
+ version: 4b6ea7319e214d98c938f12692336f7ca9348d6b
+- name: github.com/spf13/afero
+ version: cc9c21814bb945440253108c4d3c65c85aac3c68
+ subpackages:
+ - mem
+ - sftp
- name: github.com/spf13/cast
- version: 27b586b42e29bec072fe7379259cc719e1289da6
+ version: e31f36ffc91a2ba9ddb72a4b6a607ff9b3d3cb63
- name: github.com/spf13/cobra
- version: f62e98d28ab7ad31d707ba837a966378465c7b57
+ version: 7c674d9e72017ed25f6d2b5e497a1368086b6a6f
- name: github.com/spf13/jwalterweatherman
version: 33c24e77fb80341fe7130ee7c594256ff08ccc46
- name: github.com/spf13/pflag
- version: 1560c1005499d61b80f865c04d39ca7505bf7f0b
+ version: f676131e2660dc8cd88de99f7486d34aa8172635
- name: github.com/spf13/viper
- version: b53595fb56a492ecef90ee0457595a999eb6ec15
+ version: 346299ea79e446ebdddb834371ceba2e5926b732
- name: github.com/square/go-jose
- version: f7dab0d84e417e827829f248ef197af7d87c714c
+ version: a3927f83df1b1516f9e9dec71839c93e6bcf1db0
subpackages:
- json
- - cipher
- name: github.com/stretchr/testify
- version: d77da356e56a7428ad25149ca77381849a6a5232
+ version: f390dcf405f7b83c997eac1b06768bb9f44dec18
subpackages:
- assert
- require
- name: github.com/toqueteos/webbrowser
version: 21fc9f95c83442fd164094666f7cb4f9fdd56cd6
+- name: github.com/urfave/negroni
+ version: fde5e16d32adc7ad637e9cd9ad21d4ebc6192535
- name: golang.org/x/crypto
- version: 911fafb28f4ee7c7bd483539a6c96190bbbccc3f
+ version: e0d166c33c321d0ff863f459a5882096e334f508
subpackages:
- bcrypt
- pbkdf2
- blowfish
+ - ssh
+ - curve25519
+ - ed25519
+ - ed25519/internal/edwards25519
- name: golang.org/x/net
- version: 4d38db76854b199960801a1734443fd02870d7e1
+ version: 075e191f18186a8ff2becaf64478e30f4545cdad
subpackages:
- context
+ - publicsuffix
- name: golang.org/x/oauth2
- version: 1364adb2c63445016c5ed4518fc71f6a3cda6169
+ version: 04e1573abc896e70388bd387a69753c378d46466
subpackages:
- clientcredentials
- internal
@@ -126,8 +138,13 @@ imports:
version: a646d33e2ee3172a661fc09bca23bb4889a41bc8
subpackages:
- unix
+- name: golang.org/x/text
+ version: 2910a502d2bf9e43193af9d68ca516529614eed3
+ subpackages:
+ - transform
+ - unicode/norm
- name: google.golang.org/appengine
- version: 267c27e7492265b84fc6719503b14a1e17975d79
+ version: b4728023490a62e70ba739ff62aa65ffcca84210
subpackages:
- urlfetch
- internal
@@ -137,17 +154,32 @@ imports:
- internal/log
- internal/remote_api
- name: gopkg.in/dancannon/gorethink.v2
- version: c5ed2858e1bf5840d4d1e8897467dd433a80a64d
+ version: 27d3045458910e2fc56025a0b52caaaa96414a26
subpackages:
- encoding
- ql2
- types
-- name: gopkg.in/dgrijalva/jwt-go.v2
- version: 268038b363c7a8d7306b8e35bf77a1fde4b0c402
- name: gopkg.in/fatih/pool.v2
version: 20a0a429c5f93de45c90f5f09ea297c25e0929b3
+- name: gopkg.in/square/go-jose.v1
+ version: a3927f83df1b1516f9e9dec71839c93e6bcf1db0
+ subpackages:
+ - cipher
+ - json
- name: gopkg.in/tylerb/graceful.v1
version: c838c13b2beeea4f4f54496da96a3a6ae567c37a
- name: gopkg.in/yaml.v2
version: e4d366fc3c7938e2958e662b4258c7a89e1f0e3e
-devImports: []
+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
diff --git a/glide.yaml b/glide.yaml
index 7f436e65122..8170488bc2f 100644
--- a/glide.yaml
+++ b/glide.yaml
@@ -1,46 +1,66 @@
package: github.com/ory-am/hydra
import:
- package: github.com/Sirupsen/logrus
+ version: ~0.10.0
- package: github.com/asaskevich/govalidator
-- package: github.com/dgrijalva/jwt-go
+ version: ~4.0.0
+- package: gopkg.in/dancannon/gorethink.v2
+ version: ~2.1.3
- package: github.com/go-errors/errors
- package: github.com/julienschmidt/httprouter
+ version: ~1.1.0
+- package: github.com/meatballhat/negroni-logrus
+- package: github.com/moul/http2curl
- package: github.com/ory-am/common
subpackages:
- pkg
- rand/sequence
- package: github.com/ory-am/fosite
+ version: ~0.2.3
subpackages:
- - fosite-example/store
- - handler/core
- - handler/core/client
- - handler/core/explicit
- - handler/core/implicit
- - handler/core/refresh
- - handler/core/strategy
- - handler/oidc
- - handler/oidc/explicit
- - handler/oidc/hybrid
- - handler/oidc/implicit
- - handler/oidc/strategy
+ - compose
+ - fosite-example/pkg
+ - handler/oauth2
+ - handler/openid
- hash
- token/hmac
- token/jwt
- package: github.com/ory-am/ladon
+ version: ~0.2.0
- package: github.com/pborman/uuid
+ version: ~1.0.0
+- package: github.com/pkg/errors
+ version: ~0.7.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
subpackages:
- json
- package: github.com/stretchr/testify
+ version: ~1.1.3
subpackages:
- assert
- require
+- package: github.com/toqueteos/webbrowser
+ version: ~1.0.0
+- package: github.com/urfave/negroni
+ version: ~0.2.0
- package: golang.org/x/net
subpackages:
- context
- package: golang.org/x/oauth2
subpackages:
- clientcredentials
+- package: github.com/dgrijalva/jwt-go
+ version: ~2.7.0
+- package: gopkg.in/tylerb/graceful.v1
+ version: ~1.2.11
- package: gopkg.in/yaml.v2
+testImport:
+- package: github.com/gorilla/mux
+ version: ~1.1.0
+- package: github.com/ory-am/dockertest
+ version: ~2.2.2
diff --git a/internal/firewall.go b/internal/firewall.go
index b5355122019..e776c46a71a 100644
--- a/internal/firewall.go
+++ b/internal/firewall.go
@@ -5,7 +5,7 @@ import (
"time"
"github.com/ory-am/fosite"
- "github.com/ory-am/fosite/handler/core"
+ foauth2 "github.com/ory-am/fosite/handler/oauth2"
"github.com/ory-am/hydra/firewall"
. "github.com/ory-am/hydra/oauth2"
"github.com/ory-am/hydra/pkg"
@@ -33,14 +33,20 @@ func NewFirewall(issuer string, subject string, scopes fosite.Arguments, p ...la
return &warden.LocalWarden{
Warden: ladonWarden,
- TokenValidator: &core.CoreValidator{
- AccessTokenStrategy: pkg.HMACStrategy,
- AccessTokenStorage: fositeStore,
+ OAuth2: &fosite.Fosite{
+ Store: fositeStore,
+ TokenValidators: fosite.TokenValidators{
+ &foauth2.CoreValidator{
+ CoreStrategy: pkg.HMACStrategy,
+ CoreStorage: fositeStore,
+ ScopeStrategy: fosite.HierarchicScopeStrategy,
+ },
+ },
+ ScopeStrategy: fosite.HierarchicScopeStrategy,
},
Issuer: issuer,
AccessTokenLifespan: time.Hour,
- },
- conf.Client(oauth2.NoContext, &oauth2.Token{
+ }, conf.Client(oauth2.NoContext, &oauth2.Token{
AccessToken: tokens[0][1],
Expiry: time.Now().Add(time.Hour),
TokenType: "bearer",
diff --git a/internal/fosite_store_rethinkdb.go b/internal/fosite_store_rethinkdb.go
index 8c8c0417530..858a3d993e3 100644
--- a/internal/fosite_store_rethinkdb.go
+++ b/internal/fosite_store_rethinkdb.go
@@ -91,7 +91,7 @@ func (s *FositeRehinkDBStore) publishInsert(table r.Term, id string, requester f
ID: id,
RequestedAt: requester.GetRequestedAt(),
Client: requester.GetClient().(*client.Client),
- Scopes: requester.GetScopes(),
+ Scopes: requester.GetRequestedScopes(),
GrantedScopes: requester.GetGrantedScopes(),
Form: requester.GetRequestForm(),
Session: sess,
@@ -107,8 +107,38 @@ func (s *FositeRehinkDBStore) publishDelete(table r.Term, id string) error {
}
return nil
}
+
+func waitFor(i RDBItems, id string) error {
+ c := make(chan bool)
+
+ go func() {
+ loopWait := time.Millisecond
+ _, ok := i[id]
+ for !ok {
+ time.Sleep(loopWait)
+ loopWait = loopWait * time.Duration(int64(2))
+ if loopWait > time.Second {
+ loopWait = time.Second
+ }
+ _, ok = i[id]
+ }
+
+ c <- true
+ }()
+
+ select {
+ case <-c:
+ return nil
+ case <-time.After(time.Minute / 2):
+ return errors.New("Timed out waiting for write confirmation")
+ }
+}
+
func (s *FositeRehinkDBStore) CreateOpenIDConnectSession(_ context.Context, authorizeCode string, requester fosite.Requester) error {
- return s.publishInsert(s.IDSessionsTable, authorizeCode, requester)
+ if err := s.publishInsert(s.IDSessionsTable, authorizeCode, requester); err != nil {
+ return err
+ }
+ return waitFor(s.IDSessions, authorizeCode)
}
func (s *FositeRehinkDBStore) GetOpenIDConnectSession(_ context.Context, authorizeCode string, requester fosite.Requester) (fosite.Requester, error) {
@@ -126,7 +156,10 @@ func (s *FositeRehinkDBStore) DeleteOpenIDConnectSession(_ context.Context, auth
}
func (s *FositeRehinkDBStore) CreateAuthorizeCodeSession(_ context.Context, code string, requester fosite.Requester) error {
- return s.publishInsert(s.AuthorizeCodesTable, code, requester)
+ if err := s.publishInsert(s.AuthorizeCodesTable, code, requester); err != nil {
+ return err
+ }
+ return waitFor(s.AuthorizeCodes, code)
}
func (s *FositeRehinkDBStore) GetAuthorizeCodeSession(_ context.Context, code string, sess interface{}) (fosite.Requester, error) {
@@ -145,7 +178,10 @@ func (s *FositeRehinkDBStore) DeleteAuthorizeCodeSession(_ context.Context, code
}
func (s *FositeRehinkDBStore) CreateAccessTokenSession(_ context.Context, signature string, requester fosite.Requester) error {
- return s.publishInsert(s.AccessTokensTable, signature, requester)
+ if err := s.publishInsert(s.AccessTokensTable, signature, requester); err != nil {
+ return err
+ }
+ return waitFor(s.AccessTokens, signature)
}
func (s *FositeRehinkDBStore) GetAccessTokenSession(_ context.Context, signature string, sess interface{}) (fosite.Requester, error) {
@@ -164,7 +200,10 @@ func (s *FositeRehinkDBStore) DeleteAccessTokenSession(_ context.Context, signat
}
func (s *FositeRehinkDBStore) CreateRefreshTokenSession(_ context.Context, signature string, requester fosite.Requester) error {
- return s.publishInsert(s.RefreshTokensTable, signature, requester)
+ if err := s.publishInsert(s.RefreshTokensTable, signature, requester); err != nil {
+ return err
+ }
+ return waitFor(s.RefreshTokens, signature)
}
func (s *FositeRehinkDBStore) GetRefreshTokenSession(_ context.Context, signature string, sess interface{}) (fosite.Requester, error) {
@@ -183,7 +222,10 @@ func (s *FositeRehinkDBStore) DeleteRefreshTokenSession(_ context.Context, signa
}
func (s *FositeRehinkDBStore) CreateImplicitAccessTokenSession(_ context.Context, code string, req fosite.Requester) error {
- return s.publishInsert(s.ImplicitTable, code, req)
+ if err := s.publishInsert(s.ImplicitTable, code, req); err != nil {
+ return err
+ }
+ return waitFor(s.Implicit, code)
}
func (s *FositeRehinkDBStore) PersistAuthorizeCodeGrantSession(ctx context.Context, authorizeCode, accessSignature, refreshSignature string, request fosite.Requester) error {
diff --git a/internal/fosite_store_test.go b/internal/fosite_store_test.go
index dca21084094..7f0b6c4bfcb 100644
--- a/internal/fosite_store_test.go
+++ b/internal/fosite_store_test.go
@@ -8,13 +8,13 @@ import (
"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"
"golang.org/x/net/context"
r "gopkg.in/dancannon/gorethink.v2"
- "gopkg.in/ory-am/dockertest.v2"
)
var rethinkManager *FositeRehinkDBStore
@@ -112,8 +112,6 @@ func TestColdStartRethinkManager(t *testing.T) {
err = m.CreateAccessTokenSession(ctx, id, &defaultRequest)
pkg.AssertError(t, false, err)
- time.Sleep(100 * time.Millisecond)
-
_, err = m.GetAuthorizeCodeSession(ctx, id, &testSession{})
pkg.AssertError(t, false, err)
_, err = m.GetAccessTokenSession(ctx, id, &testSession{})
@@ -145,8 +143,6 @@ func TestCreateGetDeleteAuthorizeCodes(t *testing.T) {
err = m.CreateAuthorizeCodeSession(ctx, "4321", &defaultRequest)
pkg.AssertError(t, false, err, "%s", k)
- time.Sleep(100 * time.Millisecond)
-
res, err := m.GetAuthorizeCodeSession(ctx, "4321", &testSession{})
pkg.RequireError(t, false, err, "%s", k)
c.AssertObjectKeysEqual(t, &defaultRequest, res, "Scopes", "GrantedScopes", "Form", "Session")
@@ -170,8 +166,6 @@ func TestCreateGetDeleteAccessTokenSession(t *testing.T) {
err = m.CreateAccessTokenSession(ctx, "4321", &defaultRequest)
pkg.AssertError(t, false, err, "%s", k)
- time.Sleep(100 * time.Millisecond)
-
res, err := m.GetAccessTokenSession(ctx, "4321", &testSession{})
pkg.RequireError(t, false, err, "%s", k)
c.AssertObjectKeysEqual(t, &defaultRequest, res, "Scopes", "GrantedScopes", "Form", "Session")
@@ -195,8 +189,6 @@ func TestCreateGetDeleteOpenIDConnectSession(t *testing.T) {
err = m.CreateOpenIDConnectSession(ctx, "4321", &defaultRequest)
pkg.AssertError(t, false, err, "%s", k)
- time.Sleep(100 * time.Millisecond)
-
res, err := m.GetOpenIDConnectSession(ctx, "4321", &fosite.Request{
Session: &testSession{},
})
@@ -222,8 +214,6 @@ func TestCreateGetDeleteRefreshTokenSession(t *testing.T) {
err = m.CreateRefreshTokenSession(ctx, "4321", &defaultRequest)
pkg.AssertError(t, false, err, "%s", k)
- time.Sleep(100 * time.Millisecond)
-
res, err := m.GetRefreshTokenSession(ctx, "4321", &testSession{})
pkg.RequireError(t, false, err, "%s", k)
c.AssertObjectKeysEqual(t, &defaultRequest, res, "Scopes", "GrantedScopes", "Form", "Session")
diff --git a/jwk/handler.go b/jwk/handler.go
index 61db796bd25..dcbd1ef0201 100644
--- a/jwk/handler.go
+++ b/jwk/handler.go
@@ -60,7 +60,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.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: "rn:hydra:keys:" + setName + ":" + keyName,
Action: "delete",
}, "hydra.keys.delete"); err != nil {
@@ -80,7 +80,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.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: "rn:hydra:keys:" + setName,
Action: "delete",
}, "hydra.keys.delete"); err != nil {
@@ -101,7 +101,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.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: "rn:hydra:keys:" + set,
Action: "create",
}, "hydra.keys.create"); err != nil {
@@ -139,7 +139,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.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: "rn:hydra:keys:" + set,
Action: "update",
}, "hydra.keys.update"); err != nil {
@@ -178,7 +178,7 @@ func (h *Handler) UpdateKey(w http.ResponseWriter, r *http.Request, ps httproute
return
}
- if _, err := h.W.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: "rn:hydra:keys:" + set + ":" + key.KeyID,
Action: "update",
}, "hydra.keys.update"); err != nil {
@@ -199,7 +199,7 @@ 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.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: "rn:hydra:keys:" + setName + ":" + keyName,
Action: "get",
}, "hydra.keys.get"); err != nil {
@@ -227,7 +227,7 @@ func (h *Handler) GetKeySet(w http.ResponseWriter, r *http.Request, ps httproute
}
for _, key := range keys.Keys {
- if _, err := h.W.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: "rn:hydra:keys:" + setName + ":" + key.KeyID,
Action: "get",
}, "hydra.keys.get"); err != nil {
diff --git a/jwk/manager_rethinkdb.go b/jwk/manager_rethinkdb.go
index bf6b8540e3d..d97660e9057 100644
--- a/jwk/manager_rethinkdb.go
+++ b/jwk/manager_rethinkdb.go
@@ -7,6 +7,8 @@ import (
"time"
+ "fmt"
+
"github.com/Sirupsen/logrus"
"github.com/go-errors/errors"
"github.com/ory-am/hydra/pkg"
@@ -239,7 +241,7 @@ func (m *RethinkManager) ColdStart() error {
for clients.Next(&raw) {
pt, err := m.Cipher.Decrypt(raw.Key)
if err != nil {
- return errors.New(err)
+ return errors.New(fmt.Sprintf("Could not decrypt JSON Web Keys because: %s. This usually happens when a wrong system secret is being used", err.Error()))
}
if err := json.Unmarshal(pt, &key); err != nil {
diff --git a/jwk/manager_test.go b/jwk/manager_test.go
index db4ca6d82a0..15d83d29313 100644
--- a/jwk/manager_test.go
+++ b/jwk/manager_test.go
@@ -6,6 +6,7 @@ import (
"testing"
"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"
@@ -22,7 +23,6 @@ import (
"github.com/ory-am/fosite/rand"
"github.com/square/go-jose"
"golang.org/x/net/context"
- "gopkg.in/ory-am/dockertest.v2"
)
var managers = map[string]Manager{}
diff --git a/main.go b/main.go
index 030c463796a..7b02fd387d5 100644
--- a/main.go
+++ b/main.go
@@ -9,8 +9,10 @@ import (
)
func main() {
- if os.Getenv("HYDRA_PROFILING") == "1" {
- defer profile.Start().Stop()
+ if os.Getenv("PROFILING") == "cpu" {
+ defer profile.Start(profile.CPUProfile).Stop()
+ } else if os.Getenv("PROFILING") == "memory" {
+ defer profile.Start(profile.MemProfile).Stop()
}
switch os.Getenv("LOG_LEVEL") {
diff --git a/oauth2/consent_strategy.go b/oauth2/consent_strategy.go
index d8ade698200..c5479bf9d49 100644
--- a/oauth2/consent_strategy.go
+++ b/oauth2/consent_strategy.go
@@ -6,13 +6,13 @@ import (
"crypto/rsa"
+ "github.com/dgrijalva/jwt-go"
"github.com/go-errors/errors"
"github.com/ory-am/fosite"
- "github.com/ory-am/fosite/handler/oidc/strategy"
+ "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"
- "gopkg.in/dgrijalva/jwt-go.v2"
)
const (
@@ -66,19 +66,29 @@ func (s *DefaultConsentStrategy) ValidateResponse(a fosite.AuthorizeRequester, t
a.GrantScope(scope)
}
+ var idExt map[string]interface{}
+ var atExt map[string]interface{}
+ if ext, ok := t.Claims["id_ext"].(map[string]interface{}); ok {
+ idExt = ext
+ }
+ if ext, ok := t.Claims["id_ext"].(map[string]interface{}); ok {
+ atExt = ext
+ }
+
return &Session{
Subject: subject,
- DefaultSession: &strategy.DefaultSession{
+ DefaultSession: &openid.DefaultSession{
Claims: &ejwt.IDTokenClaims{
Audience: a.GetClient().GetID(),
Subject: subject,
Issuer: s.Issuer,
IssuedAt: time.Now(),
ExpiresAt: time.Now().Add(s.DefaultIDTokenLifespan),
- Extra: t.Claims,
+ Extra: idExt,
},
Headers: &ejwt.Headers{},
},
+ Extra: atExt,
}, err
}
@@ -106,7 +116,7 @@ func (s *DefaultConsentStrategy) IssueChallenge(authorizeRequest fosite.Authoriz
token := jwt.New(jwt.SigningMethodRS256)
token.Claims = map[string]interface{}{
"jti": uuid.New(),
- "scp": authorizeRequest.GetScopes(),
+ "scp": authorizeRequest.GetRequestedScopes(),
"aud": authorizeRequest.GetClient().GetID(),
"exp": time.Now().Add(s.DefaultChallengeLifespan).Unix(),
"redir": redirectURL,
diff --git a/oauth2/handler.go b/oauth2/handler.go
index 3f0d125a6fd..be6b020b30e 100644
--- a/oauth2/handler.go
+++ b/oauth2/handler.go
@@ -7,9 +7,6 @@ import (
"github.com/go-errors/errors"
"github.com/julienschmidt/httprouter"
"github.com/ory-am/fosite"
- csh "github.com/ory-am/fosite/handler/core/strategy"
- "github.com/ory-am/fosite/handler/oidc/strategy"
- "github.com/ory-am/fosite/token/jwt"
"github.com/ory-am/hydra/pkg"
)
@@ -18,8 +15,9 @@ const (
)
type Handler struct {
- OAuth2 fosite.OAuth2Provider
- Consent ConsentStrategy
+ OAuth2 fosite.OAuth2Provider
+ Consent ConsentStrategy
+ ForcedHTTP bool
ConsentURL url.URL
}
@@ -28,19 +26,14 @@ func (h *Handler) SetRoutes(r *httprouter.Router) {
r.POST("/oauth2/token", h.TokenHandler)
r.GET("/oauth2/auth", h.AuthHandler)
r.POST("/oauth2/auth", h.AuthHandler)
+ r.GET("/oauth2/consent", h.DefaultConsentHandler)
}
func (o *Handler) TokenHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
- var session = Session{
- DefaultSession: &strategy.DefaultSession{
- Claims: new(jwt.IDTokenClaims),
- Headers: new(jwt.Headers),
- HMACSession: new(csh.HMACSession),
- },
- }
+ var session = NewSession("")
var ctx = fosite.NewContext()
- accessRequest, err := o.OAuth2.NewAccessRequest(ctx, r, &session)
+ accessRequest, err := o.OAuth2.NewAccessRequest(ctx, r, session)
if err != nil {
pkg.LogError(err)
o.OAuth2.WriteAccessError(w, accessRequest, err)
@@ -49,6 +42,11 @@ func (o *Handler) TokenHandler(w http.ResponseWriter, r *http.Request, _ httprou
if accessRequest.GetGrantTypes().Exact("client_credentials") {
session.Subject = accessRequest.GetClient().GetID()
+ for _, scope := range accessRequest.GetRequestedScopes() {
+ if fosite.HierarchicScopeStrategy(accessRequest.GetClient().GetScopes(), scope) {
+ accessRequest.GrantScope(scope)
+ }
+ }
}
accessResponse, err := o.OAuth2.NewAccessResponse(ctx, r, accessRequest)
@@ -105,9 +103,10 @@ func (o *Handler) AuthHandler(w http.ResponseWriter, r *http.Request, _ httprout
func (o *Handler) redirectToConsent(w http.ResponseWriter, r *http.Request, authorizeRequest fosite.AuthorizeRequester) error {
schema := "https"
- if r.TLS == nil {
+ if o.ForcedHTTP {
schema = "http"
}
+
challenge, err := o.Consent.IssueChallenge(authorizeRequest, schema+"://"+r.Host+r.URL.String())
if err != nil {
return err
diff --git a/oauth2/handler_consent.go b/oauth2/handler_consent.go
new file mode 100644
index 00000000000..65e446f2699
--- /dev/null
+++ b/oauth2/handler_consent.go
@@ -0,0 +1,30 @@
+package oauth2
+
+import (
+ "net/http"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/julienschmidt/httprouter"
+)
+
+func (o *Handler) DefaultConsentHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+ logrus.Warnln("It looks like no consent endpoint was set. All OAuth2 flows except client credentials will fail.")
+
+ w.Write([]byte(`
+
+
+ Misconfigured consent endpoint
+
+
+
+ It looks like you forgot to set the consent endpoint url, which can be set using the CONSENT_ENDPOINT
+ environment variable.
+
+
+ If you are an administrator, please read
+ the guide to understand what you need to do. If you are a user, please contact the administrator.
+
+
+
+`))
+}
diff --git a/oauth2/oauth2_auth_code_test.go b/oauth2/oauth2_auth_code_test.go
index 6f2797b69b7..c13565850af 100644
--- a/oauth2/oauth2_auth_code_test.go
+++ b/oauth2/oauth2_auth_code_test.go
@@ -6,6 +6,7 @@ import (
"testing"
"time"
+ "github.com/dgrijalva/jwt-go"
"github.com/go-errors/errors"
"github.com/julienschmidt/httprouter"
ejwt "github.com/ory-am/fosite/token/jwt"
@@ -15,7 +16,6 @@ import (
"github.com/pborman/uuid"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
- "gopkg.in/dgrijalva/jwt-go.v2"
)
func TestAuthCode(t *testing.T) {
@@ -39,6 +39,7 @@ func TestAuthCode(t *testing.T) {
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
"aud": "app-client",
+ "scp": []string{"hydra"},
})
pkg.RequireError(t, false, err)
diff --git a/oauth2/oauth2_test.go b/oauth2/oauth2_test.go
index 4469f365b24..f7c9636aab8 100644
--- a/oauth2/oauth2_test.go
+++ b/oauth2/oauth2_test.go
@@ -10,12 +10,8 @@ import (
"github.com/go-errors/errors"
"github.com/julienschmidt/httprouter"
"github.com/ory-am/fosite"
- "github.com/ory-am/fosite/handler/core"
- "github.com/ory-am/fosite/handler/core/client"
- "github.com/ory-am/fosite/handler/core/explicit"
- "github.com/ory-am/fosite/handler/core/strategy"
+ "github.com/ory-am/fosite/compose"
"github.com/ory-am/fosite/hash"
- "github.com/ory-am/fosite/token/hmac"
hc "github.com/ory-am/hydra/client"
"github.com/ory-am/hydra/internal"
"github.com/ory-am/hydra/jwk"
@@ -23,7 +19,7 @@ import (
"github.com/ory-am/hydra/pkg"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
- "gopkg.in/dgrijalva/jwt-go.v2"
+ "github.com/dgrijalva/jwt-go"
)
var hasher = &hash.BCrypt{}
@@ -41,60 +37,37 @@ var store = &internal.FositeMemoryStore{
}
var keyManager = &jwk.MemoryManager{}
-
var keyGenerator = &jwk.RS256Generator{}
-var hmacStrategy = &strategy.HMACSHAStrategy{
- Enigma: &hmac.HMACStrategy{
- GlobalSecret: []byte("some-super-cool-secret-that-nobody-knows"),
- },
- AuthorizeCodeLifespan: time.Hour,
- AccessTokenLifespan: time.Hour,
-}
-
-var authCodeHandler = &explicit.AuthorizeExplicitGrantTypeHandler{
- AccessTokenStrategy: hmacStrategy,
- RefreshTokenStrategy: hmacStrategy,
- AuthorizeCodeStrategy: hmacStrategy,
- AuthorizeCodeGrantStorage: store,
- AuthCodeLifespan: time.Hour,
- AccessTokenLifespan: time.Hour,
-}
-
+var fc = &compose.Config{}
var handler = &Handler{
- OAuth2: &fosite.Fosite{
- Store: store,
- MandatoryScope: "hydra",
- AuthorizeEndpointHandlers: fosite.AuthorizeEndpointHandlers{
- authCodeHandler,
+ OAuth2: compose.Compose(
+ fc,
+ store,
+ &compose.CommonStrategy{
+ CoreStrategy: compose.NewOAuth2HMACStrategy(fc, []byte("some super secret secret")),
+ OpenIDConnectTokenStrategy: compose.NewOpenIDConnectStrategy(pkg.MustRSAKey()),
},
- TokenEndpointHandlers: fosite.TokenEndpointHandlers{
- authCodeHandler,
- &client.ClientCredentialsGrantHandler{
- HandleHelper: &core.HandleHelper{
- AccessTokenStrategy: hmacStrategy,
- AccessTokenStorage: store,
- AccessTokenLifespan: time.Hour,
- },
- },
- },
- AuthorizedRequestValidators: fosite.AuthorizedRequestValidators{},
- Hasher: hasher,
- },
+ compose.OAuth2AuthorizeExplicitFactory,
+ compose.OAuth2AuthorizeImplicitFactory,
+ compose.OAuth2ClientCredentialsGrantFactory,
+ compose.OAuth2RefreshTokenGrantFactory,
+ compose.OpenIDConnectExplicit,
+ compose.OpenIDConnectHybrid,
+ compose.OpenIDConnectImplicit,
+ ),
Consent: &DefaultConsentStrategy{
- Issuer: "https://hydra.localhost",
+ Issuer: "http://hydra.localhost",
KeyManager: keyManager,
DefaultChallengeLifespan: time.Hour,
DefaultIDTokenLifespan: time.Hour * 24,
},
+ ForcedHTTP: true,
}
var router = httprouter.New()
-
var ts *httptest.Server
-
var oauthConfig *oauth2.Config
-
var oauthClientConfig *clientcredentials.Config
func init() {
@@ -114,6 +87,7 @@ func init() {
RedirectURIs: []string{ts.URL + "/callback"},
ResponseTypes: []string{"id_token", "code", "token"},
GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"},
+ Scopes: "hydra",
}
c, _ := url.Parse(ts.URL + "/consent")
@@ -126,6 +100,7 @@ func init() {
RedirectURIs: []string{ts.URL + "/callback"},
ResponseTypes: []string{"id_token", "code", "token"},
GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"},
+ Scopes: "hydra",
}
oauthConfig = &oauth2.Config{
diff --git a/oauth2/session.go b/oauth2/session.go
index f6dd3036808..c180c7e8069 100644
--- a/oauth2/session.go
+++ b/oauth2/session.go
@@ -1,23 +1,25 @@
package oauth2
import (
- csh "github.com/ory-am/fosite/handler/core/strategy"
- "github.com/ory-am/fosite/handler/oidc/strategy"
+ "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"`
- *strategy.DefaultSession `json:"idToken"`
+ 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: &strategy.DefaultSession{
- Claims: new(jwt.IDTokenClaims),
- Headers: new(jwt.Headers),
- HMACSession: new(csh.HMACSession),
+ DefaultSession: &openid.DefaultSession{
+ Claims: new(jwt.IDTokenClaims),
+ Headers: new(jwt.Headers),
},
+ HMACSession: new(oauth2.HMACSession),
}
}
diff --git a/pkg/errors.go b/pkg/errors.go
index d4beaf4f15c..baedba906fb 100644
--- a/pkg/errors.go
+++ b/pkg/errors.go
@@ -1,9 +1,6 @@
package pkg
import (
- "net/http"
- "net/url"
-
log "github.com/Sirupsen/logrus"
"github.com/go-errors/errors"
"github.com/ory-am/hydra/herodot"
@@ -11,9 +8,7 @@ import (
)
var (
- ErrNotFound = errors.New("Not found")
- ErrUnauthorized = errors.New("Unauthorized")
- ErrForbidden = errors.New("Forbidden")
+ ErrNotFound = errors.New("Not found")
)
type stackTracer interface {
@@ -22,20 +17,12 @@ type stackTracer interface {
func LogError(err error) {
if e, ok := err.(*herodot.Error); ok {
- log.WithError(err).WithField("stack", e.Err.ErrorStack()).Printf("Got error.")
+ log.WithError(err).WithField("stack", e.Err.ErrorStack()).Infoln("An error occured")
} else if e, ok := err.(*errors.Error); ok {
- log.WithError(err).WithField("stack", e.ErrorStack()).Printf("Got error.")
+ log.WithError(err).WithField("stack", e.ErrorStack()).Infoln("An error occurred")
} else if e, ok := err.(stackTracer); ok {
- log.WithError(err).WithField("stack", e.StackTrace()).Printf("Got error.")
+ log.WithError(err).WithField("stack", e.StackTrace()).Infoln("An error occured")
} else {
- log.WithError(err).Printf("Got error.")
+ log.WithError(err).Infoln("An error occured")
}
}
-
-func ForwardToErrorHandler(w http.ResponseWriter, r *http.Request, err error, errorHandlerURL url.URL) {
- q := errorHandlerURL.Query()
- q.Set("error", err.Error())
- errorHandlerURL.RawQuery = q.Encode()
-
- http.Redirect(w, r, errorHandlerURL.String(), http.StatusFound)
-}
diff --git a/pkg/fosite_storer.go b/pkg/fosite_storer.go
index f13244be2be..2cbf9746705 100644
--- a/pkg/fosite_storer.go
+++ b/pkg/fosite_storer.go
@@ -2,18 +2,15 @@ package pkg
import (
"github.com/ory-am/fosite"
- "github.com/ory-am/fosite/handler/core"
- "github.com/ory-am/fosite/handler/core/explicit"
- "github.com/ory-am/fosite/handler/core/implicit"
- "github.com/ory-am/fosite/handler/core/refresh"
- "github.com/ory-am/fosite/handler/oidc"
+ "github.com/ory-am/fosite/handler/oauth2"
+ "github.com/ory-am/fosite/handler/openid"
)
type FositeStorer interface {
- core.AccessTokenStorage
+ oauth2.AccessTokenStorage
fosite.Storage
- explicit.AuthorizeCodeGrantStorage
- refresh.RefreshTokenGrantStorage
- implicit.ImplicitGrantStorage
- oidc.OpenIDConnectRequestStorage
+ oauth2.AuthorizeCodeGrantStorage
+ oauth2.RefreshTokenGrantStorage
+ oauth2.ImplicitGrantStorage
+ openid.OpenIDConnectRequestStorage
}
diff --git a/pkg/helper/dry.go b/pkg/helper/dry.go
new file mode 100644
index 00000000000..0f9013a29eb
--- /dev/null
+++ b/pkg/helper/dry.go
@@ -0,0 +1,20 @@
+package helper
+
+import (
+ "net/http"
+
+ "github.com/go-errors/errors"
+ "github.com/moul/http2curl"
+)
+
+func DoDryRequest(dry bool, req *http.Request) error {
+ if dry {
+ command, err := http2curl.GetCurlCommand(req)
+ if err != nil {
+ return errors.New(err)
+ }
+
+ return errors.Errorf("Because you are using the dry option, the request will not be executed. The curl equivalent of this command is: \n\n%s\n", command)
+ }
+ return nil
+}
diff --git a/pkg/rsa.go b/pkg/rsa.go
new file mode 100644
index 00000000000..1b7c8b89dc4
--- /dev/null
+++ b/pkg/rsa.go
@@ -0,0 +1,14 @@
+package pkg
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+)
+
+func MustRSAKey() *rsa.PrivateKey {
+ key, err := rsa.GenerateKey(rand.Reader, 1024)
+ if err != nil {
+ panic(err)
+ }
+ return key
+}
diff --git a/pkg/superagent.go b/pkg/superagent.go
index 1a756e720cd..a9064cacf68 100644
--- a/pkg/superagent.go
+++ b/pkg/superagent.go
@@ -3,13 +3,11 @@ package pkg
import (
"bytes"
"encoding/json"
- "net/http"
-
"io/ioutil"
+ "net/http"
"github.com/go-errors/errors"
- "github.com/moul/http2curl"
- "github.com/spf13/viper"
+ "github.com/ory-am/hydra/pkg/helper"
)
type SuperAgent struct {
@@ -22,20 +20,11 @@ func NewSuperAgent(rawurl string) *SuperAgent {
return &SuperAgent{
URL: rawurl,
Client: http.DefaultClient,
- Dry: viper.GetBool("dry"),
}
}
func (s *SuperAgent) doDry(req *http.Request) error {
- if s.Dry {
- command, err := http2curl.GetCurlCommand(req)
- if err != nil {
- return errors.New(err)
- }
-
- return errors.Errorf("Because you are using the dry option, the request will not be executed. You can execute this command using: \n\n%s", command)
- }
- return nil
+ return helper.DoDryRequest(s.Dry, req)
}
func (s *SuperAgent) Delete() error {
@@ -127,16 +116,10 @@ func (s *SuperAgent) send(method string, in interface{}, out interface{}) error
return errors.New(err)
}
- expectedStatus := http.StatusOK
- if method == "POST" {
- expectedStatus = http.StatusCreated
- }
- if resp.StatusCode != expectedStatus {
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := ioutil.ReadAll(resp.Body)
- return errors.Errorf("Expected status code %d, got %d.\n%s", expectedStatus, resp.StatusCode, body)
- }
-
- if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
+ return errors.Errorf("Expected 2xx status code but got %d.\n%s", resp.StatusCode, body)
+ } else if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
body, _ := ioutil.ReadAll(resp.Body)
return errors.Errorf("%s: %s", err, body)
}
diff --git a/pkg/test_helpers.go b/pkg/test_helpers.go
index 211fdad11ed..8c2013a1574 100644
--- a/pkg/test_helpers.go
+++ b/pkg/test_helpers.go
@@ -2,18 +2,25 @@ package pkg
import (
"testing"
-
"time"
"github.com/go-errors/errors"
- "github.com/ory-am/fosite/fosite-example/store"
- "github.com/ory-am/fosite/handler/core/strategy"
+ "github.com/ory-am/fosite/fosite-example/pkg"
+ "github.com/ory-am/fosite/handler/oauth2"
"github.com/ory-am/fosite/token/hmac"
"github.com/ory-am/ladon"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
+var HMACStrategy = &oauth2.HMACSHAStrategy{
+ Enigma: &hmac.HMACStrategy{
+ GlobalSecret: []byte("1234567890123456789012345678901234567890"),
+ },
+ AccessTokenLifespan: time.Hour,
+ AuthorizeCodeLifespan: time.Hour,
+}
+
func RequireError(t *testing.T, expectError bool, err error, args ...interface{}) {
if err != nil && !expectError {
t.Logf("Unexpected error: %s\n", err.Error())
@@ -46,8 +53,8 @@ func LadonWarden(ps map[string]ladon.Policy) ladon.Warden {
}
}
-func FositeStore() *store.Store {
- return store.NewStore()
+func FositeStore() *pkg.Store {
+ return pkg.NewStore()
}
func Tokens(length int) (res [][]string) {
@@ -57,11 +64,3 @@ func Tokens(length int) (res [][]string) {
}
return res
}
-
-var HMACStrategy = &strategy.HMACSHAStrategy{
- Enigma: &hmac.HMACStrategy{
- GlobalSecret: []byte("1234567890123456789012345678901234567890"),
- },
- AccessTokenLifespan: time.Hour,
- AuthorizeCodeLifespan: time.Hour,
-}
diff --git a/policy/handler.go b/policy/handler.go
index a2957a39f30..1d8c0a53da1 100644
--- a/policy/handler.go
+++ b/policy/handler.go
@@ -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.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
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.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
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.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
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.HTTPActionAllowed(ctx, r, &ladon.Request{
+ if _, err := h.W.TokenAllowed(ctx, h.W.TokenFromRequest(r), &ladon.Request{
Resource: fmt.Sprintf(policiesResource, id),
Action: "get",
}, scope); err != nil {
diff --git a/sdk/client.go b/sdk/client.go
index 9c3f6facd7b..f74519650e9 100644
--- a/sdk/client.go
+++ b/sdk/client.go
@@ -1,4 +1,3 @@
-// Wraps hydra HTTP Manager's
package sdk
import (
@@ -43,7 +42,7 @@ var defaultOptions = []option{
ClusterURL(os.Getenv("HYDRA_CLUSTER_URL")),
ClientID(os.Getenv("HYDRA_CLIENT_ID")),
ClientSecret(os.Getenv("HYDRA_CLIENT_SECRET")),
- Scopes("core", "hydra"),
+ Scopes("hydra"),
}
// Connect instantiates a new client to communicate with Hydra
diff --git a/sdk/doc.go b/sdk/doc.go
new file mode 100644
index 00000000000..e60e0ee2ba8
--- /dev/null
+++ b/sdk/doc.go
@@ -0,0 +1,27 @@
+// Package SDK offers convenience functions for Go code around Hydra's HTTP APIs.
+//
+// import "github.com/ory-am/hydra/sdk"
+// import "github.com/ory-am/hydra/client"
+// var hydra, err = sdk.Connect(
+// sdk.ClientID("client-id"),
+// sdk.ClientSecret("client-secret"),
+// sdk.ClustURL("https://localhost:4444"),
+// )
+//
+// // Create a new OAuth2 client
+// var newClient, err = hydra.Client.CreateClient(&client.Client{
+// ID: "deadbeef",
+// Secret: "sup3rs3cret",
+// RedirectURIs: []string{"http://yourapp/callback"},
+// // ...
+// })
+//
+// // Retrieve newly created client
+// var gotClient, err = hydra.Client.GetClient(newClient.ID)
+//
+// // Remove the newly created client
+// var err = hydra.Client.DeleteClient(newClient.ID)
+//
+// // Retrieve list of all clients
+// var clients, err = hydra.Client.GetClients()
+package sdk
diff --git a/warden/doc.go b/warden/doc.go
new file mode 100644
index 00000000000..caf9144758a
--- /dev/null
+++ b/warden/doc.go
@@ -0,0 +1,9 @@
+// Package warden decides if access requests should be allowed or denied. In a scientific taxonomy, the warden
+// is classified as a Policy Decision Point. THe warden's primary goal is to implement `github.com/ory-am/hydra/firewall.Firewall`.
+//
+// This package is structured as follows:
+// * handler.go: A HTTP handler capable of validating access tokens.
+// * warden_http.go: A Go API using HTTP to validate access tokens.
+// * warden_local.go: A Go API using storage managers to validate access tokens.
+// * warden_test.go: Functional tests all of the above.
+package warden
diff --git a/warden/handler.go b/warden/handler.go
index 11530733a27..ab3144c713b 100644
--- a/warden/handler.go
+++ b/warden/handler.go
@@ -11,18 +11,18 @@ import (
"github.com/ory-am/hydra/firewall"
"github.com/ory-am/hydra/herodot"
"github.com/ory-am/ladon"
- "golang.org/x/net/context"
)
const (
- AuthorizedHandlerPath = "/warden/authorized"
- AllowedHandlerPath = "/warden/allowed"
+ TokenValidHandlerPath = "/warden/token/valid"
+ TokenAllowedHandlerPath = "/warden/token/allowed"
+ AllowedHandlerPath = "/warden/allowed"
+ IntrospectPath = "/oauth2/introspect"
)
type WardenHandler struct {
H herodot.Herodot
Warden firewall.Firewall
- Ladon ladon.Warden
}
func NewHandler(c *config.Config, router *httprouter.Router) *WardenHandler {
@@ -31,22 +31,15 @@ func NewHandler(c *config.Config, router *httprouter.Router) *WardenHandler {
h := &WardenHandler{
H: &herodot.JSON{},
Warden: ctx.Warden,
- Ladon: &ladon.Ladon{
- Manager: ctx.LadonManager,
- },
}
h.SetRoutes(router)
return h
}
-type WardenResponse struct {
- *firewall.Context
-}
-
type WardenAuthorizedRequest struct {
- Scopes []string `json:"scopes"`
- Assertion string `json:"assertion"`
+ Scopes []string `json:"scopes"`
+ Token string `json:"token"`
}
type WardenAccessRequest struct {
@@ -54,14 +47,56 @@ type WardenAccessRequest struct {
*WardenAuthorizedRequest
}
+var notAllowed = struct {
+ Allowed bool `json:"allowed"`
+}{Allowed: false}
+
+var invalid = struct {
+ Valid bool `json:"valid"`
+}{Valid: false}
+
+var inactive = struct {
+ Active bool `json:"active"`
+}{Active: false}
+
func (h *WardenHandler) SetRoutes(r *httprouter.Router) {
- r.POST(AuthorizedHandlerPath, h.Authorized)
+ r.POST(TokenValidHandlerPath, h.TokenValid)
+ r.POST(TokenAllowedHandlerPath, h.TokenAllowed)
r.POST(AllowedHandlerPath, h.Allowed)
+ r.POST(IntrospectPath, h.Introspect)
+}
+
+func (h *WardenHandler) Introspect(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+ ctx := herodot.NewContext()
+ clientCtx, err := h.Warden.InspectToken(ctx, TokenFromRequest(r))
+ if err != nil {
+ h.H.WriteError(ctx, w, r, err)
+ return
+ }
+
+ if err := r.ParseForm(); err != nil {
+ h.H.WriteError(ctx, w, r, err)
+ return
+ }
+
+ auth, err := h.Warden.IntrospectToken(ctx, r.PostForm.Get("token"))
+ if err != nil {
+ h.H.Write(ctx, w, r, &inactive)
+ return
+ } else if clientCtx.Subject != auth.Audience {
+ h.H.Write(ctx, w, r, &inactive)
+ return
+ }
+
+ h.H.Write(ctx, w, r, auth)
}
-func (h *WardenHandler) Authorized(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+func (h *WardenHandler) TokenValid(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
ctx := herodot.NewContext()
- clientCtx, err := h.authorizeClient(ctx, w, r)
+ clientCtx, 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
@@ -74,48 +109,84 @@ func (h *WardenHandler) Authorized(w http.ResponseWriter, r *http.Request, _ htt
}
defer r.Body.Close()
- authContext, err := h.Warden.Authorized(ctx, ar.Assertion, ar.Scopes...)
+ authContext, err := h.Warden.InspectToken(ctx, ar.Token, ar.Scopes...)
if err != nil {
- h.H.WriteError(ctx, w, r, err)
+ h.H.Write(ctx, w, r, &invalid)
return
}
authContext.Audience = clientCtx.Subject
- h.H.Write(ctx, w, r, authContext)
-
+ 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) {
- ctx := herodot.NewContext()
- clientCtx, err := h.authorizeClient(ctx, w, r)
- if err != nil {
+ var ctx = herodot.NewContext()
+ if _, err := h.Warden.TokenAllowed(ctx, h.Warden.TokenFromRequest(r), &ladon.Request{
+ Resource: "rn:hydra:warden:allowed",
+ Action: "decide",
+ }, "hydra.warden"); err != nil {
h.H.WriteError(ctx, w, r, err)
return
}
- var ar WardenAccessRequest
- if err := json.NewDecoder(r.Body).Decode(&ar); err != nil {
+ var access = new(ladon.Request)
+ if err := json.NewDecoder(r.Body).Decode(access); err != nil {
h.H.WriteError(ctx, w, r, errors.New(err))
return
}
+ defer r.Body.Close()
+
+ if err := h.Warden.IsAllowed(ctx, access); err != nil {
+ h.H.Write(ctx, w, r, ¬Allowed)
+ return
+ }
+
+ res := notAllowed
+ res.Allowed = true
+ h.H.Write(ctx, w, r, &res)
+}
- authContext, err := h.Warden.ActionAllowed(ctx, ar.Assertion, ar.Request, ar.Scopes...)
+func (h *WardenHandler) TokenAllowed(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+ ctx := herodot.NewContext()
+ clientCtx, err := h.Warden.TokenAllowed(ctx, h.Warden.TokenFromRequest(r), &ladon.Request{
+ Resource: "rn:hydra:warden:token:allowed",
+ Action: "decide",
+ }, "warden.token")
if err != nil {
h.H.WriteError(ctx, w, r, err)
return
}
- authContext.Audience = clientCtx.Subject
- h.H.Write(ctx, w, r, authContext)
-}
+ var ar = WardenAccessRequest{
+ Request: new(ladon.Request),
+ WardenAuthorizedRequest: new(WardenAuthorizedRequest),
+ }
+ if err := json.NewDecoder(r.Body).Decode(&ar); err != nil {
+ h.H.WriteError(ctx, w, r, errors.New(err))
+ return
+ }
+ defer r.Body.Close()
-func (h *WardenHandler) authorizeClient(ctx context.Context, w http.ResponseWriter, r *http.Request) (*firewall.Context, error) {
- authctx, err := h.Warden.Authorized(ctx, TokenFromRequest(r), "core")
+ authContext, err := h.Warden.TokenAllowed(ctx, ar.Token, ar.Request, ar.Scopes...)
if err != nil {
- return nil, err
+ h.H.Write(ctx, w, r, ¬Allowed)
+ return
}
- return authctx, nil
+ authContext.Audience = clientCtx.Subject
+ h.H.Write(ctx, w, r, struct {
+ *firewall.Context
+ Allowed bool `json:"allowed"`
+ }{
+ Context: authContext,
+ Allowed: true,
+ })
}
func TokenFromRequest(r *http.Request) string {
diff --git a/warden/warden_http.go b/warden/warden_http.go
index 9632c25315c..482d563fe28 100644
--- a/warden/warden_http.go
+++ b/warden/warden_http.go
@@ -1,100 +1,133 @@
package warden
import (
- "bytes"
- "encoding/json"
- "io/ioutil"
"net/http"
"net/url"
"github.com/go-errors/errors"
- . "github.com/ory-am/hydra/firewall"
+ "github.com/ory-am/fosite"
+ "github.com/ory-am/hydra/firewall"
"github.com/ory-am/hydra/pkg"
"github.com/ory-am/ladon"
"golang.org/x/net/context"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
+ "bytes"
+ "io/ioutil"
+ "strconv"
+ "encoding/json"
)
type HTTPWarden struct {
- Client *http.Client
-
+ Client *http.Client
+ Dry bool
Endpoint *url.URL
}
+func (w *HTTPWarden) TokenFromRequest(r *http.Request) string {
+ return fosite.AccessTokenFromRequest(r)
+}
+
func (w *HTTPWarden) SetClient(c *clientcredentials.Config) {
w.Client = c.Client(oauth2.NoContext)
}
-func (w *HTTPWarden) ActionAllowed(ctx context.Context, token string, a *ladon.Request, scopes ...string) (*Context, error) {
- return w.doRequest(AllowedHandlerPath, &WardenAccessRequest{
- Request: a,
- WardenAuthorizedRequest: &WardenAuthorizedRequest{
- Assertion: token,
- Scopes: scopes,
- },
- })
-}
+func (w *HTTPWarden) IntrospectToken(ctx context.Context, token string) (*firewall.Introspection, error) {
+ var resp = new(firewall.Introspection)
+ var ep = *w.Endpoint
+ ep.Path = IntrospectPath
+ agent := &pkg.SuperAgent{URL: ep.String(), Client: w.Client}
-func (w *HTTPWarden) HTTPActionAllowed(ctx context.Context, r *http.Request, a *ladon.Request, scopes ...string) (*Context, error) {
- token := TokenFromRequest(r)
- if token == "" {
- return nil, errors.New(pkg.ErrUnauthorized)
+ data := url.Values{"token": []string{token}}
+ hreq, err := http.NewRequest("POST", ep.String(), bytes.NewBufferString(data.Encode()))
+ if err != nil {
+ return nil, errors.New(err)
}
- return w.ActionAllowed(ctx, token, a, scopes...)
-}
+ hreq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ hreq.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
+ hres, err := w.Client.Do(hreq)
+ if err != nil {
+ return nil, errors.New(err)
+ }
-func (w *HTTPWarden) Authorized(ctx context.Context, token string, scopes ...string) (*Context, error) {
- return w.doRequest(AuthorizedHandlerPath, &WardenAuthorizedRequest{
- Assertion: token,
- Scopes: scopes,
- })
-}
+ 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)
+ return nil, errors.Errorf("%s: %s", err, body)
+ }
-func (w *HTTPWarden) HTTPAuthorized(ctx context.Context, r *http.Request, scopes ...string) (*Context, error) {
- token := TokenFromRequest(r)
- if token == "" {
- return nil, errors.New(pkg.ErrUnauthorized)
+ if err := agent.POST(&struct {
+ Token string `json:"token"`
+ }{Token: token}, &hres); err != nil {
+ return nil, err
+ } else if !resp.Active {
+ return nil, errors.New("Token is malformed, expired or otherwise invalid")
}
- return w.Authorized(ctx, token, scopes...)
+ return resp, nil
}
-func (w *HTTPWarden) doRequest(path string, request interface{}) (*Context, error) {
- out, err := json.Marshal(request)
- if err != nil {
- return nil, errors.New(err)
- }
-
- var ep = new(url.URL)
- *ep = *w.Endpoint
- ep.Path = path
- req, err := http.NewRequest("POST", ep.String(), bytes.NewBuffer(out))
- if err != nil {
- return nil, errors.New(err)
- }
+func (w *HTTPWarden) TokenAllowed(ctx context.Context, token string, a *ladon.Request, scopes ...string) (*firewall.Context, error) {
+ var resp = struct {
+ *firewall.Context
+ Allowed bool `json:"allowed"`
+ }{}
- req.Header.Set("Content-Type", "application/json")
- resp, err := w.Client.Do(req)
- if err != nil {
- return nil, errors.New(err)
+ var ep = *w.Endpoint
+ ep.Path = TokenAllowedHandlerPath
+ agent := &pkg.SuperAgent{URL: ep.String(), Client: w.Client}
+ if err := agent.POST(&WardenAccessRequest{
+ WardenAuthorizedRequest: &WardenAuthorizedRequest{
+ Token: token,
+ Scopes: scopes,
+ },
+ Request: a,
+ }, &resp); err != nil {
+ return nil, err
+ } else if !resp.Allowed {
+ return nil, errors.New("Token is not valid")
}
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- all, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return nil, errors.New(err)
- }
+ return resp.Context, nil
+}
- return nil, errors.Errorf("Got error (%d): %s", resp.StatusCode, all)
+func (w *HTTPWarden) IsAllowed(ctx context.Context, a *ladon.Request) error {
+ var allowed = struct {
+ Allowed bool `json:"allowed"`
+ }{}
+
+ var ep = *w.Endpoint
+ ep.Path = AllowedHandlerPath
+ agent := &pkg.SuperAgent{URL: ep.String(), Client: w.Client}
+ if err := agent.POST(a, &allowed); err != nil {
+ return err
+ } else if !allowed.Allowed {
+ return errors.New("Forbidden")
}
- var epResp WardenResponse
- if err := json.NewDecoder(resp.Body).Decode(&epResp); err != nil {
- return nil, errors.New(err)
+ return nil
+}
+
+func (w *HTTPWarden) InspectToken(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 epResp.Context, nil
+ return resp.Context, nil
}
diff --git a/warden/warden_local.go b/warden/warden_local.go
index 9be52d972cd..b0c1bc28639 100644
--- a/warden/warden_local.go
+++ b/warden/warden_local.go
@@ -5,208 +5,143 @@ import (
"time"
+ "strings"
+
"github.com/Sirupsen/logrus"
"github.com/go-errors/errors"
"github.com/ory-am/fosite"
- "github.com/ory-am/fosite/handler/core"
- . "github.com/ory-am/hydra/firewall"
- "github.com/ory-am/hydra/herodot"
+ "github.com/ory-am/hydra/firewall"
"github.com/ory-am/hydra/oauth2"
- "github.com/ory-am/hydra/pkg"
"github.com/ory-am/ladon"
"golang.org/x/net/context"
)
type LocalWarden struct {
- Warden ladon.Warden
- TokenValidator *core.CoreValidator
+ Warden ladon.Warden
+ OAuth2 fosite.OAuth2Provider
AccessTokenLifespan time.Duration
Issuer string
}
-func (w *LocalWarden) actionAllowed(ctx context.Context, a *ladon.Request, scopes []string, oauthRequest fosite.AccessRequester, session *oauth2.Session) (*Context, error) {
- session = oauthRequest.GetSession().(*oauth2.Session)
- if a.Subject != "" && a.Subject != session.Subject {
- logrus.WithFields(logrus.Fields{
- "scopes": scopes,
- "subject": a.Subject,
- "audience": oauthRequest.GetClient().GetID(),
- "request": a,
- "reason": "subject mismatch",
- }).Infof("Access denied")
- return nil, errors.New("Subject mismatch " + a.Subject + " - " + session.Subject)
- }
-
- if !matchScopes(oauthRequest.GetGrantedScopes(), scopes, session, oauthRequest.GetClient()) {
- logrus.WithFields(logrus.Fields{
- "scopes": scopes,
- "subject": a.Subject,
- "audience": oauthRequest.GetClient().GetID(),
- "request": a,
- "reason": "scope mismatch",
- }).Infof("Access denied")
- return nil, errors.New(herodot.ErrForbidden)
- }
+func (w *LocalWarden) TokenFromRequest(r *http.Request) string {
+ return fosite.AccessTokenFromRequest(r)
+}
- a.Subject = session.Subject
- if err := w.Warden.IsAllowed(a); err != nil {
- logrus.WithFields(logrus.Fields{
- "scopes": scopes,
- "subject": a.Subject,
- "audience": oauthRequest.GetClient().GetID(),
- "request": a,
- "reason": "policy effect is deny",
- }).Infof("Access denied")
- return nil, err
+func (w *LocalWarden) IntrospectToken(ctx context.Context, token string) (*firewall.Introspection, error) {
+ var session = new(oauth2.Session)
+ var auth, err = w.OAuth2.ValidateToken(ctx, token, fosite.AccessToken, session)
+ if err != nil {
+ logrus.WithError(err).Infof("Token introspection failed")
+ return &firewall.Introspection{
+ Active: false,
+ }, err
}
- logrus.WithFields(logrus.Fields{
- "scopes": scopes,
- "subject": a.Subject,
- "audience": oauthRequest.GetClient().GetID(),
- "request": a,
- }).Infof("Access granted")
-
- return &Context{
- Subject: session.Subject,
- GrantedScopes: oauthRequest.GetGrantedScopes(),
- Issuer: w.Issuer,
- Audience: oauthRequest.GetClient().GetID(),
- IssuedAt: oauthRequest.GetRequestedAt(),
- ExpiresAt: session.AccessTokenExpiresAt(oauthRequest.GetRequestedAt().Add(w.AccessTokenLifespan)),
+ session = auth.GetSession().(*oauth2.Session)
+ return &firewall.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(),
}, nil
}
-func (w *LocalWarden) ActionAllowed(ctx context.Context, token string, a *ladon.Request, scopes ...string) (*Context, error) {
- var session = new(oauth2.Session)
- var oauthRequest = fosite.NewAccessRequest(session)
- if err := w.TokenValidator.ValidateToken(ctx, oauthRequest, token); err != nil {
- return nil, err
+func (w *LocalWarden) IsAllowed(ctx context.Context, a *ladon.Request) error {
+ if err := w.Warden.IsAllowed(a); err != nil {
+ logrus.WithFields(logrus.Fields{
+ "subject": a.Subject,
+ "request": a,
+ "reason": "request denied by policies",
+ }).WithError(err).Infof("Access denied")
+ return err
}
- return w.actionAllowed(ctx, a, scopes, oauthRequest, session)
+ return nil
}
-func (w *LocalWarden) HTTPActionAllowed(ctx context.Context, r *http.Request, a *ladon.Request, scopes ...string) (*Context, error) {
+func (w *LocalWarden) TokenAllowed(ctx context.Context, token string, a *ladon.Request, scopes ...string) (*firewall.Context, error) {
var session = new(oauth2.Session)
- var oauthRequest = fosite.NewAccessRequest(session)
-
- if err := w.TokenValidator.ValidateRequest(ctx, r, oauthRequest); errors.Is(err, fosite.ErrUnknownRequest) {
- logrus.WithFields(logrus.Fields{
- "scopes": scopes,
- "subject": a.Subject,
- "audience": oauthRequest.GetClient().GetID(),
- "request": a,
- "reason": "unknown request",
- }).Infof("Access denied")
- return nil, errors.New(pkg.ErrUnauthorized)
- } else if err != nil {
+ var auth, err = w.OAuth2.ValidateToken(ctx, token, fosite.AccessToken, session, scopes...)
+ if err != nil {
logrus.WithFields(logrus.Fields{
- "scopes": scopes,
- "subject": a.Subject,
- "audience": oauthRequest.GetClient().GetID(),
- "request": a,
- "reason": "token validation failed",
- }).Infof("Access denied")
+ "subject": a.Subject,
+ "request": a,
+ "reason": "token could not be validated",
+ }).WithError(err).Infof("Access denied")
return nil, err
}
- return w.actionAllowed(ctx, a, scopes, oauthRequest, session)
+ return w.allowed(ctx, a, scopes, auth, session)
}
-func (w *LocalWarden) Authorized(ctx context.Context, token string, scopes ...string) (*Context, error) {
+func (w *LocalWarden) InspectToken(ctx context.Context, token string, scopes ...string) (*firewall.Context, error) {
var session = new(oauth2.Session)
var oauthRequest = fosite.NewAccessRequest(session)
- if err := w.TokenValidator.ValidateToken(ctx, oauthRequest, token); err != nil {
+ 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 validation failed",
- }).Infof("Access denied")
+ }).WithError(err).Infof("Access denied")
return nil, err
}
- session = oauthRequest.GetSession().(*oauth2.Session)
- if !matchScopes(oauthRequest.GetGrantedScopes(), scopes, session, oauthRequest.Client) {
- logrus.WithFields(logrus.Fields{
- "scopes": scopes,
- "subject": session,
- "audience": oauthRequest.GetClient().GetID(),
- "reason": "scope mismatch",
- }).Infof("Access denied")
- return nil, errors.New(herodot.ErrForbidden)
- }
-
- return &Context{
- Subject: session.Subject,
- GrantedScopes: oauthRequest.GetGrantedScopes(),
- Issuer: w.Issuer,
- Audience: oauthRequest.GetClient().GetID(),
- IssuedAt: oauthRequest.GetRequestedAt(),
- ExpiresAt: session.AccessTokenExpiresAt(oauthRequest.GetRequestedAt().Add(w.AccessTokenLifespan)),
- }, nil
+ return w.newContext(auth), nil
}
-func (w *LocalWarden) HTTPAuthorized(ctx context.Context, r *http.Request, scopes ...string) (*Context, error) {
- var session = new(oauth2.Session)
- var oauthRequest = fosite.NewAccessRequest(session)
-
- if err := w.TokenValidator.ValidateRequest(ctx, r, oauthRequest); errors.Is(err, fosite.ErrUnknownRequest) {
- logrus.WithFields(logrus.Fields{
- "scopes": scopes,
- "subject": session.Subject,
- "audience": oauthRequest.GetClient().GetID(),
- "reason": "unknown request",
- }).Infof("Access denied")
- return nil, errors.New(pkg.ErrUnauthorized)
- } else if err != nil {
+func (w *LocalWarden) allowed(ctx context.Context, a *ladon.Request, 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": session.Subject,
+ "subject": a.Subject,
"audience": oauthRequest.GetClient().GetID(),
- "reason": "token validation failed",
- }).Infof("Access denied")
+ "request": a,
+ "reason": "subject mismatch",
+ }).WithError(err).Infof("Access denied")
return nil, err
}
- session = oauthRequest.GetSession().(*oauth2.Session)
- if !matchScopes(oauthRequest.GetGrantedScopes(), scopes, session, oauthRequest.Client) {
+ a.Subject = session.Subject
+ if err := w.Warden.IsAllowed(a); err != nil {
logrus.WithFields(logrus.Fields{
"scopes": scopes,
- "subject": session.Subject,
+ "subject": a.Subject,
"audience": oauthRequest.GetClient().GetID(),
- "reason": "scope mismatch",
- }).Infof("Access denied")
- return nil, errors.New(herodot.ErrForbidden)
+ "request": a,
+ "reason": "policy effect is deny",
+ }).WithError(err).Infof("Access denied")
+ return nil, err
}
- return &Context{
+ return w.newContext(oauthRequest), nil
+}
+
+func (w *LocalWarden) newContext(oauthRequest fosite.AccessRequester) *firewall.Context {
+ session := oauthRequest.GetSession().(*oauth2.Session)
+ c := &firewall.Context{
Subject: session.Subject,
GrantedScopes: oauthRequest.GetGrantedScopes(),
Issuer: w.Issuer,
Audience: oauthRequest.GetClient().GetID(),
IssuedAt: oauthRequest.GetRequestedAt(),
ExpiresAt: session.AccessTokenExpiresAt(oauthRequest.GetRequestedAt().Add(w.AccessTokenLifespan)),
- }, nil
-}
-
-func matchScopes(granted []string, requested []string, session *oauth2.Session, c fosite.Client) bool {
- scopes := &fosite.DefaultScopes{Scopes: granted}
- for _, r := range requested {
- if !scopes.Grant(r) {
- logrus.WithFields(logrus.Fields{
- "reason": "scope mismatch",
- "granted_scopes": granted,
- "requested_scopes": requested,
- "audience": c.GetID(),
- "subject": session.Subject,
- }).Infof("Authentication failed.")
- return false
- }
+ Extra: session.Extra,
}
- return true
+ logrus.WithFields(logrus.Fields{
+ "subject": c.Subject,
+ "audience": oauthRequest.GetClient().GetID(),
+ }).Infof("Access granted")
+
+ return c
}
diff --git a/warden/warden_test.go b/warden/warden_test.go
index e0e5b54f044..60ef22957ae 100644
--- a/warden/warden_test.go
+++ b/warden/warden_test.go
@@ -2,7 +2,6 @@ package warden_test
import (
"log"
- "net/http"
"net/http/httptest"
"net/url"
"testing"
@@ -10,7 +9,7 @@ import (
"github.com/julienschmidt/httprouter"
"github.com/ory-am/fosite"
- "github.com/ory-am/fosite/handler/core"
+ foauth2 "github.com/ory-am/fosite/handler/oauth2"
"github.com/ory-am/hydra/firewall"
"github.com/ory-am/hydra/herodot"
"github.com/ory-am/hydra/oauth2"
@@ -30,15 +29,15 @@ var ladonWarden = pkg.LadonWarden(map[string]ladon.Policy{
"1": &ladon.DefaultPolicy{
ID: "1",
Subjects: []string{"alice"},
- Resources: []string{"matrix"},
- Actions: []string{"create"},
+ 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{},
+ Actions: []string{"decide"},
Effect: ladon.AllowAccess,
},
})
@@ -52,9 +51,16 @@ var tokens = pkg.Tokens(3)
func init() {
wardens["local"] = &warden.LocalWarden{
Warden: ladonWarden,
- TokenValidator: &core.CoreValidator{
- AccessTokenStrategy: pkg.HMACStrategy,
- AccessTokenStorage: fositeStore,
+ OAuth2: &fosite.Fosite{
+ Store: fositeStore,
+ TokenValidators: fosite.TokenValidators{
+ &foauth2.CoreValidator{
+ CoreStrategy: pkg.HMACStrategy,
+ CoreStorage: fositeStore,
+ ScopeStrategy: fosite.HierarchicScopeStrategy,
+ },
+ },
+ ScopeStrategy: fosite.HierarchicScopeStrategy,
},
Issuer: "tests",
AccessTokenLifespan: time.Hour,
@@ -62,14 +68,13 @@ func init() {
r := httprouter.New()
serv := &warden.WardenHandler{
- Ladon: ladonWarden,
H: &herodot.JSON{},
Warden: wardens["local"],
}
serv.SetRoutes(r)
ts = httptest.NewServer(r)
- url, err := url.Parse(ts.URL + warden.AllowedHandlerPath)
+ url, err := url.Parse(ts.URL + warden.TokenAllowedHandlerPath)
if err != nil {
log.Fatalf("%s", err)
}
@@ -77,16 +82,19 @@ func init() {
ar := fosite.NewAccessRequest(oauth2.NewSession("alice"))
ar.GrantedScopes = fosite.Arguments{"core"}
ar.RequestedAt = now
+ ar.Client = &fosite.DefaultClient{ID: "siri"}
fositeStore.CreateAccessTokenSession(nil, tokens[0][0], ar)
ar2 := fosite.NewAccessRequest(oauth2.NewSession("siri"))
ar2.GrantedScopes = fosite.Arguments{"core"}
ar2.RequestedAt = now
+ 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
+ ar3.Client = &fosite.DefaultClient{ID: "doesnt-exist"}
ar3.Session.(*oauth2.Session).AccessTokenExpiry = time.Now().Add(-time.Hour)
fositeStore.CreateAccessTokenSession(nil, tokens[2][0], ar3)
@@ -165,6 +173,17 @@ func TestActionAllowed(t *testing.T) {
scopes: []string{"core"},
expectErr: true,
},
+ {
+ token: tokens[0][1],
+ req: &ladon.Request{
+ Subject: "alice",
+ Resource: "matrix",
+ Action: "create",
+ Context: ladon.Context{},
+ },
+ scopes: []string{"illegal"},
+ expectErr: true,
+ },
{
token: tokens[0][1],
req: &ladon.Request{
@@ -183,16 +202,43 @@ func TestActionAllowed(t *testing.T) {
},
},
} {
- ctx, err := w.ActionAllowed(context.Background(), c.token, c.req, c.scopes...)
+ ctx, err := w.TokenAllowed(context.Background(), c.token, c.req, c.scopes...)
pkg.AssertError(t, c.expectErr, err, "ActionAllowed case", n, k)
if err == nil && c.assert != nil {
c.assert(ctx)
}
+ }
+ }
+}
- httpreq := &http.Request{Header: http.Header{}}
- httpreq.Header.Set("Authorization", "bearer "+c.token)
- ctx, err = w.HTTPActionAllowed(context.Background(), httpreq, c.req, c.scopes...)
- pkg.AssertError(t, c.expectErr, err, "HTTPAuthorized case", n, k)
+func TestIntrospect(t *testing.T) {
+ for n, w := range wardens {
+ for k, c := range []struct {
+ token string
+ expectErr bool
+ assert func(*firewall.Introspection)
+ }{
+ {
+ token: "invalid",
+ expectErr: true,
+ },
+ {
+ token: tokens[2][1],
+ expectErr: true,
+ },
+ {
+ token: tokens[0][1],
+ expectErr: false,
+ assert: func(c *firewall.Introspection) {
+ assert.Equal(t, "alice", c.Subject)
+ assert.Equal(t, "tests", c.Issuer)
+ assert.Equal(t, now.Add(time.Hour).Unix(), c.ExpiresAt)
+ assert.Equal(t, now.Unix(), c.IssuedAt)
+ },
+ },
+ } {
+ ctx, err := w.IntrospectToken(context.Background(), c.token)
+ pkg.AssertError(t, c.expectErr, err, "TestIntrospect case", n, k)
if err == nil && c.assert != nil {
c.assert(ctx)
}
@@ -200,7 +246,51 @@ func TestActionAllowed(t *testing.T) {
}
}
-func TestAuthorized(t *testing.T) {
+func TestAllowed(t *testing.T) {
+ for n, w := range wardens {
+ for k, c := range []struct {
+ req *ladon.Request
+ expectErr bool
+ assert func(*firewall.Context)
+ }{
+ {
+ req: &ladon.Request{
+ Subject: "alice",
+ Resource: "other-thing",
+ Action: "create",
+ Context: ladon.Context{},
+ },
+ expectErr: true,
+ },
+ {
+ req: &ladon.Request{
+ Subject: "alice",
+ Resource: "matrix",
+ Action: "delete",
+ Context: ladon.Context{},
+ },
+ expectErr: true,
+ },
+ {
+ req: &ladon.Request{
+ Subject: "alice",
+ Resource: "matrix",
+ Action: "create",
+ Context: ladon.Context{},
+ },
+ expectErr: false,
+ },
+ } {
+ err := w.IsAllowed(context.Background(), c.req)
+ pkg.AssertError(t, c.expectErr, err, "TestAllowed case", n, k)
+ t.Logf("Passed test case %d\n", k)
+ }
+ t.Logf("Passed tests %s\n", n)
+ }
+
+}
+
+func TestTokenValid(t *testing.T) {
for n, w := range wardens {
for k, c := range []struct {
token string
@@ -221,6 +311,11 @@ func TestAuthorized(t *testing.T) {
scopes: []string{"foo"},
expectErr: true,
},
+ {
+ token: tokens[1][1],
+ scopes: []string{"illegal"},
+ expectErr: true,
+ },
{
token: tokens[1][1],
scopes: []string{"core"},
@@ -238,19 +333,11 @@ func TestAuthorized(t *testing.T) {
expectErr: true,
},
} {
- ctx, err := w.Authorized(context.Background(), c.token, c.scopes...)
+ ctx, err := w.InspectToken(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)
}
-
- httpreq := &http.Request{Header: http.Header{}}
- httpreq.Header.Set("Authorization", "bearer "+c.token)
- ctx, err = w.HTTPAuthorized(context.Background(), httpreq, c.scopes...)
- pkg.AssertError(t, c.expectErr, err, "HTTPAuthorized case", n, k)
- if err == nil && c.assert != nil {
- c.assert(ctx)
- }
}
}
}