Skip to content

Commit

Permalink
Merge branch 'feat/device_auth_grant' of https://github.com/pedrule/h…
Browse files Browse the repository at this point in the history
…ydra into feat/device_auth_grant
  • Loading branch information
pedrule committed Sep 1, 2022
2 parents 295a372 + 1f0468f commit 0f1dbe7
Show file tree
Hide file tree
Showing 16 changed files with 192 additions and 10 deletions.
6 changes: 3 additions & 3 deletions consent/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const (
LoginPath = "/oauth2/auth/requests/login"
ConsentPath = "/oauth2/auth/requests/consent"
LogoutPath = "/oauth2/auth/requests/logout"
DevicePath = "/oauth2/auth/requests/device/usercode"
DevicePath = "/oauth2/auth/requests/device"
SessionsPath = "/oauth2/auth/sessions"
)

Expand Down Expand Up @@ -787,7 +787,7 @@ func (h *Handler) GetLogoutRequest(w http.ResponseWriter, r *http.Request, ps ht

func (h *Handler) VerifyDeviceAuthUserCode(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
type payload struct {
UserCode string `json:"userCode"`
UserCode string `json:"user_code"`
}

var body payload
Expand All @@ -797,7 +797,7 @@ func (h *Handler) VerifyDeviceAuthUserCode(w http.ResponseWriter, r *http.Reques
}

if body.UserCode == "" {
h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint(`Request body parameter 'userCode' is not defined but should have been.`)))
h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint(`Request body parameter 'user_code' is not defined but should have been.`)))
return
}

Expand Down
1 change: 1 addition & 0 deletions consent/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,7 @@ type DeviceLinkRequest struct {
WasHandled bool `json:"-" db:"was_handled,r"`

Verifier string `json:"-" db:"verifier"`
CSRF string `json:"-" db:"csrf"`

// AuthenticatedAt sqlxx.NullTime `json:"-" db:"authenticated_at"`
RequestedAt time.Time `json:"-" db:"requested_at"`
Expand Down
7 changes: 3 additions & 4 deletions driver/config/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const (
KeyOAuth2ClientRegistrationURL = "webfinger.oidc_discovery.client_registration_url"
KeyOAuth2TokenURL = "webfinger.oidc_discovery.token_url" // #nosec G101
KeyOAuth2AuthURL = "webfinger.oidc_discovery.auth_url"
KeyOAuth2DeviceAuthURL = "webfinger.oidc_discovery.device_auth_url"
KeyOAuth2DeviceAuthorisationURL = "webfinger.oidc_discovery.device_authorization_url"
KeyJWKSURL = "webfinger.oidc_discovery.jwks_url"
KeyOIDCDiscoverySupportedClaims = "webfinger.oidc_discovery.supported_claims"
KeyOIDCDiscoverySupportedScope = "webfinger.oidc_discovery.supported_scope"
Expand Down Expand Up @@ -413,11 +413,10 @@ func (p *Provider) JWKSURL() *url.URL {
return p.p.RequestURIF(KeyJWKSURL, urlx.AppendPaths(p.IssuerURL(), "/.well-known/jwks.json"))
}

func (p *Provider) OAuth2DeviceAuthURL() *url.URL {
return p.p.RequestURIF(KeyOAuth2DeviceAuthURL, urlx.AppendPaths(p.PublicURL(), "/oauth2/device/auth"))
func (p *Provider) OAuth2DeviceAuthorisationURL() *url.URL {
return p.p.RequestURIF(KeyOAuth2DeviceAuthorisationURL, urlx.AppendPaths(p.PublicURL(), "/oauth2/device/auth"))
}


func (p *Provider) TokenRefreshHookURL() *url.URL {
return p.p.URIF(KeyRefreshTokenHookURL, nil)
}
Expand Down
1 change: 1 addition & 0 deletions driver/config/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ func TestViperProviderValidates(t *testing.T) {
// webfinger
assert.Equal(t, []string{"hydra.openid.id-token"}, c.WellKnownKeys())
assert.Equal(t, urlx.ParseOrPanic("https://example.com"), c.OAuth2ClientRegistrationURL())
assert.Equal(t, urlx.ParseOrPanic("https://example.com/device_authorization"), c.OAuth2DeviceAuthorisationURL())
assert.Equal(t, urlx.ParseOrPanic("https://example.com/jwks.json"), c.JWKSURL())
assert.Equal(t, urlx.ParseOrPanic("https://example.com/auth"), c.OAuth2AuthURL())
assert.Equal(t, urlx.ParseOrPanic("https://example.com/token"), c.OAuth2TokenURL())
Expand Down
1 change: 1 addition & 0 deletions internal/.hydra.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ webfinger:
auth_url: https://example.com/auth
token_url: https://example.com/token
client_registration_url: https://example.com
device_authorization_url: https://example.com/device_authorization
supported_claims:
- username
supported_scope:
Expand Down
3 changes: 3 additions & 0 deletions internal/httpclient/models/well_known.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions oauth2/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ type WellKnown struct {
// example: https://playground.ory.sh/ory-hydra/admin/client
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`

// URL of the authorization server's device authorization endpoint
DeviceAuthorisationEndpoint string `json:"device_authorization_endpoint"`

// URL of the OP's OAuth 2.0 Token Endpoint
//
// required: true
Expand Down
10 changes: 7 additions & 3 deletions oauth2/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,14 +240,15 @@ func (h *Handler) WellKnownHandler(w http.ResponseWriter, r *http.Request) {
JWKsURI: h.c.JWKSURL().String(),
RevocationEndpoint: urlx.AppendPaths(h.c.IssuerURL(), RevocationPath).String(),
RegistrationEndpoint: h.c.OAuth2ClientRegistrationURL().String(),
DeviceAuthorisationEndpoint: h.c.OAuth2DeviceAuthorisationURL().String(),
SubjectTypes: h.c.SubjectTypesSupported(),
ResponseTypes: []string{"code", "code id_token", "id_token", "token id_token", "token", "token id_token code"},
ClaimsSupported: h.c.OIDCDiscoverySupportedClaims(),
ScopesSupported: h.c.OIDCDiscoverySupportedScope(),
UserinfoEndpoint: h.c.OIDCDiscoveryUserinfoEndpoint().String(),
TokenEndpointAuthMethodsSupported: []string{"client_secret_post", "client_secret_basic", "private_key_jwt", "none"},
IDTokenSigningAlgValuesSupported: []string{"RS256"},
GrantTypesSupported: []string{"authorization_code", "implicit", "client_credentials", "refresh_token"},
GrantTypesSupported: []string{"authorization_code", "implicit", "client_credentials", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"},
ResponseModesSupported: []string{"query", "fragment"},
UserinfoSigningAlgValuesSupported: []string{"none", "RS256"},
RequestParameterSupported: true,
Expand Down Expand Up @@ -849,8 +850,10 @@ func (h *Handler) DeviceAuthHandler(w http.ResponseWriter, r *http.Request, _ ht
}

// Generate the request URL
reqURL := h.c.OAuth2DeviceAuthURL()
reqURL.RawQuery = r.URL.RawQuery
reqURL := h.c.OAuth2DeviceAuthorisationURL()
reqURL.RawQuery = deviceAuthorizeRequest.GetRequestForm().Encode()

csrf := strings.Replace(uuid.New(), "-", "", -1)

// Create a Device Link Request, referencing the Device Code generated
linkRequest := &consent.DeviceLinkRequest{
Expand All @@ -860,6 +863,7 @@ func (h *Handler) DeviceAuthHandler(w http.ResponseWriter, r *http.Request, _ ht
RequestedScope: []string(deviceAuthorizeRequest.GetRequestedScopes()),
RequestedAudience: []string(deviceAuthorizeRequest.GetRequestedAudience()),
Client: deviceAuthorizeRequest.GetClient().(*client.Client),
CSRF: csrf,
RequestedAt: time.Now().Truncate(time.Second).UTC(),
OpenIDConnectContext: nil,
RequestURL: reqURL.String(),
Expand Down
1 change: 1 addition & 0 deletions oauth2/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ func TestHandlerWellKnown(t *testing.T) {
JWKsURI: conf.JWKSURL().String(),
RevocationEndpoint: urlx.AppendPaths(conf.IssuerURL(), oauth2.RevocationPath).String(),
RegistrationEndpoint: conf.OAuth2ClientRegistrationURL().String(),
DeviceAuthorisationEndpoint: conf.OAuth2DeviceAuthorisationURL().String(),
SubjectTypes: []string{"pairwise", "public"},
ResponseTypes: []string{"code", "code id_token", "id_token", "token id_token", "token", "token id_token code"},
ClaimsSupported: conf.OIDCDiscoverySupportedClaims(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS hydra_oauth2_device_link_request;
DROP TABLE IF EXISTS hydra_oauth2_device_code;
DROP TABLE IF EXISTS hydra_oauth2_user_code;
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
CREATE TABLE IF NOT EXISTS hydra_oauth2_device_link_request
(
challenge VARCHAR(40) NOT NULL PRIMARY KEY,
verifier VARCHAR(40) NOT NULL,
client_id VARCHAR(255) NOT NULL REFERENCES hydra_client (id) ON DELETE CASCADE,
request_url TEXT NOT NULL,
requested_scope TEXT NOT NULL,
device_code VARCHAR(255) NOT NULL,
csrf VARCHAR(40) NOT NULL,
requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
oidc_context TEXT NOT NULL,
login_challenge VARCHAR(40) NULL REFERENCES hydra_oauth2_authentication_request (challenge) ON DELETE SET NULL,
requested_at_audience TEXT NOT NULL DEFAULT '',
UNIQUE (challenge)
);

CREATE INDEX hydra_oauth2_device_link_request_client_id_idx ON hydra_oauth2_consent_request (client_id);
CREATE INDEX hydra_oauth2_device_link_request_subject_idx ON hydra_oauth2_consent_request (subject);
CREATE INDEX hydra_oauth2_device_link_request_login_session_id_idx ON hydra_oauth2_consent_request (login_session_id);
CREATE INDEX hydra_oauth2_device_link_request_login_challenge_idx ON hydra_oauth2_consent_request (login_challenge);

---
CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code
(
signature VARCHAR(255) NOT NULL PRIMARY KEY,
request_id VARCHAR(40) NOT NULL,
requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
client_id VARCHAR(255) NOT NULL REFERENCES hydra_client (id) ON DELETE CASCADE,
scope TEXT NOT NULL,
granted_scope TEXT NOT NULL,
form_data TEXT NOT NULL,
session_data TEXT NOT NULL,
subject VARCHAR(255) NOT NULL DEFAULT '',
active INTEGER NOT NULL DEFAULT true,
requested_audience TEXT NULL DEFAULT '',
granted_audience TEXT NULL DEFAULT '',
challenge_id VARCHAR(40) NULL REFERENCES hydra_oauth2_consent_request (challenge) ON DELETE CASCADE,
UNIQUE (request_id)
);

CREATE INDEX hydra_oauth2_device_code_requested_at_idx ON hydra_oauth2_device_code (requested_at);
CREATE INDEX hydra_oauth2_device_code_client_id_idx ON hydra_oauth2_device_code (client_id);
CREATE INDEX hydra_oauth2_device_code_challenge_id_idx ON hydra_oauth2_device_code (challenge_id);
CREATE INDEX hydra_oauth2_device_code_client_id_subject_idx ON hydra_oauth2_device_code (client_id, subject);

---

CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code
(
signature VARCHAR(255) NOT NULL PRIMARY KEY,
request_id VARCHAR(40) NOT NULL,
requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
client_id VARCHAR(255) NOT NULL REFERENCES hydra_client (id) ON DELETE CASCADE,
scope TEXT NOT NULL,
granted_scope TEXT NOT NULL,
form_data TEXT NOT NULL,
session_data TEXT NOT NULL,
subject VARCHAR(255) NOT NULL DEFAULT '',
active INTEGER NOT NULL DEFAULT true,
requested_audience TEXT NULL DEFAULT '',
granted_audience TEXT NULL DEFAULT '',
challenge_id VARCHAR(40) NULL REFERENCES hydra_oauth2_device_link_request (challenge) ON DELETE CASCADE,
UNIQUE (request_id)
);

CREATE INDEX hydra_oauth2_user_code_requested_at_idx ON hydra_oauth2_user_code (requested_at);
CREATE INDEX hydra_oauth2_user_code_client_id_idx ON hydra_oauth2_user_code (client_id);
CREATE INDEX hydra_oauth2_user_code_challenge_id_idx ON hydra_oauth2_user_code (challenge_id);
CREATE INDEX hydra_oauth2_user_code_client_id_subject_idx ON hydra_oauth2_user_code (client_id, subject);
91 changes: 91 additions & 0 deletions spec/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,19 @@
"title": "HandledLoginRequest is the request payload used to accept a login request.",
"type": "object"
},
"verifyDeviceRequest": {
"properties": {
"user_code": {
"description": "The end-user verification code.",
"type": "string"
}
},
"required": [
"user_code"
],
"title": "The response payload sent when verifying a device user_code request.",
"type": "object"
},
"completedRequest": {
"properties": {
"redirect_to": {
Expand Down Expand Up @@ -1506,6 +1519,11 @@
"example": "https://playground.ory.sh/ory-hydra/admin/client",
"type": "string"
},
"device_authorization_endpoint": {
"description": "URL of the server's device authorization authorization endpoint",
"example": "https://playground.ory.sh/ory-hydra/public/oauth2/device/auth",
"type": "string"
},
"request_object_signing_alg_values_supported": {
"description": "JSON array containing a list of the JWS signing algorithms (alg values) supported by the OP for Request Objects,\nwhich are described in Section 6.1 of OpenID Connect Core 1.0 [OpenID.Core]. These algorithms are used both when\nthe Request Object is passed by value (using the request parameter) and when it is passed by reference\n(using the request_uri parameter).",
"items": {
Expand Down Expand Up @@ -3037,6 +3055,79 @@
]
}
},
"/oauth2/auth/requests/device/verify": {
"post": {
"description": "Verify a user_code to initiate a device auth flow",
"operationId": "verifyDeviceRequest",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/verifyDeviceRequest"
}
}
},
"required": true,
"x-originalParamName": "Body"
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/completedRequest"
}
}
},
"description": "completedRequest"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/jsonError"
}
}
},
"description": "jsonError"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/jsonError"
}
}
},
"description": "jsonError"
},
"410": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/requestWasHandledResponse"
}
}
},
"description": "requestWasHandledResponse"
},
"500": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/jsonError"
}
}
},
"description": "jsonError"
}
},
"summary": "POST a Device Verify Request",
"tags": [
"admin"
]
}
},
"/oauth2/auth/requests/logout": {
"get": {
"description": "Use this endpoint to fetch a logout request.",
Expand Down
6 changes: 6 additions & 0 deletions spec/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,12 @@
"format": "uri",
"examples": ["https://my-service.com/clients"]
},
"device_authorization_url": {
"description": "Sets the OpenID Connect Dynamic Client Registration Endpoint",
"type": "string",
"format": "uri",
"examples": ["https://my-service.com/device_authorization"]
},
"supported_claims": {
"type": "array",
"description": "A list of supported claims to be broadcasted. Claim \"sub\" is always included.",
Expand Down

0 comments on commit 0f1dbe7

Please sign in to comment.