mirror of
https://github.com/zitadel/zitadel.git
synced 2025-10-25 20:38:48 +00:00
userinfo from events for v2 tokens
This commit is contained in:
@@ -27,6 +27,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// TODO: remove declarations: (moved to domain package)
|
||||||
ScopeProjectRolePrefix = "urn:zitadel:iam:org:project:role:"
|
ScopeProjectRolePrefix = "urn:zitadel:iam:org:project:role:"
|
||||||
ScopeProjectsRoles = "urn:zitadel:iam:org:projects:roles"
|
ScopeProjectsRoles = "urn:zitadel:iam:org:projects:roles"
|
||||||
ClaimProjectRoles = "urn:zitadel:iam:org:project:roles"
|
ClaimProjectRoles = "urn:zitadel:iam:org:project:roles"
|
||||||
@@ -179,21 +180,6 @@ func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection
|
|||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
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, clientID, projectID,
|
|
||||||
token.Audience, token.Scope,
|
|
||||||
token.AccessTokenCreation, token.AccessTokenExpiration)
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := o.repo.TokenByIDs(ctx, subject, tokenID)
|
token, err := o.repo.TokenByIDs(ctx, subject, tokenID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.ThrowPermissionDenied(nil, "OIDC-Dsfb2", "token is not valid or has expired")
|
return errors.ThrowPermissionDenied(nil, "OIDC-Dsfb2", "token is not valid or has expired")
|
||||||
|
|||||||
110
internal/api/oidc/introspect.go
Normal file
110
internal/api/oidc/introspect.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/op"
|
||||||
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
|
errz "github.com/zitadel/zitadel/internal/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionRequest]) (_ *op.Response, err error) {
|
||||||
|
clientID, err := s.authenticateResourceClient(ctx, r.Data.ClientCredentials)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
response := new(oidc.IntrospectionResponse)
|
||||||
|
tokenID, subject, err := s.getTokenIDAndSubject(ctx, r.Data.Token)
|
||||||
|
if err != nil {
|
||||||
|
// TODO: log error
|
||||||
|
return op.NewResponse(response), nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(tokenID, command.IDPrefixV2) {
|
||||||
|
err = s.introspect(ctx, response, tokenID, subject, clientID)
|
||||||
|
return op.NewResponse(response), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.storage.SetIntrospectionFromToken(ctx, response, tokenID, subject, clientID)
|
||||||
|
if err != nil {
|
||||||
|
return op.NewResponse(response), nil
|
||||||
|
}
|
||||||
|
response.Active = true
|
||||||
|
return op.NewResponse(response), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) authenticateResourceClient(ctx context.Context, cc *op.ClientCredentials) (clientID string, err error) {
|
||||||
|
if cc.ClientAssertion != "" {
|
||||||
|
verifier := op.NewJWTProfileVerifier(s.storage, op.IssuerFromContext(ctx), 1*time.Hour, time.Second)
|
||||||
|
profile, err := op.VerifyJWTAssertion(ctx, cc.ClientAssertion, verifier)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return profile.Issuer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.storage.AuthorizeClientIDSecret(ctx, cc.ClientID, cc.ClientSecret); err != nil {
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cc.ClientID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getTokenIDAndSubject(ctx context.Context, accessToken string) (idToken, subject string, err error) {
|
||||||
|
provider := s.Provider()
|
||||||
|
tokenIDSubject, err := provider.Crypto().Decrypt(accessToken)
|
||||||
|
if err == nil {
|
||||||
|
splitToken := strings.Split(tokenIDSubject, ":")
|
||||||
|
if len(splitToken) != 2 {
|
||||||
|
return "", "", errors.New("invalid token format")
|
||||||
|
}
|
||||||
|
return splitToken[0], splitToken[1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
verifier := op.NewAccessTokenVerifier(op.IssuerFromContext(ctx), s.storage.keySet)
|
||||||
|
accessTokenClaims, err := op.VerifyAccessToken[*oidc.AccessTokenClaims](ctx, accessToken, verifier)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return accessTokenClaims.JWTID, accessTokenClaims.Subject, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) introspect(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) (err error) {
|
||||||
|
// TODO: give clients their own aggregate, so we can skip this query
|
||||||
|
projectID, err := s.storage.query.ProjectIDFromClientID(ctx, clientID, false)
|
||||||
|
if err != nil {
|
||||||
|
return errz.ThrowPermissionDenied(nil, "OIDC-Adfg5", "client not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := s.storage.query.ActiveAccessTokenByToken(ctx, tokenID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !slices.ContainsFunc(token.Audience, func(aud string) bool {
|
||||||
|
return aud == token.ClientID || aud == projectID
|
||||||
|
}) {
|
||||||
|
return errz.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo, err := s.storage.query.GetOIDCUserinfo(ctx, token.UserID, token.Scope, []string{projectID})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
introspection.SetUserInfo(userinfoToOIDC(userInfo, token.Scope))
|
||||||
|
introspection.Scope = token.Scope
|
||||||
|
introspection.ClientID = token.ClientID
|
||||||
|
introspection.TokenType = oidc.BearerToken
|
||||||
|
introspection.Expiration = oidc.FromTime(token.AccessTokenExpiration)
|
||||||
|
introspection.IssuedAt = oidc.FromTime(token.AccessTokenCreation)
|
||||||
|
introspection.NotBefore = oidc.FromTime(token.AccessTokenCreation)
|
||||||
|
introspection.Audience = token.Audience
|
||||||
|
introspection.Issuer = op.IssuerFromContext(ctx)
|
||||||
|
introspection.JWTID = tokenID
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -2,10 +2,7 @@ package oidc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
"github.com/zitadel/oidc/v3/pkg/op"
|
"github.com/zitadel/oidc/v3/pkg/op"
|
||||||
@@ -163,62 +160,6 @@ func (s *Server) DeviceToken(ctx context.Context, r *op.ClientRequest[oidc.Devic
|
|||||||
return s.LegacyServer.DeviceToken(ctx, r)
|
return s.LegacyServer.DeviceToken(ctx, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) authenticateResourceClient(ctx context.Context, cc *op.ClientCredentials) (clientID string, err error) {
|
|
||||||
if cc.ClientAssertion != "" {
|
|
||||||
verifier := op.NewJWTProfileVerifier(s.storage, op.IssuerFromContext(ctx), 1*time.Hour, time.Second)
|
|
||||||
profile, err := op.VerifyJWTAssertion(ctx, cc.ClientAssertion, verifier)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return profile.Issuer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = s.storage.AuthorizeClientIDSecret(ctx, cc.ClientID, cc.ClientSecret); err != nil {
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cc.ClientID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) getTokenIDAndSubject(ctx context.Context, accessToken string) (idToken, subject string, err error) {
|
|
||||||
provider := s.Provider()
|
|
||||||
tokenIDSubject, err := provider.Crypto().Decrypt(accessToken)
|
|
||||||
if err == nil {
|
|
||||||
splitToken := strings.Split(tokenIDSubject, ":")
|
|
||||||
if len(splitToken) != 2 {
|
|
||||||
return "", "", errors.New("invalid token format")
|
|
||||||
}
|
|
||||||
return splitToken[0], splitToken[1], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
verifier := op.NewAccessTokenVerifier(op.IssuerFromContext(ctx), s.storage.keySet)
|
|
||||||
accessTokenClaims, err := op.VerifyAccessToken[*oidc.AccessTokenClaims](ctx, accessToken, verifier)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
return accessTokenClaims.JWTID, accessTokenClaims.Subject, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionRequest]) (_ *op.Response, err error) {
|
|
||||||
clientID, err := s.authenticateResourceClient(ctx, r.Data.ClientCredentials)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
response := new(oidc.IntrospectionResponse)
|
|
||||||
tokenID, subject, err := s.getTokenIDAndSubject(ctx, r.Data.Token)
|
|
||||||
if err != nil {
|
|
||||||
// TODO: log error
|
|
||||||
return op.NewResponse(response), nil
|
|
||||||
}
|
|
||||||
err = s.storage.SetIntrospectionFromToken(ctx, response, tokenID, subject, clientID)
|
|
||||||
if err != nil {
|
|
||||||
return op.NewResponse(response), nil
|
|
||||||
}
|
|
||||||
response.Active = true
|
|
||||||
return op.NewResponse(response), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) UserInfo(ctx context.Context, r *op.Request[oidc.UserInfoRequest]) (_ *op.Response, err error) {
|
func (s *Server) UserInfo(ctx context.Context, r *op.Request[oidc.UserInfoRequest]) (_ *op.Response, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|||||||
88
internal/api/oidc/userinfo.go
Normal file
88
internal/api/oidc/userinfo.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
|
)
|
||||||
|
|
||||||
|
func userinfoToOIDC(user *query.OIDCUserinfo, scopes []string) *oidc.UserInfo {
|
||||||
|
out := new(oidc.UserInfo)
|
||||||
|
for _, scope := range scopes {
|
||||||
|
switch scope {
|
||||||
|
case oidc.ScopeOpenID:
|
||||||
|
out.Subject = user.ID
|
||||||
|
case oidc.ScopeEmail:
|
||||||
|
out.UserInfoEmail = userInfoEmailToOIDC(user)
|
||||||
|
case oidc.ScopeProfile:
|
||||||
|
out.UserInfoProfile = userInfoProfileToOidc(user)
|
||||||
|
case oidc.ScopePhone:
|
||||||
|
out.UserInfoPhone = userInfoPhoneToOIDC(user)
|
||||||
|
case oidc.ScopeAddress:
|
||||||
|
out.Address = userInfoAddressToOIDC(user)
|
||||||
|
case ScopeUserMetaData:
|
||||||
|
if len(user.Metadata) > 0 {
|
||||||
|
out.AppendClaims(ClaimUserMetaData, user.Metadata)
|
||||||
|
}
|
||||||
|
case ScopeResourceOwner:
|
||||||
|
setUserInfoOrgClaims(user, out)
|
||||||
|
default:
|
||||||
|
if strings.HasPrefix(scope, domain.OrgDomainPrimaryScope) {
|
||||||
|
out.AppendClaims(domain.OrgDomainPrimaryClaim, strings.TrimPrefix(scope, domain.OrgDomainPrimaryScope))
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(scope, domain.OrgIDScope) {
|
||||||
|
out.AppendClaims(domain.OrgIDClaim, strings.TrimPrefix(scope, domain.OrgIDScope))
|
||||||
|
setUserInfoOrgClaims(user, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func userInfoEmailToOIDC(user *query.OIDCUserinfo) oidc.UserInfoEmail {
|
||||||
|
return oidc.UserInfoEmail{
|
||||||
|
Email: string(user.Email),
|
||||||
|
EmailVerified: oidc.Bool(user.IsEmailVerified),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func userInfoProfileToOidc(user *query.OIDCUserinfo) oidc.UserInfoProfile {
|
||||||
|
return oidc.UserInfoProfile{
|
||||||
|
Name: user.Name,
|
||||||
|
GivenName: user.FirstName,
|
||||||
|
FamilyName: user.LastName,
|
||||||
|
Nickname: user.NickName,
|
||||||
|
// Picture: domain.AvatarURL(o.assetAPIPrefix(ctx), user.ResourceOwner, user.Human.AvatarKey),
|
||||||
|
Gender: getGender(user.Gender),
|
||||||
|
Locale: oidc.NewLocale(user.PreferredLanguage),
|
||||||
|
UpdatedAt: oidc.FromTime(user.UpdatedAt),
|
||||||
|
// PreferredUsername: user.PreferredLoginName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func userInfoPhoneToOIDC(user *query.OIDCUserinfo) oidc.UserInfoPhone {
|
||||||
|
return oidc.UserInfoPhone{
|
||||||
|
PhoneNumber: string(user.Phone),
|
||||||
|
PhoneNumberVerified: user.IsPhoneVerified,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func userInfoAddressToOIDC(user *query.OIDCUserinfo) *oidc.UserInfoAddress {
|
||||||
|
return &oidc.UserInfoAddress{
|
||||||
|
// Formatted: ??,
|
||||||
|
StreetAddress: user.StreetAddress,
|
||||||
|
Locality: user.Locality,
|
||||||
|
Region: user.Region,
|
||||||
|
PostalCode: user.PostalCode,
|
||||||
|
Country: user.Country,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUserInfoOrgClaims(user *query.OIDCUserinfo, out *oidc.UserInfo) {
|
||||||
|
out.AppendClaims(ClaimResourceOwner+"id", user.OrgID)
|
||||||
|
out.AppendClaims(ClaimResourceOwner+"name", user.OrgName)
|
||||||
|
out.AppendClaims(ClaimResourceOwner+"primary_domain", user.OrgPrimaryDomain)
|
||||||
|
}
|
||||||
22
internal/domain/oidc_scopes.go
Normal file
22
internal/domain/oidc_scopes.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScopeOpenID = oidc.ScopeOpenID
|
||||||
|
ScopeProfile = oidc.ScopeProfile
|
||||||
|
ScopeEmail = oidc.ScopeEmail
|
||||||
|
ScopeAddress = oidc.ScopeAddress
|
||||||
|
ScopePhone = oidc.ScopePhone
|
||||||
|
ScopeOfflineAccess = oidc.ScopeOfflineAccess
|
||||||
|
|
||||||
|
ScopeProjectRolePrefix = "urn:zitadel:iam:org:project:role:"
|
||||||
|
ScopeProjectsRoles = "urn:zitadel:iam:org:projects:roles"
|
||||||
|
ClaimProjectRoles = "urn:zitadel:iam:org:project:roles"
|
||||||
|
ClaimProjectRolesFormat = "urn:zitadel:iam:org:project:%s:roles"
|
||||||
|
ScopeUserMetaData = "urn:zitadel:iam:user:metadata"
|
||||||
|
ClaimUserMetaData = ScopeUserMetaData
|
||||||
|
ScopeResourceOwner = "urn:zitadel:iam:user:resourceowner"
|
||||||
|
ClaimResourceOwner = ScopeResourceOwner + ":"
|
||||||
|
ClaimActionLogFormat = "urn:zitadel:iam:action:%s:log"
|
||||||
|
)
|
||||||
@@ -52,11 +52,8 @@ func (wm *OIDCSessionAccessTokenReadModel) Reduce() error {
|
|||||||
return wm.WriteModel.Reduce()
|
return wm.WriteModel.Reduce()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wm *OIDCSessionAccessTokenReadModel) Query() *eventstore.SearchQueryBuilder {
|
func (wm *OIDCSessionAccessTokenReadModel) addQuery(b *eventstore.SearchQueryBuilder) *eventstore.SearchQueryBuilder {
|
||||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
return b.AddQuery().
|
||||||
AwaitOpenTransactions().
|
|
||||||
AllowTimeTravel().
|
|
||||||
AddQuery().
|
|
||||||
AggregateTypes(oidcsession.AggregateType).
|
AggregateTypes(oidcsession.AggregateType).
|
||||||
AggregateIDs(wm.AggregateID).
|
AggregateIDs(wm.AggregateID).
|
||||||
EventTypes(
|
EventTypes(
|
||||||
@@ -68,6 +65,14 @@ func (wm *OIDCSessionAccessTokenReadModel) Query() *eventstore.SearchQueryBuilde
|
|||||||
Builder()
|
Builder()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wm *OIDCSessionAccessTokenReadModel) Query() *eventstore.SearchQueryBuilder {
|
||||||
|
return wm.addQuery(
|
||||||
|
eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||||
|
AwaitOpenTransactions().
|
||||||
|
AllowTimeTravel(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func (wm *OIDCSessionAccessTokenReadModel) reduceAdded(e *oidcsession.AddedEvent) {
|
func (wm *OIDCSessionAccessTokenReadModel) reduceAdded(e *oidcsession.AddedEvent) {
|
||||||
wm.UserID = e.UserID
|
wm.UserID = e.UserID
|
||||||
wm.SessionID = e.SessionID
|
wm.SessionID = e.SessionID
|
||||||
|
|||||||
254
internal/query/userinfo.go
Normal file
254
internal/query/userinfo.go
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/muhlemmer/gu"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/org"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/user"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (q *Queries) GetOIDCUserinfo(ctx context.Context, userID string, scope, roleAudience []string) (_ *OIDCUserinfo, err error) {
|
||||||
|
if slices.Contains(scope, domain.ScopeProjectsRoles) {
|
||||||
|
roleAudience = domain.AddAudScopeToAudience(ctx, roleAudience, scope)
|
||||||
|
// TODO: we need to get the project roles and user roles.
|
||||||
|
}
|
||||||
|
|
||||||
|
user := newOidcUserinfoReadModel(userID, scope)
|
||||||
|
if err = q.eventstore.FilterToQueryReducer(ctx, user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasOrgScope(scope) {
|
||||||
|
org := newoidcUserinfoOrganizationReadModel(user.ResourceOwner)
|
||||||
|
if err = q.eventstore.FilterToQueryReducer(ctx, org); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.OrgID = org.AggregateID
|
||||||
|
user.OrgName = org.Name
|
||||||
|
user.OrgPrimaryDomain = org.PrimaryDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user.OIDCUserinfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasOrgScope(scope []string) bool {
|
||||||
|
return slices.ContainsFunc(scope, func(s string) bool {
|
||||||
|
return s == domain.ScopeResourceOwner || strings.HasPrefix(s, domain.OrgIDScope)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type OIDCUserinfo struct {
|
||||||
|
ID string
|
||||||
|
UserName string
|
||||||
|
Name string
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
NickName string
|
||||||
|
PreferredLanguage language.Tag
|
||||||
|
Gender domain.Gender
|
||||||
|
Avatar string
|
||||||
|
UpdatedAt time.Time
|
||||||
|
|
||||||
|
Email domain.EmailAddress
|
||||||
|
IsEmailVerified bool
|
||||||
|
|
||||||
|
Phone domain.PhoneNumber
|
||||||
|
IsPhoneVerified bool
|
||||||
|
|
||||||
|
Country string
|
||||||
|
Locality string
|
||||||
|
PostalCode string
|
||||||
|
Region string
|
||||||
|
StreetAddress string
|
||||||
|
|
||||||
|
UserState domain.UserState
|
||||||
|
UserType domain.UserType
|
||||||
|
|
||||||
|
OrgID string
|
||||||
|
OrgName string
|
||||||
|
OrgPrimaryDomain string
|
||||||
|
|
||||||
|
Metadata map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type oidcUserinfoReadmodel struct {
|
||||||
|
eventstore.ReadModel
|
||||||
|
scope []string // Scope is used to determine events
|
||||||
|
OIDCUserinfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOidcUserinfoReadModel(userID string, scope []string) *oidcUserinfoReadmodel {
|
||||||
|
return &oidcUserinfoReadmodel{
|
||||||
|
ReadModel: eventstore.ReadModel{
|
||||||
|
AggregateID: userID,
|
||||||
|
},
|
||||||
|
scope: scope,
|
||||||
|
OIDCUserinfo: OIDCUserinfo{
|
||||||
|
ID: userID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rm *oidcUserinfoReadmodel) Query() *eventstore.SearchQueryBuilder {
|
||||||
|
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||||
|
AwaitOpenTransactions().
|
||||||
|
AllowTimeTravel().
|
||||||
|
AddQuery().
|
||||||
|
AggregateTypes(user.AggregateType).
|
||||||
|
AggregateIDs(rm.AggregateID).
|
||||||
|
EventTypes(rm.scopeToEventTypes()...).
|
||||||
|
Builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
// scopeToEventTypes sets required user events to obtain get the correct userinfo.
|
||||||
|
// Events such as UserLocked, UserDeactivated and UserRemoved are not checked,
|
||||||
|
// as access tokens should already be revoked.
|
||||||
|
func (rm *oidcUserinfoReadmodel) scopeToEventTypes() []eventstore.EventType {
|
||||||
|
types := make([]eventstore.EventType, 0, len(rm.scope))
|
||||||
|
types = append(types, user.HumanAddedType, user.MachineAddedEventType)
|
||||||
|
|
||||||
|
for _, scope := range rm.scope {
|
||||||
|
switch scope {
|
||||||
|
case domain.ScopeEmail:
|
||||||
|
types = append(types, user.HumanEmailChangedType, user.HumanEmailVerifiedType)
|
||||||
|
case domain.ScopeProfile:
|
||||||
|
types = append(types, user.HumanProfileChangedType, user.HumanAvatarAddedType, user.HumanAvatarRemovedType)
|
||||||
|
case domain.ScopePhone:
|
||||||
|
types = append(types, user.HumanPhoneChangedType, user.HumanPhoneVerifiedType, user.HumanPhoneRemovedType)
|
||||||
|
case domain.ScopeAddress:
|
||||||
|
types = append(types, user.HumanAddressChangedType)
|
||||||
|
case domain.ScopeUserMetaData:
|
||||||
|
types = append(types, user.MetadataSetType, user.MetadataRemovedType, user.MetadataRemovedAllType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slices.Compact(types)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rm *oidcUserinfoReadmodel) Reduce() error {
|
||||||
|
for _, event := range rm.Events {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *user.HumanAddedEvent:
|
||||||
|
rm.UserName = e.UserName
|
||||||
|
rm.FirstName = e.FirstName
|
||||||
|
rm.LastName = e.LastName
|
||||||
|
rm.NickName = e.NickName
|
||||||
|
rm.Name = e.DisplayName
|
||||||
|
rm.PreferredLanguage = e.PreferredLanguage
|
||||||
|
rm.Gender = e.Gender
|
||||||
|
rm.Email = e.EmailAddress
|
||||||
|
rm.Phone = e.PhoneNumber
|
||||||
|
rm.Country = e.Country
|
||||||
|
rm.Locality = e.Locality
|
||||||
|
rm.PostalCode = e.PostalCode
|
||||||
|
rm.Region = e.Region
|
||||||
|
rm.StreetAddress = e.StreetAddress
|
||||||
|
rm.UpdatedAt = e.Creation
|
||||||
|
case *user.MachineAddedEvent:
|
||||||
|
rm.UserName = e.UserName
|
||||||
|
rm.Name = e.Name
|
||||||
|
rm.UpdatedAt = e.Creation
|
||||||
|
case *user.HumanEmailChangedEvent:
|
||||||
|
rm.Email = e.EmailAddress
|
||||||
|
rm.IsEmailVerified = false
|
||||||
|
rm.UpdatedAt = e.Creation
|
||||||
|
case *user.HumanEmailVerifiedEvent:
|
||||||
|
rm.IsEmailVerified = e.IsEmailVerified
|
||||||
|
rm.UpdatedAt = e.Creation
|
||||||
|
case *user.HumanProfileChangedEvent:
|
||||||
|
rm.FirstName = e.FirstName
|
||||||
|
rm.LastName = e.LastName
|
||||||
|
rm.NickName = gu.Value(e.NickName)
|
||||||
|
rm.Name = gu.Value(e.DisplayName)
|
||||||
|
rm.PreferredLanguage = gu.Value(e.PreferredLanguage)
|
||||||
|
rm.Gender = gu.Value(e.Gender)
|
||||||
|
rm.UpdatedAt = e.Creation
|
||||||
|
case *user.HumanAvatarAddedEvent:
|
||||||
|
rm.Avatar = e.StoreKey
|
||||||
|
rm.UpdatedAt = e.Creation
|
||||||
|
case *user.HumanAvatarRemovedEvent:
|
||||||
|
rm.Avatar = ""
|
||||||
|
rm.UpdatedAt = e.Creation
|
||||||
|
case *user.HumanPhoneChangedEvent:
|
||||||
|
rm.Phone = e.PhoneNumber
|
||||||
|
rm.IsPhoneVerified = false
|
||||||
|
rm.UpdatedAt = e.Creation
|
||||||
|
case *user.HumanPhoneVerifiedEvent:
|
||||||
|
rm.IsEmailVerified = e.IsPhoneVerified
|
||||||
|
rm.UpdatedAt = e.Creation
|
||||||
|
case *user.HumanPhoneRemovedEvent:
|
||||||
|
rm.Phone = ""
|
||||||
|
rm.IsPhoneVerified = false
|
||||||
|
rm.UpdatedAt = e.Creation
|
||||||
|
case *user.HumanAddressChangedEvent:
|
||||||
|
rm.Country = gu.Value(e.Country)
|
||||||
|
rm.Locality = gu.Value(e.Locality)
|
||||||
|
rm.PostalCode = gu.Value(e.PostalCode)
|
||||||
|
rm.Region = gu.Value(e.Region)
|
||||||
|
rm.StreetAddress = gu.Value(e.StreetAddress)
|
||||||
|
rm.UpdatedAt = e.Creation
|
||||||
|
case *user.MetadataSetEvent:
|
||||||
|
rm.Metadata[e.Key] = base64.RawURLEncoding.EncodeToString(e.Value)
|
||||||
|
rm.UpdatedAt = e.Creation
|
||||||
|
case *user.MetadataRemovedEvent:
|
||||||
|
delete(rm.Metadata, e.Key)
|
||||||
|
rm.UpdatedAt = e.Creation
|
||||||
|
case *user.MetadataRemovedAllEvent:
|
||||||
|
for key := range rm.Metadata {
|
||||||
|
delete(rm.Metadata, key)
|
||||||
|
}
|
||||||
|
rm.UpdatedAt = e.Creation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rm.ReadModel.Reduce()
|
||||||
|
}
|
||||||
|
|
||||||
|
type oidcUserinfoOrganizationReadModel struct {
|
||||||
|
eventstore.ReadModel
|
||||||
|
|
||||||
|
Name string
|
||||||
|
PrimaryDomain string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newoidcUserinfoOrganizationReadModel(orgID string) *oidcUserinfoOrganizationReadModel {
|
||||||
|
return &oidcUserinfoOrganizationReadModel{
|
||||||
|
ReadModel: eventstore.ReadModel{
|
||||||
|
AggregateID: orgID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rm *oidcUserinfoOrganizationReadModel) Query() *eventstore.SearchQueryBuilder {
|
||||||
|
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||||
|
AwaitOpenTransactions().
|
||||||
|
AllowTimeTravel().
|
||||||
|
AddQuery().
|
||||||
|
AggregateTypes(org.AggregateType).
|
||||||
|
AggregateIDs(rm.AggregateID).
|
||||||
|
EventTypes(org.OrgAddedEventType, org.OrgChangedEventType, org.OrgDomainPrimarySetEventType).
|
||||||
|
Builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rm *oidcUserinfoOrganizationReadModel) Reduce() error {
|
||||||
|
for _, event := range rm.Events {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *org.OrgAddedEvent:
|
||||||
|
rm.Name = e.Name
|
||||||
|
case *org.OrgChangedEvent:
|
||||||
|
rm.Name = e.Name
|
||||||
|
case *org.DomainPrimarySetEvent:
|
||||||
|
rm.PrimaryDomain = e.Domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rm.ReadModel.Reduce()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user