zitadel/internal/api/oidc/integration_test/token_device_test.go

128 lines
4.3 KiB
Go
Raw Permalink Normal View History

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>
2025-02-25 07:33:13 +01:00
//go:build integration
package oidc_test
import (
"context"
"slices"
"testing"
"time"
feat: project v2beta resource API (#9742) # Which Problems Are Solved Resource management of projects and sub-resources was before limited by the context provided by the management API, which would mean you could only manage resources belonging to a specific organization. # How the Problems Are Solved With the addition of a resource-based API, it is now possible to manage projects and sub-resources on the basis of the resources themselves, which means that as long as you have the permission for the resource, you can create, read, update and delete it. - CreateProject to create a project under an organization - UpdateProject to update an existing project - DeleteProject to delete an existing project - DeactivateProject and ActivateProject to change the status of a project - GetProject to query for a specific project with an identifier - ListProject to query for projects and granted projects - CreateProjectGrant to create a project grant with project and granted organization - UpdateProjectGrant to update the roles of a project grant - DeactivateProjectGrant and ActivateProjectGrant to change the status of a project grant - DeleteProjectGrant to delete an existing project grant - ListProjectGrants to query for project grants - AddProjectRole to add a role to an existing project - UpdateProjectRole to change texts of an existing role - RemoveProjectRole to remove an existing role - ListProjectRoles to query for project roles # Additional Changes - Changes to ListProjects, which now contains granted projects as well - Changes to messages as defined in the [API_DESIGN](https://github.com/zitadel/zitadel/blob/main/API_DESIGN.md) - Permission checks for project functionality on query and command side - Added testing to unit tests on command side - Change update endpoints to no error returns if nothing changes in the resource - Changed all integration test utility to the new service - ListProjects now also correctly lists `granted projects` - Permission checks for project grant and project role functionality on query and command side - Change existing pre checks so that they also work resource specific without resourceowner - Added the resourceowner to the grant and role if no resourceowner is provided - Corrected import tests with project grants and roles - Added testing to unit tests on command side - Change update endpoints to no error returns if nothing changes in the resource - Changed all integration test utility to the new service - Corrected some naming in the proto files to adhere to the API_DESIGN # Additional Context Closes #9177 --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
2025-05-21 14:40:47 +02:00
"github.com/brianvoe/gofakeit/v6"
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>
2025-02-25 07:33:13 +01:00
"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) {
feat: project v2beta resource API (#9742) # Which Problems Are Solved Resource management of projects and sub-resources was before limited by the context provided by the management API, which would mean you could only manage resources belonging to a specific organization. # How the Problems Are Solved With the addition of a resource-based API, it is now possible to manage projects and sub-resources on the basis of the resources themselves, which means that as long as you have the permission for the resource, you can create, read, update and delete it. - CreateProject to create a project under an organization - UpdateProject to update an existing project - DeleteProject to delete an existing project - DeactivateProject and ActivateProject to change the status of a project - GetProject to query for a specific project with an identifier - ListProject to query for projects and granted projects - CreateProjectGrant to create a project grant with project and granted organization - UpdateProjectGrant to update the roles of a project grant - DeactivateProjectGrant and ActivateProjectGrant to change the status of a project grant - DeleteProjectGrant to delete an existing project grant - ListProjectGrants to query for project grants - AddProjectRole to add a role to an existing project - UpdateProjectRole to change texts of an existing role - RemoveProjectRole to remove an existing role - ListProjectRoles to query for project roles # Additional Changes - Changes to ListProjects, which now contains granted projects as well - Changes to messages as defined in the [API_DESIGN](https://github.com/zitadel/zitadel/blob/main/API_DESIGN.md) - Permission checks for project functionality on query and command side - Added testing to unit tests on command side - Change update endpoints to no error returns if nothing changes in the resource - Changed all integration test utility to the new service - ListProjects now also correctly lists `granted projects` - Permission checks for project grant and project role functionality on query and command side - Change existing pre checks so that they also work resource specific without resourceowner - Added the resourceowner to the grant and role if no resourceowner is provided - Corrected import tests with project grants and roles - Added testing to unit tests on command side - Change update endpoints to no error returns if nothing changes in the resource - Changed all integration test utility to the new service - Corrected some naming in the proto files to adhere to the API_DESIGN # Additional Context Closes #9177 --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
2025-05-21 14:40:47 +02:00
project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false)
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>
2025-02-25 07:33:13 +01:00
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
})
}
}