mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-16 12:58:00 +00:00
f5e9d4f57f
# Which Problems Are Solved After https://github.com/zitadel/zitadel/pull/7822 was merged we discovered that v2 tokens that where obtained through an IDP using the v1 login, can't be used for zitadel API calls. - Because we used to store the AMR claim on the auth request, but internally use the domain.UserAuthMethod type. AMR has no notion of an IDP login, so that "factor" was lost during conversion. Rendering those v2 tokens invalid on the zitadel API. - A wrong check on machine user tokens falsly allowed some tokens to be valid - The client ID was set to tokens from client credentials and JWT profile, which made client queries fail in the validation middleware. The middleware expects client ID unset for machine users. # How the Problems Are Solved Store the domain.AuthMethods directly in the auth requests and session, instead of using AMR claims with lossy conversion. - IDPs have seperate auth method, which is not an AMR claim - Machine users are treated specialy, eg auth methods are not required. - Do not set the client ID for client credentials and JWT profile # Additional Changes Cleaned up mostly unused `oidc.getInfoFromRequest()`. # Additional Context - Bugs were introduced in https://github.com/zitadel/zitadel/pull/7822 and not yet part of a release. - Reported internally.
446 lines
14 KiB
Go
446 lines
14 KiB
Go
package query
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"database/sql/driver"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"testing"
|
|
|
|
sq "github.com/Masterminds/squirrel"
|
|
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
)
|
|
|
|
var (
|
|
prepareUserAuthMethodsStmt = `SELECT projections.user_auth_methods4.token_id,` +
|
|
` projections.user_auth_methods4.creation_date,` +
|
|
` projections.user_auth_methods4.change_date,` +
|
|
` projections.user_auth_methods4.resource_owner,` +
|
|
` projections.user_auth_methods4.user_id,` +
|
|
` projections.user_auth_methods4.sequence,` +
|
|
` projections.user_auth_methods4.name,` +
|
|
` projections.user_auth_methods4.state,` +
|
|
` projections.user_auth_methods4.method_type,` +
|
|
` COUNT(*) OVER ()` +
|
|
` FROM projections.user_auth_methods4` +
|
|
` AS OF SYSTEM TIME '-1 ms'`
|
|
prepareUserAuthMethodsCols = []string{
|
|
"token_id",
|
|
"creation_date",
|
|
"change_date",
|
|
"resource_owner",
|
|
"user_id",
|
|
"sequence",
|
|
"name",
|
|
"state",
|
|
"method_type",
|
|
"count",
|
|
}
|
|
prepareActiveAuthMethodTypesStmt = `SELECT projections.users12_notifications.password_set,` +
|
|
` auth_method_types.method_type,` +
|
|
` user_idps_count.count` +
|
|
` FROM projections.users12` +
|
|
` LEFT JOIN projections.users12_notifications ON projections.users12.id = projections.users12_notifications.user_id AND projections.users12.instance_id = projections.users12_notifications.instance_id` +
|
|
` LEFT JOIN (SELECT DISTINCT(auth_method_types.method_type), auth_method_types.user_id, auth_method_types.instance_id FROM projections.user_auth_methods4 AS auth_method_types` +
|
|
` WHERE auth_method_types.state = $1) AS auth_method_types` +
|
|
` ON auth_method_types.user_id = projections.users12.id AND auth_method_types.instance_id = projections.users12.instance_id` +
|
|
` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` +
|
|
` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` +
|
|
` ON user_idps_count.user_id = projections.users12.id AND user_idps_count.instance_id = projections.users12.instance_id` +
|
|
` AS OF SYSTEM TIME '-1 ms`
|
|
prepareActiveAuthMethodTypesCols = []string{
|
|
"password_set",
|
|
"method_type",
|
|
"idps_count",
|
|
}
|
|
prepareAuthMethodTypesRequiredStmt = `SELECT projections.users12_notifications.password_set,` +
|
|
` auth_method_types.method_type,` +
|
|
` user_idps_count.count,` +
|
|
` projections.users12.type,` +
|
|
` auth_methods_force_mfa.force_mfa,` +
|
|
` auth_methods_force_mfa.force_mfa_local_only` +
|
|
` FROM projections.users12` +
|
|
` LEFT JOIN projections.users12_notifications ON projections.users12.id = projections.users12_notifications.user_id AND projections.users12.instance_id = projections.users12_notifications.instance_id` +
|
|
` LEFT JOIN (SELECT DISTINCT(auth_method_types.method_type), auth_method_types.user_id, auth_method_types.instance_id FROM projections.user_auth_methods4 AS auth_method_types` +
|
|
` WHERE auth_method_types.state = $1) AS auth_method_types` +
|
|
` ON auth_method_types.user_id = projections.users12.id AND auth_method_types.instance_id = projections.users12.instance_id` +
|
|
` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` +
|
|
` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` +
|
|
` ON user_idps_count.user_id = projections.users12.id AND user_idps_count.instance_id = projections.users12.instance_id` +
|
|
` LEFT JOIN (SELECT auth_methods_force_mfa.force_mfa, auth_methods_force_mfa.force_mfa_local_only, auth_methods_force_mfa.instance_id, auth_methods_force_mfa.aggregate_id FROM projections.login_policies5 AS auth_methods_force_mfa ORDER BY auth_methods_force_mfa.is_default) AS auth_methods_force_mfa` +
|
|
` ON (auth_methods_force_mfa.aggregate_id = projections.users12.instance_id OR auth_methods_force_mfa.aggregate_id = projections.users12.resource_owner) AND auth_methods_force_mfa.instance_id = projections.users12.instance_id` +
|
|
` AS OF SYSTEM TIME '-1 ms
|
|
`
|
|
prepareAuthMethodTypesRequiredCols = []string{
|
|
"password_set",
|
|
"type",
|
|
"method_type",
|
|
"idps_count",
|
|
"force_mfa",
|
|
"force_mfa_local_only",
|
|
}
|
|
)
|
|
|
|
func Test_UserAuthMethodPrepares(t *testing.T) {
|
|
type want struct {
|
|
sqlExpectations sqlExpectation
|
|
err checkErr
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
prepare interface{}
|
|
want want
|
|
object interface{}
|
|
}{
|
|
{
|
|
name: "prepareUserAuthMethodsQuery no result",
|
|
prepare: prepareUserAuthMethodsQuery,
|
|
want: want{
|
|
sqlExpectations: mockQueries(
|
|
regexp.QuoteMeta(prepareUserAuthMethodsStmt),
|
|
nil,
|
|
nil,
|
|
),
|
|
},
|
|
object: &AuthMethods{AuthMethods: []*AuthMethod{}},
|
|
},
|
|
{
|
|
name: "prepareUserAuthMethodsQuery one result",
|
|
prepare: prepareUserAuthMethodsQuery,
|
|
want: want{
|
|
sqlExpectations: mockQueries(
|
|
regexp.QuoteMeta(prepareUserAuthMethodsStmt),
|
|
prepareUserAuthMethodsCols,
|
|
[][]driver.Value{
|
|
{
|
|
"token_id",
|
|
testNow,
|
|
testNow,
|
|
"ro",
|
|
"user_id",
|
|
uint64(20211108),
|
|
"name",
|
|
domain.MFAStateReady,
|
|
domain.UserAuthMethodTypeU2F,
|
|
},
|
|
},
|
|
),
|
|
},
|
|
object: &AuthMethods{
|
|
SearchResponse: SearchResponse{
|
|
Count: 1,
|
|
},
|
|
AuthMethods: []*AuthMethod{
|
|
{
|
|
TokenID: "token_id",
|
|
CreationDate: testNow,
|
|
ChangeDate: testNow,
|
|
ResourceOwner: "ro",
|
|
UserID: "user_id",
|
|
Sequence: 20211108,
|
|
Name: "name",
|
|
State: domain.MFAStateReady,
|
|
Type: domain.UserAuthMethodTypeU2F,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "prepareUserAuthMethodsQuery multiple result",
|
|
prepare: prepareUserAuthMethodsQuery,
|
|
want: want{
|
|
sqlExpectations: mockQueries(
|
|
regexp.QuoteMeta(prepareUserAuthMethodsStmt),
|
|
prepareUserAuthMethodsCols,
|
|
[][]driver.Value{
|
|
{
|
|
"token_id",
|
|
testNow,
|
|
testNow,
|
|
"ro",
|
|
"user_id",
|
|
uint64(20211108),
|
|
"name",
|
|
domain.MFAStateReady,
|
|
domain.UserAuthMethodTypeU2F,
|
|
},
|
|
{
|
|
"token_id-2",
|
|
testNow,
|
|
testNow,
|
|
"ro",
|
|
"user_id",
|
|
uint64(20211108),
|
|
"name-2",
|
|
domain.MFAStateReady,
|
|
domain.UserAuthMethodTypePasswordless,
|
|
},
|
|
},
|
|
),
|
|
},
|
|
object: &AuthMethods{
|
|
SearchResponse: SearchResponse{
|
|
Count: 2,
|
|
},
|
|
AuthMethods: []*AuthMethod{
|
|
{
|
|
TokenID: "token_id",
|
|
CreationDate: testNow,
|
|
ChangeDate: testNow,
|
|
ResourceOwner: "ro",
|
|
UserID: "user_id",
|
|
Sequence: 20211108,
|
|
Name: "name",
|
|
State: domain.MFAStateReady,
|
|
Type: domain.UserAuthMethodTypeU2F,
|
|
},
|
|
{
|
|
TokenID: "token_id-2",
|
|
CreationDate: testNow,
|
|
ChangeDate: testNow,
|
|
ResourceOwner: "ro",
|
|
UserID: "user_id",
|
|
Sequence: 20211108,
|
|
Name: "name-2",
|
|
State: domain.MFAStateReady,
|
|
Type: domain.UserAuthMethodTypePasswordless,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "prepareUserAuthMethodsQuery sql err",
|
|
prepare: prepareUserAuthMethodsQuery,
|
|
want: want{
|
|
sqlExpectations: mockQueryErr(
|
|
regexp.QuoteMeta(prepareUserAuthMethodsStmt),
|
|
sql.ErrConnDone,
|
|
),
|
|
err: func(err error) (error, bool) {
|
|
if !errors.Is(err, sql.ErrConnDone) {
|
|
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
|
|
}
|
|
return nil, true
|
|
},
|
|
},
|
|
object: (*AuthMethodTypes)(nil),
|
|
},
|
|
{
|
|
name: "prepareActiveUserAuthMethodTypesQuery no result",
|
|
prepare: prepareActiveUserAuthMethodTypesQuery,
|
|
want: want{
|
|
sqlExpectations: mockQueries(
|
|
regexp.QuoteMeta(prepareActiveAuthMethodTypesStmt),
|
|
nil,
|
|
nil,
|
|
),
|
|
},
|
|
object: &AuthMethodTypes{AuthMethodTypes: []domain.UserAuthMethodType{}},
|
|
},
|
|
{
|
|
name: "prepareActiveUserAuthMethodTypesQuery one second factor",
|
|
prepare: prepareActiveUserAuthMethodTypesQuery,
|
|
want: want{
|
|
sqlExpectations: mockQueries(
|
|
regexp.QuoteMeta(prepareActiveAuthMethodTypesStmt),
|
|
prepareActiveAuthMethodTypesCols,
|
|
[][]driver.Value{
|
|
{
|
|
true,
|
|
domain.UserAuthMethodTypePasswordless,
|
|
1,
|
|
},
|
|
},
|
|
),
|
|
},
|
|
object: &AuthMethodTypes{
|
|
SearchResponse: SearchResponse{
|
|
Count: 3,
|
|
},
|
|
AuthMethodTypes: []domain.UserAuthMethodType{
|
|
domain.UserAuthMethodTypePasswordless,
|
|
domain.UserAuthMethodTypePassword,
|
|
domain.UserAuthMethodTypeIDP,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "prepareActiveUserAuthMethodTypesQuery multiple second factors",
|
|
prepare: prepareActiveUserAuthMethodTypesQuery,
|
|
want: want{
|
|
sqlExpectations: mockQueries(
|
|
regexp.QuoteMeta(prepareActiveAuthMethodTypesStmt),
|
|
prepareActiveAuthMethodTypesCols,
|
|
[][]driver.Value{
|
|
{
|
|
true,
|
|
domain.UserAuthMethodTypePasswordless,
|
|
1,
|
|
},
|
|
{
|
|
true,
|
|
domain.UserAuthMethodTypeTOTP,
|
|
1,
|
|
},
|
|
},
|
|
),
|
|
},
|
|
object: &AuthMethodTypes{
|
|
SearchResponse: SearchResponse{
|
|
Count: 4,
|
|
},
|
|
AuthMethodTypes: []domain.UserAuthMethodType{
|
|
domain.UserAuthMethodTypePasswordless,
|
|
domain.UserAuthMethodTypeTOTP,
|
|
domain.UserAuthMethodTypePassword,
|
|
domain.UserAuthMethodTypeIDP,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "prepareActiveUserAuthMethodTypesQuery sql err",
|
|
prepare: prepareActiveUserAuthMethodTypesQuery,
|
|
want: want{
|
|
sqlExpectations: mockQueryErr(
|
|
regexp.QuoteMeta(prepareActiveAuthMethodTypesStmt),
|
|
sql.ErrConnDone,
|
|
),
|
|
err: func(err error) (error, bool) {
|
|
if !errors.Is(err, sql.ErrConnDone) {
|
|
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
|
|
}
|
|
return nil, true
|
|
},
|
|
},
|
|
object: (*AuthMethodTypes)(nil),
|
|
},
|
|
{
|
|
name: "prepareUserAuthMethodTypesRequiredQuery no result",
|
|
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserAuthMethodRequirements, error)) {
|
|
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
|
|
return builder, func(rows *sql.Rows) (*UserAuthMethodRequirements, error) {
|
|
return scan(rows)
|
|
}
|
|
},
|
|
want: want{
|
|
sqlExpectations: mockQueries(
|
|
regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt),
|
|
nil,
|
|
nil,
|
|
),
|
|
},
|
|
object: &UserAuthMethodRequirements{AuthMethods: []domain.UserAuthMethodType{}, ForceMFA: false},
|
|
},
|
|
{
|
|
name: "prepareUserAuthMethodTypesRequiredQuery one second factor",
|
|
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserAuthMethodRequirements, error)) {
|
|
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
|
|
return builder, func(rows *sql.Rows) (*UserAuthMethodRequirements, error) {
|
|
return scan(rows)
|
|
}
|
|
},
|
|
want: want{
|
|
sqlExpectations: mockQueries(
|
|
regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt),
|
|
prepareAuthMethodTypesRequiredCols,
|
|
[][]driver.Value{
|
|
{
|
|
true,
|
|
domain.UserAuthMethodTypePasswordless,
|
|
1,
|
|
domain.UserTypeHuman,
|
|
true,
|
|
true,
|
|
},
|
|
},
|
|
),
|
|
},
|
|
object: &UserAuthMethodRequirements{
|
|
UserType: domain.UserTypeHuman,
|
|
AuthMethods: []domain.UserAuthMethodType{
|
|
domain.UserAuthMethodTypePasswordless,
|
|
domain.UserAuthMethodTypePassword,
|
|
domain.UserAuthMethodTypeIDP,
|
|
},
|
|
ForceMFA: true,
|
|
ForceMFALocalOnly: true,
|
|
},
|
|
},
|
|
{
|
|
name: "prepareUserAuthMethodTypesRequiredQuery multiple second factors",
|
|
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserAuthMethodRequirements, error)) {
|
|
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
|
|
return builder, func(rows *sql.Rows) (*UserAuthMethodRequirements, error) {
|
|
return scan(rows)
|
|
}
|
|
},
|
|
want: want{
|
|
sqlExpectations: mockQueries(
|
|
regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt),
|
|
prepareAuthMethodTypesRequiredCols,
|
|
[][]driver.Value{
|
|
{
|
|
true,
|
|
domain.UserAuthMethodTypePasswordless,
|
|
1,
|
|
domain.UserTypeHuman,
|
|
true,
|
|
true,
|
|
},
|
|
{
|
|
true,
|
|
domain.UserAuthMethodTypeTOTP,
|
|
1,
|
|
domain.UserTypeHuman,
|
|
true,
|
|
true,
|
|
},
|
|
},
|
|
),
|
|
},
|
|
|
|
object: &UserAuthMethodRequirements{
|
|
UserType: domain.UserTypeHuman,
|
|
AuthMethods: []domain.UserAuthMethodType{
|
|
domain.UserAuthMethodTypePasswordless,
|
|
domain.UserAuthMethodTypeTOTP,
|
|
domain.UserAuthMethodTypePassword,
|
|
domain.UserAuthMethodTypeIDP,
|
|
},
|
|
ForceMFA: true,
|
|
ForceMFALocalOnly: true,
|
|
},
|
|
},
|
|
{
|
|
name: "prepareUserAuthMethodTypesRequiredQuery sql err",
|
|
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserAuthMethodRequirements, error)) {
|
|
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
|
|
return builder, func(rows *sql.Rows) (*UserAuthMethodRequirements, error) {
|
|
return scan(rows)
|
|
}
|
|
},
|
|
want: want{
|
|
sqlExpectations: mockQueryErr(
|
|
regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt),
|
|
sql.ErrConnDone,
|
|
),
|
|
err: func(err error) (error, bool) {
|
|
if !errors.Is(err, sql.ErrConnDone) {
|
|
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
|
|
}
|
|
return nil, true
|
|
},
|
|
},
|
|
object: nil,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...)
|
|
})
|
|
}
|
|
}
|