feat: Identity brokering (#730)

* feat: add/ remove external idps

* feat: external idp add /remove

* fix: auth proto

* fix: handle login

* feat: loginpolicy on authrequest

* feat: idp providers on login

* feat: link external idp

* fix: check login policy on check username

* feat: add mapping fields for idp config

* feat: use user org id if existing

* feat: use user org id if existing

* feat: register external user

* feat: register external user

* feat: user linking

* feat: user linking

* feat: design external login

* feat: design external login

* fix: tests

* fix: regenerate login design

* feat: next step test linking process

* feat: next step test linking process

* feat: cascade remove external idps on user

* fix: tests

* fix: tests

* feat: external idp requsts on users

* fix: generate protos

* feat: login styles

* feat: login styles

* fix: link user

* fix: register user on specifig org

* fix: user linking

* fix: register external, linking auto

* fix: remove unnecessary request from proto

* fix: tests

* fix: new oidc package

* fix: migration version

* fix: policy permissions

* Update internal/ui/login/static/i18n/en.yaml

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update internal/ui/login/static/i18n/en.yaml

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update internal/ui/login/handler/renderer.go

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update internal/ui/login/handler/renderer.go

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* fix: pr requests

* Update internal/ui/login/handler/link_users_handler.go

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* fix: pr requests

* fix: pr requests

* fix: pr requests

* fix: login name size

* fix: profile image light

* fix: colors

* fix: pr requests

* fix: remove redirect uri validator

* fix: remove redirect uri validator

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
Fabi 2020-09-18 13:26:28 +02:00 committed by GitHub
parent 1d542a0c57
commit 320ddfa46d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
141 changed files with 30057 additions and 12535 deletions

View File

@ -113,7 +113,7 @@ func startZitadel(configPaths []string) {
func startUI(ctx context.Context, conf *Config, authRepo *auth_es.EsRepository) {
uis := ui.Create(conf.UI)
if *loginEnabled {
login, prefix := login.Start(conf.UI.Login, authRepo, *localDevMode)
login, prefix := login.Start(conf.UI.Login, authRepo, conf.SystemDefaults, *localDevMode)
uis.RegisterHandler(prefix, login.Handler())
}
if *consoleEnabled {

View File

@ -222,6 +222,7 @@ UI:
Port: 50003
Login:
Handler:
BaseURL: '$ZITADEL_ACCOUNTS'
OidcAuthCallbackURL: '$ZITADEL_AUTHORIZE/authorize/'
ZitadelURL: '$ZITADEL_CONSOLE'
LanguageCookieName: 'caos.zitadel.login.lang'

View File

@ -81,7 +81,7 @@
<span class="fill-space"></span>
<div class="btn-wrapper">
<ng-template appHasRole [appHasRole]="['iam.policy.write']">
<ng-template appHasRole [appHasRole]="['policy.write']">
<button [disabled]="loginPolicy" [routerLink]="[ 'policy', PolicyComponentType.LOGIN,'create' ]"
color="primary" mat-raised-button>{{'ORG.POLICY.BTN_INSTALL' | translate}}</button>
<button [disabled]="!loginPolicy" [routerLink]="[ 'policy', PolicyComponentType.LOGIN ]"

View File

@ -9,6 +9,8 @@ import (
es_sdk "github.com/caos/zitadel/internal/eventstore/sdk"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/view/model"
org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing"
usr_model "github.com/caos/zitadel/internal/user/model"
usr_es "github.com/caos/zitadel/internal/user/repository/eventsourcing"
"strings"
iam_model "github.com/caos/zitadel/internal/iam/model"
@ -19,6 +21,7 @@ type IAMRepository struct {
SearchLimit uint64
*iam_es.IAMEventstore
OrgEvents *org_es.OrgEventstore
UserEvents *usr_es.UserEventstore
View *admin_view.View
SystemDefaults systemdefaults.SystemDefaults
Roles []string
@ -83,7 +86,7 @@ func (repo *IAMRepository) IDPConfigByID(ctx context.Context, idpConfigID string
if err != nil {
return nil, err
}
return iam_es_model.IdpConfigViewToModel(idp), nil
return iam_es_model.IDPConfigViewToModel(idp), nil
}
func (repo *IAMRepository) AddOIDCIDPConfig(ctx context.Context, idp *iam_model.IDPConfig) (*iam_model.IDPConfig, error) {
idp.AggregateID = repo.SystemDefaults.IamID
@ -128,7 +131,19 @@ func (repo *IAMRepository) RemoveIDPConfig(ctx context.Context, idpConfigID stri
}
aggregates = append(aggregates, providerAgg)
}
externalIDPs, err := repo.View.ExternalIDPsByIDPConfigID(idpConfigID)
if err != nil {
return err
}
for _, externalIDP := range externalIDPs {
idpRemove := &usr_model.ExternalIDP{ObjectRoot: es_models.ObjectRoot{AggregateID: externalIDP.UserID}, IDPConfigID: externalIDP.IDPConfigID, UserID: externalIDP.ExternalUserID}
idpAgg := make([]*es_models.Aggregate, 0)
_, idpAgg, err = repo.UserEvents.PrepareRemoveExternalIDP(ctx, idpRemove, true)
if err != nil {
return err
}
aggregates = append(aggregates, idpAgg...)
}
return es_sdk.PushAggregates(ctx, repo.Eventstore.PushAggregates, nil, aggregates...)
}
@ -203,7 +218,27 @@ func (repo *IAMRepository) AddIDPProviderToLoginPolicy(ctx context.Context, prov
return repo.IAMEventstore.AddIDPProviderToLoginPolicy(ctx, provider)
}
func (repo *IAMRepository) RemoveIdpProviderFromIdpProvider(ctx context.Context, provider *iam_model.IDPProvider) error {
func (repo *IAMRepository) RemoveIDPProviderFromIDPProvider(ctx context.Context, provider *iam_model.IDPProvider) error {
aggregates := make([]*es_models.Aggregate, 0)
provider.AggregateID = repo.SystemDefaults.IamID
return repo.IAMEventstore.RemoveIDPProviderFromLoginPolicy(ctx, provider)
_, removeAgg, err := repo.IAMEventstore.PrepareRemoveIDPProviderFromLoginPolicy(ctx, provider)
if err != nil {
return err
}
aggregates = append(aggregates, removeAgg)
externalIDPs, err := repo.View.ExternalIDPsByIDPConfigID(provider.IdpConfigID)
if err != nil {
return err
}
for _, externalIDP := range externalIDPs {
idpRemove := &usr_model.ExternalIDP{ObjectRoot: es_models.ObjectRoot{AggregateID: externalIDP.UserID}, IDPConfigID: externalIDP.IDPConfigID, UserID: externalIDP.ExternalUserID}
idpAgg := make([]*es_models.Aggregate, 0)
_, idpAgg, err = repo.UserEvents.PrepareRemoveExternalIDP(ctx, idpRemove, true)
if err != nil {
return err
}
aggregates = append(aggregates, idpAgg...)
}
return es_sdk.PushAggregates(ctx, repo.Eventstore.PushAggregates, nil, aggregates...)
}

View File

@ -0,0 +1,126 @@
package handler
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/config/systemdefaults"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/iam/repository/eventsourcing"
org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing"
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
usr_view_model "github.com/caos/zitadel/internal/user/repository/view/model"
"github.com/caos/zitadel/internal/eventstore/models"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/spooler"
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
)
type ExternalIDP struct {
handler
systemDefaults systemdefaults.SystemDefaults
iamEvents *eventsourcing.IAMEventstore
orgEvents *org_es.OrgEventstore
}
const (
externalIDPTable = "adminapi.user_external_idps"
)
func (m *ExternalIDP) ViewModel() string {
return externalIDPTable
}
func (m *ExternalIDP) EventQuery() (*models.SearchQuery, error) {
sequence, err := m.view.GetLatestExternalIDPSequence()
if err != nil {
return nil, err
}
return es_models.NewSearchQuery().
AggregateTypeFilter(model.UserAggregate, iam_es_model.IAMAggregate, org_es_model.OrgAggregate).
LatestSequenceFilter(sequence.CurrentSequence), nil
}
func (m *ExternalIDP) Reduce(event *models.Event) (err error) {
switch event.AggregateType {
case model.UserAggregate:
err = m.processUser(event)
case iam_es_model.IAMAggregate, org_es_model.OrgAggregate:
err = m.processIdpConfig(event)
}
return err
}
func (m *ExternalIDP) processUser(event *models.Event) (err error) {
externalIDP := new(usr_view_model.ExternalIDPView)
switch event.Type {
case model.HumanExternalIDPAdded:
err = externalIDP.AppendEvent(event)
if err != nil {
return err
}
err = m.fillData(externalIDP)
case model.HumanExternalIDPRemoved, model.HumanExternalIDPCascadeRemoved:
err = externalIDP.SetData(event)
if err != nil {
return err
}
return m.view.DeleteExternalIDP(externalIDP.ExternalUserID, externalIDP.IDPConfigID, event.Sequence)
default:
return m.view.ProcessedExternalIDPSequence(event.Sequence)
}
if err != nil {
return err
}
return m.view.PutExternalIDP(externalIDP, externalIDP.Sequence)
}
func (m *ExternalIDP) processIdpConfig(event *models.Event) (err error) {
switch event.Type {
case iam_es_model.IDPConfigChanged, org_es_model.IDPConfigChanged:
config := new(iam_model.IDPConfig)
config.AppendEvent(event)
exterinalIDPs, err := m.view.ExternalIDPsByIDPConfigID(config.IDPConfigID)
if err != nil {
return err
}
if event.AggregateType == iam_es_model.IAMAggregate {
config, err = m.iamEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID)
} else {
config, err = m.orgEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID)
}
if err != nil {
return err
}
for _, provider := range exterinalIDPs {
m.fillConfigData(provider, config)
}
return m.view.PutExternalIDPs(event.Sequence, exterinalIDPs...)
default:
return m.view.ProcessedExternalIDPSequence(event.Sequence)
}
return nil
}
func (m *ExternalIDP) fillData(externalIDP *usr_view_model.ExternalIDPView) error {
config, err := m.orgEvents.GetIDPConfig(context.Background(), externalIDP.ResourceOwner, externalIDP.IDPConfigID)
if caos_errs.IsNotFound(err) {
config, err = m.iamEvents.GetIDPConfig(context.Background(), m.systemDefaults.IamID, externalIDP.IDPConfigID)
}
if err != nil {
return err
}
m.fillConfigData(externalIDP, config)
return nil
}
func (m *ExternalIDP) fillConfigData(externalIDP *usr_view_model.ExternalIDPView, config *iam_model.IDPConfig) {
externalIDP.IDPName = config.Name
}
func (m *ExternalIDP) OnError(event *models.Event, err error) error {
logging.LogWithFields("SPOOL-4Rsu8", "id", event.AggregateID).WithError(err).Warn("something went wrong in idp provider handler")
return spooler.HandleError(event, err, m.view.GetLatestExternalIDPFailedEvent, m.view.ProcessedExternalIDPFailedEvent, m.view.ProcessedExternalIDPSequence, m.errorCountUntilSkip)
}

View File

@ -87,6 +87,7 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, r
IAMRepository: eventstore.IAMRepository{
IAMEventstore: iam,
OrgEvents: org,
UserEvents: user,
View: view,
SystemDefaults: systemDefaults,
SearchLimit: conf.SearchLimit,

View File

@ -0,0 +1,73 @@
package view
import (
"github.com/caos/zitadel/internal/errors"
usr_model "github.com/caos/zitadel/internal/user/model"
"github.com/caos/zitadel/internal/user/repository/view"
"github.com/caos/zitadel/internal/user/repository/view/model"
global_view "github.com/caos/zitadel/internal/view/repository"
)
const (
externalIDPTable = "adminapi.user_external_idps"
)
func (v *View) ExternalIDPByExternalUserIDAndIDPConfigID(externalUserID, idpConfigID string) (*model.ExternalIDPView, error) {
return view.ExternalIDPByExternalUserIDAndIDPConfigID(v.Db, externalIDPTable, externalUserID, idpConfigID)
}
func (v *View) ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(externalUserID, idpConfigID, resourceOwner string) (*model.ExternalIDPView, error) {
return view.ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(v.Db, externalIDPTable, externalUserID, idpConfigID, resourceOwner)
}
func (v *View) ExternalIDPsByIDPConfigID(idpConfigID string) ([]*model.ExternalIDPView, error) {
return view.ExternalIDPsByIDPConfigID(v.Db, externalIDPTable, idpConfigID)
}
func (v *View) ExternalIDPsByUserID(userID string) ([]*model.ExternalIDPView, error) {
return view.ExternalIDPsByUserID(v.Db, externalIDPTable, userID)
}
func (v *View) SearchExternalIDPs(request *usr_model.ExternalIDPSearchRequest) ([]*model.ExternalIDPView, uint64, error) {
return view.SearchExternalIDPs(v.Db, externalIDPTable, request)
}
func (v *View) PutExternalIDP(externalIDP *model.ExternalIDPView, sequence uint64) error {
err := view.PutExternalIDP(v.Db, externalIDPTable, externalIDP)
if err != nil {
return err
}
return v.ProcessedExternalIDPSequence(sequence)
}
func (v *View) PutExternalIDPs(sequence uint64, externalIDPs ...*model.ExternalIDPView) error {
err := view.PutExternalIDPs(v.Db, externalIDPTable, externalIDPs...)
if err != nil {
return err
}
return v.ProcessedExternalIDPSequence(sequence)
}
func (v *View) DeleteExternalIDP(externalUserID, idpConfigID string, eventSequence uint64) error {
err := view.DeleteExternalIDP(v.Db, externalIDPTable, externalUserID, idpConfigID)
if err != nil && !errors.IsNotFound(err) {
return err
}
return v.ProcessedExternalIDPSequence(eventSequence)
}
func (v *View) GetLatestExternalIDPSequence() (*global_view.CurrentSequence, error) {
return v.latestSequence(externalIDPTable)
}
func (v *View) ProcessedExternalIDPSequence(eventSequence uint64) error {
return v.saveCurrentSequence(externalIDPTable, eventSequence)
}
func (v *View) GetLatestExternalIDPFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
return v.latestFailedEvent(externalIDPTable, sequence)
}
func (v *View) ProcessedExternalIDPFailedEvent(failedEvent *global_view.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@ -13,10 +13,12 @@ func createOidcIdpToModel(idp *admin.OidcIdpConfigCreate) *iam_model.IDPConfig {
LogoSrc: idp.LogoSrc,
Type: iam_model.IDPConfigTypeOIDC,
OIDCConfig: &iam_model.OIDCIDPConfig{
ClientID: idp.ClientId,
ClientSecretString: idp.ClientSecret,
Issuer: idp.Issuer,
Scopes: idp.Scopes,
ClientID: idp.ClientId,
ClientSecretString: idp.ClientSecret,
Issuer: idp.Issuer,
Scopes: idp.Scopes,
IDPDisplayNameMapping: oidcMappingFieldToModel(idp.IdpDisplayNameMapping),
UsernameMapping: oidcMappingFieldToModel(idp.UsernameMapping),
},
}
}
@ -31,11 +33,13 @@ func updateIdpToModel(idp *admin.IdpUpdate) *iam_model.IDPConfig {
func updateOidcIdpToModel(idp *admin.OidcIdpConfigUpdate) *iam_model.OIDCIDPConfig {
return &iam_model.OIDCIDPConfig{
IDPConfigID: idp.IdpId,
ClientID: idp.ClientId,
ClientSecretString: idp.ClientSecret,
Issuer: idp.Issuer,
Scopes: idp.Scopes,
IDPConfigID: idp.IdpId,
ClientID: idp.ClientId,
ClientSecretString: idp.ClientSecret,
Issuer: idp.Issuer,
Scopes: idp.Scopes,
IDPDisplayNameMapping: oidcMappingFieldToModel(idp.IdpDisplayNameMapping),
UsernameMapping: oidcMappingFieldToModel(idp.UsernameMapping),
}
}
@ -105,9 +109,11 @@ func idpConfigViewFromModel(idp *iam_model.IDPConfigView) *admin.IdpView_OidcCon
func oidcIdpConfigViewFromModel(idp *iam_model.IDPConfigView) *admin.OidcIdpConfigView {
return &admin.OidcIdpConfigView{
ClientId: idp.OIDCClientID,
Issuer: idp.OIDCIssuer,
Scopes: idp.OIDCScopes,
ClientId: idp.OIDCClientID,
Issuer: idp.OIDCIssuer,
Scopes: idp.OIDCScopes,
IdpDisplayNameMapping: oidcMappingFieldFromModel(idp.OIDCIDPDisplayNameMapping),
UsernameMapping: oidcMappingFieldFromModel(idp.OIDCUsernameMapping),
}
}
@ -122,6 +128,28 @@ func idpConfigStateFromModel(state iam_model.IDPConfigState) admin.IdpState {
}
}
func oidcMappingFieldFromModel(field iam_model.OIDCMappingField) admin.OIDCMappingField {
switch field {
case iam_model.OIDCMappingFieldPreferredLoginName:
return admin.OIDCMappingField_OIDCMAPPINGFIELD_PREFERRED_USERNAME
case iam_model.OIDCMappingFieldEmail:
return admin.OIDCMappingField_OIDCMAPPINGFIELD_EMAIL
default:
return admin.OIDCMappingField_OIDCMAPPINGFIELD_UNSPECIFIED
}
}
func oidcMappingFieldToModel(field admin.OIDCMappingField) iam_model.OIDCMappingField {
switch field {
case admin.OIDCMappingField_OIDCMAPPINGFIELD_PREFERRED_USERNAME:
return iam_model.OIDCMappingFieldPreferredLoginName
case admin.OIDCMappingField_OIDCMAPPINGFIELD_EMAIL:
return iam_model.OIDCMappingFieldEmail
default:
return iam_model.OIDCMappingFieldUnspecified
}
}
func idpConfigSearchRequestToModel(request *admin.IdpSearchRequest) *iam_model.IDPConfigSearchRequest {
return &iam_model.IDPConfigSearchRequest{
Limit: request.Limit,

View File

@ -122,6 +122,19 @@ func (s *Server) ChangeMyPassword(ctx context.Context, request *auth.PasswordCha
return &empty.Empty{}, err
}
func (s *Server) SearchMyExternalIDPs(ctx context.Context, request *auth.ExternalIDPSearchRequest) (*auth.ExternalIDPSearchResponse, error) {
externalIDP, err := s.repo.SearchMyExternalIDPs(ctx, externalIDPSearchRequestToModel(request))
if err != nil {
return nil, err
}
return externalIDPSearchResponseFromModel(externalIDP), nil
}
func (s *Server) RemoveMyExternalIDP(ctx context.Context, request *auth.ExternalIDPRemoveRequest) (*empty.Empty, error) {
err := s.repo.RemoveMyExternalIDP(ctx, externalIDPRemoveToModel(ctx, request))
return &empty.Empty{}, err
}
func (s *Server) GetMyPasswordComplexityPolicy(ctx context.Context, _ *empty.Empty) (*auth.PasswordComplexityPolicy, error) {
policy, err := s.repo.GetMyPasswordComplexityPolicy(ctx)
if err != nil {

View File

@ -3,7 +3,6 @@ package auth
import (
"context"
"encoding/json"
"github.com/caos/logging"
"github.com/golang/protobuf/ptypes"
"golang.org/x/text/language"
@ -242,6 +241,69 @@ func updateAddressToModel(ctx context.Context, address *auth.UpdateUserAddressRe
}
}
func externalIDPSearchRequestToModel(request *auth.ExternalIDPSearchRequest) *usr_model.ExternalIDPSearchRequest {
return &usr_model.ExternalIDPSearchRequest{
Limit: request.Limit,
Offset: request.Offset,
}
}
func externalIDPRemoveToModel(ctx context.Context, idp *auth.ExternalIDPRemoveRequest) *usr_model.ExternalIDP {
return &usr_model.ExternalIDP{
ObjectRoot: models.ObjectRoot{AggregateID: authz.GetCtxData(ctx).UserID},
IDPConfigID: idp.IdpConfigId,
UserID: idp.ExternalUserId,
}
}
func externalIDPResponseFromModel(idp *usr_model.ExternalIDP) *auth.ExternalIDPResponse {
return &auth.ExternalIDPResponse{
IdpConfigId: idp.IDPConfigID,
UserId: idp.UserID,
DisplayName: idp.DisplayName,
}
}
func externalIDPSearchResponseFromModel(response *usr_model.ExternalIDPSearchResponse) *auth.ExternalIDPSearchResponse {
viewTimestamp, err := ptypes.TimestampProto(response.Timestamp)
logging.Log("GRPC-3h8is").OnError(err).Debug("unable to parse timestamp")
return &auth.ExternalIDPSearchResponse{
Offset: response.Offset,
Limit: response.Limit,
TotalResult: response.TotalResult,
ProcessedSequence: response.Sequence,
ViewTimestamp: viewTimestamp,
Result: externalIDPViewsFromModel(response.Result),
}
}
func externalIDPViewsFromModel(externalIDPs []*usr_model.ExternalIDPView) []*auth.ExternalIDPView {
converted := make([]*auth.ExternalIDPView, len(externalIDPs))
for i, externalIDP := range externalIDPs {
converted[i] = externalIDPViewFromModel(externalIDP)
}
return converted
}
func externalIDPViewFromModel(externalIDP *usr_model.ExternalIDPView) *auth.ExternalIDPView {
creationDate, err := ptypes.TimestampProto(externalIDP.CreationDate)
logging.Log("GRPC-Sj8dw").OnError(err).Debug("unable to parse timestamp")
changeDate, err := ptypes.TimestampProto(externalIDP.ChangeDate)
logging.Log("GRPC-Nf8ue").OnError(err).Debug("unable to parse timestamp")
return &auth.ExternalIDPView{
UserId: externalIDP.UserID,
IdpConfigId: externalIDP.IDPConfigID,
ExternalUserId: externalIDP.ExternalUserID,
ExternalUserDisplayName: externalIDP.UserDisplayName,
IdpName: externalIDP.IDPName,
CreationDate: creationDate,
ChangeDate: changeDate,
}
}
func otpFromModel(otp *usr_model.OTP) *auth.MfaOtpResponse {
return &auth.MfaOtpResponse{
UserId: otp.AggregateID,

View File

@ -13,10 +13,12 @@ func createOidcIdpToModel(idp *management.OidcIdpConfigCreate) *iam_model.IDPCon
LogoSrc: idp.LogoSrc,
Type: iam_model.IDPConfigTypeOIDC,
OIDCConfig: &iam_model.OIDCIDPConfig{
ClientID: idp.ClientId,
ClientSecretString: idp.ClientSecret,
Issuer: idp.Issuer,
Scopes: idp.Scopes,
ClientID: idp.ClientId,
ClientSecretString: idp.ClientSecret,
Issuer: idp.Issuer,
Scopes: idp.Scopes,
IDPDisplayNameMapping: oidcMappingFieldToModel(idp.IdpDisplayNameMapping),
UsernameMapping: oidcMappingFieldToModel(idp.UsernameMapping),
},
}
}
@ -31,11 +33,13 @@ func updateIdpToModel(idp *management.IdpUpdate) *iam_model.IDPConfig {
func updateOidcIdpToModel(idp *management.OidcIdpConfigUpdate) *iam_model.OIDCIDPConfig {
return &iam_model.OIDCIDPConfig{
IDPConfigID: idp.IdpId,
ClientID: idp.ClientId,
ClientSecretString: idp.ClientSecret,
Issuer: idp.Issuer,
Scopes: idp.Scopes,
IDPConfigID: idp.IdpId,
ClientID: idp.ClientId,
ClientSecretString: idp.ClientSecret,
Issuer: idp.Issuer,
Scopes: idp.Scopes,
IDPDisplayNameMapping: oidcMappingFieldToModel(idp.IdpDisplayNameMapping),
UsernameMapping: oidcMappingFieldToModel(idp.UsernameMapping),
}
}
@ -89,9 +93,11 @@ func idpConfigFromModel(idp *iam_model.IDPConfig) *management.Idp_OidcConfig {
func oidcIdpConfigFromModel(idp *iam_model.OIDCIDPConfig) *management.OidcIdpConfig {
return &management.OidcIdpConfig{
ClientId: idp.ClientID,
Issuer: idp.Issuer,
Scopes: idp.Scopes,
ClientId: idp.ClientID,
Issuer: idp.Issuer,
Scopes: idp.Scopes,
IdpDisplayNameMapping: oidcMappingFieldFromModel(idp.IDPDisplayNameMapping),
UsernameMapping: oidcMappingFieldFromModel(idp.UsernameMapping),
}
}
@ -106,9 +112,11 @@ func idpConfigViewFromModel(idp *iam_model.IDPConfigView) *management.IdpView_Oi
func oidcIdpConfigViewFromModel(idp *iam_model.IDPConfigView) *management.OidcIdpConfigView {
return &management.OidcIdpConfigView{
ClientId: idp.OIDCClientID,
Issuer: idp.OIDCIssuer,
Scopes: idp.OIDCScopes,
ClientId: idp.OIDCClientID,
Issuer: idp.OIDCIssuer,
Scopes: idp.OIDCScopes,
IdpDisplayNameMapping: oidcMappingFieldFromModel(idp.OIDCIDPDisplayNameMapping),
UsernameMapping: oidcMappingFieldFromModel(idp.OIDCUsernameMapping),
}
}
@ -181,3 +189,25 @@ func idpConfigsFromView(viewIdps []*iam_model.IDPConfigView) []*management.IdpVi
}
return idps
}
func oidcMappingFieldFromModel(field iam_model.OIDCMappingField) management.OIDCMappingField {
switch field {
case iam_model.OIDCMappingFieldPreferredLoginName:
return management.OIDCMappingField_OIDCMAPPINGFIELD_PREFERRED_USERNAME
case iam_model.OIDCMappingFieldEmail:
return management.OIDCMappingField_OIDCMAPPINGFIELD_EMAIL
default:
return management.OIDCMappingField_OIDCMAPPINGFIELD_UNSPECIFIED
}
}
func oidcMappingFieldToModel(field management.OIDCMappingField) iam_model.OIDCMappingField {
switch field {
case management.OIDCMappingField_OIDCMAPPINGFIELD_PREFERRED_USERNAME:
return iam_model.OIDCMappingFieldPreferredLoginName
case management.OIDCMappingField_OIDCMAPPINGFIELD_EMAIL:
return iam_model.OIDCMappingFieldEmail
default:
return iam_model.OIDCMappingFieldUnspecified
}
}

View File

@ -2,7 +2,6 @@ package management
import (
"context"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/pkg/grpc/management"
@ -196,6 +195,19 @@ func (s *Server) SetInitialPassword(ctx context.Context, request *management.Pas
return &empty.Empty{}, err
}
func (s *Server) SearchUserExternalIDPs(ctx context.Context, request *management.ExternalIDPSearchRequest) (*management.ExternalIDPSearchResponse, error) {
externalIDP, err := s.user.SearchExternalIDPs(ctx, externalIDPSearchRequestToModel(request))
if err != nil {
return nil, err
}
return externalIDPSearchResponseFromModel(externalIDP), nil
}
func (s *Server) RemoveExternalIDP(ctx context.Context, request *management.ExternalIDPRemoveRequest) (*empty.Empty, error) {
err := s.user.RemoveExternalIDP(ctx, externalIDPRemoveToModel(request))
return &empty.Empty{}, err
}
func (s *Server) GetUserMfas(ctx context.Context, userID *management.UserID) (*management.MultiFactors, error) {
mfas, err := s.user.UserMfas(ctx, userID.Id)
if err != nil {

View File

@ -2,8 +2,8 @@ package management
import (
"encoding/json"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/model"
"github.com/golang/protobuf/ptypes"
"golang.org/x/text/language"
"google.golang.org/protobuf/encoding/protojson"
@ -66,6 +66,62 @@ func passwordRequestToModel(r *management.PasswordRequest) *usr_model.Password {
}
}
func externalIDPSearchRequestToModel(request *management.ExternalIDPSearchRequest) *usr_model.ExternalIDPSearchRequest {
return &usr_model.ExternalIDPSearchRequest{
Limit: request.Limit,
Offset: request.Offset,
Queries: []*usr_model.ExternalIDPSearchQuery{{Key: usr_model.ExternalIDPSearchKeyUserID, Method: model.SearchMethodEquals, Value: request.UserId}},
}
}
func externalIDPRemoveToModel(idp *management.ExternalIDPRemoveRequest) *usr_model.ExternalIDP {
return &usr_model.ExternalIDP{
ObjectRoot: models.ObjectRoot{AggregateID: idp.UserId},
IDPConfigID: idp.IdpConfigId,
UserID: idp.ExternalUserId,
}
}
func externalIDPSearchResponseFromModel(response *usr_model.ExternalIDPSearchResponse) *management.ExternalIDPSearchResponse {
viewTimestamp, err := ptypes.TimestampProto(response.Timestamp)
logging.Log("GRPC-3h8is").OnError(err).Debug("unable to parse timestamp")
return &management.ExternalIDPSearchResponse{
Offset: response.Offset,
Limit: response.Limit,
TotalResult: response.TotalResult,
ProcessedSequence: response.Sequence,
ViewTimestamp: viewTimestamp,
Result: externalIDPViewsFromModel(response.Result),
}
}
func externalIDPViewsFromModel(externalIDPs []*usr_model.ExternalIDPView) []*management.ExternalIDPView {
converted := make([]*management.ExternalIDPView, len(externalIDPs))
for i, externalIDP := range externalIDPs {
converted[i] = externalIDPViewFromModel(externalIDP)
}
return converted
}
func externalIDPViewFromModel(externalIDP *usr_model.ExternalIDPView) *management.ExternalIDPView {
creationDate, err := ptypes.TimestampProto(externalIDP.CreationDate)
logging.Log("GRPC-Fdu8s").OnError(err).Debug("unable to parse timestamp")
changeDate, err := ptypes.TimestampProto(externalIDP.ChangeDate)
logging.Log("GRPC-Was7u").OnError(err).Debug("unable to parse timestamp")
return &management.ExternalIDPView{
UserId: externalIDP.UserID,
IdpConfigId: externalIDP.IDPConfigID,
ExternalUserId: externalIDP.ExternalUserID,
ExternalUserDisplayName: externalIDP.UserDisplayName,
IdpName: externalIDP.IDPName,
CreationDate: creationDate,
ChangeDate: changeDate,
}
}
func userSearchRequestsToModel(project *management.UserSearchRequest) *usr_model.UserSearchRequest {
return &usr_model.UserSearchRequest{
Offset: project.Offset,

View File

@ -2,6 +2,8 @@ package repository
import (
"context"
org_model "github.com/caos/zitadel/internal/org/model"
user_model "github.com/caos/zitadel/internal/user/model"
"github.com/caos/zitadel/internal/auth_request/model"
)
@ -14,7 +16,11 @@ type AuthRequestRepository interface {
SaveAuthCode(ctx context.Context, id, code, userAgentID string) error
DeleteAuthRequest(ctx context.Context, id string) error
CheckLoginName(ctx context.Context, id, loginName, userAgentID string) error
CheckExternalUserLogin(ctx context.Context, authReqID, userAgentID string, user *model.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, password, userAgentID string, info *model.BrowserInfo) error
VerifyMfaOTP(ctx context.Context, agentID, authRequestID, code, userAgentID string, info *model.BrowserInfo) error
LinkExternalUsers(ctx context.Context, authReqID, userAgentID string) error
AutoRegisterExternalUser(ctx context.Context, user *user_model.User, externalIDP *user_model.ExternalIDP, member *org_model.OrgMember, authReqID, userAgentID, resourceOwner string) error
}

View File

@ -2,6 +2,12 @@ package eventstore
import (
"context"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/eventstore/sdk"
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/view/model"
org_event "github.com/caos/zitadel/internal/org/repository/eventsourcing"
policy_event "github.com/caos/zitadel/internal/policy/repository/eventsourcing"
"time"
"github.com/caos/logging"
@ -11,6 +17,7 @@ import (
cache "github.com/caos/zitadel/internal/auth_request/repository"
"github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model"
"github.com/caos/zitadel/internal/id"
org_model "github.com/caos/zitadel/internal/org/model"
org_view_model "github.com/caos/zitadel/internal/org/repository/view/model"
@ -22,6 +29,8 @@ import (
type AuthRequestRepo struct {
UserEvents *user_event.UserEventstore
OrgEvents *org_event.OrgEventstore
PolicyEvents *policy_event.PolicyEventstore
AuthRequests cache.AuthRequestCache
View *view.View
@ -29,6 +38,8 @@ type AuthRequestRepo struct {
UserViewProvider userViewProvider
UserEventProvider userEventProvider
OrgViewProvider orgViewProvider
LoginPolicyViewProvider loginPolicyViewProvider
IDPProviderViewProvider idpProviderViewProvider
IdGenerator id.Generator
@ -36,6 +47,8 @@ type AuthRequestRepo struct {
MfaInitSkippedLifeTime time.Duration
MfaSoftwareCheckLifeTime time.Duration
MfaHardwareCheckLifeTime time.Duration
IAMID string
}
type userSessionViewProvider interface {
@ -46,8 +59,17 @@ type userViewProvider interface {
UserByID(string) (*user_view_model.UserView, error)
}
type loginPolicyViewProvider interface {
LoginPolicyByAggregateID(string) (*iam_view_model.LoginPolicyView, error)
}
type idpProviderViewProvider interface {
IDPProvidersByAggregateID(string) ([]*iam_view_model.IDPProviderView, error)
}
type userEventProvider interface {
UserEventsByID(ctx context.Context, id string, sequence uint64) ([]*es_models.Event, error)
BulkAddExternalIDPs(ctx context.Context, userID string, externalIDPs []*user_model.ExternalIDP) error
}
type orgViewProvider interface {
@ -73,7 +95,7 @@ func (repo *AuthRequestRepo) CreateAuthRequest(ctx context.Context, request *mod
}
request.Audience = ids
if request.LoginHint != "" {
err = repo.checkLoginName(request, request.LoginHint)
err = repo.checkLoginName(ctx, request, request.LoginHint)
logging.LogWithFields("EVENT-aG311", "login name", request.LoginHint, "id", request.ID, "applicationID", request.ApplicationID).Debug("login hint invalid")
}
err = repo.AuthRequests.SaveAuthRequest(ctx, request)
@ -122,13 +144,45 @@ func (repo *AuthRequestRepo) CheckLoginName(ctx context.Context, id, loginName,
if err != nil {
return err
}
err = repo.checkLoginName(request, loginName)
err = repo.checkLoginName(ctx, request, loginName)
if err != nil {
return err
}
return repo.AuthRequests.UpdateAuthRequest(ctx, request)
}
func (repo *AuthRequestRepo) SelectExternalIDP(ctx context.Context, authReqID, idpConfigID, userAgentID string) error {
request, err := repo.getAuthRequest(ctx, authReqID, userAgentID)
if err != nil {
return err
}
err = repo.checkSelectedExternalIDP(request, idpConfigID)
if err != nil {
return err
}
return repo.AuthRequests.UpdateAuthRequest(ctx, request)
}
func (repo *AuthRequestRepo) CheckExternalUserLogin(ctx context.Context, authReqID, userAgentID string, externalUser *model.ExternalUser) error {
request, err := repo.getAuthRequest(ctx, authReqID, userAgentID)
if err != nil {
return err
}
err = repo.checkExternalUserLogin(request, externalUser.IDPConfigID, externalUser.ExternalUserID)
if errors.IsNotFound(err) {
return repo.setLinkingUser(ctx, request, externalUser)
}
if err != nil {
return err
}
return repo.AuthRequests.UpdateAuthRequest(ctx, request)
}
func (repo *AuthRequestRepo) setLinkingUser(ctx context.Context, request *model.AuthRequest, externalUser *model.ExternalUser) error {
request.LinkingUsers = append(request.LinkingUsers, externalUser)
return repo.AuthRequests.UpdateAuthRequest(ctx, request)
}
func (repo *AuthRequestRepo) SelectUser(ctx context.Context, id, userID, userAgentID string) error {
request, err := repo.getAuthRequest(ctx, id, userAgentID)
if err != nil {
@ -164,6 +218,59 @@ func (repo *AuthRequestRepo) VerifyMfaOTP(ctx context.Context, authRequestID, us
return repo.UserEvents.CheckMfaOTP(ctx, userID, code, request.WithCurrentInfo(info))
}
func (repo *AuthRequestRepo) LinkExternalUsers(ctx context.Context, authReqID, userAgentID string) error {
request, err := repo.getAuthRequest(ctx, authReqID, userAgentID)
if err != nil {
return err
}
err = linkExternalIDPs(ctx, repo.UserEventProvider, request)
if err != nil {
return err
}
request.LinkingUsers = nil
return repo.AuthRequests.UpdateAuthRequest(ctx, request)
}
func (repo *AuthRequestRepo) AutoRegisterExternalUser(ctx context.Context, registerUser *user_model.User, externalIDP *user_model.ExternalIDP, orgMember *org_model.OrgMember, authReqID, userAgentID, resourceOwner string) error {
request, err := repo.getAuthRequest(ctx, authReqID, userAgentID)
if err != nil {
return err
}
policyResourceOwner := authz.GetCtxData(ctx).OrgID
if resourceOwner != "" {
policyResourceOwner = resourceOwner
}
pwPolicy, err := repo.PolicyEvents.GetPasswordComplexityPolicy(ctx, policyResourceOwner)
if err != nil {
return err
}
orgPolicy, err := repo.OrgEvents.GetOrgIAMPolicy(ctx, policyResourceOwner)
if err != nil {
return err
}
user, aggregates, err := repo.UserEvents.PrepareRegisterUser(ctx, registerUser, externalIDP, pwPolicy, orgPolicy, resourceOwner)
if err != nil {
return err
}
if orgMember != nil {
orgMember.UserID = user.AggregateID
_, memberAggregate, err := repo.OrgEvents.PrepareAddOrgMember(ctx, orgMember, policyResourceOwner)
if err != nil {
return err
}
aggregates = append(aggregates, memberAggregate)
}
err = sdk.PushAggregates(ctx, repo.UserEvents.PushAggregates, user.AppendEvents, aggregates...)
if err != nil {
return err
}
request.UserID = user.AggregateID
request.SelectedIDPConfigID = externalIDP.IDPConfigID
request.LinkingUsers = nil
return repo.AuthRequests.UpdateAuthRequest(ctx, request)
}
func (repo *AuthRequestRepo) getAuthRequestNextSteps(ctx context.Context, id, userAgentID string, checkLoggedIn bool) (*model.AuthRequest, error) {
request, err := repo.getAuthRequest(ctx, id, userAgentID)
if err != nil {
@ -185,18 +292,114 @@ func (repo *AuthRequestRepo) getAuthRequest(ctx context.Context, id, userAgentID
if request.AgentID != userAgentID {
return nil, errors.ThrowPermissionDenied(nil, "EVENT-adk13", "Errors.AuthRequest.UserAgentNotCorresponding")
}
err = repo.fillLoginPolicy(ctx, request)
if err != nil {
return nil, err
}
return request, nil
}
func (repo *AuthRequestRepo) checkLoginName(request *model.AuthRequest, loginName string) error {
user, err := repo.View.UserByLoginName(loginName)
func (repo *AuthRequestRepo) getLoginPolicyAndIDPProviders(ctx context.Context, orgID string) (*iam_model.LoginPolicyView, []*iam_model.IDPProviderView, error) {
policy, err := repo.getLoginPolicy(ctx, orgID)
if err != nil {
return nil, nil, err
}
if !policy.AllowExternalIDP {
return policy, nil, nil
}
idpProviders, err := getLoginPolicyIDPProviders(repo.IDPProviderViewProvider, repo.IAMID, orgID, policy.Default)
if err != nil {
return nil, nil, err
}
return policy, idpProviders, nil
}
func (repo *AuthRequestRepo) fillLoginPolicy(ctx context.Context, request *model.AuthRequest) error {
orgID := request.UserOrgID
if orgID == "" {
orgID = request.GetScopeOrgID()
}
if orgID == "" {
orgID = repo.IAMID
}
policy, idpProviders, err := repo.getLoginPolicyAndIDPProviders(ctx, orgID)
if err != nil {
return err
}
request.LoginPolicy = policy
if idpProviders != nil {
request.AllowedExternalIDPs = idpProviders
}
return nil
}
func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *model.AuthRequest, loginName string) (err error) {
orgID := request.GetScopeOrgID()
user := new(user_view_model.UserView)
if orgID != "" {
user, err = repo.View.UserByLoginNameAndResourceOwner(loginName, orgID)
} else {
user, err = repo.View.UserByLoginName(loginName)
if err == nil {
err = repo.checkLoginPolicyWithResourceOwner(ctx, request, user)
if err != nil {
return err
}
}
}
if err != nil {
return err
}
request.SetUserInfo(user.ID, loginName, "", user.ResourceOwner)
return nil
}
func (repo AuthRequestRepo) checkLoginPolicyWithResourceOwner(ctx context.Context, request *model.AuthRequest, user *user_view_model.UserView) error {
loginPolicy, idpProviders, err := repo.getLoginPolicyAndIDPProviders(ctx, user.ResourceOwner)
if err != nil {
return err
}
if len(request.LinkingUsers) != 0 && !loginPolicy.AllowExternalIDP {
return errors.ThrowInvalidArgument(nil, "LOGIN-s9sio", "Errors.User.NotAllowedToLink")
}
if len(request.LinkingUsers) != 0 {
exists := linkingIDPConfigExistingInAllowedIDPs(request.LinkingUsers, idpProviders)
if !exists {
return errors.ThrowInvalidArgument(nil, "LOGIN-Dj89o", "Errors.User.NotAllowedToLink")
}
}
request.LoginPolicy = loginPolicy
request.AllowedExternalIDPs = idpProviders
return nil
}
func (repo *AuthRequestRepo) checkSelectedExternalIDP(request *model.AuthRequest, idpConfigID string) error {
for _, externalIDP := range request.AllowedExternalIDPs {
if externalIDP.IDPConfigID == idpConfigID {
request.SelectedIDPConfigID = idpConfigID
return nil
}
}
return errors.ThrowNotFound(nil, "LOGIN-Nsm8r", "Errors.User.ExternalIDP.NotAllowed")
}
func (repo *AuthRequestRepo) checkExternalUserLogin(request *model.AuthRequest, idpConfigID, externalUserID string) (err error) {
orgID := request.GetScopeOrgID()
externalIDP := new(user_view_model.ExternalIDPView)
if orgID != "" {
externalIDP, err = repo.View.ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(externalUserID, idpConfigID, orgID)
} else {
externalIDP, err = repo.View.ExternalIDPByExternalUserIDAndIDPConfigID(externalUserID, idpConfigID)
}
if err != nil {
return err
}
request.SetUserInfo(externalIDP.UserID, "", "", externalIDP.ResourceOwner)
return nil
}
func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *model.AuthRequest, checkLoggedIn bool) ([]model.NextStep, error) {
if request == nil {
return nil, errors.ThrowInvalidArgument(nil, "EVENT-ds27a", "Errors.Internal")
@ -206,7 +409,11 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *model.AuthR
return append(steps, &model.RedirectToCallbackStep{}), nil
}
if request.UserID == "" {
steps = append(steps, &model.LoginStep{})
if request.LinkingUsers != nil && len(request.LinkingUsers) > 0 {
steps = append(steps, new(model.ExternalNotFoundOptionStep))
return steps, nil
}
steps = append(steps, new(model.LoginStep))
if request.Prompt == model.PromptSelectAccount || request.Prompt == model.PromptUnspecified {
users, err := repo.usersForUserSelection(request)
if err != nil {
@ -222,23 +429,26 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *model.AuthR
if err != nil {
return nil, err
}
request.LoginName = user.PreferredLoginName
userSession, err := userSessionByIDs(ctx, repo.UserSessionViewProvider, repo.UserEventProvider, request.AgentID, user)
if err != nil {
return nil, err
}
if user.InitRequired {
return append(steps, &model.InitUserStep{PasswordSet: user.PasswordSet}), nil
}
if !user.PasswordSet {
return append(steps, &model.InitPasswordStep{}), nil
}
if request.SelectedIDPConfigID == "" {
if user.InitRequired {
return append(steps, &model.InitUserStep{PasswordSet: user.PasswordSet}), nil
}
if !user.PasswordSet {
return append(steps, &model.InitPasswordStep{}), nil
}
if !checkVerificationTime(userSession.PasswordVerification, repo.PasswordCheckLifeTime) {
return append(steps, &model.PasswordStep{}), nil
if !checkVerificationTime(userSession.PasswordVerification, repo.PasswordCheckLifeTime) {
return append(steps, &model.PasswordStep{}), nil
}
request.PasswordVerified = true
request.AuthTime = userSession.PasswordVerification
}
request.PasswordVerified = true
request.AuthTime = userSession.PasswordVerification
if step, ok := repo.mfaChecked(userSession, request, user); !ok {
return append(steps, step), nil
@ -258,6 +468,10 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *model.AuthR
return steps, nil
}
if request.LinkingUsers != nil && len(request.LinkingUsers) != 0 {
return append(steps, &model.LinkUsersStep{}), nil
}
//PLANNED: consent step
return append(steps, &model.RedirectToCallbackStep{}), nil
}
@ -322,6 +536,36 @@ func (repo *AuthRequestRepo) mfaSkippedOrSetUp(user *user_model.UserView) bool {
return checkVerificationTime(user.MfaInitSkipped, repo.MfaInitSkippedLifeTime)
}
func (repo *AuthRequestRepo) getLoginPolicy(ctx context.Context, orgID string) (*iam_model.LoginPolicyView, error) {
policy, err := repo.View.LoginPolicyByAggregateID(orgID)
if errors.IsNotFound(err) {
policy, err = repo.View.LoginPolicyByAggregateID(repo.IAMID)
if err != nil {
return nil, err
}
policy.Default = true
}
if err != nil {
return nil, err
}
return iam_es_model.LoginPolicyViewToModel(policy), err
}
func getLoginPolicyIDPProviders(provider idpProviderViewProvider, iamID, orgID string, defaultPolicy bool) ([]*iam_model.IDPProviderView, error) {
if defaultPolicy {
idpProviders, err := provider.IDPProvidersByAggregateID(iamID)
if err != nil {
return nil, err
}
return iam_es_model.IDPProviderViewsToModel(idpProviders), nil
}
idpProviders, err := provider.IDPProvidersByAggregateID(orgID)
if err != nil {
return nil, err
}
return iam_es_model.IDPProviderViewsToModel(idpProviders), nil
}
func checkVerificationTime(verificationTime time.Time, lifetime time.Duration) bool {
return verificationTime.Add(lifetime).After(time.Now().UTC())
}
@ -422,3 +666,37 @@ func userByID(ctx context.Context, viewProvider userViewProvider, eventProvider
}
return user_view_model.UserToModel(&userCopy), nil
}
func linkExternalIDPs(ctx context.Context, userEventProvider userEventProvider, request *model.AuthRequest) error {
externalIDPs := make([]*user_model.ExternalIDP, len(request.LinkingUsers))
for i, linkingUser := range request.LinkingUsers {
externalIDP := &user_model.ExternalIDP{
ObjectRoot: es_models.ObjectRoot{AggregateID: request.UserID},
IDPConfigID: linkingUser.IDPConfigID,
UserID: linkingUser.ExternalUserID,
DisplayName: linkingUser.DisplayName,
}
externalIDPs[i] = externalIDP
}
data := authz.CtxData{
UserID: "LOGIN",
OrgID: request.UserOrgID,
}
return userEventProvider.BulkAddExternalIDPs(authz.SetCtxData(ctx, data), request.UserID, externalIDPs)
}
func linkingIDPConfigExistingInAllowedIDPs(linkingUsers []*model.ExternalUser, idpProviders []*iam_model.IDPProviderView) bool {
for _, linkingUser := range linkingUsers {
exists := false
for _, idp := range idpProviders {
if idp.IDPConfigID == linkingUser.IDPConfigID {
exists = true
continue
}
}
if !exists {
return false
}
}
return true
}

View File

@ -88,12 +88,20 @@ func (m *mockEventUser) UserEventsByID(ctx context.Context, id string, sequence
return events, nil
}
func (m *mockEventUser) BulkAddExternalIDPs(ctx context.Context, userID string, externalIDPs []*user_model.ExternalIDP) error {
return nil
}
type mockEventErrUser struct{}
func (m *mockEventErrUser) UserEventsByID(ctx context.Context, id string, sequence uint64) ([]*es_models.Event, error) {
return nil, errors.ThrowInternal(nil, "id", "internal error")
}
func (m *mockEventErrUser) BulkAddExternalIDPs(ctx context.Context, userID string, externalIDPs []*user_model.ExternalIDP) error {
return errors.ThrowInternal(nil, "id", "internal error")
}
type mockViewUser struct {
InitRequired bool
PasswordSet bool
@ -185,6 +193,15 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
[]model.NextStep{&model.LoginStep{}},
nil,
},
{
"user not set no active session, linking users, external user not found option",
fields{
userSessionViewProvider: &mockViewNoUserSession{},
},
args{&model.AuthRequest{LinkingUsers: []*model.ExternalUser{{IDPConfigID: "IDPConfigID", ExternalUserID: "ExternalUserID"}}}, false},
[]model.NextStep{&model.ExternalNotFoundOptionStep{}},
nil,
},
{
"user not set, prompt select account and internal error, internal error",
fields{
@ -363,6 +380,24 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
[]model.NextStep{&model.InitPasswordStep{}},
nil,
},
{
"external user (no password set), callback",
fields{
userSessionViewProvider: &mockViewUserSession{
MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute),
},
userViewProvider: &mockViewUser{
IsEmailVerified: true,
MfaMaxSetUp: int32(model.MfaLevelSoftware),
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
MfaSoftwareCheckLifeTime: 18 * time.Hour,
},
args{&model.AuthRequest{UserID: "UserID", SelectedIDPConfigID: "IDPConfigID"}, false},
[]model.NextStep{&model.RedirectToCallbackStep{}},
nil,
},
{
"password not verified, password check step",
fields{
@ -378,6 +413,25 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
[]model.NextStep{&model.PasswordStep{}},
nil,
},
{
"external user (no password check needed), callback",
fields{
userSessionViewProvider: &mockViewUserSession{
MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute),
},
userViewProvider: &mockViewUser{
PasswordSet: true,
IsEmailVerified: true,
MfaMaxSetUp: int32(model.MfaLevelSoftware),
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
MfaSoftwareCheckLifeTime: 18 * time.Hour,
},
args{&model.AuthRequest{UserID: "UserID", SelectedIDPConfigID: "IDPConfigID"}, false},
[]model.NextStep{&model.RedirectToCallbackStep{}},
nil,
},
{
"mfa not verified, mfa check step",
fields{
@ -400,6 +454,28 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
}},
nil,
},
{
"external user, mfa not verified, mfa check step",
fields{
userSessionViewProvider: &mockViewUserSession{
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
},
userViewProvider: &mockViewUser{
PasswordSet: true,
OTPState: int32(user_model.MfaStateReady),
MfaMaxSetUp: int32(model.MfaLevelSoftware),
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
PasswordCheckLifeTime: 10 * 24 * time.Hour,
MfaSoftwareCheckLifeTime: 18 * time.Hour,
},
args{&model.AuthRequest{UserID: "UserID", SelectedIDPConfigID: "IDPConfigID"}, false},
[]model.NextStep{&model.MfaVerificationStep{
MfaProviders: []model.MfaType{model.MfaTypeOTP},
}},
nil,
},
{
"password change required and email verified, password change step",
fields{
@ -505,6 +581,30 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
[]model.NextStep{&model.RedirectToCallbackStep{}},
nil,
},
{
"linking users, link users step",
fields{
userSessionViewProvider: &mockViewUserSession{
MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute),
},
userViewProvider: &mockViewUser{
PasswordSet: true,
IsEmailVerified: true,
MfaMaxSetUp: int32(model.MfaLevelSoftware),
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
MfaSoftwareCheckLifeTime: 18 * time.Hour,
},
args{
&model.AuthRequest{
UserID: "UserID",
SelectedIDPConfigID: "IDPConfigID",
LinkingUsers: []*model.ExternalUser{{IDPConfigID: "IDPConfigID", ExternalUserID: "UserID", DisplayName: "DisplayName"}},
}, false},
[]model.NextStep{&model.LinkUsersStep{}},
nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -2,8 +2,9 @@ package eventstore
import (
"context"
"github.com/caos/logging"
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model"
auth_model "github.com/caos/zitadel/internal/auth/model"
auth_view "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
@ -41,7 +42,7 @@ func (repo *OrgRepository) SearchOrgs(ctx context.Context, request *org_model.Or
result := &org_model.OrgSearchResult{
Offset: request.Offset,
Limit: request.Limit,
TotalResult: uint64(count),
TotalResult: count,
Result: model.OrgsToModel(members),
}
if err == nil {
@ -71,7 +72,7 @@ func (repo *OrgRepository) RegisterOrg(ctx context.Context, register *auth_model
if err != nil {
return nil, err
}
user, userAggregates, err := repo.UserEventstore.PrepareRegisterUser(ctx, register.User, pwPolicy, orgPolicy, org.AggregateID)
user, userAggregates, err := repo.UserEventstore.PrepareRegisterUser(ctx, register.User, nil, pwPolicy, orgPolicy, org.AggregateID)
if err != nil {
return nil, err
}
@ -94,6 +95,18 @@ func (repo *OrgRepository) RegisterOrg(ctx context.Context, register *auth_model
return RegisterToModel(registerModel), nil
}
func (repo *OrgRepository) GetOrgIamPolicy(ctx context.Context, orgID string) (*org_model.OrgIAMPolicy, error) {
return repo.OrgEventstore.GetOrgIAMPolicy(ctx, policy_model.DefaultPolicy)
func (repo *OrgRepository) GetDefaultOrgIamPolicy(ctx context.Context) *org_model.OrgIAMPolicy {
return repo.OrgEventstore.GetDefaultOrgIAMPolicy(ctx)
}
func (repo *OrgRepository) GetOrgIamPolicy(ctx context.Context, orgID string) (*org_model.OrgIAMPolicy, error) {
return repo.OrgEventstore.GetOrgIAMPolicy(ctx, orgID)
}
func (repo *OrgRepository) GetIDPConfigByID(ctx context.Context, idpConfigID string) (*iam_model.IDPConfigView, error) {
idpConfig, err := repo.View.IDPConfigByID(idpConfigID)
if err != nil {
return nil, err
}
return iam_view_model.IDPConfigViewToModel(idpConfig), nil
}

View File

@ -21,6 +21,7 @@ import (
)
type UserRepo struct {
SearchLimit uint64
Eventstore eventstore.Eventstore
UserEvents *user_event.UserEventstore
OrgEvents *org_event.OrgEventstore
@ -32,7 +33,15 @@ func (repo *UserRepo) Health(ctx context.Context) error {
return repo.UserEvents.Health(ctx)
}
func (repo *UserRepo) Register(ctx context.Context, registerUser *model.User, orgMember *org_model.OrgMember, resourceOwner string) (*model.User, error) {
func (repo *UserRepo) Register(ctx context.Context, user *model.User, orgMember *org_model.OrgMember, resourceOwner string) (*model.User, error) {
return repo.registerUser(ctx, user, nil, orgMember, resourceOwner)
}
func (repo *UserRepo) RegisterExternalUser(ctx context.Context, user *model.User, externalIDP *model.ExternalIDP, orgMember *org_model.OrgMember, resourceOwner string) (*model.User, error) {
return repo.registerUser(ctx, user, externalIDP, orgMember, resourceOwner)
}
func (repo *UserRepo) registerUser(ctx context.Context, registerUser *model.User, externalIDP *model.ExternalIDP, orgMember *org_model.OrgMember, resourceOwner string) (*model.User, error) {
policyResourceOwner := authz.GetCtxData(ctx).OrgID
if resourceOwner != "" {
policyResourceOwner = resourceOwner
@ -45,7 +54,7 @@ func (repo *UserRepo) Register(ctx context.Context, registerUser *model.User, or
if err != nil {
return nil, err
}
user, aggregates, err := repo.UserEvents.PrepareRegisterUser(ctx, registerUser, pwPolicy, orgPolicy, resourceOwner)
user, aggregates, err := repo.UserEvents.PrepareRegisterUser(ctx, registerUser, externalIDP, pwPolicy, orgPolicy, resourceOwner)
if err != nil {
return nil, err
}
@ -87,6 +96,42 @@ func (repo *UserRepo) ChangeMyProfile(ctx context.Context, profile *model.Profil
return repo.UserEvents.ChangeProfile(ctx, profile)
}
func (repo *UserRepo) SearchMyExternalIDPs(ctx context.Context, request *model.ExternalIDPSearchRequest) (*model.ExternalIDPSearchResponse, error) {
request.EnsureLimit(repo.SearchLimit)
sequence, seqErr := repo.View.GetLatestExternalIDPSequence()
logging.Log("EVENT-5Jsi8").OnError(seqErr).Warn("could not read latest user sequence")
request.AppendUserQuery(authz.GetCtxData(ctx).UserID)
externalIDPS, count, err := repo.View.SearchExternalIDPs(request)
if err != nil {
return nil, err
}
result := &model.ExternalIDPSearchResponse{
Offset: request.Offset,
Limit: request.Limit,
TotalResult: count,
Result: usr_view_model.ExternalIDPViewsToModel(externalIDPS),
}
if seqErr == nil {
result.Sequence = sequence.CurrentSequence
result.Timestamp = sequence.CurrentTimestamp
}
return result, nil
}
func (repo *UserRepo) AddMyExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) (*model.ExternalIDP, error) {
if err := checkIDs(ctx, externalIDP.ObjectRoot); err != nil {
return nil, err
}
return repo.UserEvents.AddExternalIDP(ctx, externalIDP)
}
func (repo *UserRepo) RemoveMyExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) error {
if err := checkIDs(ctx, externalIDP.ObjectRoot); err != nil {
return err
}
return repo.UserEvents.RemoveExternalIDP(ctx, externalIDP)
}
func (repo *UserRepo) MyEmail(ctx context.Context) (*model.Email, error) {
user, err := repo.UserByID(ctx, authz.GetCtxData(ctx).UserID)
if err != nil {

View File

@ -53,6 +53,10 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, ev
iamEvents: repos.IamEvents,
iamID: systemDefaults.IamID},
&MachineKeys{handler: handler{view, bulkLimit, configs.cycleDuration("MachineKey"), errorCount}},
&LoginPolicy{handler: handler{view, bulkLimit, configs.cycleDuration("LoginPolicy"), errorCount}},
&IDPConfig{handler: handler{view, bulkLimit, configs.cycleDuration("IDPConfig"), errorCount}},
&IDPProvider{handler: handler{view, bulkLimit, configs.cycleDuration("IDPProvider"), errorCount}, systemDefaults: systemDefaults, orgEvents: repos.OrgEvents, iamEvents: repos.IamEvents},
&ExternalIDP{handler: handler{view, bulkLimit, configs.cycleDuration("ExternalIDP"), errorCount}, systemDefaults: systemDefaults, orgEvents: repos.OrgEvents, iamEvents: repos.IamEvents},
}
}

View File

@ -0,0 +1,82 @@
package handler
import (
"github.com/caos/logging"
"github.com/caos/zitadel/internal/eventstore/models"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/spooler"
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model"
"github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
)
type IDPConfig struct {
handler
}
const (
idpConfigTable = "auth.idp_configs"
)
func (m *IDPConfig) ViewModel() string {
return idpConfigTable
}
func (m *IDPConfig) EventQuery() (*models.SearchQuery, error) {
sequence, err := m.view.GetLatestIDPConfigSequence()
if err != nil {
return nil, err
}
return es_models.NewSearchQuery().
AggregateTypeFilter(model.OrgAggregate, iam_es_model.IAMAggregate).
LatestSequenceFilter(sequence.CurrentSequence), nil
}
func (m *IDPConfig) Reduce(event *models.Event) (err error) {
switch event.AggregateType {
case model.OrgAggregate:
err = m.processIdpConfig(iam_model.IDPProviderTypeOrg, event)
case iam_es_model.IAMAggregate:
err = m.processIdpConfig(iam_model.IDPProviderTypeSystem, event)
}
return err
}
func (m *IDPConfig) processIdpConfig(providerType iam_model.IDPProviderType, event *models.Event) (err error) {
idp := new(iam_view_model.IDPConfigView)
switch event.Type {
case model.IDPConfigAdded,
iam_es_model.IDPConfigAdded:
err = idp.AppendEvent(providerType, event)
case model.IDPConfigChanged, iam_es_model.IDPConfigChanged,
model.OIDCIDPConfigAdded, iam_es_model.OIDCIDPConfigAdded,
model.OIDCIDPConfigChanged, iam_es_model.OIDCIDPConfigChanged:
err = idp.SetData(event)
if err != nil {
return err
}
idp, err = m.view.IDPConfigByID(idp.IDPConfigID)
if err != nil {
return err
}
err = idp.AppendEvent(providerType, event)
case model.IDPConfigRemoved, iam_es_model.IDPConfigRemoved:
err = idp.SetData(event)
if err != nil {
return err
}
return m.view.DeleteIDPConfig(idp.IDPConfigID, event.Sequence)
default:
return m.view.ProcessedIDPConfigSequence(event.Sequence)
}
if err != nil {
return err
}
return m.view.PutIDPConfig(idp, idp.Sequence)
}
func (m *IDPConfig) OnError(event *models.Event, err error) error {
logging.LogWithFields("SPOOL-Ejf8s", "id", event.AggregateID).WithError(err).Warn("something went wrong in idp config handler")
return spooler.HandleError(event, err, m.view.GetLatestIDPConfigFailedEvent, m.view.ProcessedIDPConfigFailedEvent, m.view.ProcessedIDPConfigSequence, m.errorCountUntilSkip)
}

View File

@ -0,0 +1,120 @@
package handler
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/iam/repository/eventsourcing"
org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing"
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
"github.com/caos/zitadel/internal/eventstore/models"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/spooler"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model"
)
type IDPProvider struct {
handler
systemDefaults systemdefaults.SystemDefaults
iamEvents *eventsourcing.IAMEventstore
orgEvents *org_es.OrgEventstore
}
const (
idpProviderTable = "auth.idp_providers"
)
func (m *IDPProvider) ViewModel() string {
return idpProviderTable
}
func (m *IDPProvider) EventQuery() (*models.SearchQuery, error) {
sequence, err := m.view.GetLatestIDPProviderSequence()
if err != nil {
return nil, err
}
return es_models.NewSearchQuery().
AggregateTypeFilter(model.IAMAggregate, org_es_model.OrgAggregate).
LatestSequenceFilter(sequence.CurrentSequence), nil
}
func (m *IDPProvider) Reduce(event *models.Event) (err error) {
switch event.AggregateType {
case model.IAMAggregate, org_es_model.OrgAggregate:
err = m.processIdpProvider(event)
}
return err
}
func (m *IDPProvider) processIdpProvider(event *models.Event) (err error) {
provider := new(iam_view_model.IDPProviderView)
switch event.Type {
case model.LoginPolicyIDPProviderAdded, org_es_model.LoginPolicyIDPProviderAdded:
err = provider.AppendEvent(event)
if err != nil {
return err
}
err = m.fillData(provider)
case model.LoginPolicyIDPProviderRemoved, model.LoginPolicyIDPProviderCascadeRemoved,
org_es_model.LoginPolicyIDPProviderRemoved, org_es_model.LoginPolicyIDPProviderCascadeRemoved:
err = provider.SetData(event)
if err != nil {
return err
}
return m.view.DeleteIDPProvider(event.AggregateID, provider.IDPConfigID, event.Sequence)
case model.IDPConfigChanged, org_es_model.IDPConfigChanged:
config := new(iam_model.IDPConfig)
config.AppendEvent(event)
providers, err := m.view.IDPProvidersByIDPConfigID(config.IDPConfigID)
if err != nil {
return err
}
if provider.IDPProviderType == int32(iam_model.IDPProviderTypeSystem) {
config, err = m.iamEvents.GetIDPConfig(context.Background(), provider.AggregateID, config.IDPConfigID)
} else {
config, err = m.orgEvents.GetIDPConfig(context.Background(), provider.AggregateID, provider.IDPConfigID)
}
if err != nil {
return err
}
for _, provider := range providers {
m.fillConfigData(provider, config)
}
return m.view.PutIDPProviders(event.Sequence, providers...)
case org_es_model.LoginPolicyRemoved:
return m.view.DeleteIDPProvidersByAggregateID(event.AggregateID, event.Sequence)
default:
return m.view.ProcessedIDPProviderSequence(event.Sequence)
}
if err != nil {
return err
}
return m.view.PutIDPProvider(provider, provider.Sequence)
}
func (m *IDPProvider) fillData(provider *iam_view_model.IDPProviderView) (err error) {
var config *iam_model.IDPConfig
if provider.IDPProviderType == int32(iam_model.IDPProviderTypeSystem) {
config, err = m.iamEvents.GetIDPConfig(context.Background(), m.systemDefaults.IamID, provider.IDPConfigID)
} else {
config, err = m.orgEvents.GetIDPConfig(context.Background(), provider.AggregateID, provider.IDPConfigID)
}
if err != nil {
return err
}
m.fillConfigData(provider, config)
return nil
}
func (m *IDPProvider) fillConfigData(provider *iam_view_model.IDPProviderView, config *iam_model.IDPConfig) {
provider.Name = config.Name
provider.IDPConfigType = int32(config.Type)
}
func (m *IDPProvider) OnError(event *models.Event, err error) error {
logging.LogWithFields("SPOOL-Fjd89", "id", event.AggregateID).WithError(err).Warn("something went wrong in idp provider handler")
return spooler.HandleError(event, err, m.view.GetLatestIDPProviderFailedEvent, m.view.ProcessedIDPProviderFailedEvent, m.view.ProcessedIDPProviderSequence, m.errorCountUntilSkip)
}

View File

@ -0,0 +1,67 @@
package handler
import (
"github.com/caos/logging"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
"github.com/caos/zitadel/internal/eventstore/models"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/spooler"
iam_model "github.com/caos/zitadel/internal/iam/repository/view/model"
"github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
)
type LoginPolicy struct {
handler
}
const (
loginPolicyTable = "auth.login_policies"
)
func (m *LoginPolicy) ViewModel() string {
return loginPolicyTable
}
func (m *LoginPolicy) EventQuery() (*models.SearchQuery, error) {
sequence, err := m.view.GetLatestLoginPolicySequence()
if err != nil {
return nil, err
}
return es_models.NewSearchQuery().
AggregateTypeFilter(model.OrgAggregate, iam_es_model.IAMAggregate).
LatestSequenceFilter(sequence.CurrentSequence), nil
}
func (m *LoginPolicy) Reduce(event *models.Event) (err error) {
switch event.AggregateType {
case model.OrgAggregate, iam_es_model.IAMAggregate:
err = m.processLoginPolicy(event)
}
return err
}
func (m *LoginPolicy) processLoginPolicy(event *models.Event) (err error) {
policy := new(iam_model.LoginPolicyView)
switch event.Type {
case iam_es_model.LoginPolicyAdded, model.LoginPolicyAdded:
err = policy.AppendEvent(event)
case iam_es_model.LoginPolicyChanged, model.LoginPolicyChanged:
policy, err = m.view.LoginPolicyByAggregateID(event.AggregateID)
if err != nil {
return err
}
err = policy.AppendEvent(event)
default:
return m.view.ProcessedLoginPolicySequence(event.Sequence)
}
if err != nil {
return err
}
return m.view.PutLoginPolicy(policy, policy.Sequence)
}
func (m *LoginPolicy) OnError(event *models.Event, err error) error {
logging.LogWithFields("SPOOL-5id9s", "id", event.AggregateID).WithError(err).Warn("something went wrong in login policy handler")
return spooler.HandleError(event, err, m.view.GetLatestLoginPolicyFailedEvent, m.view.ProcessedLoginPolicyFailedEvent, m.view.ProcessedLoginPolicySequence, m.errorCountUntilSkip)
}

View File

@ -0,0 +1,126 @@
package handler
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/config/systemdefaults"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/iam/repository/eventsourcing"
org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing"
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
usr_view_model "github.com/caos/zitadel/internal/user/repository/view/model"
"github.com/caos/zitadel/internal/eventstore/models"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/spooler"
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
)
type ExternalIDP struct {
handler
systemDefaults systemdefaults.SystemDefaults
iamEvents *eventsourcing.IAMEventstore
orgEvents *org_es.OrgEventstore
}
const (
externalIDPTable = "auth.user_external_idps"
)
func (m *ExternalIDP) ViewModel() string {
return externalIDPTable
}
func (m *ExternalIDP) EventQuery() (*models.SearchQuery, error) {
sequence, err := m.view.GetLatestExternalIDPSequence()
if err != nil {
return nil, err
}
return es_models.NewSearchQuery().
AggregateTypeFilter(model.UserAggregate, iam_es_model.IAMAggregate, org_es_model.OrgAggregate).
LatestSequenceFilter(sequence.CurrentSequence), nil
}
func (m *ExternalIDP) Reduce(event *models.Event) (err error) {
switch event.AggregateType {
case model.UserAggregate:
err = m.processUser(event)
case iam_es_model.IAMAggregate, org_es_model.OrgAggregate:
err = m.processIdpConfig(event)
}
return err
}
func (m *ExternalIDP) processUser(event *models.Event) (err error) {
externalIDP := new(usr_view_model.ExternalIDPView)
switch event.Type {
case model.HumanExternalIDPAdded:
err = externalIDP.AppendEvent(event)
if err != nil {
return err
}
err = m.fillData(externalIDP)
case model.HumanExternalIDPRemoved, model.HumanExternalIDPCascadeRemoved:
err = externalIDP.SetData(event)
if err != nil {
return err
}
return m.view.DeleteExternalIDP(externalIDP.ExternalUserID, externalIDP.IDPConfigID, event.Sequence)
default:
return m.view.ProcessedExternalIDPSequence(event.Sequence)
}
if err != nil {
return err
}
return m.view.PutExternalIDP(externalIDP, externalIDP.Sequence)
}
func (m *ExternalIDP) processIdpConfig(event *models.Event) (err error) {
switch event.Type {
case iam_es_model.IDPConfigChanged, org_es_model.IDPConfigChanged:
config := new(iam_model.IDPConfig)
config.AppendEvent(event)
exterinalIDPs, err := m.view.ExternalIDPsByIDPConfigID(config.IDPConfigID)
if err != nil {
return err
}
if event.AggregateType == iam_es_model.IAMAggregate {
config, err = m.iamEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID)
} else {
config, err = m.orgEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID)
}
if err != nil {
return err
}
for _, provider := range exterinalIDPs {
m.fillConfigData(provider, config)
}
return m.view.PutExternalIDPs(event.Sequence, exterinalIDPs...)
default:
return m.view.ProcessedExternalIDPSequence(event.Sequence)
}
return nil
}
func (m *ExternalIDP) fillData(externalIDP *usr_view_model.ExternalIDPView) error {
config, err := m.orgEvents.GetIDPConfig(context.Background(), externalIDP.ResourceOwner, externalIDP.IDPConfigID)
if caos_errs.IsNotFound(err) {
config, err = m.iamEvents.GetIDPConfig(context.Background(), m.systemDefaults.IamID, externalIDP.IDPConfigID)
}
if err != nil {
return err
}
m.fillConfigData(externalIDP, config)
return nil
}
func (m *ExternalIDP) fillConfigData(externalIDP *usr_view_model.ExternalIDPView, config *iam_model.IDPConfig) {
externalIDP.IDPName = config.Name
}
func (m *ExternalIDP) OnError(event *models.Event, err error) error {
logging.LogWithFields("SPOOL-4Rsu8", "id", event.AggregateID).WithError(err).Warn("something went wrong in idp provider handler")
return spooler.HandleError(event, err, m.view.GetLatestExternalIDPFailedEvent, m.view.ProcessedExternalIDPFailedEvent, m.view.ProcessedExternalIDPSequence, m.errorCountUntilSkip)
}

View File

@ -128,6 +128,7 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, au
return &EsRepository{
spool,
eventstore.UserRepo{
SearchLimit: conf.SearchLimit,
Eventstore: es,
UserEvents: user,
OrgEvents: org,
@ -136,17 +137,22 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, au
},
eventstore.AuthRequestRepo{
UserEvents: user,
OrgEvents: org,
PolicyEvents: policy,
AuthRequests: authReq,
View: view,
UserSessionViewProvider: view,
UserViewProvider: view,
UserEventProvider: user,
OrgViewProvider: view,
IDPProviderViewProvider: view,
LoginPolicyViewProvider: view,
IdGenerator: idGenerator,
PasswordCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration,
MfaInitSkippedLifeTime: systemDefaults.VerificationLifetimes.MfaInitSkip.Duration,
MfaSoftwareCheckLifeTime: systemDefaults.VerificationLifetimes.MfaSoftwareCheck.Duration,
MfaHardwareCheckLifeTime: systemDefaults.VerificationLifetimes.MfaHardwareCheck.Duration,
IAMID: systemDefaults.IamID,
},
eventstore.TokenRepo{View: view},
eventstore.KeyRepository{

View File

@ -0,0 +1,73 @@
package view
import (
"github.com/caos/zitadel/internal/errors"
usr_model "github.com/caos/zitadel/internal/user/model"
"github.com/caos/zitadel/internal/user/repository/view"
"github.com/caos/zitadel/internal/user/repository/view/model"
global_view "github.com/caos/zitadel/internal/view/repository"
)
const (
externalIDPTable = "auth.user_external_idps"
)
func (v *View) ExternalIDPByExternalUserIDAndIDPConfigID(externalUserID, idpConfigID string) (*model.ExternalIDPView, error) {
return view.ExternalIDPByExternalUserIDAndIDPConfigID(v.Db, externalIDPTable, externalUserID, idpConfigID)
}
func (v *View) ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(externalUserID, idpConfigID, resourceOwner string) (*model.ExternalIDPView, error) {
return view.ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(v.Db, externalIDPTable, externalUserID, idpConfigID, resourceOwner)
}
func (v *View) ExternalIDPsByIDPConfigID(idpConfigID string) ([]*model.ExternalIDPView, error) {
return view.ExternalIDPsByIDPConfigID(v.Db, externalIDPTable, idpConfigID)
}
func (v *View) ExternalIDPsByUserID(userID string) ([]*model.ExternalIDPView, error) {
return view.ExternalIDPsByUserID(v.Db, externalIDPTable, userID)
}
func (v *View) SearchExternalIDPs(request *usr_model.ExternalIDPSearchRequest) ([]*model.ExternalIDPView, uint64, error) {
return view.SearchExternalIDPs(v.Db, externalIDPTable, request)
}
func (v *View) PutExternalIDP(externalIDP *model.ExternalIDPView, sequence uint64) error {
err := view.PutExternalIDP(v.Db, externalIDPTable, externalIDP)
if err != nil {
return err
}
return v.ProcessedExternalIDPSequence(sequence)
}
func (v *View) PutExternalIDPs(sequence uint64, externalIDPs ...*model.ExternalIDPView) error {
err := view.PutExternalIDPs(v.Db, externalIDPTable, externalIDPs...)
if err != nil {
return err
}
return v.ProcessedExternalIDPSequence(sequence)
}
func (v *View) DeleteExternalIDP(externalUserID, idpConfigID string, eventSequence uint64) error {
err := view.DeleteExternalIDP(v.Db, externalIDPTable, externalUserID, idpConfigID)
if err != nil && !errors.IsNotFound(err) {
return err
}
return v.ProcessedExternalIDPSequence(eventSequence)
}
func (v *View) GetLatestExternalIDPSequence() (*global_view.CurrentSequence, error) {
return v.latestSequence(externalIDPTable)
}
func (v *View) ProcessedExternalIDPSequence(eventSequence uint64) error {
return v.saveCurrentSequence(externalIDPTable, eventSequence)
}
func (v *View) GetLatestExternalIDPFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
return v.latestFailedEvent(externalIDPTable, sequence)
}
func (v *View) ProcessedExternalIDPFailedEvent(failedEvent *global_view.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@ -0,0 +1,57 @@
package view
import (
"github.com/caos/zitadel/internal/errors"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/iam/repository/view"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/view/model"
global_view "github.com/caos/zitadel/internal/view/repository"
)
const (
idpConfigTable = "auth.idp_configs"
)
func (v *View) IDPConfigByID(idpID string) (*iam_es_model.IDPConfigView, error) {
return view.IDPByID(v.Db, idpConfigTable, idpID)
}
func (v *View) GetIDPConfigsByAggregateID(aggregateID string) ([]*iam_es_model.IDPConfigView, error) {
return view.GetIDPConfigsByAggregateID(v.Db, idpConfigTable, aggregateID)
}
func (v *View) SearchIDPConfigs(request *iam_model.IDPConfigSearchRequest) ([]*iam_es_model.IDPConfigView, uint64, error) {
return view.SearchIDPs(v.Db, idpConfigTable, request)
}
func (v *View) PutIDPConfig(idp *iam_es_model.IDPConfigView, sequence uint64) error {
err := view.PutIDP(v.Db, idpConfigTable, idp)
if err != nil {
return err
}
return v.ProcessedIDPConfigSequence(sequence)
}
func (v *View) DeleteIDPConfig(idpID string, eventSequence uint64) error {
err := view.DeleteIDP(v.Db, idpConfigTable, idpID)
if err != nil && !errors.IsNotFound(err) {
return err
}
return v.ProcessedIDPConfigSequence(eventSequence)
}
func (v *View) GetLatestIDPConfigSequence() (*global_view.CurrentSequence, error) {
return v.latestSequence(idpConfigTable)
}
func (v *View) ProcessedIDPConfigSequence(eventSequence uint64) error {
return v.saveCurrentSequence(idpConfigTable, eventSequence)
}
func (v *View) GetLatestIDPConfigFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
return v.latestFailedEvent(idpConfigTable, sequence)
}
func (v *View) ProcessedIDPConfigFailedEvent(failedEvent *global_view.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@ -0,0 +1,77 @@
package view
import (
"github.com/caos/zitadel/internal/errors"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/iam/repository/view"
"github.com/caos/zitadel/internal/iam/repository/view/model"
global_view "github.com/caos/zitadel/internal/view/repository"
)
const (
idpProviderTable = "auth.idp_providers"
)
func (v *View) IDPProviderByAggregateAndIDPConfigID(aggregateID, idpConfigID string) (*model.IDPProviderView, error) {
return view.GetIDPProviderByAggregateIDAndConfigID(v.Db, idpProviderTable, aggregateID, idpConfigID)
}
func (v *View) IDPProvidersByIDPConfigID(idpConfigID string) ([]*model.IDPProviderView, error) {
return view.IDPProvidersByIdpConfigID(v.Db, idpProviderTable, idpConfigID)
}
func (v *View) IDPProvidersByAggregateID(aggregateID string) ([]*model.IDPProviderView, error) {
return view.IDPProvidersByAggregateID(v.Db, idpProviderTable, aggregateID)
}
func (v *View) SearchIDPProviders(request *iam_model.IDPProviderSearchRequest) ([]*model.IDPProviderView, uint64, error) {
return view.SearchIDPProviders(v.Db, idpProviderTable, request)
}
func (v *View) PutIDPProvider(provider *model.IDPProviderView, sequence uint64) error {
err := view.PutIDPProvider(v.Db, idpProviderTable, provider)
if err != nil {
return err
}
return v.ProcessedIDPProviderSequence(sequence)
}
func (v *View) PutIDPProviders(sequence uint64, providers ...*model.IDPProviderView) error {
err := view.PutIDPProviders(v.Db, idpProviderTable, providers...)
if err != nil {
return err
}
return v.ProcessedIDPProviderSequence(sequence)
}
func (v *View) DeleteIDPProvider(aggregateID, idpConfigID string, eventSequence uint64) error {
err := view.DeleteIDPProvider(v.Db, idpProviderTable, aggregateID, idpConfigID)
if err != nil && !errors.IsNotFound(err) {
return err
}
return v.ProcessedIDPProviderSequence(eventSequence)
}
func (v *View) DeleteIDPProvidersByAggregateID(aggregateID string, eventSequence uint64) error {
err := view.DeleteIDPProvidersByAggregateID(v.Db, idpProviderTable, aggregateID)
if err != nil && !errors.IsNotFound(err) {
return err
}
return v.ProcessedIDPProviderSequence(eventSequence)
}
func (v *View) GetLatestIDPProviderSequence() (*global_view.CurrentSequence, error) {
return v.latestSequence(idpProviderTable)
}
func (v *View) ProcessedIDPProviderSequence(eventSequence uint64) error {
return v.saveCurrentSequence(idpProviderTable, eventSequence)
}
func (v *View) GetLatestIDPProviderFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
return v.latestFailedEvent(idpProviderTable, sequence)
}
func (v *View) ProcessedIDPProviderFailedEvent(failedEvent *global_view.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@ -0,0 +1,48 @@
package view
import (
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/iam/repository/view"
"github.com/caos/zitadel/internal/iam/repository/view/model"
global_view "github.com/caos/zitadel/internal/view/repository"
)
const (
loginPolicyTable = "auth.login_policies"
)
func (v *View) LoginPolicyByAggregateID(aggregateID string) (*model.LoginPolicyView, error) {
return view.GetLoginPolicyByAggregateID(v.Db, loginPolicyTable, aggregateID)
}
func (v *View) PutLoginPolicy(policy *model.LoginPolicyView, sequence uint64) error {
err := view.PutLoginPolicy(v.Db, loginPolicyTable, policy)
if err != nil {
return err
}
return v.ProcessedLoginPolicySequence(sequence)
}
func (v *View) DeleteLoginPolicy(aggregateID string, eventSequence uint64) error {
err := view.DeleteLoginPolicy(v.Db, loginPolicyTable, aggregateID)
if err != nil && !errors.IsNotFound(err) {
return err
}
return v.ProcessedLoginPolicySequence(eventSequence)
}
func (v *View) GetLatestLoginPolicySequence() (*global_view.CurrentSequence, error) {
return v.latestSequence(loginPolicyTable)
}
func (v *View) ProcessedLoginPolicySequence(eventSequence uint64) error {
return v.saveCurrentSequence(loginPolicyTable, eventSequence)
}
func (v *View) GetLatestLoginPolicyFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
return v.latestFailedEvent(loginPolicyTable, sequence)
}
func (v *View) ProcessedLoginPolicyFailedEvent(failedEvent *global_view.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@ -23,6 +23,10 @@ func (v *View) UserByLoginName(loginName string) (*model.UserView, error) {
return view.UserByLoginName(v.Db, userTable, loginName)
}
func (v *View) UserByLoginNameAndResourceOwner(loginName, resourceOwner string) (*model.UserView, error) {
return view.UserByLoginNameAndResourceOwner(v.Db, userTable, loginName, resourceOwner)
}
func (v *View) UsersByOrgID(orgID string) ([]*model.UserView, error) {
return view.UsersByOrgID(v.Db, userTable, orgID)
}

View File

@ -3,10 +3,13 @@ package repository
import (
"context"
auth_model "github.com/caos/zitadel/internal/auth/model"
iam_model "github.com/caos/zitadel/internal/iam/model"
org_model "github.com/caos/zitadel/internal/org/model"
)
type OrgRepository interface {
RegisterOrg(context.Context, *auth_model.RegisterOrg) (*auth_model.RegisterOrg, error)
GetOrgIamPolicy(ctx context.Context, orgID string) (*org_model.OrgIAMPolicy, error)
GetDefaultOrgIamPolicy(ctx context.Context) *org_model.OrgIAMPolicy
GetIDPConfigByID(ctx context.Context, idpConfigID string) (*iam_model.IDPConfigView, error)
}

View File

@ -10,6 +10,7 @@ import (
type UserRepository interface {
Register(ctx context.Context, user *model.User, member *org_model.OrgMember, resourceOwner string) (*model.User, error)
RegisterExternalUser(ctx context.Context, user *model.User, externalIDP *model.ExternalIDP, member *org_model.OrgMember, resourceOwner string) (*model.User, error)
myUserRepo
SkipMfaInit(ctx context.Context, userID string) error
@ -58,6 +59,10 @@ type myUserRepo interface {
ChangeMyPassword(ctx context.Context, old, new string) error
SearchMyExternalIDPs(ctx context.Context, request *model.ExternalIDPSearchRequest) (*model.ExternalIDPSearchResponse, error)
AddMyExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) (*model.ExternalIDP, error)
RemoveMyExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) error
MyUserMfas(ctx context.Context) ([]*model.MultiFactor, error)
AddMyMfaOTP(ctx context.Context) (*model.OTP, error)
VerifyMyMfaOTPSetup(ctx context.Context, code string) error

View File

@ -1,6 +1,9 @@
package model
import (
"github.com/caos/zitadel/internal/iam/model"
"golang.org/x/text/language"
"strings"
"time"
"github.com/caos/zitadel/internal/errors"
@ -22,17 +25,36 @@ type AuthRequest struct {
MaxAuthAge uint32
Request Request
levelOfAssurance LevelOfAssurance
UserID string
LoginName string
DisplayName string
UserOrgID string
PossibleSteps []NextStep
PasswordVerified bool
MfasVerified []MfaType
Audience []string
AuthTime time.Time
Code string
levelOfAssurance LevelOfAssurance
UserID string
LoginName string
DisplayName string
UserOrgID string
SelectedIDPConfigID string
LinkingUsers []*ExternalUser
PossibleSteps []NextStep
PasswordVerified bool
MfasVerified []MfaType
Audience []string
AuthTime time.Time
Code string
LoginPolicy *model.LoginPolicyView
AllowedExternalIDPs []*model.IDPProviderView
}
type ExternalUser struct {
IDPConfigID string
ExternalUserID string
DisplayName string
PreferredUsername string
FirstName string
LastName string
NickName string
Email string
IsEmailVerified bool
PreferredLanguage language.Tag
Phone string
IsPhoneVerified bool
}
type Prompt int32
@ -103,3 +125,15 @@ func (a *AuthRequest) SetUserInfo(userID, loginName, displayName, userOrgID stri
a.DisplayName = displayName
a.UserOrgID = userOrgID
}
func (a *AuthRequest) GetScopeOrgID() string {
switch request := a.Request.(type) {
case *AuthRequestOIDC:
for _, scope := range request.Scopes {
if strings.HasPrefix(scope, OrgIDScope) {
strings.TrimPrefix(scope, OrgIDScope)
}
}
}
return ""
}

View File

@ -19,6 +19,8 @@ const (
NextStepMfaVerify
NextStepRedirectToCallback
NextStepChangeUsername
NextStepLinkUsers
NextStepExternalNotFoundOption
)
type UserSessionState int32
@ -53,6 +55,12 @@ type InitUserStep struct {
PasswordSet bool
}
type ExternalNotFoundOptionStep struct{}
func (s *ExternalNotFoundOptionStep) Type() NextStepType {
return NextStepExternalNotFoundOption
}
func (s *InitUserStep) Type() NextStepType {
return NextStepInitUser
}
@ -104,6 +112,12 @@ func (s *MfaVerificationStep) Type() NextStepType {
return NextStepMfaVerify
}
type LinkUsersStep struct{}
func (s *LinkUsersStep) Type() NextStepType {
return NextStepLinkUsers
}
type RedirectToCallbackStep struct{}
func (s *RedirectToCallbackStep) Type() NextStepType {

View File

@ -18,6 +18,10 @@ const (
AuthRequestTypeSAML
)
const (
OrgIDScope = "urn:zitadel:organisation:id:"
)
type AuthRequestOIDC struct {
Scopes []string
ResponseType OIDCResponseType

View File

@ -17,12 +17,14 @@ type IDPConfig struct {
type OIDCIDPConfig struct {
es_models.ObjectRoot
IDPConfigID string
ClientID string
ClientSecret *crypto.CryptoValue
ClientSecretString string
Issuer string
Scopes []string
IDPConfigID string
ClientID string
ClientSecret *crypto.CryptoValue
ClientSecretString string
Issuer string
Scopes []string
IDPDisplayNameMapping OIDCMappingField
UsernameMapping OIDCMappingField
}
type IdpConfigType int32
@ -40,6 +42,14 @@ const (
IDPConfigStateRemoved
)
type OIDCMappingField int32
const (
OIDCMappingFieldUnspecified OIDCMappingField = iota
OIDCMappingFieldPreferredLoginName
OIDCMappingFieldEmail
)
func NewIDPConfig(iamID, idpID string) *IDPConfig {
return &IDPConfig{ObjectRoot: es_models.ObjectRoot{AggregateID: iamID}, IDPConfigID: idpID}
}

View File

@ -17,11 +17,13 @@ type IDPConfigView struct {
Sequence uint64
IDPProviderType IDPProviderType
IsOIDC bool
OIDCClientID string
OIDCClientSecret *crypto.CryptoValue
OIDCIssuer string
OIDCScopes []string
IsOIDC bool
OIDCClientID string
OIDCClientSecret *crypto.CryptoValue
OIDCIssuer string
OIDCScopes []string
OIDCIDPDisplayNameMapping OIDCMappingField
OIDCUsernameMapping OIDCMappingField
}
type IDPConfigSearchRequest struct {

View File

@ -380,7 +380,10 @@ func (es *IAMEventstore) ChangeIDPOIDCConfig(ctx context.Context, config *iam_mo
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-Fms8w", "Errors.IAM.IdpIsNotOIDC")
}
if config.ClientSecretString != "" {
err = idp.OIDCConfig.CryptSecret(es.secretCrypto)
err = config.CryptSecret(es.secretCrypto)
if err != nil {
return nil, err
}
} else {
config.ClientSecret = nil
}
@ -467,20 +470,31 @@ func (es *IAMEventstore) AddIDPProviderToLoginPolicy(ctx context.Context, provid
return nil, caos_errs.ThrowInternal(nil, "EVENT-Slf9s", "Errors.Internal")
}
func (es *IAMEventstore) RemoveIDPProviderFromLoginPolicy(ctx context.Context, provider *iam_model.IDPProvider) error {
func (es *IAMEventstore) PrepareRemoveIDPProviderFromLoginPolicy(ctx context.Context, provider *iam_model.IDPProvider) (*model.IAM, *models.Aggregate, error) {
if provider == nil || !provider.IsValid() {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-Esi8c", "Errors.IdpProviderInvalid")
return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-Esi8c", "Errors.IdpProviderInvalid")
}
iam, err := es.IAMByID(ctx, provider.AggregateID)
if err != nil {
return err
return nil, nil, err
}
if _, m := iam.DefaultLoginPolicy.GetIdpProvider(provider.IdpConfigID); m == nil {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-29skr", "Errors.IAM.LoginPolicy.IdpProviderNotExisting")
return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-29skr", "Errors.IAM.LoginPolicy.IdpProviderNotExisting")
}
repoIam := model.IAMFromModel(iam)
addAggregate := LoginPolicyIDPProviderRemovedAggregate(es.Eventstore.AggregateCreator(), repoIam, &model.IDPProviderID{provider.IdpConfigID})
err = es_sdk.Push(ctx, es.PushAggregates, repoIam.AppendEvents, addAggregate)
removeAgg, err := LoginPolicyIDPProviderRemovedAggregate(ctx, es.Eventstore.AggregateCreator(), repoIam, &model.IDPProviderID{provider.IdpConfigID})
if err != nil {
return nil, nil, err
}
return repoIam, removeAgg, nil
}
func (es *IAMEventstore) RemoveIDPProviderFromLoginPolicy(ctx context.Context, provider *iam_model.IDPProvider) error {
repoIam, removeAgg, err := es.PrepareRemoveIDPProviderFromLoginPolicy(ctx, provider)
if err != nil {
return err
}
err = es_sdk.PushAggregates(ctx, es.PushAggregates, repoIam.AppendEvents, removeAgg)
if err != nil {
return err
}

View File

@ -285,17 +285,15 @@ func LoginPolicyIDPProviderAddedAggregate(aggCreator *es_models.AggregateCreator
}
}
func LoginPolicyIDPProviderRemovedAggregate(aggCreator *es_models.AggregateCreator, existing *model.IAM, provider *model.IDPProviderID) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if provider == nil || existing == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-Sml9d", "Errors.Internal")
}
agg, err := IAMAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.LoginPolicyIDPProviderRemoved, provider)
func LoginPolicyIDPProviderRemovedAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, existing *model.IAM, provider *model.IDPProviderID) (*es_models.Aggregate, error) {
if provider == nil || existing == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-Sml9d", "Errors.Internal")
}
agg, err := IAMAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.LoginPolicyIDPProviderRemoved, provider)
}
func checkExistingLoginPolicyValidation() func(...*es_models.Event) error {

View File

@ -1447,7 +1447,7 @@ func TestLoginPolicyIdpProviderRemovedAggregate(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
agg, err := LoginPolicyIDPProviderRemovedAggregate(tt.args.aggCreator, tt.args.existingIAM, tt.args.newProviderID)(tt.args.ctx)
agg, err := LoginPolicyIDPProviderRemovedAggregate(tt.args.ctx, tt.args.aggCreator, tt.args.existingIAM, tt.args.newProviderID)
if !tt.res.wantErr && len(agg.Events) != tt.res.eventLen {
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))

View File

@ -10,11 +10,12 @@ import (
type IDPConfig struct {
es_models.ObjectRoot
IDPConfigID string `json:"idpConfigId"`
State int32 `json:"-"`
Name string `json:"name,omitempty"`
Type int32 `json:"idpType,omitempty"`
LogoSrc []byte `json:"logoSrc,omitempty"`
IDPConfigID string `json:"idpConfigId"`
State int32 `json:"-"`
Name string `json:"name,omitempty"`
Type int32 `json:"idpType,omitempty"`
LogoSrc []byte `json:"logoSrc,omitempty"`
OIDCIDPConfig *OIDCIDPConfig `json:"-"`
}

View File

@ -12,11 +12,13 @@ import (
type OIDCIDPConfig struct {
es_models.ObjectRoot
IDPConfigID string `json:"idpConfigId"`
ClientID string `json:"clientId"`
ClientSecret *crypto.CryptoValue `json:"clientSecret,omitempty"`
Issuer string `json:"issuer,omitempty"`
Scopes pq.StringArray `json:"scopes,omitempty"`
IDPConfigID string `json:"idpConfigId"`
ClientID string `json:"clientId"`
ClientSecret *crypto.CryptoValue `json:"clientSecret,omitempty"`
Issuer string `json:"issuer,omitempty"`
Scopes pq.StringArray `json:"scopes,omitempty"`
IDPDisplayNameMapping int32 `json:"idpDisplayNameMapping,omitempty"`
UsernameMapping int32 `json:"usernameMapping,omitempty"`
}
func (c *OIDCIDPConfig) Changes(changed *OIDCIDPConfig) map[string]interface{} {
@ -25,7 +27,7 @@ func (c *OIDCIDPConfig) Changes(changed *OIDCIDPConfig) map[string]interface{} {
if c.ClientID != changed.ClientID {
changes["clientId"] = changed.ClientID
}
if c.ClientSecret != nil {
if c.ClientSecret != nil && c.ClientSecret != changed.ClientSecret {
changes["clientSecret"] = changed.ClientSecret
}
if c.Issuer != changed.Issuer {
@ -34,28 +36,38 @@ func (c *OIDCIDPConfig) Changes(changed *OIDCIDPConfig) map[string]interface{} {
if !reflect.DeepEqual(c.Scopes, changed.Scopes) {
changes["scopes"] = changed.Scopes
}
if c.IDPDisplayNameMapping != changed.IDPDisplayNameMapping {
changes["idpDisplayNameMapping"] = changed.IDPDisplayNameMapping
}
if c.UsernameMapping != changed.UsernameMapping {
changes["usernameMapping"] = changed.UsernameMapping
}
return changes
}
func OIDCIDPConfigFromModel(config *model.OIDCIDPConfig) *OIDCIDPConfig {
return &OIDCIDPConfig{
ObjectRoot: config.ObjectRoot,
IDPConfigID: config.IDPConfigID,
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
Issuer: config.Issuer,
Scopes: config.Scopes,
ObjectRoot: config.ObjectRoot,
IDPConfigID: config.IDPConfigID,
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
Issuer: config.Issuer,
Scopes: config.Scopes,
IDPDisplayNameMapping: int32(config.IDPDisplayNameMapping),
UsernameMapping: int32(config.UsernameMapping),
}
}
func OIDCIDPConfigToModel(config *OIDCIDPConfig) *model.OIDCIDPConfig {
return &model.OIDCIDPConfig{
ObjectRoot: config.ObjectRoot,
IDPConfigID: config.IDPConfigID,
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
Issuer: config.Issuer,
Scopes: config.Scopes,
ObjectRoot: config.ObjectRoot,
IDPConfigID: config.IDPConfigID,
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
Issuer: config.Issuer,
Scopes: config.Scopes,
IDPDisplayNameMapping: model.OIDCMappingField(config.IDPDisplayNameMapping),
UsernameMapping: model.OIDCMappingField(config.UsernameMapping),
}
}

View File

@ -22,7 +22,7 @@ func GetIDPProviderByAggregateIDAndConfigID(db *gorm.DB, table, aggregateID, idp
}
func IDPProvidersByIdpConfigID(db *gorm.DB, table string, idpConfigID string) ([]*model.IDPProviderView, error) {
members := make([]*model.IDPProviderView, 0)
providers := make([]*model.IDPProviderView, 0)
queries := []*iam_model.IDPProviderSearchQuery{
{
Key: iam_model.IDPProviderSearchKeyIdpConfigID,
@ -31,11 +31,28 @@ func IDPProvidersByIdpConfigID(db *gorm.DB, table string, idpConfigID string) ([
},
}
query := repository.PrepareSearchQuery(table, model.IDPProviderSearchRequest{Queries: queries})
_, err := query(db, &members)
_, err := query(db, &providers)
if err != nil {
return nil, err
}
return members, nil
return providers, nil
}
func IDPProvidersByAggregateID(db *gorm.DB, table string, aggregateID string) ([]*model.IDPProviderView, error) {
providers := make([]*model.IDPProviderView, 0)
queries := []*iam_model.IDPProviderSearchQuery{
{
Key: iam_model.IDPProviderSearchKeyAggregateID,
Value: aggregateID,
Method: global_model.SearchMethodEquals,
},
}
query := repository.PrepareSearchQuery(table, model.IDPProviderSearchRequest{Queries: queries})
_, err := query(db, &providers)
if err != nil {
return nil, err
}
return providers, nil
}
func SearchIDPProviders(db *gorm.DB, table string, req *iam_model.IDPProviderSearchRequest) ([]*model.IDPProviderView, uint64, error) {

View File

@ -20,6 +20,23 @@ func IDPByID(db *gorm.DB, table, idpID string) (*model.IDPConfigView, error) {
return idp, err
}
func GetIDPConfigsByAggregateID(db *gorm.DB, table string, aggregateID string) ([]*model.IDPConfigView, error) {
idps := make([]*model.IDPConfigView, 0)
queries := []*iam_model.IDPConfigSearchQuery{
{
Key: iam_model.IDPConfigSearchKeyAggregateID,
Value: aggregateID,
Method: global_model.SearchMethodEquals,
},
}
query := repository.PrepareSearchQuery(table, model.IDPConfigSearchRequest{Queries: queries})
_, err := query(db, &idps)
if err != nil {
return nil, err
}
return idps, nil
}
func SearchIDPs(db *gorm.DB, table string, req *iam_model.IDPConfigSearchRequest) ([]*model.IDPConfigView, uint64, error) {
idps := make([]*model.IDPConfigView, 0)
query := repository.PrepareSearchQuery(table, model.IDPConfigSearchRequest{Limit: req.Limit, Offset: req.Offset, Queries: req.Queries})

View File

@ -32,55 +32,61 @@ type IDPConfigView struct {
IDPState int32 `json:"-" gorm:"column:idp_state"`
IDPProviderType int32 `json:"-" gorm:"column:idp_provider_type"`
IsOIDC bool `json:"-" gorm:"column:is_oidc"`
OIDCClientID string `json:"clientId" gorm:"column:oidc_client_id"`
OIDCClientSecret *crypto.CryptoValue `json:"clientSecret" gorm:"column:oidc_client_secret"`
OIDCIssuer string `json:"issuer" gorm:"column:oidc_issuer"`
OIDCScopes pq.StringArray `json:"scopes" gorm:"column:oidc_scopes"`
IsOIDC bool `json:"-" gorm:"column:is_oidc"`
OIDCClientID string `json:"clientId" gorm:"column:oidc_client_id"`
OIDCClientSecret *crypto.CryptoValue `json:"clientSecret" gorm:"column:oidc_client_secret"`
OIDCIssuer string `json:"issuer" gorm:"column:oidc_issuer"`
OIDCScopes pq.StringArray `json:"scopes" gorm:"column:oidc_scopes"`
OIDCIDPDisplayNameMapping int32 `json:"idpDisplayNameMapping" gorm:"column:oidc_idp_display_name_mapping"`
OIDCUsernameMapping int32 `json:"usernameMapping" gorm:"column:oidc_idp_username_mapping"`
Sequence uint64 `json:"-" gorm:"column:sequence"`
}
func IDPConfigViewFromModel(idp *model.IDPConfigView) *IDPConfigView {
return &IDPConfigView{
IDPConfigID: idp.IDPConfigID,
AggregateID: idp.AggregateID,
Name: idp.Name,
LogoSrc: idp.LogoSrc,
Sequence: idp.Sequence,
CreationDate: idp.CreationDate,
ChangeDate: idp.ChangeDate,
IDPProviderType: int32(idp.IDPProviderType),
IsOIDC: idp.IsOIDC,
OIDCClientID: idp.OIDCClientID,
OIDCClientSecret: idp.OIDCClientSecret,
OIDCIssuer: idp.OIDCIssuer,
OIDCScopes: idp.OIDCScopes,
IDPConfigID: idp.IDPConfigID,
AggregateID: idp.AggregateID,
Name: idp.Name,
LogoSrc: idp.LogoSrc,
Sequence: idp.Sequence,
CreationDate: idp.CreationDate,
ChangeDate: idp.ChangeDate,
IDPProviderType: int32(idp.IDPProviderType),
IsOIDC: idp.IsOIDC,
OIDCClientID: idp.OIDCClientID,
OIDCClientSecret: idp.OIDCClientSecret,
OIDCIssuer: idp.OIDCIssuer,
OIDCScopes: idp.OIDCScopes,
OIDCIDPDisplayNameMapping: int32(idp.OIDCIDPDisplayNameMapping),
OIDCUsernameMapping: int32(idp.OIDCUsernameMapping),
}
}
func IdpConfigViewToModel(idp *IDPConfigView) *model.IDPConfigView {
func IDPConfigViewToModel(idp *IDPConfigView) *model.IDPConfigView {
return &model.IDPConfigView{
IDPConfigID: idp.IDPConfigID,
AggregateID: idp.AggregateID,
Name: idp.Name,
LogoSrc: idp.LogoSrc,
Sequence: idp.Sequence,
CreationDate: idp.CreationDate,
ChangeDate: idp.ChangeDate,
IDPProviderType: model.IDPProviderType(idp.IDPProviderType),
IsOIDC: idp.IsOIDC,
OIDCClientID: idp.OIDCClientID,
OIDCClientSecret: idp.OIDCClientSecret,
OIDCIssuer: idp.OIDCIssuer,
OIDCScopes: idp.OIDCScopes,
IDPConfigID: idp.IDPConfigID,
AggregateID: idp.AggregateID,
Name: idp.Name,
LogoSrc: idp.LogoSrc,
Sequence: idp.Sequence,
CreationDate: idp.CreationDate,
ChangeDate: idp.ChangeDate,
IDPProviderType: model.IDPProviderType(idp.IDPProviderType),
IsOIDC: idp.IsOIDC,
OIDCClientID: idp.OIDCClientID,
OIDCClientSecret: idp.OIDCClientSecret,
OIDCIssuer: idp.OIDCIssuer,
OIDCScopes: idp.OIDCScopes,
OIDCIDPDisplayNameMapping: model.OIDCMappingField(idp.OIDCIDPDisplayNameMapping),
OIDCUsernameMapping: model.OIDCMappingField(idp.OIDCUsernameMapping),
}
}
func IdpConfigViewsToModel(idps []*IDPConfigView) []*model.IDPConfigView {
result := make([]*model.IDPConfigView, len(idps))
for i, idp := range idps {
result[i] = IdpConfigViewToModel(idp)
result[i] = IDPConfigViewToModel(idp)
}
return result
}

View File

@ -39,6 +39,7 @@ func IDPProviderViewFromModel(policy *model.IDPProviderView) *IDPProviderView {
CreationDate: policy.CreationDate,
ChangeDate: policy.ChangeDate,
Name: policy.Name,
IDPConfigID: policy.IDPConfigID,
IDPConfigType: int32(policy.IDPConfigType),
IDPProviderType: int32(policy.IDPProviderType),
}
@ -51,6 +52,7 @@ func IDPProviderViewToModel(policy *IDPProviderView) *model.IDPProviderView {
CreationDate: policy.CreationDate,
ChangeDate: policy.ChangeDate,
Name: policy.Name,
IDPConfigID: policy.IDPConfigID,
IDPConfigType: model.IdpConfigType(policy.IDPConfigType),
IDPProviderType: model.IDPProviderType(policy.IDPProviderType),
}

View File

@ -8,6 +8,7 @@ import (
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/view/model"
iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model"
usr_model "github.com/caos/zitadel/internal/user/model"
"strings"
"github.com/caos/zitadel/internal/api/authz"
@ -218,7 +219,7 @@ func (repo *OrgRepository) IDPConfigByID(ctx context.Context, idpConfigID string
if err != nil {
return nil, err
}
return iam_view_model.IdpConfigViewToModel(idp), nil
return iam_view_model.IDPConfigViewToModel(idp), nil
}
func (repo *OrgRepository) AddOIDCIDPConfig(ctx context.Context, idp *iam_model.IDPConfig) (*iam_model.IDPConfig, error) {
idp.AggregateID = authz.GetCtxData(ctx).OrgID
@ -239,8 +240,27 @@ func (repo *OrgRepository) ReactivateIDPConfig(ctx context.Context, idpConfigID
}
func (repo *OrgRepository) RemoveIDPConfig(ctx context.Context, idpConfigID string) error {
aggregates := make([]*es_models.Aggregate, 0)
idp := iam_model.NewIDPConfig(authz.GetCtxData(ctx).OrgID, idpConfigID)
return repo.OrgEventstore.RemoveIDPConfig(ctx, idp)
_, agg, err := repo.OrgEventstore.PrepareRemoveIDPConfig(ctx, idp)
if err != nil {
}
aggregates = append(aggregates, agg)
externalIDPs, err := repo.View.ExternalIDPsByIDPConfigID(idpConfigID)
if err != nil {
return err
}
for _, externalIDP := range externalIDPs {
idpRemove := &usr_model.ExternalIDP{ObjectRoot: es_models.ObjectRoot{AggregateID: externalIDP.UserID}, IDPConfigID: externalIDP.IDPConfigID, UserID: externalIDP.ExternalUserID}
idpAgg := make([]*es_models.Aggregate, 0)
_, idpAgg, err = repo.UserEvents.PrepareRemoveExternalIDP(ctx, idpRemove, true)
if err != nil {
return err
}
aggregates = append(aggregates, idpAgg...)
}
return sdk.PushAggregates(ctx, repo.Eventstore.PushAggregates, nil, aggregates...)
}
func (repo *OrgRepository) ChangeOIDCIDPConfig(ctx context.Context, oidcConfig *iam_model.OIDCIDPConfig) (*iam_model.OIDCIDPConfig, error) {
@ -338,6 +358,25 @@ func (repo *OrgRepository) AddIDPProviderToLoginPolicy(ctx context.Context, prov
}
func (repo *OrgRepository) RemoveIDPProviderFromIdpProvider(ctx context.Context, provider *iam_model.IDPProvider) error {
aggregates := make([]*es_models.Aggregate, 0)
provider.AggregateID = authz.GetCtxData(ctx).OrgID
return repo.OrgEventstore.RemoveIDPProviderFromLoginPolicy(ctx, provider)
_, agg, err := repo.OrgEventstore.PrepareRemoveIDPProviderFromLoginPolicy(ctx, provider, false)
if err != nil {
return err
}
aggregates = append(aggregates, agg)
externalIDPs, err := repo.View.ExternalIDPsByIDPConfigID(provider.IdpConfigID)
if err != nil {
return err
}
for _, externalIDP := range externalIDPs {
idpRemove := &usr_model.ExternalIDP{ObjectRoot: es_models.ObjectRoot{AggregateID: externalIDP.UserID}, IDPConfigID: externalIDP.IDPConfigID, UserID: externalIDP.ExternalUserID}
idpAgg := make([]*es_models.Aggregate, 0)
_, idpAgg, err = repo.UserEvents.PrepareRemoveExternalIDP(ctx, idpRemove, true)
if err != nil {
return err
}
aggregates = append(aggregates, idpAgg...)
}
return sdk.PushAggregates(ctx, repo.Eventstore.PushAggregates, nil, aggregates...)
}

View File

@ -181,6 +181,31 @@ func (repo *UserRepo) ProfileByID(ctx context.Context, userID string) (*usr_mode
return user.GetProfile()
}
func (repo *UserRepo) SearchExternalIDPs(ctx context.Context, request *usr_model.ExternalIDPSearchRequest) (*usr_model.ExternalIDPSearchResponse, error) {
request.EnsureLimit(repo.SearchLimit)
sequence, seqErr := repo.View.GetLatestExternalIDPSequence()
logging.Log("EVENT-Qs7uf").OnError(seqErr).Warn("could not read latest external idp sequence")
externalIDPS, count, err := repo.View.SearchExternalIDPs(request)
if err != nil {
return nil, err
}
result := &usr_model.ExternalIDPSearchResponse{
Offset: request.Offset,
Limit: request.Limit,
TotalResult: count,
Result: model.ExternalIDPViewsToModel(externalIDPS),
}
if seqErr == nil {
result.Sequence = sequence.CurrentSequence
result.Timestamp = sequence.CurrentTimestamp
}
return result, nil
}
func (repo *UserRepo) RemoveExternalIDP(ctx context.Context, externalIDP *usr_model.ExternalIDP) error {
return repo.UserEvents.RemoveExternalIDP(ctx, externalIDP)
}
func (repo *UserRepo) ChangeMachine(ctx context.Context, machine *usr_model.Machine) (*usr_model.Machine, error) {
return repo.UserEvents.ChangeMachine(ctx, machine)
}

View File

@ -52,6 +52,7 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, ev
&IDPConfig{handler: handler{view, bulkLimit, configs.cycleDuration("IDPConfig"), errorCount}},
&LoginPolicy{handler: handler{view, bulkLimit, configs.cycleDuration("LoginPolicy"), errorCount}},
&IDPProvider{handler: handler{view, bulkLimit, configs.cycleDuration("IDPProvider"), errorCount}, systemDefaults: defaults, iamEvents: repos.IamEvents, orgEvents: repos.OrgEvents},
&ExternalIDP{handler: handler{view, bulkLimit, configs.cycleDuration("ExternalIDP"), errorCount}, systemDefaults: defaults, iamEvents: repos.IamEvents, orgEvents: repos.OrgEvents},
}
}

View File

@ -0,0 +1,126 @@
package handler
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/config/systemdefaults"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/iam/repository/eventsourcing"
org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing"
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
usr_view_model "github.com/caos/zitadel/internal/user/repository/view/model"
"github.com/caos/zitadel/internal/eventstore/models"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/spooler"
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
)
type ExternalIDP struct {
handler
systemDefaults systemdefaults.SystemDefaults
iamEvents *eventsourcing.IAMEventstore
orgEvents *org_es.OrgEventstore
}
const (
externalIDPTable = "management.user_external_idps"
)
func (m *ExternalIDP) ViewModel() string {
return externalIDPTable
}
func (m *ExternalIDP) EventQuery() (*models.SearchQuery, error) {
sequence, err := m.view.GetLatestExternalIDPSequence()
if err != nil {
return nil, err
}
return es_models.NewSearchQuery().
AggregateTypeFilter(model.UserAggregate, iam_es_model.IAMAggregate, org_es_model.OrgAggregate).
LatestSequenceFilter(sequence.CurrentSequence), nil
}
func (m *ExternalIDP) Reduce(event *models.Event) (err error) {
switch event.AggregateType {
case model.UserAggregate:
err = m.processUser(event)
case iam_es_model.IAMAggregate, org_es_model.OrgAggregate:
err = m.processIdpConfig(event)
}
return err
}
func (m *ExternalIDP) processUser(event *models.Event) (err error) {
externalIDP := new(usr_view_model.ExternalIDPView)
switch event.Type {
case model.HumanExternalIDPAdded:
err = externalIDP.AppendEvent(event)
if err != nil {
return err
}
err = m.fillData(externalIDP)
case model.HumanExternalIDPRemoved, model.HumanExternalIDPCascadeRemoved:
err = externalIDP.SetData(event)
if err != nil {
return err
}
return m.view.DeleteExternalIDP(externalIDP.ExternalUserID, externalIDP.IDPConfigID, event.Sequence)
default:
return m.view.ProcessedExternalIDPSequence(event.Sequence)
}
if err != nil {
return err
}
return m.view.PutExternalIDP(externalIDP, externalIDP.Sequence)
}
func (m *ExternalIDP) processIdpConfig(event *models.Event) (err error) {
switch event.Type {
case iam_es_model.IDPConfigChanged, org_es_model.IDPConfigChanged:
config := new(iam_model.IDPConfig)
config.AppendEvent(event)
exterinalIDPs, err := m.view.ExternalIDPsByIDPConfigID(config.IDPConfigID)
if err != nil {
return err
}
if event.AggregateType == iam_es_model.IAMAggregate {
config, err = m.iamEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID)
} else {
config, err = m.orgEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID)
}
if err != nil {
return err
}
for _, provider := range exterinalIDPs {
m.fillConfigData(provider, config)
}
return m.view.PutExternalIDPs(event.Sequence, exterinalIDPs...)
default:
return m.view.ProcessedExternalIDPSequence(event.Sequence)
}
return nil
}
func (m *ExternalIDP) fillData(externalIDP *usr_view_model.ExternalIDPView) error {
config, err := m.orgEvents.GetIDPConfig(context.Background(), externalIDP.ResourceOwner, externalIDP.IDPConfigID)
if caos_errs.IsNotFound(err) {
config, err = m.iamEvents.GetIDPConfig(context.Background(), m.systemDefaults.IamID, externalIDP.IDPConfigID)
}
if err != nil {
return err
}
m.fillConfigData(externalIDP, config)
return nil
}
func (m *ExternalIDP) fillConfigData(externalIDP *usr_view_model.ExternalIDPView, config *iam_model.IDPConfig) {
externalIDP.IDPName = config.Name
}
func (m *ExternalIDP) OnError(event *models.Event, err error) error {
logging.LogWithFields("SPOOL-4Rsu8", "id", event.AggregateID).WithError(err).Warn("something went wrong in idp provider handler")
return spooler.HandleError(event, err, m.view.GetLatestExternalIDPFailedEvent, m.view.ProcessedExternalIDPFailedEvent, m.view.ProcessedExternalIDPSequence, m.errorCountUntilSkip)
}

View File

@ -0,0 +1,73 @@
package view
import (
"github.com/caos/zitadel/internal/errors"
usr_model "github.com/caos/zitadel/internal/user/model"
"github.com/caos/zitadel/internal/user/repository/view"
"github.com/caos/zitadel/internal/user/repository/view/model"
global_view "github.com/caos/zitadel/internal/view/repository"
)
const (
externalIDPTable = "management.user_external_idps"
)
func (v *View) ExternalIDPByExternalUserIDAndIDPConfigID(externalUserID, idpConfigID string) (*model.ExternalIDPView, error) {
return view.ExternalIDPByExternalUserIDAndIDPConfigID(v.Db, externalIDPTable, externalUserID, idpConfigID)
}
func (v *View) ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(externalUserID, idpConfigID, resourceOwner string) (*model.ExternalIDPView, error) {
return view.ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(v.Db, externalIDPTable, externalUserID, idpConfigID, resourceOwner)
}
func (v *View) ExternalIDPsByIDPConfigID(idpConfigID string) ([]*model.ExternalIDPView, error) {
return view.ExternalIDPsByIDPConfigID(v.Db, externalIDPTable, idpConfigID)
}
func (v *View) ExternalIDPsByUserID(userID string) ([]*model.ExternalIDPView, error) {
return view.ExternalIDPsByUserID(v.Db, externalIDPTable, userID)
}
func (v *View) SearchExternalIDPs(request *usr_model.ExternalIDPSearchRequest) ([]*model.ExternalIDPView, uint64, error) {
return view.SearchExternalIDPs(v.Db, externalIDPTable, request)
}
func (v *View) PutExternalIDP(externalIDP *model.ExternalIDPView, sequence uint64) error {
err := view.PutExternalIDP(v.Db, externalIDPTable, externalIDP)
if err != nil {
return err
}
return v.ProcessedExternalIDPSequence(sequence)
}
func (v *View) PutExternalIDPs(sequence uint64, externalIDPs ...*model.ExternalIDPView) error {
err := view.PutExternalIDPs(v.Db, externalIDPTable, externalIDPs...)
if err != nil {
return err
}
return v.ProcessedExternalIDPSequence(sequence)
}
func (v *View) DeleteExternalIDP(externalUserID, idpConfigID string, eventSequence uint64) error {
err := view.DeleteExternalIDP(v.Db, externalIDPTable, externalUserID, idpConfigID)
if err != nil && !errors.IsNotFound(err) {
return err
}
return v.ProcessedExternalIDPSequence(eventSequence)
}
func (v *View) GetLatestExternalIDPSequence() (*global_view.CurrentSequence, error) {
return v.latestSequence(externalIDPTable)
}
func (v *View) ProcessedExternalIDPSequence(eventSequence uint64) error {
return v.saveCurrentSequence(externalIDPTable, eventSequence)
}
func (v *View) GetLatestExternalIDPFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
return v.latestFailedEvent(externalIDPTable, sequence)
}
func (v *View) ProcessedExternalIDPFailedEvent(failedEvent *global_view.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@ -31,6 +31,9 @@ type UserRepository interface {
UserMfas(ctx context.Context, userID string) ([]*model.MultiFactor, error)
SearchExternalIDPs(ctx context.Context, request *model.ExternalIDPSearchRequest) (*model.ExternalIDPSearchResponse, error)
RemoveExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) error
SearchMachineKeys(ctx context.Context, request *model.MachineKeySearchRequest) (*model.MachineKeySearchResponse, error)
GetMachineKey(ctx context.Context, userID, keyID string) (*model.MachineKeyView, error)
ChangeMachine(ctx context.Context, machine *model.Machine) (*model.Machine, error)

View File

@ -464,6 +464,10 @@ func (es *OrgEventstore) RemoveOrgMember(ctx context.Context, member *org_model.
return es_sdk.Push(ctx, es.PushAggregates, repoMember.AppendEvents, orgAggregate)
}
func (es *OrgEventstore) GetDefaultOrgIAMPolicy(ctx context.Context) *org_model.OrgIAMPolicy {
return es.defaultOrgIamPolicy
}
func (es *OrgEventstore) GetOrgIAMPolicy(ctx context.Context, orgID string) (*org_model.OrgIAMPolicy, error) {
existingOrg, err := es.OrgByID(ctx, org_model.NewOrg(orgID))
if err != nil && !errors.IsNotFound(err) {

View File

@ -386,7 +386,9 @@ func IDPConfigRemovedAggregate(ctx context.Context, aggCreator *es_models.Aggreg
if err != nil {
return nil, err
}
agg, err = agg.AppendEvent(model.IDPConfigRemoved, &iam_es_model.IDPConfigID{IDPConfigID: idp.IDPConfigID})
if err != nil {
return nil, err
}

View File

@ -26,6 +26,7 @@ Errors:
AddressNotFound: Addresse nicht gefunden
NotHuman: Der Benutzer muss eine Person sein
NotMachine: Der Benutzer muss technisch sein
NotAllowedToLink: Der Benutzer darf nicht mit einem externen Login Provider verlinkt werden
Username:
Reservied: Benutzername ist bereits vergeben
Code:
@ -45,6 +46,11 @@ Errors:
HasUpper: Passwort beinhaltet keinen Grossbuchstaben
HasNumber: Passwort beinhaltet keine Nummer
HasSymbol: Passwort beinhaltet kein Symbol
ExternalIDP:
Invalid: Externer IDP ungültig
IDPConfigNotExisting: IDP Provider ungültig für diese Organisation
NotAllowed: Externer IDP ist auf dieser Organisation nicht erlaubt.
MinimumExternalIDPNeeded: Mindestens ein IDP muss hinzugefügt werden.
Mfa:
Otp:
AlreadyReady: Multifaktor OTP (OneTimePassword) ist bereits eingerichtet

View File

@ -26,6 +26,7 @@ Errors:
AddressNotFound: Address not found
NotHuman: The User must be personal
NotMachine: The User must be technical
NotAllowedToLink: User is not allowed to link with external login provider
Username:
Reservied: Username is already taken
Code:
@ -45,6 +46,11 @@ Errors:
HasUpper: Password must contain upper case
HasNumber: Password must contain number
HasSymbol: Password must contain symbol
ExternalIDP:
Invalid: Externer IDP invalid
IDPConfigNotExisting: IDP provider invalid for this organisation
NotAllowed: External IDP not allowed on this organisation
MinimumExternalIDPNeeded: At least one IDP must be added
Mfa:
Otp:
AlreadyReady: Multifactor OTP (OneTimePassword) is already set up
@ -97,7 +103,7 @@ Errors:
MemberInvalid: Project member is invalid
MemberAlreadyExists: Project member already exists
MemberNotExisting: Project member doesn't exist
MinimumOneRoleNeeded: At least one role should be added
MinimumOneRoleNeeded: At least one role must be added
RoleAlreadyExists: Role already exists
RoleInvalid: Role is invalid
RoleNotExisting: Role doesn't exist

View File

@ -28,3 +28,7 @@ func (l *Login) getAuthRequestAndParseData(r *http.Request, data interface{}) (*
err = l.parser.Parse(r, data)
return authReq, err
}
func (l *Login) getParseData(r *http.Request, data interface{}) error {
return l.parser.Parse(r, data)
}

View File

@ -0,0 +1,269 @@
package handler
import (
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/rp"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/auth_request/model"
"github.com/caos/zitadel/internal/crypto"
caos_errors "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/models"
iam_model "github.com/caos/zitadel/internal/iam/model"
org_model "github.com/caos/zitadel/internal/org/model"
usr_model "github.com/caos/zitadel/internal/user/model"
"net/http"
"strings"
"time"
)
const (
queryIDPConfigID = "idpConfigID"
tmplExternalNotFoundOption = "externalnotfoundoption"
)
type externalIDPData struct {
IDPConfigID string `schema:"idpConfigID"`
}
type externalIDPCallbackData struct {
State string `schema:"state"`
Code string `schema:"code"`
}
type externalNotFoundOptionFormData struct {
Link bool `schema:"link"`
AutoRegister bool `schema:"autoregister"`
}
type externalNotFoundOptionData struct {
baseData
}
func (l *Login) handleExternalLogin(w http.ResponseWriter, r *http.Request) {
data := new(externalIDPData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if authReq == nil {
http.Redirect(w, r, l.zitadelURL, http.StatusFound)
return
}
idpConfig, err := l.getIDPConfigByID(r, data.IDPConfigID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, idpConfig.IDPConfigID, userAgentID)
if err != nil {
l.renderLogin(w, r, authReq, err)
return
}
if !idpConfig.IsOIDC {
l.renderError(w, r, authReq, caos_errors.ThrowInternal(nil, "LOGIN-Rio9s", "Errors.User.ExternalIDP.IDPTypeNotImplemented"))
return
}
l.handleOIDCAuthorize(w, r, authReq, idpConfig, EndpointExternalLoginCallback)
}
func (l *Login) handleOIDCAuthorize(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, idpConfig *iam_model.IDPConfigView, callbackEndpoint string) {
provider := l.getRPConfig(w, r, authReq, idpConfig, callbackEndpoint)
http.Redirect(w, r, rp.AuthURL(authReq.ID, provider), http.StatusFound)
}
func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Request) {
data := new(externalIDPCallbackData)
err := l.getParseData(r, data)
if err != nil {
l.renderError(w, r, nil, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
provider := l.getRPConfig(w, r, authReq, idpConfig, EndpointExternalLoginCallback)
tokens, err := rp.CodeExchange(r.Context(), data.Code, provider)
if err != nil {
l.renderLogin(w, r, authReq, err)
return
}
l.handleExternalUserAuthenticated(w, r, authReq, idpConfig, userAgentID, tokens)
}
func (l *Login) getRPConfig(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, idpConfig *iam_model.IDPConfigView, callbackEndpoint string) rp.RelayingParty {
oidcClientSecret, err := crypto.DecryptString(idpConfig.OIDCClientSecret, l.IDPConfigAesCrypto)
if err != nil {
l.renderError(w, r, authReq, err)
return nil
}
provider, err := rp.NewRelayingPartyOIDC(idpConfig.OIDCIssuer, idpConfig.OIDCClientID, oidcClientSecret, l.baseURL+callbackEndpoint, idpConfig.OIDCScopes, rp.WithVerifierOpts(rp.WithIssuedAtOffset(3*time.Second)))
if err != nil {
l.renderError(w, r, authReq, err)
return nil
}
return provider
}
func (l *Login) handleExternalUserAuthenticated(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, idpConfig *iam_model.IDPConfigView, userAgentID string, tokens *oidc.Tokens) {
externalUser := l.mapTokenToLoginUser(tokens, idpConfig)
err := l.authRepo.CheckExternalUserLogin(r.Context(), authReq.ID, userAgentID, externalUser)
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, nil)
return
}
l.renderNextStep(w, r, authReq)
}
func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) {
var errType, errMessage string
if err != nil {
errMessage = l.getErrorMessage(r, err)
}
data := externalNotFoundOptionData{
baseData: l.getBaseData(r, authReq, "ExternalNotFoundOption", errType, errMessage),
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplExternalNotFoundOption], data, nil)
}
func (l *Login) handleExternalNotFoundOptionCheck(w http.ResponseWriter, r *http.Request) {
data := new(externalNotFoundOptionFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if data.Link {
l.renderLogin(w, r, authReq, nil)
return
}
l.handleAutoRegister(w, r, authReq)
}
func (l *Login) handleAutoRegister(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest) {
orgIamPolicy, err := l.getOrgIamPolicy(r, authReq.GetScopeOrgID())
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, err)
return
}
iam, err := l.authRepo.GetIAM(r.Context())
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, err)
return
}
resourceOwner := iam.GlobalOrgID
member := &org_model.OrgMember{
ObjectRoot: models.ObjectRoot{AggregateID: iam.GlobalOrgID},
Roles: []string{orgProjectCreatorRole},
}
if authReq.GetScopeOrgID() != iam.GlobalOrgID && authReq.GetScopeOrgID() != "" {
member = nil
resourceOwner = authReq.GetScopeOrgID()
}
idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID)
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
user, externalIDP := l.mapExternalUserToLoginUser(orgIamPolicy, authReq.LinkingUsers[len(authReq.LinkingUsers)-1], idpConfig)
err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, member, authReq.ID, userAgentID, resourceOwner)
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, err)
return
}
l.renderNextStep(w, r, authReq)
}
func (l *Login) mapTokenToLoginUser(tokens *oidc.Tokens, idpConfig *iam_model.IDPConfigView) *model.ExternalUser {
displayName := tokens.IDTokenClaims.PreferredUsername
switch idpConfig.OIDCIDPDisplayNameMapping {
case iam_model.OIDCMappingFieldEmail:
if tokens.IDTokenClaims.EmailVerified && tokens.IDTokenClaims.Email != "" {
displayName = tokens.IDTokenClaims.Email
}
}
externalUser := &model.ExternalUser{
IDPConfigID: idpConfig.IDPConfigID,
ExternalUserID: tokens.IDTokenClaims.Subject,
PreferredUsername: tokens.IDTokenClaims.PreferredUsername,
DisplayName: displayName,
FirstName: tokens.IDTokenClaims.GivenName,
LastName: tokens.IDTokenClaims.FamilyName,
NickName: tokens.IDTokenClaims.Nickname,
Email: tokens.IDTokenClaims.Email,
IsEmailVerified: tokens.IDTokenClaims.EmailVerified,
}
if tokens.IDTokenClaims.PhoneNumber != "" {
externalUser.Phone = tokens.IDTokenClaims.PhoneNumber
externalUser.IsPhoneVerified = tokens.IDTokenClaims.PhoneNumberVerified
}
return externalUser
}
func (l *Login) mapExternalUserToLoginUser(orgIamPolicy *org_model.OrgIAMPolicy, linkingUser *model.ExternalUser, idpConfig *iam_model.IDPConfigView) (*usr_model.User, *usr_model.ExternalIDP) {
username := linkingUser.PreferredUsername
switch idpConfig.OIDCUsernameMapping {
case iam_model.OIDCMappingFieldEmail:
if linkingUser.IsEmailVerified && linkingUser.Email != "" {
username = linkingUser.Email
}
}
if orgIamPolicy.UserLoginMustBeDomain {
splittedUsername := strings.Split(username, "@")
if len(splittedUsername) > 1 {
username = splittedUsername[0]
}
}
user := &usr_model.User{
UserName: username,
Human: &usr_model.Human{
Profile: &usr_model.Profile{
FirstName: linkingUser.FirstName,
LastName: linkingUser.LastName,
PreferredLanguage: linkingUser.PreferredLanguage,
NickName: linkingUser.NickName,
},
Email: &usr_model.Email{
EmailAddress: linkingUser.Email,
IsEmailVerified: linkingUser.IsEmailVerified,
},
},
}
if linkingUser.Phone != "" {
user.Phone = &usr_model.Phone{
PhoneNumber: linkingUser.Phone,
IsPhoneVerified: linkingUser.IsPhoneVerified,
}
}
displayName := linkingUser.PreferredUsername
switch idpConfig.OIDCIDPDisplayNameMapping {
case iam_model.OIDCMappingFieldEmail:
if linkingUser.IsEmailVerified && linkingUser.Email != "" {
displayName = linkingUser.Email
}
}
externalIDP := &usr_model.ExternalIDP{
IDPConfigID: idpConfig.IDPConfigID,
UserID: linkingUser.ExternalUserID,
DisplayName: displayName,
}
return user, externalIDP
}

View File

@ -0,0 +1,155 @@
package handler
import (
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/rp"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/auth_request/model"
caos_errors "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/models"
iam_model "github.com/caos/zitadel/internal/iam/model"
org_model "github.com/caos/zitadel/internal/org/model"
usr_model "github.com/caos/zitadel/internal/user/model"
"net/http"
"strings"
)
func (l *Login) handleExternalRegister(w http.ResponseWriter, r *http.Request) {
data := new(externalIDPData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if authReq == nil {
http.Redirect(w, r, l.zitadelURL, http.StatusFound)
return
}
idpConfig, err := l.getIDPConfigByID(r, data.IDPConfigID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, idpConfig.IDPConfigID, userAgentID)
if err != nil {
l.renderLogin(w, r, authReq, err)
return
}
if !idpConfig.IsOIDC {
l.renderError(w, r, authReq, caos_errors.ThrowInternal(nil, "LOGIN-Rio9s", "Errors.User.ExternalIDP.IDPTypeNotImplemented"))
return
}
l.handleOIDCAuthorize(w, r, authReq, idpConfig, EndpointExternalRegisterCallback)
}
func (l *Login) handleExternalRegisterCallback(w http.ResponseWriter, r *http.Request) {
data := new(externalIDPCallbackData)
err := l.getParseData(r, data)
if err != nil {
l.renderError(w, r, nil, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
provider := l.getRPConfig(w, r, authReq, idpConfig, EndpointExternalRegisterCallback)
tokens, err := rp.CodeExchange(r.Context(), data.Code, provider)
if err != nil {
l.renderRegisterOption(w, r, authReq, err)
return
}
l.handleExternalUserRegister(w, r, authReq, idpConfig, userAgentID, tokens)
}
func (l *Login) handleExternalUserRegister(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, idpConfig *iam_model.IDPConfigView, userAgentID string, tokens *oidc.Tokens) {
orgIamPolicy, err := l.getOrgIamPolicy(r, authReq.GetScopeOrgID())
if err != nil {
l.renderRegisterOption(w, r, authReq, err)
return
}
iam, err := l.authRepo.GetIAM(r.Context())
if err != nil {
l.renderRegisterOption(w, r, authReq, err)
return
}
resourceOwner := iam.GlobalOrgID
member := &org_model.OrgMember{
ObjectRoot: models.ObjectRoot{AggregateID: iam.GlobalOrgID},
Roles: []string{orgProjectCreatorRole},
}
if authReq.GetScopeOrgID() != iam.GlobalOrgID && authReq.GetScopeOrgID() != "" {
member = nil
resourceOwner = authReq.GetScopeOrgID()
}
user, externalIDP := l.mapTokenToLoginUserAndExternalIDP(orgIamPolicy, tokens, idpConfig)
_, err = l.authRepo.RegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, member, resourceOwner)
if err != nil {
l.renderRegisterOption(w, r, authReq, err)
return
}
l.renderNextStep(w, r, authReq)
}
func (l *Login) mapTokenToLoginUserAndExternalIDP(orgIamPolicy *org_model.OrgIAMPolicy, tokens *oidc.Tokens, idpConfig *iam_model.IDPConfigView) (*usr_model.User, *usr_model.ExternalIDP) {
username := tokens.IDTokenClaims.PreferredUsername
switch idpConfig.OIDCUsernameMapping {
case iam_model.OIDCMappingFieldEmail:
if tokens.IDTokenClaims.EmailVerified && tokens.IDTokenClaims.Email != "" {
username = tokens.IDTokenClaims.Email
}
}
if orgIamPolicy.UserLoginMustBeDomain {
splittedUsername := strings.Split(username, "@")
if len(splittedUsername) > 1 {
username = splittedUsername[0]
}
}
user := &usr_model.User{
UserName: username,
Human: &usr_model.Human{
Profile: &usr_model.Profile{
FirstName: tokens.IDTokenClaims.GivenName,
LastName: tokens.IDTokenClaims.FamilyName,
PreferredLanguage: tokens.IDTokenClaims.Locale,
NickName: tokens.IDTokenClaims.Nickname,
},
Email: &usr_model.Email{
EmailAddress: tokens.IDTokenClaims.Email,
IsEmailVerified: tokens.IDTokenClaims.EmailVerified,
},
},
}
if tokens.IDTokenClaims.PhoneNumber != "" {
user.Phone = &usr_model.Phone{
PhoneNumber: tokens.IDTokenClaims.PhoneNumber,
IsPhoneVerified: tokens.IDTokenClaims.PhoneNumberVerified,
}
}
displayName := tokens.IDTokenClaims.PreferredUsername
switch idpConfig.OIDCIDPDisplayNameMapping {
case iam_model.OIDCMappingFieldEmail:
if tokens.IDTokenClaims.EmailVerified && tokens.IDTokenClaims.Email != "" {
displayName = tokens.IDTokenClaims.Email
}
}
externalIDP := &usr_model.ExternalIDP{
IDPConfigID: idpConfig.IDPConfigID,
UserID: tokens.IDTokenClaims.Subject,
DisplayName: displayName,
}
return user, externalIDP
}

View File

@ -0,0 +1,24 @@
package handler
import (
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"net/http"
"github.com/caos/zitadel/internal/auth_request/model"
)
const (
tmplLinkUsersDone = "linkusersdone"
)
func (l *Login) linkUsers(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) {
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.LinkExternalUsers(setContext(r.Context(), authReq.UserOrgID), authReq.ID, userAgentID)
l.renderLinkUsersDone(w, r, authReq, err)
}
func (l *Login) renderLinkUsersDone(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) {
var errType, errMessage string
data := l.getUserData(r, authReq, "Linking Users Done", errType, errMessage)
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplLinkUsersDone], data, nil)
}

View File

@ -2,6 +2,7 @@ package handler
import (
"context"
"github.com/caos/zitadel/internal/config/systemdefaults"
"net"
"net/http"
@ -28,11 +29,14 @@ type Login struct {
renderer *Renderer
parser *form.Parser
authRepo auth_repository.Repository
baseURL string
zitadelURL string
oidcAuthCallbackURL string
IDPConfigAesCrypto crypto.EncryptionAlgorithm
}
type Config struct {
BaseURL string
OidcAuthCallbackURL string
ZitadelURL string
LanguageCookieName string
@ -53,11 +57,17 @@ const (
handlerPrefix = "/login"
)
func CreateLogin(config Config, authRepo *eventsourcing.EsRepository, localDevMode bool) (*Login, string) {
func CreateLogin(config Config, authRepo *eventsourcing.EsRepository, systemDefaults systemdefaults.SystemDefaults, localDevMode bool) (*Login, string) {
aesCrypto, err := crypto.NewAESCrypto(systemDefaults.IDPConfigVerificationKey)
if err != nil {
logging.Log("HANDL-s90ew").WithError(err).Debug("error create new aes crypto")
}
login := &Login{
oidcAuthCallbackURL: config.OidcAuthCallbackURL,
baseURL: config.BaseURL,
zitadelURL: config.ZitadelURL,
authRepo: authRepo,
IDPConfigAesCrypto: aesCrypto,
}
prefix := ""
if localDevMode {

View File

@ -30,12 +30,12 @@ func (l *Login) handleLogin(w http.ResponseWriter, r *http.Request) {
}
func (l *Login) handleLoginName(w http.ResponseWriter, r *http.Request) {
authSession, err := l.getAuthRequest(r)
authReq, err := l.getAuthRequest(r)
if err != nil {
l.renderError(w, r, authSession, err)
l.renderError(w, r, authReq, err)
return
}
l.renderLogin(w, r, authSession, nil)
l.renderLogin(w, r, authReq, nil)
}
func (l *Login) handleLoginNameCheck(w http.ResponseWriter, r *http.Request) {
@ -46,6 +46,10 @@ func (l *Login) handleLoginNameCheck(w http.ResponseWriter, r *http.Request) {
return
}
if data.Register {
if authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowExternalIDP && authReq.AllowedExternalIDPs != nil && len(authReq.AllowedExternalIDPs) > 0 {
l.handleRegisterOption(w, r)
return
}
l.handleRegister(w, r)
return
}

View File

@ -1,10 +0,0 @@
package handler
import (
org_model "github.com/caos/zitadel/internal/org/model"
"net/http"
)
func (l *Login) getOrgIamPolicy(r *http.Request, orgID string) (*org_model.OrgIAMPolicy, error) {
return l.authRepo.GetOrgIamPolicy(r.Context(), orgID)
}

View File

@ -0,0 +1,18 @@
package handler
import (
iam_model "github.com/caos/zitadel/internal/iam/model"
org_model "github.com/caos/zitadel/internal/org/model"
"net/http"
)
func (l *Login) getOrgIamPolicy(r *http.Request, orgID string) (*org_model.OrgIAMPolicy, error) {
if orgID == "" {
return l.authRepo.GetDefaultOrgIamPolicy(r.Context()), nil
}
return l.authRepo.GetOrgIamPolicy(r.Context(), orgID)
}
func (l *Login) getIDPConfigByID(r *http.Request, idpConfigID string) (*iam_model.IDPConfigView, error) {
return l.authRepo.GetIDPConfigByID(r.Context(), idpConfigID)
}

View File

@ -66,11 +66,16 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) {
return
}
resourceOwner := iam.GlobalOrgID
member := &org_model.OrgMember{
ObjectRoot: models.ObjectRoot{AggregateID: iam.GlobalOrgID},
Roles: []string{orgProjectCreatorRole},
}
user, err := l.authRepo.Register(setContext(r.Context(), iam.GlobalOrgID), data.toUserModel(), member, iam.GlobalOrgID)
if authRequest.GetScopeOrgID() != "" && authRequest.GetScopeOrgID() != iam.GlobalOrgID {
member = nil
resourceOwner = authRequest.GetScopeOrgID()
}
user, err := l.authRepo.Register(setContext(r.Context(), resourceOwner), data.toUserModel(), member, resourceOwner)
if err != nil {
l.renderRegister(w, r, authRequest, data, err)
return

View File

@ -0,0 +1,53 @@
package handler
import (
"github.com/caos/zitadel/internal/auth_request/model"
"net/http"
)
const (
tmplRegisterOption = "registeroption"
)
type registerOptionFormData struct {
UsernamePassword bool `schema:"usernamepassword"`
}
type registerOptionData struct {
baseData
}
func (l *Login) handleRegisterOption(w http.ResponseWriter, r *http.Request) {
data := new(registerOptionFormData)
authRequest, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authRequest, err)
return
}
l.renderRegisterOption(w, r, authRequest, nil)
}
func (l *Login) renderRegisterOption(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) {
var errType, errMessage string
if err != nil {
errMessage = l.getErrorMessage(r, err)
}
data := registerOptionData{
baseData: l.getBaseData(r, authReq, "RegisterOption", errType, errMessage),
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplRegisterOption], data, nil)
}
func (l *Login) handleRegisterOptionCheck(w http.ResponseWriter, r *http.Request) {
data := new(registerOptionFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if data.UsernamePassword {
l.handleRegister(w, r)
return
}
l.handleRegisterOption(w, r)
}

View File

@ -3,14 +3,14 @@ package handler
import (
"errors"
"fmt"
"github.com/caos/logging"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/gorilla/csrf"
"golang.org/x/text/language"
"html/template"
"net/http"
"path"
"github.com/caos/logging"
"github.com/gorilla/csrf"
"golang.org/x/text/language"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/auth_request/model"
caos_errs "github.com/caos/zitadel/internal/errors"
@ -32,28 +32,31 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str
pathPrefix: pathPrefix,
}
tmplMapping := map[string]string{
tmplError: "error.html",
tmplLogin: "login.html",
tmplUserSelection: "select_user.html",
tmplPassword: "password.html",
tmplMfaVerify: "mfa_verify.html",
tmplMfaPrompt: "mfa_prompt.html",
tmplMfaInitVerify: "mfa_init_verify.html",
tmplMfaInitDone: "mfa_init_done.html",
tmplMailVerification: "mail_verification.html",
tmplMailVerified: "mail_verified.html",
tmplInitPassword: "init_password.html",
tmplInitPasswordDone: "init_password_done.html",
tmplInitUser: "init_user.html",
tmplInitUserDone: "init_user_done.html",
tmplPasswordResetDone: "password_reset_done.html",
tmplChangePassword: "change_password.html",
tmplChangePasswordDone: "change_password_done.html",
tmplRegister: "register.html",
tmplLogoutDone: "logout_done.html",
tmplRegisterOrg: "register_org.html",
tmplChangeUsername: "change_username.html",
tmplChangeUsernameDone: "change_username_done.html",
tmplError: "error.html",
tmplLogin: "login.html",
tmplUserSelection: "select_user.html",
tmplPassword: "password.html",
tmplMfaVerify: "mfa_verify.html",
tmplMfaPrompt: "mfa_prompt.html",
tmplMfaInitVerify: "mfa_init_verify.html",
tmplMfaInitDone: "mfa_init_done.html",
tmplMailVerification: "mail_verification.html",
tmplMailVerified: "mail_verified.html",
tmplInitPassword: "init_password.html",
tmplInitPasswordDone: "init_password_done.html",
tmplInitUser: "init_user.html",
tmplInitUserDone: "init_user_done.html",
tmplPasswordResetDone: "password_reset_done.html",
tmplChangePassword: "change_password.html",
tmplChangePasswordDone: "change_password_done.html",
tmplRegisterOption: "register_option.html",
tmplRegister: "register.html",
tmplLogoutDone: "logout_done.html",
tmplRegisterOrg: "register_org.html",
tmplChangeUsername: "change_username.html",
tmplChangeUsernameDone: "change_username_done.html",
tmplLinkUsersDone: "link_users_done.html",
tmplExternalNotFoundOption: "external_not_found_option.html",
}
funcs := map[string]interface{}{
"resourceUrl": func(file string) string {
@ -65,6 +68,12 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str
"loginUrl": func() string {
return path.Join(r.pathPrefix, EndpointLogin)
},
"externalIDPAuthURL": func(authReqID, idpConfigID string) string {
return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s&%s=%s", EndpointExternalLogin, queryAuthRequestID, authReqID, queryIDPConfigID, idpConfigID))
},
"externalIDPRegisterURL": func(authReqID, idpConfigID string) string {
return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s&%s=%s", EndpointExternalRegister, queryAuthRequestID, authReqID, queryIDPConfigID, idpConfigID))
},
"registerUrl": func(id string) string {
return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s", EndpointRegister, queryAuthRequestID, id))
},
@ -107,6 +116,9 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str
"changePasswordUrl": func() string {
return path.Join(r.pathPrefix, EndpointChangePassword)
},
"registerOptionUrl": func() string {
return path.Join(r.pathPrefix, EndpointRegisterOption)
},
"registrationUrl": func() string {
return path.Join(r.pathPrefix, EndpointRegister)
},
@ -116,6 +128,9 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str
"changeUsernameUrl": func() string {
return path.Join(r.pathPrefix, EndpointChangeUsername)
},
"externalNotFoundOptionUrl": func() string {
return path.Join(r.pathPrefix, EndpointExternalNotFoundOption)
},
"selectedLanguage": func(l string) bool {
return false
},
@ -186,6 +201,10 @@ func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq *
l.renderInitUser(w, r, authReq, "", "", step.PasswordSet, nil)
case *model.ChangeUsernameStep:
l.renderChangeUsername(w, r, authReq, nil)
case *model.LinkUsersStep:
l.linkUsers(w, r, authReq, err)
case *model.ExternalNotFoundOptionStep:
l.renderExternalNotFoundOption(w, r, authReq, err)
default:
l.renderInternalError(w, r, authReq, caos_errs.ThrowInternal(nil, "APP-ds3QF", "step no possible"))
}
@ -204,11 +223,12 @@ func (l *Login) getUserData(r *http.Request, authReq *model.AuthRequest, title s
return userData{
baseData: l.getBaseData(r, authReq, title, errType, errMessage),
profileData: l.getProfileData(authReq),
Linking: len(authReq.LinkingUsers) > 0,
}
}
func (l *Login) getBaseData(r *http.Request, authReq *model.AuthRequest, title string, errType, errMessage string) baseData {
return baseData{
baseData := baseData{
errorData: errorData{
ErrType: errType,
ErrMessage: errMessage,
@ -217,10 +237,16 @@ func (l *Login) getBaseData(r *http.Request, authReq *model.AuthRequest, title s
Title: title,
Theme: l.getTheme(r),
ThemeMode: l.getThemeMode(r),
OrgID: l.getOrgID(authReq),
AuthReqID: getRequestID(authReq, r),
CSRF: csrf.TemplateField(r),
Nonce: http_mw.GetNonce(r),
}
if authReq != nil {
baseData.LoginPolicy = authReq.LoginPolicy
baseData.IDPProviders = authReq.AllowedExternalIDPs
}
return baseData
}
func (l *Login) getProfileData(authReq *model.AuthRequest) profileData {
@ -253,6 +279,19 @@ func (l *Login) getThemeMode(r *http.Request) string {
return "" //TODO: impl
}
func (l *Login) getOrgID(authReq *model.AuthRequest) string {
if authReq == nil {
return ""
}
if authReq.UserOrgID != "" {
return authReq.UserOrgID
}
if authReq.Request == nil {
return ""
}
return authReq.GetScopeOrgID()
}
func getRequestID(authReq *model.AuthRequest, r *http.Request) string {
if authReq != nil {
return authReq.ID
@ -275,13 +314,16 @@ func (l *Login) cspErrorHandler(err error) http.Handler {
type baseData struct {
errorData
Lang string
Title string
Theme string
ThemeMode string
AuthReqID string
CSRF template.HTML
Nonce string
Lang string
Title string
Theme string
ThemeMode string
OrgID string
AuthReqID string
CSRF template.HTML
Nonce string
LoginPolicy *iam_model.LoginPolicyView
IDPProviders []*iam_model.IDPProviderView
}
type errorData struct {
@ -295,6 +337,7 @@ type userData struct {
PasswordChecked string
MfaProviders []model.MfaType
SelectedMfaProvider model.MfaType
Linking bool
}
type profileData struct {
@ -315,7 +358,8 @@ type passwordData struct {
type userSelectionData struct {
baseData
Users []model.UserSelection
Users []model.UserSelection
Linking bool
}
type mfaData struct {

View File

@ -7,26 +7,32 @@ import (
)
const (
EndpointRoot = "/"
EndpointHealthz = "/healthz"
EndpointReadiness = "/ready"
EndpointLogin = "/login"
EndpointLoginName = "/loginname"
EndpointUserSelection = "/userselection"
EndpointChangeUsername = "/username/change"
EndpointPassword = "/password"
EndpointInitPassword = "/password/init"
EndpointChangePassword = "/password/change"
EndpointPasswordReset = "/password/reset"
EndpointInitUser = "/user/init"
EndpointMfaVerify = "/mfa/verify"
EndpointMfaPrompt = "/mfa/prompt"
EndpointMfaInitVerify = "/mfa/init/verify"
EndpointMailVerification = "/mail/verification"
EndpointMailVerified = "/mail/verified"
EndpointRegister = "/register"
EndpointRegisterOrg = "/register/org"
EndpointLogoutDone = "/logout/done"
EndpointRoot = "/"
EndpointHealthz = "/healthz"
EndpointReadiness = "/ready"
EndpointLogin = "/login"
EndpointExternalLogin = "/login/externalidp"
EndpointExternalLoginCallback = "/login/externalidp/callback"
EndpointLoginName = "/loginname"
EndpointUserSelection = "/userselection"
EndpointChangeUsername = "/username/change"
EndpointPassword = "/password"
EndpointInitPassword = "/password/init"
EndpointChangePassword = "/password/change"
EndpointPasswordReset = "/password/reset"
EndpointInitUser = "/user/init"
EndpointMfaVerify = "/mfa/verify"
EndpointMfaPrompt = "/mfa/prompt"
EndpointMfaInitVerify = "/mfa/init/verify"
EndpointMailVerification = "/mail/verification"
EndpointMailVerified = "/mail/verified"
EndpointRegisterOption = "/register/option"
EndpointRegister = "/register"
EndpointExternalRegister = "/register/externalidp"
EndpointExternalRegisterCallback = "/register/externalidp/callback"
EndpointRegisterOrg = "/register/org"
EndpointLogoutDone = "/logout/done"
EndpointExternalNotFoundOption = "/externaluser/option"
EndpointResources = "/resources"
)
@ -38,6 +44,8 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M
router.HandleFunc(EndpointHealthz, login.handleHealthz).Methods(http.MethodGet)
router.HandleFunc(EndpointReadiness, login.handleReadiness).Methods(http.MethodGet)
router.HandleFunc(EndpointLogin, login.handleLogin).Methods(http.MethodGet, http.MethodPost)
router.HandleFunc(EndpointExternalLogin, login.handleExternalLogin).Methods(http.MethodGet)
router.HandleFunc(EndpointExternalLoginCallback, login.handleExternalLoginCallback).Methods(http.MethodGet)
router.HandleFunc(EndpointLoginName, login.handleLoginName).Methods(http.MethodGet)
router.HandleFunc(EndpointLoginName, login.handleLoginNameCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointUserSelection, login.handleSelectUser).Methods(http.MethodPost)
@ -55,8 +63,13 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M
router.HandleFunc(EndpointMailVerification, login.handleMailVerification).Methods(http.MethodGet)
router.HandleFunc(EndpointMailVerification, login.handleMailVerificationCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointChangePassword, login.handleChangePassword).Methods(http.MethodPost)
router.HandleFunc(EndpointRegisterOption, login.handleRegisterOption).Methods(http.MethodGet)
router.HandleFunc(EndpointRegisterOption, login.handleRegisterOptionCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointExternalNotFoundOption, login.handleExternalNotFoundOptionCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointRegister, login.handleRegister).Methods(http.MethodGet)
router.HandleFunc(EndpointRegister, login.handleRegisterCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointExternalRegister, login.handleExternalRegister).Methods(http.MethodGet)
router.HandleFunc(EndpointExternalRegisterCallback, login.handleExternalRegisterCallback).Methods(http.MethodGet)
router.HandleFunc(EndpointLogoutDone, login.handleLogoutDone).Methods(http.MethodGet)
router.PathPrefix(EndpointResources).Handler(login.handleResources(staticDir)).Methods(http.MethodGet)
router.HandleFunc(EndpointRegisterOrg, login.handleRegisterOrg).Methods(http.MethodGet)

View File

@ -19,6 +19,7 @@ func (l *Login) renderUserSelection(w http.ResponseWriter, r *http.Request, auth
data := userSelectionData{
baseData: l.getBaseData(r, authReq, "Select User", "", ""),
Users: selectionData.Users,
Linking: len(authReq.LinkingUsers) > 0,
}
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplUserSelection], data, nil)
}

View File

@ -2,6 +2,7 @@ package login
import (
"github.com/caos/zitadel/internal/auth/repository/eventsourcing"
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/ui/login/handler"
)
@ -9,6 +10,6 @@ type Config struct {
Handler handler.Config
}
func Start(config Config, authRepo *eventsourcing.EsRepository, localDevMode bool) (*handler.Login, string) {
return handler.CreateLogin(config.Handler, authRepo, localDevMode)
func Start(config Config, authRepo *eventsourcing.EsRepository, systemdefaults systemdefaults.SystemDefaults, localDevMode bool) (*handler.Login, string) {
return handler.CreateLogin(config.Handler, authRepo, systemdefaults, localDevMode)
}

View File

@ -11,12 +11,17 @@ Password:
Login:
Title: Anmeldung
Description: Gib deine Benutzerdaten ein.
TitleLinking: Anmeldung für Benutzer Linking
DescriptionLinking: Gib deine Benutzerdaten ein um den externen Benutzer mit einem ZITADEL Benutzer zu linken.
Loginname: Loginname
LoginnamePlaceHolder: username@domain
ExternalLogin: Melde dich mit einem externen Benutzer an
UserSelection:
Title: Account auswählen
Description: Wähle deinen Account aus.
TitleLinking: Account auswählen um zu verlinken
DescriptionLinking: Wähle deinen Account, um diesen mit deinem externen Benutzer zu verlinken.
OtherUser: Anderer Benutzer
SessionState0: aktiv
SessionState1: inaktiv
@ -103,6 +108,11 @@ EmailVerificationDone:
Title: E-Mail Verifizierung
Description: Deine E-Mail Adresse wurde erfolgreich verifiziert.
RegisterOption:
Title: Registrations Möglichkeiten
Description: Wähle aus wie du dich registrieren möchtest.
RegisterUsernamePassword: Mit Benutzername Passwort
Registration:
Title: Registration
Description: Gib deine Benutzerangaben an. Die E-Mail Adresse wird als Benutzernamen verwendet.
@ -147,6 +157,16 @@ RegistrationOrg:
TosLinkText: AGBs
TosLink: https://zitadel.ch/pdf/agb.pdf
LinkingUsersDone:
Title: Benutzerlinking
Description: Benuzterlinking erledigt.
ExternalNotFoundOption:
Title: Externer Benutzer
Description: Externer Benutzer konnte nicht gefunden werden. Willst du deinen Benutzer mit einem bestehenden verlinken oder diesen als neuen Benutzer registrieren.
Link: Verlinken
AutoRegister: Automatisches registrieren
LogoutDone:
Title: Ausgeloggt
Description: Du wurdest erfolgreich ausgeloggt.
@ -173,6 +193,7 @@ Errors:
UserIDMissing: UserID ist leer
Invalid: Userdaten sind ungültig
DomainNotAllowedAsUsername: Domäne ist bereits reserviert und kann nicht verwendet werden
NotAllowedToLink: Der Benutzer darf nicht mit einem externen Login Provider verlinkt werden
Password:
ConfirmationWrong: Passwort Bestätigung stimmt nicht überein
Empty: Passwort ist leer
@ -202,5 +223,7 @@ Errors:
NotReady: Multifaktor OTP (OneTimePassword) ist nicht bereit
Locked: Benutzer ist gesperrt
NotActive: Benutzer ist nicht aktiv
ExternalIDP:
IDPTypeNotImplemented: IDP Typ ist nicht implementiert
optional: (optional)

View File

@ -1,12 +1,17 @@
Login:
Title: Login
Description: Enter your logindata.
TitleLinking: Login for userlinking
DescriptionLinking: Enter your login data to link your external user with a ZITADEL user.
Loginname: Loginname
LoginnamePlaceHolder: username@domain
ExternalLogin: Login with an external user.
UserSelection:
Title: Select account
Description: Select your account.
TitleLinking: Select account for userlinking
DescriptionLinking: Select your account to link with your external user.
OtherUser: Other User
SessionState0: active
SessionState1: inactive
@ -103,6 +108,11 @@ EmailVerificationDone:
Title: E-Mail Verification
Description: Your email address has been successfully verified.
RegistrationOption:
Title: Registration Options
Description: Choose how you'd like to register
RegisterUsernamePassword: With username password
Registration:
Title: Registration
Description: Enter your Userdata. Your email address will be used as loginname.
@ -147,11 +157,20 @@ RegistrationOrg:
TosLinkText: TOS
TosLink: https://zitadel.ch/pdf/tos.pdf
LogoutDone:
Title: Logged out
Description: You have logged out successfully.
LinkingUsersDone:
Title: Userlinking
Description: Userlinking done.
ExternalNotFoundOption:
Title: External User
Description: External user not found. Do you want to link your user or auto register a new one.
Link: Link
AutoRegister: Auto register
Actions:
Login: login
Next: next
@ -163,7 +182,6 @@ Actions:
Cancel: cancel
Save: save
Errors:
Internal: An internal error occured
AuthRequest:
@ -175,6 +193,7 @@ Errors:
UserIDMissing: UserID is empty
Invalid: Invalid userdata
DomainNotAllowedAsUsername: Domain is already reserved and cannot be used
NotAllowedToLink: User is not allowed to link with external login provider
Password:
ConfirmationWrong: Passwordconfirmation is wrong
Empty: Password is empty
@ -204,6 +223,8 @@ Errors:
NotReady: Multifactor OTP (OneTimePassword) isn't ready
Locked: User is locked
NotActive: User is not active
ExternalIDP:
IDPTypeNotImplemented: IDP Type is not implemented
optional: (optional)

View File

@ -73,7 +73,7 @@
*, *::before, *::after {
box-sizing: border-box;
font-family: Lato;
font-size: 18px;
font-size: 16px;
font-weight: 400;
}
@ -97,6 +97,7 @@ h1 {
font-family: Aileron;
font-weight: 300;
font-size: 40px;
text-align: center;
}
h2 {
@ -122,7 +123,13 @@ header .logo {
margin: 30px;
}
.content {
.head {
width: 100%;
max-width: 1000px;
margin: auto;
}
.content form {
margin: auto;
padding: 20px;
width: 100%;
@ -137,11 +144,14 @@ a {
a:hover {
color: #f60075;
}
a.tos-link {
font-size: 14px;
}
button {
button, .button {
background-color: #282828;
color: #760038;
border: 2px solid #760038;
border: 1px solid #760038;
border-radius: 5px;
width: 100%;
max-width: 600px;
@ -149,36 +159,39 @@ button {
transition: all 0.3s ease 0s;
cursor: pointer;
outline: none;
display: inline-block;
text-align: center;
line-height: 40px;
}
button:hover {
button:hover, .button:hover {
background-color: #f60075;
color: #282828;
border: 2px solid #f60075;
border: 1px solid #f60075;
}
button.primary {
button.primary, .button.primary {
background-color: #760038;
color: white;
border: none;
}
button.primary:hover {
button.primary:hover, .button.primary:hover {
background-color: #f60075;
}
button:disabled {
background-color: #505050;
border: 2px solid #505050;
button:disabled, .button:disabled {
background-color: #999999;
border: 1px solid #999999;
}
button:disabled:hover {
background-color: #505050;
border: 2px solid #505050;
button:disabled:hover, .button:disabled:hover {
background-color: #999999;
border: 1px solid #999999;
}
input:not([type=radio]), select {
background-color: #252525;
color: white;
height: 50px;
border: 2px solid #505050;
border: 1px solid #999999;
border-radius: 5px;
padding-left: 15px;
padding-left: 8px;
}
form button.user-selection .profile-image, .login-profile .profile-image {
@ -213,7 +226,7 @@ form button.user-selection:hover .profile-image, .login-profile:hover .profile-i
text-align: center;
}
.login-profile .names div:first-of-type {
font-size: 40px;
font-size: 26px;
font-weight: 300;
}
.login-profile .names div:nth-of-type(2) {
@ -244,20 +257,21 @@ form .field.check-box {
display: flex;
}
form .field.check-box input[type=checkbox] {
height: 20px;
height: 16px;
vertical-align: middle;
}
form .field.check-box label {
height: 20px;
height: 16px;
text-transform: inherit;
display: inline-block;
padding: 2px 0 0 15px;
width: 100%;
color: white;
}
form label {
color: #898989;
text-transform: uppercase;
font-size: 0.9rem;
font-size: 0.8rem;
margin-bottom: 3px;
}
form label span.optional {
@ -301,7 +315,7 @@ form button.user-selection .sessionstate {
height: 20px;
width: 20px;
border-radius: 20px;
border-color: #505050;
border-color: #999999;
border-style: solid;
border-width: 1px;
position: absolute;
@ -362,6 +376,7 @@ form ul#passwordcomplexity {
flex-wrap: wrap;
padding: 0;
list-style: none;
margin-bottom: 0;
}
form ul#passwordcomplexity li {
flex: 1 0 50%;
@ -444,4 +459,8 @@ footer {
padding: 10px;
}
.error {
color: #F20D6B;
}
/*# sourceMappingURL=dark.css.map */

View File

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/caos/variables.scss","../../scss/variables.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCMW;EDLX;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA,kBCDc;EDEd,OCDQ;EDER;EACA;EACA;;;AAMJ;EACI,OCXQ;EDYR,aClBS;EDmBT;EACA,WEzBS;;;AF4Bb;EACI,OClBQ;EDmBR,aCzBS;ED0BT;EACA,WE/BU;;;AFkCd;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OChDW;EDiDX;EACA;;AAEA;EACI,OCpDY;;;ADwDpB;EACI,kBC5Dc;ED6Dd,OC3DW;ED4DX;EACA;EACA;EACA;EACA,QE7EU;EF8EV;EACA;EACA;;AACA;EACI,kBCpEY;EDqEZ,OCxEU;EDyEV;;AAGJ;EACI,kBC3EO;ED4EP,OC7EI;ED8EJ;;AACA;EACI,kBC9EQ;;ADkFhB;EACI,kBEzEW;EF0EX;;AAEA;EACI,kBE7EO;EF8EP;;;AAKZ;EACI,kBEnFmB;EFoFnB,OCjGQ;EDkGR,QE9GU;EF+GV;EACA;EACA;;;AAIA;EACI;EACA;EACA;EACA;EE9GN;;AACA;EFyGE;IExGA;IACA;;;AF+GA;EElHF;;AACA;EFiHE;IEhHA;IACA;;;;AFsHA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI,WE7IC;EF8ID;;AAGJ;EACI;EACA;EACA;EACA,OE/HC;;;AFqIT;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAIR;EACI,OE5KK;EF6KL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;AAIR;EACI;EACA;EACA,OCjNI;EDkNJ;EACA;EACA;EACA;;AAEA;EACI;EACA,kBE5MW;;AF+Mf;EACI;;AAIR;EACI;;AAKA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA,cErOO;EFsOP;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAKR;EACI;;AAEA;EACI;;AAEA;EACI;;AAEJ;EACI,OEjQP;;AFwQL;EACI;;AAEJ;EACI;EACA;EACA;EACA;EE3RV;;AACA;EFsRM;IErRJ;IACA;;;AF6RQ;EACI;EACA;EElSd;;AACA;EF+RU;IE9RR;IACA;;;AFoSI;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA,OE3SN;;AFgTE;EACI,OElTL;;AFuTP;EACI;;AACA;EACI;EACA;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI,MC1VI;;AD6VR;EACI,MC/VU;;;ADoWd;EACI;EACA;;;AAIR;EAEQ;EAEJ;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AAGJ;EACI;EACA;EACA","file":"dark.css"}
{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/caos/variables.scss","../../scss/variables.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCMW;EDLX;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA,kBCDc;EDEd,OCDQ;EDER;EACA;EACA;;;AAMJ;EACI,OCXQ;EDYR,aClBS;EDmBT;EACA,WEzBS;EF0BT;;;AAGJ;EACI,OCnBQ;EDoBR,aC1BS;ED2BT;EACA,WEhCU;;;AFmCd;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OCvDW;EDwDX;EACA;;AAEA;EACI,OC3DY;;AD8DhB;EACI;;;AAIR;EACI,kBCvEc;EDwEd,OCtEW;EDuEX;EACA;EACA;EACA;EACA,QExFU;EFyFV;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI,kBCnFY;EDoFZ,OCvFU;EDwFV;;AAGJ;EACI,kBC1FO;ED2FP,OC5FI;ED6FJ;;AACA;EACI,kBC7FQ;;ADiGhB;EACI,kBEvFW;EFwFX;;AAEA;EACI,kBE3FO;EF4FP;;;AAOZ;EACI,kBEnGmB;EFoGnB,OClHQ;EDmHR,QE/HU;EFgIV;EACA;EACA;;;AAIA;EACI;EACA;EACA;EACA;EE9HN;;AACA;EFyHE;IExHA;IACA;;;AF+HA;EElIF;;AACA;EFiIE;IEhIA;IACA;;;;AFsIA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI,WE5JE;EF6JF;;AAGJ;EACI;EACA;EACA;EACA,OE/IC;;;AFqJT;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA,OCpMA;;ADwMR;EACI,OE7LK;EF8LL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;AAIR;EACI;EACA;EACA,OCnOI;EDoOJ;EACA;EACA;EACA;;AAEA;EACI;EACA,kBE7NW;;AFgOf;EACI;;AAIR;EACI;;AAKA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA,cEtPO;EFuPP;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAKR;EACI;;AAEA;EACI;;AAEA;EACI;;AAEJ;EACI,OElRP;;AFyRL;EACI;;AAEJ;EACI;EACA;EACA;EACA;EE5SV;;AACA;EFuSM;IEtSJ;IACA;;;AF8SQ;EACI;EACA;EEnTd;;AACA;EFgTU;IE/SR;IACA;;;AFqTI;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA,OE7TN;;AFkUE;EACI,OEpUL;;AFyUP;EACI;;AACA;EACI;EACA;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI,MC7WI;;ADgXR;EACI,MClXU;;;ADuXd;EACI;EACA;;;AAIR;EAEQ;EAEJ;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI,OEpZO","file":"dark.css"}

View File

@ -73,7 +73,7 @@
*, *::before, *::after {
box-sizing: border-box;
font-family: Lato;
font-size: 18px;
font-size: 16px;
font-weight: 400;
}
@ -97,6 +97,7 @@ h1 {
font-family: Aileron;
font-weight: 300;
font-size: 40px;
text-align: center;
}
h2 {
@ -122,7 +123,13 @@ header .logo {
margin: 30px;
}
.content {
.head {
width: 100%;
max-width: 1000px;
margin: auto;
}
.content form {
margin: auto;
padding: 20px;
width: 100%;
@ -137,11 +144,14 @@ a {
a:hover {
color: #f60075;
}
a.tos-link {
font-size: 14px;
}
button {
button, .button {
background-color: #282828;
color: #760038;
border: 2px solid #760038;
border: 1px solid #760038;
border-radius: 5px;
width: 100%;
max-width: 600px;
@ -149,36 +159,39 @@ button {
transition: all 0.3s ease 0s;
cursor: pointer;
outline: none;
display: inline-block;
text-align: center;
line-height: 40px;
}
button:hover {
button:hover, .button:hover {
background-color: #f60075;
color: #282828;
border: 2px solid #f60075;
border: 1px solid #f60075;
}
button.primary {
button.primary, .button.primary {
background-color: #760038;
color: white;
border: none;
}
button.primary:hover {
button.primary:hover, .button.primary:hover {
background-color: #f60075;
}
button:disabled {
background-color: #505050;
border: 2px solid #505050;
button:disabled, .button:disabled {
background-color: #999999;
border: 1px solid #999999;
}
button:disabled:hover {
background-color: #505050;
border: 2px solid #505050;
button:disabled:hover, .button:disabled:hover {
background-color: #999999;
border: 1px solid #999999;
}
input:not([type=radio]), select {
background-color: #252525;
color: white;
height: 50px;
border: 2px solid #505050;
border: 1px solid #999999;
border-radius: 5px;
padding-left: 15px;
padding-left: 8px;
}
form button.user-selection .profile-image, .login-profile .profile-image {
@ -213,7 +226,7 @@ form button.user-selection:hover .profile-image, .login-profile:hover .profile-i
text-align: center;
}
.login-profile .names div:first-of-type {
font-size: 40px;
font-size: 26px;
font-weight: 300;
}
.login-profile .names div:nth-of-type(2) {
@ -244,20 +257,21 @@ form .field.check-box {
display: flex;
}
form .field.check-box input[type=checkbox] {
height: 20px;
height: 16px;
vertical-align: middle;
}
form .field.check-box label {
height: 20px;
height: 16px;
text-transform: inherit;
display: inline-block;
padding: 2px 0 0 15px;
width: 100%;
color: white;
}
form label {
color: #898989;
text-transform: uppercase;
font-size: 0.9rem;
font-size: 0.8rem;
margin-bottom: 3px;
}
form label span.optional {
@ -301,7 +315,7 @@ form button.user-selection .sessionstate {
height: 20px;
width: 20px;
border-radius: 20px;
border-color: #505050;
border-color: #999999;
border-style: solid;
border-width: 1px;
position: absolute;
@ -362,6 +376,7 @@ form ul#passwordcomplexity {
flex-wrap: wrap;
padding: 0;
list-style: none;
margin-bottom: 0;
}
form ul#passwordcomplexity li {
flex: 1 0 50%;
@ -444,6 +459,10 @@ footer {
padding: 10px;
}
.error {
color: #F20D6B;
}
html {
background-color: white;
color: #282828;
@ -454,66 +473,75 @@ html header .logo {
html h1, html h2 {
color: #282828;
}
html button {
html button, html .button {
background-color: white;
color: #760038;
border: 2px solid #760038;
border: 1px solid #760038;
}
html button:hover {
html button:hover, html .button:hover {
background-color: #f60075;
border: 2px solid #f60075;
border: 1px solid #f60075;
color: #FFFFFF;
}
html button.primary {
html button.primary, html .button.primary {
background-color: #760038;
color: white;
color: #FFFFFF;
border: none;
box-shadow: 0px 10px 30px #760038;
}
html button.primary:hover {
html button.primary:hover, html .button.primary:hover {
background-color: #f60075;
}
html button.clean {
html button:disabled, html .button:disabled {
background-color: #999999;
border: 1px solid #999999;
}
html button:disabled:hover, html .button:disabled:hover {
background-color: #999999;
border: 1px solid #999999;
}
html button.clean, html .button.clean {
color: #282828;
}
html button.clean:hover {
html button.clean:hover, html .button.clean:hover {
border: none;
background-color: #FFFFFF;
}
html button.user-selection .profile-image {
html button.user-selection .profile-image, html .button.user-selection .profile-image {
background-image: url("../../../images/icon-user-light.png");
}
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) {
html button.user-selection .profile-image {
html button.user-selection .profile-image, html .button.user-selection .profile-image {
background-image: url("../../../images/icon-user-light@2x.png");
background-size: 80px 80px;
}
}
html button.user-selection:hover {
html button.user-selection:hover, html .button.user-selection:hover {
background-color: #FFFFFF;
}
html button.user-selection:hover .profile-image {
html button.user-selection:hover .profile-image, html .button.user-selection:hover .profile-image {
background-image: url("../../../images/icon-user-light-hover.png");
}
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) {
html button.user-selection:hover .profile-image {
html button.user-selection:hover .profile-image, html .button.user-selection:hover .profile-image {
background-image: url("../../../images/icon-user-light-hover@2x.png");
background-size: 80px 80px;
}
}
html button.other-user .other-user-image {
html button.other-user .other-user-image, html .button.other-user .other-user-image {
background-image: url("../../../images/icon-newuser-light.png");
}
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) {
html button.other-user .other-user-image {
html button.other-user .other-user-image, html .button.other-user .other-user-image {
background-image: url("../../../images/icon-newuser-light@2x.png");
background-size: 80px 60px;
}
}
html button.other-user:hover .other-user-image {
html button.other-user:hover .other-user-image, html .button.other-user:hover .other-user-image {
background-image: url("../../../images/icon-newuser-light-hover.png");
}
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) {
html button.other-user:hover .other-user-image {
html button.other-user:hover .other-user-image, html .button.other-user:hover .other-user-image {
background-image: url("../../../images/icon-newuser-light-hover@2x.png");
background-size: 80px 60px;
}
@ -532,4 +560,41 @@ html footer {
background-image: url("../gradientdeco-full.svg");
}
form .field.check-box label {
color: #282828;
}
form ul#passwordcomplexity li i {
color: #50CA3D;
}
form ul#passwordcomplexity li.invalid i {
color: #F20D6B;
}
.login-profile .profile-image, form button.user-selection .profile-image {
background-image: url("../../../images/icon-user-light.png");
}
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) {
.login-profile .profile-image, form button.user-selection .profile-image {
background-image: url("../../../images/icon-user-light@2x.png");
background-size: 80px 80px;
}
}
.login-profile:hover .profile-image, form button.user-selection:hover .profile-image {
background-image: url("../../../images/icon-user-light-hover.png");
}
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) {
.login-profile:hover .profile-image, form button.user-selection:hover .profile-image {
background-image: url("../../../images/icon-user-light-hover@2x.png");
background-size: 80px 80px;
}
}
.free-tier {
border: 2px solid #F20D6B;
}
.error {
color: #F20D6B;
}
/*# sourceMappingURL=light.css.map */

View File

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/caos/variables.scss","../../scss/variables.scss","../../scss/light.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCMW;EDLX;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA,kBCDc;EDEd,OCDQ;EDER;EACA;EACA;;;AAMJ;EACI,OCXQ;EDYR,aClBS;EDmBT;EACA,WEzBS;;;AF4Bb;EACI,OClBQ;EDmBR,aCzBS;ED0BT;EACA,WE/BU;;;AFkCd;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OChDW;EDiDX;EACA;;AAEA;EACI,OCpDY;;;ADwDpB;EACI,kBC5Dc;ED6Dd,OC3DW;ED4DX;EACA;EACA;EACA;EACA,QE7EU;EF8EV;EACA;EACA;;AACA;EACI,kBCpEY;EDqEZ,OCxEU;EDyEV;;AAGJ;EACI,kBC3EO;ED4EP,OC7EI;ED8EJ;;AACA;EACI,kBC9EQ;;ADkFhB;EACI,kBEzEW;EF0EX;;AAEA;EACI,kBE7EO;EF8EP;;;AAKZ;EACI,kBEnFmB;EFoFnB,OCjGQ;EDkGR,QE9GU;EF+GV;EACA;EACA;;;AAIA;EACI;EACA;EACA;EACA;EE9GN;;AACA;EFyGE;IExGA;IACA;;;AF+GA;EElHF;;AACA;EFiHE;IEhHA;IACA;;;;AFsHA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI,WE7IC;EF8ID;;AAGJ;EACI;EACA;EACA;EACA,OE/HC;;;AFqIT;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAIR;EACI,OE5KK;EF6KL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;AAIR;EACI;EACA;EACA,OCjNI;EDkNJ;EACA;EACA;EACA;;AAEA;EACI;EACA,kBE5MW;;AF+Mf;EACI;;AAIR;EACI;;AAKA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA,cErOO;EFsOP;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAKR;EACI;;AAEA;EACI;;AAEA;EACI;;AAEJ;EACI,OEjQP;;AFwQL;EACI;;AAEJ;EACI;EACA;EACA;EACA;EE3RV;;AACA;EFsRM;IErRJ;IACA;;;AF6RQ;EACI;EACA;EElSd;;AACA;EF+RU;IE9RR;IACA;;;AFoSI;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA,OE3SN;;AFgTE;EACI,OElTL;;AFuTP;EACI;;AACA;EACI;EACA;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI,MC1VI;;AD6VR;EACI,MC/VU;;;ADoWd;EACI;EACA;;;AAIR;EAEQ;EAEJ;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AAGJ;EACI;EACA;EACA;;;AG/ZJ;EACI,kBFeQ;EEdR,OFac;;AERd;EACI;;AAGJ;EACI,OFGU;;AEAd;EACI;EACA;EACA;;AAEA;EACI,kBFIa;EEHb;;AAGJ;EACI,kBFTG;EEUH,OFXA;EEYA;EACA;;AACA;EACI,kBFbI;;AEiBZ;EACI,OFrBM;;AEuBN;EACI;EACA,kBDEY;;ACGhB;ED9BV;;AACA;EC6BU;ID5BR;IACA;;;AC+BQ;EACI,kBDRY;;ACUZ;EDrCd;;AACA;ECoCc;IDnCZ;IACA;;;ACyCQ;ED5CV;;AACA;EC2CU;ID1CR;IACA;;;AC8CY;EDjDd;;AACA;ECgDc;ID/CZ;IACA;;;ACqDA;EACI,kBD9BoB;EC+BpB,OF1DU;;AE8DV;EACI,MF/DM;;AEkEV;EACI,MFlEA;;AEsER;EAEQ","file":"light.css"}
{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/caos/variables.scss","../../scss/variables.scss","../../scss/light.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCMW;EDLX;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA,kBCDc;EDEd,OCDQ;EDER;EACA;EACA;;;AAMJ;EACI,OCXQ;EDYR,aClBS;EDmBT;EACA,WEzBS;EF0BT;;;AAGJ;EACI,OCnBQ;EDoBR,aC1BS;ED2BT;EACA,WEhCU;;;AFmCd;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OCvDW;EDwDX;EACA;;AAEA;EACI,OC3DY;;AD8DhB;EACI;;;AAIR;EACI,kBCvEc;EDwEd,OCtEW;EDuEX;EACA;EACA;EACA;EACA,QExFU;EFyFV;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI,kBCnFY;EDoFZ,OCvFU;EDwFV;;AAGJ;EACI,kBC1FO;ED2FP,OC5FI;ED6FJ;;AACA;EACI,kBC7FQ;;ADiGhB;EACI,kBEvFW;EFwFX;;AAEA;EACI,kBE3FO;EF4FP;;;AAOZ;EACI,kBEnGmB;EFoGnB,OClHQ;EDmHR,QE/HU;EFgIV;EACA;EACA;;;AAIA;EACI;EACA;EACA;EACA;EE9HN;;AACA;EFyHE;IExHA;IACA;;;AF+HA;EElIF;;AACA;EFiIE;IEhIA;IACA;;;;AFsIA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI,WE5JE;EF6JF;;AAGJ;EACI;EACA;EACA;EACA,OE/IC;;;AFqJT;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA,OCpMA;;ADwMR;EACI,OE7LK;EF8LL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;AAIR;EACI;EACA;EACA,OCnOI;EDoOJ;EACA;EACA;EACA;;AAEA;EACI;EACA,kBE7NW;;AFgOf;EACI;;AAIR;EACI;;AAKA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA,cEtPO;EFuPP;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAKR;EACI;;AAEA;EACI;;AAEA;EACI;;AAEJ;EACI,OElRP;;AFyRL;EACI;;AAEJ;EACI;EACA;EACA;EACA;EE5SV;;AACA;EFuSM;IEtSJ;IACA;;;AF8SQ;EACI;EACA;EEnTd;;AACA;EFgTU;IE/SR;IACA;;;AFqTI;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA,OE7TN;;AFkUE;EACI,OEpUL;;AFyUP;EACI;;AACA;EACI;EACA;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI,MC7WI;;ADgXR;EACI,MClXU;;;ADuXd;EACI;EACA;;;AAIR;EAEQ;EAEJ;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI,OEpZO;;;AClCX;EACI,kBFeQ;EEdR,OFac;;AERd;EACI;;AAGJ;EACI,OFGU;;AEAd;EACI;EACA;EACA;;AAEA;EACI,kBFIa;EEHb;EACA,ODqBgB;;AClBpB;EACI,kBFVG;EEWH,ODgBgB;ECfhB;EACA;;AACA;EACI,kBFdI;;AEkBZ;EACI,kBDRO;ECSP;;AAEA;EACI,kBDZG;ECaH;;AAIR;EACI,OFhCM;;AEkCN;EACI;EACA,kBDPY;;ACYhB;EDxCV;;AACA;ECuCU;IDtCR;IACA;;;ACyCQ;EACI,kBDjBY;;ACmBZ;ED/Cd;;AACA;EC8Cc;ID7CZ;IACA;;;ACmDQ;EDtDV;;AACA;ECqDU;IDpDR;IACA;;;ACwDY;ED3Dd;;AACA;EC0Dc;IDzDZ;IACA;;;AC+DA;EACI,kBDvCoB;ECwCpB,OFrEU;;AEyEV;EACI,MF1EM;;AE6EV;EACI,MF7EA;;AEiFR;EAEQ;;;AAMR;EACI,OF3FU;;AE+Fb;EACI,OD9DM;;ACkEN;EACI,ODpEG;;;AC4EZ;ED5GF;;AACA;EC2GE;ID1GA;IACA;;;AC6GA;EDhHF;;AACA;EC+GE;ID9GA;IACA;;;;ACkHJ;EACI;;;AAGJ;EACI,OD1FY","file":"light.css"}

View File

@ -14,19 +14,20 @@ html {
color: $fontColorLight;
}
button {
button, .button {
background-color: $backgroundColorLight;
color: $primaryColorLight;
border: 2px solid $primaryColorLight;
border: 1px solid $primaryColorLight;
&:hover {
background-color: $primaryColorHoverLight;
border: 2px solid $primaryColorHoverLight;
border: 1px solid $primaryColorHoverLight;
color: $buttonBackgroundColorHoverLight
}
&.primary {
background-color: $primaryColor;
color: $fontColor;
color: $buttonBackgroundColorHoverLight;
border: none;
box-shadow: 0px 10px 30px $primaryColor;
&:hover {
@ -34,6 +35,16 @@ html {
}
}
&:disabled {
background-color: $inputBorderColor;
border: 1px solid $inputBorderColor;
&:hover {
background-color: $inputBorderColor;
border: 1px solid $inputBorderColor;
}
}
&.clean {
color: $fontColorLight;
@ -91,3 +102,40 @@ html {
}
}
}
form {
.field.check-box label {
color: $fontColorLight;
}
ul#passwordcomplexity li {
i {
color: $okColorLight;
}
&.invalid {
i {
color: $nokColorLight;
}
}
}
}
%profile-image {
.profile-image {
@include retina-background-image($profileImgLight, "png", false, 80px, 80px);
}
&:hover .profile-image {
@include retina-background-image($profileImgLight, "png", true, 80px, 80px);
}
}
.free-tier {
border: 2px solid $nokColorLight;
}
.error {
color: $nokColorLight;
}

View File

@ -3,7 +3,7 @@
*, *::before, *::after {
box-sizing: border-box;
font-family: $standardFont;
font-size: 18px;
font-size: 16px;
font-weight: 400;
}
@ -30,6 +30,7 @@ h1 {
font-family: $headerFont;
font-weight: 300;
font-size: $headerSize;
text-align: center;
}
h2 {
@ -56,7 +57,13 @@ header {
}
}
.content {
.head {
width: 100%;
max-width: 1000px;
margin: auto;
}
.content form {
margin: auto;
padding: 20px;
width: 100%;
@ -71,12 +78,16 @@ a {
&:hover {
color: $primaryColorHover;
}
&.tos-link {
font-size: 14px;
}
}
button {
button, .button {
background-color: $backgroundColor;
color: $primaryColor;
border: 2px solid $primaryColor;
border: 1px solid $primaryColor;
border-radius: 5px;
width: 100%;
max-width: 600px;
@ -84,10 +95,14 @@ button {
transition: all 0.3s ease 0s;
cursor: pointer;
outline: none;
display: inline-block;
text-align: center;
line-height: 40px;
&:hover {
background-color: $primaryColorHover;
color: $backgroundColor;
border: 2px solid $primaryColorHover;
border: 1px solid $primaryColorHover;
}
&.primary {
@ -101,22 +116,24 @@ button {
&:disabled {
background-color: $inputBorderColor;
border: 2px solid $inputBorderColor;
border: 1px solid $inputBorderColor;
&:hover {
background-color: $inputBorderColor;
border: 2px solid $inputBorderColor;
border: 1px solid $inputBorderColor;
}
}
}
input:not([type='radio']), select {
background-color: $inputBackgroundColor;
color: $fontColor;
height: $inputHeight;
border: 2px solid $inputBorderColor;
border: 1px solid $inputBorderColor;
border-radius: 5px;
padding-left: 15px;
padding-left: 8px;
}
%profile-image {
@ -145,7 +162,7 @@ input:not([type='radio']), select {
text-align: center;
div:first-of-type {
font-size: $headerSize;
font-size: $header3Size;
font-weight: 300;
}
@ -184,23 +201,24 @@ form {
display: flex;
input[type='checkbox'] {
height: 20px;
height: 16px;
vertical-align: middle;
}
& label {
height: 20px;
height: 16px;
text-transform: inherit;
display: inline-block;
padding: 2px 0 0 15px;
width: 100%;
color: $fontColor;
}
}
label {
color: $labelColor;
text-transform: uppercase;
font-size: 0.9rem;
font-size: 0.8rem;
margin-bottom: 3px;
span.optional {
@ -320,6 +338,7 @@ form {
flex-wrap: wrap;
padding: 0;
list-style: none;
margin-bottom: 0;
li {
flex: 1 0 50%;
@ -412,7 +431,11 @@ footer {
}
.free-tier {
border: 2px solid #F20D6B;
border: 2px solid $nokColor;
border-radius: 5px;
padding: 10px;
}
.error {
color: $nokColor;
}

View File

@ -6,6 +6,7 @@ $headerFont: Lato;
$inputHeight: 50px;
$headerSize: 40px;
$header2Size: 30px;
$header3Size: 26px;
$retina: "only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx)";
@mixin retina-background-image($file, $type, $hover, $width, $height) {
@ -27,13 +28,14 @@ $fontColor: #BBBBC8;
$primaryColor: #3574C6;
$primaryColorHover: lighten($primaryColor, 10%);
$labelColor: #898989;
$inputBorderColor: #505050;
$inputBorderColor: #999999;
$inputBackgroundColor: #252525;
$buttonBackgroundColorHover: $inputBackgroundColor;
$profileImgDark: "../../../images/icon-user-dark";
$otherUserImgDark: "../../../images/icon-newuser-dark";
$nokColor: #F20D6B;
$okColor: #0DF279;
$errorColor: red;
// ----- LIGHT-THEME --------
@ -45,3 +47,5 @@ $inputBackgroundColorLight: #FFFFFF;
$buttonBackgroundColorHoverLight: $inputBackgroundColorLight;
$profileImgLight: "../../../images/icon-user-light";
$otherUserImgLight: "../../../images/icon-newuser-light";
$nokColorLight: #F20D6B;
$okColorLight: #50CA3D;

View File

@ -73,7 +73,7 @@
*, *::before, *::after {
box-sizing: border-box;
font-family: Lato;
font-size: 18px;
font-size: 16px;
font-weight: 400;
}
@ -98,6 +98,7 @@ h1 {
font-family: Lato;
font-weight: 300;
font-size: 40px;
text-align: center;
}
h2 {
@ -123,7 +124,13 @@ header .logo {
margin: 30px;
}
.content {
.head {
width: 100%;
max-width: 1000px;
margin: auto;
}
.content form {
margin: auto;
padding: 20px;
width: 100%;
@ -138,11 +145,14 @@ a {
a:hover {
color: #5b8fd3;
}
a.tos-link {
font-size: 14px;
}
button {
button, .button {
background-color: #282828;
color: #3574C6;
border: 2px solid #3574C6;
border: 1px solid #3574C6;
border-radius: 5px;
width: 100%;
max-width: 600px;
@ -150,36 +160,39 @@ button {
transition: all 0.3s ease 0s;
cursor: pointer;
outline: none;
display: inline-block;
text-align: center;
line-height: 40px;
}
button:hover {
button:hover, .button:hover {
background-color: #5b8fd3;
color: #282828;
border: 2px solid #5b8fd3;
border: 1px solid #5b8fd3;
}
button.primary {
button.primary, .button.primary {
background-color: #3574C6;
color: #BBBBC8;
border: none;
}
button.primary:hover {
button.primary:hover, .button.primary:hover {
background-color: #5b8fd3;
}
button:disabled {
background-color: #505050;
border: 2px solid #505050;
button:disabled, .button:disabled {
background-color: #999999;
border: 1px solid #999999;
}
button:disabled:hover {
background-color: #505050;
border: 2px solid #505050;
button:disabled:hover, .button:disabled:hover {
background-color: #999999;
border: 1px solid #999999;
}
input:not([type=radio]), select {
background-color: #252525;
color: #BBBBC8;
height: 50px;
border: 2px solid #505050;
border: 1px solid #999999;
border-radius: 5px;
padding-left: 15px;
padding-left: 8px;
}
form button.user-selection .profile-image, .login-profile .profile-image {
@ -214,7 +227,7 @@ form button.user-selection:hover .profile-image, .login-profile:hover .profile-i
text-align: center;
}
.login-profile .names div:first-of-type {
font-size: 40px;
font-size: 26px;
font-weight: 300;
}
.login-profile .names div:nth-of-type(2) {
@ -245,20 +258,21 @@ form .field.check-box {
display: flex;
}
form .field.check-box input[type=checkbox] {
height: 20px;
height: 16px;
vertical-align: middle;
}
form .field.check-box label {
height: 20px;
height: 16px;
text-transform: inherit;
display: inline-block;
padding: 2px 0 0 15px;
width: 100%;
color: #BBBBC8;
}
form label {
color: #898989;
text-transform: uppercase;
font-size: 0.9rem;
font-size: 0.8rem;
margin-bottom: 3px;
}
form label span.optional {
@ -302,7 +316,7 @@ form button.user-selection .sessionstate {
height: 20px;
width: 20px;
border-radius: 20px;
border-color: #505050;
border-color: #999999;
border-style: solid;
border-width: 1px;
position: absolute;
@ -363,6 +377,7 @@ form ul#passwordcomplexity {
flex-wrap: wrap;
padding: 0;
list-style: none;
margin-bottom: 0;
}
form ul#passwordcomplexity li {
flex: 1 0 50%;
@ -444,4 +459,8 @@ footer {
padding: 10px;
}
.error {
color: #F20D6B;
}
/*# sourceMappingURL=dark.css.map */

View File

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/variables.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCHW;EDIX;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA,kBCOc;EDNd,OCOQ;EDNR;EACA;EACA;EAEI;;;AAIR;EACI,OCHQ;EDIR,aC3BS;ED4BT;EACA,WCzBS;;;AD4Bb;EACI,OCVQ;EDWR,aClCS;EDmCT;EACA,WC/BU;;;ADkCd;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OCxCW;EDyCX;EACA;;AAEA;EACI,OC5CY;;;ADgDpB;EACI,kBCpDc;EDqDd,OCnDW;EDoDX;EACA;EACA;EACA;EACA,QC7EU;ED8EV;EACA;EACA;;AACA;EACI,kBC5DY;ED6DZ,OChEU;EDiEV;;AAGJ;EACI,kBCnEO;EDoEP,OCrEI;EDsEJ;;AACA;EACI,kBCtEQ;;AD0EhB;EACI,kBCzEW;ED0EX;;AAEA;EACI,kBC7EO;ED8EP;;;AAKZ;EACI,kBCnFmB;EDoFnB,OCzFQ;ED0FR,QC9GU;ED+GV;EACA;EACA;;;AAIA;EACI;EACA;EACA;EACA;EC9GN;;AACA;EDyGE;ICxGA;IACA;;;AD+GA;EClHF;;AACA;EDiHE;IChHA;IACA;;;;ADsHA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI,WC7IC;ED8ID;;AAGJ;EACI;EACA;EACA;EACA,OC/HC;;;ADqIT;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAIR;EACI,OC5KK;ED6KL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;AAIR;EACI;EACA;EACA,OCzMI;ED0MJ;EACA;EACA;EACA;;AAEA;EACI;EACA,kBC5MW;;AD+Mf;EACI;;AAIR;EACI;;AAKA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA,cCrOO;EDsOP;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAKR;EACI;;AAEA;EACI;;AAEA;EACI;;AAEJ;EACI,OCjQP;;ADwQL;EACI;;AAEJ;EACI;EACA;EACA;EACA;EC3RV;;AACA;EDsRM;ICrRJ;IACA;;;AD6RQ;EACI;EACA;EClSd;;AACA;ED+RU;IC9RR;IACA;;;ADoSI;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA,OC3SN;;ADgTE;EACI,OClTL;;ADuTP;EACI;;AACA;EACI;EACA;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI,MClVI;;ADqVR;EACI,MCvVU;;;AD4Vd;EACI;EACA;;;AAIR;EAII;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AAGJ;EACI;EACA;EACA","file":"dark.css"}
{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/variables.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCHW;EDIX;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA,kBCQc;EDPd,OCQQ;EDPR;EACA;EACA;EAEI;;;AAIR;EACI,OCFQ;EDGR,aC3BS;ED4BT;EACA,WCzBS;ED0BT;;;AAGJ;EACI,OCVQ;EDWR,aCnCS;EDoCT;EACA,WChCU;;;ADmCd;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OC9CW;ED+CX;EACA;;AAEA;EACI,OClDY;;ADqDhB;EACI;;;AAIR;EACI,kBC9Dc;ED+Dd,OC7DW;ED8DX;EACA;EACA;EACA;EACA,QCxFU;EDyFV;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI,kBC1EY;ED2EZ,OC9EU;ED+EV;;AAGJ;EACI,kBCjFO;EDkFP,OCnFI;EDoFJ;;AACA;EACI,kBCpFQ;;ADwFhB;EACI,kBCvFW;EDwFX;;AAEA;EACI,kBC3FO;ED4FP;;;AAOZ;EACI,kBCnGmB;EDoGnB,OCzGQ;ED0GR,QC/HU;EDgIV;EACA;EACA;;;AAIA;EACI;EACA;EACA;EACA;EC9HN;;AACA;EDyHE;ICxHA;IACA;;;AD+HA;EClIF;;AACA;EDiIE;IChIA;IACA;;;;ADsIA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI,WC5JE;ED6JF;;AAGJ;EACI;EACA;EACA;EACA,OC/IC;;;ADqJT;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA,OC3LA;;AD+LR;EACI,OC7LK;ED8LL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;AAIR;EACI;EACA;EACA,OC1NI;ED2NJ;EACA;EACA;EACA;;AAEA;EACI;EACA,kBC7NW;;ADgOf;EACI;;AAIR;EACI;;AAKA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA,cCtPO;EDuPP;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAKR;EACI;;AAEA;EACI;;AAEA;EACI;;AAEJ;EACI,OClRP;;ADyRL;EACI;;AAEJ;EACI;EACA;EACA;EACA;EC5SV;;AACA;EDuSM;ICtSJ;IACA;;;AD8SQ;EACI;EACA;ECnTd;;AACA;EDgTU;IC/SR;IACA;;;ADqTI;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA,OC7TN;;ADkUE;EACI,OCpUL;;ADyUP;EACI;;AACA;EACI;EACA;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI,MCpWI;;ADuWR;EACI,MCzWU;;;AD8Wd;EACI;EACA;;;AAIR;EAII;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI,OCpZO","file":"dark.css"}

View File

@ -73,7 +73,7 @@
*, *::before, *::after {
box-sizing: border-box;
font-family: Lato;
font-size: 18px;
font-size: 16px;
font-weight: 400;
}
@ -98,6 +98,7 @@ h1 {
font-family: Lato;
font-weight: 300;
font-size: 40px;
text-align: center;
}
h2 {
@ -123,7 +124,13 @@ header .logo {
margin: 30px;
}
.content {
.head {
width: 100%;
max-width: 1000px;
margin: auto;
}
.content form {
margin: auto;
padding: 20px;
width: 100%;
@ -138,11 +145,14 @@ a {
a:hover {
color: #5b8fd3;
}
a.tos-link {
font-size: 14px;
}
button {
button, .button {
background-color: #282828;
color: #3574C6;
border: 2px solid #3574C6;
border: 1px solid #3574C6;
border-radius: 5px;
width: 100%;
max-width: 600px;
@ -150,36 +160,39 @@ button {
transition: all 0.3s ease 0s;
cursor: pointer;
outline: none;
display: inline-block;
text-align: center;
line-height: 40px;
}
button:hover {
button:hover, .button:hover {
background-color: #5b8fd3;
color: #282828;
border: 2px solid #5b8fd3;
border: 1px solid #5b8fd3;
}
button.primary {
button.primary, .button.primary {
background-color: #3574C6;
color: #BBBBC8;
border: none;
}
button.primary:hover {
button.primary:hover, .button.primary:hover {
background-color: #5b8fd3;
}
button:disabled {
background-color: #505050;
border: 2px solid #505050;
button:disabled, .button:disabled {
background-color: #999999;
border: 1px solid #999999;
}
button:disabled:hover {
background-color: #505050;
border: 2px solid #505050;
button:disabled:hover, .button:disabled:hover {
background-color: #999999;
border: 1px solid #999999;
}
input:not([type=radio]), select {
background-color: #252525;
color: #BBBBC8;
height: 50px;
border: 2px solid #505050;
border: 1px solid #999999;
border-radius: 5px;
padding-left: 15px;
padding-left: 8px;
}
form button.user-selection .profile-image, .login-profile .profile-image {
@ -214,7 +227,7 @@ form button.user-selection:hover .profile-image, .login-profile:hover .profile-i
text-align: center;
}
.login-profile .names div:first-of-type {
font-size: 40px;
font-size: 26px;
font-weight: 300;
}
.login-profile .names div:nth-of-type(2) {
@ -245,20 +258,21 @@ form .field.check-box {
display: flex;
}
form .field.check-box input[type=checkbox] {
height: 20px;
height: 16px;
vertical-align: middle;
}
form .field.check-box label {
height: 20px;
height: 16px;
text-transform: inherit;
display: inline-block;
padding: 2px 0 0 15px;
width: 100%;
color: #BBBBC8;
}
form label {
color: #898989;
text-transform: uppercase;
font-size: 0.9rem;
font-size: 0.8rem;
margin-bottom: 3px;
}
form label span.optional {
@ -302,7 +316,7 @@ form button.user-selection .sessionstate {
height: 20px;
width: 20px;
border-radius: 20px;
border-color: #505050;
border-color: #999999;
border-style: solid;
border-width: 1px;
position: absolute;
@ -363,6 +377,7 @@ form ul#passwordcomplexity {
flex-wrap: wrap;
padding: 0;
list-style: none;
margin-bottom: 0;
}
form ul#passwordcomplexity li {
flex: 1 0 50%;
@ -444,6 +459,10 @@ footer {
padding: 10px;
}
.error {
color: #F20D6B;
}
html {
background-color: #f5f5f5;
color: #282828;
@ -455,66 +474,75 @@ html header .logo {
html h1, html h2 {
color: #282828;
}
html button {
html button, html .button {
background-color: #f5f5f5;
color: #3574C6;
border: 2px solid #3574C6;
border: 1px solid #3574C6;
}
html button:hover {
html button:hover, html .button:hover {
background-color: #5b8fd3;
border: 2px solid #5b8fd3;
border: 1px solid #5b8fd3;
color: #FFFFFF;
}
html button.primary {
html button.primary, html .button.primary {
background-color: #3574C6;
color: #BBBBC8;
color: #FFFFFF;
border: none;
box-shadow: 0px 10px 30px #3574C6;
}
html button.primary:hover {
html button.primary:hover, html .button.primary:hover {
background-color: #5b8fd3;
}
html button.clean {
html button:disabled, html .button:disabled {
background-color: #999999;
border: 1px solid #999999;
}
html button:disabled:hover, html .button:disabled:hover {
background-color: #999999;
border: 1px solid #999999;
}
html button.clean, html .button.clean {
color: #282828;
}
html button.clean:hover {
html button.clean:hover, html .button.clean:hover {
border: none;
background-color: #FFFFFF;
}
html button.user-selection .profile-image {
html button.user-selection .profile-image, html .button.user-selection .profile-image {
background-image: url("../../../images/icon-user-light.png");
}
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) {
html button.user-selection .profile-image {
html button.user-selection .profile-image, html .button.user-selection .profile-image {
background-image: url("../../../images/icon-user-light@2x.png");
background-size: 80px 80px;
}
}
html button.user-selection:hover {
html button.user-selection:hover, html .button.user-selection:hover {
background-color: #FFFFFF;
}
html button.user-selection:hover .profile-image {
html button.user-selection:hover .profile-image, html .button.user-selection:hover .profile-image {
background-image: url("../../../images/icon-user-light-hover.png");
}
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) {
html button.user-selection:hover .profile-image {
html button.user-selection:hover .profile-image, html .button.user-selection:hover .profile-image {
background-image: url("../../../images/icon-user-light-hover@2x.png");
background-size: 80px 80px;
}
}
html button.other-user .other-user-image {
html button.other-user .other-user-image, html .button.other-user .other-user-image {
background-image: url("../../../images/icon-newuser-light.png");
}
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) {
html button.other-user .other-user-image {
html button.other-user .other-user-image, html .button.other-user .other-user-image {
background-image: url("../../../images/icon-newuser-light@2x.png");
background-size: 80px 60px;
}
}
html button.other-user:hover .other-user-image {
html button.other-user:hover .other-user-image, html .button.other-user:hover .other-user-image {
background-image: url("../../../images/icon-newuser-light-hover.png");
}
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) {
html button.other-user:hover .other-user-image {
html button.other-user:hover .other-user-image, html .button.other-user:hover .other-user-image {
background-image: url("../../../images/icon-newuser-light-hover@2x.png");
background-size: 80px 60px;
}
@ -529,5 +557,41 @@ html #qrcode svg rect.color {
html #qrcode svg rect.bg-color {
fill: #f5f5f5;
}
form .field.check-box label {
color: #282828;
}
form ul#passwordcomplexity li i {
color: #50CA3D;
}
form ul#passwordcomplexity li.invalid i {
color: #F20D6B;
}
.login-profile .profile-image, form button.user-selection .profile-image {
background-image: url("../../../images/icon-user-light.png");
}
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) {
.login-profile .profile-image, form button.user-selection .profile-image {
background-image: url("../../../images/icon-user-light@2x.png");
background-size: 80px 80px;
}
}
.login-profile:hover .profile-image, form button.user-selection:hover .profile-image {
background-image: url("../../../images/icon-user-light-hover.png");
}
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) {
.login-profile:hover .profile-image, form button.user-selection:hover .profile-image {
background-image: url("../../../images/icon-user-light-hover@2x.png");
background-size: 80px 80px;
}
}
.free-tier {
border: 2px solid #F20D6B;
}
.error {
color: #F20D6B;
}
/*# sourceMappingURL=light.css.map */

View File

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/variables.scss","../../scss/light.scss"],"names":[],"mappings":"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","file":"light.css"}
{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/variables.scss","../../scss/light.scss"],"names":[],"mappings":"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","file":"light.css"}

View File

@ -1,8 +1,10 @@
{{template "main-top" .}}
{{ template "user-profile" . }}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "PasswordChange.Description"}}</p>
<p>{{t "PasswordChange.Description"}}</p>
</div>
<form action="{{ changePasswordUrl }}" method="POST">
@ -42,8 +44,8 @@
<div class="actions">
<button type="submit" id="change-password-button" name="resend" value="false" class="primary right">{{t "Actions.Next"}}</button>
<a href="{{ loginUrl }}">
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
<a class="button secondary" href="{{ loginUrl }}">
{{t "Actions.Cancel"}}
</a>
</div>
</form>

View File

@ -1,8 +1,10 @@
{{template "main-top" .}}
{{ template "user-profile" . }}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "PasswordChangeDone.Description"}}</p>
<p>{{t "PasswordChangeDone.Description"}}</p>
</div>
<form action="{{ loginUrl }}" method="POST">
@ -13,8 +15,8 @@
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
<a href="{{ loginUrl }}">
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
<a class="button secondary" href="{{ loginUrl }}">
{{t "Actions.Cancel"}}
</a>
</div>
</form>

View File

@ -1,8 +1,10 @@
{{template "main-top" .}}
{{ template "user-profile" . }}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "UsernameChange.Description"}}</p>
<p>{{t "UsernameChange.Description"}}</p>
</div>
<form action="{{ changeUsernameUrl }}" method="POST">
@ -21,8 +23,8 @@
<div class="actions">
<button type="submit" id="submit-button" value="false" class="primary right">{{t "Actions.Next"}}</button>
<a href="{{ loginUrl }}">
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
<a class="button secondary" href="{{ loginUrl }}">
{{t "Actions.Cancel"}}
</a>
</div>
</form>

View File

@ -1,8 +1,10 @@
{{template "main-top" .}}
{{ template "user-profile" . }}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "UsernameChangeDone.Description"}}</p>
<p>{{t "UsernameChangeDone.Description"}}</p>
</div>
<form action="{{ loginUrl }}" method="POST">

View File

@ -0,0 +1,31 @@
{{template "main-top" .}}
<div class="head">
<h1>{{t "ExternalNotFoundOption.Title"}}</h1>
<p>{{t "ExternalNotFoundOption.Description"}}</p>
</div>
<form action="{{ externalNotFoundOptionUrl }}" method="POST">
{{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<div class="actions">
<button class="secondary right" name="link" value="true" formnovalidate>{{t "ExternalNotFoundOption.Link"}}</button>
<button class="secondary right" name="autoregister" value="true" formnovalidate>{{t "ExternalNotFoundOption.AutoRegister"}}</button>
<a class="button secondary" href="{{ loginUrl .AuthReqID }}">
{{t "Actions.Back"}}
</a>
</div>
{{template "error-message" .}}
</form>
<script src="{{ resourceUrl "scripts/form_submit.js" }}"></script>
<script src="{{ resourceUrl "scripts/default_form_validation.js" }}"></script>
{{template "main-bottom" .}}

View File

@ -1,8 +1,10 @@
{{template "main-top" .}}
{{ template "user-profile" . }}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "InitPassword.Description" }}</p>
<p>{{t "InitPassword.Description" }}</p>
</div>
<form action="{{ initPasswordUrl }}" method="POST">
@ -47,8 +49,8 @@
<div class="actions">
<button type="submit" id="init-button" name="resend" value="false" class="primary right" >{{t "Actions.Next"}}</button>
<button type="submit" name="resend" value="true" class="secondary right" formnovalidate>{{t "Actions.Resend" }}</button>
<a href="{{ loginUrl }}">
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
<a class="button secondary" href="{{ loginUrl }}">
{{t "Actions.Cancel"}}
</a>
</div>
</form>

View File

@ -1,8 +1,10 @@
{{template "main-top" .}}
{{ template "user-profile" . }}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "PasswordSetDone.Description"}}</p>
<p>{{t "PasswordSetDone.Description"}}</p>
</div>
<form action="{{ loginUrl }}" method="POST">
@ -12,8 +14,8 @@
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
<a href="{{ loginUrl }}">
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
<a class="button secondary" href="{{ loginUrl }}">
{{t "Actions.Cancel"}}
</a>
</div>
</form>

View File

@ -1,8 +1,10 @@
{{template "main-top" .}}
{{ template "user-profile" . }}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "InitUser.Description" }}</p>
<p>{{t "InitUser.Description" }}</p>
</div>
<form action="{{ initUserUrl }}" method="POST">
@ -49,8 +51,8 @@
value="false"
class="primary right">{{t "Actions.Next"}}</button>
<button type="submit" name="resend" value="true" class="secondary right" formnovalidate>{{t "Actions.Resend" }}</button>
<a href="{{ loginUrl }}">
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
<a class="button secondary" href="{{ loginUrl }}">
{{t "Actions.Cancel"}}
</a>
</div>
</form>

View File

@ -1,8 +1,10 @@
{{template "main-top" .}}
{{ template "user-profile" . }}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "InitUserDone.Description"}}</p>
<p>{{t "InitUserDone.Description"}}</p>
</div>
<form action="{{ loginUrl }}" method="POST">
@ -12,8 +14,8 @@
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
<a href="{{ loginUrl }}">
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
<a class="button secondary" href="{{ loginUrl }}">
{{t "Actions.Cancel"}}
</a>
</div>
</form>

View File

@ -0,0 +1,25 @@
{{template "main-top" .}}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "LinkingUsersDone.Description"}}</p>
</div>
<form action="{{ loginUrl }}" method="POST">
{{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
<a class="button secondary" href="{{ loginUrl }}">
{{t "Actions.Cancel"}}
</a>
</div>
</form>
{{template "main-bottom" .}}

View File

@ -1,8 +1,16 @@
{{template "main-top" .}}
<h1>{{t "Login.Title"}}</h1>
<p>{{t "Login.Description"}}</p>
<div class="head">
{{if .Linking}}
<h1>{{t "Login.TitleLinking"}}</h1>
<p>{{t "Login.DescriptionLinking"}}</p>
{{else}}
<h1>{{t "Login.Title"}}</h1>
<p>{{t "Login.Description"}}</p>
{{end}}
</div>
<form action="{{ loginNameUrl }}" method="POST">
@ -10,19 +18,36 @@
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
{{if .LoginPolicy.AllowUsernamePassword }}
<div class="fields">
<div class="field">
<label class="label" for="loginName">{{t "Login.Loginname"}}</label>
<input class="input" type="text" id="loginName" name="loginName" placeholder="{{t "Login.LoginnamePlaceHolder"}}" value="{{ .LoginName }}" autocomplete="username" autofocus required>
</div>
</div>
{{end}}
{{template "error-message" .}}
<div class="actions">
<button class="primary right" id="submit-button" type="submit">{{t "Actions.Next"}}</button>
<button class="secondary right" name="register" value="true" formnovalidate>{{t "Actions.Register"}}</button>
<button class="primary" id="submit-button" type="submit">{{t "Actions.Next"}}</button>
{{if .LoginPolicy.AllowRegister}}
<button class="secondary" name="register" value="true" formnovalidate>{{t "Actions.Register"}}</button>
{{end}}
</div>
{{if .LoginPolicy.AllowExternalIDP}}
<div class="actions idp-providers">
<p>{{t "Login.ExternalLogin"}}</p>
{{ $reqid := .AuthReqID}}
{{range $provider := .IDPProviders}}
<a href="{{ externalIDPAuthURL $reqid $provider.IDPConfigID}}" class="button secondary idp-providers">
{{$provider.Name}}
</a>
{{end}}
</div>
{{end}}
</form>

View File

@ -1,8 +1,9 @@
{{template "main-top" .}}
<h1>{{t "LogoutDone.Title"}}</h1>
<p>{{t "LogoutDone.Description"}}</p>
<div class="head">
<h1>{{t "LogoutDone.Title"}}</h1>
<p> {{t "LogoutDone.Description"}}</p>
</div>
<form action="{{ loginUrl }}" method="POST">
{{ .CSRF }}

View File

@ -1,8 +1,10 @@
{{template "main-top" .}}
{{ template "user-profile" . }}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "EmailVerification.Description"}}</p>
<p>{{t "EmailVerification.Description"}}</p>
</div>
<form action="{{ mailVerificationUrl }}" method="POST">
@ -25,8 +27,8 @@
{{ if .UserID }}
<button type="submit" name="resend" value="true" class="secondary right" formnovalidate>{{t "Actions.Resend"}}</button>
{{ end }}
<a href="{{ loginUrl }}">
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
<a class="button secondary" href="{{ loginUrl }}">
{{t "Actions.Cancel"}}
</a>
</div>
</form>

View File

@ -1,8 +1,10 @@
{{template "main-top" .}}
{{ template "user-profile" . }}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "EmailVerificationDone.Description"}}</p>
<p>{{t "EmailVerificationDone.Description"}}</p>
</div>
<form action="{{ loginUrl }}" method="POST">
@ -13,8 +15,8 @@
<div class="actions">
<button class="primary right" type="submit">{{if .AuthReqID }}{{t "Actions.Next"}}{{else}}{{t "Actions.Login"}}{{end}}</button>
<a href="{{ loginUrl }}">
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
<a class="button secondary" href="{{ loginUrl }}">
{{t "Actions.Cancel"}}
</a>
</div>
</form>

View File

@ -1,8 +1,10 @@
{{template "main-top" .}}
{{ template "user-profile" . }}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "MfaInitDone.Description"}}</p>
<p>{{t "MfaInitDone.Description"}}</p>
</div>
<form action="{{ loginUrl }}" method="POST">
@ -13,8 +15,8 @@
<div class="actions">
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
<a href="{{ loginUrl }}">
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
<a class="button secondary" href="{{ loginUrl }}">
{{t "Actions.Cancel"}}
</a>
</div>
</form>

View File

@ -1,8 +1,10 @@
{{template "main-top" .}}
{{ template "user-profile" . }}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "MfaInitVerify.Description"}}</p>
<p>{{t "MfaInitVerify.Description"}}</p>
</div>
<form action="{{ mfaInitVerifyUrl }}" method="POST">
@ -35,11 +37,11 @@
<div class="actions">
<button class="primary right" id="submit-button" type="submit">{{t "Actions.Next"}}</button>
<a href="{{ mfaPromptChangeUrl .AuthReqID .MfaType }}">
<button class="secondary" type="button">{{t "Actions.Back"}}</button>
<a class="button secondary" href="{{ mfaPromptChangeUrl .AuthReqID .MfaType }}">
{{t "Actions.Back"}}
</a>
<a href="{{ loginUrl }}">
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
<a class="button secondary" href="{{ loginUrl }}">
{{t "Actions.Cancel"}}
</a>
</div>
</form>

View File

@ -1,8 +1,10 @@
{{template "main-top" .}}
{{ template "user-profile" . }}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "MfaPrompt.Description"}}</p>
<p>{{t "MfaPrompt.Description"}}</p>
</div>
<form action="{{ mfaPromptUrl }}" method="POST">
@ -25,8 +27,8 @@
{{if not .MfaRequired}}
<button class="default right" name="skip" value="true" type="submit" formnovalidate>{{t "Actions.Skip"}}</button>
{{end}}
<a href="{{ loginUrl }}">
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
<a class="button secondary" href="{{ loginUrl }}">
{{t "Actions.Cancel"}}
</a>
</div>
</form>

View File

@ -1,8 +1,10 @@
{{template "main-top" .}}
{{ template "user-profile" . }}
<div class="head">
{{ template "user-profile" . }}
<p>{{t "MfaVerify.Description"}}</p>
<p>{{t "MfaVerify.Description"}}</p>
</div>
<form action="{{ mfaVerifyUrl }}" method="POST">
@ -22,8 +24,8 @@
<div class="actions">
<button class="primary right" id="submit-button" type="submit">{{t "Actions.Next"}}</button>
<a href="{{ loginUrl }}">
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
<a class="button secondary" href="{{ loginUrl }}">
{{t "Actions.Cancel"}}
</a>
</div>
</form>

Some files were not shown because too many files have changed in this diff Show More