mirror of
https://github.com/zitadel/zitadel.git
synced 2025-02-28 19:47:23 +00:00
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:
parent
f2e82d57ac
commit
911200aa9b
@ -560,7 +560,7 @@ func startAPIs(
|
||||
if err := apis.RegisterService(ctx, oidc_v2beta.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil {
|
||||
if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcServer, config.ExternalSecure, keys.OIDC)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// After SAML provider so that the callback endpoint can be used
|
||||
|
165
docs/docs/guides/integrate/login-ui/device-auth.mdx
Normal file
165
docs/docs/guides/integrate/login-ui/device-auth.mdx
Normal file
@ -0,0 +1,165 @@
|
||||
---
|
||||
title: Support for the Device Authorization Grant in a Custom Login UI
|
||||
sidebar_label: Device Authorization
|
||||
---
|
||||
|
||||
In case one of your applications requires the [OAuth2 Device Authorization Grant](/docs/guides/integrate/login/oidc/device-authorization) this guide will show you how to implement
|
||||
this in your application as well as the custom login UI.
|
||||
|
||||
The following flow shows you the different components you need to enable OAuth2 Device Authorization Grant for your login.
|
||||

|
||||
|
||||
1. Your application makes a device authorization request to your login UI
|
||||
2. The login UI proxies the request to ZITADEL.
|
||||
3. ZITADEL parses the request and does what it needs to interpret certain parameters (e.g., organization scope, etc.)
|
||||
4. ZITADEL returns the device authorization response
|
||||
5. Your application presents the `user_code` and `verification_uri` or maybe even renders a QR code with the `verification_uri_complete` for the user to scan
|
||||
6. Your application starts a polling mechanism to check if the user has approved the device authorization request on the token endpoint
|
||||
7. When the user opens the browser at the verification_uri, he can enter the user_code, or it's automatically filled in, if they scan the QR code
|
||||
8. Request the device authorization request from the ZITADEL API using the user_code
|
||||
9. Your login UI allows to approve or deny the device request
|
||||
10. In case they approved, authenticate the user in your login UI by creating and updating a session with all the checks you need.
|
||||
11. Inform ZITADEL about the decision:
|
||||
1. Authorize the device authorization request by sending the session and the previously retrieved id of the device authorization request to the ZITADEL API
|
||||
2. In case they denied, deny the device authorization from the ZITADEL API using the previously retrieved id of the device authorization request
|
||||
12. Notify the user that they can close the window now and return to the application.
|
||||
13. Your applications request to the token endpoint now receives the tokens or an error if the user denied the request.
|
||||
|
||||
## Example
|
||||
|
||||
Let's assume you host your login UI on the following URL:
|
||||
```
|
||||
https://login.example.com
|
||||
```
|
||||
|
||||
## Device Authorization Request
|
||||
|
||||
A user opens your application and is unauthenticated, the application will create the following request:
|
||||
```HTTP
|
||||
POST /oauth/v2/device_authorization HTTP/1.1
|
||||
Host: login.example.com
|
||||
Content-type: application/x-www-form-urlencoded
|
||||
|
||||
client_id=170086824411201793&
|
||||
scope=openid%20email%20profile
|
||||
```
|
||||
|
||||
The request includes all the relevant information for the OAuth2 Device Authorization Grant and in this example we also have some scopes for the user.
|
||||
|
||||
You now have to proxy the auth request from your own UI to the device authorization Endpoint of ZITADEL.
|
||||
For more information, see [OIDC Proxy](./typescript-repo#oidc-proxy) for the necessary headers.
|
||||
|
||||
:::note
|
||||
The version and the optional custom URI for the available login UI is configurable under the application settings.
|
||||
:::
|
||||
|
||||
The endpoint will return the device authorization response:
|
||||
```json
|
||||
{
|
||||
"device_code": "0jbAZbU3ClK-Mkt0li4U1A",
|
||||
"user_code": "FWRK-JGWK",
|
||||
"verification_uri": "https://login.example.com/device",
|
||||
"verification_uri_complete": "https://login.example.com/device?user_code=FWRK-JGWK",
|
||||
"expires_in": 300,
|
||||
"interval": 5
|
||||
}
|
||||
```
|
||||
|
||||
The device presents the `user_code` and `verification_uri` or maybe even render a QR code with the `verification_uri_complete` for the user to scan.
|
||||
|
||||
Your login will have to provide a page on the `verification_uri` where the user can enter the `user_code`, or it's automatically filled in, if they scan the QR code.
|
||||
|
||||
### Get the Device Authorization Request by User Code
|
||||
|
||||
With the user_code entered by the user you will now be able to get the information of the device authorization request.
|
||||
[Get Device Authorization Request Documentation](/docs/apis/resources/oidc_service_v2/oidc-service-get-device-authorization-request)
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url https://$ZITADEL_DOMAIN/v2/oidc/device_authorization/FWRK-JGWK \
|
||||
--header 'Authorization: Bearer '"$TOKEN"''
|
||||
```
|
||||
|
||||
Response Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceAuthorizationRequest": {
|
||||
"id": "XzNejv6NxqVU8Qur5uxEh7f_Wi1p0qUu4PJTJ6JUIx0xtJ2uqmU",
|
||||
"clientId": "170086824411201793",
|
||||
"scope": [
|
||||
"openid",
|
||||
"profile"
|
||||
],
|
||||
"appName": "TV App",
|
||||
"projectName": "My Project"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Present the user with the information of the device authorization request and allow them to approve or deny the request.
|
||||
|
||||
### Perform Login
|
||||
|
||||
After you have initialized the OIDC flow you can implement the login.
|
||||
Implement all the steps you like the user the go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service/session-service-set-session) the user-session.
|
||||
|
||||
Read the following resources for more information about the different checks:
|
||||
- [Username and Password](./username-password)
|
||||
- [External Identity Provider](./external-login)
|
||||
- [Passkeys](./passkey)
|
||||
- [Multi-Factor](./mfa)
|
||||
|
||||
### Authorize the Device Authorization Request
|
||||
|
||||
To finalize the auth request and connect an existing user session with it, you have to update the auth request with the session token.
|
||||
On the create and update user session request you will always get a session token in the response.
|
||||
|
||||
The latest session token has to be sent to the following request:
|
||||
|
||||
Read more about the [Authorize or Deny Device Authorization Request Documentation](/docs/apis/resources/oidc_service_v2/oidc-service-authorize-device-authorization)
|
||||
|
||||
Make sure that the authorization header is from an account which is permitted to finalize the Auth Request through the `IAM_LOGIN_CLIENT` role.
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url $ZITADEL_DOMAIN/v2/oidc/device_authorization/XzNejv6NxqVU8Qur5uxEh7f_Wi1p0qUu4PJTJ6JUIx0xtJ2uqmU \
|
||||
--header 'Accept: application/json' \
|
||||
--header 'Authorization: Bearer '"$TOKEN"''\
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"session": {
|
||||
"sessionId": "225307381909694508",
|
||||
"sessionToken": "7N5kQCvC4jIf2OuBjwfyWSX2FUKbQqg4iG3uWT-TBngMhlS9miGUwpyUaN0HJ8OcbSzk4QHZy_Bvvv"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
If you don't get any error back, the request succeeded, and you can notify the user that they can close the window now and return to the application.
|
||||
|
||||
### Deny the Device Authorization Request
|
||||
|
||||
If the user denies the device authorization request, you can deny the request by sending the following request:
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url $ZITADEL_DOMAIN/v2/oidc/device_authorization/ \
|
||||
--header 'Accept: application/json' \
|
||||
--header 'Authorization: Bearer '"$TOKEN"''\
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"deny": {}
|
||||
}'
|
||||
```
|
||||
|
||||
If you don't get any error back, the request succeeded, and you can notify the user that they can close the window now and return to the application.
|
||||
|
||||
### Device Authorization Endpoints
|
||||
|
||||
All OAuth2 Device Authorization Grant endpoints are provided by ZITADEL. In your login UI you just have to proxy them through and send them directly to the backend.
|
||||
|
||||
These endpoints are:
|
||||
- Well-known
|
||||
- Device Authorization Endpoint
|
||||
- Token
|
||||
|
||||
Additionally, we recommend you to proxy all the other [OIDC relevant endpoints](./oidc-standard#endpoints).
|
@ -56,7 +56,7 @@ With the ID from the redirect before you will now be able to get the information
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url https://$ZITADEL_DOMAIN/v2/oidc/auth_requests/V2_224908753244265546 \
|
||||
--header 'Authorization: Bearer '"$TOKEN"''\
|
||||
--header 'Authorization: Bearer '"$TOKEN"''
|
||||
```
|
||||
|
||||
Response Example:
|
||||
@ -90,7 +90,7 @@ Read the following resources for more information about the different checks:
|
||||
|
||||
### Finalize Auth Request
|
||||
|
||||
To finalize the auth request and connect an existing user session with it you have to update the auth request with the session token.
|
||||
To finalize the auth request and connect an existing user session with it, you have to update the auth request with the session token.
|
||||
On the create and update user session request you will always get a session token in the response.
|
||||
|
||||
The latest session token has to be sent to the following request:
|
||||
@ -128,7 +128,7 @@ Example Response:
|
||||
|
||||
### OIDC Endpoints
|
||||
|
||||
All OIDC relevant endpoints are provided by ZITADEL. In you login UI you just have to proxy them through and send them directly to the backend.
|
||||
All OIDC relevant endpoints are provided by ZITADEL. In your login UI you just have to proxy them through and send them directly to the backend.
|
||||
|
||||
These are endpoints like:
|
||||
- Userinfo
|
||||
|
@ -56,7 +56,7 @@ With the ID from the redirect before you will now be able to get the information
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url https://$ZITADEL_DOMAIN/v2/saml/saml_requests/V2_224908753244265546 \
|
||||
--header 'Authorization: Bearer '"$TOKEN"''\
|
||||
--header 'Authorization: Bearer '"$TOKEN"''
|
||||
```
|
||||
|
||||
Response Example:
|
||||
@ -87,7 +87,7 @@ Read the following resources for more information about the different checks:
|
||||
|
||||
### Finalize SAML Request
|
||||
|
||||
To finalize the SAML request and connect an existing user session with it you have to update the SAML Request with the session token.
|
||||
To finalize the SAML request and connect an existing user session with it, you have to update the SAML Request with the session token.
|
||||
On the create and update user session request you will always get a session token in the response.
|
||||
|
||||
The latest session token has to be sent to the following request:
|
||||
|
@ -328,6 +328,7 @@ module.exports = {
|
||||
"guides/integrate/login-ui/logout",
|
||||
"guides/integrate/login-ui/oidc-standard",
|
||||
"guides/integrate/login-ui/saml-standard",
|
||||
"guides/integrate/login-ui/device-auth",
|
||||
"guides/integrate/login-ui/typescript-repo",
|
||||
],
|
||||
},
|
||||
|
BIN
docs/static/img/guides/login-ui/device-auth-flow.png
vendored
Normal file
BIN
docs/static/img/guides/login-ui/device-auth-flow.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 80 KiB |
@ -13,6 +13,7 @@ import (
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
@ -636,6 +637,230 @@ func TestServer_CreateCallback_Permission(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_GetDeviceAuthorizationRequest(t *testing.T) {
|
||||
project, err := Instance.CreateProject(CTX)
|
||||
require.NoError(t, err)
|
||||
client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dep func() (*oidc.DeviceAuthorizationResponse, error)
|
||||
ctx context.Context
|
||||
want *oidc.DeviceAuthorizationResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Not found",
|
||||
dep: func() (*oidc.DeviceAuthorizationResponse, error) {
|
||||
return &oidc.DeviceAuthorizationResponse{
|
||||
UserCode: "notFound",
|
||||
}, nil
|
||||
},
|
||||
ctx: CTX,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
dep: func() (*oidc.DeviceAuthorizationResponse, error) {
|
||||
return Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
|
||||
},
|
||||
ctx: CTX,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
deviceAuth, err := tt.dep()
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := Client.GetDeviceAuthorizationRequest(tt.ctx, &oidc_pb.GetDeviceAuthorizationRequestRequest{
|
||||
UserCode: deviceAuth.UserCode,
|
||||
})
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
authRequest := got.GetDeviceAuthorizationRequest()
|
||||
assert.NotNil(t, authRequest)
|
||||
assert.NotEmpty(t, authRequest.GetId())
|
||||
assert.Equal(t, client.GetClientId(), authRequest.GetClientId())
|
||||
assert.Contains(t, authRequest.GetScope(), "openid")
|
||||
assert.NotEmpty(t, authRequest.GetAppName())
|
||||
assert.NotEmpty(t, authRequest.GetProjectName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) {
|
||||
project, err := Instance.CreateProject(CTX)
|
||||
require.NoError(t, err)
|
||||
client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE)
|
||||
require.NoError(t, err)
|
||||
sessionResp := createSession(t, CTX, Instance.Users[integration.UserTypeOrgOwner].ID)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
req *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest
|
||||
AuthError string
|
||||
want *oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse
|
||||
wantURL *url.URL
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Not found",
|
||||
ctx: CTX,
|
||||
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: "123",
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: sessionResp.GetSessionToken(),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "session not found",
|
||||
ctx: CTX,
|
||||
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: func() string {
|
||||
req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
|
||||
require.NoError(t, err)
|
||||
var id string
|
||||
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
|
||||
resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
|
||||
UserCode: req.UserCode,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
id = resp.GetDeviceAuthorizationRequest().GetId()
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
return id
|
||||
}(),
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: "foo",
|
||||
SessionToken: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "session token invalid",
|
||||
ctx: CTX,
|
||||
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: func() string {
|
||||
req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
|
||||
require.NoError(t, err)
|
||||
var id string
|
||||
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
|
||||
resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
|
||||
UserCode: req.UserCode,
|
||||
})
|
||||
assert.NoError(collectT, err)
|
||||
id = resp.GetDeviceAuthorizationRequest().GetId()
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
return id
|
||||
}(),
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "deny device authorization",
|
||||
ctx: CTX,
|
||||
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: func() string {
|
||||
req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
|
||||
require.NoError(t, err)
|
||||
var id string
|
||||
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
|
||||
resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
|
||||
UserCode: req.UserCode,
|
||||
})
|
||||
assert.NoError(collectT, err)
|
||||
id = resp.GetDeviceAuthorizationRequest().GetId()
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
return id
|
||||
}(),
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Deny{},
|
||||
},
|
||||
want: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "authorize, no permission, error",
|
||||
ctx: CTX,
|
||||
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: func() string {
|
||||
req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
|
||||
require.NoError(t, err)
|
||||
var id string
|
||||
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
|
||||
resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
|
||||
UserCode: req.UserCode,
|
||||
})
|
||||
assert.NoError(collectT, err)
|
||||
id = resp.GetDeviceAuthorizationRequest().GetId()
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
return id
|
||||
}(),
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: sessionResp.GetSessionToken(),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "authorize, with permission",
|
||||
ctx: CTXLoginClient,
|
||||
req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: func() string {
|
||||
req, err := Instance.CreateDeviceAuthorizationRequest(CTX, client.GetClientId(), "openid")
|
||||
require.NoError(t, err)
|
||||
var id string
|
||||
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
|
||||
resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
|
||||
UserCode: req.UserCode,
|
||||
})
|
||||
assert.NoError(collectT, err)
|
||||
id = resp.GetDeviceAuthorizationRequest().GetId()
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
return id
|
||||
}(),
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: sessionResp.GetSessionToken(),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := Client.AuthorizeOrDenyDeviceAuthorization(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createSession(t *testing.T, ctx context.Context, userID string) *session.CreateSessionResponse {
|
||||
sessionResp, err := Instance.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
|
@ -2,6 +2,7 @@ package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
@ -28,6 +29,54 @@ func (s *Server) GetAuthRequest(ctx context.Context, req *oidc_pb.GetAuthRequest
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) {
|
||||
switch v := req.GetCallbackKind().(type) {
|
||||
case *oidc_pb.CreateCallbackRequest_Error:
|
||||
return s.failAuthRequest(ctx, req.GetAuthRequestId(), v.Error)
|
||||
case *oidc_pb.CreateCallbackRequest_Session:
|
||||
return s.linkSessionToAuthRequest(ctx, req.GetAuthRequestId(), v.Session)
|
||||
default:
|
||||
return nil, zerrors.ThrowUnimplementedf(nil, "OIDCv2-zee7A", "verification oneOf %T in method CreateCallback not implemented", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) GetDeviceAuthorizationRequest(ctx context.Context, req *oidc_pb.GetDeviceAuthorizationRequestRequest) (*oidc_pb.GetDeviceAuthorizationRequestResponse, error) {
|
||||
deviceRequest, err := s.query.DeviceAuthRequestByUserCode(ctx, req.GetUserCode())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encrypted, err := s.encryption.Encrypt([]byte(deviceRequest.DeviceCode))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &oidc_pb.GetDeviceAuthorizationRequestResponse{
|
||||
DeviceAuthorizationRequest: &oidc_pb.DeviceAuthorizationRequest{
|
||||
Id: base64.RawURLEncoding.EncodeToString(encrypted),
|
||||
ClientId: deviceRequest.ClientID,
|
||||
Scope: deviceRequest.Scopes,
|
||||
AppName: deviceRequest.AppName,
|
||||
ProjectName: deviceRequest.ProjectName,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) AuthorizeOrDenyDeviceAuthorization(ctx context.Context, req *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest) (*oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse, error) {
|
||||
deviceCode, err := s.deviceCodeFromID(req.GetDeviceAuthorizationId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch req.GetDecision().(type) {
|
||||
case *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session:
|
||||
_, err = s.command.ApproveDeviceAuthWithSession(ctx, deviceCode, req.GetSession().GetSessionId(), req.GetSession().GetSessionToken())
|
||||
case *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Deny:
|
||||
_, err = s.command.CancelDeviceAuth(ctx, deviceCode, domain.DeviceAuthCanceledDenied)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse{}, nil
|
||||
}
|
||||
|
||||
func authRequestToPb(a *query.AuthRequest) *oidc_pb.AuthRequest {
|
||||
pba := &oidc_pb.AuthRequest{
|
||||
Id: a.ID,
|
||||
@ -87,17 +136,6 @@ func (s *Server) checkPermission(ctx context.Context, clientID string, userID st
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) {
|
||||
switch v := req.GetCallbackKind().(type) {
|
||||
case *oidc_pb.CreateCallbackRequest_Error:
|
||||
return s.failAuthRequest(ctx, req.GetAuthRequestId(), v.Error)
|
||||
case *oidc_pb.CreateCallbackRequest_Session:
|
||||
return s.linkSessionToAuthRequest(ctx, req.GetAuthRequestId(), v.Session)
|
||||
default:
|
||||
return nil, zerrors.ThrowUnimplementedf(nil, "OIDCv2-zee7A", "verification oneOf %T in method CreateCallback not implemented", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae *oidc_pb.AuthorizationError) (*oidc_pb.CreateCallbackResponse, error) {
|
||||
details, aar, err := s.command.FailAuthRequest(ctx, authRequestID, errorReasonToDomain(ae.GetError()))
|
||||
if err != nil {
|
||||
@ -215,3 +253,11 @@ func errorReasonToOIDC(reason oidc_pb.ErrorReason) string {
|
||||
return "server_error"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) deviceCodeFromID(deviceAuthID string) (string, error) {
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(deviceAuthID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return s.encryption.DecryptString(decoded, s.encryption.EncryptionKeyID())
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
"github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
|
||||
)
|
||||
@ -20,6 +21,7 @@ type Server struct {
|
||||
|
||||
op *oidc.Server
|
||||
externalSecure bool
|
||||
encryption crypto.EncryptionAlgorithm
|
||||
}
|
||||
|
||||
type Config struct{}
|
||||
@ -29,12 +31,14 @@ func CreateServer(
|
||||
query *query.Queries,
|
||||
op *oidc.Server,
|
||||
externalSecure bool,
|
||||
encryption crypto.EncryptionAlgorithm,
|
||||
) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
op: op,
|
||||
externalSecure: externalSecure,
|
||||
encryption: encryption,
|
||||
}
|
||||
}
|
||||
|
||||
|
127
internal/api/oidc/integration_test/token_device_test.go
Normal file
127
internal/api/oidc/integration_test/token_device_test.go
Normal file
@ -0,0 +1,127 @@
|
||||
//go:build integration
|
||||
|
||||
package oidc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/app"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/auth"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
|
||||
)
|
||||
|
||||
func TestServer_DeviceAuth(t *testing.T) {
|
||||
project, err := Instance.CreateProject(CTX)
|
||||
require.NoError(t, err)
|
||||
client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
scope []string
|
||||
decision func(t *testing.T, id string)
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "authorized",
|
||||
scope: []string{},
|
||||
decision: func(t *testing.T, id string) {
|
||||
sessionID, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
||||
_, err = Instance.Client.OIDCv2.AuthorizeOrDenyDeviceAuthorization(CTXLOGIN, &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: id,
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionID,
|
||||
SessionToken: sessionToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "authorized, with ZITADEL",
|
||||
scope: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, domain.ProjectScopeZITADEL},
|
||||
decision: func(t *testing.T, id string) {
|
||||
sessionID, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
|
||||
_, err = Instance.Client.OIDCv2.AuthorizeOrDenyDeviceAuthorization(CTXLOGIN, &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: id,
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionID,
|
||||
SessionToken: sessionToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "denied",
|
||||
scope: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, domain.ProjectScopeZITADEL},
|
||||
decision: func(t *testing.T, id string) {
|
||||
_, err = Instance.Client.OIDCv2.AuthorizeOrDenyDeviceAuthorization(CTXLOGIN, &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{
|
||||
DeviceAuthorizationId: id,
|
||||
Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Deny{
|
||||
Deny: &oidc_pb.Deny{},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
wantErr: oidc.ErrAccessDenied(),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
provider, err := rp.NewRelyingPartyOIDC(CTX, Instance.OIDCIssuer(), client.GetClientId(), "", "", tt.scope)
|
||||
require.NoError(t, err)
|
||||
deviceAuthorization, err := rp.DeviceAuthorization(CTX, tt.scope, provider, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
relyingPartyDone := make(chan struct{})
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(CTX, 1*time.Minute)
|
||||
defer func() {
|
||||
cancel()
|
||||
relyingPartyDone <- struct{}{}
|
||||
}()
|
||||
tokens, err := rp.DeviceAccessToken(ctx, deviceAuthorization.DeviceCode, time.Duration(deviceAuthorization.Interval)*time.Second, provider)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
|
||||
if tokens == nil {
|
||||
return
|
||||
}
|
||||
_, err = Instance.Client.Auth.GetMyUser(integration.WithAuthorizationToken(CTX, tokens.AccessToken), &auth.GetMyUserRequest{})
|
||||
if slices.Contains(tt.scope, domain.ProjectScopeZITADEL) {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
}()
|
||||
|
||||
var req *oidc_pb.GetDeviceAuthorizationRequestResponse
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
|
||||
assert.EventuallyWithT(t, func(collectT *assert.CollectT) {
|
||||
req, err = Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTX, &oidc_pb.GetDeviceAuthorizationRequestRequest{
|
||||
UserCode: deviceAuthorization.UserCode,
|
||||
})
|
||||
assert.NoError(collectT, err)
|
||||
}, retryDuration, tick)
|
||||
|
||||
tt.decision(t, req.GetDeviceAuthorizationRequest().GetId())
|
||||
|
||||
<-relyingPartyDone
|
||||
})
|
||||
}
|
||||
}
|
@ -42,6 +42,9 @@ func (s *Server) DeviceToken(ctx context.Context, r *op.ClientRequest[oidc.Devic
|
||||
if state == domain.DeviceAuthStateExpired {
|
||||
return nil, oidc.ErrExpiredDeviceCode()
|
||||
}
|
||||
if state == domain.DeviceAuthStateDenied {
|
||||
return nil, oidc.ErrAccessDenied()
|
||||
}
|
||||
return nil, oidc.ErrAccessDenied().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError)
|
||||
}
|
||||
return nil, oidc.ErrInvalidGrant().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError)
|
||||
}
|
||||
|
@ -59,6 +59,9 @@ func (c *Commands) ApproveDeviceAuth(
|
||||
if !model.State.Exists() {
|
||||
return nil, zerrors.ThrowNotFound(nil, "COMMAND-Hief9", "Errors.DeviceAuth.NotFound")
|
||||
}
|
||||
if model.State != domain.DeviceAuthStateInitiated {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-GEJL3", "Errors.DeviceAuth.AlreadyHandled")
|
||||
}
|
||||
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(ctx, model.aggregate, userID, userOrgID, authMethods, authTime, preferredLanguage, userAgent, sessionID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -71,6 +74,60 @@ func (c *Commands) ApproveDeviceAuth(
|
||||
return writeModelToObjectDetails(&model.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) ApproveDeviceAuthWithSession(
|
||||
ctx context.Context,
|
||||
deviceCode,
|
||||
sessionID,
|
||||
sessionToken string,
|
||||
) (*domain.ObjectDetails, error) {
|
||||
model, err := c.getDeviceAuthWriteModelByDeviceCode(ctx, deviceCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !model.State.Exists() {
|
||||
return nil, zerrors.ThrowNotFound(nil, "COMMAND-D2hf2", "Errors.DeviceAuth.NotFound")
|
||||
}
|
||||
if model.State != domain.DeviceAuthStateInitiated {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-D30Jf", "Errors.DeviceAuth.AlreadyHandled")
|
||||
}
|
||||
if err := c.checkPermission(ctx, domain.PermissionSessionLink, model.ResourceOwner, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID())
|
||||
err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = sessionWriteModel.CheckIsActive(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.sessionTokenVerifier(ctx, sessionToken, sessionWriteModel.AggregateID, sessionWriteModel.TokenID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(
|
||||
ctx,
|
||||
model.aggregate,
|
||||
sessionWriteModel.UserID,
|
||||
sessionWriteModel.UserResourceOwner,
|
||||
sessionWriteModel.AuthMethodTypes(),
|
||||
sessionWriteModel.AuthenticationTime(),
|
||||
sessionWriteModel.PreferredLanguage,
|
||||
sessionWriteModel.UserAgent,
|
||||
sessionID,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(model, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return writeModelToObjectDetails(&model.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) CancelDeviceAuth(ctx context.Context, id string, reason domain.DeviceAuthCanceled) (*domain.ObjectDetails, error) {
|
||||
model, err := c.getDeviceAuthWriteModelByDeviceCode(ctx, id)
|
||||
if err != nil {
|
||||
|
@ -82,6 +82,7 @@ func (m *DeviceAuthWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
deviceauth.AddedEventType,
|
||||
deviceauth.ApprovedEventType,
|
||||
deviceauth.CanceledEventType,
|
||||
deviceauth.DoneEventType,
|
||||
).
|
||||
Builder()
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/repository/deviceauth"
|
||||
"github.com/zitadel/zitadel/internal/repository/oidcsession"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
@ -265,6 +266,310 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) {
|
||||
ctx := authz.WithInstanceID(context.Background(), "instance1")
|
||||
now := time.Now()
|
||||
pushErr := errors.New("pushErr")
|
||||
|
||||
type fields struct {
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error)
|
||||
checkPermission domain.PermissionCheck
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
deviceCode string
|
||||
sessionID string
|
||||
sessionToken string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantDetails *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "not found error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx,
|
||||
"notfound",
|
||||
"sessionID",
|
||||
"sessionToken",
|
||||
},
|
||||
wantErr: zerrors.ThrowNotFound(nil, "COMMAND-D2hf2", "Errors.DeviceAuth.NotFound"),
|
||||
},
|
||||
{
|
||||
name: "not initialized, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("deviceCode", "instance1"),
|
||||
"client_id", "deviceCode", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"projectID", "clientID"}, true,
|
||||
),
|
||||
),
|
||||
eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewCanceledEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("deviceCode", "instance1"),
|
||||
domain.DeviceAuthCanceledDenied,
|
||||
)),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx,
|
||||
"deviceCode",
|
||||
"sessionID",
|
||||
"sessionToken",
|
||||
},
|
||||
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-D30Jf", "Errors.DeviceAuth.AlreadyHandled"),
|
||||
},
|
||||
{
|
||||
name: "missing permission, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("deviceCode", "instance1"),
|
||||
"client_id", "deviceCode", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"projectID", "clientID"}, true,
|
||||
),
|
||||
)),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx,
|
||||
"deviceCode",
|
||||
"sessionID",
|
||||
"sessionToken",
|
||||
},
|
||||
wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
|
||||
},
|
||||
{
|
||||
name: "session not active, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("deviceCode", "instance1"),
|
||||
"client_id", "deviceCode", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"projectID", "clientID"}, true,
|
||||
),
|
||||
)),
|
||||
expectFilter(),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx,
|
||||
"deviceCode",
|
||||
"sessionID",
|
||||
"sessionToken",
|
||||
},
|
||||
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Flk38", "Errors.Session.NotExisting"),
|
||||
},
|
||||
{
|
||||
name: "invalid session token, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("deviceCode", "instance1"),
|
||||
"client_id", "deviceCode", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"projectID", "clientID"}, true,
|
||||
),
|
||||
)),
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
session.NewAddedEvent(ctx,
|
||||
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
&domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
)),
|
||||
)),
|
||||
tokenVerifier: newMockTokenVerifierInvalid(),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx,
|
||||
"deviceCode",
|
||||
"sessionID",
|
||||
"invalidToken",
|
||||
},
|
||||
wantErr: zerrors.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid"),
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("deviceCode", "instance1"),
|
||||
"client_id", "deviceCode", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"projectID", "clientID"}, true,
|
||||
),
|
||||
)),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(ctx,
|
||||
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
&domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
session.NewUserCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
"userID", "orgID", testNow, &language.Afrikaans),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
session.NewPasswordCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
testNow),
|
||||
),
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
session.NewLifetimeSetEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
2*time.Minute),
|
||||
),
|
||||
),
|
||||
expectPushFailed(pushErr,
|
||||
deviceauth.NewApprovedEvent(
|
||||
ctx, deviceauth.NewAggregate("deviceCode", "instance1"), "userID", "orgID",
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
testNow, &language.Afrikaans, &domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
"sessionID",
|
||||
),
|
||||
),
|
||||
),
|
||||
tokenVerifier: newMockTokenVerifierValid(),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx,
|
||||
"deviceCode",
|
||||
"sessionID",
|
||||
"sessionToken",
|
||||
},
|
||||
wantErr: pushErr,
|
||||
},
|
||||
{
|
||||
name: "authorized",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(eventFromEventPusherWithInstanceID(
|
||||
"instance1",
|
||||
deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("deviceCode", "instance1"),
|
||||
"client_id", "deviceCode", "456", now,
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"projectID", "clientID"}, true,
|
||||
),
|
||||
)),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(ctx,
|
||||
&session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
&domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
session.NewUserCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
"userID", "orgID", testNow, &language.Afrikaans),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
session.NewPasswordCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
testNow),
|
||||
),
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
session.NewLifetimeSetEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate,
|
||||
2*time.Minute),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
deviceauth.NewApprovedEvent(
|
||||
ctx, deviceauth.NewAggregate("deviceCode", "instance1"), "userID", "orgID",
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
|
||||
testNow, &language.Afrikaans, &domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{"foo": []string{"bar"}},
|
||||
},
|
||||
"sessionID",
|
||||
),
|
||||
),
|
||||
),
|
||||
tokenVerifier: newMockTokenVerifierValid(),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx,
|
||||
"deviceCode",
|
||||
"sessionID",
|
||||
"sessionToken",
|
||||
},
|
||||
wantDetails: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
sessionTokenVerifier: tt.fields.tokenVerifier,
|
||||
checkPermission: tt.fields.checkPermission,
|
||||
}
|
||||
gotDetails, err := c.ApproveDeviceAuthWithSession(tt.args.ctx, tt.args.deviceCode, tt.args.sessionID, tt.args.sessionToken)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assertObjectDetails(t, tt.wantDetails, gotDetails)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_CancelDeviceAuth(t *testing.T) {
|
||||
ctx := authz.WithInstanceID(context.Background(), "instance1")
|
||||
now := time.Now()
|
||||
|
@ -67,6 +67,8 @@ type AuthRequestDevice struct {
|
||||
UserCode string
|
||||
Scopes []string
|
||||
Audience []string
|
||||
AppName string
|
||||
ProjectName string
|
||||
}
|
||||
|
||||
func (*AuthRequestDevice) Type() AuthRequestType {
|
||||
|
@ -144,7 +144,17 @@ func (m *MockRepository) ExpectPushFailed(err error, expectedCommands []eventsto
|
||||
assert.Equal(m.MockPusher.ctrl.T, expectedCommand.Creator(), commands[i].Creator())
|
||||
assert.Equal(m.MockPusher.ctrl.T, expectedCommand.Type(), commands[i].Type())
|
||||
assert.Equal(m.MockPusher.ctrl.T, expectedCommand.Revision(), commands[i].Revision())
|
||||
assert.Equal(m.MockPusher.ctrl.T, expectedCommand.Payload(), commands[i].Payload())
|
||||
var expectedPayload []byte
|
||||
expectedPayload, ok := expectedCommand.Payload().([]byte)
|
||||
if !ok {
|
||||
expectedPayload, _ = json.Marshal(expectedCommand.Payload())
|
||||
}
|
||||
if string(expectedPayload) == "" {
|
||||
expectedPayload = []byte("null")
|
||||
}
|
||||
gotPayload, _ := json.Marshal(commands[i].Payload())
|
||||
|
||||
assert.Equal(m.MockPusher.ctrl.T, expectedPayload, gotPayload)
|
||||
assert.ElementsMatch(m.MockPusher.ctrl.T, expectedCommand.UniqueConstraints(), commands[i].UniqueConstraints())
|
||||
}
|
||||
|
||||
|
@ -466,3 +466,11 @@ func (i *Instance) CreateOIDCJWTProfileClient(ctx context.Context) (machine *man
|
||||
|
||||
return machine, name, keyResp.GetKeyDetails(), nil
|
||||
}
|
||||
|
||||
func (i *Instance) CreateDeviceAuthorizationRequest(ctx context.Context, clientID string, scopes ...string) (*oidc.DeviceAuthorizationResponse, error) {
|
||||
provider, err := i.CreateRelyingParty(ctx, clientID, "", scopes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rp.DeviceAuthorization(ctx, scopes, provider, nil)
|
||||
}
|
||||
|
@ -86,15 +86,24 @@ var deviceAuthSelectColumns = []string{
|
||||
DeviceAuthRequestColumnUserCode.identifier(),
|
||||
DeviceAuthRequestColumnScopes.identifier(),
|
||||
DeviceAuthRequestColumnAudience.identifier(),
|
||||
AppColumnName.identifier(),
|
||||
ProjectColumnName.identifier(),
|
||||
}
|
||||
|
||||
func prepareDeviceAuthQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*domain.AuthRequestDevice, error)) {
|
||||
return sq.Select(deviceAuthSelectColumns...).From(deviceAuthRequestTable.identifier()).PlaceholderFormat(sq.Dollar),
|
||||
return sq.Select(deviceAuthSelectColumns...).
|
||||
From(deviceAuthRequestTable.identifier()).
|
||||
LeftJoin(join(AppOIDCConfigColumnClientID, DeviceAuthRequestColumnClientID)).
|
||||
LeftJoin(join(AppColumnID, AppOIDCConfigColumnAppID)).
|
||||
LeftJoin(join(ProjectColumnID, AppColumnProjectID)).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(row *sql.Row) (*domain.AuthRequestDevice, error) {
|
||||
dst := new(domain.AuthRequestDevice)
|
||||
var (
|
||||
scopes database.TextArray[string]
|
||||
audience database.TextArray[string]
|
||||
appName sql.NullString
|
||||
projectName sql.NullString
|
||||
)
|
||||
|
||||
err := row.Scan(
|
||||
@ -103,15 +112,20 @@ func prepareDeviceAuthQuery(ctx context.Context, db prepareDatabase) (sq.SelectB
|
||||
&dst.UserCode,
|
||||
&scopes,
|
||||
&audience,
|
||||
&appName,
|
||||
&projectName,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, zerrors.ThrowNotFound(err, "QUERY-Sah9a", "Errors.DeviceAuth.NotExisting")
|
||||
return nil, zerrors.ThrowNotFound(err, "QUERY-Sah9a", "Errors.DeviceAuth.NotFound")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-Voo3o", "Errors.Internal")
|
||||
}
|
||||
dst.Scopes = scopes
|
||||
dst.Audience = audience
|
||||
dst.AppName = appName.String
|
||||
dst.ProjectName = projectName.String
|
||||
|
||||
return dst, nil
|
||||
}
|
||||
}
|
||||
|
@ -24,8 +24,17 @@ const (
|
||||
` projections.device_auth_requests2.device_code,` +
|
||||
` projections.device_auth_requests2.user_code,` +
|
||||
` projections.device_auth_requests2.scopes,` +
|
||||
` projections.device_auth_requests2.audience` +
|
||||
` FROM projections.device_auth_requests2`
|
||||
` projections.device_auth_requests2.audience,` +
|
||||
` projections.apps7.name,` +
|
||||
` projections.projects4.name` +
|
||||
` FROM projections.device_auth_requests2` +
|
||||
` LEFT JOIN projections.apps7_oidc_configs` +
|
||||
` ON projections.device_auth_requests2.client_id = projections.apps7_oidc_configs.client_id` +
|
||||
` AND projections.device_auth_requests2.instance_id = projections.apps7_oidc_configs.instance_id` +
|
||||
` LEFT JOIN projections.apps7 ON projections.apps7_oidc_configs.app_id = projections.apps7.id` +
|
||||
` AND projections.apps7_oidc_configs.instance_id = projections.apps7.instance_id` +
|
||||
` LEFT JOIN projections.projects4 ON projections.apps7.project_id = projections.projects4.id` +
|
||||
` AND projections.apps7.instance_id = projections.projects4.instance_id`
|
||||
expectedDeviceAuthWhereUserCodeQueryC = expectedDeviceAuthQueryC +
|
||||
` WHERE projections.device_auth_requests2.instance_id = $1` +
|
||||
` AND projections.device_auth_requests2.user_code = $2`
|
||||
@ -40,6 +49,8 @@ var (
|
||||
"user-code",
|
||||
database.TextArray[string]{"a", "b", "c"},
|
||||
[]string{"projectID", "clientID"},
|
||||
"appName",
|
||||
"projectName",
|
||||
}
|
||||
expectedDeviceAuth = &domain.AuthRequestDevice{
|
||||
ClientID: "client-id",
|
||||
@ -47,6 +58,8 @@ var (
|
||||
UserCode: "user-code",
|
||||
Scopes: []string{"a", "b", "c"},
|
||||
Audience: []string{"projectID", "clientID"},
|
||||
AppName: "appName",
|
||||
ProjectName: "projectName",
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -561,6 +561,7 @@ Errors:
|
||||
AlreadyExists: Auth Request вече съществува
|
||||
NotExisting: Auth Request не съществува
|
||||
WrongLoginClient: Auth Request, създаден от друг клиент за влизане
|
||||
AlreadyHandled: Заявката за удостоверяване вече е обработена
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Токенът за опресняване е невалиден
|
||||
Token:
|
||||
@ -571,8 +572,12 @@ Errors:
|
||||
AlreadyExists: SAMLRequest вече съществува
|
||||
NotExisting: SAMLRequest не съществува
|
||||
WrongLoginClient: SAMLRequest, създаден от друг клиент за влизане
|
||||
AlreadyHandled: SAML заявката вече е обработена
|
||||
SAMLSession:
|
||||
InvalidClient: SAMLResponse не е издаден за този клиент
|
||||
DeviceAuth:
|
||||
NotFound: Заявката за авторизация на устройство не съществува
|
||||
AlreadyHandled: Заявката за авторизация на устройство вече е обработена
|
||||
Feature:
|
||||
NotExisting: Функцията не съществува
|
||||
TypeNotSupported: Типът функция не се поддържа
|
||||
|
@ -541,6 +541,7 @@ Errors:
|
||||
AlreadyExists: Požadavek na autentizaci již existuje
|
||||
NotExisting: Požadavek na autentizaci neexistuje
|
||||
WrongLoginClient: Požadavek na autentizaci vytvořen jiným klientem přihlášení
|
||||
AlreadyHandled: Žádost o ověření již byla zpracována
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Obnovovací token je neplatný
|
||||
Token:
|
||||
@ -551,8 +552,12 @@ Errors:
|
||||
AlreadyExists: SAMLRequest již existuje
|
||||
NotExisting: SAMLRequest neexistuje
|
||||
WrongLoginClient: SAMLRequest vytvořený jiným přihlašovacím klientem
|
||||
AlreadyHandled: SAML požadavek již byl zpracován
|
||||
SAMLSession:
|
||||
InvalidClient: Pro tohoto klienta nebyla vydána odpověď SAMLResponse
|
||||
DeviceAuth:
|
||||
NotFound: Žádost o autorizaci zařízení neexistuje
|
||||
AlreadyHandled: Žádost o autorizaci zařízení již byla zpracována
|
||||
Feature:
|
||||
NotExisting: Funkce neexistuje
|
||||
TypeNotSupported: Typ funkce není podporován
|
||||
|
@ -543,6 +543,7 @@ Errors:
|
||||
AlreadyExists: Auth Request existiert bereits
|
||||
NotExisting: Auth Request existiert nicht
|
||||
WrongLoginClient: Auth Request wurde von einem anderen Login-Client erstellt
|
||||
AlreadyHandled: Auth Request wurde bereits bearbeitet
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Refresh Token ist ungültig
|
||||
Token:
|
||||
@ -553,8 +554,12 @@ Errors:
|
||||
AlreadyExists: SAMLRequest existiert bereits
|
||||
NotExisting: SAMLRequest existiert nicht
|
||||
WrongLoginClient: SAMLRequest wurde con einem andere Login-Client erstellt
|
||||
AlreadyHandled: SAMLRequest wurde bereits bearbeitet
|
||||
SAMLSession:
|
||||
InvalidClient: SAMLResponse wurde nicht für diesen Client ausgestellt
|
||||
DeviceAuth:
|
||||
NotFound: Die Geräteautorisierungsanforderung existiert nicht
|
||||
AlreadyHandled: Die Geräteautorisierungsanforderung wurde bereits bearbeitet
|
||||
Feature:
|
||||
NotExisting: Feature existiert nicht
|
||||
TypeNotSupported: Feature Typ wird nicht unterstützt
|
||||
|
@ -544,6 +544,7 @@ Errors:
|
||||
AlreadyExists: Auth Request already exists
|
||||
NotExisting: Auth Request does not exist
|
||||
WrongLoginClient: Auth Request created by other login client
|
||||
AlreadyHandled: Auth Request has already been handled
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Refresh Token is invalid
|
||||
Token:
|
||||
@ -554,8 +555,12 @@ Errors:
|
||||
AlreadyExists: SAMLRequest already exists
|
||||
NotExisting: SAMLRequest does not exist
|
||||
WrongLoginClient: SAMLRequest created by other login client
|
||||
AlreadyHandled: SAMLRequest has already been handled
|
||||
SAMLSession:
|
||||
InvalidClient: SAMLResponse was not issued for this client
|
||||
DeviceAuth:
|
||||
NotFound: Device Authorization Request does not exist
|
||||
AlreadyHandled: Device Authorization Request has already been handled
|
||||
Feature:
|
||||
NotExisting: Feature does not exist
|
||||
TypeNotSupported: Feature type is not supported
|
||||
|
@ -543,6 +543,7 @@ Errors:
|
||||
AlreadyExists: Auth Request ya existe
|
||||
NotExisting: Auth Request no existe
|
||||
WrongLoginClient: Auth Request creado por otro cliente de inicio de sesión
|
||||
AlreadyHandled: Auth Request ya ha sido procesada
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: El token de refresco no es válido
|
||||
Token:
|
||||
@ -553,8 +554,12 @@ Errors:
|
||||
AlreadyExists: SAMLRequest ya existe
|
||||
NotExisting: SAMLRequest no existe
|
||||
WrongLoginClient: SAMLRequest creado por otro cliente de inicio de sesión
|
||||
AlreadyHandled: SAMLRequest ya ha sido procesada
|
||||
SAMLSession:
|
||||
InvalidClient: SAMLResponse no ha sido emitido para este cliente
|
||||
DeviceAuth:
|
||||
NotFound: La solicitud de autorización del dispositivo no existe
|
||||
AlreadyHandled: La solicitud de autorización del dispositivo ya ha sido procesada
|
||||
Feature:
|
||||
NotExisting: La característica no existe
|
||||
TypeNotSupported: El tipo de característica no es compatible
|
||||
|
@ -543,6 +543,7 @@ Errors:
|
||||
AlreadyExists: Auth Request existe déjà
|
||||
NotExisting: Auth Request n'existe pas
|
||||
WrongLoginClient: Auth Request créé par un autre client de connexion
|
||||
AlreadyHandled: Auth Request a déjà été traitée
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Le jeton de rafraîchissement n'est pas valide
|
||||
Token:
|
||||
@ -553,8 +554,12 @@ Errors:
|
||||
AlreadyExists: SAMLRequest existe déjà
|
||||
NotExisting: SAMLRequest n'existe pas
|
||||
WrongLoginClient: SAMLRequest créé par un autre client de connexion
|
||||
AlreadyHandled: SAMLRequest a déjà été traitée
|
||||
SAMLSession:
|
||||
InvalidClient: SAMLResponse n'a pas été émise pour ce client
|
||||
DeviceAuth:
|
||||
NotFound: La demande d'autorisation de l'appareil n'existe pas
|
||||
AlreadyHandled: La demande d'autorisation de l'appareil a déjà été traitée
|
||||
Feature:
|
||||
NotExisting: La fonctionnalité n'existe pas
|
||||
TypeNotSupported: Le type de fonctionnalité n'est pas pris en charge
|
||||
|
@ -543,6 +543,7 @@ Errors:
|
||||
AlreadyExists: Az Auth Request már létezik
|
||||
NotExisting: Az Auth Request nem létezik
|
||||
WrongLoginClient: Az Auth Requestet egy másik bejelentkezési kliens hozta létre
|
||||
AlreadyHandled: A hitelesítési kérelem már feldolgozva
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Az Refresh Token érvénytelen
|
||||
Token:
|
||||
@ -553,8 +554,12 @@ Errors:
|
||||
AlreadyExists: A SAMLRequest már létezik
|
||||
NotExisting: A SAMLRequest nem létezik
|
||||
WrongLoginClient: A SAMLRequest egy másik bejelentkezési ügyfél által létrehozott
|
||||
AlreadyHandled: A SAMLRequest már feldolgozva
|
||||
SAMLSession:
|
||||
InvalidClient: SAMLResponse nem lett kiadva ehhez az ügyfélhez
|
||||
DeviceAuth:
|
||||
NotFound: Az eszközengedélyezési kérelem nem létezik
|
||||
AlreadyHandled: Az eszközengedélyezési kérelem már feldolgozva
|
||||
Feature:
|
||||
NotExisting: A funkció nem létezik
|
||||
TypeNotSupported: A funkció típusa nem támogatott
|
||||
|
@ -543,6 +543,7 @@ Errors:
|
||||
AlreadyExists: Permintaan Otentikasi sudah ada
|
||||
NotExisting: Permintaan Otentikasi tidak ada
|
||||
WrongLoginClient: Permintaan Otentikasi dibuat oleh klien login lain
|
||||
AlreadyHandled: Permintaan Otentikasi sudah ditangani
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Token Penyegaran tidak valid
|
||||
Token:
|
||||
@ -553,8 +554,12 @@ Errors:
|
||||
AlreadyExists: SAMLRequest sudah ada
|
||||
NotExisting: SAMLRequest tidak ada
|
||||
WrongLoginClient: SAMLRequest dibuat oleh klien login lainnya
|
||||
AlreadyHandled: SAMLRequest sudah ditangani
|
||||
SAMLSession:
|
||||
InvalidClient: SAMLResponse tidak dikeluarkan untuk klien ini
|
||||
DeviceAuth:
|
||||
NotFound: Permintaan Otorisasi Perangkat tidak ada
|
||||
AlreadyHandled: Permintaan Otorisasi Perangkat sudah ditangani
|
||||
Feature:
|
||||
NotExisting: Fitur tidak ada
|
||||
TypeNotSupported: Jenis fitur tidak didukung
|
||||
|
@ -543,6 +543,7 @@ Errors:
|
||||
AlreadyExists: Auth Request esiste già
|
||||
NotExisting: Auth Request non esiste
|
||||
WrongLoginClient: Auth Request creato da un altro client di accesso
|
||||
AlreadyHandled: Auth Request è già stata gestita
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Refresh Token non è valido
|
||||
Token:
|
||||
@ -553,8 +554,12 @@ Errors:
|
||||
AlreadyExists: SAMLRequest esiste già
|
||||
NotExisting: SAMLRequest non esiste
|
||||
WrongLoginClient: SAMLRequest creato da un altro client di accesso
|
||||
AlreadyHandled: SAMLRequest è già stata gestita
|
||||
SAMLSession:
|
||||
InvalidClient: SAMLResponse non è stato emesso per questo client
|
||||
DeviceAuth:
|
||||
NotFound: La richiesta di autorizzazione del dispositivo non esiste
|
||||
AlreadyHandled: La richiesta di autorizzazione del dispositivo è già stata gestita
|
||||
Feature:
|
||||
NotExisting: La funzionalità non esiste
|
||||
TypeNotSupported: Il tipo di funzionalità non è supportato
|
||||
|
@ -544,6 +544,7 @@ Errors:
|
||||
AlreadyExists: AuthRequestはすでに存在する
|
||||
NotExisting: AuthRequest が存在しません
|
||||
WrongLoginClient: 他のログインクライアントによって作成された AuthRequest
|
||||
AlreadyHandled: 認証リクエストは既に処理済みです
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: 無効なリフレッシュトークンです
|
||||
Token:
|
||||
@ -554,8 +555,12 @@ Errors:
|
||||
AlreadyExists: SAMLリクエストはすでに存在します
|
||||
NotExisting: SAMLリクエストが存在しません
|
||||
WrongLoginClient: 他のログイン クライアントによって作成された SAMLRequest
|
||||
AlreadyHandled: SAMLリクエストは既に処理済みです
|
||||
SAMLSession:
|
||||
InvalidClient: このクライアントに対してSAMLResponseは発行されませんでした
|
||||
DeviceAuth:
|
||||
NotFound: デバイス認証リクエストが存在しません
|
||||
AlreadyHandled: デバイス認証リクエストは既に処理済みです
|
||||
Feature:
|
||||
NotExisting: 機能が存在しません
|
||||
TypeNotSupported: 機能タイプはサポートされていません
|
||||
|
@ -544,6 +544,7 @@ Errors:
|
||||
AlreadyExists: 인증 요청이 이미 존재합니다
|
||||
NotExisting: 인증 요청이 존재하지 않습니다
|
||||
WrongLoginClient: 다른 로그인 클라이언트에 의해 생성된 인증 요청
|
||||
AlreadyHandled: 인증 요청이 이미 처리되었습니다
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: 새로 고침 토큰이 유효하지 않습니다
|
||||
Token:
|
||||
@ -554,8 +555,12 @@ Errors:
|
||||
AlreadyExists: SAMLRequest가 이미 존재합니다
|
||||
NotExisting: SAMLRequest가 존재하지 않습니다
|
||||
WrongLoginClient: 다른 로그인 클라이언트가 생성한 SAMLRequest
|
||||
AlreadyHandled: SAML 요청이 이미 처리되었습니다
|
||||
SAMLSession:
|
||||
InvalidClient: 이 클라이언트에 대해 SAMLResponse가 발행되지 않았습니다.
|
||||
DeviceAuth:
|
||||
NotFound: 장치 인증 요청이 존재하지 않습니다
|
||||
AlreadyHandled: 장치 인증 요청이 이미 처리되었습니다
|
||||
Feature:
|
||||
NotExisting: 기능이 존재하지 않습니다
|
||||
TypeNotSupported: 기능 유형이 지원되지 않습니다
|
||||
|
@ -542,6 +542,7 @@ Errors:
|
||||
AlreadyExists: Барањето за автентикација веќе постои
|
||||
NotExisting: Барањето за автентикација не постои
|
||||
WrongLoginClient: Барањето за автификација беше креирано од друг клиент за најавување
|
||||
AlreadyHandled: Барањето за автентикација е веќе обработено
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Токенот за освежување е неважечки
|
||||
Token:
|
||||
@ -552,8 +553,12 @@ Errors:
|
||||
AlreadyExists: SAMLRequest веќе постои
|
||||
NotExisting: SAMLRequest не постои
|
||||
WrongLoginClient: SAML Барање создадено од друг клиент за најавување
|
||||
AlreadyHandled: SAML барањето е веќе обработено
|
||||
SAMLSession:
|
||||
InvalidClient: SAMLResponse не беше издаден за овој клиент
|
||||
DeviceAuth:
|
||||
NotFound: Барањето за авторизација на уредот не постои
|
||||
AlreadyHandled: Барањето за авторизација на уредот е веќе обработено
|
||||
Feature:
|
||||
NotExisting: Функцијата не постои
|
||||
TypeNotSupported: Типот на функција не е поддржан
|
||||
|
@ -543,6 +543,7 @@ Errors:
|
||||
AlreadyExists: Auth Verzoek bestaat al
|
||||
NotExisting: Auth Verzoek bestaat niet
|
||||
WrongLoginClient: Auth Verzoek aangemaakt door andere login client
|
||||
AlreadyHandled: Authenticatieverzoek is al verwerkt
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Refresh Token is ongeldig
|
||||
Token:
|
||||
@ -553,8 +554,12 @@ Errors:
|
||||
AlreadyExists: SAMLRequest bestaat al
|
||||
NotExisting: SAMLRequest bestaat niet
|
||||
WrongLoginClient: SAMLRequest aangemaakt door andere login client
|
||||
AlreadyHandled: SAML-verzoek is al verwerkt
|
||||
SAMLSession:
|
||||
InvalidClient: SAMLResponse is niet uitgegeven voor deze client
|
||||
DeviceAuth:
|
||||
NotFound: Apparaatautorisatieverzoek bestaat niet
|
||||
AlreadyHandled: Apparaatautorisatieverzoek is al verwerkt
|
||||
Feature:
|
||||
NotExisting: Functie bestaat niet
|
||||
TypeNotSupported: Functie type wordt niet ondersteund
|
||||
|
@ -543,6 +543,7 @@ Errors:
|
||||
AlreadyExists: Auth Request już istnieje
|
||||
NotExisting: Auth Request nie istnieje
|
||||
WrongLoginClient: Auth Request utworzony przez innego klienta logowania
|
||||
AlreadyHandled: Żądanie uwierzytelnienia zostało już obsłużone
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Refresh Token jest nieprawidłowy
|
||||
Token:
|
||||
@ -553,8 +554,12 @@ Errors:
|
||||
AlreadyExists: SAMLRequest już istnieje
|
||||
NotExisting: SAMLRequest nie istnieje
|
||||
WrongLoginClient: SAMLRequest utworzony przez innego klienta logowania
|
||||
AlreadyHandled: Żądanie SAML zostało już obsłużone
|
||||
SAMLSession:
|
||||
InvalidClient: SAMLResponse nie został wydany dla tego klienta
|
||||
DeviceAuth:
|
||||
NotFound: Żądanie autoryzacji urządzenia nie istnieje
|
||||
AlreadyHandled: Żądanie autoryzacji urządzenia zostało już obsłużone
|
||||
Feature:
|
||||
NotExisting: Funkcja nie istnieje
|
||||
TypeNotSupported: Typ funkcji nie jest obsługiwany
|
||||
|
@ -542,6 +542,7 @@ Errors:
|
||||
AlreadyExists: A solicitação de autenticação já existe
|
||||
NotExisting: A solicitação de autenticação não existe
|
||||
WrongLoginClient: A solicitação de autenticação foi criada por outro cliente de login
|
||||
AlreadyHandled: O pedido de autenticação já foi processado
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: O Refresh Token é inválido
|
||||
Token:
|
||||
@ -552,8 +553,12 @@ Errors:
|
||||
AlreadyExists: O SAMLRequest já existe
|
||||
NotExisting: O SAMLRequest não existe
|
||||
WrongLoginClient: SAMLRequest criado por outro cliente de login
|
||||
AlreadyHandled: O pedido SAML já foi processado
|
||||
SAMLSession:
|
||||
InvalidClient: O SAMLResponse não foi emitido para este cliente
|
||||
DeviceAuth:
|
||||
NotFound: O pedido de autorização do dispositivo não existe
|
||||
AlreadyHandled: O pedido de autorização do dispositivo já foi processado
|
||||
Feature:
|
||||
NotExisting: O recurso não existe
|
||||
TypeNotSupported: O tipo de recurso não é compatível
|
||||
|
@ -532,6 +532,7 @@ Errors:
|
||||
AlreadyExists: Запрос на аутентификацию уже существует
|
||||
NotExisting: Запрос на аутентификацию не существует
|
||||
WrongLoginClient: Запрос на аутентификацию, созданный другим клиентом входа
|
||||
AlreadyHandled: Запрос аутентификации уже обработан
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Маркер обновления недействителен
|
||||
Token:
|
||||
@ -542,8 +543,12 @@ Errors:
|
||||
AlreadyExists: SAMLRequest уже существует
|
||||
NotExisting: SAMLRequest не существует
|
||||
WrongLoginClient: SAMLRequest создан другим клиентом входа
|
||||
AlreadyHandled: Запрос SAML уже обработан
|
||||
SAMLSession:
|
||||
InvalidClient: SAMLResponse не был отправлен для этого клиента
|
||||
DeviceAuth:
|
||||
NotFound: Запрос авторизации устройства не существует
|
||||
AlreadyHandled: Запрос авторизации устройства уже обработан
|
||||
Feature:
|
||||
NotExisting: ункция не существует
|
||||
TypeNotSupported: Тип объекта не поддерживается
|
||||
|
@ -543,6 +543,7 @@ Errors:
|
||||
AlreadyExists: Autentiseringsbegäran finns redan
|
||||
NotExisting: Autentiseringsbegäran existerar inte
|
||||
WrongLoginClient: Autentiseringsbegäran skapad av annan inloggningsklient
|
||||
AlreadyHandled: Autentiseringsbegäran har redan hanterats
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Uppdateringstoken är ogiltig
|
||||
Token:
|
||||
@ -553,8 +554,12 @@ Errors:
|
||||
AlreadyExists: SAMLRequest finns redan
|
||||
NotExisting: SAMLRequest finns inte
|
||||
WrongLoginClient: SAMLRequest skapad av annan inloggningsklient
|
||||
AlreadyHandled: SAML-begäran har redan hanterats
|
||||
SAMLSession:
|
||||
InvalidClient: SAMLResponse utfärdades inte för den här klienten
|
||||
DeviceAuth:
|
||||
NotFound: Begäran om enhetsauktorisering finns inte
|
||||
AlreadyHandled: Begäran om enhetsauktorisering har redan hanterats
|
||||
Feature:
|
||||
NotExisting: Funktionen existerar inte
|
||||
TypeNotSupported: Funktionstypen stöds inte
|
||||
|
@ -543,6 +543,7 @@ Errors:
|
||||
AlreadyExists: AuthRequest已经存在
|
||||
NotExisting: AuthRequest不存在
|
||||
WrongLoginClient: 其他登录客户端创建的AuthRequest
|
||||
AlreadyHandled: 身份验证请求已被处理
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Refresh Token 无效
|
||||
Token:
|
||||
@ -553,8 +554,12 @@ Errors:
|
||||
AlreadyExists: SAMLRequest 已存在
|
||||
NotExisting: SAMLRequest不存在
|
||||
WrongLoginClient: 其他登录客户端创建的 SAMLRequest
|
||||
AlreadyHandled: SAML请求已被处理
|
||||
SAMLSession:
|
||||
InvalidClient: 未向该客户端发出 SAMLResponse
|
||||
DeviceAuth:
|
||||
NotFound: 设备授权请求不存在
|
||||
AlreadyHandled: 设备授权请求已被处理
|
||||
Feature:
|
||||
NotExisting: 功能不存在
|
||||
TypeNotSupported: 不支持功能类型
|
||||
|
@ -115,3 +115,16 @@ enum ErrorReason {
|
||||
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;
|
||||
}
|
@ -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 {}
|
Loading…
x
Reference in New Issue
Block a user