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: implement RFC 8628 #3912

Open
wants to merge 38 commits into
base: master
Choose a base branch
from
Open

Conversation

nsklikas
Copy link

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

h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrNotFound.WithWrap(err).WithHint(`'user_code' session not found`)))
return
}
err = h.r.RFC8628HMACStrategy().ValidateUserCode(ctx, userCodeRequest, reqBody.UserCode)
Copy link
Member

Choose a reason for hiding this comment

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

reqBody.UserCode is not actually being validated by validateusercode. We can probably remove it? Or should it be validated?

q := reqURL.Query()
q.Add("client_id", userCodeRequest.GetClient().GetID())
reqURL.RawQuery = q.Encode()
}
Copy link
Member

Choose a reason for hiding this comment

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

Should we not always use the client from the usercode request? Why is it possible to override the client here? This is a bit confusing to me as to why we need this.

require.Contains(t, result.RedirectTo, "device_verifier")
}

func TestAcceptDuplicateDeviceRequest(t *testing.T) {
Copy link
Member

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:

  1. Test if we can accept the same device challenge twice and that the device verifier is valid
  2. Test if we can accept the device challenge once, use it, then try again, and fail

validateResponse: func(resp *http.Response) {
require.EqualValues(t, http.StatusBadRequest, resp.StatusCode)
},
},
Copy link
Member

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

@@ -41,6 +41,7 @@ import (
)

const (
DeviceVerificationPath = "/oauth2/device/verify"
Copy link
Member

Choose a reason for hiding this comment

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

Is this URL coming from some spec?

// If there were multiple flows created for the same user_code then we may end up with multiple flow objects
// persisted to the database, while only one of them was actually used to validate the user_code
// (see https://github.com/ory/hydra/pull/3851#discussion_r1843678761)
// TODO: We should wrap these queries in a transaction
Copy link
Member

Choose a reason for hiding this comment

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

TODO is open here

x.LogError(r, err, h.r.Logger())
h.r.Writer().WriteError(w, r, err)
return
}
Copy link
Member

Choose a reason for hiding this comment

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

Is this necessary? The audit logger is really not something we are using/maintaining. You can probably just log to the regular logger

if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
Copy link
Member

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 = ? ?

Copy link
Member

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

Copy link
Member

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?

"prompt",
"acr_values",
"id_token_hint",
"nonce",
Copy link
Member

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?

CREATE INDEX hydra_oauth2_device_auth_codes_request_id_idx ON hydra_oauth2_device_auth_codes (request_id, nid);
CREATE INDEX hydra_oauth2_device_auth_codes_client_id_idx ON hydra_oauth2_device_auth_codes (client_id, nid);
CREATE INDEX hydra_oauth2_device_auth_codes_challenge_id_idx ON hydra_oauth2_device_auth_codes (challenge_id);
CREATE UNIQUE INDEX hydra_oauth2_device_auth_codes_user_code_signature_idx ON hydra_oauth2_device_auth_codes (user_code_signature, nid);
Copy link
Member

Choose a reason for hiding this comment

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

I only found queries for device_code_signature (primary key) and request_id) - do we need the challenge-id?

Also are we not missing a foreign key onto the oauth2 flow table (I'm not sure if we have that in the other tables)

Since you're referencing nid as a foreign key, it will also need an index where it's the prefix.

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.

3 participants