feat(api): allow Device Authorization Grant using custom login UI (#9387)

# Which Problems Are Solved

The OAuth2 Device Authorization Grant could not yet been handled through
the new login UI, resp. using the session API.
This PR adds the ability for the login UI to get the required
information to display the user and handle their decision (approve with
authorization or deny) using the OIDC Service API.

# How the Problems Are Solved

- Added a `GetDeviceAuthorizationRequest` endpoint, which allows getting
the `id`, `client_id`, `scope`, `app_name` and `project_name` of the
device authorization request
- Added a `AuthorizeOrDenyDeviceAuthorization` endpoint, which allows to
approve/authorize with the session information or deny the request. The
identification of the request is done by the `device_authorization_id` /
`id` returned in the previous request.
- To prevent leaking the `device_code` to the UI, but still having an
easy reference, it's encrypted and returned as `id`, resp. decrypted
when used.
- Fixed returned error types for device token responses on token
endpoint:
- Explicitly return `access_denied` (without internal error) when user
denied the request
  - Default to `invalid_grant` instead of `access_denied`
- Explicitly check on initial state when approving the reqeust
- Properly handle done case (also relates to initial check) 
- Documented the flow and handling in custom UIs (according to OIDC /
SAML)

# Additional Changes

- fixed some typos and punctuation in the corresponding OIDC / SAML
guides.
- added some missing translations for auth and saml request

# Additional Context

- closes #6239

---------

Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
This commit is contained in:
Livio Spring
2025-02-25 07:33:13 +01:00
committed by GitHub
parent f2e82d57ac
commit 911200aa9b
39 changed files with 1210 additions and 35 deletions

View File

@@ -114,4 +114,17 @@ enum ErrorReason {
ERROR_REASON_REQUEST_NOT_SUPPORTED = 14;
ERROR_REASON_REQUEST_URI_NOT_SUPPORTED = 15;
ERROR_REASON_REGISTRATION_NOT_SUPPORTED = 16;
}
message DeviceAuthorizationRequest {
// The unique identifier of the device authorization request to be used for authorizing or denying the request.
string id = 1;
// The client_id of the application that initiated the device authorization request.
string client_id = 2;
// The scopes requested by the application.
repeated string scope = 3;
// Name of the client application.
string app_name = 4;
// Name of the project the client application is part of.
string project_name = 5;
}

View File

@@ -147,6 +147,58 @@ service OIDCService {
};
};
}
// Get device authorization request
//
// Get the device authorization based on the provided "user code".
// This will return the device authorization request, which contains the device authorization id
// that is required to authorize the request once the user signed in or to deny it.
rpc GetDeviceAuthorizationRequest(GetDeviceAuthorizationRequestRequest) returns (GetDeviceAuthorizationRequestResponse) {
option (google.api.http) = {
get: "/v2/oidc/device_authorization/{user_code}"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "authenticated"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
// Authorize or deny device authorization
//
// Authorize or deny the device authorization request based on the provided device authorization id.
rpc AuthorizeOrDenyDeviceAuthorization(AuthorizeOrDenyDeviceAuthorizationRequest) returns (AuthorizeOrDenyDeviceAuthorizationResponse) {
option (google.api.http) = {
post: "/v2/oidc/device_authorization/{device_authorization_id}"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "authenticated"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
}
message GetAuthRequestRequest {
@@ -217,3 +269,42 @@ message CreateCallbackResponse {
];
}
message GetDeviceAuthorizationRequestRequest {
// The user_code returned by the device authorization request and provided to the user by the device.
string user_code = 1 [
(validate.rules).string = {len: 9},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 9;
max_length: 9;
example: "\"K9LV-3DMQ\"";
}
];
}
message GetDeviceAuthorizationRequestResponse {
DeviceAuthorizationRequest device_authorization_request = 1;
}
message AuthorizeOrDenyDeviceAuthorizationRequest {
// The device authorization id returned when submitting the user code.
string device_authorization_id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
}
];
// The decision of the user to authorize or deny the device authorization request.
oneof decision {
option (validate.required) = true;
// To authorize the device authorization request, the user's session must be provided.
Session session = 2;
// Deny the device authorization request.
Deny deny = 3;
}
}
message Deny{}
message AuthorizeOrDenyDeviceAuthorizationResponse {}