ZITADEL is missing enforcement of organization scopes
Description
ZITADEL is an open source identity management platform. Versions prior to 3.4.9 and 4.0.0 through 4.12.2 allowed users to bypass organization enforcement during authentication. Zitadel allows applications to enforce an organzation context during authentication using scopes (urn:zitadel:iam:org:id:{id} and urn:zitadel:iam:org:domain:primary:{domainname}). If enforced, a user needs to be part of the required organization to sign in. While this was properly enforced for OAuth2/OIDC authorization requests in login V1, corresponding controls were missing for device authorization requests and all login V2 and OIDC API V2 endpoints. This allowed users to bypass the restriction and sign in with users from other organizations. Note that this enforcement allows for an additional check during authentication and applications relying on authorizations / roles assignments are not affected by this bypass. This issue has been patched in versions 3.4.9 and 4.12.3.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/zitadel/zitadelGo | >= 4.0.0-rc.1, < 4.12.3 | 4.12.3 |
github.com/zitadel/zitadelGo | >= 3.0.0-rc.1, < 3.4.9 | 3.4.9 |
github.com/zitadel/zitadelGo | < 1.80.0-v2.20.0.20260317120401-d90285929ca0 | 1.80.0-v2.20.0.20260317120401-d90285929ca0 |
Affected products
1Patches
1d90285929ca0fix: enforce organization scopes
21 files changed · +730 −118
apps/login/src/app/(login)/accounts/page.tsx+20 −7 modified@@ -17,13 +17,27 @@ export async function generateMetadata(): Promise<Metadata> { return { title: t("title") }; } -async function loadSessions({ serviceConfig }: { serviceConfig: ServiceConfig }) { +async function loadSessions({ + serviceConfig, + organization, +}: { + serviceConfig: ServiceConfig; + organization?: string; +}) { const cookieIds = await getAllSessionCookieIds(); if (cookieIds && cookieIds.length) { - const response = await listSessions({ serviceConfig, ids: cookieIds.filter((id) => !!id) as string[], + const response = await listSessions({ + serviceConfig, + ids: cookieIds.filter((id) => !!id) as string[], }); - return response?.sessions ?? []; + + let sessions = response?.sessions ?? []; + if (organization) { + sessions = sessions.filter((s) => s.factors?.user?.organizationId === organization); + } + + return sessions; } else { console.info("No session cookie found."); return []; @@ -41,16 +55,15 @@ export default async function Page(props: { searchParams: Promise<Record<string let defaultOrganization; if (!organization) { - const org: Organization | null = await getDefaultOrg({ serviceConfig, }); + const org: Organization | null = await getDefaultOrg({ serviceConfig }); if (org) { defaultOrganization = org.id; } } - let sessions = await loadSessions({ serviceConfig }); + let sessions = await loadSessions({ serviceConfig, organization }); - const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization, - }); + const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization }); const params = new URLSearchParams();
apps/login/src/lib/server/flow-initiation.ts+2 −2 modified@@ -258,7 +258,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr } else if (authRequest.prompt.includes(Prompt.NONE)) { const securitySettings = await getSecuritySettings({ serviceConfig }); - const selectedSession = await findValidSession({ serviceConfig, sessions, authRequest }); + const selectedSession = await findValidSession({ serviceConfig, sessions, authRequest, organization }); const noSessionResponse = NextResponse.json({ error: "No active session found" }, { status: 400 }); setCSPHeaders(noSessionResponse, serviceConfig, securitySettings); @@ -294,7 +294,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr return callbackResponse; } else { - let selectedSession = await findValidSession({ serviceConfig, sessions, authRequest }); + let selectedSession = await findValidSession({ serviceConfig, sessions, authRequest, organization }); if (!selectedSession || !selectedSession.id) { return gotoAccounts({
apps/login/src/lib/session.ts+7 −1 modified@@ -142,13 +142,15 @@ export async function findValidSession({ sessions, authRequest, samlRequest, + organization, }: { serviceConfig: ServiceConfig; sessions: Session[]; authRequest?: AuthRequest; samlRequest?: SAMLRequest; + organization?: string; }): Promise<Session | undefined> { - const sessionsWithHint = sessions.filter((s) => { + let sessionsWithHint = sessions.filter((s) => { if (authRequest && authRequest.hintUserId) { return s.factors?.user?.id === authRequest.hintUserId; } @@ -163,6 +165,10 @@ export async function findValidSession({ return true; }); + if (organization) { + sessionsWithHint = sessionsWithHint.filter((s) => s.factors?.user?.organizationId === organization); + } + if (sessionsWithHint.length === 0) { return undefined; }
internal/api/grpc/oidc/v2/integration_test/oidc_test.go+106 −6 modified@@ -15,6 +15,7 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/app" filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" @@ -163,6 +164,25 @@ func TestServer_CreateCallback(t *testing.T) { }, wantErr: true, }, + { + name: "invalid organization", + ctx: CTXLoginClient, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + org := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()) + _, authRequestID, err := Instance.CreateOIDCAuthRequest(CTXLoginClient, client.GetClientId(), Instance.Users.Get(integration.UserTypeLogin).ID, redirectURI, oidc.ScopeOpenID, domain.OrgIDScope+org.OrganizationId) + require.NoError(t, err) + return authRequestID + }(), + CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + }, + }, + wantErr: true, + }, { name: "fail callback", ctx: CTXLoginClient, @@ -356,6 +376,31 @@ func TestServer_CreateCallback(t *testing.T) { }, wantErr: false, }, + { + name: "callback with required organization", + ctx: CTXLoginClient, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + _, authRequestID, err := Instance.CreateOIDCAuthRequest(CTXLoginClient, client.GetClientId(), Instance.Users.Get(integration.UserTypeLogin).ID, redirectURI, oidc.ScopeOpenID, domain.OrgIDScope+Instance.DefaultOrg.Id) + require.NoError(t, err) + return authRequestID + }(), + CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + }, + }, + want: &oidc_pb.CreateCallbackResponse{ + CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`, + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Instance.ID(), + }, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -749,7 +794,7 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { ctx: CTXLoginClient, req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ DeviceAuthorizationId: func() string { - req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") + req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), oidc.ScopeOpenID) require.NoError(t, err) var id string retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) @@ -776,7 +821,7 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { ctx: CTXLoginClient, req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ DeviceAuthorizationId: func() string { - req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") + req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), oidc.ScopeOpenID) require.NoError(t, err) var id string retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) @@ -798,12 +843,40 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { }, wantErr: true, }, + { + name: "invalid organization", + ctx: CTXLoginClient, + req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ + DeviceAuthorizationId: func() string { + org := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()) + req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), oidc.ScopeOpenID, domain.OrgIDScope+org.OrganizationId) + require.NoError(t, err) + var id string + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) + require.EventuallyWithT(t, func(collectT *assert.CollectT) { + resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTXLoginClient, &oidc_pb.GetDeviceAuthorizationRequestRequest{ + UserCode: req.UserCode, + }) + assert.NoError(collectT, err) + id = resp.GetDeviceAuthorizationRequest().GetId() + }, retryDuration, tick) + return id + }(), + Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + }, + }, + wantErr: true, + }, { name: "deny device authorization", ctx: CTXLoginClient, req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ DeviceAuthorizationId: func() string { - req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") + req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), oidc.ScopeOpenID) require.NoError(t, err) var id string retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) @@ -826,7 +899,7 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { ctx: CTX, req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ DeviceAuthorizationId: func() string { - req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") + req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), oidc.ScopeOpenID) require.NoError(t, err) var id string retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) @@ -849,7 +922,7 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { ctx: CTX, req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ DeviceAuthorizationId: func() string { - req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") + req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), oidc.ScopeOpenID) require.NoError(t, err) var id string retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) @@ -876,7 +949,34 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { ctx: CTXLoginClient, req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ DeviceAuthorizationId: func() string { - req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") + req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), oidc.ScopeOpenID) + require.NoError(t, err) + var id string + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) + require.EventuallyWithT(t, func(collectT *assert.CollectT) { + resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTXLoginClient, &oidc_pb.GetDeviceAuthorizationRequestRequest{ + UserCode: req.UserCode, + }) + assert.NoError(collectT, err) + id = resp.GetDeviceAuthorizationRequest().GetId() + }, retryDuration, tick) + return id + }(), + Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + }, + }, + wantErr: false, + }, + { + name: "authorize, with organization scope", + ctx: CTXLoginClient, + req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ + DeviceAuthorizationId: func() string { + req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), oidc.ScopeOpenID, domain.OrgIDScope+Instance.DefaultOrg.GetId()) require.NoError(t, err) var id string retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute)
internal/api/oidc/auth_request_converter.go+7 −7 modified@@ -119,13 +119,13 @@ func CreateAuthRequestToBusiness(ctx context.Context, authReq *oidc.AuthRequest, UserID: userID, InstanceID: authz.GetInstance(ctx).InstanceID(), Audience: audience, - Request: &domain.AuthRequestOIDC{ - Scopes: authReq.Scopes, - ResponseType: ResponseTypeToBusiness(authReq.ResponseType), - ResponseMode: ResponseModeToBusiness(authReq.ResponseMode), - Nonce: authReq.Nonce, - CodeChallenge: CodeChallengeToBusiness(authReq.CodeChallenge, authReq.CodeChallengeMethod), - }, + Request: domain.NewAuthRequestOIDC( + authReq.Scopes, + ResponseTypeToBusiness(authReq.ResponseType), + ResponseModeToBusiness(authReq.ResponseMode), + authReq.Nonce, + CodeChallengeToBusiness(authReq.CodeChallenge, authReq.CodeChallengeMethod), + ), } }
internal/api/oidc/auth_request.go+48 −7 modified@@ -80,25 +80,31 @@ func (o *OPStorage) CreateAuthRequest(ctx context.Context, req *oidc.AuthRequest } } -func (o *OPStorage) createAuthRequestScopeAndAudience(ctx context.Context, clientID string, reqScope []string) (scope, audience []string, err error) { +func (o *OPStorage) createAuthRequestScopeAndAudience(ctx context.Context, clientID string, reqScope []string) (scope, audience []string, orgID string, err error) { project, err := o.query.ProjectByClientID(ctx, clientID) if err != nil { - return nil, nil, err + return nil, nil, "", err } + + orgID, err = o.assertOrgScope(ctx, reqScope) + if err != nil { + return nil, nil, "", err + } + scope, err = o.assertProjectRoleScopesByProject(ctx, project, reqScope) if err != nil { - return nil, nil, err + return nil, nil, "", err } audience, err = o.audienceFromProjectID(ctx, project.ID) audience = domain.AddAudScopeToAudience(ctx, audience, scope) if err != nil { - return nil, nil, err + return nil, nil, "", err } - return scope, audience, nil + return scope, audience, orgID, nil } func (o *OPStorage) createAuthRequestLoginClient(ctx context.Context, req *oidc.AuthRequest, hintUserID, loginClient string) (op.AuthRequest, error) { - scope, audience, err := o.createAuthRequestScopeAndAudience(ctx, req.ClientID, req.Scopes) + scope, audience, orgID, err := o.createAuthRequestScopeAndAudience(ctx, req.ClientID, req.Scopes) if err != nil { return nil, err } @@ -118,6 +124,7 @@ func (o *OPStorage) createAuthRequestLoginClient(ctx context.Context, req *oidc. UILocales: UILocalesToBusiness(req.UILocales), MaxAge: MaxAgeToBusiness(req.MaxAge), Issuer: o.contextToIssuer(ctx), + OrganizationID: orgID, } if req.LoginHint != "" { authRequest.LoginHint = &req.LoginHint @@ -138,7 +145,8 @@ func (o *OPStorage) createAuthRequest(ctx context.Context, req *oidc.AuthRequest if !ok { return nil, zerrors.ThrowPreconditionFailed(nil, "OIDC-sd436", "no user agent id") } - scope, audience, err := o.createAuthRequestScopeAndAudience(ctx, req.ClientID, req.Scopes) + // we do not need to handle the orgID for the v1 login, since it handles it already + scope, audience, _, err := o.createAuthRequestScopeAndAudience(ctx, req.ClientID, req.Scopes) if err != nil { return nil, err } @@ -530,6 +538,39 @@ func (o *OPStorage) GetRefreshTokenInfo(ctx context.Context, clientID string, to return refreshToken.UserID, refreshToken.ID, nil } +// assertOrgScope checks the scopes for organization scopes and returns the orgID if exactly one org scope is found. +// If multiple org scopes are found or if the org scope is invalid, an error is returned. +// For backwards compatibility, we support both orgID and orgDomain scopes, but they need to be consistent, +// meaning that if both are provided, they need to belong to the same organization. +func (o *OPStorage) assertOrgScope(ctx context.Context, scopes []string) (string, error) { + var id string + for _, scope := range scopes { + if orgID, ok := strings.CutPrefix(scope, domain.OrgIDScope); ok { + org, err := o.query.OrgByID(ctx, orgID) + if err != nil { + return "", err + } + if id != "" && id != org.ID { + return "", oidc.ErrInvalidScope().WithDescription("Only one organization scope may be provided") + } + id = org.ID + continue + } + + if orgDomain, ok := strings.CutPrefix(scope, domain.OrgDomainPrimaryScope); ok { + org, err := o.query.OrgByPrimaryDomain(ctx, orgDomain) + if err != nil { + return "", err + } + if id != "" && id != org.ID { + return "", oidc.ErrInvalidScope().WithDescription("Only one organization scope may be provided") + } + id = org.ID + } + } + return id, nil +} + func (o *OPStorage) assertProjectRoleScopesByProject(ctx context.Context, project *query.Project, scopes []string) ([]string, error) { for _, scope := range scopes { if strings.HasPrefix(scope, ScopeProjectRolePrefix) {
internal/api/oidc/device_auth.go+2 −2 modified@@ -76,11 +76,11 @@ func (o *OPStorage) StoreDeviceAuthorization(ctx context.Context, clientID, devi logger.OnError(err).Error(logMsg) span.EndWithError(err) }() - scope, audience, err := o.createAuthRequestScopeAndAudience(ctx, clientID, scope) + scope, audience, orgID, err := o.createAuthRequestScopeAndAudience(ctx, clientID, scope) if err != nil { return err } - details, err := o.command.AddDeviceAuth(ctx, clientID, deviceCode, userCode, expires, scope, audience, slices.Contains(scope, oidc.ScopeOfflineAccess)) + details, err := o.command.AddDeviceAuth(ctx, clientID, deviceCode, userCode, orgID, expires, scope, audience, slices.Contains(scope, oidc.ScopeOfflineAccess)) if err == nil { logger.SetFields("details", details).Debug(logMsg) }
internal/api/oidc/integration_test/auth_request_test.go+15 −0 modified@@ -17,6 +17,7 @@ import ( http_utils "github.com/zitadel/zitadel/internal/api/http" oidc_api "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" "github.com/zitadel/zitadel/pkg/grpc/session/v2" @@ -36,6 +37,20 @@ func TestOPStorage_CreateAuthRequest(t *testing.T) { id2 := createAuthRequestNoLoginClientHeader(t, Instance, clientIDV2, redirectURI) require.Contains(t, id2, command.IDPrefixV2) + + // valid org scope must succeed + _, _, err := Instance.CreateOIDCAuthRequest(CTX, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, redirectURI, oidc.ScopeOpenID, domain.OrgIDScope+Instance.DefaultOrg.Id) + require.NoError(t, err) + + _, _, err = Instance.CreateOIDCAuthRequest(CTX, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, redirectURI, oidc.ScopeOpenID, domain.OrgIDScope+Instance.DefaultOrg.Id) + require.NoError(t, err) + + // invalid org scope must fail + _, _, err = Instance.CreateOIDCAuthRequest(CTX, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, redirectURI, oidc.ScopeOpenID, domain.OrgIDScope+"invalid") + require.Error(t, err) + + _, _, err = Instance.CreateOIDCAuthRequest(CTX, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, redirectURI, oidc.ScopeOpenID, domain.OrgIDScope+"invalid") + require.Error(t, err) } func TestOPStorage_CreateAccessToken_code(t *testing.T) {
internal/command/auth_request.go+23 −17 modified@@ -30,6 +30,7 @@ type AuthRequest struct { HintUserID *string NeedRefreshToken bool Issuer string + OrganizationID string } type CurrentAuthRequest struct { @@ -75,6 +76,7 @@ func (c *Commands) AddAuthRequest(ctx context.Context, authRequest *AuthRequest) authRequest.HintUserID, authRequest.NeedRefreshToken, authRequest.Issuer, + authRequest.OrganizationID, )) if err != nil { return nil, err @@ -116,6 +118,9 @@ func (c *Commands) LinkSessionToAuthRequest(ctx context.Context, id, sessionID, return nil, nil, err } } + if writeModel.OrganizationID != "" && writeModel.OrganizationID != sessionWriteModel.UserResourceOwner { + return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-59ljd", "Errors.User.NotAllowedOrg") + } if err := c.pushAppendAndReduce(ctx, writeModel, authrequest.NewSessionLinkedEvent( ctx, &authrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate, @@ -171,23 +176,24 @@ func (c *Commands) AddAuthRequestCode(ctx context.Context, authRequestID, code s func authRequestWriteModelToCurrentAuthRequest(writeModel *AuthRequestWriteModel) (_ *CurrentAuthRequest) { return &CurrentAuthRequest{ AuthRequest: &AuthRequest{ - ID: writeModel.AggregateID, - LoginClient: writeModel.LoginClient, - ClientID: writeModel.ClientID, - RedirectURI: writeModel.RedirectURI, - State: writeModel.State, - Nonce: writeModel.Nonce, - Scope: writeModel.Scope, - Audience: writeModel.Audience, - ResponseType: writeModel.ResponseType, - ResponseMode: writeModel.ResponseMode, - CodeChallenge: writeModel.CodeChallenge, - Prompt: writeModel.Prompt, - UILocales: writeModel.UILocales, - MaxAge: writeModel.MaxAge, - LoginHint: writeModel.LoginHint, - HintUserID: writeModel.HintUserID, - Issuer: writeModel.Issuer, + ID: writeModel.AggregateID, + LoginClient: writeModel.LoginClient, + ClientID: writeModel.ClientID, + RedirectURI: writeModel.RedirectURI, + State: writeModel.State, + Nonce: writeModel.Nonce, + Scope: writeModel.Scope, + Audience: writeModel.Audience, + ResponseType: writeModel.ResponseType, + ResponseMode: writeModel.ResponseMode, + CodeChallenge: writeModel.CodeChallenge, + Prompt: writeModel.Prompt, + UILocales: writeModel.UILocales, + MaxAge: writeModel.MaxAge, + LoginHint: writeModel.LoginHint, + HintUserID: writeModel.HintUserID, + Issuer: writeModel.Issuer, + OrganizationID: writeModel.OrganizationID, }, SessionID: writeModel.SessionID, UserID: writeModel.UserID,
internal/command/auth_request_model.go+2 −0 modified@@ -37,6 +37,7 @@ type AuthRequestWriteModel struct { AuthRequestState domain.AuthRequestState NeedRefreshToken bool Issuer string + OrganizationID string } func NewAuthRequestWriteModel(ctx context.Context, id string) *AuthRequestWriteModel { @@ -70,6 +71,7 @@ func (m *AuthRequestWriteModel) Reduce() error { m.AuthRequestState = domain.AuthRequestStateAdded m.NeedRefreshToken = e.NeedRefreshToken m.Issuer = e.Issuer + m.OrganizationID = e.OrganizationID case *authrequest.SessionLinkedEvent: m.SessionID = e.SessionID m.UserID = e.UserID
internal/command/auth_request_test.go+187 −12 modified@@ -63,6 +63,7 @@ func TestCommands_AddAuthRequest(t *testing.T) { nil, false, "issuer", + "", ), ), ), @@ -103,6 +104,7 @@ func TestCommands_AddAuthRequest(t *testing.T) { gu.Ptr("hintUserID"), false, "issuer", + "organizationID", ), ), ), @@ -124,12 +126,13 @@ func TestCommands_AddAuthRequest(t *testing.T) { Challenge: "challenge", Method: domain.CodeChallengeMethodS256, }, - Prompt: []domain.Prompt{domain.PromptNone}, - UILocales: []string{"en", "de"}, - MaxAge: gu.Ptr(time.Duration(0)), - LoginHint: gu.Ptr("loginHint"), - HintUserID: gu.Ptr("hintUserID"), - Issuer: "issuer", + Prompt: []domain.Prompt{domain.PromptNone}, + UILocales: []string{"en", "de"}, + MaxAge: gu.Ptr(time.Duration(0)), + LoginHint: gu.Ptr("loginHint"), + HintUserID: gu.Ptr("hintUserID"), + Issuer: "issuer", + OrganizationID: "organizationID", }, }, &CurrentAuthRequest{ @@ -148,12 +151,13 @@ func TestCommands_AddAuthRequest(t *testing.T) { Challenge: "challenge", Method: domain.CodeChallengeMethodS256, }, - Prompt: []domain.Prompt{domain.PromptNone}, - UILocales: []string{"en", "de"}, - MaxAge: gu.Ptr(time.Duration(0)), - LoginHint: gu.Ptr("loginHint"), - HintUserID: gu.Ptr("hintUserID"), - Issuer: "issuer", + Prompt: []domain.Prompt{domain.PromptNone}, + UILocales: []string{"en", "de"}, + MaxAge: gu.Ptr(time.Duration(0)), + LoginHint: gu.Ptr("loginHint"), + HintUserID: gu.Ptr("hintUserID"), + Issuer: "issuer", + OrganizationID: "organizationID", }, }, nil, @@ -239,6 +243,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), eventFromEventPusher( @@ -282,6 +287,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -324,6 +330,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -364,6 +371,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -427,6 +435,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -455,6 +464,71 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { wantErr: zerrors.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid"), }, }, + { + "invalid organization", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate, + "loginClient", + "clientID", + "redirectURI", + "state", + "nonce", + []string{"openid"}, + []string{"audience"}, + domain.OIDCResponseTypeCode, + domain.OIDCResponseModeQuery, + nil, + nil, + nil, + nil, + nil, + nil, + true, + "issuer", + "organizationID", + ), + ), + ), + expectFilter( + eventFromEventPusher( + session.NewAddedEvent(mockCtx, + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), + eventFromEventPusher( + session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "org1", testNow, &language.Afrikaans), + ), + eventFromEventPusher( + session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + testNow), + ), + eventFromEventPusherWithCreationDateNow( + session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + 2*time.Minute), + ), + ), + ), + tokenVerifier: newMockTokenVerifierValid(), + }, + args{ + ctx: mockCtx, + id: "V2_id", + sessionID: "sessionID", + sessionToken: "token", + }, + res{ + wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-59ljd", "Errors.User.NotAllowedOrg"), + }, + }, { "linked", fields{ @@ -479,6 +553,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -545,6 +620,98 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { }, }, }, + { + "linked with organization check", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate, + "loginClient", + "clientID", + "redirectURI", + "state", + "nonce", + []string{"openid"}, + []string{"audience"}, + domain.OIDCResponseTypeCode, + domain.OIDCResponseModeQuery, + nil, + nil, + nil, + nil, + nil, + nil, + true, + "issuer", + "org1", + ), + ), + ), + expectFilter( + eventFromEventPusher( + session.NewAddedEvent(mockCtx, + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), + eventFromEventPusher( + session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "org1", testNow, &language.Afrikaans), + ), + eventFromEventPusher( + session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + testNow), + ), + eventFromEventPusherWithCreationDateNow( + session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + 2*time.Minute), + ), + ), + expectPush( + authrequest.NewSessionLinkedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate, + "sessionID", + "userID", + testNow, + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + ), + ), + ), + tokenVerifier: newMockTokenVerifierValid(), + }, + args{ + ctx: mockCtx, + id: "V2_id", + sessionID: "sessionID", + sessionToken: "token", + }, + res{ + details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, + authReq: &CurrentAuthRequest{ + AuthRequest: &AuthRequest{ + ID: "V2_id", + LoginClient: "loginClient", + ClientID: "clientID", + RedirectURI: "redirectURI", + State: "state", + Nonce: "nonce", + Scope: []string{"openid"}, + Audience: []string{"audience"}, + ResponseType: domain.OIDCResponseTypeCode, + ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", + OrganizationID: "org1", + }, + SessionID: "sessionID", + UserID: "userID", + AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + }, + }, + }, { "linked with login client check", fields{ @@ -569,6 +736,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -660,6 +828,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -752,6 +921,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -845,6 +1015,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -970,6 +1141,7 @@ func TestCommands_FailAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -1009,6 +1181,7 @@ func TestCommands_FailAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -1113,6 +1286,7 @@ func TestCommands_AddAuthRequestCode(t *testing.T) { gu.Ptr("hintUserID"), true, "issuer", + "", ), ), ), @@ -1152,6 +1326,7 @@ func TestCommands_AddAuthRequestCode(t *testing.T) { gu.Ptr("hintUserID"), true, "issuer", + "", ), ), eventFromEventPusher(
internal/command/device_auth.go+8 −1 modified@@ -15,7 +15,7 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddDeviceAuth(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes, audience []string, needRefreshToken bool) (*domain.ObjectDetails, error) { +func (c *Commands) AddDeviceAuth(ctx context.Context, clientID, deviceCode, userCode, orgID string, expires time.Time, scopes, audience []string, needRefreshToken bool) (*domain.ObjectDetails, error) { aggr := deviceauth.NewAggregate(deviceCode, authz.GetInstance(ctx).InstanceID()) model := NewDeviceAuthWriteModel(deviceCode, aggr.ResourceOwner) @@ -29,6 +29,7 @@ func (c *Commands) AddDeviceAuth(ctx context.Context, clientID, deviceCode, user scopes, audience, needRefreshToken, + orgID, )) if err != nil { return nil, err @@ -62,6 +63,9 @@ func (c *Commands) ApproveDeviceAuth( if model.State != domain.DeviceAuthStateInitiated { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-GEJL3", "Errors.DeviceAuth.AlreadyHandled") } + if model.OrganizationID != "" && model.OrganizationID != userOrgID { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-3tgws", "Errors.User.NotAllowedOrg") + } pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(ctx, model.aggregate, userID, userOrgID, authMethods, authTime, preferredLanguage, userAgent, sessionID)) if err != nil { return nil, err @@ -105,6 +109,9 @@ func (c *Commands) ApproveDeviceAuthWithSession( if err := c.sessionTokenVerifier(ctx, sessionToken, sessionWriteModel.AggregateID, sessionWriteModel.TokenID); err != nil { return nil, err } + if model.OrganizationID != "" && model.OrganizationID != sessionWriteModel.UserResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-D2j34", "Errors.User.NotAllowedOrg") + } pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent( ctx,
internal/command/device_auth_model.go+2 −0 modified@@ -29,6 +29,7 @@ type DeviceAuthWriteModel struct { UserAgent *domain.UserAgent NeedRefreshToken bool SessionID string + OrganizationID string } func NewDeviceAuthWriteModel(deviceCode, resourceOwner string) *DeviceAuthWriteModel { @@ -53,6 +54,7 @@ func (m *DeviceAuthWriteModel) Reduce() error { m.Audience = e.Audience m.State = e.State m.NeedRefreshToken = e.NeedRefreshToken + m.OrganizationID = e.OrganizationID case *deviceauth.ApprovedEvent: m.State = domain.DeviceAuthStateApproved m.UserID = e.UserID
internal/command/device_auth_test.go+227 −24 modified@@ -49,6 +49,7 @@ func TestCommands_AddDeviceAuth(t *testing.T) { scopes []string audience []string needRefreshToken bool + organizationID string } tests := []struct { name string @@ -66,7 +67,7 @@ func TestCommands_AddDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "orgID", ), )), }, @@ -79,6 +80,7 @@ func TestCommands_AddDeviceAuth(t *testing.T) { scopes: []string{"a", "b", "c"}, audience: []string{"projectID", "clientID"}, needRefreshToken: true, + organizationID: "orgID", }, wantDetails: &domain.ObjectDetails{ ResourceOwner: "instance1", @@ -93,7 +95,7 @@ func TestCommands_AddDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, false, + []string{"projectID", "clientID"}, false, "orgID", )), ), }, @@ -106,6 +108,7 @@ func TestCommands_AddDeviceAuth(t *testing.T) { scopes: []string{"a", "b", "c"}, audience: []string{"projectID", "clientID"}, needRefreshToken: false, + organizationID: "orgID", }, wantErr: pushErr, }, @@ -115,7 +118,17 @@ func TestCommands_AddDeviceAuth(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore(t), } - gotDetails, err := c.AddDeviceAuth(tt.args.ctx, tt.args.clientID, tt.args.deviceCode, tt.args.userCode, tt.args.expires, tt.args.scopes, tt.args.audience, tt.args.needRefreshToken) + gotDetails, err := c.AddDeviceAuth( + tt.args.ctx, + tt.args.clientID, + tt.args.deviceCode, + tt.args.userCode, + tt.args.organizationID, + tt.args.expires, + tt.args.scopes, + tt.args.audience, + tt.args.needRefreshToken, + ) require.ErrorIs(t, err, tt.wantErr) assertObjectDetails(t, tt.wantDetails, gotDetails) }) @@ -168,6 +181,35 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { }, wantErr: zerrors.ThrowNotFound(nil, "COMMAND-Hief9", "Errors.DeviceAuth.NotFound"), }, + { + name: "invalid org", + fields: fields{ + eventstore: expectEventstore( + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("123", "instance1"), + "client_id", "123", "456", now, + []string{"a", "b", "c"}, + []string{"projectID", "clientID"}, true, "orgID2", + ), + )), + ), + }, + args: args{ + ctx, "123", "subj", "orgID", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + time.Unix(123, 456), &language.Afrikaans, &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + "sessionID", + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-3tgws", "Errors.User.NotAllowedOrg"), + }, { name: "push error", fields: fields{ @@ -179,7 +221,7 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), expectPushFailed(pushErr, @@ -221,7 +263,51 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", + ), + )), + expectPush( + deviceauth.NewApprovedEvent( + ctx, deviceauth.NewAggregate("123", "instance1"), "subj", "orgID", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + time.Unix(123, 456), &language.Afrikaans, &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + "sessionID", + ), + ), + ), + }, + args: args{ + ctx, "123", "subj", "orgID", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + time.Unix(123, 456), &language.Afrikaans, &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + "sessionID", + }, + wantDetails: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + { + name: "success with organizationID in device auth", + fields: fields{ + eventstore: expectEventstore( + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("123", "instance1"), + "client_id", "123", "456", now, + []string{"a", "b", "c"}, + []string{"projectID", "clientID"}, true, "orgID", ), )), expectPush( @@ -317,7 +403,7 @@ func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) { deviceauth.NewAggregate("deviceCode", "instance1"), "client_id", "deviceCode", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), ), eventFromEventPusherWithInstanceID( @@ -349,7 +435,7 @@ func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) { deviceauth.NewAggregate("deviceCode", "instance1"), "client_id", "deviceCode", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), ), @@ -374,7 +460,7 @@ func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) { deviceauth.NewAggregate("deviceCode", "instance1"), "client_id", "deviceCode", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), expectFilter(), @@ -400,7 +486,7 @@ func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) { deviceauth.NewAggregate("deviceCode", "instance1"), "client_id", "deviceCode", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), expectFilter(eventFromEventPusherWithInstanceID( @@ -426,6 +512,57 @@ func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) { }, wantErr: zerrors.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid"), }, + { + name: "invalid organization, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("deviceCode", "instance1"), + "client_id", "deviceCode", "456", now, + []string{"a", "b", "c"}, + []string{"projectID", "clientID"}, true, "orgID2", + ), + )), + expectFilter( + eventFromEventPusherWithInstanceID( + "instance1", + session.NewAddedEvent(ctx, + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), + eventFromEventPusher( + session.NewUserCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "orgID", testNow, &language.Afrikaans), + ), + eventFromEventPusher( + session.NewPasswordCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + testNow), + ), + eventFromEventPusherWithCreationDateNow( + session.NewLifetimeSetEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + 2*time.Minute), + ), + ), + ), + tokenVerifier: newMockTokenVerifierValid(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx, + "deviceCode", + "sessionID", + "sessionToken", + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-D2j34", "Errors.User.NotAllowedOrg"), + }, { name: "push error", fields: fields{ @@ -437,7 +574,7 @@ func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) { deviceauth.NewAggregate("deviceCode", "instance1"), "client_id", "deviceCode", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), expectFilter( @@ -501,7 +638,73 @@ func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) { deviceauth.NewAggregate("deviceCode", "instance1"), "client_id", "deviceCode", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", + ), + )), + expectFilter( + eventFromEventPusher( + session.NewAddedEvent(ctx, + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), + ), + eventFromEventPusher( + session.NewUserCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "orgID", testNow, &language.Afrikaans), + ), + eventFromEventPusher( + session.NewPasswordCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + testNow), + ), + eventFromEventPusherWithCreationDateNow( + session.NewLifetimeSetEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + 2*time.Minute), + ), + ), + expectPush( + deviceauth.NewApprovedEvent( + ctx, deviceauth.NewAggregate("deviceCode", "instance1"), "userID", "orgID", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + testNow, &language.Afrikaans, &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + "sessionID", + ), + ), + ), + tokenVerifier: newMockTokenVerifierValid(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx, + "deviceCode", + "sessionID", + "sessionToken", + }, + wantDetails: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + { + name: "authorized with organizationID in device auth", + fields: fields{ + eventstore: expectEventstore( + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("deviceCode", "instance1"), + "client_id", "deviceCode", "456", now, + []string{"a", "b", "c"}, + []string{"projectID", "clientID"}, true, "orgID", ), )), expectFilter( @@ -613,7 +816,7 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), ), @@ -633,7 +836,7 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), expectPushFailed(pushErr, @@ -659,7 +862,7 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), expectPush( @@ -687,7 +890,7 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), expectPush( @@ -767,7 +970,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, false, + []string{"audience"}, false, "", ), ), ), @@ -806,7 +1009,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(-time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, false, + []string{"audience"}, false, "", ), ), ), @@ -835,7 +1038,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(-time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, false, + []string{"audience"}, false, "", ), ), eventFromEventPusherWithInstanceID( @@ -867,7 +1070,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(-time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, false, + []string{"audience"}, false, "", ), ), eventFromEventPusherWithInstanceID( @@ -899,7 +1102,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(-time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, false, + []string{"audience"}, false, "", ), ), eventFromEventPusherWithInstanceID( @@ -937,7 +1140,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(-time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, false, + []string{"audience"}, false, "", ), ), eventFromEventPusherWithInstanceID( @@ -1001,7 +1204,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(-time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, false, + []string{"audience"}, false, "", ), ), eventFromEventPusherWithInstanceID( @@ -1098,7 +1301,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(-time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, false, + []string{"audience"}, false, "", ), ), eventFromEventPusherWithInstanceID( @@ -1202,7 +1405,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(-time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, true, + []string{"audience"}, true, "", ), ), eventFromEventPusherWithInstanceID(
internal/command/oidc_session_test.go+6 −0 modified@@ -138,6 +138,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("hintUserID"), true, "issuer", + "", ), ), eventFromEventPusher( @@ -183,6 +184,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("hintUserID"), true, "issuer", + "", ), ), eventFromEventPusher( @@ -236,6 +238,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("hintUserID"), true, "issuer", + "", ), ), eventFromEventPusher( @@ -334,6 +337,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("hintUserID"), true, "issuer", + "", ), ), eventFromEventPusher( @@ -468,6 +472,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("hintUserID"), true, "issuer", + "", ), ), eventFromEventPusher( @@ -611,6 +616,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("hintUserID"), false, "issuer", + "", ), ), eventFromEventPusher(
internal/domain/auth_request.go+6 −12 modified@@ -230,24 +230,18 @@ func (a *AuthRequest) AppendAudIfNotExisting(aud string) { } func (a *AuthRequest) GetScopeOrgPrimaryDomain() string { - switch request := a.Request.(type) { - case *AuthRequestOIDC: - for _, scope := range request.Scopes { - if strings.HasPrefix(scope, OrgDomainPrimaryScope) { - return strings.TrimPrefix(scope, OrgDomainPrimaryScope) - } + for _, scope := range a.Request.GetScopes() { + if strings.HasPrefix(scope, OrgDomainPrimaryScope) { + return strings.TrimPrefix(scope, OrgDomainPrimaryScope) } } return "" } func (a *AuthRequest) GetScopeOrgID() string { - switch request := a.Request.(type) { - case *AuthRequestOIDC: - for _, scope := range request.Scopes { - if strings.HasPrefix(scope, OrgIDScope) { - return strings.TrimPrefix(scope, OrgIDScope) - } + for _, scope := range a.Request.GetScopes() { + if strings.HasPrefix(scope, OrgIDScope) { + return strings.TrimPrefix(scope, OrgIDScope) } } return ""
internal/domain/request.go+39 −0 modified@@ -17,6 +17,7 @@ const ( type Request interface { Type() AuthRequestType IsValid() bool + GetScopes() []string } type AuthRequestType int32 @@ -35,6 +36,18 @@ type AuthRequestOIDC struct { CodeChallenge *OIDCCodeChallenge } +func NewAuthRequestOIDC( + scopes []string, + responseType OIDCResponseType, + responseMode OIDCResponseMode, + nonce string, + codeChallenge *OIDCCodeChallenge, +) *AuthRequestOIDC { + return &AuthRequestOIDC{ + scopes, responseType, responseMode, nonce, codeChallenge, + } +} + func (a *AuthRequestOIDC) Type() AuthRequestType { return AuthRequestTypeOIDC } @@ -44,6 +57,10 @@ func (a *AuthRequestOIDC) IsValid() bool { a.CodeChallenge == nil || a.CodeChallenge != nil && a.CodeChallenge.IsValid() } +func (a *AuthRequestOIDC) GetScopes() []string { + return a.Scopes +} + type AuthRequestSAML struct { ID string BindingType string @@ -61,6 +78,10 @@ func (a *AuthRequestSAML) IsValid() bool { return true } +func (*AuthRequestSAML) GetScopes() []string { + return nil +} + type AuthRequestDevice struct { ClientID string DeviceCode string @@ -71,10 +92,28 @@ type AuthRequestDevice struct { ProjectName string } +func NewAuthRequestDevice( + clientID string, + deviceCode string, + userCode string, + scopes []string, + audience []string, + appName string, + projectName string, +) *AuthRequestDevice { + return &AuthRequestDevice{ + clientID, deviceCode, userCode, scopes, audience, appName, projectName, + } +} + func (*AuthRequestDevice) Type() AuthRequestType { return AuthRequestTypeDevice } func (a *AuthRequestDevice) IsValid() bool { return a.DeviceCode != "" && a.UserCode != "" } + +func (a *AuthRequestDevice) GetScopes() []string { + return a.Scopes +}
internal/query/device_auth.go+7 −10 modified@@ -98,18 +98,20 @@ func prepareDeviceAuthQuery() (sq.SelectBuilder, func(*sql.Row) (*domain.AuthReq LeftJoin(join(ProjectColumnID, AppColumnProjectID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*domain.AuthRequestDevice, error) { - dst := new(domain.AuthRequestDevice) var ( + clientID string + deviceCode string + userCode string scopes database.TextArray[string] audience database.TextArray[string] appName sql.NullString projectName sql.NullString ) err := row.Scan( - &dst.ClientID, - &dst.DeviceCode, - &dst.UserCode, + &clientID, + &deviceCode, + &userCode, &scopes, &audience, &appName, @@ -121,11 +123,6 @@ func prepareDeviceAuthQuery() (sq.SelectBuilder, func(*sql.Row) (*domain.AuthReq if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-Voo3o", "Errors.Internal") } - dst.Scopes = scopes - dst.Audience = audience - dst.AppName = appName.String - dst.ProjectName = projectName.String - - return dst, nil + return domain.NewAuthRequestDevice(clientID, deviceCode, userCode, scopes, audience, appName.String, projectName.String), nil } }
internal/query/device_auth_test.go+9 −9 modified@@ -52,15 +52,15 @@ var ( "appName", "projectName", } - expectedDeviceAuth = &domain.AuthRequestDevice{ - ClientID: "client-id", - DeviceCode: "device1", - UserCode: "user-code", - Scopes: []string{"a", "b", "c"}, - Audience: []string{"projectID", "clientID"}, - AppName: "appName", - ProjectName: "projectName", - } + expectedDeviceAuth = domain.NewAuthRequestDevice( + "client-id", + "device1", + "user-code", + []string{"a", "b", "c"}, + []string{"projectID", "clientID"}, + "appName", + "projectName", + ) ) func TestQueries_DeviceAuthRequestByUserCode(t *testing.T) {
internal/repository/authrequest/auth_request.go+4 −1 modified@@ -39,6 +39,7 @@ type AddedEvent struct { HintUserID *string `json:"hint_user_id,omitempty"` NeedRefreshToken bool `json:"need_refresh_token,omitempty"` Issuer string `json:"issuer,omitempty"` + OrganizationID string `json:"organization_id,omitempty"` } func (e *AddedEvent) Payload() interface{} { @@ -67,7 +68,8 @@ func NewAddedEvent(ctx context.Context, loginHint, hintUserID *string, needRefreshToken bool, - issuer string, + issuer, + organizationID string, ) *AddedEvent { return &AddedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -92,6 +94,7 @@ func NewAddedEvent(ctx context.Context, HintUserID: hintUserID, NeedRefreshToken: needRefreshToken, Issuer: issuer, + OrganizationID: organizationID, } }
internal/repository/deviceauth/device_auth.go+3 −0 modified@@ -29,6 +29,7 @@ type AddedEvent struct { Audience []string State domain.DeviceAuthState NeedRefreshToken bool + OrganizationID string } func (e *AddedEvent) SetBaseEvent(b *eventstore.BaseEvent) { @@ -53,13 +54,15 @@ func NewAddedEvent( scopes []string, audience []string, needRefreshToken bool, + organizationID string, ) *AddedEvent { return &AddedEvent{ eventstore.NewBaseEventForPush( ctx, aggregate, AddedEventType, ), clientID, deviceCode, userCode, expires, scopes, audience, domain.DeviceAuthStateInitiated, needRefreshToken, + organizationID, } }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-g2pf-ww5m-2r9mghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-33132ghsaADVISORY
- github.com/zitadel/zitadel/commit/d90285929ca019fa817f31551fd0883429dda2a8ghsax_refsource_MISCWEB
- github.com/zitadel/zitadel/releases/tag/v3.4.9ghsax_refsource_MISCWEB
- github.com/zitadel/zitadel/releases/tag/v4.12.3ghsax_refsource_MISCWEB
- github.com/zitadel/zitadel/security/advisories/GHSA-g2pf-ww5m-2r9mghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.