-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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: implement RFC 8628 #3912
base: master
Are you sure you want to change the base?
feat: implement RFC 8628 #3912
Conversation
baaef03
to
469c5e1
Compare
469c5e1
to
7970287
Compare
7970287
to
8790af4
Compare
8790af4
to
14cf7cd
Compare
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.
Very nice! I'll push some minor changes from my side and left a couple of comments.
I primarily cleaned up error handling to make it standards compliant
require.Contains(t, result.RedirectTo, "device_verifier") | ||
} | ||
|
||
func TestAcceptDuplicateDeviceRequest(t *testing.T) { |
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 testing if we can accept the same device challenge twice? If so, we need two tests:
- Test if we can accept the same device challenge twice and that the device verifier is valid
- Test if we can accept the device challenge once, use it, then try again, and fail
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.
I think that 1 is handled by this test
About 2, a device challenge can be used multiple times. The reason for that is that only the consent challenge ID is unique in the database. Multiple flows with the same device challenge can be persisted in the database, as long as they have a different consent challenge ID and are associated with a different user_code. I think that the same is true for the login_challenge
.
Do you think that the device_challenge
should be unique?
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.
Yes, the device_challenge must be unique, otherwise we have a potential replay attack vector
validateResponse: func(resp *http.Response) { | ||
require.EqualValues(t, http.StatusBadRequest, resp.StatusCode) | ||
}, | ||
}, |
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.
Maybe you can add a test case here where a device auth code is already used
if err != nil { | ||
h.r.Writer().WriteError(w, r, err) | ||
return | ||
} |
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.
Change this to UPDATE ... SET accepted WHER nid=... AND request_id = ?
?
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.
Oh, I'm sorry, it appears that there are quite a lot of values being copied around here
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.
Do you know which values need to be updated here? Is it all of them? Or just some?
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.
I'm not sure to which line you are referring to.
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.
Damn, looks like my comment was saved on the wrong file. I'm resolving this
oauth2/handler.go
Outdated
"prompt", | ||
"acr_values", | ||
"id_token_hint", | ||
"nonce", |
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.
This looks copy pasted - could you make this list a variable for re-use?
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 taken from https://github.com/ory/fosite/blob/master/handler/openid/flow_explicit_auth.go#L29. I will make it a variable here, should I add a comment with a link to fosite?
persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql
Outdated
Show resolved
Hide resolved
0a4fa13
to
2e8cf7c
Compare
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.
A few more comments, but I think we're getting quite close. Preventing replay attacks definitely needs to be addressed before merge
persistence/sql/migrations/20241609000001000000_device_flow.cockroach.up.sql
Outdated
Show resolved
Hide resolved
@@ -0,0 +1,45 @@ | |||
CREATE TABLE IF NOT EXISTS hydra_oauth2_device_auth_codes |
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.
You can remove all files in this directory
9e2e241
to
d338178
Compare
Let me know when this is good for another review |
@aeneasr sorry for the late response, everything should be good for another review now. |
73b187d
to
9c81f0f
Compare
I tried performing the flow using the included CLI tooling, but it failed with this:
|
Or does device auth always use post form submit? From looking at the go oauth2 library it appears to be the case? |
Would it be possible to get ory/hydra-login-consent-node#123 ready to merge so that the quickstart can be used for the device auth code flow? :) |
I've fixed up the node PR: ory/hydra-login-consent-node#161 And made the CLI changes.
Otherwise it seems like everything is working! I was able to perform the device auth code flow end to end :) ps: the node pr includes a pre-release of the SDK that has the device auth code flow methods included |
One last thing we need is some docs. I was thinking a dedicated page for this grant type:
@christiannwamba anything I missed? |
It is a little bit weird. I think there was a discussion around this in the original PR as well, but I can't fine it right now. RFC section 3.1 says:
Reading this, I understand that the client must authenticate when making the device authz request. But the golang oauth2 lib does not provide client authentication in that request. There is an open issue about it golang/oauth2#685, that hasn't been addressed for quite some time. My guess is that most people will use public clients for the device flow and that's why they don't care. I think that some of the public providers don't require client authn for this request either, so I was conflicted about whether I should add it or not. We could make the CLI work by patching the http client used to add the basic auth header, I agree that this would provide better UX. I will give it a shot. |
I think that covers it. Should go under the guides section. |
It‘s fine - the device flow should actually only use public clients as end devices can‘t keep secrets. I confirmed that this works. |
What I did:
What I found and what I believe needs to be addressed:
|
Looks like I messed up the tests - very sorry about that! I‘m currently PTO but trying to squeeze it in :) |
Nw, I will try to fix them |
Not implementing the reject endpoint was part of the plan. I thought that it could be added at a later time if needed. The
It should be tested by https://github.com/nsklikas/hydra/blob/canonical-master/oauth2/oauth2_device_code_test.go#L624
Good catch, removed
I thought that the tables are cleaned asynchronously by hydra janitor. Am I missing something? The janitor work was supposed to be handled on a different PR.
That's correct, will update the schema
It shouldn't be possible to do that, if you were able to get a
AFAICT the device flow is not supported by the
Good point, I will redact it.
Great point, I will create
Right, will add it |
Ideally we would not need the janitor (although the janitor should still try to clean the table) but instead the row is deleted when the token exchange happens:
Without DB clean up it won't be possible to cut a release as it will cause very large DB growth very quickly |
Regarding rejection - I'm not sure how others handle it. Is it possible to reject the device screen in other implementations? It probably should at least programmatically be possible - for example when the device verification endpoint decides "uhhhm nope, this should not be handled by that client". But might be a bit of a fabricated use case. I would be ok not having a reject endpoint, I think. |
No problem with adding it, but can you point me to where this happens for the other oauth2 tokens as I can't really find it. To be clear what you suggest is that instead of marking the token as used after it is accepted/rejected (soft delete), we should instead delete the row (hard delete), right? Also keep in mind that this won't protect you from someone malicious who will spam the device_authorization endpoint. |
I think that most provider simply have a button for accepting the code, no button for rejecting it. The [RFC}(https://datatracker.ietf.org/doc/html/rfc8628#section-3.3) says:
In Hydra it is possible to implement this behavior with the consent API. I am not sure if the reject endpoint will actually ever be used or not. IMHO it is a nice-to-have, but not required for an MVP. |
3541e40
to
d64e1c3
Compare
Hm the spec seems to clearly indicate we need a rejection option. At this point I would like to clarify that my personal expectation is to land this feature/PR with all important aspects covered - I understand everyone has different expectations on what MVP means but for Ory Hydra it implies stability, docs, tests and spec compliance :) That‘s why this project is so popular! As I said before - we are super close and I think from the business logic aspect we have almost all bases covered :)Am 11.02.2025 um 10:34 schrieb Nikos Sklikas ***@***.***>:
Regarding rejection - I'm not sure how others handle it. Is it possible to reject the device screen in other implementations?
It probably should at least programmatically be possible - for example when the device verification endpoint decides "uhhhm nope, this should not be handled by that client". But might be a bit of a fabricated use case. I would be ok not having a reject endpoint, I think.
I think that most provider simply have a button for accepting the code, no button for rejecting it.
The [RFC}(https://datatracker.ietf.org/doc/html/rfc8628#section-3.3) says:
The authorization server prompts the end user to
identify the device authorization session by entering the "user_code"
provided by the client. The authorization server should then inform
the user about the action they are undertaking and ask them to
approve or deny the request. Once the user interaction is complete,
the server instructs the user to return to their device.
In Hydra it is possible to implement this behavior with the consent API. I am not sure if the reject endpoint will actually ever be used or not. IMHO it is a nice-to-have, but not required for an MVP.
—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you were mentioned.Message ID: ***@***.***>
|
It would get removed on token exchange - so after the consent flow is finished. You can search for DeleteOpenIDSession (or similar) in Fosite for this!Am 11.02.2025 um 10:28 schrieb Nikos Sklikas ***@***.***>:
Ideally we would not need the janitor (although the janitor should still try to clean the table) but instead the row is deleted when the token exchange happens:
* Add janitor clean up
* Clean up row after use
Without DB clean up it won't be possible to cut a release as it will cause very large DB growth very quickly
No problem with adding it, but can you point me to where this happens for the other oauth2 tokens as I can't really find it.
To be clear what you suggest is that instead of marking the token as used after it is accepted/rejected (soft delete), we should instead delete the row (hard delete), right?
Also keep in mind that this won't protect you from someone malicious who will spam the device_authorization endpoint.
—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you were mentioned.Message ID: ***@***.***>
|
But that is different, My question is why would you expect the database to grow that big with the device tokens and not with any of the other tokens? |
Agreed, what I meant is that IMO the current implementation is RFC compliant. AFAICT rejecting the |
Fair point - the current behavior is fine then in my view! |
The authorization code is kept around because we need it for re use detection. The oidc, pkce tables are transient tables which get cleaned up after the state is no longer required. For the user code, do we need re use detection as well? Meaning that re using the code would invalidate the previous issued tokens? If not, we can and probably should delete it once the token exchange is completed so that the code is freed from the unique constraint. In my understanding, the user/device code is also transient and only needed to perform token exchange. Is that correct? |
ps: we have customers with several TB of data in the individual tables so cleaning them up is definitely important :) |
Understood, yes they are transient. Given that retries are part of the flow, I think that re-use detection is not needed. |
@aeneasr all of your comments should be resolved now except for:
AFAICT ory/hydra-login-consent-node#161 needs to be merged, in order to implement the e2e tests. Is there anything that I should do on that end? I will try to open a PR for the docs soon. |
Just let me know if the 161 implementation is correct - then I‘ll merge that one! |
d64e1c3
to
ebfad8a
Compare
Pushed cypress tests for the device flow. I used ory/hydra-login-consent-node#161 to run them, everything seems to work fine. Since there is no success page in that implementation, I set the success page to something in the client app to make the test implementation easier, I hope that's fine. |
cd8162e
to
d55caa4
Compare
app.get("/oauth2/device", async (req, res) => { | ||
const client = { | ||
id: req.query.client_id, | ||
secret: req.query.client_secret, |
Check warning
Code scanning / CodeQL
Sensitive data read from GET request Medium test
Route handler
headers.set( | ||
"Authorization", | ||
"Basic " + | ||
Buffer.from(req.query.client_id + ":" + req.query.client_secret).toString( |
Check warning
Code scanning / CodeQL
Sensitive data read from GET request Medium test
Route handler
The tests should be fixed when the hydra-login-consent-node PR is merged. I also opened a PR for the docs. It will need some work, but most of the required information should be there. |
I've pushed the missing success screen, sorry about that! |
AFAICT a new release is needed for |
It uses master IIRC, so no release required |
AFAICT it uses the latest release on the npm store (see https://github.com/ory/hydra/blob/master/test/e2e/oauth2-client/package.json#L17) |
I see, I probably confused it with kratos CI - let’s see if this passes: https://github.com/ory/hydra-login-consent-node/releases/tag/v2.4.0-alpha.0 |
It failed, seems like it's missing a token or something |
This PR is a continuation of #3851. I have created it from my own personal repo and I have invited people from Ory to contribute so that we can speed up things. I think that most of the comments in the old PR were resolved, but I can copy them to this PR if we wish to keep the discussion history.
Implements the Device Authorization Grant to enable authentication for headless machines (see https://datatracker.ietf.org/doc/html/rfc8628)
Related issue(s)
Implements RFC 8628.
This PR is based on the work done on #3252, by @supercairos and @BuzzBumbleBee. That PR was based on an older version of Hydra and was missing some features/tests.
We have prepared a spec, that describes our design and implementation. We have tried to mimic the existing logic in Hydra and not make changes that would disrupt the existing workflows
Checklist
introduces a new feature.
contributing code guidelines.
vulnerability. If this pull request addresses a security vulnerability, I
confirm that I got the approval (please contact
security@ory.sh) from the maintainers to push
the changes.
works.
Further Comments
Notes:
memory
andpostgres
databases. The tests pass all of them.go.mod
.Testing
To test this you need to built the hydra image:
make docker
This will create an image with the name:
oryd/hydra:latest-sqlite
To run the flow you can use our UI, from https://github.com/canonical/identity-platform-login-ui/tree/hydra-device-test:
Create a client for Hydra:
docker exec -it identity-platform-login-ui-hydra-1 hydra create client --endpoint http://localhost:4445 --grant-type authorization_code,refresh_token,urn:ietf:params:oauth:grant-type:device_code --scope openid,offline_access,email,profile --token-endpoint-auth-method client_secret_post
Use that client to perform the device flow:
docker exec -it identity-platform-login-ui-hydra-1 hydra perform device-code --client-id <client-id> --client-secret <client-secret> -e http://localhost:4444 --scope openid,offline_access,email,profile
The user for logging in is:
test@example.com
test