Skip to content

Conversation

@nsklikas
Copy link
Contributor

@nsklikas nsklikas commented Dec 18, 2024

This patch introduces the OAuth 2.0 Device Authorization Grant to Ory Hydra. The OAuth 2.0 device authorization grant is designed for Internet-connected devices that either lack a browser to perform a user-agent-based authorization or are input constrained to the extent that requiring the user to input text in order to authenticate during the authorization flow is impractical. It enables OAuth clients on such devices (like smart TVs, media consoles, digital picture frames, and printers) to obtain user authorization to access protected resources by using a user agent on a separate device.

The OAuth 2.0 Device Authorization Grant may also become relevant for AI Agent authentication flows and is generally an amazing step and innovation for this project.

A very special thanks goes to @nsklikas from Canonical, @supercairos from shadow.tech and @BuzzBumbleBee.

For more details, please check out the documentation (ory/docs#2026)

To implement this feature, you will need to implement two additional screens in your login and consent application. A reference implementation can be found here.

Closes #3851
Closes #3252
Closes #3230
Closes #2416


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

  • I have read the contributing guidelines.
  • I have referenced an issue containing the design document if my change
    introduces a new feature.
  • I am following the
    contributing code guidelines.
  • I have read the security policy.
  • I confirm that this pull request does not address a security
    vulnerability. If this pull request addresses a security vulnerability, I
    confirm that I got the approval (please contact
    [email protected]) from the maintainers to push
    the changes.
  • I have added tests that prove my fix is effective or that my feature
    works.
  • I have added or changed the documentation.

Further Comments

Notes:

  • The current implementation has been manually tested only for memory and postgres databases. The tests pass all of them.
  • Fosite is installed from our fork to ease testing. Once the relevant PR in fosite is merged, we will update 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:

git clone [email protected]:canonical/identity-platform-login-ui.git -b hydra-device-test
cd identity-platform-login-ui/
# The image name is hard-coded in the docker-compose file
docker compose up --remove-orphans --force-recreate -d

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:

@nsklikas nsklikas mentioned this pull request Dec 18, 2024
7 tasks
@nsklikas nsklikas requested a review from a team as a code owner January 8, 2025 15:49
Copy link
Member

@aeneasr aeneasr left a 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

Copy link
Member

@aeneasr aeneasr left a 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

@aeneasr
Copy link
Member

aeneasr commented Jan 29, 2025

Let me know when this is good for another review

@nsklikas nsklikas requested a review from aeneasr January 29, 2025 10:08
@nsklikas
Copy link
Contributor Author

@aeneasr sorry for the late response, everything should be good for another review now.

@aeneasr
Copy link
Member

aeneasr commented Feb 8, 2025

I tried performing the flow using the included CLI tooling, but it failed with this:

go run . perform device-code --endpoint  http://127.0.0.1:4444 --client-id $code_client_id --client-secret $code_client_secret
Failed to perform the device authorization request
%!(EXTRA string=oauth2: cannot fetch token: 401 Unauthorized
Response: {"error":"invalid_client","error_description":"Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The OAuth 2.0 Client supports client authentication method 'client_secret_basic', but method 'client_secret_post' was requested. You must configure the OAuth 2.0 client's 'token_endpoint_auth_method' value to accept 'client_secret_post'."})exit status 1
  1. It appears that the error message is not properly formatted (thus the "extra" part)
  2. The client should per default use "client_secret_basic" like the other commands

@aeneasr
Copy link
Member

aeneasr commented Feb 8, 2025

Or does device auth always use post form submit? From looking at the go oauth2 library it appears to be the case?

@aeneasr
Copy link
Member

aeneasr commented Feb 8, 2025

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? :)

@aeneasr
Copy link
Member

aeneasr commented Feb 9, 2025

I've fixed up the node PR: ory/hydra-login-consent-node#161

And made the CLI changes.

The node PR is still missing the device auth complete screen which we should add I added the screen - please verify it's correctness

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

@aeneasr
Copy link
Member

aeneasr commented Feb 10, 2025

One last thing we need is some docs. I was thinking a dedicated page for this grant type:

  1. Brief introduction
  2. End to end example
  3. Config options
  4. Implementation of a custom ui for the verification and success screen

@christiannwamba anything I missed?

@nsklikas
Copy link
Contributor Author

Or does device auth always use post form submit? From looking at the go oauth2 library it appears to be the case?

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:

The client authentication requirements of Section 3.2.1 of [RFC6749]
apply to requests on this endpoint, which means that confidential
clients (those that have established client credentials) authenticate
in the same manner as when making requests to the token endpoint, and
public clients provide the "client_id" parameter to identify
themselves.

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.

@christiannwamba
Copy link

One last thing we need is some docs. I was thinking a dedicated page for this grant type:

  1. Brief introduction
  2. End to end example
  3. Config options
  4. Implementation of a custom ui for the verification and success screen

@christiannwamba anything I missed?

I think that covers it. Should go under the guides section.

@aeneasr
Copy link
Member

aeneasr commented Feb 10, 2025

Or does device auth always use post form submit? From looking at the go oauth2 library it appears to be the case?

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:

The client authentication requirements of Section 3.2.1 of [RFC6749]
apply to requests on this endpoint, which means that confidential
clients (those that have established client credentials) authenticate
in the same manner as when making requests to the token endpoint, and
public clients provide the "client_id" parameter to identify
themselves.

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.

It‘s fine - the device flow should actually only use public clients as end devices can‘t keep secrets. I confirmed that this works.

@aeneasr
Copy link
Member

aeneasr commented Feb 10, 2025

What I did:

  1. Clean up the SQL migrations - there were a couple of NULL values that should not be null and inconsistencies between the database variants.
  2. Change the config structure for device URLs.
  3. Add code docs with my understanding of the business logic and simplify/clean up a bit.
  4. Wrapped writes in performOAuth2DeviceVerificationFlow in a transaction to reduce latency.

What I found and what I believe needs to be addressed:

  1. It does not appear to be possible to reject a device user auth code. I also noticed that the field DeviceError is unused (probably because the functionality is missing).
  2. Please verify that device_authorization_grant_id_token_lifespan (and refresh, access) are effective with a test.
  3. DeviceUserCodeAcceptedAt is unused as far as I can tell. When in doubt, leave it out :)
  4. Clean of up device table is missing - we are not removing the row like we do with the oidc tables post-use and there is no clean up process. This means that in production you'll quickly run into DB growth issues. Ideally, the hydra_oauth2_device_auth_codes row gets removed upon use.
  5. The polling interval is purely informational and is part of the OAuth2 response. Is that understanding correct? If so, please document it as such in the config schema JSON.
  6. Looking at the code I was wondering if we can somehow create a sequence where in HandleOAuth2DeviceAuthorizationRequest we can bypass device/login/consent verifiation? I've tried to hack around this but in my view it is not possible - please confirm:
    1. You can not get a login challenge without device verification, and you can't get a login verifier without a login challenge
    2. The same applies to the consent challenge - without a login verifier you can't get a consent verifier
  7. Can you please add missing cypress e2e tests for a public client performing the flow?
  8. In the database, user_code is leaking into request_url in the flow table (and potentially other tables). This isn't great because the user code should really only be available to the user.
  9. It took me quite some time to understand what is happening with CreateLoginRequest which now accepts a flow as well. As far as I understand, this is because we now create the flow (sometimes) as part of the device flow which happens before login. I'm wondering if it wouldn't be simpler to just use the existing device flow and update the state, instead of calling CreateLoginRequest.
  10. While we have a fallback URL for device success (oauth2/fallbacks/device/done), a fallback URL for device verification is missing (oauth2/fallbacks/device).

@aeneasr
Copy link
Member

aeneasr commented Feb 10, 2025

Looks like I messed up the tests - very sorry about that! I‘m currently PTO but trying to squeeze it in :)

@nsklikas
Copy link
Contributor Author

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

@nsklikas
Copy link
Contributor Author

What I found and what I believe needs to be addressed:

1. [ ]  It does not appear to be possible to reject a device user auth code. I also noticed that the field `DeviceError` is unused (probably because the functionality is missing).

Not implementing the reject endpoint was part of the plan. I thought that it could be added at a later time if needed. The DeviceError should be used by the reject endpoint (once implemented), I will remove it for now since we are missing that functionality.

2. [ ]  Please verify that  `device_authorization_grant_id_token_lifespan` (and refresh, access) are effective with a test.

It should be tested by https://github.com/nsklikas/hydra/blob/canonical-master/oauth2/oauth2_device_code_test.go#L624

3. [ ]  `DeviceUserCodeAcceptedAt` is unused as far as I can tell. When in doubt, leave it out :)

Good catch, removed

4. [ ]  Clean of up device table is missing - we are not removing the row like we do with the oidc tables post-use and there is no clean up process. This means that in production you'll quickly run into DB growth issues. Ideally, the `hydra_oauth2_device_auth_codes` row gets removed upon use.

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.

5. [ ]  The polling interval is purely informational and is part of the OAuth2 response. Is that understanding correct? If so, please document it as such in the config schema JSON.

That's correct, will update the schema

6. [ ]  Looking at the code I was wondering if we can somehow create a sequence where in `HandleOAuth2DeviceAuthorizationRequest` we can bypass device/login/consent verifiation? I've tried to hack around this but in my view it is not possible - please confirm:
   
   1. You can not get a login challenge without device verification, and you can't get a login verifier without a login challenge
   2. The same applies to the consent challenge - without a login verifier you can't get a consent verifier

It shouldn't be possible to do that, if you were able to get a login_verifier/consent_challenge/consent_verifier then that would mean that you would be able to do that for the common auth_code flow as well, as we are relying on the same logic/methods. Going through the code, you should not be able to get a login_challenge if you don't have a valid device_verifier.

7. [ ]  Can you please add missing cypress e2e tests for a public client performing the flow?

AFAICT the device flow is not supported by the simple-oauth2 lib, which is used in the e2e tests. I will try to implement the calls on my own.

8. [ ]  In the database, `user_code` is leaking into `request_url` in the flow table (and potentially other tables). This isn't great because the user code should really only be available to the user.

Good point, I will redact it.

9. [ ]  It took me quite some time to understand what is happening with `CreateLoginRequest` which now accepts a flow as well. As far as I understand, this is because we now create the flow (sometimes) as part of the device flow which happens before login. I'm wondering if it wouldn't be simpler to just use the existing device flow and update the state, instead of calling `CreateLoginRequest`.

Great point, I will create CreateLoginRequestFromDeviceRequest to handle this case.

10. [ ]  While we have a fallback URL for device success (`oauth2/fallbacks/device/done`), a fallback URL for device verification is missing (`oauth2/fallbacks/device`).

Right, will add it

@nsklikas nsklikas force-pushed the canonical-master branch 2 times, most recently from 3277d14 to 1e55e05 Compare February 24, 2025 17:15
@aeneasr
Copy link
Member

aeneasr commented Feb 26, 2025

To test:

code_client=$(go run . create client \
    --endpoint http://127.0.0.1:4445 \
    --grant-type authorization_code,refresh_token,urn:ietf:params:oauth:grant-type:device_code \
    --response-type code,id_token \
    --token-endpoint-auth-method none \
    --format json \
    --scope openid --scope offline \
    --redirect-uri http://127.0.0.1:5555/callback)

code_client_id=$(echo $code_client | jq -r '.client_id')
code_client_secret=$(echo $code_client | jq -r '.client_secret')

go run . perform device-code --endpoint  http://127.0.0.1:4444  --client-id $code_client_id 

Copy link
Member

@aeneasr aeneasr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What a mountain of work. Thank you @nsklikas, @supercairos and @BuzzBumbleBee. I know it took way longer than you probably have ever thought, but I am very glad that Canonical stepped up and did not give up on this huge project. The result is truly amazing and I am very happy to approve and merge this PR. I will do one last last last round of sanity checks but the great collaboration in recent weeks gives me a ton of confidence that we have solved this huge challenge extremely well. Thank you everyoneryone, again!

@aeneasr
Copy link
Member

aeneasr commented Feb 26, 2025

Ok, the latest changes make sense. Let's do it :)

@aeneasr aeneasr merged commit 5215d24 into ory:master Feb 26, 2025
54 checks passed
@meysam81
Copy link

wonderful job everyone ❤️

@aeneasr when can we expect a github release? 🙏

Comment on lines +28 to +29
deviceChallenge
deviceVerifier
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nsklikas injecting those here breaks the existing ENUM. This should really be appended - unfortunately I missed this during review, but for future reference.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shit, sorry I didn't think of that. Thanks for pointing it out, will try to be mindful of this in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Device Authorization Grant: RFC 8628

6 participants