diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go index 12b18daf0..9d586383b 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -3639,6 +3639,28 @@ func TestServer_CreateUser_And_Compare(t *testing.T) { }, } }, + }, { + name: "machine access token type jwt", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + ctx: OrgCTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, + }, + }, + }, + }, + assert: func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, getResponse.GetUser().GetMachine().GetAccessTokenType()) + }, + } + }, }} for i, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -4308,6 +4330,35 @@ func TestServer_UpdateUser_And_Compare(t *testing.T) { }, } }, + }, { + name: "machine accessTokenType", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + ctx: OrgCTX, + create: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "Donald", + }, + }, + }, + update: &user.UpdateUserRequest{ + UserId: runId, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + AccessTokenType: gu.Ptr(user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT), + }, + }, + }, + }, + assert: func(t *testing.T, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, getResponse.GetUser().GetMachine().GetAccessTokenType()) + }, + } + }, }} for i, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/api/grpc/user/v2/machine.go b/internal/api/grpc/user/v2/machine.go index 7889d2c6d..a3823058e 100644 --- a/internal/api/grpc/user/v2/machine.go +++ b/internal/api/grpc/user/v2/machine.go @@ -17,7 +17,7 @@ func (s *Server) createUserTypeMachine(ctx context.Context, machinePb *user.Crea Username: userName, Name: machinePb.Name, Description: machinePb.GetDescription(), - AccessTokenType: domain.OIDCTokenTypeBearer, + AccessTokenType: accessTokenTypeToDomain(machinePb.GetAccessTokenType()), ObjectRoot: models.ObjectRoot{ ResourceOwner: orgId, AggregateID: userId, @@ -51,10 +51,27 @@ func (s *Server) updateUserTypeMachine(ctx context.Context, machinePb *user.Upda } func updateMachineUserToCommand(userId string, userName *string, machine *user.UpdateUserRequest_Machine) *command.ChangeMachine { + var accessTokenType *domain.OIDCTokenType + if machine.AccessTokenType != nil { + tokenType := accessTokenTypeToDomain(*machine.AccessTokenType) + accessTokenType = &tokenType + } return &command.ChangeMachine{ - ID: userId, - Username: userName, - Name: machine.Name, - Description: machine.Description, + ID: userId, + Username: userName, + Name: machine.Name, + Description: machine.Description, + AccessTokenType: accessTokenType, + } +} + +func accessTokenTypeToDomain(accessTokenType user.AccessTokenType) domain.OIDCTokenType { + switch accessTokenType { + case user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER: + return domain.OIDCTokenTypeBearer + case user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT: + return domain.OIDCTokenTypeJWT + default: + return domain.OIDCTokenTypeBearer } } diff --git a/internal/api/grpc/user/v2/machine_test.go b/internal/api/grpc/user/v2/machine_test.go index 96d77d8fa..a5dc710b1 100644 --- a/internal/api/grpc/user/v2/machine_test.go +++ b/internal/api/grpc/user/v2/machine_test.go @@ -9,6 +9,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) @@ -40,15 +41,17 @@ func Test_patchMachineUserToCommand(t *testing.T) { userId: "userId", userName: gu.Ptr("userName"), machine: &user.UpdateUserRequest_Machine{ - Name: gu.Ptr("name"), - Description: gu.Ptr("description"), + Name: gu.Ptr("name"), + Description: gu.Ptr("description"), + AccessTokenType: gu.Ptr(user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT), }, }, want: &command.ChangeMachine{ - ID: "userId", - Username: gu.Ptr("userName"), - Name: gu.Ptr("name"), - Description: gu.Ptr("description"), + ID: "userId", + Username: gu.Ptr("userName"), + Name: gu.Ptr("name"), + Description: gu.Ptr("description"), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeJWT), }, }} for _, tt := range tests { diff --git a/internal/command/user_v2_machine.go b/internal/command/user_v2_machine.go index 3de1ea9af..c391d6a3a 100644 --- a/internal/command/user_v2_machine.go +++ b/internal/command/user_v2_machine.go @@ -11,11 +11,12 @@ import ( ) type ChangeMachine struct { - ID string - ResourceOwner string - Username *string - Name *string - Description *string + ID string + ResourceOwner string + Username *string + Name *string + Description *string + AccessTokenType *domain.OIDCTokenType // Details are set after a successful execution of the command Details *domain.ObjectDetails @@ -31,6 +32,9 @@ func (h *ChangeMachine) Changed() bool { if h.Description != nil { return true } + if h.AccessTokenType != nil { + return true + } return false } @@ -64,6 +68,9 @@ func (c *Commands) ChangeUserMachine(ctx context.Context, machine *ChangeMachine if machine.Description != nil && *machine.Description != existingMachine.Description { machineChanges = append(machineChanges, user.ChangeDescription(*machine.Description)) } + if machine.AccessTokenType != nil && *machine.AccessTokenType != existingMachine.AccessTokenType { + machineChanges = append(machineChanges, user.ChangeAccessTokenType(*machine.AccessTokenType)) + } if len(machineChanges) > 0 { cmds = append(cmds, user.NewMachineChangedEvent(ctx, &existingMachine.Aggregate().Aggregate, machineChanges)) } diff --git a/internal/command/user_v2_machine_test.go b/internal/command/user_v2_machine_test.go index c5883286b..831ebdd3d 100644 --- a/internal/command/user_v2_machine_test.go +++ b/internal/command/user_v2_machine_test.go @@ -284,6 +284,60 @@ func TestCommandSide_ChangeUserMachine(t *testing.T) { }, }, }, + { + name: "change machine accessTokenType, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + expectPush( + user.NewMachineChangedEvent(context.Background(), + &userAgg.Aggregate, + []user.MachineChanges{ + user.ChangeAccessTokenType(domain.OIDCTokenTypeJWT), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeJWT), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "change machine accessTokenType, no change", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index f8c33646d..87c181713 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -2112,6 +2112,19 @@ message CreateUserRequest{ example: "\"The user calls the session API in the continuous integration pipeline for acceptance tests.\""; } ]; + // The access token type defines the type of access token that is generated for the user. + // By default, the access token type is an opaque Bearer token. You can change it to JWT, + // which will include more information about the user and their permissions in the token itself. + // This can be useful for machine users that need to call services that require more information about the user, + // without having to call the userinfo or introspection endpoint. + // However, revoked JWTs may still be considered valid until they expire. + // Always call the userinfo or introspection endpoint to verify that a JWT is still valid. + AccessTokenType access_token_type = 3 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ACCESS_TOKEN_TYPE_BEARER\""; + } + ]; } // The unique identifier of the organization the user belongs to. string organization_id = 1 [ @@ -2160,8 +2173,8 @@ message CreateUserRequest{ Human human = 4; // Users of type machine are users that are meant to be used by a machine. // In order to authenticate, [add a secret](/docs/reference/api/user/zitadel.user.v2.UserService.AddSecret), [a key](/docs/reference/api/user/zitadel.user.v2.UserService.AddKey) or [a personal access token](/docs/reference/api/user/zitadel.user.v2.UserService.AddPersonalAccessToken) to the user. - // Tokens generated for new users of type machine will be of an opaque Bearer type. - // You can change the users token type to JWT by using the management v1 service method UpdateMachine. + // By default, tokens generated for new users of type machine will be of an opaque Bearer type. + // You can change it to JWT by specifying the access token type in the machine field. Machine machine = 5; } option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { @@ -2543,6 +2556,19 @@ message UpdateUserRequest{ example: "\"The user calls the session API in the continuous integration pipeline for acceptance tests.\""; } ]; + // The access token type defines the type of access token that is generated for the user. + // By default, the access token type is an opaque Bearer token. You can change it to JWT, + // which will include more information about the user and their permissions in the token itself. + // This can be useful for machine users that need to call services that require more information about the user, + // without having to call the userinfo or introspection endpoint. + // However, revoked JWT tokens might still be considered valid until they expire. + // Therefore, you must call the userinfo or introspection endpoint to check whether they are still valid. + optional AccessTokenType access_token_type = 3 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ACCESS_TOKEN_TYPE_BEARER\""; + } + ]; } // The user id is the users unique identifier in the instance. // It can't be changed.