feat(API): support V2 token and session token usage (#6180)

This PR adds support for userinfo and introspection of V2 tokens. Further V2 access tokens and session tokens can be used for authentication on the ZITADEL API (like the current access tokens).
This commit is contained in:
Livio Spring 2023-07-14 13:16:16 +02:00 committed by GitHub
parent 4589ddad4a
commit 80961125a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1309 additions and 181 deletions

View File

@ -22,7 +22,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version: '1.20'
- name: Source checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx

View File

@ -17,7 +17,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version: '1.20'
- name: Source checkout
uses: actions/checkout@v3
- name: Set up QEMU

View File

@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version: '1.20'
- name: Source checkout
uses: actions/checkout@v3
with:

View File

@ -1,4 +1,4 @@
ARG GO_VERSION=1.19
ARG GO_VERSION=1.20
#######################
## Go dependencies

2
go.mod
View File

@ -1,6 +1,6 @@
module github.com/zitadel/zitadel
go 1.19
go 1.20
require (
cloud.google.com/go/storage v1.30.1

View File

@ -20,7 +20,8 @@ import (
const (
BearerPrefix = "Bearer "
SessionTokenFormat = "sess_%s:%s"
SessionTokenPrefix = "sess_"
SessionTokenFormat = SessionTokenPrefix + "%s:%s"
)
type TokenVerifier struct {

View File

@ -49,7 +49,9 @@ func TestMain(m *testing.M) {
}
func TestServer_GetAuthRequest(t *testing.T) {
client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI)
project, err := Tester.CreateProject(CTX)
require.NoError(t, err)
client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, project.GetId())
require.NoError(t, err)
authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
require.NoError(t, err)
@ -91,7 +93,9 @@ func TestServer_GetAuthRequest(t *testing.T) {
}
func TestServer_CreateCallback(t *testing.T) {
client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI)
project, err := Tester.CreateProject(CTX)
require.NoError(t, err)
client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, project.GetId())
require.NoError(t, err)
sessionResp, err := Tester.Client.SessionV2.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{

View File

@ -156,10 +156,11 @@ func userFactorToPb(factor query.SessionUserFactor) *session.UserFactor {
return nil
}
return &session.UserFactor{
VerifiedAt: timestamppb.New(factor.UserCheckedAt),
Id: factor.UserID,
LoginName: factor.LoginName,
DisplayName: factor.DisplayName,
VerifiedAt: timestamppb.New(factor.UserCheckedAt),
Id: factor.UserID,
LoginName: factor.LoginName,
DisplayName: factor.DisplayName,
OrganisationId: factor.ResourceOwner,
}
}

View File

@ -4,12 +4,15 @@ package session_test
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/metadata"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
@ -35,6 +38,7 @@ func TestMain(m *testing.M) {
CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
User = Tester.CreateHumanUser(CTX)
Tester.SetUserPassword(CTX, User.GetUserId(), integration.UserPassword)
Tester.RegisterUserPasskey(CTX, User.GetUserId())
return m.Run()
}())
@ -377,3 +381,51 @@ func TestServer_SetSession_flow(t *testing.T) {
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, wantFactors...)
})
}
func Test_ZITADEL_API_missing_authentication(t *testing.T) {
// create new, empty session
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{Domain: Tester.Config.ExternalDomain})
require.NoError(t, err)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", createResp.GetSessionToken()))
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: createResp.GetSessionId()})
require.Error(t, err)
require.Nil(t, sessionResp)
}
func Test_ZITADEL_API_missing_mfa(t *testing.T) {
id, token, _, _ := Tester.CreatePasswordSession(t, CTX, User.GetUserId(), integration.UserPassword)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.Error(t, err)
require.Nil(t, sessionResp)
}
func Test_ZITADEL_API_success(t *testing.T) {
id, token, _, _ := Tester.CreatePasskeySession(t, CTX, User.GetUserId())
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
require.NotNil(t, id, sessionResp.GetSession().GetFactors().GetPasskey().GetVerifiedAt().AsTime())
}
func Test_ZITADEL_API_session_not_found(t *testing.T) {
id, token, _, _ := Tester.CreatePasskeySession(t, CTX, User.GetUserId())
// test session token works
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
_, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
//terminate the session and test it does not work anymore
_, err = Tester.Client.SessionV2.DeleteSession(CTX, &session.DeleteSessionRequest{
SessionId: id,
SessionToken: gu.Ptr(token),
})
require.NoError(t, err)
ctx = metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
_, err = Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.Error(t, err)
}

View File

@ -46,6 +46,7 @@ func Test_sessionsToPb(t *testing.T) {
UserCheckedAt: past,
LoginName: "donald",
DisplayName: "donald duck",
ResourceOwner: "org1",
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
@ -62,6 +63,7 @@ func Test_sessionsToPb(t *testing.T) {
UserCheckedAt: past,
LoginName: "donald",
DisplayName: "donald duck",
ResourceOwner: "org1",
},
PasswordFactor: query.SessionPasswordFactor{
PasswordCheckedAt: past,
@ -81,6 +83,7 @@ func Test_sessionsToPb(t *testing.T) {
UserCheckedAt: past,
LoginName: "donald",
DisplayName: "donald duck",
ResourceOwner: "org1",
},
PasskeyFactor: query.SessionPasskeyFactor{
PasskeyCheckedAt: past,
@ -105,10 +108,11 @@ func Test_sessionsToPb(t *testing.T) {
Sequence: 123,
Factors: &session.Factors{
User: &session.UserFactor{
VerifiedAt: timestamppb.New(past),
Id: "345",
LoginName: "donald",
DisplayName: "donald duck",
VerifiedAt: timestamppb.New(past),
Id: "345",
LoginName: "donald",
DisplayName: "donald duck",
OrganisationId: "org1",
},
},
Metadata: map[string][]byte{"hello": []byte("world")},
@ -120,10 +124,11 @@ func Test_sessionsToPb(t *testing.T) {
Sequence: 123,
Factors: &session.Factors{
User: &session.UserFactor{
VerifiedAt: timestamppb.New(past),
Id: "345",
LoginName: "donald",
DisplayName: "donald duck",
VerifiedAt: timestamppb.New(past),
Id: "345",
LoginName: "donald",
DisplayName: "donald duck",
OrganisationId: "org1",
},
Password: &session.PasswordFactor{
VerifiedAt: timestamppb.New(past),
@ -138,10 +143,11 @@ func Test_sessionsToPb(t *testing.T) {
Sequence: 123,
Factors: &session.Factors{
User: &session.UserFactor{
VerifiedAt: timestamppb.New(past),
Id: "345",
LoginName: "donald",
DisplayName: "donald duck",
VerifiedAt: timestamppb.New(past),
Id: "345",
LoginName: "donald",
DisplayName: "donald duck",
OrganisationId: "org1",
},
Passkey: &session.PasskeyFactor{
VerifiedAt: timestamppb.New(past),

View File

@ -5,7 +5,6 @@ package oidc_test
import (
"context"
"net/url"
"os"
"testing"
"time"
@ -17,63 +16,14 @@ import (
oidc_api "github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/integration"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
var (
CTX context.Context
CTXLOGIN context.Context
Tester *integration.Tester
User *user.AddHumanUserResponse
armPasskey = []string{oidc_api.UserPresence, oidc_api.MFA}
armPassword = []string{oidc_api.PWD}
)
const (
redirectURI = "oidcIntegrationTest://callback"
redirectURIImplicit = "http://localhost:9999/callback"
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
User = Tester.CreateHumanUser(CTX)
Tester.RegisterUserPasskey(CTX, User.GetUserId())
CTXLOGIN, _ = Tester.WithAuthorization(ctx, integration.Login), errCtx
return m.Run()
}())
}
func createClient(t testing.TB) string {
app, err := Tester.CreateOIDCNativeClient(CTX, redirectURI)
require.NoError(t, err)
return app.GetClientId()
}
func createImplicitClient(t testing.TB) string {
app, err := Tester.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit)
require.NoError(t, err)
return app.GetClientId()
}
func createAuthRequest(t testing.TB, clientID, redirectURI string, scope ...string) string {
redURL, err := Tester.CreateOIDCAuthRequest(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...)
require.NoError(t, err)
return redURL
}
func createAuthRequestImplicit(t testing.TB, clientID, redirectURI string, scope ...string) string {
redURL, err := Tester.CreateOIDCAuthRequestImplicit(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...)
require.NoError(t, err)
return redURL
}
func TestOPStorage_CreateAuthRequest(t *testing.T) {
clientID := createClient(t)
@ -101,7 +51,7 @@ func TestOPStorage_CreateAccessToken_code(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code)
require.NoError(t, err)
assertTokens(t, tokens, false)
assertTokenClaims(t, tokens.IDTokenClaims, startTime, changeTime)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
// callback on a succeeded request must fail
linkResp, err = Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
@ -155,7 +105,7 @@ func TestOPStorage_CreateAccessToken_implicit(t *testing.T) {
require.NoError(t, err)
claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), accessToken, idToken, provider.IDTokenVerifier())
require.NoError(t, err)
assertTokenClaims(t, claims, startTime, changeTime)
assertIDTokenClaims(t, claims, armPasskey, startTime, changeTime)
// callback on a succeeded request must fail
linkResp, err = Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
@ -190,7 +140,7 @@ func TestOPStorage_CreateAccessAndRefreshTokens_code(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code)
require.NoError(t, err)
assertTokens(t, tokens, true)
assertTokenClaims(t, tokens.IDTokenClaims, startTime, changeTime)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
}
func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) {
@ -215,7 +165,7 @@ func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code)
require.NoError(t, err)
assertTokens(t, tokens, true)
assertTokenClaims(t, tokens.IDTokenClaims, startTime, changeTime)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
// test actual refresh grant
newTokens, err := refreshTokens(t, clientID, tokens.RefreshToken)
@ -227,7 +177,7 @@ func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) {
claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), newTokens.AccessToken, idToken, provider.IDTokenVerifier())
require.NoError(t, err)
// auth time must still be the initial
assertTokenClaims(t, claims, startTime, changeTime)
assertIDTokenClaims(t, claims, armPasskey, startTime, changeTime)
// refresh with an old refresh_token must fail
_, err = rp.RefreshAccessToken(provider, tokens.RefreshToken, "", "")
@ -268,8 +218,8 @@ func assertTokens(t *testing.T, tokens *oidc.Tokens[*oidc.IDTokenClaims], requir
}
}
func assertTokenClaims(t *testing.T, claims *oidc.IDTokenClaims, sessionStart, sessionChange time.Time) {
func assertIDTokenClaims(t *testing.T, claims *oidc.IDTokenClaims, arm []string, sessionStart, sessionChange time.Time) {
assert.Equal(t, User.GetUserId(), claims.Subject)
assert.Equal(t, []string{oidc_api.UserPresence, oidc_api.MFA}, claims.AuthenticationMethodsReferences)
assert.WithinRange(t, claims.AuthTime.AsTime().UTC(), sessionStart.Add(-1*time.Second), sessionChange.Add(1*time.Second))
assert.Equal(t, arm, claims.AuthenticationMethodsReferences)
assertOIDCTimeRange(t, claims.AuthTime, sessionStart, sessionChange)
}

View File

@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/dop251/goja"
"github.com/zitadel/logging"
@ -17,6 +18,7 @@ import (
"github.com/zitadel/zitadel/internal/actions/object"
"github.com/zitadel/zitadel/internal/api/authz"
api_http "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
@ -119,18 +121,26 @@ func (o *OPStorage) AuthorizeClientIDSecret(ctx context.Context, id string, secr
func (o *OPStorage) SetUserinfoFromToken(ctx context.Context, userInfo *oidc.UserInfo, tokenID, subject, origin string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if strings.HasPrefix(tokenID, command.IDPrefixV2) {
token, err := o.query.ActiveAccessTokenByToken(ctx, tokenID)
if err != nil {
return err
}
if err = o.isOriginAllowed(ctx, token.ClientID, origin); err != nil {
return err
}
return o.setUserinfo(ctx, userInfo, token.UserID, token.ClientID, token.Scope, nil)
}
token, err := o.repo.TokenByIDs(ctx, subject, tokenID)
if err != nil {
return errors.ThrowPermissionDenied(nil, "OIDC-Dsfb2", "token is not valid or has expired")
}
if token.ApplicationID != "" {
app, err := o.query.AppByOIDCClientID(ctx, token.ApplicationID, false)
if err != nil {
if err = o.isOriginAllowed(ctx, token.ApplicationID, origin); err != nil {
return err
}
if origin != "" && !api_http.IsOriginAllowed(app.OIDCConfig.AllowedOrigins, origin) {
return errors.ThrowPermissionDenied(nil, "OIDC-da1f3", "origin is not allowed")
}
}
return o.setUserinfo(ctx, userInfo, token.UserID, token.ApplicationID, token.Scopes, nil)
}
@ -154,6 +164,24 @@ func (o *OPStorage) SetUserinfoFromScopes(ctx context.Context, userInfo *oidc.Us
}
func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if strings.HasPrefix(tokenID, command.IDPrefixV2) {
token, err := o.query.ActiveAccessTokenByToken(ctx, tokenID)
if err != nil {
return err
}
projectID, err := o.query.ProjectIDFromClientID(ctx, clientID, false)
if err != nil {
return errors.ThrowPermissionDenied(nil, "OIDC-Adfg5", "client not found")
}
return o.introspect(ctx, introspection,
tokenID, token.UserID, token.ClientID, projectID,
token.Audience, token.Scope,
token.AccessTokenCreation, token.AccessTokenExpiration)
}
token, err := o.repo.TokenByIDs(ctx, subject, tokenID)
if err != nil {
return errors.ThrowPermissionDenied(nil, "OIDC-Dsfb2", "token is not valid or has expired")
@ -168,27 +196,10 @@ func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection
return errors.ThrowPreconditionFailed(err, "OIDC-AGefw", "Errors.Internal")
}
}
for _, aud := range token.Audience {
if aud == clientID || aud == projectID {
userInfo := new(oidc.UserInfo)
err := o.setUserinfo(ctx, userInfo, subject, clientID, token.Scopes, []string{projectID}) // always
if err != nil {
return err
}
introspection.SetUserInfo(userInfo)
introspection.Scope = token.Scopes
introspection.ClientID = token.ApplicationID
introspection.TokenType = oidc.BearerToken
introspection.Expiration = oidc.FromTime(token.Expiration)
introspection.IssuedAt = oidc.FromTime(token.CreationDate)
introspection.NotBefore = oidc.FromTime(token.CreationDate)
introspection.Audience = token.Audience
introspection.Issuer = op.IssuerFromContext(ctx)
introspection.JWTID = token.ID
return nil
}
}
return errors.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
return o.introspect(ctx, introspection,
token.ID, token.UserID, token.ApplicationID, projectID,
token.Audience, token.Scopes,
token.CreationDate, token.Expiration)
}
func (o *OPStorage) ClientCredentialsTokenRequest(ctx context.Context, clientID string, scope []string) (op.TokenRequest, error) {
@ -230,6 +241,55 @@ func (o *OPStorage) ClientCredentials(ctx context.Context, clientID, clientSecre
}, nil
}
// isOriginAllowed checks whether a call by the client to the endpoint is allowed from the provided origin
// if no origin is provided, no error will be returned
func (o *OPStorage) isOriginAllowed(ctx context.Context, clientID, origin string) error {
if origin == "" {
return nil
}
app, err := o.query.AppByOIDCClientID(ctx, clientID, false)
if err != nil {
return err
}
if api_http.IsOriginAllowed(app.OIDCConfig.AllowedOrigins, origin) {
return nil
}
return errors.ThrowPermissionDenied(nil, "OIDC-da1f3", "origin is not allowed")
}
func (o *OPStorage) introspect(
ctx context.Context,
introspection *oidc.IntrospectionResponse,
tokenID, subject, clientID, projectID string,
audience, scope []string,
tokenCreation, tokenExpiration time.Time,
) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
for _, aud := range audience {
if aud == clientID || aud == projectID {
userInfo := new(oidc.UserInfo)
err = o.setUserinfo(ctx, userInfo, subject, clientID, scope, []string{projectID}) // always
if err != nil {
return err
}
introspection.SetUserInfo(userInfo)
introspection.Scope = scope
introspection.ClientID = clientID
introspection.TokenType = oidc.BearerToken
introspection.Expiration = oidc.FromTime(tokenExpiration)
introspection.IssuedAt = oidc.FromTime(tokenCreation)
introspection.NotBefore = oidc.FromTime(tokenCreation)
introspection.Audience = audience
introspection.Issuer = op.IssuerFromContext(ctx)
introspection.JWTID = tokenID
return nil
}
}
return errors.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
}
func (o *OPStorage) checkOrgScopes(ctx context.Context, user *query.User, scopes []string) ([]string, error) {
for i := len(scopes) - 1; i >= 0; i-- {
scope := scopes[i]

View File

@ -0,0 +1,135 @@
//go:build integration
package oidc_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v2/pkg/client/rp"
"github.com/zitadel/oidc/v2/pkg/client/rs"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/zitadel/pkg/grpc/authn"
"github.com/zitadel/zitadel/pkg/grpc/management"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
)
func TestOPStorage_SetUserinfoFromToken(t *testing.T) {
clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionID,
SessionToken: sessionToken,
},
},
})
require.NoError(t, err)
// code exchange
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
tokens, err := exchangeTokens(t, clientID, code)
require.NoError(t, err)
assertTokens(t, tokens, true)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
// test actual userinfo
provider, err := Tester.CreateRelyingParty(clientID, redirectURI)
require.NoError(t, err)
userinfo, err := rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
require.NoError(t, err)
assertUserinfo(t, userinfo)
}
func TestOPStorage_SetIntrospectionFromToken(t *testing.T) {
project, err := Tester.CreateProject(CTX)
require.NoError(t, err)
app, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, project.GetId())
require.NoError(t, err)
api, err := Tester.CreateAPIClient(CTX, project.GetId())
require.NoError(t, err)
keyResp, err := Tester.Client.Mgmt.AddAppKey(CTX, &management.AddAppKeyRequest{
ProjectId: project.GetId(),
AppId: api.GetAppId(),
Type: authn.KeyType_KEY_TYPE_JSON,
ExpirationDate: nil,
})
require.NoError(t, err)
resourceServer, err := Tester.CreateResourceServer(keyResp.GetKeyDetails())
require.NoError(t, err)
scope := []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess}
authRequestID := createAuthRequest(t, app.GetClientId(), redirectURI, scope...)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionID,
SessionToken: sessionToken,
},
},
})
require.NoError(t, err)
// code exchange
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
tokens, err := exchangeTokens(t, app.GetClientId(), code)
require.NoError(t, err)
assertTokens(t, tokens, true)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
// test actual introspection
introspection, err := rs.Introspect(context.Background(), resourceServer, tokens.AccessToken)
require.NoError(t, err)
assertIntrospection(t, introspection,
Tester.OIDCIssuer(), app.GetClientId(),
scope, []string{app.GetClientId(), api.GetClientId(), project.GetId()},
tokens.Expiry, tokens.Expiry.Add(-12*time.Hour))
}
func assertUserinfo(t *testing.T, userinfo *oidc.UserInfo) {
assert.Equal(t, User.GetUserId(), userinfo.Subject)
assert.Equal(t, "Mickey", userinfo.GivenName)
assert.Equal(t, "Mouse", userinfo.FamilyName)
assert.Equal(t, "Mickey Mouse", userinfo.Name)
assert.NotEmpty(t, userinfo.PreferredUsername)
assert.Equal(t, userinfo.PreferredUsername, userinfo.Email)
assert.False(t, bool(userinfo.EmailVerified))
assertOIDCTime(t, userinfo.UpdatedAt, User.GetDetails().GetChangeDate().AsTime())
}
func assertIntrospection(
t *testing.T,
introspection *oidc.IntrospectionResponse,
issuer, clientID string,
scope, audience []string,
expiration, creation time.Time,
) {
assert.True(t, introspection.Active)
assert.Equal(t, scope, []string(introspection.Scope))
assert.Equal(t, clientID, introspection.ClientID)
assert.Equal(t, oidc.BearerToken, introspection.TokenType)
assertOIDCTime(t, introspection.Expiration, expiration)
assertOIDCTime(t, introspection.IssuedAt, creation)
assertOIDCTime(t, introspection.NotBefore, creation)
assert.Equal(t, User.GetUserId(), introspection.Subject)
assert.ElementsMatch(t, audience, introspection.Audience)
assert.Equal(t, issuer, introspection.Issuer)
assert.NotEmpty(t, introspection.JWTID)
assert.NotEmpty(t, introspection.Username)
assert.Equal(t, introspection.Username, introspection.PreferredUsername)
assert.Equal(t, "Mickey", introspection.GivenName)
assert.Equal(t, "Mouse", introspection.FamilyName)
assert.Equal(t, "Mickey Mouse", introspection.Name)
assert.Equal(t, introspection.Username, introspection.Email)
assert.False(t, bool(introspection.EmailVerified))
assertOIDCTime(t, introspection.UpdatedAt, User.GetDetails().GetChangeDate().AsTime())
}

View File

@ -0,0 +1,248 @@
//go:build integration
package oidc_test
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v2/pkg/oidc"
"google.golang.org/grpc/metadata"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/auth"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
var (
CTX context.Context
CTXLOGIN context.Context
Tester *integration.Tester
User *user.AddHumanUserResponse
)
const (
redirectURI = "oidcIntegrationTest://callback"
redirectURIImplicit = "http://localhost:9999/callback"
zitadelAudienceScope = domain.ProjectIDScope + domain.ProjectIDScopeZITADEL + domain.AudSuffix
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
User = Tester.CreateHumanUser(CTX)
Tester.SetUserPassword(CTX, User.GetUserId(), integration.UserPassword)
Tester.RegisterUserPasskey(CTX, User.GetUserId())
CTXLOGIN, _ = Tester.WithAuthorization(ctx, integration.Login), errCtx
return m.Run()
}())
}
func Test_ZITADEL_API_missing_audience_scope(t *testing.T) {
clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionID,
SessionToken: sessionToken,
},
},
})
require.NoError(t, err)
// code exchange
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
tokens, err := exchangeTokens(t, clientID, code)
require.NoError(t, err)
assertTokens(t, tokens, false)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
require.Error(t, err)
require.Nil(t, myUserResp)
}
func Test_ZITADEL_API_missing_authentication(t *testing.T) {
clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, zitadelAudienceScope)
createResp, err := Tester.Client.SessionV2.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{UserId: User.GetUserId()},
},
},
Domain: Tester.Config.ExternalDomain,
})
require.NoError(t, err)
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: createResp.GetSessionId(),
SessionToken: createResp.GetSessionToken(),
},
},
})
require.NoError(t, err)
// code exchange
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
tokens, err := exchangeTokens(t, clientID, code)
require.NoError(t, err)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
require.Error(t, err)
require.Nil(t, myUserResp)
}
func Test_ZITADEL_API_missing_mfa(t *testing.T) {
clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, zitadelAudienceScope)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasswordSession(t, CTX, User.GetUserId(), integration.UserPassword)
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionID,
SessionToken: sessionToken,
},
},
})
require.NoError(t, err)
// code exchange
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
tokens, err := exchangeTokens(t, clientID, code)
require.NoError(t, err)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPassword, startTime, changeTime)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
require.Error(t, err)
require.Nil(t, myUserResp)
}
func Test_ZITADEL_API_success(t *testing.T) {
clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, zitadelAudienceScope)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionID,
SessionToken: sessionToken,
},
},
})
require.NoError(t, err)
// code exchange
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
tokens, err := exchangeTokens(t, clientID, code)
require.NoError(t, err)
assertTokens(t, tokens, false)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
require.NoError(t, err)
require.Equal(t, User.GetUserId(), myUserResp.GetUser().GetId())
}
func Test_ZITADEL_API_inactive_access_token(t *testing.T) {
clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess, zitadelAudienceScope)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionID,
SessionToken: sessionToken,
},
},
})
require.NoError(t, err)
// code exchange
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
tokens, err := exchangeTokens(t, clientID, code)
require.NoError(t, err)
assertTokens(t, tokens, true)
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
// make sure token works
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
require.NoError(t, err)
require.Equal(t, User.GetUserId(), myUserResp.GetUser().GetId())
// refresh token
newTokens, err := refreshTokens(t, clientID, tokens.RefreshToken)
require.NoError(t, err)
assert.NotEmpty(t, newTokens.AccessToken)
// use invalidated token
ctx = metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
myUserResp, err = Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
require.Error(t, err)
require.Nil(t, myUserResp)
}
func createClient(t testing.TB) string {
project, err := Tester.CreateProject(CTX)
require.NoError(t, err)
app, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, project.GetId())
require.NoError(t, err)
return app.GetClientId()
}
func createImplicitClient(t testing.TB) string {
app, err := Tester.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit)
require.NoError(t, err)
return app.GetClientId()
}
func createAuthRequest(t testing.TB, clientID, redirectURI string, scope ...string) string {
redURL, err := Tester.CreateOIDCAuthRequest(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...)
require.NoError(t, err)
return redURL
}
func createAuthRequestImplicit(t testing.TB, clientID, redirectURI string, scope ...string) string {
redURL, err := Tester.CreateOIDCAuthRequestImplicit(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...)
require.NoError(t, err)
return redURL
}
func assertOIDCTime(t *testing.T, actual oidc.Time, expected time.Time) {
assertOIDCTimeRange(t, actual, expected, expected)
}
func assertOIDCTimeRange(t *testing.T, actual oidc.Time, expectedStart, expectedEnd time.Time) {
assert.WithinRange(t, actual.AsTime(), expectedStart.Add(-1*time.Second), expectedEnd.Add(1*time.Second))
}

View File

@ -15,7 +15,9 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/authz/repository/eventsourcing/view"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
@ -28,7 +30,6 @@ import (
type TokenVerifierRepo struct {
TokenVerificationKey crypto.EncryptionAlgorithm
IAMID string
Eventstore v1.Eventstore
View *view.View
Query *query.Queries
@ -92,6 +93,21 @@ func (repo *TokenVerifierRepo) VerifyAccessToken(ctx context.Context, tokenStrin
if !ok {
return "", "", "", "", "", caos_errs.ThrowUnauthenticated(nil, "APP-Reb32", "invalid token")
}
if strings.HasPrefix(tokenID, command.IDPrefixV2) {
userID, clientID, resourceOwner, err = repo.verifyAccessTokenV2(ctx, tokenID, verifierClientID, projectID)
return
}
if sessionID, ok := strings.CutPrefix(tokenID, authz.SessionTokenPrefix); ok {
userID, clientID, resourceOwner, err = repo.verifySessionToken(ctx, sessionID, tokenString)
return
}
return repo.verifyAccessTokenV1(ctx, tokenID, subject, verifierClientID, projectID)
}
func (repo *TokenVerifierRepo) verifyAccessTokenV1(ctx context.Context, tokenID, subject, verifierClientID, projectID string) (userID string, agentID string, clientID, prefLang, resourceOwner string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
_, tokenSpan := tracing.NewNamedSpan(ctx, "token")
token, err := repo.tokenByID(ctx, tokenID, subject)
tokenSpan.EndWithError(err)
@ -104,12 +120,89 @@ func (repo *TokenVerifierRepo) VerifyAccessToken(ctx context.Context, tokenStrin
if token.IsPAT {
return token.UserID, "", "", "", token.ResourceOwner, nil
}
for _, aud := range token.Audience {
if verifierClientID == aud || projectID == aud {
return token.UserID, token.UserAgentID, token.ApplicationID, token.PreferredLanguage, token.ResourceOwner, nil
}
if err = verifyAudience(token.Audience, verifierClientID, projectID); err != nil {
return "", "", "", "", "", err
}
return "", "", "", "", "", caos_errs.ThrowUnauthenticated(nil, "APP-Zxfako", "invalid audience")
return token.UserID, token.UserAgentID, token.ApplicationID, token.PreferredLanguage, token.ResourceOwner, nil
}
func (repo *TokenVerifierRepo) verifyAccessTokenV2(ctx context.Context, token, verifierClientID, projectID string) (userID, clientID, resourceOwner string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
activeToken, err := repo.Query.ActiveAccessTokenByToken(ctx, token)
if err != nil {
return "", "", "", err
}
if err = verifyAudience(activeToken.Audience, verifierClientID, projectID); err != nil {
return "", "", "", err
}
if err = repo.checkAuthentication(ctx, activeToken.AuthMethods, activeToken.UserID); err != nil {
return "", "", "", err
}
return activeToken.UserID, activeToken.ClientID, activeToken.ResourceOwner, nil
}
func (repo *TokenVerifierRepo) verifySessionToken(ctx context.Context, sessionID, token string) (userID, clientID, resourceOwner string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
session, err := repo.Query.SessionByID(ctx, false, sessionID, token)
if err != nil {
return "", "", "", err
}
if err = repo.checkAuthentication(ctx, authMethodsFromSession(session), session.UserFactor.UserID); err != nil {
return "", "", "", err
}
return session.UserFactor.UserID, "", session.UserFactor.ResourceOwner, nil
}
// checkAuthentication ensures the session or token was authenticated (at least a single [domain.UserAuthMethodType]).
// It will also check if there was a multi factor authentication, if either MFA is forced by the login policy or if the user has set up any
func (repo *TokenVerifierRepo) checkAuthentication(ctx context.Context, authMethods []domain.UserAuthMethodType, userID string) error {
if len(authMethods) == 0 {
return caos_errs.ThrowPermissionDenied(nil, "AUTHZ-Kl3p0", "authentication required")
}
if domain.HasMFA(authMethods) {
return nil
}
availableAuthMethods, forceMFA, err := repo.Query.ListUserAuthMethodTypesRequired(setCallerCtx(ctx, userID), userID, false)
if err != nil {
return err
}
if forceMFA || domain.HasMFA(availableAuthMethods) {
return caos_errs.ThrowPermissionDenied(nil, "AUTHZ-Kl3p0", "mfa required")
}
return nil
}
func authMethodsFromSession(session *query.Session) []domain.UserAuthMethodType {
types := make([]domain.UserAuthMethodType, 0, domain.UserAuthMethodTypeIDP)
if !session.PasswordFactor.PasswordCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypePassword)
}
if !session.PasskeyFactor.PasskeyCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypePasswordless)
}
if !session.IntentFactor.IntentCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeIDP)
}
// TODO: add checks with https://github.com/zitadel/zitadel/issues/5477
/*
if !session.TOTPFactor.TOTPCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeOTP)
}
if !session.U2FFactor.U2FCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeU2F)
}
*/
return types
}
func setCallerCtx(ctx context.Context, userID string) context.Context {
ctxData := authz.GetCtxData(ctx)
ctxData.UserID = userID
return authz.SetCtxData(ctx, ctxData)
}
func (repo *TokenVerifierRepo) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error) {
@ -184,6 +277,15 @@ func (repo *TokenVerifierRepo) decryptAccessToken(token string) (string, error)
return tokenIDSubject, nil
}
func verifyAudience(audience []string, verifierClientID, projectID string) error {
for _, aud := range audience {
if verifierClientID == aud || projectID == aud {
return nil
}
}
return caos_errs.ThrowUnauthenticated(nil, "APP-Zxfako", "invalid audience")
}
type openIDKeySet struct {
*query.Queries
}

View File

@ -14,6 +14,7 @@ import (
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/repository/authrequest"
"github.com/zitadel/zitadel/internal/repository/oidcsession"
"github.com/zitadel/zitadel/internal/repository/user"
)
// AddOIDCSessionAccessToken creates a new OIDC Session, creates an access token and returns its id and expiration.
@ -101,6 +102,10 @@ func (c *Commands) newOIDCSessionAddEvents(ctx context.Context, authRequestID st
if sessionWriteModel.State != domain.SessionStateActive {
return nil, caos_errs.ThrowPreconditionFailed(nil, "OIDCS-sjkl3", "Errors.Session.Terminated")
}
resourceOwner, err := c.getResourceOwnerOfSessionUser(ctx, sessionWriteModel.UserID, sessionWriteModel.InstanceID)
if err != nil {
return nil, err
}
accessTokenLifetime, refreshTokenLifeTime, refreshTokenIdleLifetime, err := c.tokenTokenLifetimes(ctx)
if err != nil {
return nil, err
@ -114,7 +119,7 @@ func (c *Commands) newOIDCSessionAddEvents(ctx context.Context, authRequestID st
eventstore: c.eventstore,
idGenerator: c.idGenerator,
encryptionAlg: c.keyAlgorithm,
oidcSessionWriteModel: NewOIDCSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID()),
oidcSessionWriteModel: NewOIDCSessionWriteModel(sessionID, resourceOwner),
sessionWriteModel: sessionWriteModel,
authRequestWriteModel: authRequestWriteModel,
accessTokenLifetime: accessTokenLifetime,
@ -123,6 +128,22 @@ func (c *Commands) newOIDCSessionAddEvents(ctx context.Context, authRequestID st
}, nil
}
func (c *Commands) getResourceOwnerOfSessionUser(ctx context.Context, userID, instanceID string) (string, error) {
events, err := c.eventstore.Filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
InstanceID(instanceID).
AllowTimeTravel().
OrderAsc().
Limit(1).
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(userID).
Builder())
if err != nil || len(events) != 1 {
return "", caos_errs.ThrowInternal(err, "OIDCS-sferh", "Errors.Internal")
}
return events[0].Aggregate().ResourceOwner, nil
}
func (c *Commands) decryptRefreshToken(refreshToken string) (refreshTokenID string, err error) {
decoded, err := base64.RawURLEncoding.DecodeString(refreshToken)
if err != nil {
@ -144,7 +165,7 @@ func (c *Commands) newOIDCSessionUpdateEvents(ctx context.Context, oidcSessionID
if err != nil {
return nil, err
}
sessionWriteModel := NewOIDCSessionWriteModel(oidcSessionID, authz.GetInstance(ctx).InstanceID())
sessionWriteModel := NewOIDCSessionWriteModel(oidcSessionID, "")
if err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel); err != nil {
return nil, err
}

View File

@ -20,6 +20,7 @@ type OIDCSessionWriteModel struct {
AuthMethods []domain.UserAuthMethodType
AuthTime time.Time
State domain.OIDCSessionState
AccessTokenCreation time.Time
AccessTokenExpiration time.Time
RefreshTokenID string
RefreshTokenExpiration time.Time
@ -82,6 +83,11 @@ func (wm *OIDCSessionWriteModel) reduceAdded(e *oidcsession.AddedEvent) {
wm.AuthMethods = e.AuthMethods
wm.AuthTime = e.AuthTime
wm.State = domain.OIDCSessionStateActive
// the write model might be initialized without resource owner,
// so update the aggregate
if wm.ResourceOwner == "" {
wm.aggregate = &oidcsession.NewAggregate(wm.AggregateID, e.Aggregate().ResourceOwner).Aggregate
}
}
func (wm *OIDCSessionWriteModel) reduceAccessTokenAdded(e *oidcsession.AccessTokenAddedEvent) {

View File

@ -9,6 +9,7 @@ import (
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
@ -21,6 +22,7 @@ import (
"github.com/zitadel/zitadel/internal/repository/authrequest"
"github.com/zitadel/zitadel/internal/repository/oidcsession"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/repository/user"
)
var (
@ -108,7 +110,7 @@ func TestCommands_AddOIDCSessionAccessToken(t *testing.T) {
authrequest.NewCodeExchangedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
),
),
expectFilter(),
expectFilter(), // inactive session
),
},
args{
@ -173,15 +175,22 @@ func TestCommands_AddOIDCSessionAccessToken(t *testing.T) {
testNow),
),
),
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
"username", "firstName", "lastName", "", "", language.English, domain.GenderUnspecified, "", false,
),
),
),
expectFilter(), // token lifetime
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID("instanceID",
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
),
eventFromEventPusherWithInstanceID("instanceID",
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"accessTokenID", []string{"openid"}, time.Hour),
),
eventFromEventPusherWithInstanceID("instanceID",
@ -302,7 +311,7 @@ func TestCommands_AddOIDCSessionRefreshAndAccessToken(t *testing.T) {
authrequest.NewCodeExchangedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
),
),
expectFilter(),
expectFilter(), // inactive session
),
},
args{
@ -367,19 +376,26 @@ func TestCommands_AddOIDCSessionRefreshAndAccessToken(t *testing.T) {
testNow),
),
),
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
"username", "firstName", "lastName", "", "", language.English, domain.GenderUnspecified, "", false,
),
),
),
expectFilter(), // token lifetime
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID("instanceID",
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
),
eventFromEventPusherWithInstanceID("instanceID",
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"accessTokenID", []string{"openid", "offline_access"}, time.Hour),
),
eventFromEventPusherWithInstanceID("instanceID",
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"refreshTokenID", 7*24*time.Hour, 24*time.Hour),
),
eventFromEventPusherWithInstanceID("instanceID",
@ -489,11 +505,11 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
),
eventFromEventPusher(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
),
),
@ -515,15 +531,15 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
),
eventFromEventPusher(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
),
eventFromEventPusher(
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"refreshTokenID", 7*24*time.Hour, 24*time.Hour),
),
),
@ -545,15 +561,15 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
),
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
),
eventFromEventPusherWithCreationDateNow(
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"refreshTokenID", 7*24*time.Hour, 24*time.Hour),
),
),
@ -561,11 +577,11 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID("instanceID",
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"accessTokenID", []string{"openid", "offline_access"}, time.Hour),
),
eventFromEventPusherWithInstanceID("instanceID",
oidcsession.NewRefreshTokenRenewedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewRefreshTokenRenewedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"refreshTokenID2", 24*time.Hour),
),
},
@ -668,11 +684,11 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) {
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
),
eventFromEventPusher(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
),
),
@ -693,15 +709,15 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) {
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
),
eventFromEventPusher(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
),
eventFromEventPusher(
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"refreshTokenID", 7*24*time.Hour, 24*time.Hour),
),
),
@ -722,15 +738,15 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) {
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
),
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
),
eventFromEventPusherWithCreationDateNow(
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"refreshTokenID", 7*24*time.Hour, 24*time.Hour),
),
),

View File

@ -60,6 +60,31 @@ func (f UserAuthMethodType) Valid() bool {
return f >= 0 && f < userAuthMethodTypeCount
}
// HasMFA checks whether the user authenticated with multiple auth factors.
// This can either be true if the list contains a [UserAuthMethodType] which by itself is MFA (e.g. [UserAuthMethodTypePasswordless])
// or if multiple factors were used (e.g. [UserAuthMethodTypePassword] and [UserAuthMethodTypeU2F])
func HasMFA(methods []UserAuthMethodType) bool {
var factors int
for _, method := range methods {
switch method {
case UserAuthMethodTypePassword:
factors++
case UserAuthMethodTypePasswordless:
return true
case UserAuthMethodTypeU2F:
factors++
case UserAuthMethodTypeOTP:
factors++
case UserAuthMethodTypeIDP:
factors++
case UserAuthMethodTypeUnspecified,
userAuthMethodTypeCount:
// ignore
}
}
return factors > 1
}
type PersonalAccessTokenState int32
const (

View File

@ -17,6 +17,7 @@ import (
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/auth"
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
@ -29,6 +30,7 @@ type Client struct {
CC *grpc.ClientConn
Admin admin.AdminServiceClient
Mgmt mgmt.ManagementServiceClient
Auth auth.AuthServiceClient
UserV2 user.UserServiceClient
SessionV2 session.SessionServiceClient
OIDCv2 oidc_pb.OIDCServiceClient
@ -40,6 +42,7 @@ func newClient(cc *grpc.ClientConn) Client {
CC: cc,
Admin: admin.NewAdminServiceClient(cc),
Mgmt: mgmt.NewManagementServiceClient(cc),
Auth: auth.NewAuthServiceClient(cc),
UserV2: user.NewUserServiceClient(cc),
SessionV2: session.NewSessionServiceClient(cc),
OIDCv2: oidc_pb.NewOIDCServiceClient(cc),
@ -134,6 +137,14 @@ func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) {
logging.OnError(err).Fatal("create user passkey")
}
func (s *Tester) SetUserPassword(ctx context.Context, userID, password string) {
_, err := s.Client.UserV2.SetPassword(ctx, &user.SetPasswordRequest{
UserId: userID,
NewPassword: &user.Password{Password: password},
})
logging.OnError(err).Fatal("set user password")
}
func (s *Tester) AddGenericOAuthProvider(t *testing.T) string {
ctx := authz.WithInstance(context.Background(), s.Instance)
id, _, err := s.Commands.AddOrgGenericOAuthProvider(ctx, s.Organisation.ID, command.GenericOAuthProvider{
@ -219,3 +230,20 @@ func (s *Tester) CreatePasskeySession(t *testing.T, ctx context.Context, userID
return createResp.GetSessionId(), updateResp.GetSessionToken(),
createResp.GetDetails().GetChangeDate().AsTime(), updateResp.GetDetails().GetChangeDate().AsTime()
}
func (s *Tester) CreatePasswordSession(t *testing.T, ctx context.Context, userID, password string) (id, token string, start, change time.Time) {
createResp, err := s.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{UserId: userID},
},
Password: &session.CheckPassword{
Password: password,
},
},
Domain: s.Config.ExternalDomain,
})
require.NoError(t, err)
return createResp.GetSessionId(), createResp.GetSessionToken(),
createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime()
}

View File

@ -64,6 +64,7 @@ const (
const (
FirstInstanceUsersKey = "first"
UserPassword = "VeryS3cret!"
)
// User information with a Personal Access Token.

View File

@ -8,25 +8,20 @@ import (
"strings"
"time"
"github.com/zitadel/oidc/v2/pkg/client"
"github.com/zitadel/oidc/v2/pkg/client/rp"
"github.com/zitadel/oidc/v2/pkg/client/rs"
"github.com/zitadel/oidc/v2/pkg/oidc"
http_util "github.com/zitadel/zitadel/internal/api/http"
oidc_internal "github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/pkg/grpc/app"
"github.com/zitadel/zitadel/pkg/grpc/management"
)
func (s *Tester) CreateOIDCNativeClient(ctx context.Context, redirectURI string) (*management.AddOIDCAppResponse, error) {
project, err := s.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
})
if err != nil {
return nil, err
}
func (s *Tester) CreateOIDCNativeClient(ctx context.Context, redirectURI, projectID string) (*management.AddOIDCAppResponse, error) {
return s.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{
ProjectId: project.GetId(),
ProjectId: projectID,
Name: fmt.Sprintf("app-%d", time.Now().UnixNano()),
RedirectUris: []string{redirectURI},
ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
@ -74,6 +69,20 @@ func (s *Tester) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI s
})
}
func (s *Tester) CreateProject(ctx context.Context) (*management.AddProjectResponse, error) {
return s.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
})
}
func (s *Tester) CreateAPIClient(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) {
return s.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{
ProjectId: projectID,
Name: fmt.Sprintf("api-%d", time.Now().UnixNano()),
AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
})
}
func (s *Tester) CreateOIDCAuthRequest(clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) {
provider, err := s.CreateRelyingParty(clientID, redirectURI, scope...)
if err != nil {
@ -123,12 +132,23 @@ func (s *Tester) CreateOIDCAuthRequestImplicit(clientID, loginClient, redirectUR
return strings.TrimPrefix(loc.String(), prefixWithHost), nil
}
func (s *Tester) OIDCIssuer() string {
return http_util.BuildHTTP(s.Config.ExternalDomain, s.Config.Port, s.Config.ExternalSecure)
}
func (s *Tester) CreateRelyingParty(clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) {
issuer := http_util.BuildHTTP(s.Config.ExternalDomain, s.Config.Port, s.Config.ExternalSecure)
if len(scope) == 0 {
scope = []string{oidc.ScopeOpenID}
}
return rp.NewRelyingPartyOIDC(issuer, clientID, "", redirectURI, scope)
return rp.NewRelyingPartyOIDC(s.OIDCIssuer(), clientID, "", redirectURI, scope)
}
func (s *Tester) CreateResourceServer(keyFileData []byte) (rs.ResourceServer, error) {
keyFile, err := client.ConfigFromKeyFileData(keyFileData)
if err != nil {
return nil, err
}
return rs.NewResourceServerJWTProfile(s.OIDCIssuer(), keyFile.ClientID, keyFile.KeyID, []byte(keyFile.Key))
}
func CheckRedirect(url string, headers map[string]string) (*url.URL, error) {
@ -153,11 +173,3 @@ func CheckRedirect(url string, headers map[string]string) (*url.URL, error) {
return resp.Location()
}
func (s *Tester) CreateSession(ctx context.Context, userID string) (string, string, error) {
session, err := s.Commands.CreateSession(ctx, []command.SessionCommand{command.CheckUser(userID)}, "domain.tld", nil)
if err != nil {
return "", "", err
}
return session.ID, session.NewToken, nil
}

View File

@ -0,0 +1,113 @@
package query
import (
"context"
"strings"
"time"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/oidcsession"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
type OIDCSessionAccessTokenReadModel struct {
eventstore.WriteModel
UserID string
SessionID string
ClientID string
Audience []string
Scope []string
AuthMethods []domain.UserAuthMethodType
AuthTime time.Time
State domain.OIDCSessionState
AccessTokenID string
AccessTokenCreation time.Time
AccessTokenExpiration time.Time
}
func newOIDCSessionAccessTokenWriteModel(id string) *OIDCSessionAccessTokenReadModel {
return &OIDCSessionAccessTokenReadModel{
WriteModel: eventstore.WriteModel{
AggregateID: id,
},
}
}
func (wm *OIDCSessionAccessTokenReadModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *oidcsession.AddedEvent:
wm.reduceAdded(e)
case *oidcsession.AccessTokenAddedEvent:
wm.reduceAccessTokenAdded(e)
}
}
return wm.WriteModel.Reduce()
}
func (wm *OIDCSessionAccessTokenReadModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AllowTimeTravel().
AddQuery().
AggregateTypes(oidcsession.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
oidcsession.AddedType,
oidcsession.AccessTokenAddedType,
).
Builder()
}
func (wm *OIDCSessionAccessTokenReadModel) reduceAdded(e *oidcsession.AddedEvent) {
wm.UserID = e.UserID
wm.SessionID = e.SessionID
wm.ClientID = e.ClientID
wm.Audience = e.Audience
wm.Scope = e.Scope
wm.AuthMethods = e.AuthMethods
wm.AuthTime = e.AuthTime
wm.State = domain.OIDCSessionStateActive
}
func (wm *OIDCSessionAccessTokenReadModel) reduceAccessTokenAdded(e *oidcsession.AccessTokenAddedEvent) {
wm.AccessTokenID = e.ID
wm.AccessTokenCreation = e.CreationDate()
wm.AccessTokenExpiration = e.CreationDate().Add(e.Lifetime)
}
// ActiveAccessTokenByToken will check if the token is active by retrieving the OIDCSession events from the eventstore.
// refreshed or expired tokens will return an error
func (q *Queries) ActiveAccessTokenByToken(ctx context.Context, token string) (model *OIDCSessionAccessTokenReadModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
split := strings.Split(token, "-")
if len(split) != 2 {
return nil, caos_errs.ThrowPermissionDenied(nil, "QUERY-SAhtk", "Errors.OIDCSession.Token.Invalid")
}
model, err = q.accessTokenByOIDCSessionAndTokenID(ctx, split[0], split[1])
if err != nil {
return nil, err
}
if !model.AccessTokenExpiration.After(time.Now()) {
return nil, caos_errs.ThrowPermissionDenied(nil, "QUERY-SAF3rf", "Errors.OIDCSession.Token.Expired")
}
return
}
func (q *Queries) accessTokenByOIDCSessionAndTokenID(ctx context.Context, oidcSessionID, tokenID string) (model *OIDCSessionAccessTokenReadModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
model = newOIDCSessionAccessTokenWriteModel(oidcSessionID)
if err = q.eventstore.FilterToQueryReducer(ctx, model); err != nil {
return nil, caos_errs.ThrowPermissionDenied(err, "QUERY-ASfe2", "Errors.OIDCSession.Token.Invalid")
}
if model.AccessTokenID != tokenID {
return nil, caos_errs.ThrowPermissionDenied(nil, "QUERY-M2u9w", "Errors.OIDCSession.Token.Invalid")
}
return model, nil
}

View File

@ -40,6 +40,7 @@ type Session struct {
type SessionUserFactor struct {
UserID string
ResourceOwner string
UserCheckedAt time.Time
LoginName string
DisplayName string
@ -225,6 +226,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
SessionColumnUserCheckedAt.identifier(),
LoginNameNameCol.identifier(),
HumanDisplayNameCol.identifier(),
UserResourceOwnerCol.identifier(),
SessionColumnPasswordCheckedAt.identifier(),
SessionColumnIntentCheckedAt.identifier(),
SessionColumnPasskeyCheckedAt.identifier(),
@ -232,7 +234,8 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
SessionColumnToken.identifier(),
).From(sessionsTable.identifier()).
LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)).
LeftJoin(join(HumanUserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))).
LeftJoin(join(HumanUserIDCol, SessionColumnUserID)).
LeftJoin(join(UserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))).
PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Session, string, error) {
session := new(Session)
@ -241,6 +244,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
userCheckedAt sql.NullTime
loginName sql.NullString
displayName sql.NullString
userResourceOwner sql.NullString
passwordCheckedAt sql.NullTime
intentCheckedAt sql.NullTime
passkeyCheckedAt sql.NullTime
@ -262,6 +266,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
&userCheckedAt,
&loginName,
&displayName,
&userResourceOwner,
&passwordCheckedAt,
&intentCheckedAt,
&passkeyCheckedAt,
@ -281,6 +286,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
session.UserFactor.UserCheckedAt = userCheckedAt.Time
session.UserFactor.LoginName = loginName.String
session.UserFactor.DisplayName = displayName.String
session.UserFactor.ResourceOwner = userResourceOwner.String
session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time
@ -304,6 +310,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
SessionColumnUserCheckedAt.identifier(),
LoginNameNameCol.identifier(),
HumanDisplayNameCol.identifier(),
UserResourceOwnerCol.identifier(),
SessionColumnPasswordCheckedAt.identifier(),
SessionColumnIntentCheckedAt.identifier(),
SessionColumnPasskeyCheckedAt.identifier(),
@ -311,7 +318,8 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
countColumn.identifier(),
).From(sessionsTable.identifier()).
LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)).
LeftJoin(join(HumanUserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))).
LeftJoin(join(HumanUserIDCol, SessionColumnUserID)).
LeftJoin(join(UserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))).
PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Sessions, error) {
sessions := &Sessions{Sessions: []*Session{}}
@ -323,6 +331,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
userCheckedAt sql.NullTime
loginName sql.NullString
displayName sql.NullString
userResourceOwner sql.NullString
passwordCheckedAt sql.NullTime
intentCheckedAt sql.NullTime
passkeyCheckedAt sql.NullTime
@ -343,6 +352,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
&userCheckedAt,
&loginName,
&displayName,
&userResourceOwner,
&passwordCheckedAt,
&intentCheckedAt,
&passkeyCheckedAt,
@ -358,6 +368,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
session.UserFactor.UserCheckedAt = userCheckedAt.Time
session.UserFactor.LoginName = loginName.String
session.UserFactor.DisplayName = displayName.String
session.UserFactor.ResourceOwner = userResourceOwner.String
session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time

View File

@ -29,6 +29,7 @@ var (
` projections.sessions3.user_checked_at,` +
` projections.login_names2.login_name,` +
` projections.users8_humans.display_name,` +
` projections.users8.resource_owner,` +
` projections.sessions3.password_checked_at,` +
` projections.sessions3.intent_checked_at,` +
` projections.sessions3.passkey_checked_at,` +
@ -37,6 +38,7 @@ var (
` FROM projections.sessions3` +
` LEFT JOIN projections.login_names2 ON projections.sessions3.user_id = projections.login_names2.user_id AND projections.sessions3.instance_id = projections.login_names2.instance_id` +
` LEFT JOIN projections.users8_humans ON projections.sessions3.user_id = projections.users8_humans.user_id AND projections.sessions3.instance_id = projections.users8_humans.instance_id` +
` LEFT JOIN projections.users8 ON projections.sessions3.user_id = projections.users8.id AND projections.sessions3.instance_id = projections.users8.instance_id` +
` AS OF SYSTEM TIME '-1 ms'`)
expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions3.id,` +
` projections.sessions3.creation_date,` +
@ -50,6 +52,7 @@ var (
` projections.sessions3.user_checked_at,` +
` projections.login_names2.login_name,` +
` projections.users8_humans.display_name,` +
` projections.users8.resource_owner,` +
` projections.sessions3.password_checked_at,` +
` projections.sessions3.intent_checked_at,` +
` projections.sessions3.passkey_checked_at,` +
@ -58,6 +61,7 @@ var (
` FROM projections.sessions3` +
` LEFT JOIN projections.login_names2 ON projections.sessions3.user_id = projections.login_names2.user_id AND projections.sessions3.instance_id = projections.login_names2.instance_id` +
` LEFT JOIN projections.users8_humans ON projections.sessions3.user_id = projections.users8_humans.user_id AND projections.sessions3.instance_id = projections.users8_humans.instance_id` +
` LEFT JOIN projections.users8 ON projections.sessions3.user_id = projections.users8.id AND projections.sessions3.instance_id = projections.users8.instance_id` +
` AS OF SYSTEM TIME '-1 ms'`)
sessionCols = []string{
@ -73,6 +77,7 @@ var (
"user_checked_at",
"login_name",
"display_name",
"user_resource_owner",
"password_checked_at",
"intent_checked_at",
"passkey_checked_at",
@ -93,6 +98,7 @@ var (
"user_checked_at",
"login_name",
"display_name",
"user_resource_owner",
"password_checked_at",
"intent_checked_at",
"passkey_checked_at",
@ -145,6 +151,7 @@ func Test_SessionsPrepare(t *testing.T) {
testNow,
"login-name",
"display-name",
"resourceOwner",
testNow,
testNow,
testNow,
@ -172,6 +179,7 @@ func Test_SessionsPrepare(t *testing.T) {
UserCheckedAt: testNow,
LoginName: "login-name",
DisplayName: "display-name",
ResourceOwner: "resourceOwner",
},
PasswordFactor: SessionPasswordFactor{
PasswordCheckedAt: testNow,
@ -210,6 +218,7 @@ func Test_SessionsPrepare(t *testing.T) {
testNow,
"login-name",
"display-name",
"resourceOwner",
testNow,
testNow,
testNow,
@ -228,6 +237,7 @@ func Test_SessionsPrepare(t *testing.T) {
testNow,
"login-name2",
"display-name2",
"resourceOwner",
testNow,
testNow,
testNow,
@ -255,6 +265,7 @@ func Test_SessionsPrepare(t *testing.T) {
UserCheckedAt: testNow,
LoginName: "login-name",
DisplayName: "display-name",
ResourceOwner: "resourceOwner",
},
PasswordFactor: SessionPasswordFactor{
PasswordCheckedAt: testNow,
@ -283,6 +294,7 @@ func Test_SessionsPrepare(t *testing.T) {
UserCheckedAt: testNow,
LoginName: "login-name2",
DisplayName: "display-name2",
ResourceOwner: "resourceOwner",
},
PasswordFactor: SessionPasswordFactor{
PasswordCheckedAt: testNow,
@ -374,6 +386,7 @@ func Test_SessionPrepare(t *testing.T) {
testNow,
"login-name",
"display-name",
"resourceOwner",
testNow,
testNow,
testNow,
@ -396,6 +409,7 @@ func Test_SessionPrepare(t *testing.T) {
UserCheckedAt: testNow,
LoginName: "login-name",
DisplayName: "display-name",
ResourceOwner: "resourceOwner",
},
PasswordFactor: SessionPasswordFactor{
PasswordCheckedAt: testNow,

View File

@ -79,6 +79,12 @@ var (
name: "count",
table: userIDPsCountTable,
}
forceMFATable = loginPolicyTable.setAlias("auth_methods_force_mfa")
forceMFAInstanceID = LoginPolicyColumnInstanceID.setTable(forceMFATable)
forceMFAOrgID = LoginPolicyColumnOrgID.setTable(forceMFATable)
forceMFAIsDefault = LoginPolicyColumnIsDefault.setTable(forceMFATable)
forceMFAForce = LoginPolicyColumnForceMFA.setTable(forceMFATable)
)
type AuthMethods struct {
@ -170,6 +176,36 @@ func (q *Queries) ListActiveUserAuthMethodTypes(ctx context.Context, userID stri
return userAuthMethodTypes, err
}
func (q *Queries) ListUserAuthMethodTypesRequired(ctx context.Context, userID string, withOwnerRemoved bool) (userAuthMethodTypes []domain.UserAuthMethodType, forceMFA bool, err error) {
ctxData := authz.GetCtxData(ctx)
if ctxData.UserID != userID {
if err := q.checkPermission(ctx, domain.PermissionUserRead, ctxData.OrgID, userID); err != nil {
return nil, false, err
}
}
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
query, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, q.client)
eq := sq.Eq{
UserIDCol.identifier(): userID,
UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
}
if !withOwnerRemoved {
eq[UserOwnerRemovedCol.identifier()] = false
}
stmt, args, err := query.Where(eq).ToSql()
if err != nil {
return nil, false, errors.ThrowInvalidArgument(err, "QUERY-E5ut4", "Errors.Query.InvalidRequest")
}
rows, err := q.client.QueryContext(ctx, stmt, args...)
if err != nil || rows.Err() != nil {
return nil, false, errors.ThrowInternal(err, "QUERY-Dun75", "Errors.Internal")
}
return scan(rows)
}
func NewUserAuthMethodUserIDSearchQuery(value string) (SearchQuery, error) {
return NewTextQuery(UserAuthMethodColumnUserID, value, TextEquals)
}
@ -311,26 +347,11 @@ func prepareUserAuthMethodsQuery(ctx context.Context, db prepareDatabase) (sq.Se
}
func prepareActiveUserAuthMethodTypesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) {
authMethodsQuery, authMethodsArgs, err := sq.Select(
"DISTINCT("+authMethodTypeTypes.identifier()+")",
authMethodTypeUserID.identifier(),
authMethodTypeInstanceID.identifier()).
From(authMethodTypeTable.identifier()).
Where(sq.Eq{authMethodTypeState.identifier(): domain.MFAStateReady}).
ToSql()
authMethodsQuery, authMethodsArgs, err := prepareAuthMethodQuery()
if err != nil {
return sq.SelectBuilder{}, nil
}
idpsQuery, _, err := sq.Select(
userIDPsCountUserID.identifier(),
userIDPsCountInstanceID.identifier(),
"COUNT("+userIDPsCountUserID.identifier()+") AS "+userIDPsCountCount.name).
From(userIDPsCountTable.identifier()).
GroupBy(
userIDPsCountUserID.identifier(),
userIDPsCountInstanceID.identifier(),
).
ToSql()
idpsQuery, err := prepareAuthMethodsIDPsQuery()
if err != nil {
return sq.SelectBuilder{}, nil
}
@ -386,3 +407,106 @@ func prepareActiveUserAuthMethodTypesQuery(ctx context.Context, db prepareDataba
}, nil
}
}
func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) ([]domain.UserAuthMethodType, bool, error)) {
loginPolicyQuery, err := prepareAuthMethodsForceMFAQuery()
if err != nil {
return sq.SelectBuilder{}, nil
}
authMethodsQuery, authMethodsArgs, err := prepareAuthMethodQuery()
if err != nil {
return sq.SelectBuilder{}, nil
}
idpsQuery, err := prepareAuthMethodsIDPsQuery()
if err != nil {
return sq.SelectBuilder{}, nil
}
return sq.Select(
NotifyPasswordSetCol.identifier(),
authMethodTypeTypes.identifier(),
userIDPsCountCount.identifier(),
forceMFAForce.identifier()).
From(userTable.identifier()).
LeftJoin(join(NotifyUserIDCol, UserIDCol)).
LeftJoin("("+authMethodsQuery+") AS "+authMethodTypeTable.alias+" ON "+
authMethodTypeUserID.identifier()+" = "+UserIDCol.identifier()+" AND "+
authMethodTypeInstanceID.identifier()+" = "+UserInstanceIDCol.identifier(),
authMethodsArgs...).
LeftJoin("(" + idpsQuery + ") AS " + userIDPsCountTable.alias + " ON " +
userIDPsCountUserID.identifier() + " = " + UserIDCol.identifier() + " AND " +
userIDPsCountInstanceID.identifier() + " = " + UserInstanceIDCol.identifier()).
LeftJoin("(" + loginPolicyQuery + ") AS " + forceMFATable.alias + " ON " +
"(" + forceMFAOrgID.identifier() + " = " + UserInstanceIDCol.identifier() + " OR " + forceMFAOrgID.identifier() + " = " + UserResourceOwnerCol.identifier() + ") AND " +
forceMFAInstanceID.identifier() + " = " + UserInstanceIDCol.identifier() + db.Timetravel(call.Took(ctx))).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) ([]domain.UserAuthMethodType, bool, error) {
userAuthMethodTypes := make([]domain.UserAuthMethodType, 0)
var passwordSet sql.NullBool
var idp sql.NullInt64
var forceMFA sql.NullBool
for rows.Next() {
var authMethodType sql.NullInt16
err := rows.Scan(
&passwordSet,
&authMethodType,
&idp,
&forceMFA,
)
if err != nil {
return nil, false, err
}
if authMethodType.Valid {
userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodType(authMethodType.Int16))
}
}
if passwordSet.Valid && passwordSet.Bool {
userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodTypePassword)
}
if idp.Valid && idp.Int64 > 0 {
logging.Error("IDP", idp.Int64)
userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodTypeIDP)
}
if err := rows.Close(); err != nil {
return nil, false, errors.ThrowInternal(err, "QUERY-W4zje", "Errors.Query.CloseRows")
}
return userAuthMethodTypes, forceMFA.Bool, nil
}
}
func prepareAuthMethodsIDPsQuery() (string, error) {
idpsQuery, _, err := sq.Select(
userIDPsCountUserID.identifier(),
userIDPsCountInstanceID.identifier(),
"COUNT("+userIDPsCountUserID.identifier()+") AS "+userIDPsCountCount.name).
From(userIDPsCountTable.identifier()).
GroupBy(
userIDPsCountUserID.identifier(),
userIDPsCountInstanceID.identifier(),
).
ToSql()
return idpsQuery, err
}
func prepareAuthMethodQuery() (string, []interface{}, error) {
return sq.Select(
"DISTINCT("+authMethodTypeTypes.identifier()+")",
authMethodTypeUserID.identifier(),
authMethodTypeInstanceID.identifier()).
From(authMethodTypeTable.identifier()).
Where(sq.Eq{authMethodTypeState.identifier(): domain.MFAStateReady}).
ToSql()
}
func prepareAuthMethodsForceMFAQuery() (string, error) {
loginPolicyQuery, _, err := sq.Select(
forceMFAForce.identifier(),
forceMFAInstanceID.identifier(),
forceMFAOrgID.identifier(),
).
From(forceMFATable.identifier()).
OrderBy(forceMFAIsDefault.identifier()).
ToSql()
return loginPolicyQuery, err
}

View File

@ -1,6 +1,7 @@
package query
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
@ -8,6 +9,8 @@ import (
"regexp"
"testing"
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/zitadel/internal/domain"
)
@ -53,6 +56,28 @@ var (
"method_type",
"idps_count",
}
prepareAuthMethodTypesRequiredStmt = `SELECT projections.users8_notifications.password_set,` +
` auth_method_types.method_type,` +
` user_idps_count.count,` +
` auth_methods_force_mfa.force_mfa` +
` FROM projections.users8` +
` LEFT JOIN projections.users8_notifications ON projections.users8.id = projections.users8_notifications.user_id AND projections.users8.instance_id = projections.users8_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.users8.id AND auth_method_types.instance_id = projections.users8.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.users8.id AND user_idps_count.instance_id = projections.users8.instance_id` +
` LEFT JOIN (SELECT auth_methods_force_mfa.force_mfa, auth_methods_force_mfa.instance_id, auth_methods_force_mfa.aggregate_id FROM projections.login_policies4 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.users8.instance_id OR auth_methods_force_mfa.aggregate_id = projections.users8.resource_owner) AND auth_methods_force_mfa.instance_id = projections.users8.instance_id` +
` AS OF SYSTEM TIME '-1 ms
`
prepareAuthMethodTypesRequiredCols = []string{
"password_set",
"method_type",
"idps_count",
"force_mfa",
}
)
func Test_UserAuthMethodPrepares(t *testing.T) {
@ -288,6 +313,131 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
},
object: nil,
},
{
name: "prepareUserAuthMethodTypesRequiredQuery no result",
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) {
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) {
authMethods, forceMFA, err := scan(rows)
if err != nil {
return nil, err
}
return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA}, nil
}
},
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt),
nil,
nil,
),
},
object: &testUserAuthMethodTypesRequired{authMethods: []domain.UserAuthMethodType{}, forceMFA: false},
},
{
name: "prepareUserAuthMethodTypesRequiredQuery one second factor",
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) {
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) {
authMethods, forceMFA, err := scan(rows)
if err != nil {
return nil, err
}
return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA}, nil
}
},
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt),
prepareAuthMethodTypesRequiredCols,
[][]driver.Value{
{
true,
domain.UserAuthMethodTypePasswordless,
1,
true,
},
},
),
},
object: &testUserAuthMethodTypesRequired{
authMethods: []domain.UserAuthMethodType{
domain.UserAuthMethodTypePasswordless,
domain.UserAuthMethodTypePassword,
domain.UserAuthMethodTypeIDP,
},
forceMFA: true,
},
},
{
name: "prepareUserAuthMethodTypesRequiredQuery multiple second factors",
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) {
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) {
authMethods, forceMFA, err := scan(rows)
if err != nil {
return nil, err
}
return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA}, nil
}
},
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt),
prepareAuthMethodTypesRequiredCols,
[][]driver.Value{
{
true,
domain.UserAuthMethodTypePasswordless,
1,
true,
},
{
true,
domain.UserAuthMethodTypeOTP,
1,
true,
},
},
),
},
object: &testUserAuthMethodTypesRequired{
authMethods: []domain.UserAuthMethodType{
domain.UserAuthMethodTypePasswordless,
domain.UserAuthMethodTypeOTP,
domain.UserAuthMethodTypePassword,
domain.UserAuthMethodTypeIDP,
},
forceMFA: true,
},
},
{
name: "prepareUserAuthMethodTypesRequiredQuery sql err",
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) {
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) {
authMethods, forceMFA, err := scan(rows)
if err != nil {
return nil, err
}
return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA}, nil
}
},
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) {
@ -295,3 +445,9 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
})
}
}
// testUserAuthMethodTypesRequired is required as assetPrepare is only able to return a single object from scan
type testUserAuthMethodTypesRequired struct {
authMethods []domain.UserAuthMethodType
forceMFA bool
}

View File

@ -514,6 +514,9 @@ Errors:
WrongLoginClient: Auth Request, създаден от друг клиент за влизане
OIDCSession:
RefreshTokenInvalid: Токенът за опресняване е невалиден
Token:
Invalid: Токенът е невалиден
Expired: Токенът е изтекъл
AggregateTypes:
action: Действие

View File

@ -496,6 +496,10 @@ Errors:
WrongLoginClient: Auth Request wurde von einem anderen Login-Client erstellt
OIDCSession:
RefreshTokenInvalid: Refresh Token ist ungültig
Token:
Invalid: Token ist ungültig
Expired: Token ist abgelaufen
AggregateTypes:
action: Action
instance: Instanz

View File

@ -496,6 +496,9 @@ Errors:
WrongLoginClient: Auth Request created by other login client
OIDCSession:
RefreshTokenInvalid: Refresh Token is invalid
Token:
Invalid: Token is invalid
Expired: Token is expired
AggregateTypes:
action: Action

View File

@ -496,6 +496,9 @@ Errors:
WrongLoginClient: Auth Request creado por otro cliente de inicio de sesión
OIDCSession:
RefreshTokenInvalid: El token de refresco no es válido
Token:
Invalid: El token no es válido
Expired: El token ha caducado
AggregateTypes:
action: Acción

View File

@ -496,6 +496,9 @@ Errors:
WrongLoginClient: Auth Request créé par un autre client de connexion
OIDCSession:
RefreshTokenInvalid: Le jeton de rafraîchissement n'est pas valide
Token:
Invalid: Le jeton n'est pas valide
Expired: Le jeton est expiré
AggregateTypes:
action: Action

View File

@ -496,6 +496,9 @@ Errors:
WrongLoginClient: Auth Request creato da un altro client di accesso
OIDCSession:
RefreshTokenInvalid: Refresh Token non è valido
Token:
Invalid: Token non è valido
Expired: Token è scaduto
AggregateTypes:
action: Azione

View File

@ -485,6 +485,9 @@ Errors:
WrongLoginClient: 他のログインクライアントによって作成された AuthRequest
OIDCSession:
RefreshTokenInvalid: 無効なリフレッシュトークンです
Token:
Invalid: トークンが無効です
Expired: トークンの有効期限が切れている
AggregateTypes:
action: アクション

View File

@ -490,6 +490,15 @@ Errors:
TokenCreationFailed: Неуспешно креирање на токен
InvalidToken: Токенот за намера е невалиден
OtherUser: Намерата е за друг корисник
AuthRequest:
AlreadyExists: Барањето за автентикација веќе постои
NotExisting: Барањето за автентикација не постои
WrongLoginClient: Барањето за автификација беше креирано од друг клиент за најавување
OIDCSession:
RefreshTokenInvalid: Токенот за освежување е неважечки
Token:
Invalid: токенот е неважечки
Expired: токенот е истечен
AggregateTypes:
action: Акција

View File

@ -496,6 +496,9 @@ Errors:
WrongLoginClient: Auth Request utworzony przez innego klienta logowania
OIDCSession:
RefreshTokenInvalid: Refresh Token jest nieprawidłowy
Token:
Invalid: Token jest nieprawidłowy
Expired: Token wygasł
AggregateTypes:
action: Działanie

View File

@ -496,6 +496,9 @@ Errors:
WrongLoginClient: 其他登录客户端创建的AuthRequest
OIDCSession:
RefreshTokenInvalid: Refresh Token 无效
Token:
Invalid: 令牌无效
Expired: 令牌已过期
AggregateTypes:
action: 动作

View File

@ -74,6 +74,11 @@ message UserFactor {
description: "\"display name of the checked user\"";
}
];
string organisation_id = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "\"organisation id of the checked user\"";
}
];
}
message PasswordFactor {