Skip to content

fix(oidc): portal-origin logins provision customer-type users - #19

Merged
JOY (JOY) merged 2 commits into
devfrom
fix/portal-oidc-customer-type
Sep 21, 2026
Merged

JOY (JOY) merged 2 commits into
devfrom
fix/portal-oidc-customer-type

Conversation

@JOY

Copy link
Copy Markdown

Follow-up to #18 completing the everyone-signs-in-with-DOS-ID model for the customer support portal.

The portal's DOS ID button round-trips a /support/* return path inside the signed OIDC state, so the entry surface is already identifiable. Portal-origin first logins now provision customer-type users (UserTypeUser) with no staff role, no organization/team provisioning, no agent profile, and no break-glass elevation. Staff-surface logins keep the #18 behaviour (employee type plus claim/allowlist-derived roles). User type is fixed at creation and never flipped by the other surface (fail-closed: the dashboard middleware already rejects non-employee users).

Before this, a customer signing in through the portal became an employee user - and before #18, received the admin role. That defect is inherited from upstream huabeitech/agent-desk, which still grants admin to every first-time OIDC user (verified against upstream/main).

Verification: new tests cover portal-origin provisioning (customer type, zero roles/orgs/agent profiles), allowlist-ignoring on the portal surface, and the /support prefix matcher; existing staff-surface OIDC tests updated and passing; go build + go vet clean.

The support portal's DOS ID button round-trips a /support/* return path,
so the signed state already identifies the entry surface. Use it to
decide provisioning at creation time:

- portal-origin first logins create UserTypeUser customers with no staff
  role, no org/team sync, no agent profile; the break-glass allowlist
  never elevates on the portal surface
- staff-surface logins keep the Wave 2 behaviour (employee type plus
  claim/allowlist-derived roles)
- user type is fixed at creation and never flipped by the other surface
  (fail-closed; the dashboard middleware already rejects non-employees)

Before this, a customer signing in through the portal became an employee
user (and, before the redirect-only change, received the admin role -
a defect inherited from upstream agent-desk, which still grants admin to
every first-time OIDC user).
@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 4723a5ac-9e72-4c41-88f7-9cb0d1d94cc9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces logic to distinguish OIDC logins originating from the customer support portal from those starting at the staff dashboard, ensuring portal-origin logins provision customer-type users without staff roles or profiles. The review feedback highlights a critical security vulnerability where query parameters or URL fragments in the redirect path would bypass the support portal detection, incorrectly provisioning customers as employees. Additionally, the feedback suggests a defense-in-depth check to restrict staff provisioning to employee-type users and recommends adding corresponding test cases for URL parameter and fragment handling.

Comment on lines +71 to +74
func isSupportPortalNext(next string) bool {
next = strings.TrimSpace(next)
return next == "/support" || strings.HasPrefix(next, "/support/")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

The current implementation of isSupportPortalNext does not handle query parameters or URL fragments (e.g., /support?ticket=123 or /support/tickets?resolved=true#anchor). If a customer logs in via the support portal with any query parameters or fragments in the redirect path, isSupportPortalNext will return false. This causes them to be incorrectly provisioned as an employee (UserTypeEmployee) and receive default staff roles, which is a critical security vulnerability (privilege escalation).

We should strip query parameters and fragments before performing the prefix and equality checks.

func isSupportPortalNext(next string) bool {
	next = strings.TrimSpace(next)
	if idx := strings.IndexAny(next, "?#"); idx != -1 {
		next = next[:idx]
	}
	return next == "/support" || strings.HasPrefix(next, "/support/")
}

Comment on lines +130 to +136
if !portalOrigin {
s.ensureDefaultOIDCRole(ctx.Tx, user)
s.ensureBreakGlassAdminRole(ctx.Tx, authCfg, user, profile)
s.syncOIDCUserOrganizations(ctx.Tx, user, profile)
s.syncOIDCUserTeams(ctx.Tx, user, profile)
_, _ = AgentProfileService.EnsureAgentProfileForUser(ctx.Tx, user)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

As a defense-in-depth measure, we should ensure that staff provisioning (roles, organization/team sync, and agent profile creation) is only executed if the user is actually an employee (UserTypeEmployee). Currently, if an existing customer (UserTypeUser) attempts to log in via the staff surface (portalOrigin == false), they would still have staff roles, organizations, and agent profiles provisioned for them in the database, even though they are rejected by the dashboard middleware.

Suggested change
if !portalOrigin {
s.ensureDefaultOIDCRole(ctx.Tx, user)
s.ensureBreakGlassAdminRole(ctx.Tx, authCfg, user, profile)
s.syncOIDCUserOrganizations(ctx.Tx, user, profile)
s.syncOIDCUserTeams(ctx.Tx, user, profile)
_, _ = AgentProfileService.EnsureAgentProfileForUser(ctx.Tx, user)
}
if !portalOrigin && user.UserType == enums.UserTypeEmployee {
s.ensureDefaultOIDCRole(ctx.Tx, user)
s.ensureBreakGlassAdminRole(ctx.Tx, authCfg, user, profile)
s.syncOIDCUserOrganizations(ctx.Tx, user, profile)
s.syncOIDCUserTeams(ctx.Tx, user, profile)
_, _ = AgentProfileService.EnsureAgentProfileForUser(ctx.Tx, user)
}

Comment on lines +278 to +283
{next: "/support/community", expected: true},
{next: "/support", expected: true},
{next: " /support/tickets ", expected: true},
{next: "/dashboard", expected: false},
{next: "", expected: false},
{next: "/supportevil", expected: false},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add test cases to verify that isSupportPortalNext correctly handles query parameters and URL fragments.

		{next: "/support/community", expected: true},
		{next: "/support", expected: true},
		{next: " /support/tickets ", expected: true},
		{next: "/support?ticket=123", expected: true},
		{next: "/support/tickets?resolved=true#anchor", expected: true},
		{next: "/dashboard", expected: false},
		{next: "", expected: false},
		{next: "/supportevil", expected: false},

@JOY
JOY (JOY) merged commit 20fd8e7 into dev Sep 21, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant