2023-07-14 11:16:16 +00:00
|
|
|
package query
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2024-05-16 05:07:56 +00:00
|
|
|
"golang.org/x/text/language"
|
|
|
|
|
2023-07-14 11:16:16 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
|
|
"github.com/zitadel/zitadel/internal/repository/oidcsession"
|
2023-07-19 11:17:39 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/repository/session"
|
2024-02-28 09:30:05 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/repository/user"
|
2023-07-14 11:16:16 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
2023-12-08 14:30:55 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
2023-07-14 11:16:16 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type OIDCSessionAccessTokenReadModel struct {
|
2024-06-12 09:11:36 +00:00
|
|
|
eventstore.ReadModel
|
2023-07-14 11:16:16 +00:00
|
|
|
|
|
|
|
UserID string
|
|
|
|
SessionID string
|
|
|
|
ClientID string
|
|
|
|
Audience []string
|
|
|
|
Scope []string
|
|
|
|
AuthMethods []domain.UserAuthMethodType
|
|
|
|
AuthTime time.Time
|
2024-05-16 05:07:56 +00:00
|
|
|
Nonce string
|
2023-07-14 11:16:16 +00:00
|
|
|
State domain.OIDCSessionState
|
|
|
|
AccessTokenID string
|
|
|
|
AccessTokenCreation time.Time
|
|
|
|
AccessTokenExpiration time.Time
|
2024-05-16 05:07:56 +00:00
|
|
|
PreferredLanguage *language.Tag
|
|
|
|
UserAgent *domain.UserAgent
|
2024-03-20 10:18:46 +00:00
|
|
|
Reason domain.TokenReason
|
|
|
|
Actor *domain.TokenActor
|
2023-07-14 11:16:16 +00:00
|
|
|
}
|
|
|
|
|
2023-07-19 11:17:39 +00:00
|
|
|
func newOIDCSessionAccessTokenReadModel(id string) *OIDCSessionAccessTokenReadModel {
|
2023-07-14 11:16:16 +00:00
|
|
|
return &OIDCSessionAccessTokenReadModel{
|
2024-06-12 09:11:36 +00:00
|
|
|
ReadModel: eventstore.ReadModel{
|
2023-07-14 11:16:16 +00:00
|
|
|
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)
|
2023-07-17 12:33:37 +00:00
|
|
|
case *oidcsession.AccessTokenRevokedEvent,
|
|
|
|
*oidcsession.RefreshTokenRevokedEvent:
|
|
|
|
wm.reduceTokenRevoked(event)
|
2023-07-14 11:16:16 +00:00
|
|
|
}
|
|
|
|
}
|
2024-06-12 09:11:36 +00:00
|
|
|
return wm.ReadModel.Reduce()
|
2023-07-14 11:16:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (wm *OIDCSessionAccessTokenReadModel) Query() *eventstore.SearchQueryBuilder {
|
|
|
|
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
|
|
|
AddQuery().
|
|
|
|
AggregateTypes(oidcsession.AggregateType).
|
|
|
|
AggregateIDs(wm.AggregateID).
|
|
|
|
EventTypes(
|
|
|
|
oidcsession.AddedType,
|
|
|
|
oidcsession.AccessTokenAddedType,
|
2023-07-17 12:33:37 +00:00
|
|
|
oidcsession.AccessTokenRevokedType,
|
|
|
|
oidcsession.RefreshTokenRevokedType,
|
2023-07-14 11:16:16 +00:00
|
|
|
).
|
|
|
|
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
|
2024-05-16 05:07:56 +00:00
|
|
|
wm.Nonce = e.Nonce
|
|
|
|
wm.PreferredLanguage = e.PreferredLanguage
|
|
|
|
wm.UserAgent = e.UserAgent
|
2023-07-14 11:16:16 +00:00
|
|
|
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)
|
2024-03-20 10:18:46 +00:00
|
|
|
wm.Reason = e.Reason
|
|
|
|
wm.Actor = e.Actor
|
2023-07-14 11:16:16 +00:00
|
|
|
}
|
|
|
|
|
2023-07-17 12:33:37 +00:00
|
|
|
func (wm *OIDCSessionAccessTokenReadModel) reduceTokenRevoked(e eventstore.Event) {
|
|
|
|
wm.AccessTokenID = ""
|
2023-10-19 10:19:10 +00:00
|
|
|
wm.AccessTokenExpiration = e.CreatedAt()
|
2023-07-17 12:33:37 +00:00
|
|
|
}
|
|
|
|
|
2023-07-14 11:16:16 +00:00
|
|
|
// ActiveAccessTokenByToken will check if the token is active by retrieving the OIDCSession events from the eventstore.
|
2023-07-19 11:17:39 +00:00
|
|
|
// Refreshed or expired tokens will return an error as well as if the underlying sessions has been terminated.
|
2023-07-14 11:16:16 +00:00
|
|
|
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 {
|
2024-08-26 10:15:40 +00:00
|
|
|
return nil, zerrors.ThrowUnauthenticated(nil, "QUERY-LJK2W", "Errors.OIDCSession.Token.Invalid")
|
2023-07-14 11:16:16 +00:00
|
|
|
}
|
|
|
|
model, err = q.accessTokenByOIDCSessionAndTokenID(ctx, split[0], split[1])
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if !model.AccessTokenExpiration.After(time.Now()) {
|
2024-08-26 10:15:40 +00:00
|
|
|
return nil, zerrors.ThrowUnauthenticated(nil, "QUERY-SAF3rf", "Errors.OIDCSession.Token.Expired")
|
2023-07-14 11:16:16 +00:00
|
|
|
}
|
2024-06-12 09:11:36 +00:00
|
|
|
if err = q.checkSessionNotTerminatedAfter(ctx, model.SessionID, model.UserID, model.Position, model.UserAgent.GetFingerprintID()); err != nil {
|
2023-07-19 11:17:39 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return model, nil
|
2023-07-14 11:16:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (q *Queries) accessTokenByOIDCSessionAndTokenID(ctx context.Context, oidcSessionID, tokenID string) (model *OIDCSessionAccessTokenReadModel, err error) {
|
|
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
|
|
defer func() { span.EndWithError(err) }()
|
|
|
|
|
2023-07-19 11:17:39 +00:00
|
|
|
model = newOIDCSessionAccessTokenReadModel(oidcSessionID)
|
2023-07-14 11:16:16 +00:00
|
|
|
if err = q.eventstore.FilterToQueryReducer(ctx, model); err != nil {
|
2024-08-26 10:15:40 +00:00
|
|
|
return nil, zerrors.ThrowUnauthenticated(err, "QUERY-ASfe2", "Errors.OIDCSession.Token.Invalid")
|
2023-07-14 11:16:16 +00:00
|
|
|
}
|
|
|
|
if model.AccessTokenID != tokenID {
|
2024-08-26 10:15:40 +00:00
|
|
|
return nil, zerrors.ThrowUnauthenticated(nil, "QUERY-M2u9w", "Errors.OIDCSession.Token.Invalid")
|
2023-07-14 11:16:16 +00:00
|
|
|
}
|
|
|
|
return model, nil
|
|
|
|
}
|
2023-07-19 11:17:39 +00:00
|
|
|
|
2024-05-16 05:07:56 +00:00
|
|
|
// checkSessionNotTerminatedAfter checks if a [session.TerminateType] event (or user events leading to a session termination)
|
|
|
|
// occurred after a certain time and will return an error if so.
|
2024-09-24 16:43:29 +00:00
|
|
|
func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, userID string, position float64, fingerprintID string) (err error) {
|
2023-07-19 11:17:39 +00:00
|
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
|
|
defer func() { span.EndWithError(err) }()
|
|
|
|
|
2024-02-28 09:30:05 +00:00
|
|
|
model := &sessionTerminatedModel{
|
2024-05-16 05:07:56 +00:00
|
|
|
sessionID: sessionID,
|
2024-06-12 09:11:36 +00:00
|
|
|
position: position,
|
2024-05-16 05:07:56 +00:00
|
|
|
userID: userID,
|
|
|
|
fingerPrintID: fingerprintID,
|
2024-02-28 09:30:05 +00:00
|
|
|
}
|
|
|
|
err = q.eventstore.FilterToQueryReducer(ctx, model)
|
|
|
|
if err != nil {
|
2024-08-26 10:15:40 +00:00
|
|
|
return zerrors.ThrowUnauthenticated(err, "QUERY-SJ642", "Errors.Internal")
|
2024-02-28 09:30:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if model.terminated {
|
2024-08-26 10:15:40 +00:00
|
|
|
return zerrors.ThrowUnauthenticated(nil, "QUERY-IJL3H", "Errors.OIDCSession.Token.Invalid")
|
2024-02-28 09:30:05 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type sessionTerminatedModel struct {
|
2024-09-24 16:43:29 +00:00
|
|
|
position float64
|
2024-05-16 05:07:56 +00:00
|
|
|
sessionID string
|
|
|
|
userID string
|
|
|
|
fingerPrintID string
|
2024-02-28 09:30:05 +00:00
|
|
|
|
|
|
|
events int
|
|
|
|
terminated bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *sessionTerminatedModel) Reduce() error {
|
|
|
|
s.terminated = s.events > 0
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *sessionTerminatedModel) AppendEvents(events ...eventstore.Event) {
|
|
|
|
s.events += len(events)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *sessionTerminatedModel) Query() *eventstore.SearchQueryBuilder {
|
|
|
|
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
2024-09-24 16:43:29 +00:00
|
|
|
PositionAfter(s.position).
|
2023-07-19 11:17:39 +00:00
|
|
|
AddQuery().
|
|
|
|
AggregateTypes(session.AggregateType).
|
2024-02-28 09:30:05 +00:00
|
|
|
AggregateIDs(s.sessionID).
|
2023-07-19 11:17:39 +00:00
|
|
|
EventTypes(
|
|
|
|
session.TerminateType,
|
|
|
|
).
|
2024-02-28 09:30:05 +00:00
|
|
|
Builder()
|
|
|
|
if s.userID == "" {
|
|
|
|
return query
|
2023-07-19 11:17:39 +00:00
|
|
|
}
|
2024-02-28 09:30:05 +00:00
|
|
|
return query.
|
|
|
|
AddQuery().
|
|
|
|
AggregateTypes(user.AggregateType).
|
|
|
|
AggregateIDs(s.userID).
|
|
|
|
EventTypes(
|
|
|
|
user.UserDeactivatedType,
|
|
|
|
user.UserLockedType,
|
|
|
|
user.UserRemovedType,
|
|
|
|
).
|
2024-05-16 05:07:56 +00:00
|
|
|
Or(). // for specific logout on v1 sessions from the same user agent
|
|
|
|
AggregateTypes(user.AggregateType).
|
|
|
|
AggregateIDs(s.userID).
|
|
|
|
EventTypes(
|
|
|
|
user.HumanSignedOutType,
|
|
|
|
).
|
|
|
|
EventData(map[string]interface{}{"userAgentID": s.fingerPrintID}).
|
2024-02-28 09:30:05 +00:00
|
|
|
Builder()
|
2023-07-19 11:17:39 +00:00
|
|
|
}
|