diff --git a/cmd/root.go b/cmd/root.go index 7c29f28163..19e8d152a8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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" ) @@ -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", "") diff --git a/metrics/metrics.go b/metrics/metrics.go index c3287f3c90..f690f1b4ca 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -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", diff --git a/oauth2/consent.go b/oauth2/consent.go index 2a3dc70fe5..688ad0c304 100644 --- a/oauth2/consent.go +++ b/oauth2/consent.go @@ -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) } diff --git a/oauth2/consent_handler.go b/oauth2/consent_handler.go new file mode 100644 index 0000000000..0c95f9e729 --- /dev/null +++ b/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) +} diff --git a/oauth2/consent_manager.go b/oauth2/consent_manager.go new file mode 100644 index 0000000000..d7d81270f4 --- /dev/null +++ b/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 +} diff --git a/oauth2/consent_manager_http.go b/oauth2/consent_manager_http.go new file mode 100644 index 0000000000..c709907835 --- /dev/null +++ b/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 +} diff --git a/oauth2/consent_manager_memory.go b/oauth2/consent_manager_memory.go new file mode 100644 index 0000000000..d8bb457bf4 --- /dev/null +++ b/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 + } +} diff --git a/oauth2/consent_manager_test.go b/oauth2/consent_manager_test.go new file mode 100644 index 0000000000..de7d9eb393 --- /dev/null +++ b/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()) + }) + } +} diff --git a/oauth2/consent_strategy.go b/oauth2/consent_strategy.go index 43c6d9b4df..86ead6087d 100644 --- a/oauth2/consent_strategy.go +++ b/oauth2/consent_strategy.go @@ -1,23 +1,20 @@ package oauth2 import ( - "crypto/rsa" - "fmt" "time" - "github.com/dgrijalva/jwt-go" + "net/http" + "github.com/gorilla/sessions" "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" ejwt "github.com/ory/fosite/token/jwt" - "github.com/ory/hydra/jwk" "github.com/pborman/uuid" "github.com/pkg/errors" ) const ( - ConsentChallengeKey = "hydra.consent.challenge" - ConsentEndpointKey = "hydra.consent.response" + CookieCSRFKey = "consent_csrf" ) type DefaultConsentStrategy struct { @@ -25,145 +22,90 @@ type DefaultConsentStrategy struct { DefaultIDTokenLifespan time.Duration DefaultChallengeLifespan time.Duration - KeyManager jwk.Manager + ConsentManager ConsentRequestManager } -func (s *DefaultConsentStrategy) ValidateResponse(a fosite.AuthorizeRequester, token string, session *sessions.Session) (claims *Session, err error) { - t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { - if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { - return nil, errors.Errorf("Unexpected signing method: %v", t.Header["alg"]) - } - - pk, err := s.KeyManager.GetKey(ConsentEndpointKey, "public") - if err != nil { - return nil, err - } - - rsaKey, ok := jwk.First(pk.Keys).Key.(*rsa.PublicKey) - if !ok { - return nil, errors.New("Could not convert to RSA Private Key") - } - return rsaKey, nil - }) +func (s *DefaultConsentStrategy) ValidateConsentRequest(req fosite.AuthorizeRequester, session string, cookie *sessions.Session) (claims *Session, err error) { + consent, err := s.ConsentManager.GetConsentRequest(session) if err != nil { - return nil, errors.Wrap(err, "The consent response is not a valid JSON Web Token") - } - - // make sure to use MapClaims since that is the default.. - jwtClaims, ok := t.Claims.(jwt.MapClaims) - if err != nil || !ok { - return nil, errors.Errorf("Couldn't parse token: %v", err) - } else if !t.Valid { - return nil, errors.Errorf("Token is invalid") + return nil, errors.WithStack(err) } - if j, ok := session.Values["consent_jti"]; !ok { - return nil, errors.Errorf("Session cookie is missing anti-replay token") - } else if js, ok := j.(string); !ok { - return nil, errors.Errorf("Session cookie anti-replay value is not a string") - } else if js != ejwt.ToString(jwtClaims["jti"]) { - return nil, errors.Errorf("Session cookie anti-replay value does not match value from consent response") - } - delete(session.Values, "jti") - - if time.Now().After(ejwt.ToTime(jwtClaims["exp"])) { + if time.Now().After(consent.ExpiresAt) { return nil, errors.Errorf("Token expired") } - if ejwt.ToString(jwtClaims["aud"]) != a.GetClient().GetID() { + if consent.Audience != req.GetClient().GetID() { return nil, errors.Errorf("Audience mismatch") } - subject := ejwt.ToString(jwtClaims["sub"]) - if subject == "" { + if consent.Subject == "" { return nil, errors.Errorf("Subject key is empty or undefined in consent response, check your payload.") } - scopes := toStringSlice(jwtClaims["scp"]) - for _, scope := range scopes { - a.GrantScope(scope) + if j, ok := cookie.Values[CookieCSRFKey]; !ok { + return nil, errors.Errorf("Session cookie is missing anti-replay token") + } else if js, ok := j.(string); !ok { + return nil, errors.Errorf("Session cookie anti-replay value is not a string") + } else if js != consent.CSRF { + return nil, errors.Errorf("Session cookie anti-replay value does not match value from consent response") } - var idExt map[string]interface{} - var atExt map[string]interface{} - if ext, ok := jwtClaims["id_ext"].(map[string]interface{}); ok { - idExt = ext - } - if ext, ok := jwtClaims["at_ext"].(map[string]interface{}); ok { - atExt = ext + if !consent.IsConsentGranted() { + err := errors.New("The resource owner denied consent for this request") + return nil, &fosite.RFC6749Error{ + Name: "Resource owner denied consent", + Description: err.Error(), + Debug: err.Error(), + Hint: "Token validation failed.", + Code: http.StatusUnauthorized, + } } - // add key id to session headers - extHeader := map[string]interface{}{ - "kid": "public", + for _, scope := range consent.GrantedScopes { + req.GrantScope(scope) } - sess := &Session{ + delete(cookie.Values, CookieCSRFKey) + + return &Session{ DefaultSession: &openid.DefaultSession{ Claims: &ejwt.IDTokenClaims{ - Audience: a.GetClient().GetID(), - Subject: subject, + Audience: req.GetClient().GetID(), + Subject: consent.Subject, Issuer: s.Issuer, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(s.DefaultIDTokenLifespan), - Extra: idExt, + Extra: consent.IDTokenExtra, }, - Headers: &ejwt.Headers{extHeader}, - Subject: subject, + // required for lookup on jwk endpoint + Headers: &ejwt.Headers{Extra: map[string]interface{}{"kid": "public"}}, + Subject: consent.Subject, }, - Extra: atExt, - } - - return sess, err + Extra: consent.AccessTokenExtra, + }, err } -func toStringSlice(i interface{}) []string { - if r, ok := i.([]string); ok { - return r - } else if r, ok := i.(fosite.Arguments); ok { - return r - } else if r, ok := i.([]interface{}); ok { - ret := make([]string, 0) - for _, y := range r { - s, ok := y.(string) - if ok { - ret = append(ret, s) - } - } - return ret - } - return []string{} -} +func (s *DefaultConsentStrategy) CreateConsentRequest(req fosite.AuthorizeRequester, redirectURL string, cookie *sessions.Session) (string, error) { + csrf := uuid.New() + id := uuid.New() -func (s *DefaultConsentStrategy) IssueChallenge(authorizeRequest fosite.AuthorizeRequester, redirectURL string, session *sessions.Session) (string, error) { - token := jwt.New(jwt.SigningMethodRS256) - jti := uuid.New() - token.Claims = jwt.MapClaims{ - "jti": jti, - "scp": authorizeRequest.GetRequestedScopes(), - "aud": authorizeRequest.GetClient().GetID(), - "exp": time.Now().Add(s.DefaultChallengeLifespan).Unix(), - "redir": redirectURL, + cookie.Values[CookieCSRFKey] = csrf + consent := &ConsentRequest{ + ID: id, + CSRF: csrf, + GrantedScopes: []string{}, + RequestedScope: req.GetRequestedScopes(), + Audience: req.GetClient().GetID(), + ExpiresAt: time.Now().Add(s.DefaultChallengeLifespan), + RedirectURL: redirectURL + "&consent=" + id, + AccessTokenExtra: map[string]interface{}{}, + IDTokenExtra: map[string]interface{}{}, } - session.Values["consent_jti"] = jti - ks, err := s.KeyManager.GetKey(ConsentChallengeKey, "private") - if err != nil { - return "", errors.WithStack(err) - } - - rsaKey, ok := jwk.First(ks.Keys).Key.(*rsa.PrivateKey) - if !ok { - return "", errors.New("Could not convert to RSA Private Key") - } - - var signature, encoded string - if encoded, err = token.SigningString(); err != nil { - return "", errors.WithStack(err) - } else if signature, err = token.Method.Sign(encoded, rsaKey); err != nil { + if err := s.ConsentManager.PersistConsentRequest(consent); err != nil { return "", errors.WithStack(err) } - return fmt.Sprintf("%s.%s", encoded, signature), nil - + return id, nil } diff --git a/oauth2/consent_test.go b/oauth2/consent_test.go index 1c2f05d842..2d2147aa16 100644 --- a/oauth2/consent_test.go +++ b/oauth2/consent_test.go @@ -1,20 +1 @@ package oauth2 - -import ( - "testing" - - "github.com/ory/fosite" - "github.com/stretchr/testify/assert" -) - -func TestToStringSlice(t *testing.T) { - assert.Equal(t, []string{"foo"}, toStringSlice((map[string]interface{}{ - "scp": fosite.Arguments{"foo"}, - })["scp"])) - assert.Equal(t, []string{"foo"}, toStringSlice((map[string]interface{}{ - "scp": []string{"foo"}, - })["scp"])) - assert.Equal(t, []string{"foo"}, toStringSlice((map[string]interface{}{ - "scp": []interface{}{"foo", 123}, - })["scp"])) -} diff --git a/oauth2/fosite_store_test.go b/oauth2/fosite_store_test.go index ff5c49a54d..ff6b164c29 100644 --- a/oauth2/fosite_store_test.go +++ b/oauth2/fosite_store_test.go @@ -7,6 +7,7 @@ import ( "flag" + "github.com/jmoiron/sqlx" "github.com/ory/fosite" "github.com/ory/hydra/client" "github.com/ory/hydra/integration" @@ -29,6 +30,11 @@ func init() { } } +var ( + pgsqldb *sqlx.DB + mysqldb *sqlx.DB +) + func TestMain(m *testing.M) { flag.Parse() if !testing.Short() { @@ -48,6 +54,7 @@ func connectToPG() { logrus.Fatalf("Could not create postgres schema: %v", err) } + pgsqldb = db clientManagers["postgres"] = s } @@ -58,6 +65,7 @@ func connectToMySQL() { logrus.Fatalf("Could not create postgres schema: %v", err) } + mysqldb = db clientManagers["mysql"] = s } diff --git a/oauth2/handler.go b/oauth2/handler.go index b64d4b4356..1e35c32fb3 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -17,9 +17,9 @@ import ( const ( OpenIDConnectKeyName = "hydra.openid.id-token" - ConsentPath = "/oauth2/consent" - TokenPath = "/oauth2/token" - AuthPath = "/oauth2/auth" + DefaultConsentPath = "/oauth2/consent-fallback" + TokenPath = "/oauth2/token" + AuthPath = "/oauth2/auth" WellKnownPath = "/.well-known/openid-configuration" JWKPath = "/.well-known/jwks.json" @@ -86,7 +86,7 @@ func (h *Handler) SetRoutes(r *httprouter.Router) { r.POST(TokenPath, h.TokenHandler) r.GET(AuthPath, h.AuthHandler) r.POST(AuthPath, h.AuthHandler) - r.GET(ConsentPath, h.DefaultConsentHandler) + r.GET(DefaultConsentPath, h.DefaultConsentHandler) r.POST(IntrospectPath, h.IntrospectHandler) r.POST(RevocationPath, h.RevocationHandler) r.GET(WellKnownPath, h.WellKnownHandler) @@ -290,8 +290,8 @@ func (h *Handler) AuthHandler(w http.ResponseWriter, r *http.Request, _ httprout } // A session_token will be available if the user was authenticated an gave consent - consentToken := authorizeRequest.GetRequestForm().Get("consent") - if consentToken == "" { + consent := authorizeRequest.GetRequestForm().Get("consent") + if consent == "" { // otherwise redirect to log in endpoint if err := h.redirectToConsent(w, r, authorizeRequest); err != nil { pkg.LogError(err, h.L) @@ -299,16 +299,6 @@ func (h *Handler) AuthHandler(w http.ResponseWriter, r *http.Request, _ httprout return } return - } else if consentToken == "denied" { - err := errors.New("The resource owner denied consent for this request") - h.writeAuthorizeError(w, authorizeRequest, &fosite.RFC6749Error{ - Name: "Resource owner denied consent", - Description: err.Error(), - Debug: err.Error(), - Hint: "Token validation failed.", - Code: http.StatusUnauthorized, - }) - return } cookie, err := h.CookieStore.Get(r, consentCookieName) @@ -320,7 +310,7 @@ func (h *Handler) AuthHandler(w http.ResponseWriter, r *http.Request, _ httprout // decode consent_token claims // verify anti-CSRF (inject state) and anti-replay token (expiry time, good value would be 10 seconds) - session, err := h.Consent.ValidateResponse(authorizeRequest, consentToken, cookie) + session, err := h.Consent.ValidateConsentRequest(authorizeRequest, consent, cookie) if err != nil { pkg.LogError(err, h.L) h.writeAuthorizeError(w, authorizeRequest, errors.Wrap(fosite.ErrAccessDenied, "")) @@ -369,14 +359,14 @@ func (h *Handler) redirectToConsent(w http.ResponseWriter, r *http.Request, auth } authUrl.RawQuery = r.URL.RawQuery - challenge, err := h.Consent.IssueChallenge(authorizeRequest, authUrl.String(), cookie) + challenge, err := h.Consent.CreateConsentRequest(authorizeRequest, authUrl.String(), cookie) if err != nil { return err } p := h.ConsentURL q := p.Query() - q.Set("challenge", challenge) + q.Set("consent", challenge) p.RawQuery = q.Encode() if err := cookie.Save(r, w); err != nil { diff --git a/oauth2/handler_consent_test.go b/oauth2/handler_consent_test.go index 898a956c04..225b341177 100644 --- a/oauth2/handler_consent_test.go +++ b/oauth2/handler_consent_test.go @@ -21,7 +21,7 @@ func TestHandlerConsent(t *testing.T) { h.SetRoutes(r) ts := httptest.NewServer(r) - res, err := http.Get(ts.URL + "/oauth2/consent") + res, err := http.Get(ts.URL + DefaultConsentPath) assert.Nil(t, err) defer res.Body.Close() diff --git a/oauth2/handler_test.go b/oauth2/handler_test.go index 98aa037aaf..26b1576218 100644 --- a/oauth2/handler_test.go +++ b/oauth2/handler_test.go @@ -59,11 +59,11 @@ type FakeConsentStrategy struct { RedirectURL string } -func (s *FakeConsentStrategy) ValidateResponse(authorizeRequest fosite.AuthorizeRequester, token string, session *sessions.Session) (claims *Session, err error) { +func (s *FakeConsentStrategy) ValidateConsentRequest(authorizeRequest fosite.AuthorizeRequester, token string, session *sessions.Session) (claims *Session, err error) { return nil, nil } -func (s *FakeConsentStrategy) IssueChallenge(authorizeRequest fosite.AuthorizeRequester, redirectURL string, session *sessions.Session) (token string, err error) { +func (s *FakeConsentStrategy) CreateConsentRequest(authorizeRequest fosite.AuthorizeRequester, redirectURL string, session *sessions.Session) (token string, err error) { s.RedirectURL = redirectURL return "token", nil } diff --git a/oauth2/oauth2_auth_code_test.go b/oauth2/oauth2_auth_code_test.go index e76290592a..dbe9c30838 100644 --- a/oauth2/oauth2_auth_code_test.go +++ b/oauth2/oauth2_auth_code_test.go @@ -9,12 +9,8 @@ import ( "testing" "time" - "github.com/dgrijalva/jwt-go" "github.com/julienschmidt/httprouter" - ejwt "github.com/ory/fosite/token/jwt" - "github.com/ory/hydra/jwk" . "github.com/ory/hydra/oauth2" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" @@ -23,37 +19,21 @@ import ( func TestAuthCode(t *testing.T) { var code string var validConsent bool + router.GET("/consent", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - tok, err := jwt.Parse(r.URL.Query().Get("challenge"), func(tt *jwt.Token) (interface{}, error) { - if _, ok := tt.Method.(*jwt.SigningMethodRSA); !ok { - return nil, errors.Errorf("Unexpected signing method: %v", tt.Header["alg"]) - } - - pk, err := keyManager.GetKey(ConsentChallengeKey, "public") - require.NoError(t, err) - return jwk.MustRSAPublic(jwk.First(pk.Keys)), nil - }) - require.NoError(t, err) - require.True(t, tok.Valid) - - jwtClaims, ok := tok.Claims.(jwt.MapClaims) - require.True(t, ok) - require.NotEmpty(t, jwtClaims) - - expl := map[string]interface{}{"foo": "bar", "baz": map[string]interface{}{"foo": "baz"}} - consent, err := signConsentToken(map[string]interface{}{ - "jti": jwtClaims["jti"], - "exp": time.Now().Add(time.Hour).Unix(), - "iat": time.Now().Unix(), - "sub": "foo", - "aud": "app-client", - "scp": []string{"hydra", "offline"}, - "at_ext": expl, - "id_ext": expl, - }) - require.NoError(t, err) - - http.Redirect(w, r, ejwt.ToString(jwtClaims["redir"])+"&consent="+consent, http.StatusFound) + cr, err := consentClient.GetConsentRequest(r.URL.Query().Get("consent")) + assert.NoError(t, err) + + assert.EqualValues(t, []string{"hydra.*", "offline"}, cr.RequestedScope) + assert.Equal(t, r.URL.Query().Get("consent"), cr.ID) + assert.True(t, strings.Contains(cr.RedirectURL, "oauth2/auth?client_id=app-client")) + + require.NoError(t, consentClient.AcceptConsentRequest(r.URL.Query().Get("consent"), &AcceptConsentRequestPayload{ + Subject: "foo", + GrantScopes: []string{"hydra.*", "offline"}, + })) + + http.Redirect(w, r, cr.RedirectURL, http.StatusFound) validConsent = true }) @@ -79,6 +59,8 @@ func TestAuthCode(t *testing.T) { token, err := oauthConfig.Exchange(oauth2.NoContext, code) require.NoError(t, err, code) + t.Logf("Got extra: %v", token) + time.Sleep(time.Second * 5) res, err := testRefresh(t, token) diff --git a/oauth2/oauth2_test.go b/oauth2/oauth2_test.go index 9bd035260d..d1c0f5c701 100644 --- a/oauth2/oauth2_test.go +++ b/oauth2/oauth2_test.go @@ -1,21 +1,20 @@ package oauth2_test import ( - "fmt" "net/http/httptest" "net/url" "time" - "github.com/dgrijalva/jwt-go" "github.com/gorilla/sessions" "github.com/julienschmidt/httprouter" "github.com/ory/fosite" "github.com/ory/fosite/compose" + "github.com/ory/herodot" hc "github.com/ory/hydra/client" - "github.com/ory/hydra/jwk" + hcompose "github.com/ory/hydra/compose" . "github.com/ory/hydra/oauth2" "github.com/ory/hydra/pkg" - "github.com/pkg/errors" + "github.com/ory/ladon" "github.com/sirupsen/logrus" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" @@ -34,9 +33,6 @@ var store = &FositeMemoryStore{ RefreshTokens: make(map[string]fosite.Requester), } -var keyManager = &jwk.MemoryManager{} -var keyGenerator = &jwk.RS256Generator{} - var fc = &compose.Config{ AccessTokenLifespan: time.Second, } @@ -62,7 +58,7 @@ var handler = &Handler{ ), Consent: &DefaultConsentStrategy{ Issuer: "http://hydra.localhost", - KeyManager: keyManager, + ConsentManager: consentManager, DefaultChallengeLifespan: time.Hour, DefaultIDTokenLifespan: time.Hour * 24, }, @@ -77,28 +73,34 @@ var ts *httptest.Server var oauthConfig *oauth2.Config var oauthClientConfig *clientcredentials.Config +var localWarden, httpClient = hcompose.NewMockFirewall("foo", "app-client", fosite.Arguments{ConsentScope}, &ladon.DefaultPolicy{ + ID: "1", + Subjects: []string{"app-client"}, + Resources: []string{"rn:hydra:oauth2:consent:requests:<.*>"}, + Actions: []string{"get", "accept", "reject"}, + Effect: ladon.AllowAccess, +}) + +var consentHandler *ConsentSessionHandler +var consentManager = NewConsentRequestMemoryManager() +var consentClient *HTTPConsentManager + func init() { - keys, err := keyGenerator.Generate("") - pkg.Must(err, "") - keyManager.AddKeySet(ConsentChallengeKey, keys) + consentHandler = &ConsentSessionHandler{ + H: herodot.NewJSONWriter(nil), + W: localWarden, + M: consentManager, + } - keys, err = keyGenerator.Generate("") - pkg.Must(err, "") - keyManager.AddKeySet(ConsentEndpointKey, keys) ts = httptest.NewServer(router) - handler.Issuer = ts.URL handler.SetRoutes(router) + consentHandler.SetRoutes(router) + h, _ := hasher.Hash([]byte("secret")) - store.Manager.(*hc.MemoryManager).Clients["app"] = hc.Client{ - ID: "app", - Secret: string(h), - RedirectURIs: []string{ts.URL + "/callback"}, - ResponseTypes: []string{"id_token", "code", "token"}, - GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"}, - Scope: "hydra", - } + u, _ := url.Parse(ts.URL + ConsentSessionPath) + consentClient = &HTTPConsentManager{Client: httpClient, Endpoint: u} c, _ := url.Parse(ts.URL + "/consent") handler.ConsentURL = *c @@ -109,7 +111,7 @@ func init() { RedirectURIs: []string{ts.URL + "/callback"}, ResponseTypes: []string{"id_token", "code", "token"}, GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"}, - Scope: "hydra", + Scope: "hydra.* offline", } oauthConfig = &oauth2.Config{ @@ -120,36 +122,13 @@ func init() { TokenURL: ts.URL + "/oauth2/token", }, RedirectURL: ts.URL + "/callback", - Scopes: []string{"hydra"}, + Scopes: []string{"hydra.*", "offline"}, } oauthClientConfig = &clientcredentials.Config{ ClientID: "app-client", ClientSecret: "secret", TokenURL: ts.URL + "/oauth2/token", - Scopes: []string{"hydra"}, - } -} - -func signConsentToken(claims jwt.MapClaims) (string, error) { - token := jwt.New(jwt.SigningMethodRS256) - token.Claims = claims - - keys, err := keyManager.GetKey(ConsentEndpointKey, "private") - if err != nil { - return "", errors.WithStack(err) + Scopes: []string{"hydra.consent", "offline"}, } - rsaKey, err := jwk.ToRSAPrivate(jwk.First(keys.Keys)) - if err != nil { - return "", err - } - - var signature, encoded string - if encoded, err = token.SigningString(); err != nil { - return "", errors.WithStack(err) - } else if signature, err = token.Method.Sign(encoded, rsaKey); err != nil { - return "", errors.WithStack(err) - } - - return fmt.Sprintf("%s.%s", encoded, signature), nil } diff --git a/pkg/superagent.go b/pkg/superagent.go index 3ea9ee517e..255d27fd2f 100644 --- a/pkg/superagent.go +++ b/pkg/superagent.go @@ -100,6 +100,10 @@ func (s *SuperAgent) Update(o interface{}) error { return s.send("PUT", o, o) } +func (s *SuperAgent) Patch(o interface{}) error { + return s.send("PATCH", o, o) +} + func (s *SuperAgent) send(method string, in interface{}, out interface{}) error { if s.Client == nil { s.Client = http.DefaultClient diff --git a/sdk/consent_test.go b/sdk/consent_test.go index 101b67ca66..89b7683f45 100644 --- a/sdk/consent_test.go +++ b/sdk/consent_test.go @@ -37,7 +37,7 @@ func TestConsentHelper(t *testing.T) { ar := fosite.NewAuthorizeRequest() ar.Client = &fosite.DefaultClient{ID: "foobarclient"} - challenge, err := s.IssueChallenge(ar, "http://hydra/oauth2/auth?client_id=foobarclient", &sessions.Session{Values: map[interface{}]interface{}{}}) + challenge, err := s.CreateConsentRequest(ar, "http://hydra/oauth2/auth?client_id=foobarclient", &sessions.Session{Values: map[interface{}]interface{}{}}) require.Nil(t, err) claims, err := c.VerifyChallenge(challenge)