Skip to content

Commit

Permalink
feat: add consent handler for accepting a user_code
Browse files Browse the repository at this point in the history
  • Loading branch information
nsklikas committed Mar 1, 2024
1 parent 22f7151 commit 1d440e4
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 2 deletions.
2 changes: 2 additions & 0 deletions client/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/ory/fosite"
foauth2 "github.com/ory/fosite/handler/oauth2"
"github.com/ory/fosite/handler/rfc8628"
"github.com/ory/hydra/v2/jwk"
"github.com/ory/hydra/v2/x"
)
Expand All @@ -23,5 +24,6 @@ type Registry interface {
ClientHasher() fosite.Hasher
OpenIDJWTStrategy() jwk.JWTSigner
OAuth2HMACStrategy() *foauth2.HMACSHAStrategy
RFC8628HMACStrategy() rfc8628.RFC8628CodeStrategy
config.Provider
}
121 changes: 121 additions & 0 deletions consent/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/url"
"time"

"github.com/ory/hydra/v2/client"
"github.com/ory/hydra/v2/flow"
"github.com/ory/hydra/v2/oauth2/flowctx"
"github.com/ory/hydra/v2/x/events"
Expand All @@ -35,6 +36,7 @@ type Handler struct {

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"
Expand Down Expand Up @@ -66,6 +68,8 @@ func (h *Handler) SetRoutes(admin *httprouterx.RouterAdmin) {
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
Expand Down Expand Up @@ -1037,3 +1041,120 @@ func (h *Handler) getOAuth2LogoutRequest(w http.ResponseWriter, r *http.Request,

h.r.Writer().Write(w, r, request)
}

// Verify OAuth 2.0 User Code Request
//
// swagger:parameters verifyUserCodeRequest
type verifyUserCodeRequest struct {
// in: query
// required: true
Challenge string `json:"device_challenge"`

// in: body
Body flow.DeviceGrantVerifyUserCodeRequest
}

// swagger:route PUT /admin/oauth2/auth/requests/device/accept oAuth2 acceptUserCodeRequest
//
// # Accepts a device grant request
//
// Accepts a device grant 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, ps httprouter.Params) {
ctx := r.Context()

challenge := stringsx.Coalesce(
r.URL.Query().Get("device_challenge"),
r.URL.Query().Get("challenge"),
)
if challenge == "" {
h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'challenge' is not defined but should have been.`)))
return
}

var p flow.HandledDeviceUserAuthRequest
d := json.NewDecoder(r.Body)
d.DisallowUnknownFields()
if err := d.Decode(&p); err != nil {
h.r.Writer().WriteErrorCode(w, r, http.StatusBadRequest, errorsx.WithStack(err))
return
}

cr, err := h.r.ConsentManager().GetDeviceUserAuthRequest(ctx, challenge)
if err != nil {
h.r.Writer().WriteError(w, r, errorsx.WithStack(err))
return
}

p.ID = challenge
p.RequestedAt = cr.RequestedAt
p.HandledAt = sqlxx.NullTime(time.Now().UTC())

f, err := flowctx.Decode[flow.DeviceFlow](ctx, h.r.FlowCipher(), challenge, flowctx.AsDeviceChallenge)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}

if p.UserCode == "" {
h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Field 'user_code' must not be empty.")))
return
}

userCodeSignature, err := h.r.RFC8628HMACStrategy().UserCodeSignature(r.Context(), p.UserCode)
if err != nil {
h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithHint(`'user_code' signature could not be computed`)))
return
}
userCodeRequest, err := h.r.OAuth2Storage().GetUserCodeSession(r.Context(), userCodeSignature, &fosite.DefaultSession{})
if err != nil {
h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrNotFound.WithWrap(err).WithHint(`'user_code' session not found`)))
return
}

p.Client = userCodeRequest.GetClient().(*client.Client)
p.ClientID = userCodeRequest.GetClient().GetID()
p.RequestedScope = []string(userCodeRequest.GetRequestedScopes())
p.RequestedAudience = []string(userCodeRequest.GetRequestedAudience())

err = h.r.OAuth2Storage().InvalidateUserCodeSession(r.Context(), userCodeSignature)
if err != nil {
h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithHint(`Could not invalidate 'user_code'`)))
return
}

hr, err := h.r.ConsentManager().HandleDeviceUserAuthRequest(ctx, f, challenge, &p)
if err != nil {
h.r.Writer().WriteError(w, r, errorsx.WithStack(err))
return
}

ru, err := url.Parse(hr.RequestURL)
if err != nil {
h.r.Writer().WriteError(w, r, err)
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(),
})
}
4 changes: 2 additions & 2 deletions consent/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ type (
RejectLogoutRequest(ctx context.Context, challenge string) error
VerifyAndInvalidateLogoutRequest(ctx context.Context, verifier string) (*flow.LogoutRequest, error)

CreateDeviceUserAuthRequest(ctx context.Context, req *flow.DeviceUserAuthRequest) (*flow.Flow, error)
CreateDeviceUserAuthRequest(ctx context.Context, req *flow.DeviceUserAuthRequest) (*flow.DeviceFlow, error)
GetDeviceUserAuthRequest(ctx context.Context, challenge string) (*flow.DeviceUserAuthRequest, error)
HandleDeviceUserAuthRequest(ctx context.Context, f *flow.Flow, challenge string, r *flow.HandledDeviceUserAuthRequest) (*flow.DeviceUserAuthRequest, error)
HandleDeviceUserAuthRequest(ctx context.Context, f *flow.DeviceFlow, challenge string, r *flow.HandledDeviceUserAuthRequest) (*flow.DeviceUserAuthRequest, error)
VerifyAndInvalidateDeviceUserAuthRequest(ctx context.Context, verifier string) (*flow.HandledDeviceUserAuthRequest, error)
}

Expand Down
2 changes: 2 additions & 0 deletions x/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const (
// LoginRejected will be emitted when the login UI rejects a login request.
LoginRejected semconv.Event = "OAuth2LoginRejected"

DeviceUserCodeAccepted semconv.Event = "OAuth2DeviceUserCodeAccepted"

// ConsentAccepted will be emitted when the consent UI accepts a consent request.
ConsentAccepted semconv.Event = "OAuth2ConsentAccepted"

Expand Down
2 changes: 2 additions & 0 deletions x/fosite_storer.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/handler/pkce"
"github.com/ory/fosite/handler/rfc7523"
"github.com/ory/fosite/handler/rfc8628"
"github.com/ory/fosite/handler/verifiable"
)

Expand All @@ -21,6 +22,7 @@ type FositeStorer interface {
openid.OpenIDConnectRequestStorage
pkce.PKCERequestStorage
rfc7523.RFC7523KeyStorage
rfc8628.RFC8628CoreStorage
verifiable.NonceManager
oauth2.ResourceOwnerPasswordCredentialsGrantStorage

Expand Down

0 comments on commit 1d440e4

Please sign in to comment.