-
-
Notifications
You must be signed in to change notification settings - Fork 367
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
Device grant flow (migrate to master) #701
base: master
Are you sure you want to change the base?
Changes from 52 commits
ce02ff3
9976360
ae0144c
60e109b
7b5a9ba
b3e10dd
e216bb5
0ff2699
3665fc6
d710a40
eefdf61
57fb5ba
74ef995
873f022
17b8665
375d89e
dbd7860
8585c75
4f7eb2b
eea03e0
341d411
0f0668b
a9aaaba
fef42ec
b085d07
3106872
9a30db2
7bbeb02
289f9a2
142b7ff
6f66f32
5722afa
a243b93
46cdb1e
3483d6e
86c4f95
e23a26a
a99ed89
88abd8d
1b29706
3313b71
e99e2bc
9279963
e60949d
ad37f5d
ea146be
9fcb161
1b306bb
98709f3
709a443
8a2cf5c
0395b8e
d2316f4
0e6f5fa
b1fbd36
0847036
5b41e88
5f9b0b1
bc70138
c4b608a
6cded09
7ad3851
10419a9
5bc8783
e8b1654
193f7d7
eee739a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// Copyright © 2023 Ory Corp | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package compose | ||
|
||
import ( | ||
"github.com/ory/fosite" | ||
"github.com/ory/fosite/handler/oauth2" | ||
"github.com/ory/fosite/handler/rfc8628" | ||
) | ||
|
||
// RFC8628DeviceFactory creates an OAuth2 device code grant ("Device Authorization Grant") handler and registers | ||
// an user code, device code, access token and a refresh token validator. | ||
func RFC8628DeviceFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { | ||
return &rfc8628.DeviceAuthHandler{ | ||
Strategy: strategy.(rfc8628.RFC8628CodeStrategy), | ||
Storage: storage.(rfc8628.RFC8628CodeStorage), | ||
Config: config, | ||
} | ||
} | ||
|
||
// OAuth2DeviceCodeFactory creates an OAuth2 device authorization grant ("device authorization flow") handler and registers | ||
// an access token, refresh token and authorize code validator. | ||
func RFC8628DeviceAuthorizationTokenFactory(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { | ||
return &rfc8628.DeviceCodeTokenEndpointHandler{ | ||
GenericCodeTokenEndpointHandler: oauth2.GenericCodeTokenEndpointHandler{ | ||
CodeTokenEndpointHandler: &rfc8628.DeviceAuthorizeHandler{ | ||
DeviceStrategy: strategy.(rfc8628.DeviceCodeStrategy), | ||
DeviceStorage: storage.(rfc8628.DeviceCodeStorage), | ||
}, | ||
AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy), | ||
RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy), | ||
CoreStorage: storage.(oauth2.CoreStorage), | ||
TokenRevocationStorage: storage.(oauth2.TokenRevocationStorage), | ||
Config: config, | ||
}, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,6 +62,7 @@ var ( | |
_ RevocationHandlersProvider = (*Config)(nil) | ||
_ PushedAuthorizeRequestHandlersProvider = (*Config)(nil) | ||
_ PushedAuthorizeRequestConfigProvider = (*Config)(nil) | ||
_ DeviceEndpointHandlersProvider = (*Config)(nil) | ||
) | ||
|
||
type Config struct { | ||
|
@@ -75,6 +76,18 @@ type Config struct { | |
// AuthorizeCodeLifespan sets how long an authorize code is going to be valid. Defaults to fifteen minutes. | ||
AuthorizeCodeLifespan time.Duration | ||
|
||
// Sets how long a device user/device code pair is valid for | ||
DeviceAndUserCodeLifespan time.Duration | ||
|
||
// DeviceAuthTokenPollingInterval sets the interval that clients should check for device code grants | ||
DeviceAuthTokenPollingInterval time.Duration | ||
|
||
// DeviceVerificationURL is the URL of the device verification endpoint, this is is included with the device code request responses | ||
DeviceVerificationURL string | ||
|
||
// DeviceDoneURL is the URL of the user is redirected to once the verification is completed | ||
DeviceDoneURL string | ||
|
||
// IDTokenLifespan sets the default id token lifetime. Defaults to one hour. | ||
IDTokenLifespan time.Duration | ||
|
||
|
@@ -194,6 +207,12 @@ type Config struct { | |
// PushedAuthorizeEndpointHandlers is a list of handlers that are called before the PAR endpoint is served. | ||
PushedAuthorizeEndpointHandlers PushedAuthorizeEndpointHandlers | ||
|
||
// DeviceEndpointHandlers is a list of handlers that are called before the device endpoint is served. | ||
DeviceEndpointHandlers DeviceEndpointHandlers | ||
|
||
// DeviceAuthorizeEndpointHandlers is a list of handlers that are called before the device authorize endpoint is served. | ||
DeviceAuthorizeEndpointHandlers DeviceAuthorizeEndpointHandlers | ||
|
||
// GlobalSecret is the global secret used to sign and verify signatures. | ||
GlobalSecret []byte | ||
|
||
|
@@ -242,6 +261,14 @@ func (c *Config) GetTokenIntrospectionHandlers(ctx context.Context) TokenIntrosp | |
return c.TokenIntrospectionHandlers | ||
} | ||
|
||
func (c *Config) GetDeviceEndpointHandlers(ctx context.Context) DeviceEndpointHandlers { | ||
return c.DeviceEndpointHandlers | ||
} | ||
|
||
func (c *Config) GetDeviceAuthorizeEndpointHandlers(ctx context.Context) DeviceAuthorizeEndpointHandlers { | ||
return c.DeviceAuthorizeEndpointHandlers | ||
} | ||
|
||
func (c *Config) GetRevocationHandlers(ctx context.Context) RevocationHandlers { | ||
return c.RevocationHandlers | ||
} | ||
|
@@ -360,6 +387,13 @@ func (c *Config) GetAuthorizeCodeLifespan(_ context.Context) time.Duration { | |
return c.AuthorizeCodeLifespan | ||
} | ||
|
||
func (c *Config) GetDeviceAndUserCodeLifespan(_ context.Context) time.Duration { | ||
if c.AuthorizeCodeLifespan == 0 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't it be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch :) |
||
return time.Minute * 10 | ||
} | ||
return c.DeviceAndUserCodeLifespan | ||
} | ||
|
||
// GeIDTokenLifespan returns how long an id token should be valid. Defaults to one hour. | ||
func (c *Config) GetIDTokenLifespan(_ context.Context) time.Duration { | ||
if c.IDTokenLifespan == 0 { | ||
|
@@ -488,3 +522,18 @@ func (c *Config) GetPushedAuthorizeContextLifespan(ctx context.Context) time.Dur | |
func (c *Config) EnforcePushedAuthorize(ctx context.Context) bool { | ||
return c.IsPushedAuthorizeEnforced | ||
} | ||
|
||
func (c *Config) GetDeviceDone(ctx context.Context) string { | ||
return c.DeviceDoneURL | ||
} | ||
|
||
func (c *Config) GetDeviceVerificationURL(ctx context.Context) string { | ||
return c.DeviceVerificationURL | ||
} | ||
|
||
func (c *Config) GetDeviceAuthTokenPollingInterval(ctx context.Context) time.Duration { | ||
if c.DeviceAuthTokenPollingInterval == 0 { | ||
return time.Second * 10 | ||
hperl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
return c.DeviceAuthTokenPollingInterval | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// Copyright © 2023 Ory Corp | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package fosite | ||
|
||
// DeviceAuthorizeRequest is an implementation of DeviceAuthorizeRequester | ||
type DeviceAuthorizeRequest struct { | ||
signature string | ||
Request | ||
} | ||
|
||
func (d *DeviceAuthorizeRequest) GetDeviceCodeSignature() string { | ||
return d.signature | ||
} | ||
|
||
func (d *DeviceAuthorizeRequest) SetDeviceCodeSignature(signature string) { | ||
d.signature = signature | ||
} | ||
|
||
func NewDeviceAuthorizeRequest() *DeviceAuthorizeRequest { | ||
return &DeviceAuthorizeRequest{ | ||
Request: *NewRequest(), | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,37 @@ | ||||||
// Copyright © 2023 Ory Corp | ||||||
// SPDX-License-Identifier: Apache-2.0 | ||||||
|
||||||
package fosite | ||||||
|
||||||
import ( | ||||||
"context" | ||||||
"net/http" | ||||||
|
||||||
"github.com/ory/fosite/i18n" | ||||||
"github.com/ory/x/errorsx" | ||||||
) | ||||||
|
||||||
func (f *Fosite) NewDeviceAuthorizeRequest(ctx context.Context, r *http.Request) (DeviceAuthorizeRequester, error) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the URL the user calls, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, all the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is a nice mnemonic! How about renaming it to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've renamed all the files/objects from Done ✅ |
||||||
request := NewDeviceAuthorizeRequest() | ||||||
request.Lang = i18n.GetLangFromRequest(f.Config.GetMessageCatalog(ctx), r) | ||||||
|
||||||
if err := r.ParseForm(); err != nil { | ||||||
return nil, errorsx.WithStack(ErrInvalidRequest.WithHint("Unable to parse HTTP body, make sure to send a properly formatted form request body.").WithWrap(err).WithDebug(err.Error())) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hydra registers this as GET, so the reference to the request body is wrong, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It actually parse the query string into the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought that for authorization code, this was because of PAR (https://datatracker.ietf.org/doc/html/rfc9126)? So that endpoint actually supports POST. Right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No it only support GET. Do you want me to change to way I parse the query params? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've refactored the way I'm querying the QueryParams. I now use |
||||||
} | ||||||
request.Form = r.Form | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this be
Suggested change
or do we expect URL query parameters as well? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't remember clearly, but I will try to look a bit deeper once I hook the new refactoring with Ory/Hydra There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ping :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I've fixed this one also, It should be good |
||||||
|
||||||
verifier := request.GetRequestForm().Get("device_verifier") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi, may I know what There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't remember what this code is for ? Maybe @BuzzBumbleBee knows ? BTW, I've wrote some sample clients to test it here:
hperl marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
if verifier != "" { | ||||||
client, err := f.Store.GetClient(ctx, request.GetRequestForm().Get("client_id")) | ||||||
hperl marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
if err != nil { | ||||||
return nil, errorsx.WithStack(ErrInvalidClient.WithHint("The requested OAuth 2.0 Client does not exist.").WithWrap(err).WithDebug(err.Error())) | ||||||
} | ||||||
request.Client = client | ||||||
|
||||||
if !client.GetGrantTypes().Has(string(GrantTypeDeviceCode)) { | ||||||
return nil, errorsx.WithStack(ErrInvalidGrant.WithHint("The requested OAuth 2.0 Client does not have the 'urn:ietf:params:oauth:grant-type:device_code' grant.")) | ||||||
} | ||||||
} | ||||||
|
||||||
return request, nil | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this always the same URL or would it differ on a per client basis?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's configured by the 'client'
This URL is used once the user has validated his login on his computer to display that he logged-in successfully and he can (physically) go back to his device.
It will be usally implemented by the ui-side of hydra but can bu actually any website.