From c0cfbb2bdc42ca09eee5cb909afa4d0749e7590f Mon Sep 17 00:00:00 2001 From: binarybeach Date: Sun, 3 May 2026 23:55:25 -1000 Subject: [PATCH] =?UTF-8?q?binarybeachio:=20trusted=20view=20=E2=80=94=20m?= =?UTF-8?q?irror=20OAuth=20adapter=20create-shape=20(Profile,=20username,?= =?UTF-8?q?=20is=5Femail=5Fverified)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plane's OAuth adapter (apps/api/plane/authentication/adapter/base.py:289-342) creates User AND Profile when a new identity arrives. My trusted view was calling User.objects.get_or_create() without the Profile, so the SPA's /api/users/me/profile/ 404'd and the SPA bounced the user back to /login in an onboarding loop. Mirror the adapter's full create-shape: random username (uuid hex), first/last names from JWT claims, is_password_autoset=True, is_email_verified=True, random password (so Django's auth hash is non-empty for break-glass), then Profile.objects.create(user=user). Wrapped in a transaction so partial creation can't leave the DB inconsistent. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plane/authentication/views/app/trusted.py | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/apps/api/plane/authentication/views/app/trusted.py b/apps/api/plane/authentication/views/app/trusted.py index e36235fb1..a6e6a8b8f 100644 --- a/apps/api/plane/authentication/views/app/trusted.py +++ b/apps/api/plane/authentication/views/app/trusted.py @@ -38,12 +38,14 @@ import logging import os import time +import uuid from typing import Optional, Tuple from urllib.parse import urlparse import jwt as pyjwt import redis import requests +from django.db import transaction from django.http import HttpResponseRedirect, HttpResponseNotFound from django.views import View @@ -55,7 +57,7 @@ from plane.authentication.utils.host import base_host from plane.authentication.utils.login import user_login from plane.authentication.utils.redirection_path import get_redirection_path from plane.authentication.utils.user_auth_workflow import post_user_auth_workflow -from plane.db.models import User +from plane.db.models import Profile, User from plane.settings.redis import redis_instance from plane.utils.path_validator import get_safe_redirect_url @@ -231,18 +233,34 @@ class TrustedSignInEndpoint(View): if not email: return _redirect_with_error(request, "TRUSTED_JWT_TOKEN_INVALID", "TRUSTED_JWT_TOKEN_NO_EMAIL", next_path) - # Find-or-create. Plane's User model uses email as a unique natural key; - # other OAuth providers do the same lookup via the OauthAdapter base. - # We mirror that behavior here without going through OauthAdapter — this - # endpoint is a NEW entry-point, not a fifth OAuth provider. - user, created = User.objects.get_or_create( - email=email, - defaults={ - "first_name": claims.get("first_name") or claims.get("given_name") or "", - "last_name": claims.get("last_name") or claims.get("family_name") or "", - "is_password_autoset": True, - }, - ) + # Find-or-create. We mirror the User-creation shape that + # OauthAdapter.complete_login_or_signup() produces (apps/api/plane/ + # authentication/adapter/base.py:289-342) — same field set, same + # required side-effect of creating a Profile row. Skipping the Profile + # would cause Plane's SPA /api/users/me/profile/ to 404 and bounce + # the user back to /login in an onboarding loop. + user = User.objects.filter(email=email).first() + created = user is None + if created: + with transaction.atomic(): + user = User( + email=email, + username=uuid.uuid4().hex, + first_name=claims.get("first_name") or claims.get("given_name") or "", + last_name=claims.get("last_name") or claims.get("family_name") or "", + is_password_autoset=True, + is_email_verified=True, + ) + # Random password — user signs in via SSO; the password exists + # only so Django's auth machinery has a non-empty hash and the + # break-glass admin pattern can be applied later by ALTER-ing + # this user out of band if needed. + user.set_password(uuid.uuid4().hex) + user.save() + # Profile row is mandatory: every Plane API endpoint that + # touches user state (workspace listings, onboarding) reads + # Profile, and the SPA's /api/users/me/profile/ 404s without it. + Profile.objects.create(user=user) # Plane's existing post-auth workflow (default workspace, invitations, etc.) post_user_auth_workflow(user=user, is_signup=created, request=request)