mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-09 20:13:40 +00:00
Merge branch 'usage-telemetry' of github.com:zitadel/zitadel into usage-telemetry
This commit is contained in:
commit
0dd50018ef
@ -23,10 +23,12 @@ type FirstInstance struct {
|
||||
DefaultLanguage language.Tag
|
||||
Org command.OrgSetup
|
||||
MachineKeyPath string
|
||||
PatPath string
|
||||
|
||||
instanceSetup command.InstanceSetup
|
||||
userEncryptionKey *crypto.KeyConfig
|
||||
smtpEncryptionKey *crypto.KeyConfig
|
||||
oidcEncryptionKey *crypto.KeyConfig
|
||||
masterKey string
|
||||
db *sql.DB
|
||||
es *eventstore.Eventstore
|
||||
@ -59,6 +61,14 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
|
||||
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,
|
||||
mig.defaults,
|
||||
mig.zitadelRoles,
|
||||
@ -73,13 +83,12 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
|
||||
nil,
|
||||
userAlg,
|
||||
nil,
|
||||
nil,
|
||||
oidcEncryption,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -101,25 +110,43 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
_, _, key, _, err := cmd.SetUpInstance(ctx, &mig.instanceSetup)
|
||||
if key == nil {
|
||||
_, token, key, _, err := cmd.SetUpInstance(ctx, &mig.instanceSetup)
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
if mig.MachineKeyPath != "" {
|
||||
f, err = os.OpenFile(mig.MachineKeyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if path != "" {
|
||||
f, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
keyDetails, err := key.Detail()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintln(f, string(keyDetails))
|
||||
_, err = fmt.Fprintln(f, content)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -72,6 +72,7 @@ type Steps struct {
|
||||
type encryptionKeyConfig struct {
|
||||
User *crypto.KeyConfig
|
||||
SMTP *crypto.KeyConfig
|
||||
OIDC *crypto.KeyConfig
|
||||
}
|
||||
|
||||
func MustNewSteps(v *viper.Viper) *Steps {
|
||||
|
@ -75,6 +75,7 @@ func Setup(config *Config, steps *Steps, masterKey string) {
|
||||
steps.FirstInstance.instanceSetup = config.DefaultInstance
|
||||
steps.FirstInstance.userEncryptionKey = config.EncryptionKeys.User
|
||||
steps.FirstInstance.smtpEncryptionKey = config.EncryptionKeys.SMTP
|
||||
steps.FirstInstance.oidcEncryptionKey = config.EncryptionKeys.OIDC
|
||||
steps.FirstInstance.masterKey = masterKey
|
||||
steps.FirstInstance.db = dbClient.DB
|
||||
steps.FirstInstance.es = eventstoreClient
|
||||
|
@ -1,5 +1,6 @@
|
||||
FirstInstance:
|
||||
MachineKeyPath:
|
||||
PatPath:
|
||||
InstanceName: ZITADEL
|
||||
DefaultLanguage: en
|
||||
Org:
|
||||
@ -30,6 +31,8 @@ FirstInstance:
|
||||
MachineKey:
|
||||
ExpirationDate:
|
||||
Type:
|
||||
Pat:
|
||||
ExpirationDate:
|
||||
|
||||
CorrectCreationDate:
|
||||
FailAfter: 5m
|
||||
|
@ -1,16 +1,33 @@
|
||||
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';
|
||||
|
||||
@Directive({
|
||||
selector: '[cnslBack]',
|
||||
})
|
||||
export class BackDirective {
|
||||
new: Boolean = false;
|
||||
@HostListener('click')
|
||||
onClick(): void {
|
||||
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) {
|
||||
// this.renderer2.removeStyle(this.elRef.nativeElement, 'visibility');
|
||||
} else {
|
||||
|
@ -378,7 +378,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
if (resp.clientId || resp.clientSecret) {
|
||||
this.showSavedDialog(resp);
|
||||
} else {
|
||||
this.router.navigate(['projects', this.projectId, 'apps', resp.appId]);
|
||||
this.router.navigate(['projects', this.projectId, 'apps', resp.appId], { queryParams: { new: true } });
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -396,7 +396,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
if (resp.clientId || resp.clientSecret) {
|
||||
this.showSavedDialog(resp);
|
||||
} else {
|
||||
this.router.navigate(['projects', this.projectId, 'apps', resp.appId]);
|
||||
this.router.navigate(['projects', this.projectId, 'apps', resp.appId], { queryParams: { new: true } });
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -410,7 +410,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
.addSAMLApp(this.samlAppRequest)
|
||||
.then((resp) => {
|
||||
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) => {
|
||||
this.loading = false;
|
||||
@ -436,7 +436,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.router.navigate(['projects', this.projectId, 'apps', added.appId]);
|
||||
this.router.navigate(['projects', this.projectId, 'apps', added.appId], { queryParams: { new: true } });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,7 @@ export class ProjectRoleCreateComponent implements OnInit, OnDestroy {
|
||||
.bulkAddProjectRoles(this.projectId, rolesToAdd)
|
||||
.then(() => {
|
||||
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) => {
|
||||
this.toast.showError(error);
|
||||
|
@ -33,7 +33,7 @@ export class ProjectCreateComponent {
|
||||
.addProject(this.project)
|
||||
.then((resp: AddProjectResponse.AsObject) => {
|
||||
this.toast.showInfo('PROJECT.TOAST.CREATED', true);
|
||||
this.router.navigate(['projects', resp.id]);
|
||||
this.router.navigate(['projects', resp.id], { queryParams: { new: true } });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
|
@ -71,7 +71,7 @@ export class UserCreateMachineComponent implements OnDestroy {
|
||||
this.toast.showInfo('USER.TOAST.CREATED', true);
|
||||
const id = data.userId;
|
||||
if (id) {
|
||||
this.router.navigate(['users', id]);
|
||||
this.router.navigate(['users', id], { queryParams: { new: true } });
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
|
@ -183,7 +183,7 @@ export class UserCreateComponent implements OnInit, OnDestroy {
|
||||
.then((data) => {
|
||||
this.loading = false;
|
||||
this.toast.showInfo('USER.TOAST.CREATED', true);
|
||||
this.router.navigate(['users', data.userId]);
|
||||
this.router.navigate(['users', data.userId], { queryParams: { new: true } });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.loading = false;
|
||||
|
@ -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 customer’s 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
|
||||
- Force majeure events.
|
||||
|
||||
|
@ -154,7 +154,7 @@ Projections:
|
||||
RequeueEvery: 300s
|
||||
```
|
||||
|
||||
### Manage your Data
|
||||
### Manage your data
|
||||
|
||||
When designing your backup strategy,
|
||||
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).
|
||||
|
||||
|
||||
## Data Initialization
|
||||
## Data initialization
|
||||
|
||||
- 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.
|
||||
@ -210,3 +210,19 @@ DefaultInstance:
|
||||
|
||||
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).
|
||||
|
||||
## 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.
|
||||
|
@ -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,
|
||||
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)
|
||||
@ -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)
|
||||
:::
|
||||
|
||||
### 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).
|
||||
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
|
||||
- [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:
|
||||
|
||||
- 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 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.
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
### General Available
|
||||
### General available
|
||||
|
||||
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).
|
||||
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.
|
||||
|
@ -27,7 +27,7 @@ var (
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(func() int {
|
||||
ctx, errCtx, cancel := integration.Contexts(time.Hour)
|
||||
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
|
||||
defer cancel()
|
||||
|
||||
Tester = integration.NewTester(ctx)
|
||||
@ -55,7 +55,7 @@ retry:
|
||||
s = resp.GetSession()
|
||||
break retry
|
||||
}
|
||||
if status.Convert(err).Code() == codes.NotFound {
|
||||
if code := status.Convert(err).Code(); code == codes.NotFound || code == codes.PermissionDenied {
|
||||
select {
|
||||
case <-CTX.Done():
|
||||
t.Fatal(CTX.Err(), err)
|
||||
|
@ -113,13 +113,11 @@ func createInstancePbToAddMachine(req *system_pb.CreateInstanceRequest_Machine,
|
||||
// Scopes are currently static and can not be overwritten
|
||||
Scopes: []string{oidc.ScopeOpenID, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner},
|
||||
}
|
||||
|
||||
if !defaultMachine.Pat.ExpirationDate.IsZero() {
|
||||
pat.ExpirationDate = defaultMachine.Pat.ExpirationDate
|
||||
} else if req.PersonalAccessToken.ExpirationDate.IsValid() {
|
||||
if req.GetPersonalAccessToken().GetExpirationDate().IsValid() {
|
||||
pat.ExpirationDate = req.PersonalAccessToken.ExpirationDate.AsTime()
|
||||
} else if defaultMachine.Pat != nil && !defaultMachine.Pat.ExpirationDate.IsZero() {
|
||||
pat.ExpirationDate = defaultMachine.Pat.ExpirationDate
|
||||
}
|
||||
|
||||
machine.Pat = &pat
|
||||
}
|
||||
|
||||
|
@ -3,13 +3,13 @@ package user
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"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"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/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 {
|
||||
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{
|
||||
Details: object.DomainToDetailsPb(details.ObjectDetails),
|
||||
PasskeyId: details.PasskeyID,
|
||||
Details: objectDetails,
|
||||
PasskeyId: details.ID,
|
||||
PublicKeyCredentialCreationOptions: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) {
|
||||
resourceOwner := authz.GetCtxData(ctx).ResourceOwner
|
||||
pkc, err := protojson.Marshal(req.GetPublicKeyCredential())
|
||||
pkc, err := req.GetPublicKeyCredential().MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "USERv2-Pha2o", "Errors.Internal")
|
||||
}
|
||||
|
@ -94,7 +94,8 @@ func TestServer_RegisterPasskey(t *testing.T) {
|
||||
},
|
||||
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",
|
||||
args: args{
|
||||
|
@ -50,7 +50,7 @@ func Test_passkeyAuthenticatorToDomain(t *testing.T) {
|
||||
|
||||
func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
||||
type args struct {
|
||||
details *domain.PasskeyRegistrationDetails
|
||||
details *domain.WebAuthNRegistrationDetails
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
@ -70,13 +70,13 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
||||
{
|
||||
name: "unmarshall error",
|
||||
args: args{
|
||||
details: &domain.PasskeyRegistrationDetails{
|
||||
details: &domain.WebAuthNRegistrationDetails{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
PasskeyID: "123",
|
||||
ID: "123",
|
||||
PublicKeyCredentialCreationOptions: []byte(`\\`),
|
||||
},
|
||||
err: nil,
|
||||
@ -86,13 +86,13 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
||||
{
|
||||
name: "ok",
|
||||
args: args{
|
||||
details: &domain.PasskeyRegistrationDetails{
|
||||
details: &domain.WebAuthNRegistrationDetails{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
PasskeyID: "123",
|
||||
ID: "123",
|
||||
PublicKeyCredentialCreationOptions: []byte(`{"foo": "bar"}`),
|
||||
},
|
||||
err: nil,
|
||||
|
44
internal/api/grpc/user/v2/u2f.go
Normal file
44
internal/api/grpc/user/v2/u2f.go
Normal 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
|
||||
}
|
167
internal/api/grpc/user/v2/u2f_integration_test.go
Normal file
167
internal/api/grpc/user/v2/u2f_integration_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
97
internal/api/grpc/user/v2/u2f_test.go
Normal file
97
internal/api/grpc/user/v2/u2f_test.go
Normal 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: ×tamppb.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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -53,6 +53,9 @@ func (p *Storage) GetEntityByID(ctx context.Context, entityID string) (*servicep
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if app.State != domain.AppStateActive {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "SAML-sdaGg", "app is not active")
|
||||
}
|
||||
return serviceprovider.NewServiceProvider(
|
||||
app.ID,
|
||||
&serviceprovider.Config{
|
||||
@ -67,6 +70,9 @@ func (p *Storage) GetEntityIDByAppID(ctx context.Context, appID string) (string,
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if app.State != domain.AppStateActive {
|
||||
return "", errors.ThrowPreconditionFailed(nil, "SAML-sdaGg", "app is not active")
|
||||
}
|
||||
return app.SAMLConfig.EntityID, nil
|
||||
}
|
||||
|
||||
|
@ -288,18 +288,18 @@ func (l *Login) handleExternalUserAuthenticated(
|
||||
externalUser := mapIDPUserToExternalUser(user, provider.ID)
|
||||
// check and fill in local linked user
|
||||
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)
|
||||
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 {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
// if action is done and no user linked then link or register
|
||||
if errors.IsNotFound(externalErr) {
|
||||
l.externalUserNotExisting(w, r, authReq, provider, externalUser)
|
||||
l.externalUserNotExisting(w, r, authReq, provider, externalUser, externalUserChange)
|
||||
return
|
||||
}
|
||||
if provider.IsAutoUpdate || len(externalUser.Metadatas) > 0 || externalUserChange {
|
||||
@ -334,7 +334,7 @@ func (l *Login) handleExternalUserAuthenticated(
|
||||
// * external not found overview:
|
||||
// - creation by 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()
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ type AuthRequestRepository interface {
|
||||
CheckLoginName(ctx context.Context, id, loginName, userAgentID string) 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
|
||||
SetLinkingUser(ctx context.Context, request *domain.AuthRequest, externalUser *domain.ExternalUser) error
|
||||
SelectUser(ctx context.Context, id, userID, 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
|
||||
|
@ -278,6 +278,16 @@ func (repo *AuthRequestRepo) SetExternalUserLogin(ctx context.Context, authReqID
|
||||
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 {
|
||||
request.LinkingUsers = append(request.LinkingUsers, externalUser)
|
||||
return repo.AuthRequests.UpdateAuthRequest(ctx, request)
|
||||
|
@ -16,7 +16,7 @@ import (
|
||||
|
||||
// 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.
|
||||
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 {
|
||||
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.
|
||||
// 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)
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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[0] = user.NewHumanPasswordlessAddedEvent(ctx, userAgg, wm.WebauthNTokenID, webAuthN.Challenge)
|
||||
for i, event := range events {
|
||||
@ -90,9 +90,9 @@ func (c *Commands) pushUserPasskey(ctx context.Context, wm *HumanWebAuthNWriteMo
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &domain.PasskeyRegistrationDetails{
|
||||
return &domain.WebAuthNRegistrationDetails{
|
||||
ObjectDetails: writeModelToObjectDetails(&wm.WriteModel),
|
||||
PasskeyID: wm.WebauthNTokenID,
|
||||
ID: wm.WebauthNTokenID,
|
||||
PublicKeyCredentialCreationOptions: webAuthN.CredentialCreationData,
|
||||
}, nil
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ func TestCommands_RegisterUserPasskey(t *testing.T) {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *domain.PasskeyRegistrationDetails
|
||||
want *domain.WebAuthNRegistrationDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
@ -449,7 +449,7 @@ func TestCommands_pushUserPasskey(t *testing.T) {
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
if tt.wantErr == nil {
|
||||
assert.NotEmpty(t, got.PublicKeyCredentialCreationOptions)
|
||||
assert.Equal(t, "123", got.PasskeyID)
|
||||
assert.Equal(t, "123", got.ID)
|
||||
assert.Equal(t, "org1", got.ObjectDetails.ResourceOwner)
|
||||
}
|
||||
})
|
||||
|
46
internal/command/user_v2_u2f.go
Normal file
46
internal/command/user_v2_u2f.go
Normal 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
|
||||
}
|
215
internal/command/user_v2_u2f_test.go
Normal file
215
internal/command/user_v2_u2f_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -42,7 +42,7 @@ type AuthRequest struct {
|
||||
PrivateLabelingSetting PrivateLabelingSetting
|
||||
SelectedIDPConfigID string
|
||||
LinkingUsers []*ExternalUser
|
||||
PossibleSteps []NextStep
|
||||
PossibleSteps []NextStep `json:"-"`
|
||||
PasswordVerified bool
|
||||
MFAsVerified []MFAType
|
||||
Audience []string
|
||||
|
@ -21,9 +21,9 @@ type PasskeyCodeDetails struct {
|
||||
Code string
|
||||
}
|
||||
|
||||
type PasskeyRegistrationDetails struct {
|
||||
type WebAuthNRegistrationDetails struct {
|
||||
*ObjectDetails
|
||||
|
||||
PasskeyID string
|
||||
ID string
|
||||
PublicKeyCredentialCreationOptions []byte
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user