fix(login): resolve the registration org consistently with the login policy (#12621)

# Which Problems Are Solved

When resolving the organization for a self-registration, the resource
owner was determined only from an explicitly requested org, falling back
to the default org. The login policy applied to the same request,
however, is resolved from the requested org, the user org, and only then
the default org. As a result the two could diverge — e.g. after the
login context had already resolved a user's organization — and a user
could be registered in a different organization than the one whose
settings were evaluated for the request.

# How the Problems Are Solved

`determineResourceOwner` now uses the same resolution order as the
policy lookup (requested org, then user org, then default org), so the
organization a user is registered in matches the one the request was
evaluated against.

# Additional Changes

Added a unit test covering the resolution order.

# Additional Context

Aligns login v1 registration org resolution with the behavior in login
v2.

Co-authored-by: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Livio Spring
2026-08-28 10:37:52 +00:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 27ce10276b
commit 711d688294
3 changed files with 70 additions and 1 deletions
+7 -1
View File
@@ -42,9 +42,15 @@ type registerData struct {
}
func determineResourceOwner(ctx context.Context, authRequest *domain.AuthRequest) string {
if authRequest != nil && authRequest.RequestedOrgID != "" {
if authRequest == nil {
return authz.GetInstance(ctx).DefaultOrganisationID()
}
if authRequest.RequestedOrgID != "" {
return authRequest.RequestedOrgID
}
if authRequest.UserOrgID != "" {
return authRequest.UserOrgID
}
return authz.GetInstance(ctx).DefaultOrganisationID()
}
@@ -0,0 +1,52 @@
package login
import (
"context"
"testing"
"github.com/zitadel/zitadel/internal/domain"
)
func Test_determineResourceOwner(t *testing.T) {
// Note: the nil-authRequest and "both empty" cases fall back to
// authz.GetInstance(ctx).DefaultOrganisationID() and are covered by the
// integration/handler tests where an instance is present in the context.
// Here we lock the resolution order for the cases that do not touch the
// instance: RequestedOrgID takes precedence over UserOrgID, and UserOrgID
// is used as the fallback before the default org.
tests := []struct {
name string
authRequest *domain.AuthRequest
want string
}{
{
name: "requested org wins over user org",
authRequest: &domain.AuthRequest{RequestedOrgID: "requested", UserOrgID: "user"},
want: "requested",
},
{
name: "requested org only",
authRequest: &domain.AuthRequest{RequestedOrgID: "requested"},
want: "requested",
},
{
name: "user org used when no requested org (matches fillPolicies resolution)",
authRequest: &domain.AuthRequest{UserOrgID: "user"},
want: "user",
},
{
// domain discovery sets BOTH RequestedOrgID and UserOrgID to the
// discovered org, so it still resolves to the discovered org.
name: "domain discovery result",
authRequest: &domain.AuthRequest{RequestedOrgID: "discovered", UserOrgID: "discovered"},
want: "discovered",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := determineResourceOwner(context.Background(), tt.authRequest); got != tt.want {
t.Errorf("determineResourceOwner() = %q, want %q", got, tt.want)
}
})
}
}