Skip to content

fix(auth): JIT organization sync in checkExists and canonical org provisioning - #30

Merged
JOY (JOY) merged 1 commit into
mainfrom
dev
Sep 3, 2026
Merged

fix(auth): JIT organization sync in checkExists and canonical org provisioning#30
JOY (JOY) merged 1 commit into
mainfrom
dev

Conversation

@JOY

@JOY JOY (JOY) commented Sep 3, 2026

Copy link
Copy Markdown

What kind of change does this PR introduce?

Bug fix & Ecosystem Auth: Backend (apps/backend, libraries/nestjs-libraries). Fixes organization synchronization during generic OAuth login in AuthService.checkExists() and enables passing canonical orgId during user/organization creation.

Why was this change needed?

Previously, when an existing user logged in via DOS ID SSO, AuthService.checkExists() immediately returned a JWT without synchronizing updated claims and organizations from the OIDC userinfo payload. Furthermore, initial registration created an auto-generated UUID for the organization instead of reusing the canonical ecosystem orgId provided by DOS.Me.

Technical Details & Scope

  • apps/backend/src/services/auth/auth.service.ts:
    • Extracted and centralized syncUserOrganizations(userId, organizations) method to handle organization discovery, name synchronization, and user role assignment.
    • Updated checkExists() to run syncUserOrganizations() and update user personal details before issuing the session JWT.
    • Updated loginOrRegisterProvider() to pass canonical orgId when creating the initial organization and user.
  • libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts & organization.service.ts:
    • Added optional orgId field to CreateOrgUserDto input on createOrgAndUser(), allowing the first organization to inherit the exact UUID from DOS ID.

Verification & Testing

  • Verified TypeScript typechecking and production build with pnpm --filter ./apps/backend run build.
  • Verified live PostgreSQL database synchronization on Supabase post schema.
  • Cleaned up legacy dummy organizations and confirmed joy@dos.ai has 3 canonical organizations (JOY, DOS, Crove) with ULTIMATE subscriptions and correctly mapped OAuth applications.

QA

  1. Log in with DOS ID on post.crove.com/auth or beta-post.crove.com/auth
  2. Open the Organization selector in the header / profile dropdown
  3. Verify that all canonical organizations from DOS.Me (JOY, DOS, Crove) appear with their correct canonical names and Super-Admin roles
  4. Switch between organizations and confirm active context changes smoothly

Checklist:

  • My code follows the project's code style and architectural conventions.
  • Local build passes (pnpm run build).
  • Tests and typecheck have been verified without errors.
  • Documentation has been updated (if applicable).
  • No secrets or sensitive credentials are included in this PR.
  • I have filled in the QA / Verification section above with real steps to verify this change.

Note

High Risk
Changes authentication linking, org membership, and role assignment on every OAuth checkExists/login path; incorrect sync or role mapping could grant wrong org access or duplicate orgs.

Overview
OAuth login now keeps users, org memberships, and org names in sync with the identity provider instead of only issuing a JWT for returning users.

A new syncUserOrganizations helper centralizes provider org claims: it renames orgs when names drift, joins users to orgs that already exist by canonical ID, or creates orgs with the provider’s ID. Role mapping treats OWNER/SUPERADMIN as super-admin internally and downgrades to ADMIN when calling addUserToOrg (which only accepts USER/ADMIN). loginOrRegisterProvider uses this helper for both existing and newly registered users and passes orgId from the first provider org into createOrgAndUser so the initial org isn’t a random UUID.

checkExists (OAuth callback before full login) now resolves users by email when provider ID lookup fails, updates display name, runs org sync, then returns a JWT—closing the gap where DOS ID SSO skipped claim updates.

createOrgAndUser in the organization repository/service accepts an optional orgId to set the organization primary key at creation time.

Reviewed by Cursor Bugbot for commit 4bc0111. Configure here.

@cursor

cursor Bot commented Sep 3, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_6e9f038a-fe65-4cb3-9bf6-5a457d477d26)

@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 refactors the organization synchronization logic in AuthService into a reusable syncUserOrganizations method, simplifies the organization and user creation flow, and adds email-based user lookup as a fallback. The review highlights three important issues: a critical bug where passing an existing orgId to createOrgAndUser can cause a database unique constraint violation, a high-severity security risk of OAuth Account Takeover if email verification is not checked, and a medium-severity issue where non-admin users can inadvertently rename organizations.

Comment on lines +226 to +238
const create = await this._organizationService.createOrgAndUser(
{
company: companyName,
email: providerUser.email,
password: '',
provider,
providerId: providerUser.id,
datafast_visitor_id: body.datafast_visitor_id || '',
orgId: firstOrg?.id,
},
ip,
userAgent
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

If firstOrg?.id already exists in the database (e.g., when a second user from the same organization registers), calling createOrgAndUser with orgId: firstOrg.id will throw a database unique constraint violation error on the organization.id primary key, causing the registration to fail completely.

Before calling createOrgAndUser, you must check if the organization already exists. If it does, you should create the user and associate them with the existing organization instead of attempting to recreate the organization.

Comment on lines +399 to +401
if (!checkExists && user.email) {
checkExists = await this._userService.getUserByEmail(user.email);
}

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-high high

Logging in a user solely by matching their email address from an external OAuth provider without verifying that the email is verified by the provider can lead to Account Takeover (ATO) vulnerabilities. If a malicious actor registers an account on the OAuth provider using a victim's email address (without verification) and then logs in via OAuth, they will gain access to the victim's account on this platform.

Please ensure that:

  1. The OAuth provider's payload indicates that the email is verified (e.g., email_verified: true or equivalent claim).
  2. You link the provider to the user's account (updating providerName and providerId) so that subsequent logins are securely mapped via getUserByProvider rather than repeatedly falling back to email matching.

Comment on lines +158 to +174
if (existing) {
if (orgInfo.name && existing.name !== orgInfo.name) {
await this._organizationService
.updateOrganizationName(orgInfo.id, orgInfo.name)
.catch(() => {});
}
} else {
const orgExistsInDb = await this._organizationService.getOrgById(orgInfo.id);
if (orgExistsInDb) {
if (orgInfo.name && orgExistsInDb.name !== orgInfo.name) {
await this._organizationService
.updateOrganizationName(orgInfo.id, orgInfo.name)
.catch(() => {});
}
await this._organizationService
.addUserToOrg(userId, makeId(5), orgInfo.id, role === 'SUPERADMIN' ? 'ADMIN' : role)
.catch(() => {});

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

Updating the organization name should be restricted to users with administrative privileges (e.g., OWNER, SUPERADMIN, or ADMIN). Currently, any user logging in who has a matching organization in their OAuth claims will trigger an organization name update, regardless of their role. This could allow a regular member to inadvertently rename the organization for all users.

Consider checking the user's role before calling updateOrganizationName.

Suggested change
if (existing) {
if (orgInfo.name && existing.name !== orgInfo.name) {
await this._organizationService
.updateOrganizationName(orgInfo.id, orgInfo.name)
.catch(() => {});
}
} else {
const orgExistsInDb = await this._organizationService.getOrgById(orgInfo.id);
if (orgExistsInDb) {
if (orgInfo.name && orgExistsInDb.name !== orgInfo.name) {
await this._organizationService
.updateOrganizationName(orgInfo.id, orgInfo.name)
.catch(() => {});
}
await this._organizationService
.addUserToOrg(userId, makeId(5), orgInfo.id, role === 'SUPERADMIN' ? 'ADMIN' : role)
.catch(() => {});
if (existing) {
if (orgInfo.name && existing.name !== orgInfo.name && (role === 'SUPERADMIN' || role === 'ADMIN')) {
await this._organizationService
.updateOrganizationName(orgInfo.id, orgInfo.name)
.catch(() => {});
}
} else {
const orgExistsInDb = await this._organizationService.getOrgById(orgInfo.id);
if (orgExistsInDb) {
if (orgInfo.name && orgExistsInDb.name !== orgInfo.name && (role === 'SUPERADMIN' || role === 'ADMIN')) {
await this._organizationService
.updateOrganizationName(orgInfo.id, orgInfo.name)
.catch(() => {});
}
await this._organizationService
.addUserToOrg(userId, makeId(5), orgInfo.id, role === 'SUPERADMIN' ? 'ADMIN' : role)
.catch(() => {});

@JOY
JOY (JOY) merged commit 34d41c6 into main Sep 3, 2026
19 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