Merge branch 'usage-telemetry' of github.com:zitadel/zitadel into usage-telemetry

This commit is contained in:
Elio Bischof 2023-06-16 14:48:10 +02:00
commit 0dd50018ef
No known key found for this signature in database
GPG Key ID: 7B383FDE4DDBF1BD
31 changed files with 793 additions and 74 deletions

View File

@ -23,10 +23,12 @@ type FirstInstance struct {
DefaultLanguage language.Tag DefaultLanguage language.Tag
Org command.OrgSetup Org command.OrgSetup
MachineKeyPath string MachineKeyPath string
PatPath string
instanceSetup command.InstanceSetup instanceSetup command.InstanceSetup
userEncryptionKey *crypto.KeyConfig userEncryptionKey *crypto.KeyConfig
smtpEncryptionKey *crypto.KeyConfig smtpEncryptionKey *crypto.KeyConfig
oidcEncryptionKey *crypto.KeyConfig
masterKey string masterKey string
db *sql.DB db *sql.DB
es *eventstore.Eventstore es *eventstore.Eventstore
@ -59,6 +61,14 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
return err return err
} }
if err = verifyKey(mig.oidcEncryptionKey, keyStorage); err != nil {
return err
}
oidcEncryption, err := crypto.NewAESCrypto(mig.oidcEncryptionKey, keyStorage)
if err != nil {
return err
}
cmd, err := command.StartCommands(mig.es, cmd, err := command.StartCommands(mig.es,
mig.defaults, mig.defaults,
mig.zitadelRoles, mig.zitadelRoles,
@ -73,13 +83,12 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
nil, nil,
userAlg, userAlg,
nil, nil,
nil, oidcEncryption,
nil, nil,
nil, nil,
nil, nil,
nil, nil,
) )
if err != nil { if err != nil {
return err return err
} }
@ -101,25 +110,43 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
} }
} }
_, _, key, _, err := cmd.SetUpInstance(ctx, &mig.instanceSetup) _, token, key, _, err := cmd.SetUpInstance(ctx, &mig.instanceSetup)
if key == nil { if err != nil {
return err
}
if mig.instanceSetup.Org.Machine != nil &&
((mig.instanceSetup.Org.Machine.Pat != nil && token == "") ||
(mig.instanceSetup.Org.Machine.MachineKey != nil && key == nil)) {
return err return err
} }
if key != nil {
keyDetails, err := key.Detail()
if err != nil {
return err
}
if err := outputStdoutOrPath(mig.MachineKeyPath, string(keyDetails)); err != nil {
return err
}
}
if token != "" {
if err := outputStdoutOrPath(mig.PatPath, token); err != nil {
return err
}
}
return nil
}
func outputStdoutOrPath(path string, content string) (err error) {
f := os.Stdout f := os.Stdout
if mig.MachineKeyPath != "" { if path != "" {
f, err = os.OpenFile(mig.MachineKeyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) f, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil { if err != nil {
return err return err
} }
defer f.Close() defer f.Close()
} }
_, err = fmt.Fprintln(f, content)
keyDetails, err := key.Detail()
if err != nil {
return err
}
_, err = fmt.Fprintln(f, string(keyDetails))
return err return err
} }

View File

@ -72,6 +72,7 @@ type Steps struct {
type encryptionKeyConfig struct { type encryptionKeyConfig struct {
User *crypto.KeyConfig User *crypto.KeyConfig
SMTP *crypto.KeyConfig SMTP *crypto.KeyConfig
OIDC *crypto.KeyConfig
} }
func MustNewSteps(v *viper.Viper) *Steps { func MustNewSteps(v *viper.Viper) *Steps {

View File

@ -75,6 +75,7 @@ func Setup(config *Config, steps *Steps, masterKey string) {
steps.FirstInstance.instanceSetup = config.DefaultInstance steps.FirstInstance.instanceSetup = config.DefaultInstance
steps.FirstInstance.userEncryptionKey = config.EncryptionKeys.User steps.FirstInstance.userEncryptionKey = config.EncryptionKeys.User
steps.FirstInstance.smtpEncryptionKey = config.EncryptionKeys.SMTP steps.FirstInstance.smtpEncryptionKey = config.EncryptionKeys.SMTP
steps.FirstInstance.oidcEncryptionKey = config.EncryptionKeys.OIDC
steps.FirstInstance.masterKey = masterKey steps.FirstInstance.masterKey = masterKey
steps.FirstInstance.db = dbClient.DB steps.FirstInstance.db = dbClient.DB
steps.FirstInstance.es = eventstoreClient steps.FirstInstance.es = eventstoreClient

View File

@ -1,5 +1,6 @@
FirstInstance: FirstInstance:
MachineKeyPath: MachineKeyPath:
PatPath:
InstanceName: ZITADEL InstanceName: ZITADEL
DefaultLanguage: en DefaultLanguage: en
Org: Org:
@ -30,6 +31,8 @@ FirstInstance:
MachineKey: MachineKey:
ExpirationDate: ExpirationDate:
Type: Type:
Pat:
ExpirationDate:
CorrectCreationDate: CorrectCreationDate:
FailAfter: 5m FailAfter: 5m

View File

@ -1,16 +1,33 @@
import { Directive, ElementRef, HostListener, Renderer2 } from '@angular/core'; import { Directive, ElementRef, HostListener, Renderer2 } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { take } from 'rxjs';
import { NavigationService } from 'src/app/services/navigation.service'; import { NavigationService } from 'src/app/services/navigation.service';
@Directive({ @Directive({
selector: '[cnslBack]', selector: '[cnslBack]',
}) })
export class BackDirective { export class BackDirective {
new: Boolean = false;
@HostListener('click') @HostListener('click')
onClick(): void { onClick(): void {
this.navigation.back(); this.navigation.back();
// Go back again to avoid create dialog starts again
if (this.new) {
this.navigation.back();
}
} }
constructor(private navigation: NavigationService, private elRef: ElementRef, private renderer2: Renderer2) { constructor(
private navigation: NavigationService,
private elRef: ElementRef,
private renderer2: Renderer2,
private route: ActivatedRoute,
) {
// Check if a new element was created using a create dialog
this.route.queryParams.pipe(take(1)).subscribe((params) => {
this.new = params['new'];
});
if (navigation.isBackPossible) { if (navigation.isBackPossible) {
// this.renderer2.removeStyle(this.elRef.nativeElement, 'visibility'); // this.renderer2.removeStyle(this.elRef.nativeElement, 'visibility');
} else { } else {

View File

@ -378,7 +378,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
if (resp.clientId || resp.clientSecret) { if (resp.clientId || resp.clientSecret) {
this.showSavedDialog(resp); this.showSavedDialog(resp);
} else { } else {
this.router.navigate(['projects', this.projectId, 'apps', resp.appId]); this.router.navigate(['projects', this.projectId, 'apps', resp.appId], { queryParams: { new: true } });
} }
}) })
.catch((error) => { .catch((error) => {
@ -396,7 +396,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
if (resp.clientId || resp.clientSecret) { if (resp.clientId || resp.clientSecret) {
this.showSavedDialog(resp); this.showSavedDialog(resp);
} else { } else {
this.router.navigate(['projects', this.projectId, 'apps', resp.appId]); this.router.navigate(['projects', this.projectId, 'apps', resp.appId], { queryParams: { new: true } });
} }
}) })
.catch((error) => { .catch((error) => {
@ -410,7 +410,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
.addSAMLApp(this.samlAppRequest) .addSAMLApp(this.samlAppRequest)
.then((resp) => { .then((resp) => {
this.loading = false; this.loading = false;
this.router.navigate(['projects', this.projectId, 'apps', resp.appId]); this.router.navigate(['projects', this.projectId, 'apps', resp.appId], { queryParams: { new: true } });
}) })
.catch((error) => { .catch((error) => {
this.loading = false; this.loading = false;
@ -436,7 +436,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
}); });
dialogRef.afterClosed().subscribe(() => { dialogRef.afterClosed().subscribe(() => {
this.router.navigate(['projects', this.projectId, 'apps', added.appId]); this.router.navigate(['projects', this.projectId, 'apps', added.appId], { queryParams: { new: true } });
}); });
} }

View File

@ -88,7 +88,7 @@ export class ProjectRoleCreateComponent implements OnInit, OnDestroy {
.bulkAddProjectRoles(this.projectId, rolesToAdd) .bulkAddProjectRoles(this.projectId, rolesToAdd)
.then(() => { .then(() => {
this.toast.showInfo('PROJECT.TOAST.ROLESCREATED', true); this.toast.showInfo('PROJECT.TOAST.ROLESCREATED', true);
this.router.navigate(['projects', this.projectId], { queryParams: { id: 'roles' } }); this.router.navigate(['projects', this.projectId], { queryParams: { id: 'roles', new: true } });
}) })
.catch((error) => { .catch((error) => {
this.toast.showError(error); this.toast.showError(error);

View File

@ -33,7 +33,7 @@ export class ProjectCreateComponent {
.addProject(this.project) .addProject(this.project)
.then((resp: AddProjectResponse.AsObject) => { .then((resp: AddProjectResponse.AsObject) => {
this.toast.showInfo('PROJECT.TOAST.CREATED', true); this.toast.showInfo('PROJECT.TOAST.CREATED', true);
this.router.navigate(['projects', resp.id]); this.router.navigate(['projects', resp.id], { queryParams: { new: true } });
}) })
.catch((error) => { .catch((error) => {
this.toast.showError(error); this.toast.showError(error);

View File

@ -71,7 +71,7 @@ export class UserCreateMachineComponent implements OnDestroy {
this.toast.showInfo('USER.TOAST.CREATED', true); this.toast.showInfo('USER.TOAST.CREATED', true);
const id = data.userId; const id = data.userId;
if (id) { if (id) {
this.router.navigate(['users', id]); this.router.navigate(['users', id], { queryParams: { new: true } });
} }
}) })
.catch((error: any) => { .catch((error: any) => {

View File

@ -183,7 +183,7 @@ export class UserCreateComponent implements OnInit, OnDestroy {
.then((data) => { .then((data) => {
this.loading = false; this.loading = false;
this.toast.showInfo('USER.TOAST.CREATED', true); this.toast.showInfo('USER.TOAST.CREATED', true);
this.router.navigate(['users', data.userId]); this.router.navigate(['users', data.userId], { queryParams: { new: true } });
}) })
.catch((error) => { .catch((error) => {
this.loading = false; this.loading = false;

View File

@ -17,7 +17,7 @@ Last revised: June 14, 2022
**Downtime** means any period of time in which Core Services are not Available within the Region of the customers organization. Downtime excludes any time in which ZITADEL Cloud is not Available because of **Downtime** means any period of time in which Core Services are not Available within the Region of the customers organization. Downtime excludes any time in which ZITADEL Cloud is not Available because of
- Announced maintenance work - [Announced maintenance work](/docs/support/software-release-cycles-support#maintenance)
- Emergency maintenance - Emergency maintenance
- Force majeure events. - Force majeure events.

View File

@ -154,7 +154,7 @@ Projections:
RequeueEvery: 300s RequeueEvery: 300s
``` ```
### Manage your Data ### Manage your data
When designing your backup strategy, When designing your backup strategy,
it is worth knowing that it is worth knowing that
@ -173,7 +173,7 @@ please refer to the corresponding docs
or [for PostgreSQL](https://www.postgresql.org/docs/current/admin.html). or [for PostgreSQL](https://www.postgresql.org/docs/current/admin.html).
## Data Initialization ## Data initialization
- You can configure instance defaults in the DefaultInstance section. - You can configure instance defaults in the DefaultInstance section.
If you plan to eventually create [multiple virtual instances](/concepts/structure/instance#multiple-virtual-instances), these defaults take effect. If you plan to eventually create [multiple virtual instances](/concepts/structure/instance#multiple-virtual-instances), these defaults take effect.
@ -210,3 +210,19 @@ DefaultInstance:
If you host ZITADEL as a service, If you host ZITADEL as a service,
you might want to [limit usage and/or execute tasks on certain usage units and levels](/self-hosting/manage/quotas). you might want to [limit usage and/or execute tasks on certain usage units and levels](/self-hosting/manage/quotas).
## Minimum system requirements
### General resource usage
ZITADEL consumes around 512MB RAM and can run with less than 1 CPU core.
The database consumes around 2 CPU under normal conditions and 6GB RAM with some caching to it.
:::info Password hashing
Be aware of CPU spikes when hashing passwords. We recommend to have 4 CPU cores available for this purpose.
:::
### Production HA cluster
It is recommended to build a minimal high-availability with 3 Nodes with 4 CPU and 16GB memory each.
Excluding non-essential services, such as log collection, metrics etc, the resources could be reduced to around 4 CPU and 8GB memory each.

View File

@ -1,10 +1,10 @@
--- ---
title: Support States & Software Release Cycle title: Support states & software release cycle
--- ---
## Support States ## Support states
It's important to note that support may differ depending on the feature, and not all features may be fully supported. It's important to note that support may differ depending on the feature, and not all features may be fully supported.
We always strive to provide the best support possible for our customers and community, We always strive to provide the best support possible for our customers and community,
but we may not be able to provide immediate or comprehensive support for all features. but we may not be able to provide immediate or comprehensive support for all features.
Also the support may differ depending on your contracts. Read more about it on our [Legal page](/docs/legal) Also the support may differ depending on your contracts. Read more about it on our [Legal page](/docs/legal)
@ -21,7 +21,7 @@ In case you are eligible to [support services](/docs/legal/support-services) get
Please report any security issues immediately to the indicated address in our [security.txt](https://zitadel.com/.well-known/security.txt) Please report any security issues immediately to the indicated address in our [security.txt](https://zitadel.com/.well-known/security.txt)
::: :::
### Enterprise Supported ### Enterprise supported
Enterprise supported features are those where we provide support only to users eligible for enterprise [support services](/docs/legal/support-services). Enterprise supported features are those where we provide support only to users eligible for enterprise [support services](/docs/legal/support-services).
These features should be functional for eligible users, but may have some limitations for a broader use. These features should be functional for eligible users, but may have some limitations for a broader use.
@ -34,39 +34,94 @@ If you encounter issues with an enterprise supported feature and you are eligibl
- LDAP Identity Provider - LDAP Identity Provider
- [Terraform Provider](https://github.com/zitadel/terraform-provider-zitadel) - [Terraform Provider](https://github.com/zitadel/terraform-provider-zitadel)
### Community Supported ### Community supported
Community supported features are those that have been developed by our community and may not have undergone extensive testing or support from our team. Community supported features are those that have been developed by our community and may not have undergone extensive testing or support from our team.
If you encounter issues with a community supported feature, we encourage you to seek help from our community or other online resources, where other users can provide assistance: If you encounter issues with a community supported feature, we encourage you to seek help from our community or other online resources, where other users can provide assistance:
- Join our [Discord Chat](https://zitadel.com/chat) - Join our [Discord Chat](https://zitadel.com/chat)
- Search [Github Issues](https://github.com/search?q=org%3Azitadel+&type=issues) and report a new issue - Search [Github Issues](https://github.com/search?q=org%3Azitadel+&type=issues) and report a new issue
- Search [Github Discussions](https://github.com/search?q=org%3Azitadel+&type=discussions) and open a new discussion as question or idea - Search [Github Discussions](https://github.com/search?q=org%3Azitadel+&type=discussions) and open a new discussion as question or idea
## Software Release Cycle ## Software release cycle
It's important to note that both Alpha and Beta software can have breaking changes, meaning they are not backward-compatible with previous versions of the software. It's important to note that both Alpha and Beta software can have breaking changes, meaning they are not backward-compatible with previous versions of the software.
Therefore, it's recommended to use caution when using Alpha and Beta software, and to always back up important data before installing or testing new software versions. Therefore, it's recommended to use caution when using Alpha and Beta software, and to always back up important data before installing or testing new software versions.
Only features in General Availability will be covered by support services. Only features in General Availability will be covered by support services.
We encourage our community to test Alpha and Beta software and provide feedback via our [Discord Chat](https://zitadel.com/chat). We encourage our community to test Alpha and Beta software and provide feedback via our [Discord Chat](https://zitadel.com/chat).
### Alpha ### Alpha
The Alpha state is our initial testing phase. The Alpha state is our initial testing phase.
It is available to everyone, but it is not yet complete and may contain bugs and incomplete features. It is available to everyone, but it is not yet complete and may contain bugs and incomplete features.
We recommend that users exercise caution when using Alpha software and avoid using it for critical tasks, as support is limited during this phase. We recommend that users exercise caution when using Alpha software and avoid using it for critical tasks, as support is limited during this phase.
### Beta ### Beta
The Beta state comes after the Alpha phase and is a more stable version of the software. The Beta state comes after the Alpha phase and is a more stable version of the software.
It is feature-complete, but may still contain bugs that need to be fixed before general availability. It is feature-complete, but may still contain bugs that need to be fixed before general availability.
While it is available to everyone, we recommend that users exercise caution when using Beta software and avoid using it for critical tasks. While it is available to everyone, we recommend that users exercise caution when using Beta software and avoid using it for critical tasks.
During this phase, support is limited as we focus on testing and bug fixing. During this phase, support is limited as we focus on testing and bug fixing.
### General Available ### General available
Generally available features are available to everyone and have the appropriate test coverage to be used for critical tasks. Generally available features are available to everyone and have the appropriate test coverage to be used for critical tasks.
The software will be backwards-compatible with previous versions, for exceptions we will publish a [technical advisory](https://zitadel.com/docs/support/technical_advisory). The software will be backwards-compatible with previous versions, for exceptions we will publish a [technical advisory](https://zitadel.com/docs/support/technical_advisory).
Features in General Availability are not marked explicitly. Features in General Availability are not marked explicitly.
## Release types
All release channels receive regular updates and bug fixes.
However, the timing and frequency of updates may differ between the channels.
The choice between the "release candidate", "latest" and "stable" release channels depends on the specific requirements, preferences, and risk tolerance of the users.
[List of all releases](https://github.com/zitadel/zitadel/releases)
### Release candidate
A release candidate refers to a pre-release version that is distributed to a limited group of users or customers for testing and evaluation purposes before a wider release.
It allows a selected group, such as our open source community or early adopters, to provide valuable feedback, identify potential issues, and help refine the software.
Please note that since it is not the final version, the release candidate may still contain some bugs or issues that are addressed before the official release.
Release candidates are accessible for our open source community, but will not be deployed to the ZITADEL Cloud Platform.
### Latest
The "latest" release channel is designed for users who prefer to access the most recent updates, features, and enhancements as soon as they become available.
It provides early access to new functionalities and improvements but may involve a higher degree of risk as it is the most actively developed version.
Users opting for the latest release channel should be aware that occasional bugs or issues may arise due to the ongoing development process.
### Stable
The "stable" release channel is intended for users seeking a more reliable and production-ready version of the software.
It offers a well-tested and validated release with fewer known issues and a higher level of stability.
The stable release channel undergoes rigorous quality assurance and testing processes to ensure that it meets the highest standards of reliability and performance.
It is recommended for users who prioritize stability over immediate access to the latest features.
Current Stable Version:
```yaml reference
https://github.com/zitadel/zitadel/blob/main/release-channels.yaml
```
## Maintenance
ZITADEL Cloud follows a regular deployment cycle to ensure our product remains up-to-date, secure, and provides new features.
Our standard deployment cycle occurs every two weeks, during which we implement updates, bug fixes, and enhancements to improve the functionality and performance of our product.
In certain circumstances, we may require additional deployments beyond the regular two-week cycle.
This can occur for example when we have substantial updates or feature releases that require additional time for thorough testing and implementation or security fixes.
During deployments, we strive to minimize any disruptions and do not expect any downtime.
### Release deployment with risk of downtime
In rare situations where deploying releases that may carry a risk of increased latency or short downtime, we have a well-defined procedure in place to ensure transparent communication.
Prior to such deployments, we publish information on our status page, which can be accessed by visiting [https://status.zitadel.com/](https://status.zitadel.com/).
We also recommend that you subscribe to those updates on the [status page](https://status.zitadel.com/).
We make it a priority to inform you of any potential impact well in advance.
In adherence to our commitment to transparency, we provide a minimum notice period of five working days before deploying a release that poses a risk of downtime.
This gives you time to plan accordingly, make any necessary adjustments, or reach out to our support team for assistance.
Our team works diligently to minimize the risk of downtime during these releases. We thoroughly test and verify each update before deployment to ensure the highest level of stability and reliability.

View File

@ -27,7 +27,7 @@ var (
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
os.Exit(func() int { os.Exit(func() int {
ctx, errCtx, cancel := integration.Contexts(time.Hour) ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
defer cancel() defer cancel()
Tester = integration.NewTester(ctx) Tester = integration.NewTester(ctx)
@ -55,7 +55,7 @@ retry:
s = resp.GetSession() s = resp.GetSession()
break retry break retry
} }
if status.Convert(err).Code() == codes.NotFound { if code := status.Convert(err).Code(); code == codes.NotFound || code == codes.PermissionDenied {
select { select {
case <-CTX.Done(): case <-CTX.Done():
t.Fatal(CTX.Err(), err) t.Fatal(CTX.Err(), err)

View File

@ -113,13 +113,11 @@ func createInstancePbToAddMachine(req *system_pb.CreateInstanceRequest_Machine,
// Scopes are currently static and can not be overwritten // Scopes are currently static and can not be overwritten
Scopes: []string{oidc.ScopeOpenID, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner}, Scopes: []string{oidc.ScopeOpenID, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner},
} }
if req.GetPersonalAccessToken().GetExpirationDate().IsValid() {
if !defaultMachine.Pat.ExpirationDate.IsZero() {
pat.ExpirationDate = defaultMachine.Pat.ExpirationDate
} else if req.PersonalAccessToken.ExpirationDate.IsValid() {
pat.ExpirationDate = req.PersonalAccessToken.ExpirationDate.AsTime() pat.ExpirationDate = req.PersonalAccessToken.ExpirationDate.AsTime()
} else if defaultMachine.Pat != nil && !defaultMachine.Pat.ExpirationDate.IsZero() {
pat.ExpirationDate = defaultMachine.Pat.ExpirationDate
} }
machine.Pat = &pat machine.Pat = &pat
} }

View File

@ -3,13 +3,13 @@ package user
import ( import (
"context" "context"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/structpb"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors" caos_errs "github.com/zitadel/zitadel/internal/errors"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
) )
@ -41,24 +41,32 @@ func passkeyAuthenticatorToDomain(pa user.PasskeyAuthenticator) domain.Authentic
} }
} }
func passkeyRegistrationDetailsToPb(details *domain.PasskeyRegistrationDetails, err error) (*user.RegisterPasskeyResponse, error) { func webAuthNRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*object_pb.Details, *structpb.Struct, error) {
if err != nil {
return nil, nil, err
}
options := new(structpb.Struct)
if err := options.UnmarshalJSON(details.PublicKeyCredentialCreationOptions); err != nil {
return nil, nil, caos_errs.ThrowInternal(err, "USERv2-Dohr6", "Errors.Internal")
}
return object.DomainToDetailsPb(details.ObjectDetails), options, nil
}
func passkeyRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*user.RegisterPasskeyResponse, error) {
objectDetails, options, err := webAuthNRegistrationDetailsToPb(details, err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
options := new(structpb.Struct)
if err := protojson.Unmarshal(details.PublicKeyCredentialCreationOptions, options); err != nil {
return nil, caos_errs.ThrowInternal(err, "USERv2-Dohr6", "Errors.Internal")
}
return &user.RegisterPasskeyResponse{ return &user.RegisterPasskeyResponse{
Details: object.DomainToDetailsPb(details.ObjectDetails), Details: objectDetails,
PasskeyId: details.PasskeyID, PasskeyId: details.ID,
PublicKeyCredentialCreationOptions: options, PublicKeyCredentialCreationOptions: options,
}, nil }, nil
} }
func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) { func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) {
resourceOwner := authz.GetCtxData(ctx).ResourceOwner resourceOwner := authz.GetCtxData(ctx).ResourceOwner
pkc, err := protojson.Marshal(req.GetPublicKeyCredential()) pkc, err := req.GetPublicKeyCredential().MarshalJSON()
if err != nil { if err != nil {
return nil, caos_errs.ThrowInternal(err, "USERv2-Pha2o", "Errors.Internal") return nil, caos_errs.ThrowInternal(err, "USERv2-Pha2o", "Errors.Internal")
} }

View File

@ -94,7 +94,8 @@ func TestServer_RegisterPasskey(t *testing.T) {
}, },
wantErr: true, wantErr: true,
}, },
/* TODO after we are able to obtain a Bearer token for a human user /* TODO: after we are able to obtain a Bearer token for a human user
https://github.com/zitadel/zitadel/issues/6022
{ {
name: "human user", name: "human user",
args: args{ args: args{

View File

@ -50,7 +50,7 @@ func Test_passkeyAuthenticatorToDomain(t *testing.T) {
func Test_passkeyRegistrationDetailsToPb(t *testing.T) { func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
type args struct { type args struct {
details *domain.PasskeyRegistrationDetails details *domain.WebAuthNRegistrationDetails
err error err error
} }
tests := []struct { tests := []struct {
@ -70,13 +70,13 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
{ {
name: "unmarshall error", name: "unmarshall error",
args: args{ args: args{
details: &domain.PasskeyRegistrationDetails{ details: &domain.WebAuthNRegistrationDetails{
ObjectDetails: &domain.ObjectDetails{ ObjectDetails: &domain.ObjectDetails{
Sequence: 22, Sequence: 22,
EventDate: time.Unix(3000, 22), EventDate: time.Unix(3000, 22),
ResourceOwner: "me", ResourceOwner: "me",
}, },
PasskeyID: "123", ID: "123",
PublicKeyCredentialCreationOptions: []byte(`\\`), PublicKeyCredentialCreationOptions: []byte(`\\`),
}, },
err: nil, err: nil,
@ -86,13 +86,13 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
{ {
name: "ok", name: "ok",
args: args{ args: args{
details: &domain.PasskeyRegistrationDetails{ details: &domain.WebAuthNRegistrationDetails{
ObjectDetails: &domain.ObjectDetails{ ObjectDetails: &domain.ObjectDetails{
Sequence: 22, Sequence: 22,
EventDate: time.Unix(3000, 22), EventDate: time.Unix(3000, 22),
ResourceOwner: "me", ResourceOwner: "me",
}, },
PasskeyID: "123", ID: "123",
PublicKeyCredentialCreationOptions: []byte(`{"foo": "bar"}`), PublicKeyCredentialCreationOptions: []byte(`{"foo": "bar"}`),
}, },
err: nil, err: nil,

View File

@ -0,0 +1,44 @@
package user
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
func (s *Server) RegisterU2F(ctx context.Context, req *user.RegisterU2FRequest) (*user.RegisterU2FResponse, error) {
return u2fRegistrationDetailsToPb(
s.command.RegisterUserU2F(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner),
)
}
func u2fRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*user.RegisterU2FResponse, error) {
objectDetails, options, err := webAuthNRegistrationDetailsToPb(details, err)
if err != nil {
return nil, err
}
return &user.RegisterU2FResponse{
Details: objectDetails,
U2FId: details.ID,
PublicKeyCredentialCreationOptions: options,
}, nil
}
func (s *Server) VerifyU2FRegistration(ctx context.Context, req *user.VerifyU2FRegistrationRequest) (*user.VerifyU2FRegistrationResponse, error) {
resourceOwner := authz.GetCtxData(ctx).ResourceOwner
pkc, err := req.GetPublicKeyCredential().MarshalJSON()
if err != nil {
return nil, caos_errs.ThrowInternal(err, "USERv2-IeTh4", "Errors.Internal")
}
objectDetails, err := s.command.HumanVerifyU2FSetup(ctx, req.GetUserId(), resourceOwner, req.GetTokenName(), "", pkc)
if err != nil {
return nil, err
}
return &user.VerifyU2FRegistrationResponse{
Details: object.DomainToDetailsPb(objectDetails),
}, nil
}

View File

@ -0,0 +1,167 @@
//go:build integration
package user_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
"github.com/zitadel/zitadel/internal/integration"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
func TestServer_RegisterU2F(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
type args struct {
ctx context.Context
req *user.RegisterU2FRequest
}
tests := []struct {
name string
args args
want *user.RegisterU2FResponse
wantErr bool
}{
{
name: "missing user id",
args: args{
ctx: CTX,
req: &user.RegisterU2FRequest{},
},
wantErr: true,
},
{
name: "user mismatch",
args: args{
ctx: CTX,
req: &user.RegisterU2FRequest{
UserId: userID,
},
},
wantErr: true,
},
/* TODO: after we are able to obtain a Bearer token for a human user
https://github.com/zitadel/zitadel/issues/6022
{
name: "human user",
args: args{
ctx: CTX,
req: &user.RegisterU2FRequest{
UserId: userID,
},
},
want: &user.RegisterU2FResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ID,
},
},
},
*/
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.RegisterU2F(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
if tt.want != nil {
assert.NotEmpty(t, got.GetU2FId())
assert.NotEmpty(t, got.GetPublicKeyCredentialCreationOptions())
_, err = Tester.WebAuthN.CreateAttestationResponse(got.GetPublicKeyCredentialCreationOptions())
require.NoError(t, err)
}
})
}
}
func TestServer_VerifyU2FRegistration(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
/* TODO after we are able to obtain a Bearer token for a human user
pkr, err := Client.RegisterU2F(CTX, &user.RegisterU2FRequest{
UserId: userID,
})
require.NoError(t, err)
require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions())
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
require.NoError(t, err)
*/
type args struct {
ctx context.Context
req *user.VerifyU2FRegistrationRequest
}
tests := []struct {
name string
args args
want *user.VerifyU2FRegistrationResponse
wantErr bool
}{
{
name: "missing user id",
args: args{
ctx: CTX,
req: &user.VerifyU2FRegistrationRequest{
U2FId: "123",
TokenName: "nice name",
},
},
wantErr: true,
},
/* TODO after we are able to obtain a Bearer token for a human user
{
name: "success",
args: args{
ctx: CTX,
req: &user.VerifyU2FRegistrationRequest{
UserId: userID,
U2FId: pkr.GetU2FId(),
PublicKeyCredential: attestationResponse,
TokenName: "nice name",
},
},
want: &user.VerifyU2FRegistrationResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ID,
},
},
},
*/
{
name: "wrong credential",
args: args{
ctx: CTX,
req: &user.VerifyU2FRegistrationRequest{
UserId: userID,
U2FId: "123",
PublicKeyCredential: &structpb.Struct{
Fields: map[string]*structpb.Value{"foo": {Kind: &structpb.Value_StringValue{StringValue: "bar"}}},
},
TokenName: "nice name",
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.VerifyU2FRegistration(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@ -0,0 +1,97 @@
package user
import (
"io"
"testing"
"time"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
func Test_u2fRegistrationDetailsToPb(t *testing.T) {
type args struct {
details *domain.WebAuthNRegistrationDetails
err error
}
tests := []struct {
name string
args args
want *user.RegisterU2FResponse
wantErr error
}{
{
name: "an error",
args: args{
details: nil,
err: io.ErrClosedPipe,
},
wantErr: io.ErrClosedPipe,
},
{
name: "unmarshall error",
args: args{
details: &domain.WebAuthNRegistrationDetails{
ObjectDetails: &domain.ObjectDetails{
Sequence: 22,
EventDate: time.Unix(3000, 22),
ResourceOwner: "me",
},
ID: "123",
PublicKeyCredentialCreationOptions: []byte(`\\`),
},
err: nil,
},
wantErr: caos_errs.ThrowInternal(nil, "USERv2-Dohr6", "Errors.Internal"),
},
{
name: "ok",
args: args{
details: &domain.WebAuthNRegistrationDetails{
ObjectDetails: &domain.ObjectDetails{
Sequence: 22,
EventDate: time.Unix(3000, 22),
ResourceOwner: "me",
},
ID: "123",
PublicKeyCredentialCreationOptions: []byte(`{"foo": "bar"}`),
},
err: nil,
},
want: &user.RegisterU2FResponse{
Details: &object.Details{
Sequence: 22,
ChangeDate: &timestamppb.Timestamp{
Seconds: 3000,
Nanos: 22,
},
ResourceOwner: "me",
},
U2FId: "123",
PublicKeyCredentialCreationOptions: &structpb.Struct{
Fields: map[string]*structpb.Value{"foo": {Kind: &structpb.Value_StringValue{StringValue: "bar"}}},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := u2fRegistrationDetailsToPb(tt.args.details, tt.args.err)
require.ErrorIs(t, err, tt.wantErr)
if !proto.Equal(tt.want, got) {
t.Errorf("Not equal:\nExpected\n%s\nActual:%s", tt.want, got)
}
if tt.want != nil {
grpc.AllFieldsSet(t, got.ProtoReflect())
}
})
}
}

View File

@ -53,6 +53,9 @@ func (p *Storage) GetEntityByID(ctx context.Context, entityID string) (*servicep
if err != nil { if err != nil {
return nil, err return nil, err
} }
if app.State != domain.AppStateActive {
return nil, errors.ThrowPreconditionFailed(nil, "SAML-sdaGg", "app is not active")
}
return serviceprovider.NewServiceProvider( return serviceprovider.NewServiceProvider(
app.ID, app.ID,
&serviceprovider.Config{ &serviceprovider.Config{
@ -67,6 +70,9 @@ func (p *Storage) GetEntityIDByAppID(ctx context.Context, appID string) (string,
if err != nil { if err != nil {
return "", err return "", err
} }
if app.State != domain.AppStateActive {
return "", errors.ThrowPreconditionFailed(nil, "SAML-sdaGg", "app is not active")
}
return app.SAMLConfig.EntityID, nil return app.SAMLConfig.EntityID, nil
} }

View File

@ -288,18 +288,18 @@ func (l *Login) handleExternalUserAuthenticated(
externalUser := mapIDPUserToExternalUser(user, provider.ID) externalUser := mapIDPUserToExternalUser(user, provider.ID)
// check and fill in local linked user // check and fill in local linked user
externalErr := l.authRepo.CheckExternalUserLogin(setContext(r.Context(), ""), authReq.ID, authReq.AgentID, externalUser, domain.BrowserInfoFromRequest(r)) externalErr := l.authRepo.CheckExternalUserLogin(setContext(r.Context(), ""), authReq.ID, authReq.AgentID, externalUser, domain.BrowserInfoFromRequest(r))
if !errors.IsNotFound(externalErr) { if externalErr != nil && !errors.IsNotFound(externalErr) {
l.renderError(w, r, authReq, externalErr) l.renderError(w, r, authReq, externalErr)
return return
} }
externalUser, externalUserChange, err := l.runPostExternalAuthenticationActions(externalUser, tokens(session), authReq, r, user, externalErr) externalUser, externalUserChange, err := l.runPostExternalAuthenticationActions(externalUser, tokens(session), authReq, r, user, nil)
if err != nil { if err != nil {
l.renderError(w, r, authReq, err) l.renderError(w, r, authReq, err)
return return
} }
// if action is done and no user linked then link or register // if action is done and no user linked then link or register
if errors.IsNotFound(externalErr) { if errors.IsNotFound(externalErr) {
l.externalUserNotExisting(w, r, authReq, provider, externalUser) l.externalUserNotExisting(w, r, authReq, provider, externalUser, externalUserChange)
return return
} }
if provider.IsAutoUpdate || len(externalUser.Metadatas) > 0 || externalUserChange { if provider.IsAutoUpdate || len(externalUser.Metadatas) > 0 || externalUserChange {
@ -334,7 +334,7 @@ func (l *Login) handleExternalUserAuthenticated(
// * external not found overview: // * external not found overview:
// - creation by user // - creation by user
// - linking to existing user // - linking to existing user
func (l *Login) externalUserNotExisting(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, provider *query.IDPTemplate, externalUser *domain.ExternalUser) { func (l *Login) externalUserNotExisting(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, provider *query.IDPTemplate, externalUser *domain.ExternalUser, changed bool) {
resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID() resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID()
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner { if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner {
@ -360,6 +360,12 @@ func (l *Login) externalUserNotExisting(w http.ResponseWriter, r *http.Request,
l.renderExternalNotFoundOption(w, r, authReq, orgIAMPolicy, human, idpLink, err) l.renderExternalNotFoundOption(w, r, authReq, orgIAMPolicy, human, idpLink, err)
return return
} }
if changed {
if err := l.authRepo.SetLinkingUser(r.Context(), authReq, externalUser); err != nil {
l.renderError(w, r, authReq, err)
return
}
}
l.autoCreateExternalUser(w, r, authReq) l.autoCreateExternalUser(w, r, authReq)
} }

View File

@ -17,6 +17,7 @@ type AuthRequestRepository interface {
CheckLoginName(ctx context.Context, id, loginName, userAgentID string) error CheckLoginName(ctx context.Context, id, loginName, userAgentID string) error
CheckExternalUserLogin(ctx context.Context, authReqID, userAgentID string, user *domain.ExternalUser, info *domain.BrowserInfo) error CheckExternalUserLogin(ctx context.Context, authReqID, userAgentID string, user *domain.ExternalUser, info *domain.BrowserInfo) error
SetExternalUserLogin(ctx context.Context, authReqID, userAgentID string, user *domain.ExternalUser) error SetExternalUserLogin(ctx context.Context, authReqID, userAgentID string, user *domain.ExternalUser) error
SetLinkingUser(ctx context.Context, request *domain.AuthRequest, externalUser *domain.ExternalUser) error
SelectUser(ctx context.Context, id, userID, userAgentID string) error SelectUser(ctx context.Context, id, userID, userAgentID string) error
SelectExternalIDP(ctx context.Context, authReqID, idpConfigID, userAgentID string) error SelectExternalIDP(ctx context.Context, authReqID, idpConfigID, userAgentID string) error
VerifyPassword(ctx context.Context, id, userID, resourceOwner, password, userAgentID string, info *domain.BrowserInfo) error VerifyPassword(ctx context.Context, id, userID, resourceOwner, password, userAgentID string, info *domain.BrowserInfo) error

View File

@ -278,6 +278,16 @@ func (repo *AuthRequestRepo) SetExternalUserLogin(ctx context.Context, authReqID
return repo.AuthRequests.UpdateAuthRequest(ctx, request) return repo.AuthRequests.UpdateAuthRequest(ctx, request)
} }
func (repo *AuthRequestRepo) SetLinkingUser(ctx context.Context, request *domain.AuthRequest, externalUser *domain.ExternalUser) error {
for i, user := range request.LinkingUsers {
if user.ExternalUserID == externalUser.ExternalUserID {
request.LinkingUsers[i] = externalUser
return repo.AuthRequests.UpdateAuthRequest(ctx, request)
}
}
return nil
}
func (repo *AuthRequestRepo) setLinkingUser(ctx context.Context, request *domain.AuthRequest, externalUser *domain.ExternalUser) error { func (repo *AuthRequestRepo) setLinkingUser(ctx context.Context, request *domain.AuthRequest, externalUser *domain.ExternalUser) error {
request.LinkingUsers = append(request.LinkingUsers, externalUser) request.LinkingUsers = append(request.LinkingUsers, externalUser)
return repo.AuthRequests.UpdateAuthRequest(ctx, request) return repo.AuthRequests.UpdateAuthRequest(ctx, request)

View File

@ -16,7 +16,7 @@ import (
// RegisterUserPasskey creates a passkey registration for the current authenticated user. // RegisterUserPasskey creates a passkey registration for the current authenticated user.
// UserID, ussualy taken from the request is compaired against the user ID in the context. // UserID, ussualy taken from the request is compaired against the user ID in the context.
func (c *Commands) RegisterUserPasskey(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment) (*domain.PasskeyRegistrationDetails, error) { func (c *Commands) RegisterUserPasskey(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment) (*domain.WebAuthNRegistrationDetails, error) {
if err := authz.UserIDInCTX(ctx, userID); err != nil { if err := authz.UserIDInCTX(ctx, userID); err != nil {
return nil, err return nil, err
} }
@ -25,7 +25,7 @@ func (c *Commands) RegisterUserPasskey(ctx context.Context, userID, resourceOwne
// RegisterUserPasskeyWithCode registers a new passkey for a unauthenticated user id. // RegisterUserPasskeyWithCode registers a new passkey for a unauthenticated user id.
// The resource is protected by the code, identified by the codeID. // The resource is protected by the code, identified by the codeID.
func (c *Commands) RegisterUserPasskeyWithCode(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment, codeID, code string, alg crypto.EncryptionAlgorithm) (*domain.PasskeyRegistrationDetails, error) { func (c *Commands) RegisterUserPasskeyWithCode(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment, codeID, code string, alg crypto.EncryptionAlgorithm) (*domain.WebAuthNRegistrationDetails, error) {
event, err := c.verifyUserPasskeyCode(ctx, userID, resourceOwner, codeID, code, alg) event, err := c.verifyUserPasskeyCode(ctx, userID, resourceOwner, codeID, code, alg)
if err != nil { if err != nil {
return nil, err return nil, err
@ -63,7 +63,7 @@ func (c *Commands) verifyUserPasskeyCodeFailed(ctx context.Context, wm *HumanPas
logging.WithFields("userID", userAgg.ID).OnError(err).Error("RegisterUserPasskeyWithCode push failed") logging.WithFields("userID", userAgg.ID).OnError(err).Error("RegisterUserPasskeyWithCode push failed")
} }
func (c *Commands) registerUserPasskey(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment, events ...eventCallback) (*domain.PasskeyRegistrationDetails, error) { func (c *Commands) registerUserPasskey(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment, events ...eventCallback) (*domain.WebAuthNRegistrationDetails, error) {
wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, userID, resourceOwner, authenticator) wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, userID, resourceOwner, authenticator)
if err != nil { if err != nil {
return nil, err return nil, err
@ -79,7 +79,7 @@ func (c *Commands) createUserPasskey(ctx context.Context, userID, resourceOwner
return c.addHumanWebAuthN(ctx, userID, resourceOwner, false, passwordlessTokens, authenticator, domain.UserVerificationRequirementRequired) return c.addHumanWebAuthN(ctx, userID, resourceOwner, false, passwordlessTokens, authenticator, domain.UserVerificationRequirementRequired)
} }
func (c *Commands) pushUserPasskey(ctx context.Context, wm *HumanWebAuthNWriteModel, userAgg *eventstore.Aggregate, webAuthN *domain.WebAuthNToken, events ...eventCallback) (*domain.PasskeyRegistrationDetails, error) { func (c *Commands) pushUserPasskey(ctx context.Context, wm *HumanWebAuthNWriteModel, userAgg *eventstore.Aggregate, webAuthN *domain.WebAuthNToken, events ...eventCallback) (*domain.WebAuthNRegistrationDetails, error) {
cmds := make([]eventstore.Command, len(events)+1) cmds := make([]eventstore.Command, len(events)+1)
cmds[0] = user.NewHumanPasswordlessAddedEvent(ctx, userAgg, wm.WebauthNTokenID, webAuthN.Challenge) cmds[0] = user.NewHumanPasswordlessAddedEvent(ctx, userAgg, wm.WebauthNTokenID, webAuthN.Challenge)
for i, event := range events { for i, event := range events {
@ -90,9 +90,9 @@ func (c *Commands) pushUserPasskey(ctx context.Context, wm *HumanWebAuthNWriteMo
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &domain.PasskeyRegistrationDetails{ return &domain.WebAuthNRegistrationDetails{
ObjectDetails: writeModelToObjectDetails(&wm.WriteModel), ObjectDetails: writeModelToObjectDetails(&wm.WriteModel),
PasskeyID: wm.WebauthNTokenID, ID: wm.WebauthNTokenID,
PublicKeyCredentialCreationOptions: webAuthN.CredentialCreationData, PublicKeyCredentialCreationOptions: webAuthN.CredentialCreationData,
}, nil }, nil
} }

View File

@ -46,7 +46,7 @@ func TestCommands_RegisterUserPasskey(t *testing.T) {
name string name string
fields fields fields fields
args args args args
want *domain.PasskeyRegistrationDetails want *domain.WebAuthNRegistrationDetails
wantErr error wantErr error
}{ }{
{ {
@ -449,7 +449,7 @@ func TestCommands_pushUserPasskey(t *testing.T) {
require.ErrorIs(t, err, tt.wantErr) require.ErrorIs(t, err, tt.wantErr)
if tt.wantErr == nil { if tt.wantErr == nil {
assert.NotEmpty(t, got.PublicKeyCredentialCreationOptions) assert.NotEmpty(t, got.PublicKeyCredentialCreationOptions)
assert.Equal(t, "123", got.PasskeyID) assert.Equal(t, "123", got.ID)
assert.Equal(t, "org1", got.ObjectDetails.ResourceOwner) assert.Equal(t, "org1", got.ObjectDetails.ResourceOwner)
} }
}) })

View File

@ -0,0 +1,46 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
)
func (c *Commands) RegisterUserU2F(ctx context.Context, userID, resourceOwner string) (*domain.WebAuthNRegistrationDetails, error) {
if err := authz.UserIDInCTX(ctx, userID); err != nil {
return nil, err
}
return c.registerUserU2F(ctx, userID, resourceOwner)
}
func (c *Commands) registerUserU2F(ctx context.Context, userID, resourceOwner string) (*domain.WebAuthNRegistrationDetails, error) {
wm, userAgg, webAuthN, err := c.createUserU2F(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
return c.pushUserU2F(ctx, wm, userAgg, webAuthN)
}
func (c *Commands) createUserU2F(ctx context.Context, userID, resourceOwner string) (*HumanWebAuthNWriteModel, *eventstore.Aggregate, *domain.WebAuthNToken, error) {
tokens, err := c.getHumanU2FTokens(ctx, userID, resourceOwner)
if err != nil {
return nil, nil, nil, err
}
return c.addHumanWebAuthN(ctx, userID, resourceOwner, false, tokens, domain.AuthenticatorAttachmentUnspecified, domain.UserVerificationRequirementRequired)
}
func (c *Commands) pushUserU2F(ctx context.Context, wm *HumanWebAuthNWriteModel, userAgg *eventstore.Aggregate, webAuthN *domain.WebAuthNToken) (*domain.WebAuthNRegistrationDetails, error) {
cmd := user.NewHumanU2FAddedEvent(ctx, userAgg, wm.WebauthNTokenID, webAuthN.Challenge)
err := c.pushAppendAndReduce(ctx, wm, cmd)
if err != nil {
return nil, err
}
return &domain.WebAuthNRegistrationDetails{
ObjectDetails: writeModelToObjectDetails(&wm.WriteModel),
ID: wm.WebauthNTokenID,
PublicKeyCredentialCreationOptions: webAuthN.CredentialCreationData,
}, nil
}

View File

@ -0,0 +1,215 @@
package command
import (
"context"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/id"
id_mock "github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/user"
webauthn_helper "github.com/zitadel/zitadel/internal/webauthn"
)
func TestCommands_RegisterUserU2F(t *testing.T) {
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
ctx = authz.WithRequestedDomain(ctx, "example.com")
webauthnConfig := &webauthn_helper.Config{
DisplayName: "test",
ExternalSecure: true,
}
userAgg := &user.NewAggregate("user1", "org1").Aggregate
type fields struct {
eventstore *eventstore.Eventstore
idGenerator id.Generator
}
type args struct {
userID string
resourceOwner string
}
tests := []struct {
name string
fields fields
args args
want *domain.WebAuthNRegistrationDetails
wantErr error
}{
{
name: "wrong user",
args: args{
userID: "foo",
resourceOwner: "org1",
},
wantErr: caos_errs.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
},
{
name: "get human passwordless error",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilterError(io.ErrClosedPipe),
),
},
args: args{
userID: "user1",
resourceOwner: "org1",
},
wantErr: io.ErrClosedPipe,
},
{
name: "id generator error",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(), // getHumanPasswordlessTokens
expectFilter(eventFromEventPusher(
user.NewHumanAddedEvent(ctx,
userAgg,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.German,
domain.GenderUnspecified,
"email@test.ch",
true,
),
)),
expectFilter(eventFromEventPusher(
org.NewOrgAddedEvent(ctx,
&org.NewAggregate("org1").Aggregate,
"org1",
),
)),
expectFilter(eventFromEventPusher(
org.NewDomainPolicyAddedEvent(ctx,
&org.NewAggregate("org1").Aggregate,
false, false, false,
),
)),
),
idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe),
},
args: args{
userID: "user1",
resourceOwner: "org1",
},
wantErr: io.ErrClosedPipe,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
idGenerator: tt.fields.idGenerator,
webauthnConfig: webauthnConfig,
}
_, err := c.RegisterUserU2F(ctx, tt.args.userID, tt.args.resourceOwner)
require.ErrorIs(t, err, tt.wantErr)
// successful case can't be tested due to random challenge.
})
}
}
func TestCommands_pushUserU2F(t *testing.T) {
ctx := authz.WithRequestedDomain(context.Background(), "example.com")
webauthnConfig := &webauthn_helper.Config{
DisplayName: "test",
ExternalSecure: true,
}
userAgg := &user.NewAggregate("user1", "org1").Aggregate
prep := []expect{
expectFilter(), // getHumanU2FTokens
expectFilter(eventFromEventPusher(
user.NewHumanAddedEvent(ctx,
userAgg,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.German,
domain.GenderUnspecified,
"email@test.ch",
true,
),
)),
expectFilter(eventFromEventPusher(
org.NewOrgAddedEvent(ctx,
&org.NewAggregate("org1").Aggregate,
"org1",
),
)),
expectFilter(eventFromEventPusher(
org.NewDomainPolicyAddedEvent(ctx,
&org.NewAggregate("org1").Aggregate,
false, false, false,
),
)),
expectFilter(eventFromEventPusher(
user.NewHumanWebAuthNAddedEvent(eventstore.NewBaseEventForPush(
ctx, &org.NewAggregate("org1").Aggregate, user.HumanPasswordlessTokenAddedType,
), "111", "challenge"),
)),
}
tests := []struct {
name string
expectPush func(challenge string) expect
wantErr error
}{
{
name: "push error",
expectPush: func(challenge string) expect {
return expectPushFailed(io.ErrClosedPipe, []*repository.Event{eventFromEventPusher(
user.NewHumanU2FAddedEvent(ctx,
userAgg, "123", challenge,
),
)})
},
wantErr: io.ErrClosedPipe,
},
{
name: "success",
expectPush: func(challenge string) expect {
return expectPush([]*repository.Event{eventFromEventPusher(
user.NewHumanU2FAddedEvent(ctx,
userAgg, "123", challenge,
),
)})
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: eventstoreExpect(t, prep...),
webauthnConfig: webauthnConfig,
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
}
wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, "user1", "org1", domain.AuthenticatorAttachmentCrossPlattform)
require.NoError(t, err)
c.eventstore = eventstoreExpect(t, tt.expectPush(webAuthN.Challenge))
got, err := c.pushUserU2F(ctx, wm, userAgg, webAuthN)
require.ErrorIs(t, err, tt.wantErr)
if tt.wantErr == nil {
assert.NotEmpty(t, got.PublicKeyCredentialCreationOptions)
assert.Equal(t, "123", got.ID)
assert.Equal(t, "org1", got.ObjectDetails.ResourceOwner)
}
})
}
}

View File

@ -42,7 +42,7 @@ type AuthRequest struct {
PrivateLabelingSetting PrivateLabelingSetting PrivateLabelingSetting PrivateLabelingSetting
SelectedIDPConfigID string SelectedIDPConfigID string
LinkingUsers []*ExternalUser LinkingUsers []*ExternalUser
PossibleSteps []NextStep PossibleSteps []NextStep `json:"-"`
PasswordVerified bool PasswordVerified bool
MFAsVerified []MFAType MFAsVerified []MFAType
Audience []string Audience []string

View File

@ -21,9 +21,9 @@ type PasskeyCodeDetails struct {
Code string Code string
} }
type PasskeyRegistrationDetails struct { type WebAuthNRegistrationDetails struct {
*ObjectDetails *ObjectDetails
PasskeyID string ID string
PublicKeyCredentialCreationOptions []byte PublicKeyCredentialCreationOptions []byte
} }