mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
* fix(crypto): use authenticated encryption for IDP intent tokens IDP intent tokens were encrypted with AES-CFB which provides no integrity protection, making them malleable. Switch to A256GCM via the existing AuthAlgorithm interface (EncryptToken/DecryptToken), the same approach already used for OIDC and session tokens. The idpConfigEncryption key type is widened from EncryptionAlgorithm to AuthEncryptionAlgorithm so that data-at-rest encryption (IDP client secrets, LDAP bind passwords, SAML keys) continues to use the CFB path unmodified. Co-authored-by: Cursor <[email protected]> * fix(proto): raise IDP intent token max length to 500 for authenticated tokens The switch to authenticated A256GCM (JWE compact serialization) makes IDP intent tokens longer (~270 chars) than the previous AES-CFB base64 output. Raise the max_len/max_length validation and OpenAPI constraints on every idp_intent_token field from 200 to 500 so the new tokens are accepted: user v2/v2beta (idp + user_service), session v2/v2beta, and resources user v3alpha. Co-Authored-By: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: Cursor <[email protected]> Co-authored-by: Claude Opus 4.8 <[email protected]>
202 lines
5.9 KiB
Go
202 lines
5.9 KiB
Go
package domain
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
|
"github.com/zitadel/zitadel/internal/repository/session"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
type CheckIDPIntentType struct {
|
|
ID string
|
|
Token string
|
|
}
|
|
|
|
type IDPIntentCheckCommand struct {
|
|
CheckIntent *CheckIDPIntentType
|
|
|
|
SessionID string
|
|
InstanceID string
|
|
EncAlgo crypto.AuthEncryptionAlgorithm
|
|
|
|
FetchedUser User
|
|
IsCheckComplete bool
|
|
IntentLastVerified time.Time
|
|
}
|
|
|
|
// NewIDPIntentCheckCommand returns an IDPIntentCheckCommand initialized with the input values.
|
|
//
|
|
// If encryptionAlgo is nil, the default [crypto.EncryptionAlgorithm] will be used
|
|
func NewIDPIntentCheckCommand(request *CheckIDPIntentType, sessionID, instanceID string, encryptionAlgo crypto.AuthEncryptionAlgorithm) *IDPIntentCheckCommand {
|
|
idpCheckCommand := &IDPIntentCheckCommand{
|
|
CheckIntent: request,
|
|
SessionID: sessionID,
|
|
InstanceID: instanceID,
|
|
EncAlgo: idpEncryptionAlgo,
|
|
}
|
|
|
|
if encryptionAlgo != nil {
|
|
idpCheckCommand.EncAlgo = encryptionAlgo
|
|
}
|
|
|
|
return idpCheckCommand
|
|
}
|
|
|
|
// Events implements [Commander].
|
|
func (i *IDPIntentCheckCommand) Events(ctx context.Context, opts *InvokeOpts) ([]eventstore.Command, error) {
|
|
if i.CheckIntent == nil || !i.IsCheckComplete {
|
|
return nil, nil
|
|
}
|
|
|
|
return []eventstore.Command{
|
|
session.NewIntentCheckedEvent(ctx, &session.NewAggregate(i.SessionID, i.InstanceID).Aggregate, i.IntentLastVerified),
|
|
idpintent.NewConsumedEvent(ctx, &idpintent.NewAggregate(i.CheckIntent.ID, "").Aggregate),
|
|
}, nil
|
|
}
|
|
|
|
// Execute implements [Commander].
|
|
func (i *IDPIntentCheckCommand) Execute(ctx context.Context, opts *InvokeOpts) (err error) {
|
|
if i.CheckIntent == nil {
|
|
return nil
|
|
}
|
|
|
|
intentRepo := opts.idpIntentRepo
|
|
sessionRepo := opts.sessionRepo
|
|
|
|
beginner, ok := opts.DB().(database.Beginner)
|
|
if !ok {
|
|
return zerrors.ThrowInternal(nil, "DOM-jiuIbh", "database doesn't implement database.Beginner")
|
|
}
|
|
|
|
tx, txErr := beginner.Begin(ctx, nil)
|
|
if txErr != nil {
|
|
return zerrors.ThrowInternal(txErr, "DOM-Msuwn2", "failed starting transaction")
|
|
}
|
|
|
|
defer func() {
|
|
if endErr := tx.End(ctx, txErr); endErr != nil {
|
|
err = endErr
|
|
}
|
|
}()
|
|
|
|
removedRows, err := intentRepo.Delete(ctx, opts.DB(), intentRepo.PrimaryKeyCondition(i.InstanceID, i.CheckIntent.ID))
|
|
if err != nil {
|
|
txErr = zerrors.ThrowInternal(err, "DOM-j1s5Eu", "failed deleting IDP intent")
|
|
return txErr
|
|
}
|
|
if removedRows != 1 {
|
|
txErr = zerrors.ThrowInternal(NewRowsReturnedMismatchError(1, removedRows), "DOM-3CBpdB", "unexpected number of rows deleted")
|
|
return txErr
|
|
}
|
|
|
|
idpFactor := &SessionFactorIdentityProviderIntent{LastVerifiedAt: time.Now()}
|
|
updateCount, updateErr := sessionRepo.Update(ctx, opts.DB(),
|
|
sessionRepo.PrimaryKeyCondition(i.InstanceID, i.SessionID),
|
|
sessionRepo.SetFactor(idpFactor),
|
|
)
|
|
if updateErr != nil {
|
|
txErr = zerrors.ThrowInternal(updateErr, "DOM-pec0al", "failed updating session")
|
|
return txErr
|
|
}
|
|
|
|
if updateCount == 0 {
|
|
txErr = zerrors.ThrowNotFound(nil, "DOM-CopO4e", "session not found")
|
|
return txErr
|
|
}
|
|
if updateCount > 1 {
|
|
txErr = zerrors.ThrowInternal(NewMultipleObjectsUpdatedError(1, updateCount), "DOM-mlbibw", "unexpected number of rows updated")
|
|
return txErr
|
|
}
|
|
|
|
i.IntentLastVerified = idpFactor.LastVerifiedAt
|
|
i.IsCheckComplete = true
|
|
|
|
return nil
|
|
}
|
|
|
|
// String implements [Commander].
|
|
func (i *IDPIntentCheckCommand) String() string {
|
|
return "IDPIntentCheckCommand"
|
|
}
|
|
|
|
// Validate implements [Commander].
|
|
func (i *IDPIntentCheckCommand) Validate(ctx context.Context, opts *InvokeOpts) (err error) {
|
|
if i.CheckIntent == nil {
|
|
return nil
|
|
}
|
|
|
|
if i.SessionID == "" {
|
|
return zerrors.ThrowPreconditionFailed(nil, "DOM-5Y8pb4", "Errors.Missing.SessionID")
|
|
}
|
|
if i.InstanceID == "" {
|
|
return zerrors.ThrowPreconditionFailed(nil, "DOM-Q4YFIq", "Errors.Missing.InstanceID")
|
|
}
|
|
|
|
sessionRepo := opts.sessionRepo
|
|
idpIntentRepo := opts.idpIntentRepo
|
|
userRepo := opts.userRepo
|
|
|
|
session, err := sessionRepo.Get(ctx, opts.DB(), database.WithCondition(sessionRepo.PrimaryKeyCondition(i.InstanceID, i.SessionID)))
|
|
if err = handleGetError(err, "DOM-EhIgey", "session"); err != nil {
|
|
return err
|
|
}
|
|
|
|
if session.UserID == "" {
|
|
return zerrors.ThrowPreconditionFailed(nil, "DOM-IJcVkV", "Errors.User.UserIDMissing")
|
|
}
|
|
|
|
if err := crypto.CheckToken(i.EncAlgo, i.CheckIntent.Token, i.CheckIntent.ID); err != nil {
|
|
return err
|
|
}
|
|
|
|
intent, err := idpIntentRepo.Get(ctx, opts.DB(), database.WithCondition(idpIntentRepo.PrimaryKeyCondition(i.InstanceID, i.CheckIntent.ID)))
|
|
if err = handleGetError(err, "DOM-5XkWJV", "intent"); err != nil {
|
|
return err
|
|
}
|
|
|
|
if intent.State != IDPIntentStateSucceeded {
|
|
return zerrors.ThrowPreconditionFailed(nil, "DOM-0UKHku", "Errors.Intent.NotSucceeded")
|
|
}
|
|
|
|
if intent.ExpiresAt == nil || intent.ExpiresAt.Before(time.Now()) {
|
|
return zerrors.ThrowPreconditionFailed(nil, "DOM-kDR1XK", "Errors.Intent.Expired")
|
|
}
|
|
|
|
user, err := userRepo.Get(ctx, opts.DB(), database.WithCondition(userRepo.PrimaryKeyCondition(i.InstanceID, session.UserID)))
|
|
if err = handleGetError(err, "DOM-Vnx2G9", "user"); err != nil {
|
|
return err
|
|
}
|
|
if user.Human == nil {
|
|
return zerrors.ThrowInternal(nil, "DOM-FkX5lZ", "user not human")
|
|
}
|
|
|
|
if intentUsr := intent.UserID; intentUsr != "" {
|
|
if intentUsr != session.UserID {
|
|
return zerrors.ThrowPreconditionFailed(nil, "DOM-FLdnLH", "Errors.Intent.OtherUser")
|
|
}
|
|
i.FetchedUser = *user
|
|
return nil
|
|
}
|
|
|
|
var matchingLink *IdentityProviderLink
|
|
for _, idpLink := range user.Human.IdentityProviderLinks {
|
|
if idpLink.ProviderID == intent.IDPID {
|
|
matchingLink = idpLink
|
|
break
|
|
}
|
|
}
|
|
if matchingLink == nil {
|
|
return zerrors.ThrowPreconditionFailed(nil, "DOM-XuNkt7", "Errors.Intent.OtherUser")
|
|
}
|
|
|
|
i.FetchedUser = *user
|
|
return nil
|
|
}
|
|
|
|
var _ Commander = (*IDPIntentCheckCommand)(nil)
|