feat: invite user link (#8578)

# Which Problems Are Solved

As an administrator I want to be able to invite users to my application
with the API V2, some user data I will already prefil, the user should
add the authentication method themself (password, passkey, sso).

# How the Problems Are Solved

- A user can now be created with a email explicitly set to false.
- If a user has no verified email and no authentication method, an
`InviteCode` can be created through the User V2 API.
  - the code can be returned or sent through email
- additionally `URLTemplate` and an `ApplicatioName` can provided for
the email
- The code can be resent and verified through the User V2 API
- The V1 login allows users to verify and resend the code and set a
password (analog user initialization)
- The message text for the user invitation can be customized

# Additional Changes

- `verifyUserPasskeyCode` directly uses `crypto.VerifyCode` (instead of
`verifyEncryptedCode`)
- `verifyEncryptedCode` is removed (unnecessarily queried for the code
generator)

# Additional Context

- closes #8310
- TODO: login V2 will have to implement invite flow:
https://github.com/zitadel/typescript/issues/166
This commit is contained in:
Livio Spring
2024-09-11 12:53:55 +02:00
committed by GitHub
parent 02c78a19c6
commit a07b2f4677
114 changed files with 3898 additions and 293 deletions

View File

@@ -3588,6 +3588,71 @@ service AdminService {
};
}
rpc GetDefaultInviteUserMessageText(GetDefaultInviteUserMessageTextRequest) returns (GetDefaultInviteUserMessageTextResponse) {
option (google.api.http) = {
get: "/text/default/message/invite_user/{language}";
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.read";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "Message Texts";
summary: "Get Default Invite User Message Text";
description: "Get the default text of the invite user message/email that is stored as translation files in ZITADEL itself. The text will be sent to the users of all organizations, that do not have a custom text configured. The message is sent when an invite code email is requested."
};
}
rpc GetCustomInviteUserMessageText(GetCustomInviteUserMessageTextRequest) returns (GetCustomInviteUserMessageTextResponse) {
option (google.api.http) = {
get: "/text/message/invite_user/{language}";
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.read";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "Message Texts";
summary: "Get Custom Invite User Message Text";
description: "Get the custom text of the invite user message/email that is overwritten on the instance as settings/database. The text will be sent to the users of all organizations, that do not have a custom text configured. The message is sent when an invite code email is requested."
};
}
rpc SetDefaultInviteUserMessageText(SetDefaultInviteUserMessageTextRequest) returns (SetDefaultInviteUserMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/invite_user/{language}";
body: "*";
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.write";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "Message Texts";
summary: "Set Default Invite User Message Text";
description: "Set the custom text of the invite user message/email that is overwritten on the instance as settings/database. The text will be sent to the users of all organizations, that do not have a custom text configured. The message is sent when an invite code email is requested. The Following Variables can be used: {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} {{.ApplicationName}}"
};
}
rpc ResetCustomInviteUserMessageTextToDefault(ResetCustomInviteUserMessageTextToDefaultRequest) returns (ResetCustomInviteUserMessageTextToDefaultResponse) {
option (google.api.http) = {
delete: "/text/message/invite_user/{language}"
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.delete"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "Message Texts";
summary: "Reset Custom Invite User Message Text to Default";
description: "Removes the custom text of the invite user message that is overwritten on the instance and triggers the text from the translation files stored in ZITADEL itself. The text will be sent to the users of all organizations, that do not have a custom text configured."
};
}
rpc GetDefaultLoginTexts(GetDefaultLoginTextsRequest) returns (GetDefaultLoginTextsResponse) {
option (google.api.http) = {
get: "/text/default/login/{language}";
@@ -7789,6 +7854,89 @@ message ResetCustomPasswordChangeMessageTextToDefaultResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetDefaultInviteUserMessageTextRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message GetDefaultInviteUserMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
message GetCustomInviteUserMessageTextRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message GetCustomInviteUserMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
message SetDefaultInviteUserMessageTextRequest {
string language = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"de\"";
min_length: 1;
max_length: 200;
}
];
string title = 2 [
(validate.rules).string = {max_bytes: 2000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"Invitation to {{.ApplicationName}}\""
max_length: 500;
}
];
string pre_header = 3 [
(validate.rules).string = {max_bytes: 2000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"Invitation to {{.ApplicationName}}\""
max_length: 500;
}
];
string subject = 4 [
(validate.rules).string = {max_bytes: 2000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"Invitation to {{.ApplicationName}}\""
max_length: 500;
}
];
string greeting = 5 [
(validate.rules).string = {max_bytes: 4000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"Hello {{.DisplayName}},\""
max_length: 1000;
}
];
string text = 6 [
(validate.rules).string = {max_bytes: 40000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"Your user has been invited to {{.ApplicationName}}. Please click the button below to finish the invite process. If you didn't ask for this mail, please ignore it.\""
max_length: 10000;
}
];
string button_text = 7 [
(validate.rules).string = {max_bytes: 4000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"Accept invite\""
max_length: 1000;
}
];
string footer_text = 8 [(validate.rules).string = {max_len: 8000}];
}
message SetDefaultInviteUserMessageTextResponse {
zitadel.v1.ObjectDetails details = 1;
}
message ResetCustomInviteUserMessageTextToDefaultRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message ResetCustomInviteUserMessageTextToDefaultResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetDefaultPasswordlessRegistrationMessageTextRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
@@ -8239,6 +8387,7 @@ message DataOrg {
repeated zitadel.management.v1.SetCustomVerifySMSOTPMessageTextRequest verify_sms_otp_messages = 37;
repeated zitadel.management.v1.SetCustomVerifyEmailOTPMessageTextRequest verify_email_otp_messages = 38;
repeated zitadel.management.v1.SetCustomInviteUserMessageTextRequest invite_user_messages = 39;
}
message ImportDataResponse{

View File

@@ -6496,6 +6496,103 @@ service ManagementService {
};
}
rpc GetCustomInviteUserMessageText(GetCustomInviteUserMessageTextRequest) returns (GetCustomInviteUserMessageTextResponse) {
option (google.api.http) = {
get: "/text/message/invite_user/{language}";
};
option (zitadel.v1.auth_option) = {
permission: "policy.read";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "Message Texts";
summary: "Get Custom Invite User Message Text";
description: "Get the custom text of the password-changed message/email that is configured on the organization. The message is sent when an invite code email is requested."
parameters: {
headers: {
name: "x-zitadel-orgid";
description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data.";
type: STRING,
required: false;
};
};
};
}
rpc GetDefaultInviteUserMessageText(GetDefaultInviteUserMessageTextRequest) returns (GetDefaultInviteUserMessageTextResponse) {
option (google.api.http) = {
get: "/text/default/message/invite_user/{language}";
};
option (zitadel.v1.auth_option) = {
permission: "policy.read";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "Message Texts";
summary: "Get Default Invite User Message Text";
description: "Get the default text of the invite user message/email that is configured on the instance or as translation files in ZITADEL itself. The message is sent when an invite code email is requested."
parameters: {
headers: {
name: "x-zitadel-orgid";
description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data.";
type: STRING,
required: false;
};
};
};
}
rpc SetCustomInviteUserMessageCustomText(SetCustomInviteUserMessageTextRequest) returns (SetCustomInviteUserMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/invite_user/{language}";
body: "*";
};
option (zitadel.v1.auth_option) = {
permission: "policy.write";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "Message Texts";
summary: "Set Custom Invite User Message Text";
description: "Set the custom text of the invite user message/email for the organization. The message is sent when an invite code email is requested. The Following Variables can be used: {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} {{.ApplicationName}}"
parameters: {
headers: {
name: "x-zitadel-orgid";
description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data.";
type: STRING,
required: false;
};
};
};
}
rpc ResetCustomInviteUserMessageTextToDefault(ResetCustomInviteUserMessageTextToDefaultRequest) returns (ResetCustomInviteUserMessageTextToDefaultResponse) {
option (google.api.http) = {
delete: "/text/message/invite_user/{language}"
};
option (zitadel.v1.auth_option) = {
permission: "policy.delete"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "Message Texts";
summary: "Reset Custom Invite User Message Text to Default";
description: "Removes the custom text of the invite user message from the organization and therefore the default texts from the instance or translation files will be triggered for the users."
parameters: {
headers: {
name: "x-zitadel-orgid";
description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data.";
type: STRING,
required: false;
};
};
};
}
rpc GetCustomLoginTexts(GetCustomLoginTextsRequest) returns (GetCustomLoginTextsResponse) {
option (google.api.http) = {
get: "/text/login/{language}";
@@ -11919,6 +12016,86 @@ message ResetCustomPasswordChangeMessageTextToDefaultResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetCustomInviteUserMessageTextRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message GetCustomInviteUserMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
message GetDefaultInviteUserMessageTextRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message GetDefaultInviteUserMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
message SetCustomInviteUserMessageTextRequest {
string language = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"de\""
}
];
string title = 2 [
(validate.rules).string = {max_bytes: 2000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"Invitation to {{.ApplicationName}}\""
max_length: 500;
}
];
string pre_header = 3 [
(validate.rules).string = {max_bytes: 2000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"Invitation to {{.ApplicationName}}\""
max_length: 500;
}
];
string subject = 4 [
(validate.rules).string = {max_bytes: 2000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"Invitation to {{.ApplicationName}}\""
max_length: 500;
}
];
string greeting = 5 [
(validate.rules).string = {max_bytes: 4000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"Hello {{.DisplayName}},\""
max_length: 1000;
}
];
string text = 6 [
(validate.rules).string = {max_bytes: 40000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"Your user has been invited to {{.ApplicationName}}. Please click the button below to finish the invite process. If you didn't ask for this mail, please ignore it.\""
max_length: 10000;
}
];
string button_text = 7 [
(validate.rules).string = {max_bytes: 4000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"Accept invite\""
max_length: 500;
}
];
string footer_text = 8 [(validate.rules).string = {max_bytes: 8000}];
}
message SetCustomInviteUserMessageTextResponse {
zitadel.v1.ObjectDetails details = 1;
}
message ResetCustomInviteUserMessageTextToDefaultRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message ResetCustomInviteUserMessageTextToDefaultResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetOrgIDPByIDRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}

View File

@@ -23,7 +23,7 @@ message SetHumanEmail {
oneof verification {
SendEmailVerificationCode send_code = 2;
ReturnEmailVerificationCode return_code = 3;
bool is_verified = 4 [(validate.rules).bool.const = true];
bool is_verified = 4;
}
}

View File

@@ -282,3 +282,29 @@ enum AuthFactorState {
AUTH_FACTOR_STATE_READY = 2;
AUTH_FACTOR_STATE_REMOVED = 3;
}
message SendInviteCode {
// Optionally set a url_template, which will be used in the invite mail sent by ZITADEL to guide the user to your invitation page.
// If no template is set, the default ZITADEL url will be used.
optional string url_template = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"https://example.com/user/invite?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}\"";
}
];
// Optionally set an application name, which will be used in the invite mail sent by ZITADEL.
// If no application name is set, ZITADEL will be used as default.
optional string application_name = 2 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"CustomerPortal\"";
}
];
}
message ReturnInviteCode {}

View File

@@ -1083,6 +1083,79 @@ service UserService {
};
};
}
// Create an invite code for a user
//
// Create an invite code for a user to initialize their first authentication method (password, passkeys, IdP) depending on the organization's available methods.
rpc CreateInviteCode (CreateInviteCodeRequest) returns (CreateInviteCodeResponse) {
option (google.api.http) = {
post: "/v2/users/{user_id}/invite_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";
}
};
};
}
// Resend an invite code for a user
//
// Resend an invite code for a user to initialize their first authentication method (password, passkeys, IdP) depending on the organization's available methods.
// A resend is only possible if a code has been created previously and sent to the user. If there is no code or it was directly returned, an error will be returned.
rpc ResendInviteCode (ResendInviteCodeRequest) returns (ResendInviteCodeResponse) {
option (google.api.http) = {
post: "/v2/users/{user_id}/invite_code/resend"
};
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";
}
};
};
}
// Verify an invite code for a user
//
// Verify the invite code of a user previously issued. This will set their email to a verified state and
// allow the user to set up their first authentication method (password, passkeys, IdP) depending on the organization's available methods.
rpc VerifyInviteCode (VerifyInviteCodeRequest) returns (VerifyInviteCodeResponse) {
option (google.api.http) = {
post: "/v2/users/{user_id}/invite_code/verify"
};
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 AddHumanUserRequest{
@@ -2076,3 +2149,67 @@ enum AuthenticationMethodType {
AUTHENTICATION_METHOD_TYPE_OTP_SMS = 6;
AUTHENTICATION_METHOD_TYPE_OTP_EMAIL = 7;
}
message CreateInviteCodeRequest {
string user_id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"69629026806489455\"";
}
];
// if no verification is specified, an email is sent with the default url and application name (ZITADEL)
oneof verification {
SendInviteCode send_code = 2;
ReturnInviteCode return_code = 3;
}
}
message CreateInviteCodeResponse {
zitadel.object.v2.Details details = 1;
// The invite code is returned if the verification was set to return_code.
optional string invite_code = 2;
}
message ResendInviteCodeRequest {
string user_id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"69629026806489455\"";
}
];
}
message ResendInviteCodeResponse {
zitadel.object.v2.Details details = 1;
}
message VerifyInviteCodeRequest {
string user_id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"69629026806489455\"";
}
];
string verification_code = 2 [
(validate.rules).string = {min_len: 1, max_len: 20},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 20;
example: "\"SKJd342k\"";
description: "\"the verification code generated during the invite code request\"";
}
];
}
message VerifyInviteCodeResponse {
zitadel.object.v2.Details details = 1;
}

View File

@@ -90,6 +90,8 @@ message DataOrg {
repeated DataAppKey app_keys = 38;
repeated DataMachineKey machine_keys = 39;
repeated zitadel.management.v1.SetCustomInviteUserMessageTextRequest invite_user_messages = 40;
}
message DataOIDCIDP{