zitadel/internal/command/auth_request_model.go
Tim Möhlmann 1aa8c49e41
fix(oidc): store requested response_mode (#8145)
# Which Problems Are Solved

Zitadel never stored or returned the requested `response_mode` in oidc
Auth Requests. This caused the oidc library to fallback to the default
based on the response_type.

# How the Problems Are Solved

- Store the `response_mode` in the Auth request repo
- Store the `response_mode` in the Auth request v2 events
- Return the `resonse_mode` from the Auth Request v1 and v2
`ResponseMode()` methods. (Was hard-coded to an empty string)

# Additional Changes

- Populate the `response_modes_supported` to the oidc Discovery
Configuration. When it was empty, the standard specifies the default of
`query` and `fragment`. However, our oidc library also supports
`form_post` and by this fix, zitadel now also supports this.

# Additional Context

- Closes #6586
- Reported
https://discord.com/channels/927474939156643850/1151508313717084220

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
2024-06-17 09:50:12 +00:00

115 lines
3.6 KiB
Go

package command
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/authrequest"
"github.com/zitadel/zitadel/internal/zerrors"
)
type AuthRequestWriteModel struct {
eventstore.WriteModel
aggregate *eventstore.Aggregate
LoginClient string
ClientID string
RedirectURI string
State string
Nonce string
Scope []string
Audience []string
ResponseType domain.OIDCResponseType
ResponseMode domain.OIDCResponseMode
CodeChallenge *domain.OIDCCodeChallenge
Prompt []domain.Prompt
UILocales []string
MaxAge *time.Duration
LoginHint *string
HintUserID *string
SessionID string
UserID string
AuthTime time.Time
AuthMethods []domain.UserAuthMethodType
AuthRequestState domain.AuthRequestState
NeedRefreshToken bool
}
func NewAuthRequestWriteModel(ctx context.Context, id string) *AuthRequestWriteModel {
return &AuthRequestWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: id,
},
aggregate: &authrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate,
}
}
func (m *AuthRequestWriteModel) Reduce() error {
for _, event := range m.Events {
switch e := event.(type) {
case *authrequest.AddedEvent:
m.LoginClient = e.LoginClient
m.ClientID = e.ClientID
m.RedirectURI = e.RedirectURI
m.State = e.State
m.Nonce = e.Nonce
m.Scope = e.Scope
m.Audience = e.Audience
m.ResponseType = e.ResponseType
m.ResponseMode = e.ResponseMode
m.CodeChallenge = e.CodeChallenge
m.Prompt = e.Prompt
m.UILocales = e.UILocales
m.MaxAge = e.MaxAge
m.LoginHint = e.LoginHint
m.HintUserID = e.HintUserID
m.AuthRequestState = domain.AuthRequestStateAdded
m.NeedRefreshToken = e.NeedRefreshToken
case *authrequest.SessionLinkedEvent:
m.SessionID = e.SessionID
m.UserID = e.UserID
m.AuthTime = e.AuthTime
m.AuthMethods = e.AuthMethods
case *authrequest.CodeAddedEvent:
m.AuthRequestState = domain.AuthRequestStateCodeAdded
case *authrequest.FailedEvent:
m.AuthRequestState = domain.AuthRequestStateFailed
case *authrequest.CodeExchangedEvent:
m.AuthRequestState = domain.AuthRequestStateCodeExchanged
case *authrequest.SucceededEvent:
m.AuthRequestState = domain.AuthRequestStateSucceeded
}
}
return m.WriteModel.Reduce()
}
func (m *AuthRequestWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AddQuery().
AggregateTypes(authrequest.AggregateType).
AggregateIDs(m.AggregateID).
Builder()
}
// CheckAuthenticated checks that the auth request exists, a session must have been linked
// and in case of a Code Flow the code must have been exchanged
func (m *AuthRequestWriteModel) CheckAuthenticated() error {
if m.SessionID == "" {
return zerrors.ThrowPreconditionFailed(nil, "AUTHR-SF2r2", "Errors.AuthRequest.NotAuthenticated")
}
// in case of OIDC Code Flow, the code must have been exchanged
if m.ResponseType == domain.OIDCResponseTypeCode && m.AuthRequestState == domain.AuthRequestStateCodeExchanged {
return nil
}
// in case of OIDC Implicit Flow, check that the requests exists, but has not succeeded yet
if (m.ResponseType == domain.OIDCResponseTypeIDToken || m.ResponseType == domain.OIDCResponseTypeIDTokenToken) &&
m.AuthRequestState == domain.AuthRequestStateAdded {
return nil
}
return zerrors.ThrowPreconditionFailed(nil, "AUTHR-sajk3", "Errors.AuthRequest.NotAuthenticated")
}