hydra/consent/handler.go

1095 lines
34 KiB
Go

// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package consent
import (
"cmp"
"encoding/json"
"net/http"
"net/url"
"time"
"github.com/pkg/errors"
"github.com/ory/hydra/v2/client"
"github.com/ory/hydra/v2/flow"
"github.com/ory/hydra/v2/fosite"
"github.com/ory/hydra/v2/x"
"github.com/ory/hydra/v2/x/events"
"github.com/ory/x/httprouterx"
"github.com/ory/x/otelx"
keysetpagination "github.com/ory/x/pagination/keysetpagination_v2"
"github.com/ory/x/pagination/tokenpagination"
"github.com/ory/x/sqlxx"
"github.com/ory/x/urlx"
)
type Handler struct {
r InternalRegistry
}
const (
LoginPath = "/oauth2/auth/requests/login"
DevicePath = "/oauth2/auth/requests/device"
ConsentPath = "/oauth2/auth/requests/consent"
LogoutPath = "/oauth2/auth/requests/logout"
SessionsPath = "/oauth2/auth/sessions"
)
func NewHandler(r InternalRegistry) *Handler {
return &Handler{r: r}
}
func (h *Handler) SetRoutes(admin *httprouterx.RouterAdmin) {
admin.GET(LoginPath, h.getOAuth2LoginRequest)
admin.PUT(LoginPath+"/accept", h.acceptOAuth2LoginRequest)
admin.PUT(LoginPath+"/reject", h.rejectOAuth2LoginRequest)
admin.GET(ConsentPath, h.getOAuth2ConsentRequest)
admin.PUT(ConsentPath+"/accept", h.acceptOAuth2ConsentRequest)
admin.PUT(ConsentPath+"/reject", h.rejectOAuth2ConsentRequest)
admin.DELETE(SessionsPath+"/login", h.revokeOAuth2LoginSessions)
admin.GET(SessionsPath+"/consent", h.listOAuth2ConsentSessions)
admin.DELETE(SessionsPath+"/consent", h.revokeOAuth2ConsentSessions)
admin.GET(LogoutPath, h.getOAuth2LogoutRequest)
admin.PUT(LogoutPath+"/accept", h.acceptOAuth2LogoutRequest)
admin.PUT(LogoutPath+"/reject", h.rejectOAuth2LogoutRequest)
admin.PUT(DevicePath+"/accept", h.acceptUserCodeRequest)
}
// Revoke OAuth 2.0 Consent Session Parameters
//
// swagger:parameters revokeOAuth2ConsentSessions
type _ struct {
// OAuth 2.0 Consent Subject
//
// The subject whose consent sessions should be deleted.
//
// in: query
Subject string `json:"subject"`
// OAuth 2.0 Client ID
//
// If set, deletes only those consent sessions that have been granted to the specified OAuth 2.0 Client ID.
//
// in: query
Client string `json:"client"`
// Consent Request ID
//
// If set, revoke all token chains derived from this particular consent request ID.
//
// in: query
ConsentRequestID string `json:"consent_request_id"`
// Revoke All Consent Sessions
//
// If set to `true` deletes all consent sessions by the Subject that have been granted.
//
// in: query
All bool `json:"all"`
}
// swagger:route DELETE /admin/oauth2/auth/sessions/consent oAuth2 revokeOAuth2ConsentSessions
//
// # Revoke OAuth 2.0 Consent Sessions of a Subject
//
// This endpoint revokes a subject's granted consent sessions and invalidates all
// associated OAuth 2.0 Access Tokens. You may also only revoke sessions for a specific OAuth 2.0 Client ID.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 204: emptyResponse
// default: errorOAuth2
func (h *Handler) revokeOAuth2ConsentSessions(w http.ResponseWriter, r *http.Request) {
var (
subject = r.URL.Query().Get("subject")
clientID = r.URL.Query().Get("client")
consentRequestID = r.URL.Query().Get("consent_request_id")
allClients = r.URL.Query().Get("all") == "true"
)
switch {
case consentRequestID != "" && subject == "" && clientID == "":
if err := h.r.ConsentManager().RevokeConsentSessionByID(r.Context(), consentRequestID); err != nil && !errors.Is(err, x.ErrNotFound) {
h.r.Writer().WriteError(w, r, err)
return
}
events.Trace(r.Context(), events.ConsentRevoked, events.WithConsentRequestID(consentRequestID))
case consentRequestID == "" && subject != "" && clientID != "" && !allClients:
if err := h.r.ConsentManager().RevokeSubjectClientConsentSession(r.Context(), subject, clientID); err != nil && !errors.Is(err, x.ErrNotFound) {
h.r.Writer().WriteError(w, r, err)
return
}
events.Trace(r.Context(), events.ConsentRevoked, events.WithSubject(subject), events.WithClientID(clientID))
case consentRequestID == "" && subject != "" && clientID == "" && allClients:
if err := h.r.ConsentManager().RevokeSubjectConsentSession(r.Context(), subject); err != nil && !errors.Is(err, x.ErrNotFound) {
h.r.Writer().WriteError(w, r, err)
return
}
events.Trace(r.Context(), events.ConsentRevoked, events.WithSubject(subject))
default:
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrInvalidRequest.WithHint("Invalid combination of query parameters.")))
return
}
w.WriteHeader(http.StatusNoContent)
}
// List OAuth 2.0 Consent Session Parameters
//
// swagger:parameters listOAuth2ConsentSessions
type _ struct {
tokenpagination.RequestParameters
// The subject to list the consent sessions for.
//
// in: query
// required: true
Subject string `json:"subject"`
// The login session id to list the consent sessions for.
//
// in: query
// required: false
LoginSessionId string `json:"login_session_id"`
}
// swagger:route GET /admin/oauth2/auth/sessions/consent oAuth2 listOAuth2ConsentSessions
//
// # List OAuth 2.0 Consent Sessions of a Subject
//
// This endpoint lists all subject's granted consent sessions, including client and granted scope.
// If the subject is unknown or has not granted any consent sessions yet, the endpoint returns an
// empty JSON array with status code 200 OK.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: oAuth2ConsentSessions
// default: errorOAuth2
func (h *Handler) listOAuth2ConsentSessions(w http.ResponseWriter, r *http.Request) {
subject := r.URL.Query().Get("subject")
if subject == "" {
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'subject' is not defined but should have been.`)))
return
}
pageKeys := h.r.Config().GetPaginationEncryptionKeys(r.Context())
pageOpts, err := keysetpagination.ParseQueryParams(pageKeys, r.URL.Query())
if err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrInvalidRequest.WithWrap(err).WithHintf("Unable to parse pagination parameters: %s", err)))
return
}
var requests []flow.Flow
var nextPage *keysetpagination.Paginator
if loginSessionID := r.URL.Query().Get("login_session_id"); len(loginSessionID) == 0 {
requests, nextPage, err = h.r.ConsentManager().FindSubjectsGrantedConsentRequests(r.Context(), subject, pageOpts...)
} else {
requests, nextPage, err = h.r.ConsentManager().FindSubjectsSessionGrantedConsentRequests(r.Context(), subject, loginSessionID, pageOpts...)
}
if errors.Is(err, ErrNoPreviousConsentFound) {
h.r.Writer().Write(w, r, []flow.OAuth2ConsentSession{})
return
} else if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
// For legacy reasons, this API returns the format like below. Internally, we keep a different format.
sessions := make([]*flow.OAuth2ConsentSession, len(requests))
for i, f := range requests {
sessions[i] = f.ToListConsentSessionResponse()
}
keysetpagination.SetLinkHeader(w, pageKeys, r.URL, nextPage)
h.r.Writer().Write(w, r, sessions)
}
// Revoke OAuth 2.0 Consent Login Sessions Parameters
//
// swagger:parameters revokeOAuth2LoginSessions
type _ struct {
// OAuth 2.0 Subject
//
// The subject to revoke authentication sessions for.
//
// in: query
Subject string `json:"subject"`
// Login Session ID
//
// The login session to revoke.
//
// in: query
SessionID string `json:"sid"`
}
// swagger:route DELETE /admin/oauth2/auth/sessions/login oAuth2 revokeOAuth2LoginSessions
//
// # Revokes OAuth 2.0 Login Sessions by either a Subject or a SessionID
//
// This endpoint invalidates authentication sessions. After revoking the authentication session(s), the subject
// has to re-authenticate at the Ory OAuth2 Provider. This endpoint does not invalidate any tokens.
//
// If you send the subject in a query param, all authentication sessions that belong to that subject are revoked.
// No OpenID Connect Front- or Back-channel logout is performed in this case.
//
// Alternatively, you can send a SessionID via `sid` query param, in which case, only the session that is connected
// to that SessionID is revoked. OpenID Connect Back-channel logout is performed in this case.
//
// When using Ory for the identity provider, the login provider will also invalidate the session cookie.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 204: emptyResponse
// default: errorOAuth2
func (h *Handler) revokeOAuth2LoginSessions(w http.ResponseWriter, r *http.Request) {
sid := r.URL.Query().Get("sid")
subject := r.URL.Query().Get("subject")
if sid == "" && subject == "" {
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrInvalidRequest.WithHint(`Either 'subject' or 'sid' query parameters need to be defined.`)))
return
}
if sid != "" {
if err := h.r.ConsentStrategy().HandleHeadlessLogout(r.Context(), w, r, sid); err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
w.WriteHeader(http.StatusNoContent)
return
}
if err := h.r.LoginManager().RevokeSubjectLoginSession(r.Context(), subject); err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
// Get OAuth 2.0 Login Request
//
// swagger:parameters getOAuth2LoginRequest
type _ struct {
// OAuth 2.0 Login Request Challenge
//
// in: query
// required: true
Challenge string `json:"login_challenge"`
}
// swagger:route GET /admin/oauth2/auth/requests/login oAuth2 getOAuth2LoginRequest
//
// # Get OAuth 2.0 Login Request
//
// When an authorization code, hybrid, or implicit OAuth 2.0 Flow is initiated, Ory asks the login provider
// to authenticate the subject and then tell the Ory OAuth2 Service about it.
//
// Per default, the login provider is Ory itself. You may use a different login provider which needs to be a web-app
// you write and host, and it must be able to authenticate ("show the subject a login screen")
// a subject (in OAuth2 the proper name for subject is "resource owner").
//
// The authentication challenge is appended to the login provider URL to which the subject's user-agent (browser) is redirected to. The login
// provider uses that challenge to fetch information on the OAuth2 request and then accept or reject the requested authentication process.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: oAuth2LoginRequest
// 410: oAuth2RedirectTo
// default: errorOAuth2
func (h *Handler) getOAuth2LoginRequest(w http.ResponseWriter, r *http.Request) {
var err error
ctx, span := h.r.Tracer(r.Context()).Tracer().Start(r.Context(), "consent.getOAuth2LoginRequest")
defer otelx.End(span, &err)
challenge := cmp.Or(
r.URL.Query().Get("login_challenge"),
r.URL.Query().Get("challenge"),
)
if challenge == "" {
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'challenge' is not defined but should have been.`)))
return
}
f, err := flow.DecodeFromLoginChallenge(ctx, h.r, challenge)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
if f.State.LoginWasUsed() {
h.r.Writer().WriteCode(w, r, http.StatusGone, &flow.OAuth2RedirectTo{
RedirectTo: f.RequestURL,
})
return
}
// Keep compatibility with the old / existing login request format.
lr := f.GetLoginRequest()
lr.ID = challenge // The ID of the login request is the AEAD challenge.
lr.Client.Secret = ""
h.r.Writer().Write(w, r, lr)
}
// Accept OAuth 2.0 Login Request
//
// swagger:parameters acceptOAuth2LoginRequest
type _ struct {
// OAuth 2.0 Login Request Challenge
//
// in: query
// required: true
Challenge string `json:"login_challenge"`
// in: body
Body flow.HandledLoginRequest
}
// swagger:route PUT /admin/oauth2/auth/requests/login/accept oAuth2 acceptOAuth2LoginRequest
//
// # Accept OAuth 2.0 Login Request
//
// When an authorization code, hybrid, or implicit OAuth 2.0 Flow is initiated, Ory asks the login provider
// to authenticate the subject and then tell the Ory OAuth2 Service about it.
//
// The authentication challenge is appended to the login provider URL to which the subject's user-agent (browser) is redirected to. The login
// provider uses that challenge to fetch information on the OAuth2 request and then accept or reject the requested authentication process.
//
// This endpoint tells Ory that the subject has successfully authenticated and includes additional information such as
// the subject's ID and if Ory should remember the subject's subject agent for future authentication attempts by setting
// a cookie.
//
// The response contains a redirect URL which the login provider should redirect the user-agent to.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: oAuth2RedirectTo
// default: errorOAuth2
func (h *Handler) acceptOAuth2LoginRequest(w http.ResponseWriter, r *http.Request) {
var err error
ctx, span := h.r.Tracer(r.Context()).Tracer().Start(r.Context(), "consent.acceptOAuth2LoginRequest")
defer otelx.End(span, &err)
challenge := cmp.Or(
r.URL.Query().Get("login_challenge"),
r.URL.Query().Get("challenge"),
)
if challenge == "" {
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'challenge' is not defined but should have been.`)))
return
}
var payload flow.HandledLoginRequest
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrInvalidRequest.WithWrap(err).WithHintf("Unable to decode body because: %s", err)))
return
}
if payload.Subject == "" {
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrInvalidRequest.WithHint("Field 'subject' must not be empty.")))
return
}
f, err := flow.DecodeFromLoginChallenge(ctx, h.r, challenge)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
} else if f.Subject != "" && payload.Subject != f.Subject {
// The subject that was confirmed by the login screen does not match what we
// remembered in the session cookie. We handle this gracefully by redirecting the
// original authorization request URL, but attaching "prompt=login" to the query.
// This forces the user to log in again.
requestURL, err := url.Parse(f.RequestURL)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
h.r.Writer().Write(w, r, &flow.OAuth2RedirectTo{
RedirectTo: urlx.SetQuery(requestURL, url.Values{"prompt": {"login"}}).String(),
})
return
}
if f.LoginSkip {
payload.Remember = true // If skip is true remember is also true to allow consecutive calls as the same user!
} else {
f.LoginAuthenticatedAt = sqlxx.NullTime(time.Now().UTC().
// Rounding is important to avoid SQL time synchronization issues in e.g. MySQL!
Truncate(time.Second))
}
if err := f.HandleLoginRequest(&payload); err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(err))
return
}
ru, err := url.Parse(f.RequestURL)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
verifier, err := f.ToLoginVerifier(ctx, h.r)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
events.Trace(ctx, events.LoginAccepted, events.WithClientID(f.Client.GetID()), events.WithSubject(payload.Subject))
h.r.Writer().Write(w, r, &flow.OAuth2RedirectTo{
RedirectTo: urlx.SetQuery(ru, url.Values{"login_verifier": {verifier}}).String(),
})
}
// Reject OAuth 2.0 Login Request
//
// swagger:parameters rejectOAuth2LoginRequest
type _ struct {
// OAuth 2.0 Login Request Challenge
//
// in: query
// required: true
Challenge string `json:"login_challenge"`
// in: body
Body flow.RequestDeniedError
}
// swagger:route PUT /admin/oauth2/auth/requests/login/reject oAuth2 rejectOAuth2LoginRequest
//
// # Reject OAuth 2.0 Login Request
//
// When an authorization code, hybrid, or implicit OAuth 2.0 Flow is initiated, Ory asks the login provider
// to authenticate the subject and then tell the Ory OAuth2 Service about it.
//
// The authentication challenge is appended to the login provider URL to which the subject's user-agent (browser) is redirected to. The login
// provider uses that challenge to fetch information on the OAuth2 request and then accept or reject the requested authentication process.
//
// This endpoint tells Ory that the subject has not authenticated and includes a reason why the authentication
// was denied.
//
// The response contains a redirect URL which the login provider should redirect the user-agent to.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: oAuth2RedirectTo
// default: errorOAuth2
func (h *Handler) rejectOAuth2LoginRequest(w http.ResponseWriter, r *http.Request) {
var err error
ctx, span := h.r.Tracer(r.Context()).Tracer().Start(r.Context(), "consent.rejectOAuth2LoginRequest")
defer otelx.End(span, &err)
challenge := cmp.Or(
r.URL.Query().Get("login_challenge"),
r.URL.Query().Get("challenge"),
)
if challenge == "" {
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'challenge' is not defined but should have been.`)))
return
}
var payload flow.RequestDeniedError
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrInvalidRequest.WithWrap(err).WithHintf("Unable to decode body because: %s", err)))
return
}
payload.Valid = true
payload.SetDefaults(flow.LoginRequestDeniedErrorName)
f, err := flow.DecodeFromLoginChallenge(ctx, h.r, challenge)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
if err := f.HandleLoginError(&payload); err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(err))
return
}
verifier, err := f.ToLoginVerifier(ctx, h.r)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
ru, err := url.Parse(f.RequestURL)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
events.Trace(ctx, events.LoginRejected, events.WithClientID(f.Client.GetID()), events.WithSubject(f.Subject))
h.r.Writer().Write(w, r, &flow.OAuth2RedirectTo{
RedirectTo: urlx.SetQuery(ru, url.Values{"login_verifier": {verifier}}).String(),
})
}
// Get OAuth 2.0 Consent Request
//
// swagger:parameters getOAuth2ConsentRequest
type _ struct {
// OAuth 2.0 Consent Request Challenge
//
// in: query
// required: true
Challenge string `json:"consent_challenge"`
}
// swagger:route GET /admin/oauth2/auth/requests/consent oAuth2 getOAuth2ConsentRequest
//
// # Get OAuth 2.0 Consent Request
//
// When an authorization code, hybrid, or implicit OAuth 2.0 Flow is initiated, Ory asks the login provider
// to authenticate the subject and then tell Ory now about it. If the subject authenticated, he/she must now be asked if
// the OAuth 2.0 Client which initiated the flow should be allowed to access the resources on the subject's behalf.
//
// The consent challenge is appended to the consent provider's URL to which the subject's user-agent (browser) is redirected to. The consent
// provider uses that challenge to fetch information on the OAuth2 request and then tells Ory if the subject accepted
// or rejected the request.
//
// The default consent provider is available via the Ory Managed Account Experience. To customize the consent provider, please
// head over to the OAuth 2.0 documentation.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: oAuth2ConsentRequest
// 410: oAuth2RedirectTo
// default: errorOAuth2
func (h *Handler) getOAuth2ConsentRequest(w http.ResponseWriter, r *http.Request) {
var err error
ctx, span := h.r.Tracer(r.Context()).Tracer().Start(r.Context(), "consent.getOAuth2ConsentRequest")
defer otelx.End(span, &err)
challenge := cmp.Or(
r.URL.Query().Get("consent_challenge"),
r.URL.Query().Get("challenge"),
)
if challenge == "" {
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'challenge' is not defined but should have been.`)))
return
}
f, err := flow.DecodeFromConsentChallenge(ctx, h.r, challenge)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
if f.State.ConsentWasUsed() {
h.r.Writer().WriteCode(w, r, http.StatusGone, &flow.OAuth2RedirectTo{
RedirectTo: f.RequestURL,
})
return
}
// Transform flow to the existing API format.
req := f.GetConsentRequest(challenge)
req.Client.Secret = ""
h.r.Writer().Write(w, r, req)
}
// Accept OAuth 2.0 Consent Request
//
// swagger:parameters acceptOAuth2ConsentRequest
type _ struct {
// OAuth 2.0 Consent Request Challenge
//
// in: query
// required: true
Challenge string `json:"consent_challenge"`
// in: body
Body flow.AcceptOAuth2ConsentRequest
}
// swagger:route PUT /admin/oauth2/auth/requests/consent/accept oAuth2 acceptOAuth2ConsentRequest
//
// # Accept OAuth 2.0 Consent Request
//
// When an authorization code, hybrid, or implicit OAuth 2.0 Flow is initiated, Ory asks the login provider
// to authenticate the subject and then tell Ory now about it. If the subject authenticated, he/she must now be asked if
// the OAuth 2.0 Client which initiated the flow should be allowed to access the resources on the subject's behalf.
//
// The consent challenge is appended to the consent provider's URL to which the subject's user-agent (browser) is redirected to. The consent
// provider uses that challenge to fetch information on the OAuth2 request and then tells Ory if the subject accepted
// or rejected the request.
//
// This endpoint tells Ory that the subject has authorized the OAuth 2.0 client to access resources on his/her behalf.
// The consent provider includes additional information, such as session data for access and ID tokens, and if the
// consent request should be used as basis for future requests.
//
// The response contains a redirect URL which the consent provider should redirect the user-agent to.
//
// The default consent provider is available via the Ory Managed Account Experience. To customize the consent provider, please
// head over to the OAuth 2.0 documentation.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: oAuth2RedirectTo
// default: errorOAuth2
func (h *Handler) acceptOAuth2ConsentRequest(w http.ResponseWriter, r *http.Request) {
var err error
ctx, span := h.r.Tracer(r.Context()).Tracer().Start(r.Context(), "consent.acceptOAuth2ConsentRequest")
defer otelx.End(span, &err)
challenge := cmp.Or(
r.URL.Query().Get("consent_challenge"),
r.URL.Query().Get("challenge"),
)
if challenge == "" {
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'challenge' is not defined but should have been.`)))
return
}
var payload flow.AcceptOAuth2ConsentRequest
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
h.r.Writer().WriteErrorCode(w, r, http.StatusBadRequest, errors.WithStack(err))
return
}
f, err := flow.DecodeFromConsentChallenge(ctx, h.r, challenge)
if err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(err))
return
}
if err := f.HandleConsentRequest(&payload); err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(err))
return
}
ru, err := url.Parse(f.RequestURL)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
verifier, err := f.ToConsentVerifier(ctx, h.r)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
events.Trace(ctx, events.ConsentAccepted, events.WithClientID(f.Client.GetID()), events.WithSubject(f.Subject))
h.r.Writer().Write(w, r, &flow.OAuth2RedirectTo{
RedirectTo: urlx.SetQuery(ru, url.Values{"consent_verifier": {verifier}}).String(),
})
}
// Reject OAuth 2.0 Consent Request
//
// swagger:parameters rejectOAuth2ConsentRequest
type _ struct {
// OAuth 2.0 Consent Request Challenge
//
// in: query
// required: true
Challenge string `json:"consent_challenge"`
// in: body
Body flow.RequestDeniedError
}
// swagger:route PUT /admin/oauth2/auth/requests/consent/reject oAuth2 rejectOAuth2ConsentRequest
//
// # Reject OAuth 2.0 Consent Request
//
// When an authorization code, hybrid, or implicit OAuth 2.0 Flow is initiated, Ory asks the login provider
// to authenticate the subject and then tell Ory now about it. If the subject authenticated, he/she must now be asked if
// the OAuth 2.0 Client which initiated the flow should be allowed to access the resources on the subject's behalf.
//
// The consent challenge is appended to the consent provider's URL to which the subject's user-agent (browser) is redirected to. The consent
// provider uses that challenge to fetch information on the OAuth2 request and then tells Ory if the subject accepted
// or rejected the request.
//
// This endpoint tells Ory that the subject has not authorized the OAuth 2.0 client to access resources on his/her behalf.
// The consent provider must include a reason why the consent was not granted.
//
// The response contains a redirect URL which the consent provider should redirect the user-agent to.
//
// The default consent provider is available via the Ory Managed Account Experience. To customize the consent provider, please
// head over to the OAuth 2.0 documentation.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: oAuth2RedirectTo
// default: errorOAuth2
func (h *Handler) rejectOAuth2ConsentRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
challenge := cmp.Or(
r.URL.Query().Get("consent_challenge"),
r.URL.Query().Get("challenge"),
)
if challenge == "" {
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'challenge' is not defined but should have been.`)))
return
}
var payload flow.RequestDeniedError
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
h.r.Writer().WriteErrorCode(w, r, http.StatusBadRequest, errors.WithStack(err))
return
}
payload.Valid = true
payload.SetDefaults(flow.ConsentRequestDeniedErrorName)
f, err := flow.DecodeFromConsentChallenge(ctx, h.r, challenge)
if err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(err))
return
}
if err := f.HandleConsentError(&payload); err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(err))
return
}
ru, err := url.Parse(f.RequestURL)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
verifier, err := f.ToConsentVerifier(ctx, h.r)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
events.Trace(ctx, events.ConsentRejected, events.WithClientID(f.Client.GetID()), events.WithSubject(f.Subject))
h.r.Writer().Write(w, r, &flow.OAuth2RedirectTo{
RedirectTo: urlx.SetQuery(ru, url.Values{"consent_verifier": {verifier}}).String(),
})
}
// Accept OAuth 2.0 Logout Request
//
// swagger:parameters acceptOAuth2LogoutRequest
type _ struct {
// OAuth 2.0 Logout Request Challenge
//
// in: query
// required: true
Challenge string `json:"logout_challenge"`
}
// swagger:route PUT /admin/oauth2/auth/requests/logout/accept oAuth2 acceptOAuth2LogoutRequest
//
// # Accept OAuth 2.0 Session Logout Request
//
// When a user or an application requests Ory OAuth 2.0 to remove the session state of a subject, this endpoint is used to confirm that logout request.
//
// The response contains a redirect URL which the consent provider should redirect the user-agent to.
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: oAuth2RedirectTo
// default: errorOAuth2
func (h *Handler) acceptOAuth2LogoutRequest(w http.ResponseWriter, r *http.Request) {
challenge := cmp.Or(
r.URL.Query().Get("logout_challenge"),
r.URL.Query().Get("challenge"),
)
c, err := h.r.LogoutManager().AcceptLogoutRequest(r.Context(), challenge)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
h.r.Writer().Write(w, r, &flow.OAuth2RedirectTo{
RedirectTo: urlx.SetQuery(urlx.AppendPaths(h.r.Config().PublicURL(r.Context()), "/oauth2/sessions/logout"), url.Values{"logout_verifier": {c.Verifier}}).String(),
})
}
// Reject OAuth 2.0 Logout Request
//
// swagger:parameters rejectOAuth2LogoutRequest
type _ struct {
// in: query
// required: true
Challenge string `json:"logout_challenge"`
}
// swagger:route PUT /admin/oauth2/auth/requests/logout/reject oAuth2 rejectOAuth2LogoutRequest
//
// # Reject OAuth 2.0 Session Logout Request
//
// When a user or an application requests Ory OAuth 2.0 to remove the session state of a subject, this endpoint is used to deny that logout request.
// No HTTP request body is required.
//
// The response is empty as the logout provider has to chose what action to perform next.
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 204: emptyResponse
// default: errorOAuth2
func (h *Handler) rejectOAuth2LogoutRequest(w http.ResponseWriter, r *http.Request) {
challenge := cmp.Or(
r.URL.Query().Get("logout_challenge"),
r.URL.Query().Get("challenge"),
)
if err := h.r.LogoutManager().RejectLogoutRequest(r.Context(), challenge); err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
// Get OAuth 2.0 Logout Request
//
// swagger:parameters getOAuth2LogoutRequest
type _ struct {
// in: query
// required: true
Challenge string `json:"logout_challenge"`
}
// swagger:route GET /admin/oauth2/auth/requests/logout oAuth2 getOAuth2LogoutRequest
//
// # Get OAuth 2.0 Session Logout Request
//
// Use this endpoint to fetch an Ory OAuth 2.0 logout request.
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: oAuth2LogoutRequest
// 410: oAuth2RedirectTo
// default: errorOAuth2
func (h *Handler) getOAuth2LogoutRequest(w http.ResponseWriter, r *http.Request) {
challenge := cmp.Or(
r.URL.Query().Get("logout_challenge"),
r.URL.Query().Get("challenge"),
)
request, err := h.r.LogoutManager().GetLogoutRequest(r.Context(), challenge)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
// We do not want to share the secret so remove it.
if request.Client != nil {
request.Client.Secret = ""
}
if request.WasHandled {
h.r.Writer().WriteCode(w, r, http.StatusGone, &flow.OAuth2RedirectTo{
RedirectTo: request.RequestURL,
})
return
}
h.r.Writer().Write(w, r, request)
}
// Verify OAuth 2.0 User Code Request
//
// swagger:parameters acceptUserCodeRequest
type _ struct {
// in: query
// required: true
Challenge string `json:"device_challenge"`
// in: body
Body flow.AcceptDeviceUserCodeRequest
}
// swagger:route PUT /admin/oauth2/auth/requests/device/accept oAuth2 acceptUserCodeRequest
//
// # Accepts a device grant user_code request
//
// Accepts a device grant user_code request
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: oAuth2RedirectTo
// default: errorOAuth2
func (h *Handler) acceptUserCodeRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
challenge := r.URL.Query().Get("device_challenge")
if challenge == "" {
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'device_challenge' is not defined but should have been.`)))
return
}
var reqBody flow.AcceptDeviceUserCodeRequest
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrInvalidRequest.WithWrap(err).WithHintf("Unable to decode request body: %s", err.Error())))
return
}
if reqBody.UserCode == "" {
h.r.Writer().WriteError(w, r, errors.WithStack(fosite.ErrInvalidRequest.WithHint("Field 'user_code' must not be empty.")))
return
}
f, err := flow.DecodeFromDeviceChallenge(ctx, h.r, challenge)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
userCodeSignature, err := h.r.UserCodeStrategy().UserCodeSignature(r.Context(), reqBody.UserCode)
if err != nil {
h.r.Writer().WriteError(w, r, fosite.ErrServerError.WithWrap(err).WithHint(`The 'user_code' signature could not be computed.`))
return
}
userCodeRequest, err := h.r.OAuth2Storage().GetUserCodeSession(r.Context(), userCodeSignature, nil)
if err != nil {
h.r.Writer().WriteError(w, r, fosite.ErrInvalidRequest.WithWrap(err).WithHint(`The 'user_code' session could not be found or has expired or is otherwise malformed.`))
return
}
if err := h.r.UserCodeStrategy().ValidateUserCode(ctx, userCodeRequest, reqBody.UserCode); err != nil {
h.r.Writer().WriteError(w, r, fosite.ErrInvalidRequest.WithWrap(err).WithHint(`The 'user_code' session could not be found or has expired or is otherwise malformed.`))
return
}
p := flow.HandledDeviceUserAuthRequest{
Client: userCodeRequest.GetClient().(*client.Client),
DeviceCodeRequestID: userCodeRequest.GetID(),
RequestedScope: []string(userCodeRequest.GetRequestedScopes()),
RequestedAudience: []string(userCodeRequest.GetRequestedAudience()),
}
// Append the client_id to the original RequestURL, as it is needed for the login flow
reqURL, err := url.Parse(f.RequestURL)
if err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(err))
return
}
if reqURL.Query().Get("client_id") == "" {
q := reqURL.Query()
q.Add("client_id", userCodeRequest.GetClient().GetID())
reqURL.RawQuery = q.Encode()
}
f.RequestURL = reqURL.String()
if err := f.HandleDeviceUserAuthRequest(&p); err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
ru, err := url.Parse(f.RequestURL)
if err != nil {
h.r.Writer().WriteError(w, r, fosite.ErrInvalidRequest.WithWrap(err).WithHint(`Unable to parse the request_url.`))
return
}
verifier, err := f.ToDeviceVerifier(ctx, h.r)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
events.Trace(ctx, events.DeviceUserCodeAccepted, events.WithClientID(userCodeRequest.GetClient().GetID()))
h.r.Writer().Write(w, r, &flow.OAuth2RedirectTo{
RedirectTo: urlx.SetQuery(ru, url.Values{"device_verifier": {verifier}, "client_id": {userCodeRequest.GetClient().GetID()}}).String(),
})
}