mirror of
https://github.com/zitadel/zitadel.git
synced 2025-03-01 06:07:22 +00:00
Merge branch 'next' into next-rc
# Conflicts: # cmd/setup/config.go # cmd/setup/setup.go
This commit is contained in:
commit
955f1c0808
@ -198,8 +198,11 @@ Caches:
|
||||
AutoPrune:
|
||||
Interval: 1m
|
||||
TimeOut: 5s
|
||||
# Postgres connector uses the configured database (postgres or cockraochdb) as cache.
|
||||
# It is suitable for deployments with multiple containers.
|
||||
# The cache is enabled by default because it is the default cache states for IdP form callbacks
|
||||
Postgres:
|
||||
Enabled: false
|
||||
Enabled: true
|
||||
AutoPrune:
|
||||
Interval: 15m
|
||||
TimeOut: 30s
|
||||
@ -311,7 +314,7 @@ Caches:
|
||||
# When connector is empty, this cache will be disabled.
|
||||
Connector: ""
|
||||
MaxAge: 1h
|
||||
LastUsage: 10m
|
||||
LastUseAge: 10m
|
||||
# Log enables cache-specific logging. Default to error log to stderr when omitted.
|
||||
Log:
|
||||
Level: error
|
||||
@ -322,7 +325,7 @@ Caches:
|
||||
Milestones:
|
||||
Connector: ""
|
||||
MaxAge: 1h
|
||||
LastUsage: 10m
|
||||
LastUseAge: 10m
|
||||
Log:
|
||||
Level: error
|
||||
AddSource: true
|
||||
@ -332,7 +335,17 @@ Caches:
|
||||
Organization:
|
||||
Connector: ""
|
||||
MaxAge: 1h
|
||||
LastUsage: 10m
|
||||
LastUseAge: 10m
|
||||
Log:
|
||||
Level: error
|
||||
AddSource: true
|
||||
Formatter:
|
||||
Format: text
|
||||
# IdP callbacks using form POST cache, required for handling them securely and without possible too big request urls.
|
||||
IdPFormCallbacks:
|
||||
Connector: "postgres"
|
||||
MaxAge: 1h
|
||||
LastUseAge: 10m
|
||||
Log:
|
||||
Level: error
|
||||
AddSource: true
|
||||
|
@ -48,5 +48,5 @@ func (mig *InitPushFunc) Execute(ctx context.Context, _ eventstore.Event) (err e
|
||||
}
|
||||
|
||||
func (mig *InitPushFunc) String() string {
|
||||
return "40_init_push_func"
|
||||
return "40_init_push_func_v2"
|
||||
}
|
||||
|
@ -1,82 +1,92 @@
|
||||
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
|
||||
SELECT
|
||||
c.instance_id
|
||||
, c.aggregate_type
|
||||
, c.aggregate_id
|
||||
, c.command_type AS event_type
|
||||
, cs.sequence + ROW_NUMBER() OVER (PARTITION BY c.instance_id, c.aggregate_type, c.aggregate_id ORDER BY c.in_tx_order) AS sequence
|
||||
, c.revision
|
||||
, NOW() AS created_at
|
||||
, c.payload
|
||||
, c.creator
|
||||
, cs.owner
|
||||
, EXTRACT(EPOCH FROM NOW()) AS position
|
||||
, c.in_tx_order
|
||||
FROM (
|
||||
SELECT
|
||||
c.instance_id
|
||||
, c.aggregate_type
|
||||
, c.aggregate_id
|
||||
, c.command_type
|
||||
, c.revision
|
||||
, c.payload
|
||||
, c.creator
|
||||
, c.owner
|
||||
, ROW_NUMBER() OVER () AS in_tx_order
|
||||
FROM
|
||||
UNNEST(commands) AS c
|
||||
) AS c
|
||||
JOIN (
|
||||
SELECT
|
||||
cmds.instance_id
|
||||
, cmds.aggregate_type
|
||||
, cmds.aggregate_id
|
||||
, CASE WHEN (e.owner IS NOT NULL OR e.owner <> '') THEN e.owner ELSE command_owners.owner END AS owner
|
||||
, COALESCE(MAX(e.sequence), 0) AS sequence
|
||||
FROM (
|
||||
CREATE OR REPLACE FUNCTION eventstore.latest_aggregate_state(
|
||||
instance_id TEXT
|
||||
, aggregate_type TEXT
|
||||
, aggregate_id TEXT
|
||||
|
||||
, sequence OUT BIGINT
|
||||
, owner OUT TEXT
|
||||
)
|
||||
LANGUAGE 'plpgsql'
|
||||
STABLE PARALLEL SAFE
|
||||
AS $$
|
||||
BEGIN
|
||||
SELECT
|
||||
COALESCE(e.sequence, 0) AS sequence
|
||||
, e.owner
|
||||
INTO
|
||||
sequence
|
||||
, owner
|
||||
FROM
|
||||
eventstore.events2 e
|
||||
WHERE
|
||||
e.instance_id = $1
|
||||
AND e.aggregate_type = $2
|
||||
AND e.aggregate_id = $3
|
||||
ORDER BY
|
||||
e.sequence DESC
|
||||
LIMIT 1;
|
||||
|
||||
RETURN;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command[])
|
||||
RETURNS SETOF eventstore.events2
|
||||
LANGUAGE 'plpgsql'
|
||||
STABLE PARALLEL SAFE
|
||||
ROWS 10
|
||||
AS $$
|
||||
DECLARE
|
||||
"aggregate" RECORD;
|
||||
current_sequence BIGINT;
|
||||
current_owner TEXT;
|
||||
BEGIN
|
||||
FOR "aggregate" IN
|
||||
SELECT DISTINCT
|
||||
instance_id
|
||||
, aggregate_type
|
||||
, aggregate_id
|
||||
, owner
|
||||
FROM UNNEST(commands)
|
||||
) AS cmds
|
||||
LEFT JOIN eventstore.events2 AS e
|
||||
ON cmds.instance_id = e.instance_id
|
||||
AND cmds.aggregate_type = e.aggregate_type
|
||||
AND cmds.aggregate_id = e.aggregate_id
|
||||
JOIN (
|
||||
LOOP
|
||||
SELECT
|
||||
*
|
||||
INTO
|
||||
current_sequence
|
||||
, current_owner
|
||||
FROM eventstore.latest_aggregate_state(
|
||||
"aggregate".instance_id
|
||||
, "aggregate".aggregate_type
|
||||
, "aggregate".aggregate_id
|
||||
);
|
||||
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
DISTINCT ON (
|
||||
instance_id
|
||||
, aggregate_type
|
||||
, aggregate_id
|
||||
)
|
||||
instance_id
|
||||
, aggregate_type
|
||||
, aggregate_id
|
||||
, owner
|
||||
c.instance_id
|
||||
, c.aggregate_type
|
||||
, c.aggregate_id
|
||||
, c.command_type -- AS event_type
|
||||
, COALESCE(current_sequence, 0) + ROW_NUMBER() OVER () -- AS sequence
|
||||
, c.revision
|
||||
, NOW() -- AS created_at
|
||||
, c.payload
|
||||
, c.creator
|
||||
, COALESCE(current_owner, c.owner) -- AS owner
|
||||
, EXTRACT(EPOCH FROM NOW()) -- AS position
|
||||
, c.ordinality::INT -- AS in_tx_order
|
||||
FROM
|
||||
UNNEST(commands)
|
||||
) AS command_owners ON
|
||||
cmds.instance_id = command_owners.instance_id
|
||||
AND cmds.aggregate_type = command_owners.aggregate_type
|
||||
AND cmds.aggregate_id = command_owners.aggregate_id
|
||||
GROUP BY
|
||||
cmds.instance_id
|
||||
, cmds.aggregate_type
|
||||
, cmds.aggregate_id
|
||||
, 4 -- owner
|
||||
) AS cs
|
||||
ON c.instance_id = cs.instance_id
|
||||
AND c.aggregate_type = cs.aggregate_type
|
||||
AND c.aggregate_id = cs.aggregate_id
|
||||
ORDER BY
|
||||
in_tx_order;
|
||||
$$ LANGUAGE SQL;
|
||||
UNNEST(commands) WITH ORDINALITY AS c
|
||||
WHERE
|
||||
c.instance_id = aggregate.instance_id
|
||||
AND c.aggregate_type = aggregate.aggregate_type
|
||||
AND c.aggregate_id = aggregate.aggregate_id;
|
||||
END LOOP;
|
||||
RETURN;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
|
||||
INSERT INTO eventstore.events2
|
||||
SELECT * FROM eventstore.commands_to_events(commands)
|
||||
ORDER BY in_tx_order
|
||||
RETURNING *
|
||||
$$ LANGUAGE SQL;
|
||||
|
40
cmd/setup/43.go
Normal file
40
cmd/setup/43.go
Normal file
@ -0,0 +1,40 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed 43/cockroach/*.sql
|
||||
//go:embed 43/postgres/*.sql
|
||||
createFieldsDomainIndex embed.FS
|
||||
)
|
||||
|
||||
type CreateFieldsDomainIndex struct {
|
||||
dbClient *database.DB
|
||||
}
|
||||
|
||||
func (mig *CreateFieldsDomainIndex) Execute(ctx context.Context, _ eventstore.Event) error {
|
||||
statements, err := readStatements(createFieldsDomainIndex, "43", mig.dbClient.Type())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, stmt := range statements {
|
||||
logging.WithFields("file", stmt.file, "migration", mig.String()).Info("execute statement")
|
||||
if _, err := mig.dbClient.ExecContext(ctx, stmt.query); err != nil {
|
||||
return fmt.Errorf("%s %s: %w", mig.String(), stmt.file, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mig *CreateFieldsDomainIndex) String() string {
|
||||
return "43_create_fields_domain_index"
|
||||
}
|
3
cmd/setup/43/cockroach/43.sql
Normal file
3
cmd/setup/43/cockroach/43.sql
Normal file
@ -0,0 +1,3 @@
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS fields_instance_domains_idx
|
||||
ON eventstore.fields (object_id)
|
||||
WHERE object_type = 'instance_domain' AND field_name = 'domain';
|
3
cmd/setup/43/postgres/43.sql
Normal file
3
cmd/setup/43/postgres/43.sql
Normal file
@ -0,0 +1,3 @@
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS fields_instance_domains_idx
|
||||
ON eventstore.fields (object_id) INCLUDE (instance_id)
|
||||
WHERE object_type = 'instance_domain' AND field_name = 'domain';
|
39
cmd/setup/44.go
Normal file
39
cmd/setup/44.go
Normal file
@ -0,0 +1,39 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed 44/*.sql
|
||||
replaceCurrentSequencesIndex embed.FS
|
||||
)
|
||||
|
||||
type ReplaceCurrentSequencesIndex struct {
|
||||
dbClient *database.DB
|
||||
}
|
||||
|
||||
func (mig *ReplaceCurrentSequencesIndex) Execute(ctx context.Context, _ eventstore.Event) error {
|
||||
statements, err := readStatements(replaceCurrentSequencesIndex, "44", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, stmt := range statements {
|
||||
logging.WithFields("file", stmt.file, "migration", mig.String()).Info("execute statement")
|
||||
if _, err := mig.dbClient.ExecContext(ctx, stmt.query); err != nil {
|
||||
return fmt.Errorf("%s %s: %w", mig.String(), stmt.file, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mig *ReplaceCurrentSequencesIndex) String() string {
|
||||
return "44_replace_current_sequences_index"
|
||||
}
|
3
cmd/setup/44/01_create_index.sql
Normal file
3
cmd/setup/44/01_create_index.sql
Normal file
@ -0,0 +1,3 @@
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS events2_current_sequence2
|
||||
ON eventstore.events2 USING btree
|
||||
(aggregate_id ASC, aggregate_type ASC, instance_id ASC, sequence DESC);
|
1
cmd/setup/44/02_drop_old_index.sql
Normal file
1
cmd/setup/44/02_drop_old_index.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS eventstore.events2_current_sequence;
|
@ -128,6 +128,8 @@ type Steps struct {
|
||||
s38BackChannelLogoutNotificationStart *BackChannelLogoutNotificationStart
|
||||
s40InitPushFunc *InitPushFunc
|
||||
s42Apps7OIDCConfigsLoginVersion *Apps7OIDCConfigsLoginVersion
|
||||
s43CreateFieldsDomainIndex *CreateFieldsDomainIndex
|
||||
s44ReplaceCurrentSequencesIndex *ReplaceCurrentSequencesIndex
|
||||
}
|
||||
|
||||
func MustNewSteps(v *viper.Viper) *Steps {
|
||||
|
@ -171,6 +171,8 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
steps.s38BackChannelLogoutNotificationStart = &BackChannelLogoutNotificationStart{dbClient: esPusherDBClient, esClient: eventstoreClient}
|
||||
steps.s40InitPushFunc = &InitPushFunc{dbClient: esPusherDBClient}
|
||||
steps.s42Apps7OIDCConfigsLoginVersion = &Apps7OIDCConfigsLoginVersion{dbClient: esPusherDBClient}
|
||||
steps.s43CreateFieldsDomainIndex = &CreateFieldsDomainIndex{dbClient: queryDBClient}
|
||||
steps.s44ReplaceCurrentSequencesIndex = &ReplaceCurrentSequencesIndex{dbClient: esPusherDBClient}
|
||||
|
||||
err = projection.Create(ctx, projectionDBClient, eventstoreClient, config.Projections, nil, nil, nil)
|
||||
logging.OnError(err).Fatal("unable to start projections")
|
||||
@ -224,6 +226,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
steps.s35AddPositionToIndexEsWm,
|
||||
steps.s36FillV2Milestones,
|
||||
steps.s38BackChannelLogoutNotificationStart,
|
||||
steps.s44ReplaceCurrentSequencesIndex,
|
||||
} {
|
||||
mustExecuteMigration(ctx, eventstoreClient, step, "migration failed")
|
||||
}
|
||||
@ -242,6 +245,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
steps.s33SMSConfigs3TwilioAddVerifyServiceSid,
|
||||
steps.s37Apps7OIDConfigsBackChannelLogoutURI,
|
||||
steps.s42Apps7OIDCConfigsLoginVersion,
|
||||
steps.s43CreateFieldsDomainIndex,
|
||||
} {
|
||||
mustExecuteMigration(ctx, eventstoreClient, step, "migration failed")
|
||||
}
|
||||
|
@ -317,6 +317,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
authZRepo,
|
||||
keys,
|
||||
permissionCheck,
|
||||
cacheConnectors,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -361,6 +362,7 @@ func startAPIs(
|
||||
authZRepo authz_repo.Repository,
|
||||
keys *encryption.EncryptionKeys,
|
||||
permissionCheck domain.PermissionCheck,
|
||||
cacheConnectors connector.Connectors,
|
||||
) (*api.API, error) {
|
||||
repo := struct {
|
||||
authz_repo.Repository
|
||||
@ -542,6 +544,7 @@ func startAPIs(
|
||||
keys.User,
|
||||
keys.IDPConfig,
|
||||
keys.CSRFCookieKey,
|
||||
cacheConnectors,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to start login: %w", err)
|
||||
|
@ -1,4 +1,3 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
|
||||
traefik:
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
zitadel:
|
||||
restart: "always"
|
||||
|
@ -24,7 +24,7 @@ export const Description = ({mode, link}) => {
|
||||
}
|
||||
|
||||
export const Commands = ({mode, name, lower, configfilename}) => {
|
||||
let genCert = '# Generate a self signed certificate and key.\nopenssl req -x509 -batch -subj "/CN=127.0.0.1.sslip.io/O=ZITADEL Demo" -nodes -newkey rsa:2048 -keyout ./selfsigned.key -out ./selfsigned.crt\n\n';
|
||||
let genCert = '# Generate a self signed certificate and key.\nopenssl req -x509 -batch -subj "/CN=127.0.0.1.sslip.io/O=ZITADEL Demo" -nodes -newkey rsa:2048 -keyout ./selfsigned.key -out ./selfsigned.crt 2>/dev/null\n\n';
|
||||
let connPort = "443"
|
||||
let connInsecureFlag = "--insecure "
|
||||
let connScheme = "https"
|
||||
@ -42,16 +42,16 @@ export const Commands = ({mode, name, lower, configfilename}) => {
|
||||
<CodeBlock language="bash">
|
||||
{'# Download the configuration files.'}{'\n'}
|
||||
{'export ZITADEL_CONFIG_FILES=https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/reverseproxy\n'}
|
||||
{`wget $\{ZITADEL_CONFIG_FILES\}/docker-compose.yaml -O docker-compose-base.yaml`}{'\n'}
|
||||
{'wget $\{ZITADEL_CONFIG_FILES\}/'}{lower}{'/docker-compose.yaml -O docker-compose-'}{lower}{'.yaml'}{'\n'}
|
||||
{'wget $\{ZITADEL_CONFIG_FILES\}/'}{lower}{'/'}{configfilename}{' -O '}{configfilename}{'\n'}
|
||||
{'wget $\{ZITADEL_CONFIG_FILES\}/docker-compose.yaml -O docker-compose-base.yaml --quiet \n'}
|
||||
{'wget $\{ZITADEL_CONFIG_FILES\}/'}{lower}{'/docker-compose.yaml -O docker-compose-'}{lower}{'.yaml --quiet \n'}
|
||||
{'wget $\{ZITADEL_CONFIG_FILES\}/'}{lower}{'/'}{configfilename}{' -O '}{configfilename}{' --quiet \n'}
|
||||
{'\n'}
|
||||
{genCert}
|
||||
{'# Run the database, ZITADEL and '}{name}{'.'}{'\n'}
|
||||
{'docker compose --file docker-compose-base.yaml --file docker-compose-'}{lower}{'.yaml up --detach proxy-'}{mode}{'-tls'}{'\n'}
|
||||
{'docker compose --file docker-compose-base.yaml --file docker-compose-'}{lower}{'.yaml up --detach --wait db zitadel-init zitadel-'}{mode}{'-tls proxy-'}{mode}{'-tls'}{'\n'}
|
||||
{'\n'}
|
||||
{'# Test that gRPC and HTTP APIs work. Empty brackets like {} means success.\n'}
|
||||
{'sleep 3\n'}
|
||||
{'# Make sure you have the grpcurl cli installed on your machine https://github.com/fullstorydev/grpcurl?tab=readme-ov-file#installation\n'}
|
||||
{'grpcurl '}{connInsecureFlag}{grpcPlainTextFlag}{'127.0.0.1.sslip.io:'}{connPort}{' zitadel.admin.v1.AdminService/Healthz\n'}
|
||||
{'curl '}{connInsecureFlag}{connScheme}{'://127.0.0.1.sslip.io:'}{connPort}{'/admin/v1/healthz\n'}
|
||||
</CodeBlock>
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
|
||||
proxy-disabled-tls:
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
|
||||
zitadel-disabled-tls:
|
||||
@ -17,7 +15,7 @@ services:
|
||||
ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel_user
|
||||
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel_pw
|
||||
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: root
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
|
||||
networks:
|
||||
@ -43,16 +41,16 @@ services:
|
||||
ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel_user
|
||||
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel_pw
|
||||
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: root
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
|
||||
networks:
|
||||
- 'zitadel'
|
||||
depends_on:
|
||||
zitadel-init:
|
||||
condition: 'service_completed_successfully'
|
||||
db:
|
||||
condition: 'service_healthy'
|
||||
zitadel-init:
|
||||
condition: 'service_completed_successfully'
|
||||
|
||||
zitadel-enabled-tls:
|
||||
extends:
|
||||
@ -71,7 +69,7 @@ services:
|
||||
ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel_user
|
||||
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel_pw
|
||||
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: root
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
|
||||
volumes:
|
||||
@ -109,7 +107,7 @@ services:
|
||||
ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel_user
|
||||
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel_pw
|
||||
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: root
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
|
||||
networks:
|
||||
@ -125,10 +123,9 @@ services:
|
||||
restart: 'always'
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
PGUSER: root
|
||||
POSTGRES_PASSWORD: postgres
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready", "-d", "zitadel", "-U", "postgres"]
|
||||
test: ["CMD-SHELL", "pg_isready"]
|
||||
interval: 5s
|
||||
timeout: 60s
|
||||
retries: 10
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
|
||||
proxy-disabled-tls:
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
|
||||
proxy-disabled-tls:
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
|
||||
proxy-disabled-tls:
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
|
||||
db:
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
zitadel:
|
||||
user: '$UID'
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
zitadel:
|
||||
extends:
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
user_pb "github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details {
|
||||
@ -70,3 +71,105 @@ func TextMethodToQuery(method object.TextQueryMethod) query.TextComparison {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
func AuthMethodsToPb(mfas *query.AuthMethods) []*user_pb.AuthFactor {
|
||||
factors := make([]*user_pb.AuthFactor, len(mfas.AuthMethods))
|
||||
for i, mfa := range mfas.AuthMethods {
|
||||
factors[i] = AuthMethodToPb(mfa)
|
||||
}
|
||||
return factors
|
||||
}
|
||||
|
||||
func AuthMethodToPb(mfa *query.AuthMethod) *user_pb.AuthFactor {
|
||||
factor := &user_pb.AuthFactor{
|
||||
State: MFAStateToPb(mfa.State),
|
||||
}
|
||||
switch mfa.Type {
|
||||
case domain.UserAuthMethodTypeTOTP:
|
||||
factor.Type = &user_pb.AuthFactor_Otp{
|
||||
Otp: &user_pb.AuthFactorOTP{},
|
||||
}
|
||||
case domain.UserAuthMethodTypeU2F:
|
||||
factor.Type = &user_pb.AuthFactor_U2F{
|
||||
U2F: &user_pb.AuthFactorU2F{
|
||||
Id: mfa.TokenID,
|
||||
Name: mfa.Name,
|
||||
},
|
||||
}
|
||||
case domain.UserAuthMethodTypeOTPSMS:
|
||||
factor.Type = &user_pb.AuthFactor_OtpSms{
|
||||
OtpSms: &user_pb.AuthFactorOTPSMS{},
|
||||
}
|
||||
case domain.UserAuthMethodTypeOTPEmail:
|
||||
factor.Type = &user_pb.AuthFactor_OtpEmail{
|
||||
OtpEmail: &user_pb.AuthFactorOTPEmail{},
|
||||
}
|
||||
case domain.UserAuthMethodTypeUnspecified:
|
||||
case domain.UserAuthMethodTypePasswordless:
|
||||
case domain.UserAuthMethodTypePassword:
|
||||
case domain.UserAuthMethodTypeIDP:
|
||||
case domain.UserAuthMethodTypeOTP:
|
||||
case domain.UserAuthMethodTypePrivateKey:
|
||||
}
|
||||
return factor
|
||||
}
|
||||
|
||||
func AuthFactorsToPb(authFactors []user_pb.AuthFactors) []domain.UserAuthMethodType {
|
||||
factors := make([]domain.UserAuthMethodType, len(authFactors))
|
||||
for i, authFactor := range authFactors {
|
||||
factors[i] = AuthFactorToPb(authFactor)
|
||||
}
|
||||
return factors
|
||||
}
|
||||
|
||||
func AuthFactorToPb(authFactor user_pb.AuthFactors) domain.UserAuthMethodType {
|
||||
switch authFactor {
|
||||
case user_pb.AuthFactors_OTP:
|
||||
return domain.UserAuthMethodTypeTOTP
|
||||
case user_pb.AuthFactors_OTP_SMS:
|
||||
return domain.UserAuthMethodTypeOTPSMS
|
||||
case user_pb.AuthFactors_OTP_EMAIL:
|
||||
return domain.UserAuthMethodTypeOTPEmail
|
||||
case user_pb.AuthFactors_U2F:
|
||||
return domain.UserAuthMethodTypeU2F
|
||||
default:
|
||||
return domain.UserAuthMethodTypeUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
func AuthFactorStatesToPb(authFactorStates []user_pb.AuthFactorState) []domain.MFAState {
|
||||
factorStates := make([]domain.MFAState, len(authFactorStates))
|
||||
for i, authFactorState := range authFactorStates {
|
||||
factorStates[i] = AuthFactorStateToPb(authFactorState)
|
||||
}
|
||||
return factorStates
|
||||
}
|
||||
|
||||
func AuthFactorStateToPb(authFactorState user_pb.AuthFactorState) domain.MFAState {
|
||||
switch authFactorState {
|
||||
case user_pb.AuthFactorState_AUTH_FACTOR_STATE_UNSPECIFIED:
|
||||
return domain.MFAStateUnspecified
|
||||
case user_pb.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY:
|
||||
return domain.MFAStateNotReady
|
||||
case user_pb.AuthFactorState_AUTH_FACTOR_STATE_READY:
|
||||
return domain.MFAStateReady
|
||||
case user_pb.AuthFactorState_AUTH_FACTOR_STATE_REMOVED:
|
||||
return domain.MFAStateRemoved
|
||||
default:
|
||||
return domain.MFAStateUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
func MFAStateToPb(state domain.MFAState) user_pb.AuthFactorState {
|
||||
switch state {
|
||||
case domain.MFAStateNotReady:
|
||||
return user_pb.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY
|
||||
case domain.MFAStateReady:
|
||||
return user_pb.AuthFactorState_AUTH_FACTOR_STATE_READY
|
||||
case domain.MFAStateUnspecified, domain.MFAStateRemoved:
|
||||
// Handle all remaining cases so the linter succeeds
|
||||
return user_pb.AuthFactorState_AUTH_FACTOR_STATE_UNSPECIFIED
|
||||
default:
|
||||
return user_pb.AuthFactorState_AUTH_FACTOR_STATE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -2629,6 +2631,247 @@ func TestServer_ListAuthenticationMethodTypes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ListAuthenticationFactors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args *user.ListAuthenticationFactorsRequest
|
||||
want *user.ListAuthenticationFactorsResponse
|
||||
dep func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse)
|
||||
wantErr bool
|
||||
ctx context.Context
|
||||
}{
|
||||
{
|
||||
name: "no auth",
|
||||
args: &user.ListAuthenticationFactorsRequest{},
|
||||
want: &user.ListAuthenticationFactorsResponse{
|
||||
Result: nil,
|
||||
},
|
||||
dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) {
|
||||
userIDWithoutAuth := Instance.CreateHumanUser(CTX).GetUserId()
|
||||
args.UserId = userIDWithoutAuth
|
||||
},
|
||||
ctx: CTX,
|
||||
},
|
||||
{
|
||||
name: "with u2f",
|
||||
args: &user.ListAuthenticationFactorsRequest{},
|
||||
want: &user.ListAuthenticationFactorsResponse{
|
||||
Result: []*user.AuthFactor{
|
||||
{
|
||||
State: user.AuthFactorState_AUTH_FACTOR_STATE_READY,
|
||||
},
|
||||
},
|
||||
},
|
||||
dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) {
|
||||
userWithU2F := Instance.CreateHumanUser(CTX).GetUserId()
|
||||
U2FId := Instance.RegisterUserU2F(CTX, userWithU2F)
|
||||
|
||||
args.UserId = userWithU2F
|
||||
want.Result[0].Type = &user.AuthFactor_U2F{
|
||||
U2F: &user.AuthFactorU2F{
|
||||
Id: U2FId,
|
||||
Name: "nice name",
|
||||
},
|
||||
}
|
||||
},
|
||||
ctx: CTX,
|
||||
},
|
||||
{
|
||||
name: "with totp, u2f",
|
||||
args: &user.ListAuthenticationFactorsRequest{},
|
||||
want: &user.ListAuthenticationFactorsResponse{
|
||||
Result: []*user.AuthFactor{
|
||||
{
|
||||
State: user.AuthFactorState_AUTH_FACTOR_STATE_READY,
|
||||
Type: &user.AuthFactor_Otp{
|
||||
Otp: &user.AuthFactorOTP{},
|
||||
},
|
||||
},
|
||||
{
|
||||
State: user.AuthFactorState_AUTH_FACTOR_STATE_READY,
|
||||
},
|
||||
},
|
||||
},
|
||||
dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) {
|
||||
userWithTOTP := Instance.CreateHumanUserWithTOTP(CTX, "secret").GetUserId()
|
||||
U2FIdWithTOTP := Instance.RegisterUserU2F(CTX, userWithTOTP)
|
||||
|
||||
args.UserId = userWithTOTP
|
||||
want.Result[1].Type = &user.AuthFactor_U2F{
|
||||
U2F: &user.AuthFactorU2F{
|
||||
Id: U2FIdWithTOTP,
|
||||
Name: "nice name",
|
||||
},
|
||||
}
|
||||
},
|
||||
ctx: CTX,
|
||||
},
|
||||
{
|
||||
name: "with totp, u2f filtered",
|
||||
args: &user.ListAuthenticationFactorsRequest{
|
||||
AuthFactors: []user.AuthFactors{user.AuthFactors_U2F},
|
||||
},
|
||||
want: &user.ListAuthenticationFactorsResponse{
|
||||
Result: []*user.AuthFactor{
|
||||
{
|
||||
State: user.AuthFactorState_AUTH_FACTOR_STATE_READY,
|
||||
},
|
||||
},
|
||||
},
|
||||
dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) {
|
||||
userWithTOTP := Instance.CreateHumanUserWithTOTP(CTX, "secret").GetUserId()
|
||||
U2FIdWithTOTP := Instance.RegisterUserU2F(CTX, userWithTOTP)
|
||||
|
||||
args.UserId = userWithTOTP
|
||||
want.Result[0].Type = &user.AuthFactor_U2F{
|
||||
U2F: &user.AuthFactorU2F{
|
||||
Id: U2FIdWithTOTP,
|
||||
Name: "nice name",
|
||||
},
|
||||
}
|
||||
},
|
||||
ctx: CTX,
|
||||
},
|
||||
{
|
||||
name: "with sms",
|
||||
args: &user.ListAuthenticationFactorsRequest{},
|
||||
want: &user.ListAuthenticationFactorsResponse{
|
||||
Result: []*user.AuthFactor{
|
||||
{
|
||||
State: user.AuthFactorState_AUTH_FACTOR_STATE_READY,
|
||||
Type: &user.AuthFactor_OtpSms{
|
||||
OtpSms: &user.AuthFactorOTPSMS{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) {
|
||||
userWithSMS := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.GetId(), gofakeit.Email(), gofakeit.Phone()).GetUserId()
|
||||
Instance.RegisterUserOTPSMS(CTX, userWithSMS)
|
||||
|
||||
args.UserId = userWithSMS
|
||||
},
|
||||
ctx: CTX,
|
||||
},
|
||||
{
|
||||
name: "with email",
|
||||
args: &user.ListAuthenticationFactorsRequest{},
|
||||
want: &user.ListAuthenticationFactorsResponse{
|
||||
Result: []*user.AuthFactor{
|
||||
{
|
||||
State: user.AuthFactorState_AUTH_FACTOR_STATE_READY,
|
||||
Type: &user.AuthFactor_OtpEmail{
|
||||
OtpEmail: &user.AuthFactorOTPEmail{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) {
|
||||
userWithEmail := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.GetId(), gofakeit.Email(), gofakeit.Phone()).GetUserId()
|
||||
Instance.RegisterUserOTPEmail(CTX, userWithEmail)
|
||||
|
||||
args.UserId = userWithEmail
|
||||
},
|
||||
ctx: CTX,
|
||||
},
|
||||
{
|
||||
name: "with not ready u2f",
|
||||
args: &user.ListAuthenticationFactorsRequest{},
|
||||
want: &user.ListAuthenticationFactorsResponse{
|
||||
Result: []*user.AuthFactor{},
|
||||
},
|
||||
dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) {
|
||||
userWithNotReadyU2F := Instance.CreateHumanUser(CTX).GetUserId()
|
||||
_, err := Instance.Client.UserV2.RegisterU2F(CTX, &user.RegisterU2FRequest{
|
||||
UserId: userWithNotReadyU2F,
|
||||
Domain: Instance.Domain,
|
||||
})
|
||||
logging.OnError(err).Panic("Could not register u2f")
|
||||
|
||||
args.UserId = userWithNotReadyU2F
|
||||
},
|
||||
ctx: CTX,
|
||||
},
|
||||
{
|
||||
name: "with not ready u2f state filtered",
|
||||
args: &user.ListAuthenticationFactorsRequest{
|
||||
States: []user.AuthFactorState{user.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY},
|
||||
},
|
||||
want: &user.ListAuthenticationFactorsResponse{
|
||||
Result: []*user.AuthFactor{
|
||||
{
|
||||
State: user.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY,
|
||||
},
|
||||
},
|
||||
},
|
||||
dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) {
|
||||
userWithNotReadyU2F := Instance.CreateHumanUser(CTX).GetUserId()
|
||||
U2FNotReady, err := Instance.Client.UserV2.RegisterU2F(CTX, &user.RegisterU2FRequest{
|
||||
UserId: userWithNotReadyU2F,
|
||||
Domain: Instance.Domain,
|
||||
})
|
||||
logging.OnError(err).Panic("Could not register u2f")
|
||||
|
||||
args.UserId = userWithNotReadyU2F
|
||||
want.Result[0].Type = &user.AuthFactor_U2F{
|
||||
U2F: &user.AuthFactorU2F{
|
||||
Id: U2FNotReady.GetU2FId(),
|
||||
Name: "",
|
||||
},
|
||||
}
|
||||
},
|
||||
ctx: CTX,
|
||||
},
|
||||
{
|
||||
name: "with no userId",
|
||||
args: &user.ListAuthenticationFactorsRequest{
|
||||
UserId: "",
|
||||
},
|
||||
ctx: CTX,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "with no permission",
|
||||
args: &user.ListAuthenticationFactorsRequest{},
|
||||
dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) {
|
||||
userWithTOTP := Instance.CreateHumanUserWithTOTP(CTX, "totp").GetUserId()
|
||||
|
||||
args.UserId = userWithTOTP
|
||||
},
|
||||
ctx: UserCTX,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "with unknown user",
|
||||
args: &user.ListAuthenticationFactorsRequest{
|
||||
UserId: "unknown",
|
||||
},
|
||||
want: &user.ListAuthenticationFactorsResponse{},
|
||||
ctx: CTX,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.dep != nil {
|
||||
tt.dep(tt.args, tt.want)
|
||||
}
|
||||
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
got, err := Client.ListAuthenticationFactors(tt.ctx, tt.args)
|
||||
if tt.wantErr {
|
||||
require.Error(ttt, err)
|
||||
return
|
||||
}
|
||||
require.NoError(ttt, err)
|
||||
|
||||
assert.ElementsMatch(t, tt.want.GetResult(), got.GetResult())
|
||||
}, retryDuration, tick, "timeout waiting for expected auth methods result")
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_CreateInviteCode(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
|
@ -597,6 +597,39 @@ func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.Li
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ListAuthenticationFactors(ctx context.Context, req *user.ListAuthenticationFactorsRequest) (*user.ListAuthenticationFactorsResponse, error) {
|
||||
query := new(query.UserAuthMethodSearchQueries)
|
||||
|
||||
if err := query.AppendUserIDQuery(req.UserId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authMethodsType := []domain.UserAuthMethodType{domain.UserAuthMethodTypeU2F, domain.UserAuthMethodTypeTOTP, domain.UserAuthMethodTypeOTPSMS, domain.UserAuthMethodTypeOTPEmail}
|
||||
if len(req.GetAuthFactors()) > 0 {
|
||||
authMethodsType = object.AuthFactorsToPb(req.GetAuthFactors())
|
||||
}
|
||||
if err := query.AppendAuthMethodsQuery(authMethodsType...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
states := []domain.MFAState{domain.MFAStateReady}
|
||||
if len(req.GetStates()) > 0 {
|
||||
states = object.AuthFactorStatesToPb(req.GetStates())
|
||||
}
|
||||
if err := query.AppendStatesQuery(states...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authMethods, err := s.query.SearchUserAuthMethods(ctx, query, s.checkPermission)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user.ListAuthenticationFactorsResponse{
|
||||
Result: object.AuthMethodsToPb(authMethods),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func authMethodTypesToPb(methodTypes []domain.UserAuthMethodType) []user.AuthenticationMethodType {
|
||||
methods := make([]user.AuthenticationMethodType, len(methodTypes))
|
||||
for i, method := range methodTypes {
|
||||
|
@ -3,6 +3,7 @@ package login
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/crewjam/saml/samlsp"
|
||||
@ -36,6 +37,9 @@ import (
|
||||
|
||||
const (
|
||||
queryIDPConfigID = "idpConfigID"
|
||||
queryState = "state"
|
||||
queryRelayState = "RelayState"
|
||||
queryMethod = "method"
|
||||
tmplExternalNotFoundOption = "externalnotfoundoption"
|
||||
)
|
||||
|
||||
@ -214,13 +218,36 @@ func (l *Login) handleExternalLoginCallbackForm(w http.ResponseWriter, r *http.R
|
||||
l.renderLogin(w, r, nil, err)
|
||||
return
|
||||
}
|
||||
r.Form.Add("Method", http.MethodPost)
|
||||
http.Redirect(w, r, HandlerPrefix+EndpointExternalLoginCallback+"?"+r.Form.Encode(), 302)
|
||||
state := r.Form.Get(queryState)
|
||||
if state == "" {
|
||||
state = r.Form.Get(queryRelayState)
|
||||
}
|
||||
if state == "" {
|
||||
l.renderLogin(w, r, nil, zerrors.ThrowInvalidArgument(nil, "LOGIN-dsg3f", "Errors.AuthRequest.NotFound"))
|
||||
return
|
||||
}
|
||||
l.caches.idpFormCallbacks.Set(r.Context(), &idpFormCallback{
|
||||
InstanceID: authz.GetInstance(r.Context()).InstanceID(),
|
||||
State: state,
|
||||
Form: r.Form,
|
||||
})
|
||||
v := url.Values{}
|
||||
v.Set(queryMethod, http.MethodPost)
|
||||
v.Set(queryState, state)
|
||||
http.Redirect(w, r, HandlerPrefix+EndpointExternalLoginCallback+"?"+v.Encode(), 302)
|
||||
}
|
||||
|
||||
// handleExternalLoginCallback handles the callback from a IDP
|
||||
// and tries to extract the user with the provided data
|
||||
func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Request) {
|
||||
// workaround because of CSRF on external identity provider flows using form_post
|
||||
if r.URL.Query().Get(queryMethod) == http.MethodPost {
|
||||
if err := l.setDataFromFormCallback(r, r.URL.Query().Get(queryState)); err != nil {
|
||||
l.renderLogin(w, r, nil, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
data := new(externalIDPCallbackData)
|
||||
err := l.getParseData(r, data)
|
||||
if err != nil {
|
||||
@ -230,11 +257,6 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
|
||||
if data.State == "" {
|
||||
data.State = data.RelayState
|
||||
}
|
||||
// workaround because of CSRF on external identity provider flows
|
||||
if data.Method == http.MethodPost {
|
||||
r.Method = http.MethodPost
|
||||
r.PostForm = r.Form
|
||||
}
|
||||
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID)
|
||||
@ -345,6 +367,25 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
|
||||
l.handleExternalUserAuthenticated(w, r, authReq, identityProvider, session, user, l.renderNextStep)
|
||||
}
|
||||
|
||||
func (l *Login) setDataFromFormCallback(r *http.Request, state string) error {
|
||||
r.Method = http.MethodPost
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// fallback to the form data in case the request was started before the cache was implemented
|
||||
r.PostForm = r.Form
|
||||
idpCallback, ok := l.caches.idpFormCallbacks.Get(r.Context(), idpFormCallbackIndexRequestID,
|
||||
idpFormCallbackKey(authz.GetInstance(r.Context()).InstanceID(), state))
|
||||
if ok {
|
||||
r.PostForm = idpCallback.Form
|
||||
// We need to set the form as well to make sure the data is parsed correctly.
|
||||
// Form precedes PostForm in the parsing order.
|
||||
r.Form = idpCallback.Form
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Login) tryMigrateExternalUserID(r *http.Request, session idp.Session, authReq *domain.AuthRequest, externalUser *domain.ExternalUser) (previousIDMatched bool, err error) {
|
||||
migration, ok := session.(idp.SessionSupportsMigration)
|
||||
if !ok {
|
||||
|
@ -3,6 +3,7 @@ package login
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -15,6 +16,8 @@ import (
|
||||
_ "github.com/zitadel/zitadel/internal/api/ui/login/statik"
|
||||
auth_repository "github.com/zitadel/zitadel/internal/auth/repository"
|
||||
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing"
|
||||
"github.com/zitadel/zitadel/internal/cache"
|
||||
"github.com/zitadel/zitadel/internal/cache/connector"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
@ -38,6 +41,7 @@ type Login struct {
|
||||
samlAuthCallbackURL func(context.Context, string) string
|
||||
idpConfigAlg crypto.EncryptionAlgorithm
|
||||
userCodeAlg crypto.EncryptionAlgorithm
|
||||
caches *Caches
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
@ -74,6 +78,7 @@ func CreateLogin(config Config,
|
||||
userCodeAlg crypto.EncryptionAlgorithm,
|
||||
idpConfigAlg crypto.EncryptionAlgorithm,
|
||||
csrfCookieKey []byte,
|
||||
cacheConnectors connector.Connectors,
|
||||
) (*Login, error) {
|
||||
login := &Login{
|
||||
oidcAuthCallbackURL: oidcAuthCallbackURL,
|
||||
@ -94,6 +99,12 @@ func CreateLogin(config Config,
|
||||
login.router = CreateRouter(login, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), oidcInstanceHandler, samlInstanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor, accessHandler)
|
||||
login.renderer = CreateRenderer(HandlerPrefix, staticStorage, config.LanguageCookieName)
|
||||
login.parser = form.NewParser()
|
||||
|
||||
var err error
|
||||
login.caches, err = startCaches(context.Background(), cacheConnectors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return login, nil
|
||||
}
|
||||
|
||||
@ -201,3 +212,41 @@ func setUserContext(ctx context.Context, userID, resourceOwner string) context.C
|
||||
func (l *Login) baseURL(ctx context.Context) string {
|
||||
return http_utils.DomainContext(ctx).Origin() + HandlerPrefix
|
||||
}
|
||||
|
||||
type Caches struct {
|
||||
idpFormCallbacks cache.Cache[idpFormCallbackIndex, string, *idpFormCallback]
|
||||
}
|
||||
|
||||
func startCaches(background context.Context, connectors connector.Connectors) (_ *Caches, err error) {
|
||||
caches := new(Caches)
|
||||
caches.idpFormCallbacks, err = connector.StartCache[idpFormCallbackIndex, string, *idpFormCallback](background, []idpFormCallbackIndex{idpFormCallbackIndexRequestID}, cache.PurposeIdPFormCallback, connectors.Config.IdPFormCallbacks, connectors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return caches, nil
|
||||
}
|
||||
|
||||
type idpFormCallbackIndex int
|
||||
|
||||
const (
|
||||
idpFormCallbackIndexUnspecified idpFormCallbackIndex = iota
|
||||
idpFormCallbackIndexRequestID
|
||||
)
|
||||
|
||||
type idpFormCallback struct {
|
||||
InstanceID string
|
||||
State string
|
||||
Form url.Values
|
||||
}
|
||||
|
||||
// Keys implements cache.Entry
|
||||
func (c *idpFormCallback) Keys(i idpFormCallbackIndex) []string {
|
||||
if i == idpFormCallbackIndexRequestID {
|
||||
return []string{idpFormCallbackKey(c.InstanceID, c.State)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func idpFormCallbackKey(instanceID, state string) string {
|
||||
return instanceID + "-" + state
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ InitMFAPrompt:
|
||||
InitMFAOTP:
|
||||
Title: Zwei-Faktor-Authentifizierung
|
||||
Description: Erstelle deinen Zweitfaktor. Installiere eine Authentifizierungs-App, wenn du noch keine hast.
|
||||
OTPDescription: Scanne den Code mit einer Authentifizierungs-App (z.B. Google/Mircorsoft Authenticator, Authy) oder kopiere das Secret und gib anschliessend den Code ein.
|
||||
OTPDescription: Scanne den Code mit einer Authentifizierungs-App (z.B. Google/Microsoft Authenticator, Authy) oder kopiere das Secret und gib anschliessend den Code ein.
|
||||
SecretLabel: Secret
|
||||
CodeLabel: Code
|
||||
NextButtonText: Weiter
|
||||
|
1
internal/cache/cache.go
vendored
1
internal/cache/cache.go
vendored
@ -17,6 +17,7 @@ const (
|
||||
PurposeAuthzInstance
|
||||
PurposeMilestones
|
||||
PurposeOrganization
|
||||
PurposeIdPFormCallback
|
||||
)
|
||||
|
||||
// Cache stores objects with a value of type `V`.
|
||||
|
7
internal/cache/connector/connector.go
vendored
7
internal/cache/connector/connector.go
vendored
@ -19,9 +19,10 @@ type CachesConfig struct {
|
||||
Postgres pg.Config
|
||||
Redis redis.Config
|
||||
}
|
||||
Instance *cache.Config
|
||||
Milestones *cache.Config
|
||||
Organization *cache.Config
|
||||
Instance *cache.Config
|
||||
Milestones *cache.Config
|
||||
Organization *cache.Config
|
||||
IdPFormCallbacks *cache.Config
|
||||
}
|
||||
|
||||
type Connectors struct {
|
||||
|
4
internal/cache/connector/redis/get.lua
vendored
4
internal/cache/connector/redis/get.lua
vendored
@ -13,8 +13,8 @@ end
|
||||
|
||||
-- max-age must be checked manually
|
||||
local expiry = getCall("HGET", object_id, "expiry")
|
||||
if not (expiry == nil) and expiry > 0 then
|
||||
if getTime() > expiry then
|
||||
if not (expiry == nil) and tonumber(expiry) > 0 then
|
||||
if getTime() > tonumber(expiry) then
|
||||
remove(object_id)
|
||||
return nil
|
||||
end
|
||||
|
12
internal/cache/purpose_enumer.go
vendored
12
internal/cache/purpose_enumer.go
vendored
@ -7,11 +7,11 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _PurposeName = "unspecifiedauthz_instancemilestonesorganization"
|
||||
const _PurposeName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callback"
|
||||
|
||||
var _PurposeIndex = [...]uint8{0, 11, 25, 35, 47}
|
||||
var _PurposeIndex = [...]uint8{0, 11, 25, 35, 47, 65}
|
||||
|
||||
const _PurposeLowerName = "unspecifiedauthz_instancemilestonesorganization"
|
||||
const _PurposeLowerName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callback"
|
||||
|
||||
func (i Purpose) String() string {
|
||||
if i < 0 || i >= Purpose(len(_PurposeIndex)-1) {
|
||||
@ -28,9 +28,10 @@ func _PurposeNoOp() {
|
||||
_ = x[PurposeAuthzInstance-(1)]
|
||||
_ = x[PurposeMilestones-(2)]
|
||||
_ = x[PurposeOrganization-(3)]
|
||||
_ = x[PurposeIdPFormCallback-(4)]
|
||||
}
|
||||
|
||||
var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones, PurposeOrganization}
|
||||
var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones, PurposeOrganization, PurposeIdPFormCallback}
|
||||
|
||||
var _PurposeNameToValueMap = map[string]Purpose{
|
||||
_PurposeName[0:11]: PurposeUnspecified,
|
||||
@ -41,6 +42,8 @@ var _PurposeNameToValueMap = map[string]Purpose{
|
||||
_PurposeLowerName[25:35]: PurposeMilestones,
|
||||
_PurposeName[35:47]: PurposeOrganization,
|
||||
_PurposeLowerName[35:47]: PurposeOrganization,
|
||||
_PurposeName[47:65]: PurposeIdPFormCallback,
|
||||
_PurposeLowerName[47:65]: PurposeIdPFormCallback,
|
||||
}
|
||||
|
||||
var _PurposeNames = []string{
|
||||
@ -48,6 +51,7 @@ var _PurposeNames = []string{
|
||||
_PurposeName[11:25],
|
||||
_PurposeName[25:35],
|
||||
_PurposeName[35:47],
|
||||
_PurposeName[47:65],
|
||||
}
|
||||
|
||||
// PurposeString retrieves an enum value from the enum constants string name.
|
||||
|
@ -327,7 +327,7 @@ func (i *Instance) CreateUserIDPlink(ctx context.Context, userID, externalID, id
|
||||
)
|
||||
}
|
||||
|
||||
func (i *Instance) RegisterUserPasskey(ctx context.Context, userID string) {
|
||||
func (i *Instance) RegisterUserPasskey(ctx context.Context, userID string) string {
|
||||
reg, err := i.Client.UserV2.CreatePasskeyRegistrationLink(ctx, &user_v2.CreatePasskeyRegistrationLinkRequest{
|
||||
UserId: userID,
|
||||
Medium: &user_v2.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
|
||||
@ -350,9 +350,10 @@ func (i *Instance) RegisterUserPasskey(ctx context.Context, userID string) {
|
||||
PasskeyName: "nice name",
|
||||
})
|
||||
logging.OnError(err).Panic("create user passkey")
|
||||
return pkr.GetPasskeyId()
|
||||
}
|
||||
|
||||
func (i *Instance) RegisterUserU2F(ctx context.Context, userID string) {
|
||||
func (i *Instance) RegisterUserU2F(ctx context.Context, userID string) string {
|
||||
pkr, err := i.Client.UserV2.RegisterU2F(ctx, &user_v2.RegisterU2FRequest{
|
||||
UserId: userID,
|
||||
Domain: i.Domain,
|
||||
@ -368,6 +369,21 @@ func (i *Instance) RegisterUserU2F(ctx context.Context, userID string) {
|
||||
TokenName: "nice name",
|
||||
})
|
||||
logging.OnError(err).Panic("create user u2f")
|
||||
return pkr.GetU2FId()
|
||||
}
|
||||
|
||||
func (i *Instance) RegisterUserOTPSMS(ctx context.Context, userID string) {
|
||||
_, err := i.Client.UserV2.AddOTPSMS(ctx, &user_v2.AddOTPSMSRequest{
|
||||
UserId: userID,
|
||||
})
|
||||
logging.OnError(err).Panic("create user sms")
|
||||
}
|
||||
|
||||
func (i *Instance) RegisterUserOTPEmail(ctx context.Context, userID string) {
|
||||
_, err := i.Client.UserV2.AddOTPEmail(ctx, &user_v2.AddOTPEmailRequest{
|
||||
UserId: userID,
|
||||
})
|
||||
logging.OnError(err).Panic("create user email")
|
||||
}
|
||||
|
||||
func (i *Instance) SetUserPassword(ctx context.Context, userID, password string, changeRequired bool) *object.Details {
|
||||
|
@ -270,6 +270,14 @@ func NewUserAuthMethodTypesSearchQuery(values ...domain.UserAuthMethodType) (Sea
|
||||
return NewListQuery(UserAuthMethodColumnMethodType, list, ListIn)
|
||||
}
|
||||
|
||||
func NewUserAuthMethodStatesSearchQuery(values ...domain.MFAState) (SearchQuery, error) {
|
||||
list := make([]interface{}, len(values))
|
||||
for i, value := range values {
|
||||
list[i] = value
|
||||
}
|
||||
return NewListQuery(UserAuthMethodColumnState, list, ListIn)
|
||||
}
|
||||
|
||||
func (r *UserAuthMethodSearchQueries) AppendResourceOwnerQuery(orgID string) error {
|
||||
query, err := NewUserAuthMethodResourceOwnerSearchQuery(orgID)
|
||||
if err != nil {
|
||||
@ -306,6 +314,15 @@ func (r *UserAuthMethodSearchQueries) AppendStateQuery(state domain.MFAState) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UserAuthMethodSearchQueries) AppendStatesQuery(state ...domain.MFAState) error {
|
||||
query, err := NewUserAuthMethodStatesSearchQuery(state...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Queries = append(r.Queries, query)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UserAuthMethodSearchQueries) AppendAuthMethodQuery(authMethod domain.UserAuthMethodType) error {
|
||||
query, err := NewUserAuthMethodTypeSearchQuery(authMethod)
|
||||
if err != nil {
|
||||
|
@ -276,6 +276,36 @@ message Passkey {
|
||||
];
|
||||
}
|
||||
|
||||
message AuthFactor {
|
||||
AuthFactorState state = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "current state of the auth factor";
|
||||
}
|
||||
];
|
||||
oneof type {
|
||||
AuthFactorOTP otp = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "TOTP second factor"
|
||||
}
|
||||
];
|
||||
AuthFactorU2F u2f = 3 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "U2F second factor"
|
||||
}
|
||||
];
|
||||
AuthFactorOTPSMS otp_sms = 4 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "SMS second factor"
|
||||
}
|
||||
];
|
||||
AuthFactorOTPEmail otp_email = 5 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "Email second factor"
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
enum AuthFactorState {
|
||||
AUTH_FACTOR_STATE_UNSPECIFIED = 0;
|
||||
AUTH_FACTOR_STATE_NOT_READY = 1;
|
||||
@ -283,6 +313,23 @@ enum AuthFactorState {
|
||||
AUTH_FACTOR_STATE_REMOVED = 3;
|
||||
}
|
||||
|
||||
message AuthFactorOTP {}
|
||||
message AuthFactorOTPSMS {}
|
||||
message AuthFactorOTPEmail {}
|
||||
|
||||
message AuthFactorU2F {
|
||||
string id = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"69629023906488334\""
|
||||
}
|
||||
];
|
||||
string name = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"fido key\""
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message SendInviteCode {
|
||||
// Optionally set a url_template, which will be used in the invite mail sent by ZITADEL to guide the user to your invitation page.
|
||||
// If no template is set, the default ZITADEL url will be used.
|
||||
|
@ -1110,6 +1110,28 @@ service UserService {
|
||||
};
|
||||
}
|
||||
|
||||
rpc ListAuthenticationFactors(ListAuthenticationFactorsRequest) returns (ListAuthenticationFactorsResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v2/users/{user_id}/authentication_factors/_search"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "authenticated"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// Create an invite code for a user
|
||||
//
|
||||
// Create an invite code for a user to initialize their first authentication method (password, passkeys, IdP) depending on the organization's available methods.
|
||||
@ -2216,6 +2238,41 @@ enum AuthenticationMethodType {
|
||||
AUTHENTICATION_METHOD_TYPE_OTP_EMAIL = 7;
|
||||
}
|
||||
|
||||
message ListAuthenticationFactorsRequest{
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"69629026806489455\"";
|
||||
}
|
||||
];
|
||||
repeated AuthFactors auth_factors = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "Specify the Auth Factors you are interested in"
|
||||
default: "All Auth Factors"
|
||||
}
|
||||
];
|
||||
repeated AuthFactorState states = 3 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "Specify the state of the Auth Factors"
|
||||
default: "Auth Factors that are ready"
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
enum AuthFactors {
|
||||
OTP = 0;
|
||||
OTP_SMS = 1;
|
||||
OTP_EMAIL = 2;
|
||||
U2F = 3;
|
||||
}
|
||||
|
||||
message ListAuthenticationFactorsResponse {
|
||||
repeated zitadel.user.v2.AuthFactor result = 1;
|
||||
}
|
||||
|
||||
message CreateInviteCodeRequest {
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
|
Loading…
x
Reference in New Issue
Block a user