Feat: OIDC UI bridge, Microsoft SSO,, and schema session flaws

- OAuth Callback to Use Gatehouse UI to login instead of Backend Served dull ui
- Setup Autoregister of user + org, on oauth
- Microsoft Oauth Support
- OIDCRefreshToken.access_token_id  had a narrow Column increased to VAR(255) and remove FK to sessions.id which had no use
- client_id and client.id mismatch ,backup-code consumption
This commit is contained in:
2026-02-26 23:18:31 +05:45
parent f1fff22f3e
commit 1ba5738d52
14 changed files with 732 additions and 349 deletions
+91 -45
View File
@@ -81,10 +81,11 @@ class OAuthFlowService:
400,
)
# Generate PKCE parameters (Google web applications don't use PKCE)
# Generate PKCE parameters (Google and Microsoft web applications don't use PKCE
# when a client_secret is present — they are confidential clients)
code_verifier = None
code_challenge = None
if provider_type_str not in ['google']:
if provider_type_str not in ['google', 'microsoft']:
code_verifier = secrets.token_urlsafe(32)
code_challenge = ExternalAuthService._compute_s256_challenge(code_verifier)
@@ -92,7 +93,7 @@ class OAuthFlowService:
logger.info(
f"[PKCE DEBUG] Provider type check: provider_type_str='{provider_type_str}', "
f"is_google={provider_type_str in ['google']}, "
f"will_skip_pkce={provider_type_str in ['google']}"
f"will_skip_pkce={provider_type_str in ['google', 'microsoft']}"
)
# Create OAuth state for login flow
@@ -183,10 +184,11 @@ class OAuthFlowService:
400,
)
# Generate PKCE parameters (Google web applications don't use PKCE)
# Generate PKCE parameters (Google and Microsoft web applications don't use PKCE
# when a client_secret is present — they are confidential clients)
code_verifier = None
code_challenge = None
if provider_type_str not in ['google']:
if provider_type_str not in ['google', 'microsoft']:
code_verifier = secrets.token_urlsafe(32)
code_challenge = ExternalAuthService._compute_s256_challenge(code_verifier)
@@ -194,7 +196,7 @@ class OAuthFlowService:
logger.info(
f"[PKCE DEBUG] Register flow - Provider type check: provider_type_str='{provider_type_str}', "
f"is_google={provider_type_str in ['google']}, "
f"will_skip_pkce={provider_type_str in ['google']}"
f"will_skip_pkce={provider_type_str in ['google', 'microsoft']}"
)
# Create OAuth state for register flow
@@ -404,77 +406,121 @@ class OAuthFlowService:
).first()
if not auth_method:
# User doesn't exist - check if email matches existing user
# No linked account found — check if email matches an existing user
existing_user = User.query.filter_by(
email=user_info["email"]
).first()
if existing_user:
AuditService.log_external_auth_login_failed(
organization_id=state_record.organization_id,
provider_type=provider_type_str,
# Email exists but no OAuth link — auto-link and log in
logger.info(
f"OAuth login: email {user_info['email']} matches existing user "
f"{existing_user.id}, auto-linking {provider_type_str} account"
)
auth_method = AuthenticationMethod(
user_id=existing_user.id,
method_type=provider_type,
provider_user_id=user_info["provider_user_id"],
provider_data=ExternalAuthService._encrypt_provider_data(tokens, user_info),
verified=user_info.get("email_verified", False),
is_primary=False,
last_used_at=datetime.utcnow(),
)
auth_method.save()
user = existing_user
else:
# Brand-new user — auto-register via OAuth (standard behaviour)
logger.info(
f"OAuth login: no account for {user_info['email']}, "
f"auto-creating user via {provider_type_str}"
)
user = User(
email=user_info["email"],
failure_reason="email_exists",
error_message=f"An account with email {user_info['email']} already exists",
)
raise OAuthFlowError(
f"An account with email {user_info['email']} already exists. "
"Please log in with your password and link your account from settings.",
"EMAIL_EXISTS",
400,
full_name=user_info.get("name", ""),
status="active",
email_verified=user_info.get("email_verified", False),
)
user.save()
AuditService.log_external_auth_login_failed(
organization_id=state_record.organization_id,
provider_type=provider_type_str,
provider_user_id=user_info["provider_user_id"],
email=user_info["email"],
failure_reason="account_not_found",
error_message="No Gatehouse account matches this external account",
)
raise OAuthFlowError(
"No Gatehouse account matches this external account. Please register first.",
"ACCOUNT_NOT_FOUND",
404,
auth_method = AuthenticationMethod(
user_id=user.id,
method_type=provider_type,
provider_user_id=user_info["provider_user_id"],
provider_data=ExternalAuthService._encrypt_provider_data(tokens, user_info),
verified=user_info.get("email_verified", False),
is_primary=True,
last_used_at=datetime.utcnow(),
)
auth_method.save()
AuditService.log_action(
action="user.register",
user_id=user.id,
organization_id=state_record.organization_id,
resource_type="user",
resource_id=user.id,
metadata={
"provider_type": provider_type_str,
"provider_user_id": user_info["provider_user_id"],
"auto_registered": True,
},
description=f"User auto-registered via {provider_type_str} OAuth",
success=True,
)
else:
# Existing linked account — update provider data
auth_method.provider_data = ExternalAuthService._encrypt_provider_data(
tokens, user_info
)
auth_method.last_used_at = datetime.utcnow()
auth_method.save()
user = auth_method.user
# Update provider data
auth_method.provider_data = ExternalAuthService._encrypt_provider_data(
tokens, user_info
)
auth_method.last_used_at = datetime.utcnow()
auth_method.save()
# Get user's organizations
user_orgs = user.get_organizations()
# Determine target organization
target_org = None
# Priority 1: Use organization_id from state if provided (org hint)
if state_record.organization_id:
target_org = next(
(org for org in user_orgs if org.id == state_record.organization_id),
None
)
# Priority 2: If user has exactly one organization, use it
if not target_org and len(user_orgs) == 1:
target_org = user_orgs[0]
# Priority 3: No organization or multiple organizations - need selection
# Priority 3: No orgs at all — auto-create a personal org and log in
if not target_org and len(user_orgs) == 0:
import re
import uuid
from gatehouse_app.services.organization_service import OrganizationService
org_name = f"{user_info.get('name') or user.email.split('@')[0]}'s Workspace"
# Build a URL-safe slug and ensure uniqueness with a short suffix
base_slug = re.sub(r"[^a-z0-9]+", "-", org_name.lower()).strip("-")[:40]
slug = f"{base_slug}-{uuid.uuid4().hex[:6]}"
org = OrganizationService.create_organization(
name=org_name,
slug=slug,
owner_user_id=user.id,
)
target_org = org
logger.info(
f"OAuth login: auto-created org '{org.name}' (id={org.id}) "
f"for new user {user.id}"
)
# Priority 4: Multiple orgs — need user to pick one
if not target_org:
# Mark state as used
state_record.mark_used()
logger.info(
f"OAuth login requires org selection for user={user.id}, "
f"provider={provider_type_str}, org_count={len(user_orgs)}"
)
return {
"success": True,
"flow_type": "login",
@@ -488,7 +534,7 @@ class OAuthFlowService:
{
"id": org.id,
"name": org.name,
"slug": org.slug if hasattr(org, 'slug') else None,
"slug": org.slug if hasattr(org, "slug") else None,
}
for org in user_orgs
],