Skip to content

Commit e41b70b

Browse files
livio-aCopilot
andauthored
feat(api): allow specifying access token type for user of type machine (zitadel#11599)
# Which Problems Are Solved The user service v2 didn't allow the specify or change the access token type for users of type machine and referred to using the management API, which in return was already deprecated and linked to the user service. # How the Problems Are Solved Added a possibility to specify the access token type in the creation and update request. # Additional Changes None # Additional Context - closes zitadel#10850 - requires backport to v4.x --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent a31e435 commit e41b70b

6 files changed

Lines changed: 176 additions & 18 deletions

File tree

internal/api/grpc/user/v2/integration_test/user_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3639,6 +3639,28 @@ func TestServer_CreateUser_And_Compare(t *testing.T) {
36393639
},
36403640
}
36413641
},
3642+
}, {
3643+
name: "machine access token type jwt",
3644+
testCase: func(runId string) testCase {
3645+
return testCase{
3646+
args: args{
3647+
ctx: OrgCTX,
3648+
req: &user.CreateUserRequest{
3649+
OrganizationId: Instance.DefaultOrg.Id,
3650+
UserId: &runId,
3651+
UserType: &user.CreateUserRequest_Machine_{
3652+
Machine: &user.CreateUserRequest_Machine{
3653+
Name: "donald",
3654+
AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT,
3655+
},
3656+
},
3657+
},
3658+
},
3659+
assert: func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) {
3660+
assert.Equal(t, user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, getResponse.GetUser().GetMachine().GetAccessTokenType())
3661+
},
3662+
}
3663+
},
36423664
}}
36433665
for i, tt := range tests {
36443666
t.Run(tt.name, func(t *testing.T) {
@@ -4308,6 +4330,35 @@ func TestServer_UpdateUser_And_Compare(t *testing.T) {
43084330
},
43094331
}
43104332
},
4333+
}, {
4334+
name: "machine accessTokenType",
4335+
testCase: func(runId string) testCase {
4336+
return testCase{
4337+
args: args{
4338+
ctx: OrgCTX,
4339+
create: &user.CreateUserRequest{
4340+
OrganizationId: Instance.DefaultOrg.Id,
4341+
UserId: &runId,
4342+
UserType: &user.CreateUserRequest_Machine_{
4343+
Machine: &user.CreateUserRequest_Machine{
4344+
Name: "Donald",
4345+
},
4346+
},
4347+
},
4348+
update: &user.UpdateUserRequest{
4349+
UserId: runId,
4350+
UserType: &user.UpdateUserRequest_Machine_{
4351+
Machine: &user.UpdateUserRequest_Machine{
4352+
AccessTokenType: gu.Ptr(user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT),
4353+
},
4354+
},
4355+
},
4356+
},
4357+
assert: func(t *testing.T, getResponse *user.GetUserByIDResponse) {
4358+
assert.Equal(t, user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, getResponse.GetUser().GetMachine().GetAccessTokenType())
4359+
},
4360+
}
4361+
},
43114362
}}
43124363
for i, tt := range tests {
43134364
t.Run(tt.name, func(t *testing.T) {

internal/api/grpc/user/v2/machine.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func (s *Server) createUserTypeMachine(ctx context.Context, machinePb *user.Crea
1717
Username: userName,
1818
Name: machinePb.Name,
1919
Description: machinePb.GetDescription(),
20-
AccessTokenType: domain.OIDCTokenTypeBearer,
20+
AccessTokenType: accessTokenTypeToDomain(machinePb.GetAccessTokenType()),
2121
ObjectRoot: models.ObjectRoot{
2222
ResourceOwner: orgId,
2323
AggregateID: userId,
@@ -51,10 +51,27 @@ func (s *Server) updateUserTypeMachine(ctx context.Context, machinePb *user.Upda
5151
}
5252

5353
func updateMachineUserToCommand(userId string, userName *string, machine *user.UpdateUserRequest_Machine) *command.ChangeMachine {
54+
var accessTokenType *domain.OIDCTokenType
55+
if machine.AccessTokenType != nil {
56+
tokenType := accessTokenTypeToDomain(*machine.AccessTokenType)
57+
accessTokenType = &tokenType
58+
}
5459
return &command.ChangeMachine{
55-
ID: userId,
56-
Username: userName,
57-
Name: machine.Name,
58-
Description: machine.Description,
60+
ID: userId,
61+
Username: userName,
62+
Name: machine.Name,
63+
Description: machine.Description,
64+
AccessTokenType: accessTokenType,
65+
}
66+
}
67+
68+
func accessTokenTypeToDomain(accessTokenType user.AccessTokenType) domain.OIDCTokenType {
69+
switch accessTokenType {
70+
case user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER:
71+
return domain.OIDCTokenTypeBearer
72+
case user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT:
73+
return domain.OIDCTokenTypeJWT
74+
default:
75+
return domain.OIDCTokenTypeBearer
5976
}
6077
}

internal/api/grpc/user/v2/machine_test.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"golang.org/x/text/language"
1010

1111
"github.com/zitadel/zitadel/internal/command"
12+
"github.com/zitadel/zitadel/internal/domain"
1213
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
1314
)
1415

@@ -40,15 +41,17 @@ func Test_patchMachineUserToCommand(t *testing.T) {
4041
userId: "userId",
4142
userName: gu.Ptr("userName"),
4243
machine: &user.UpdateUserRequest_Machine{
43-
Name: gu.Ptr("name"),
44-
Description: gu.Ptr("description"),
44+
Name: gu.Ptr("name"),
45+
Description: gu.Ptr("description"),
46+
AccessTokenType: gu.Ptr(user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT),
4547
},
4648
},
4749
want: &command.ChangeMachine{
48-
ID: "userId",
49-
Username: gu.Ptr("userName"),
50-
Name: gu.Ptr("name"),
51-
Description: gu.Ptr("description"),
50+
ID: "userId",
51+
Username: gu.Ptr("userName"),
52+
Name: gu.Ptr("name"),
53+
Description: gu.Ptr("description"),
54+
AccessTokenType: gu.Ptr(domain.OIDCTokenTypeJWT),
5255
},
5356
}}
5457
for _, tt := range tests {

internal/command/user_v2_machine.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ import (
1111
)
1212

1313
type ChangeMachine struct {
14-
ID string
15-
ResourceOwner string
16-
Username *string
17-
Name *string
18-
Description *string
14+
ID string
15+
ResourceOwner string
16+
Username *string
17+
Name *string
18+
Description *string
19+
AccessTokenType *domain.OIDCTokenType
1920

2021
// Details are set after a successful execution of the command
2122
Details *domain.ObjectDetails
@@ -31,6 +32,9 @@ func (h *ChangeMachine) Changed() bool {
3132
if h.Description != nil {
3233
return true
3334
}
35+
if h.AccessTokenType != nil {
36+
return true
37+
}
3438
return false
3539
}
3640

@@ -64,6 +68,9 @@ func (c *Commands) ChangeUserMachine(ctx context.Context, machine *ChangeMachine
6468
if machine.Description != nil && *machine.Description != existingMachine.Description {
6569
machineChanges = append(machineChanges, user.ChangeDescription(*machine.Description))
6670
}
71+
if machine.AccessTokenType != nil && *machine.AccessTokenType != existingMachine.AccessTokenType {
72+
machineChanges = append(machineChanges, user.ChangeAccessTokenType(*machine.AccessTokenType))
73+
}
6774
if len(machineChanges) > 0 {
6875
cmds = append(cmds, user.NewMachineChangedEvent(ctx, &existingMachine.Aggregate().Aggregate, machineChanges))
6976
}

internal/command/user_v2_machine_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,60 @@ func TestCommandSide_ChangeUserMachine(t *testing.T) {
284284
},
285285
},
286286
},
287+
{
288+
name: "change machine accessTokenType, ok",
289+
fields: fields{
290+
eventstore: expectEventstore(
291+
expectFilter(
292+
eventFromEventPusher(userAddedEvent),
293+
),
294+
expectPush(
295+
user.NewMachineChangedEvent(context.Background(),
296+
&userAgg.Aggregate,
297+
[]user.MachineChanges{
298+
user.ChangeAccessTokenType(domain.OIDCTokenTypeJWT),
299+
},
300+
),
301+
),
302+
),
303+
checkPermission: newMockPermissionCheckAllowed(),
304+
},
305+
args: args{
306+
ctx: context.Background(),
307+
orgID: "org1",
308+
machine: &ChangeMachine{
309+
AccessTokenType: gu.Ptr(domain.OIDCTokenTypeJWT),
310+
},
311+
},
312+
res: res{
313+
want: &domain.ObjectDetails{
314+
ResourceOwner: "org1",
315+
},
316+
},
317+
},
318+
{
319+
name: "change machine accessTokenType, no change",
320+
fields: fields{
321+
eventstore: expectEventstore(
322+
expectFilter(
323+
eventFromEventPusher(userAddedEvent),
324+
),
325+
),
326+
checkPermission: newMockPermissionCheckAllowed(),
327+
},
328+
args: args{
329+
ctx: context.Background(),
330+
orgID: "org1",
331+
machine: &ChangeMachine{
332+
AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
333+
},
334+
},
335+
res: res{
336+
want: &domain.ObjectDetails{
337+
ResourceOwner: "org1",
338+
},
339+
},
340+
},
287341
}
288342
for _, tt := range tests {
289343
t.Run(tt.name, func(t *testing.T) {

proto/zitadel/user/v2/user_service.proto

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2112,6 +2112,19 @@ message CreateUserRequest{
21122112
example: "\"The user calls the session API in the continuous integration pipeline for acceptance tests.\"";
21132113
}
21142114
];
2115+
// The access token type defines the type of access token that is generated for the user.
2116+
// By default, the access token type is an opaque Bearer token. You can change it to JWT,
2117+
// which will include more information about the user and their permissions in the token itself.
2118+
// This can be useful for machine users that need to call services that require more information about the user,
2119+
// without having to call the userinfo or introspection endpoint.
2120+
// However, revoked JWTs may still be considered valid until they expire.
2121+
// Always call the userinfo or introspection endpoint to verify that a JWT is still valid.
2122+
AccessTokenType access_token_type = 3 [
2123+
(validate.rules).enum = {defined_only: true},
2124+
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
2125+
example: "\"ACCESS_TOKEN_TYPE_BEARER\"";
2126+
}
2127+
];
21152128
}
21162129
// The unique identifier of the organization the user belongs to.
21172130
string organization_id = 1 [
@@ -2160,8 +2173,8 @@ message CreateUserRequest{
21602173
Human human = 4;
21612174
// Users of type machine are users that are meant to be used by a machine.
21622175
// 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.
2163-
// Tokens generated for new users of type machine will be of an opaque Bearer type.
2164-
// You can change the users token type to JWT by using the management v1 service method UpdateMachine.
2176+
// By default, tokens generated for new users of type machine will be of an opaque Bearer type.
2177+
// You can change it to JWT by specifying the access token type in the machine field.
21652178
Machine machine = 5;
21662179
}
21672180
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
@@ -2543,6 +2556,19 @@ message UpdateUserRequest{
25432556
example: "\"The user calls the session API in the continuous integration pipeline for acceptance tests.\"";
25442557
}
25452558
];
2559+
// The access token type defines the type of access token that is generated for the user.
2560+
// By default, the access token type is an opaque Bearer token. You can change it to JWT,
2561+
// which will include more information about the user and their permissions in the token itself.
2562+
// This can be useful for machine users that need to call services that require more information about the user,
2563+
// without having to call the userinfo or introspection endpoint.
2564+
// However, revoked JWT tokens might still be considered valid until they expire.
2565+
// Therefore, you must call the userinfo or introspection endpoint to check whether they are still valid.
2566+
optional AccessTokenType access_token_type = 3 [
2567+
(validate.rules).enum = {defined_only: true},
2568+
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
2569+
example: "\"ACCESS_TOKEN_TYPE_BEARER\"";
2570+
}
2571+
];
25462572
}
25472573
// The user id is the users unique identifier in the instance.
25482574
// It can't be changed.

0 commit comments

Comments
 (0)