diff --git a/proto/zitadel/user/v2alpha/auth.proto b/proto/zitadel/user/v2alpha/auth.proto new file mode 100644 index 0000000000..754877fd4e --- /dev/null +++ b/proto/zitadel/user/v2alpha/auth.proto @@ -0,0 +1,50 @@ +syntax = "proto3"; + +package zitadel.user.v2alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha;user"; + +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +enum PasskeyAuthenticator { + PASSKEY_AUTHENTICATOR_UNSPECIFIED = 0; + PASSKEY_AUTHENTICATOR_PLATFORM = 1; + PASSKEY_AUTHENTICATOR_CROSS_PLATFORM = 2; +} + +message SendPasskeyRegistrationLink { + 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/passkey/register?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}\""; + description: "\"Optionally set a url_template, which will be used in the mail sent by ZITADEL to guide the user to your passkey registration page. If no template is set, the default ZITADEL url will be used.\"" + } + ]; +} + +message ReturnPasskeyRegistrationCode {} + +message PasskeyRegistrationCode { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"id to the one time code generated by ZITADEL\""; + example: "\"e2a48d6a-362b-4db6-a1fb-34feab84dc62\""; + max_length: 200; + } + ]; + string code = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"one time code generated by ZITADEL\""; + example: "\"SomeSpecialCode\""; + max_length: 200; + } + ]; +} diff --git a/proto/zitadel/user/v2alpha/email.proto b/proto/zitadel/user/v2alpha/email.proto index 151348b55a..3022ffc2a6 100644 --- a/proto/zitadel/user/v2alpha/email.proto +++ b/proto/zitadel/user/v2alpha/email.proto @@ -30,7 +30,6 @@ message SetHumanEmail { message SendEmailVerificationCode { optional string url_template = 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; diff --git a/proto/zitadel/user/v2alpha/user_service.proto b/proto/zitadel/user/v2alpha/user_service.proto index f899d91f7a..66e1bc4516 100644 --- a/proto/zitadel/user/v2alpha/user_service.proto +++ b/proto/zitadel/user/v2alpha/user_service.proto @@ -4,11 +4,13 @@ package zitadel.user.v2alpha; import "zitadel/object/v2alpha/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/user/v2alpha/auth.proto"; import "zitadel/user/v2alpha/email.proto"; import "zitadel/user/v2alpha/password.proto"; import "zitadel/user/v2alpha/user.proto"; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; @@ -153,6 +155,73 @@ service UserService { }; }; } + + rpc RegisterPasskey (RegisterPasskeyRequest) returns (RegisterPasskeyResponse) { + option (google.api.http) = { + post: "/users/{user_id}/passkeys" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Start the registration of passkey for a user"; + description: "Start the registration of a passkey for a user, as a response the public key credential creation options are returned, which are used to verify the passkey." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + rpc VerifyPasskeyRegistration (VerifyPasskeyRegistrationRequest) returns (VerifyPasskeyRegistrationResponse) { + option (google.api.http) = { + post: "/users/{user_id}/passkeys/{passkey_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Verify a passkey for a user"; + description: "Verify the passkey registration with the public key credential." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + rpc CreatePasskeyRegistrationLink (CreatePasskeyRegistrationLinkRequest) returns (CreatePasskeyRegistrationLinkResponse) { + option (google.api.http) = { + post: "/users/{user_id}/passkeys/registration_link" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "user.passkey.write" + } + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Create a passkey registration link for a user"; + description: "Create a passkey registration link which includes a code and either return it or send it to the user." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } } message AddHumanUserRequest{ @@ -254,3 +323,110 @@ message VerifyEmailRequest{ message VerifyEmailResponse{ zitadel.object.v2alpha.Details details = 1; } + +message RegisterPasskeyRequest{ + 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: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; + optional PasskeyRegistrationCode code = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"one time code generated by ZITADEL; required to start the passkey registration without user authentication\""; + } + ]; + PasskeyAuthenticator authenticator = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"Optionally specify the authenticator type of the passkey device (platform or cross-platform). If none is provided, both values are allowed.\""; + } + ]; +} + +message RegisterPasskeyResponse{ + zitadel.object.v2alpha.Details details = 1; + string passkey_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"fabde5c8-c13f-481d-a90b-5e59a001a076\"" + } + ]; + bytes public_key_credential_creation_options = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "json representation of public key credential creation options used by the passkey client" + example: "\"eyJwdWJsaWNLZXkiOnsiY2hhbGxlbmdlIoplfZm4vM21qSzBPdjltN2x6VWhnclYyejFJSlVzZnpLd0Z1TytWTWtzRW1Icz0iLCJycCI6eyJuYW1lIjoiWklUQURFTCIsImlkIjoiYWNtZS1nem9lNHgueml0YWRlbC5jbG91ZCJ9LCJ1c2VyIjp7Im5hbWUiOiJ0ZXN0dXNlcjU1QGFjbWUueml0YWRlbC5jbG91ZCIsImRpc3BsYXlOYW1lIjoiVGVzdCBUZXN0IiwiaWQiOiJNVGd5TVRVMk1qWTBNakk1TXpBMk5qSTEifSwicHViS2V5Q3JlZFBhcmFtcyI6W3sidHlwZSI6InB1YmxpYy1rZXkiLCJhbGciOi03fSx7InR5cGUiOiJwdWJsaWMta2V5IiwiYWxnIjotMzV9LHsidHlwZSI6InB1YmxpYy1rZXkiLCJhbGciOi0zNn0seyJ0eXBlIjoicHVibGljLWtleSIsImFsZyI6LTI1N30seyJ0eXBlIjoicHVibGljLWtleSIsImFsZyI6LTI1OH0seyJ0eXBlIjoicHVibGljLWtleSIsImFsZyI6LTI1OX0seyJ0eXBlIjoicHVibGljLWtleSIsImFsZyI6LTM3fSx7InR5cGUiOiJwdWJsaWMta2V5IiwiYWxnIjotMzh9LHsidHlwZSI6InB1YmxpYy1rZXkiLCJhbGciOi0zOX0seyJ0eXBlIjoicHVibGljLWtleSIsImFsZyI6LTh9XSwiYXV0aGVudGljYXRvclNlbGVjdGlvbiI6eyJ1c2VyVmVyaWZpY2F0aW9uIjoiZGlzY291cmFnZWQifn2ilGltZW91dCI6NjAwMDAsImF0dGVzdGF0aW9uIjoibm9uZSJ9fQ==\"" + } + ]; +} + +message VerifyPasskeyRegistrationRequest{ + 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: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; + string passkey_id = 2 [ + (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: "\"fabde5c8-c13f-481d-a90b-5e59a001a076\""; + } + ]; + bytes public_key_credential = 3 [ + (validate.rules).bytes = {min_len: 55, max_len: 1048576}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "JSON representation of public key credential issued by the passkey client"; + min_length: 55; + max_length: 1048576; //1 MB + } + ]; + string passkey_name = 4 [ + (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: "\"fido key\"" + } + ]; +} + +message VerifyPasskeyRegistrationResponse{ + zitadel.object.v2alpha.Details details = 1; +} + +message CreatePasskeyRegistrationLinkRequest{ + 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: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; + // if no medium is specified, an email is sent with the default url + oneof medium { + SendPasskeyRegistrationLink send_link = 2; + ReturnPasskeyRegistrationCode return_code = 3; + } +} + +message CreatePasskeyRegistrationLinkResponse{ + zitadel.object.v2alpha.Details details = 1; + // in case the medium was set to return_code, the code will be returned + optional PasskeyRegistrationCode code = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"one time code generated by ZITADEL; required to start the passkey registration without user authentication\""; + } + ]; +}