Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: oauth 2.0 device authorization grant #1

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4,769 changes: 4,769 additions & 0 deletions 3252.patch

Large diffs are not rendered by default.

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
}
90 changes: 90 additions & 0 deletions consent/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
LoginPath = "/oauth2/auth/requests/login"
ConsentPath = "/oauth2/auth/requests/consent"
LogoutPath = "/oauth2/auth/requests/logout"
DevicePath = "/oauth2/auth/requests/device"
SessionsPath = "/oauth2/auth/sessions"
)

Expand Down Expand Up @@ -63,6 +64,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+"/verify", h.verifyUserCodeRequest)
}

// Revoke OAuth 2.0 Consent Session Parameters
Expand Down Expand Up @@ -936,3 +939,90 @@ 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 DeviceGrantVerifyUserCodeRequest
}

// swagger:route PUT /admin/oauth2/auth/requests/device/verify oAuth2 verifyUserCodeRequest
//
// # Verifies a device grant request
//
// Verifies a device grant request
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: oAuth2RedirectTo
// default: errorOAuth2
func (h *Handler) verifyUserCodeRequest(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
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 DeviceGrantVerifyUserCodeRequest
d := json.NewDecoder(r.Body)
d.DisallowUnknownFields()
if err := d.Decode(&p); err != nil {
h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithWrap(err).WithHintf("Unable to decode body because: %s", 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
}

clientId := userCodeRequest.GetClient().GetID()
// UserCode & DeviceCode Request shares the same RequestId as it's the same request;
deviceRequestId := userCodeRequest.GetID()
requestedScopes := userCodeRequest.GetRequestedScopes()
requestedAudience := 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
}

// req.GetID() is actually the DeviceCodeSignature
grantRequest, err := h.r.ConsentManager().AcceptDeviceGrantRequest(r.Context(), challenge, deviceRequestId, clientId, requestedScopes, requestedAudience)
if err != nil {
h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithHint(`Could not accept device grant request`)))
return
}

h.r.Writer().Write(w, r, &OAuth2RedirectTo{
RedirectTo: urlx.SetQuery(h.c.OAuth2DeviceAuthorisationURL(r.Context()), url.Values{"device_verifier": {grantRequest.Verifier}, "client_id": {clientId}}).String(),
})
}
2 changes: 1 addition & 1 deletion consent/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"github.com/ory/hydra/v2/client"
)

func sanitizeClientFromRequest(ar fosite.AuthorizeRequester) *client.Client {
func sanitizeClientFromRequest(ar fosite.Requester) *client.Client {
return sanitizeClient(ar.GetClient().(*client.Client))
}

Expand Down
6 changes: 6 additions & 0 deletions consent/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/gofrs/uuid"

"github.com/ory/fosite"
"github.com/ory/hydra/v2/client"
)

Expand Down Expand Up @@ -59,4 +60,9 @@ type Manager interface {
AcceptLogoutRequest(ctx context.Context, challenge string) (*LogoutRequest, error)
RejectLogoutRequest(ctx context.Context, challenge string) error
VerifyAndInvalidateLogoutRequest(ctx context.Context, verifier string) (*LogoutRequest, error)

CreateDeviceGrantRequest(ctx context.Context, req *DeviceGrantRequest) error
GetDeviceGrantRequestByVerifier(ctx context.Context, verifier string) (*DeviceGrantRequest, error)
AcceptDeviceGrantRequest(ctx context.Context, challenge string, device_code_signature string, clientId string, requested_scopes fosite.Arguments, requested_aud fosite.Arguments) (*DeviceGrantRequest, error)
VerifyAndInvalidateDeviceGrantRequest(ctx context.Context, verifier string) (*DeviceGrantRequest, error)
}
1 change: 1 addition & 0 deletions consent/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var _ Strategy = new(DefaultStrategy)

type Strategy interface {
HandleOAuth2AuthorizationRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, req fosite.AuthorizeRequester) (*AcceptOAuth2ConsentRequest, error)
HandleOAuth2DeviceAuthorizationRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, req fosite.DeviceAuthorizeRequester) (*AcceptOAuth2ConsentRequest, error)
HandleOpenIDConnectLogout(ctx context.Context, w http.ResponseWriter, r *http.Request) (*LogoutResult, error)
HandleHeadlessLogout(ctx context.Context, w http.ResponseWriter, r *http.Request, sid string) error
ObfuscateSubjectIdentifier(ctx context.Context, cl fosite.Client, subject, forcedIdentifier string) (string, error)
Expand Down
Loading
Loading