Skip to content

Commit

Permalink
oauth2: replace jwk-based with http-based consent flow
Browse files Browse the repository at this point in the history
Closes #578
  • Loading branch information
arekkas authored and arekkas committed Oct 5, 2017
1 parent 2c79a31 commit fc3ee34
Show file tree
Hide file tree
Showing 18 changed files with 456 additions and 240 deletions.
3 changes: 2 additions & 1 deletion cmd/root.go
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/ory/hydra/cmd/cli"
"github.com/ory/hydra/config"
"github.com/ory/hydra/oauth2"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand Down Expand Up @@ -89,7 +90,7 @@ func initConfig() {
viper.SetDefault("CLIENT_ID", "")

viper.BindEnv("CONSENT_URL")
viper.SetDefault("CONSENT_URL", "")
viper.SetDefault("CONSENT_URL", oauth2.DefaultConsentPath)

viper.BindEnv("DATABASE_PLUGIN")
viper.SetDefault("DATABASE_PLUGIN", "")
Expand Down
2 changes: 2 additions & 0 deletions metrics/metrics.go
Expand Up @@ -163,6 +163,8 @@ func (s *Snapshot) Path(path string) *PathMetrics {
"/health",
"/keys",
"/oauth2/auth",
"/oauth2/session",
"/oauth2/consent",
"/oauth2/introspect",
"/oauth2/revoke",
"/oauth2/token",
Expand Down
4 changes: 2 additions & 2 deletions oauth2/consent.go
Expand Up @@ -6,6 +6,6 @@ import (
)

type ConsentStrategy interface {
ValidateResponse(authorizeRequest fosite.AuthorizeRequester, token string, session *sessions.Session) (claims *Session, err error)
IssueChallenge(authorizeRequest fosite.AuthorizeRequester, redirectURL string, session *sessions.Session) (token string, err error)
ValidateConsentRequest(req fosite.AuthorizeRequester, session string, cookie *sessions.Session) (claims *Session, err error)
CreateConsentRequest(req fosite.AuthorizeRequester, redirectURL string, cookie *sessions.Session) (token string, err error)
}
112 changes: 112 additions & 0 deletions oauth2/consent_handler.go
@@ -0,0 +1,112 @@
package oauth2

import (
"encoding/json"
"fmt"
"net/http"

"github.com/julienschmidt/httprouter"
"github.com/ory/herodot"
"github.com/ory/hydra/firewall"
"github.com/pkg/errors"
)

const (
ConsentRequestAccepted = "accepted"
ConsentRequestRejected = "rejected"

ConsentSessionPath = "/oauth2/consent/requests"

ConsentResource = "rn:hydra:oauth2:consent:requests:%s"
ConsentScope = "hydra.consent"
)

type ConsentSessionHandler struct {
H herodot.Writer
M ConsentRequestManager
W firewall.Firewall
}

func (h *ConsentSessionHandler) SetRoutes(r *httprouter.Router) {
r.GET(ConsentSessionPath+"/:id", h.FetchConsentRequest)
r.PATCH(ConsentSessionPath+"/:id/reject", h.RejectConsentRequestHandler)
r.PATCH(ConsentSessionPath+"/:id/accept", h.AcceptConsentRequestHandler)
}

// swagger:route GET /.well-known/openid-configuration oauth2 openid-connect WellKnownHandler
//
// Server well known configuration
//
// For more information, please refer to https://openid.net/specs/openid-connect-discovery-1_0.html
//
// Consumes:
// - application/x-www-form-urlencoded
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Security:
// oauth2:
//
// Responses:
// 200: WellKnown
// 401: genericError
// 500: genericError
func (h *ConsentSessionHandler) FetchConsentRequest(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if _, err := h.W.TokenAllowed(r.Context(), h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{
Resource: fmt.Sprintf(ConsentResource, ps.ByName("id")),
Action: "get",
}, ConsentScope); err != nil {
h.H.WriteError(w, r, err)
return
}

if session, err := h.M.GetConsentRequest(ps.ByName("id")); err != nil {
h.H.WriteError(w, r, err)
return
} else {
h.H.Write(w, r, session)
}
}

func (h *ConsentSessionHandler) RejectConsentRequestHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if _, err := h.W.TokenAllowed(r.Context(), h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{
Resource: fmt.Sprintf(ConsentResource, ps.ByName("id")),
Action: "reject",
}, ConsentScope); err != nil {
h.H.WriteError(w, r, err)
return
}

if err := h.M.RejectConsentRequest(ps.ByName("id")); err != nil {
h.H.WriteError(w, r, err)
return
}

w.WriteHeader(http.StatusNoContent)
}

func (h *ConsentSessionHandler) AcceptConsentRequestHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if _, err := h.W.TokenAllowed(r.Context(), h.W.TokenFromRequest(r), &firewall.TokenAccessRequest{
Resource: fmt.Sprintf(ConsentResource, ps.ByName("id")),
Action: "accept",
}, ConsentScope); err != nil {
h.H.WriteError(w, r, err)
return
}

var payload AcceptConsentRequestPayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
h.H.WriteError(w, r, errors.WithStack(err))
return
}

if err := h.M.AcceptConsentRequest(ps.ByName("id"), &payload); err != nil {
h.H.WriteError(w, r, err)
return
}

w.WriteHeader(http.StatusNoContent)
}
39 changes: 39 additions & 0 deletions oauth2/consent_manager.go
@@ -0,0 +1,39 @@
package oauth2

import "time"

type ConsentRequest struct {
ID string `json:"id"`
CSRF string `json:"-"`
GrantedScopes []string `json:"-"`
RequestedScope []string `json:"requested_scope,omitempty"`
Audience string `json:"audience"`
Subject string `json:"-"`
ExpiresAt time.Time `json:"expires_at"`
RedirectURL string `json:"redirect_url"`
AccessTokenExtra map[string]interface{} `json:"-"`
IDTokenExtra map[string]interface{} `json:"-"`
Consent string `json:"-"`
}

func (c *ConsentRequest) IsConsentGranted() bool {
return c.Consent == ConsentRequestAccepted
}

type AcceptConsentRequestPayload struct {
AccessTokenExtra map[string]interface{} `json:"access_token_extra"`
IDTokenExtra map[string]interface{} `json:"id_token_extra"`
Subject string `json:"subject"`
GrantScopes []string `json:"grant_scopes"`
}

type ConsentRequestClient interface {
AcceptConsentRequest(id string, payload *AcceptConsentRequestPayload) error
RejectConsentRequest(id string) error
GetConsentRequest(id string) (*ConsentRequest, error)
}

type ConsentRequestManager interface {
PersistConsentRequest(*ConsentRequest) error
ConsentRequestClient
}
47 changes: 47 additions & 0 deletions oauth2/consent_manager_http.go
@@ -0,0 +1,47 @@
package oauth2

import (
"net/http"
"net/url"

"github.com/ory/hydra/pkg"
"github.com/pkg/errors"
)

type HTTPConsentManager struct {
Client *http.Client
Endpoint *url.URL
Dry bool
FakeTLSTermination bool
}

func (m *HTTPConsentManager) AcceptConsentRequest(id string, payload *AcceptConsentRequestPayload) error {
var r = pkg.NewSuperAgent(pkg.JoinURL(m.Endpoint, id, "accept").String())
r.Client = m.Client
r.Dry = m.Dry
r.FakeTLSTermination = m.FakeTLSTermination
return r.Patch(payload)
}

func (m *HTTPConsentManager) RejectConsentRequest(id string) error {
var r = pkg.NewSuperAgent(pkg.JoinURL(m.Endpoint, id, "reject").String())
r.Client = m.Client
r.Dry = m.Dry
r.FakeTLSTermination = m.FakeTLSTermination
return r.Patch(struct{}{})
}

func (m *HTTPConsentManager) GetConsentRequest(id string) (*ConsentRequest, error) {
var c ConsentRequest

var r = pkg.NewSuperAgent(pkg.JoinURL(m.Endpoint, id).String())
r.Client = m.Client
r.Dry = m.Dry
r.FakeTLSTermination = m.FakeTLSTermination

if err := r.Get(&c); err != nil {
return nil, errors.WithStack(err)
}

return &c, nil
}
52 changes: 52 additions & 0 deletions oauth2/consent_manager_memory.go
@@ -0,0 +1,52 @@
package oauth2

import (
"github.com/ory/hydra/pkg"
"github.com/pkg/errors"
)

type ConsentRequestMemoryManager struct {
requests map[string]ConsentRequest
}

func NewConsentRequestMemoryManager() *ConsentRequestMemoryManager {
return &ConsentRequestMemoryManager{requests: map[string]ConsentRequest{}}
}

func (m *ConsentRequestMemoryManager) PersistConsentRequest(session *ConsentRequest) error {
m.requests[session.ID] = *session
return nil
}

func (m *ConsentRequestMemoryManager) AcceptConsentRequest(id string, payload *AcceptConsentRequestPayload) error {
session, err := m.GetConsentRequest(id)
if err != nil {
return err
}

session.Subject = payload.Subject
session.AccessTokenExtra = payload.AccessTokenExtra
session.IDTokenExtra = payload.IDTokenExtra
session.Consent = ConsentRequestAccepted
session.GrantedScopes = payload.GrantScopes

return m.PersistConsentRequest(session)
}

func (m *ConsentRequestMemoryManager) RejectConsentRequest(id string) error {
session, err := m.GetConsentRequest(id)
if err != nil {
return err
}

session.Consent = ConsentRequestRejected
return m.PersistConsentRequest(session)
}

func (m *ConsentRequestMemoryManager) GetConsentRequest(id string) (*ConsentRequest, error) {
if session, found := m.requests[id]; !found {
return nil, errors.Wrap(pkg.ErrNotFound, "")
} else {
return &session, nil
}
}
77 changes: 77 additions & 0 deletions oauth2/consent_manager_test.go
@@ -0,0 +1,77 @@
package oauth2

import (
"fmt"
"testing"

"github.com/pborman/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

var consentManagers = map[string]ConsentRequestManager{
"memory": NewConsentRequestMemoryManager(),
}

func TestConsentRequestManagerReadWrite(t *testing.T) {
req := &ConsentRequest{ID: uuid.New()}
for k, m := range consentManagers {
t.Run(fmt.Sprintf("case=%s", k), func(t *testing.T) {
_, err := m.GetConsentRequest("1234")
assert.Error(t, err)

require.NoError(t, m.PersistConsentRequest(req))

got, err := m.GetConsentRequest(req.ID)
require.NoError(t, err)

assert.EqualValues(t, req, got)
})
}
}

func TestConsentRequestManagerUpdate(t *testing.T) {
req := &ConsentRequest{ID: uuid.New()}
for k, m := range consentManagers {
t.Run(fmt.Sprintf("case=%s", k), func(t *testing.T) {
require.NoError(t, m.PersistConsentRequest(req))

got, err := m.GetConsentRequest(req.ID)
require.NoError(t, err)
assert.False(t, got.IsConsentGranted())

require.NoError(t, m.AcceptConsentRequest(req.ID, new(AcceptConsentRequestPayload)))
got, err = m.GetConsentRequest(req.ID)
require.NoError(t, err)
assert.True(t, got.IsConsentGranted())

require.NoError(t, m.RejectConsentRequest(req.ID))
got, err = m.GetConsentRequest(req.ID)
require.NoError(t, err)
assert.False(t, got.IsConsentGranted())
})
}
}

func TestHttpRequestClient(t *testing.T) {
req := &ConsentRequest{ID: uuid.New()}
for k, m := range consentManagers {
t.Run(fmt.Sprintf("case=%s", k), func(t *testing.T) {
require.NoError(t, m.PersistConsentRequest(req))

got, err := m.GetConsentRequest(req.ID)
require.NoError(t, err)
assert.False(t, got.IsConsentGranted())

require.NoError(t, m.AcceptConsentRequest(req.ID, new(AcceptConsentRequestPayload)))
got, err = m.GetConsentRequest(req.ID)
require.NoError(t, err)
assert.True(t, got.IsConsentGranted())

require.NoError(t, m.RejectConsentRequest(req.ID))
got, err = m.GetConsentRequest(req.ID)
require.NoError(t, err)
assert.False(t, got.IsConsentGranted())
})
}
}

0 comments on commit fc3ee34

Please sign in to comment.