-
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
consent/handler.go
Outdated
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) |
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.
reqBody.UserCode is not actually being validated by validateusercode. We can probably remove it? Or should it be validated?
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.
As discussed in the fosite PR, this method makes sure that the user_code has not expired. The authenticity of the code has been verified by the call to GetUserCodeSession
above.
q := reqURL.Query() | ||
q.Add("client_id", userCodeRequest.GetClient().GetID()) | ||
reqURL.RawQuery = q.Encode() | ||
} |
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.
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.
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.
The initial request should not contain the client_id
. We don't know the client_id when the user gets to the device verification endpoint, we need to fetch the user_code from the database to get it.
The only reason we are adding the client_id
to the request is to make the existing endpoints work without too many changes. The client_id
is parsed in https://github.com/nsklikas/hydra/blob/canonical-master/consent/strategy_default.go#L1211 to construct an authorization request that will be used to do the rest of the flow. If we didn't have this hack, we would have to refactor part of the existing default_strategy. IMHO this is work that should be done after this PR is merged.
I will add a comment to make this clearer.
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?
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
@@ -41,6 +41,7 @@ import ( | |||
) | |||
|
|||
const ( | |||
DeviceVerificationPath = "/oauth2/device/verify" |
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 URL coming from some spec?
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.
No, the spec does not specify how the user_code should be verified.
// 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 |
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.
TODO is open 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.
Yes, we discussed this with @zepatrik. We decided that there are no major issues with this and that it should be fixed later on
x.LogError(r, err, h.r.Logger()) | ||
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.
Is this necessary? The audit logger is really not something we are using/maintaining. You can probably just log to the regular logger
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.
Are referring to the logs on https://github.com/ory/hydra/pull/3912/files/14cf7cdc370102da2f487f12d7cdf4945b3e0f48#diff-dc5158991c1ce571f0a59c32b86d6fc64c55fbe11d67cdee44b1b0ae436569dfR743 and https://github.com/ory/hydra/pull/3912/files/14cf7cdc370102da2f487f12d7cdf4945b3e0f48#diff-dc5158991c1ce571f0a59c32b86d6fc64c55fbe11d67cdee44b1b0ae436569dfR748 (lines 743 and 748)?
I can change it, only reason I used that logger is because it is also used in oAuth2Authorize
as well
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.
"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?
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); |
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 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.
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 only found queries for
device_code_signature
(primary key) andrequest_id)
- do we need the challenge-id?
The challenge_id is not used as an index, the only reason it is there is to be uniform with the other oauth2_token/code
tables. AFAICT it is not used there either, but thought maybe there is another reason for it.
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)
Thought that there was some issue with adding that fk, but it seems to work now. Will try to add it.
Since you're referencing
nid
as a foreign key, it will also need an index where it's the prefix.
Not sure why we need an index for that key. When will it ever be used? Do other tables have this index?
Instead of updating the device session, we were over-writing it causing existing session info that were created from fosite to be lost.
0a4fa13
to
2e8cf7c
Compare
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
[email protected]) 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:
[email protected]
test