octa change to production sso
This commit is contained in:
parent
729a0d2d26
commit
80e28fb0eb
@ -1 +1 @@
|
||||
import{a as s}from"./index-D495l21h.js";import"./radix-vendor-CLtqm-Ae.js";import"./charts-vendor-CmYZJIYl.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-DgwXkk2Y.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-HW_ujxKo.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
||||
import{a as s}from"./index-CPucVe9U.js";import"./radix-vendor-CLtqm-Ae.js";import"./charts-vendor-CmYZJIYl.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-DgwXkk2Y.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-HW_ujxKo.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
||||
File diff suppressed because one or more lines are too long
@ -13,7 +13,7 @@
|
||||
<!-- Preload essential fonts and icons -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<script type="module" crossorigin src="/assets/index-D495l21h.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CPucVe9U.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-CmYZJIYl.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CLtqm-Ae.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
|
||||
|
||||
@ -30,14 +30,54 @@ function parseDeviceFromUserAgent(ua?: string): string {
|
||||
|
||||
|
||||
export class AuthService {
|
||||
private resolveOktaConfigForRedirectUri(redirectUri: string): {
|
||||
domain: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
apiToken: string;
|
||||
profile: 'localhost' | 'default';
|
||||
} {
|
||||
let host = '';
|
||||
try {
|
||||
host = new URL(redirectUri).hostname.toLowerCase();
|
||||
} catch {
|
||||
host = '';
|
||||
}
|
||||
const isLocalhostRedirect = host === 'localhost' || host === '127.0.0.1';
|
||||
if (isLocalhostRedirect) {
|
||||
return {
|
||||
domain: process.env.OKTA_DOMAIN_LOCALHOST || ssoConfig.oktaDomain,
|
||||
clientId: process.env.OKTA_CLIENT_ID_LOCALHOST || ssoConfig.oktaClientId,
|
||||
clientSecret: process.env.OKTA_CLIENT_SECRET_LOCALHOST || ssoConfig.oktaClientSecret,
|
||||
apiToken: process.env.OKTA_API_TOKEN_LOCALHOST || ssoConfig.oktaApiToken || '',
|
||||
profile: 'localhost',
|
||||
};
|
||||
}
|
||||
return {
|
||||
domain: ssoConfig.oktaDomain,
|
||||
clientId: ssoConfig.oktaClientId,
|
||||
clientSecret: ssoConfig.oktaClientSecret,
|
||||
apiToken: ssoConfig.oktaApiToken || '',
|
||||
profile: 'default',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch user details from Okta Users API (full profile with manager, employeeID, etc.)
|
||||
* Falls back to userinfo endpoint if Users API fails or token is not configured
|
||||
*/
|
||||
private async fetchUserFromOktaUsersAPI(oktaSub: string, email: string, accessToken: string): Promise<any> {
|
||||
private async fetchUserFromOktaUsersAPI(
|
||||
oktaSub: string,
|
||||
email: string,
|
||||
accessToken: string,
|
||||
oktaDomainOverride?: string,
|
||||
oktaApiTokenOverride?: string
|
||||
): Promise<any> {
|
||||
try {
|
||||
const oktaDomain = oktaDomainOverride || ssoConfig.oktaDomain;
|
||||
const oktaApiToken = oktaApiTokenOverride || ssoConfig.oktaApiToken;
|
||||
// Check if API token is configured
|
||||
if (!ssoConfig.oktaApiToken || ssoConfig.oktaApiToken.trim() === '') {
|
||||
if (!oktaApiToken || oktaApiToken.trim() === '') {
|
||||
logger.info('OKTA_API_TOKEN not configured, will use userinfo endpoint as fallback');
|
||||
return null;
|
||||
}
|
||||
@ -48,17 +88,17 @@ export class AuthService {
|
||||
|
||||
// First attempt: Use email (preferred method as shown in curl example)
|
||||
if (email) {
|
||||
const usersApiEndpoint = `${ssoConfig.oktaDomain}/api/v1/users/${encodeURIComponent(email)}`;
|
||||
const usersApiEndpoint = `${oktaDomain}/api/v1/users/${encodeURIComponent(email)}`;
|
||||
|
||||
logger.info('Fetching user from Okta Users API (using email)', {
|
||||
endpoint: usersApiEndpoint.replace(email, email.substring(0, 5) + '...'),
|
||||
hasApiToken: !!ssoConfig.oktaApiToken,
|
||||
hasApiToken: !!oktaApiToken,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await axios.get(usersApiEndpoint, {
|
||||
headers: {
|
||||
'Authorization': `SSWS ${ssoConfig.oktaApiToken}`,
|
||||
'Authorization': `SSWS ${oktaApiToken}`,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
validateStatus: (status) => status < 500, // Don't throw on 4xx errors
|
||||
@ -81,17 +121,17 @@ export class AuthService {
|
||||
|
||||
// Second attempt: Use oktaSub (user ID) if email lookup failed
|
||||
if (oktaSub) {
|
||||
const usersApiEndpoint = `${ssoConfig.oktaDomain}/api/v1/users/${encodeURIComponent(oktaSub)}`;
|
||||
const usersApiEndpoint = `${oktaDomain}/api/v1/users/${encodeURIComponent(oktaSub)}`;
|
||||
|
||||
logger.info('Fetching user from Okta Users API (using oktaSub)', {
|
||||
endpoint: usersApiEndpoint.replace(oktaSub, oktaSub.substring(0, 10) + '...'),
|
||||
hasApiToken: !!ssoConfig.oktaApiToken,
|
||||
hasApiToken: !!oktaApiToken,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await axios.get(usersApiEndpoint, {
|
||||
headers: {
|
||||
'Authorization': `SSWS ${ssoConfig.oktaApiToken}`,
|
||||
'Authorization': `SSWS ${oktaApiToken}`,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
validateStatus: (status) => status < 500,
|
||||
@ -269,6 +309,20 @@ export class AuthService {
|
||||
throw new Error('Email and Okta sub are required');
|
||||
}
|
||||
|
||||
const norm = (value?: unknown): string | undefined => {
|
||||
const s = String(value ?? '').trim();
|
||||
return s ? s : undefined;
|
||||
};
|
||||
const limit = (value: unknown, max: number): string | undefined => {
|
||||
const s = norm(value);
|
||||
return s ? s.slice(0, max) : undefined;
|
||||
};
|
||||
const normalizedEmail = norm(userData.email)?.toLowerCase();
|
||||
const normalizedOktaSub = norm(userData.oktaSub);
|
||||
if (!normalizedEmail || !normalizedOktaSub) {
|
||||
throw new Error('Email and Okta sub are required');
|
||||
}
|
||||
|
||||
// Prepare user data with defaults for missing fields
|
||||
// If firstName/lastName are missing, try to extract from displayName
|
||||
let firstName = userData.firstName || '';
|
||||
@ -294,13 +348,17 @@ export class AuthService {
|
||||
displayName = userData.email.split('@')[0] || 'User';
|
||||
}
|
||||
|
||||
firstName = limit(firstName, 100) || '';
|
||||
lastName = limit(lastName, 100) || '';
|
||||
displayName = limit(displayName, 200) || normalizedEmail.split('@')[0] || 'User';
|
||||
|
||||
const sessionToken = uuidv4();
|
||||
const lastLoginDevice = parseDeviceFromUserAgent(userAgent);
|
||||
|
||||
// Prepare update/create data - always include required fields
|
||||
const userUpdateData: any = {
|
||||
email: userData.email,
|
||||
oktaSub: userData.oktaSub,
|
||||
email: normalizedEmail,
|
||||
oktaSub: normalizedOktaSub,
|
||||
lastLogin: new Date(),
|
||||
sessionToken,
|
||||
lastLoginDevice,
|
||||
@ -311,25 +369,36 @@ export class AuthService {
|
||||
if (firstName) userUpdateData.firstName = firstName;
|
||||
if (lastName) userUpdateData.lastName = lastName;
|
||||
if (displayName) userUpdateData.displayName = displayName;
|
||||
if (userData.employeeId) userUpdateData.employeeId = userData.employeeId; // Optional
|
||||
if (userData.department) userUpdateData.department = userData.department;
|
||||
if (userData.designation) userUpdateData.designation = userData.designation;
|
||||
if (userData.phone) userUpdateData.phone = userData.phone;
|
||||
if (userData.manager) userUpdateData.manager = userData.manager; // Manager name from SSO
|
||||
if (userData.jobTitle) userUpdateData.jobTitle = userData.jobTitle; // Job title from SSO
|
||||
if (userData.postalAddress) userUpdateData.postalAddress = userData.postalAddress; // Address from SSO
|
||||
if (userData.mobilePhone) userUpdateData.mobilePhone = userData.mobilePhone; // Mobile phone from SSO
|
||||
if (userData.employeeNumber || userData.dealerCode) {
|
||||
userUpdateData.employeeNumber = userData.employeeNumber || userData.dealerCode;
|
||||
if (limit(userData.employeeId, 50)) userUpdateData.employeeId = limit(userData.employeeId, 50); // Optional
|
||||
if (limit(userData.department, 100)) userUpdateData.department = limit(userData.department, 100);
|
||||
if (limit(userData.designation, 100)) userUpdateData.designation = limit(userData.designation, 100);
|
||||
if (limit(userData.phone, 20)) userUpdateData.phone = limit(userData.phone, 20);
|
||||
if (limit(userData.manager, 200)) userUpdateData.manager = limit(userData.manager, 200); // Manager name from SSO
|
||||
if (limit(userData.jobTitle, 3000)) userUpdateData.jobTitle = limit(userData.jobTitle, 3000); // Job title from SSO
|
||||
if (limit(userData.postalAddress, 500)) userUpdateData.postalAddress = limit(userData.postalAddress, 500); // Address from SSO
|
||||
if (limit(userData.mobilePhone, 20)) userUpdateData.mobilePhone = limit(userData.mobilePhone, 20); // Mobile phone from SSO
|
||||
if (limit(userData.secondEmail, 255)) userUpdateData.secondEmail = limit(userData.secondEmail, 255);
|
||||
const employeeNumber = limit(userData.employeeNumber || userData.dealerCode, 50);
|
||||
if (employeeNumber) {
|
||||
userUpdateData.employeeNumber = employeeNumber;
|
||||
}
|
||||
if (userData.adGroups && Array.isArray(userData.adGroups) && userData.adGroups.length > 0) {
|
||||
userUpdateData.adGroups = userData.adGroups; // Group memberships from SSO
|
||||
userUpdateData.adGroups = userData.adGroups
|
||||
.map((group: unknown) => limit(group, 255))
|
||||
.filter(Boolean)
|
||||
.slice(0, 200); // Group memberships from SSO
|
||||
}
|
||||
|
||||
// Check if user exists by email (primary identifier)
|
||||
// Prefer matching by oktaSub, then fallback to email.
|
||||
// This avoids collisions when email changes in IdP over time.
|
||||
let user = await User.findOne({
|
||||
where: { email: userData.email }
|
||||
where: { oktaSub: normalizedOktaSub }
|
||||
});
|
||||
if (!user) {
|
||||
user = await User.findOne({
|
||||
where: { email: normalizedEmail }
|
||||
});
|
||||
}
|
||||
|
||||
if (user) {
|
||||
// Update existing user - update oktaSub if different, and other fields
|
||||
@ -353,21 +422,22 @@ export class AuthService {
|
||||
} else {
|
||||
// Create new user with required fields (email and oktaSub)
|
||||
user = await User.create({
|
||||
email: userData.email,
|
||||
oktaSub: userData.oktaSub,
|
||||
email: normalizedEmail,
|
||||
oktaSub: normalizedOktaSub,
|
||||
employeeId: userData.employeeId || null, // Optional
|
||||
firstName: firstName || null,
|
||||
lastName: lastName || null,
|
||||
displayName: displayName,
|
||||
department: userData.department || null,
|
||||
designation: userData.designation || null,
|
||||
phone: userData.phone || null,
|
||||
manager: userData.manager || null, // Manager name from SSO
|
||||
jobTitle: userData.jobTitle || null, // Job title from SSO
|
||||
postalAddress: userData.postalAddress || null, // Address from SSO
|
||||
mobilePhone: userData.mobilePhone || null,
|
||||
adGroups: userData.adGroups && Array.isArray(userData.adGroups) && userData.adGroups.length > 0 ? userData.adGroups : null,
|
||||
employeeNumber: userData.employeeNumber || userData.dealerCode || null,
|
||||
department: limit(userData.department, 100) || null,
|
||||
designation: limit(userData.designation, 100) || null,
|
||||
phone: limit(userData.phone, 20) || null,
|
||||
manager: limit(userData.manager, 200) || null, // Manager name from SSO
|
||||
jobTitle: limit(userData.jobTitle, 3000) || null, // Job title from SSO
|
||||
postalAddress: limit(userData.postalAddress, 500) || null, // Address from SSO
|
||||
mobilePhone: limit(userData.mobilePhone, 20) || null,
|
||||
secondEmail: limit(userData.secondEmail, 255) || null,
|
||||
adGroups: userUpdateData.adGroups && Array.isArray(userUpdateData.adGroups) && userUpdateData.adGroups.length > 0 ? userUpdateData.adGroups : null,
|
||||
employeeNumber: limit(userData.employeeNumber || userData.dealerCode, 50) || null,
|
||||
isActive: true,
|
||||
role: 'USER',
|
||||
lastLogin: new Date(),
|
||||
@ -860,11 +930,12 @@ export class AuthService {
|
||||
*/
|
||||
async exchangeCodeForTokens(code: string, redirectUri: string, userAgent?: string): Promise<LoginResponse> {
|
||||
try {
|
||||
const oktaConfigForRequest = this.resolveOktaConfigForRedirectUri(redirectUri);
|
||||
// Validate configuration
|
||||
if (!ssoConfig.oktaClientId || ssoConfig.oktaClientId.trim() === '') {
|
||||
if (!oktaConfigForRequest.clientId || oktaConfigForRequest.clientId.trim() === '') {
|
||||
throw new Error('OKTA_CLIENT_ID is not configured. Please set it in your .env file.');
|
||||
}
|
||||
if (!ssoConfig.oktaClientSecret || ssoConfig.oktaClientSecret.trim() === '' || ssoConfig.oktaClientSecret.includes('your_okta_client_secret')) {
|
||||
if (!oktaConfigForRequest.clientSecret || oktaConfigForRequest.clientSecret.trim() === '' || oktaConfigForRequest.clientSecret.includes('your_okta_client_secret')) {
|
||||
throw new Error('OKTA_CLIENT_SECRET is not configured. Please set it in your .env file.');
|
||||
}
|
||||
if (!code || code.trim() === '') {
|
||||
@ -876,8 +947,27 @@ export class AuthService {
|
||||
|
||||
const normalize = (s: string) => s.trim().replace(/\/+$/, '');
|
||||
const providedRedirectUri = normalize(redirectUri);
|
||||
const frontendBase = process.env.FRONTEND_URL ? normalize(process.env.FRONTEND_URL) : '';
|
||||
const canonicalRedirectUri = frontendBase ? `${frontendBase}/login/callback` : providedRedirectUri;
|
||||
const configuredFrontendBases = (process.env.FRONTEND_URL || '')
|
||||
.split(',')
|
||||
.map((s) => normalize(s))
|
||||
.filter(Boolean);
|
||||
const providedOrigin = (() => {
|
||||
try {
|
||||
return normalize(new URL(providedRedirectUri).origin);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
})();
|
||||
const matchingConfiguredBase = configuredFrontendBases.find((base) => {
|
||||
try {
|
||||
return normalize(new URL(base).origin).toLowerCase() === providedOrigin.toLowerCase();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const fallbackConfiguredBase = configuredFrontendBases[0] || '';
|
||||
const selectedFrontendBase = matchingConfiguredBase || fallbackConfiguredBase;
|
||||
const canonicalRedirectUri = selectedFrontendBase ? `${selectedFrontendBase}/login/callback` : providedRedirectUri;
|
||||
const isSecureEnv = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'uat';
|
||||
const effectiveRedirectUri = isSecureEnv ? canonicalRedirectUri : providedRedirectUri;
|
||||
|
||||
@ -885,13 +975,16 @@ export class AuthService {
|
||||
redirectUri: effectiveRedirectUri,
|
||||
providedRedirectUri,
|
||||
canonicalRedirectUri,
|
||||
configuredFrontendBases,
|
||||
selectedFrontendBase,
|
||||
oktaProfile: oktaConfigForRequest.profile,
|
||||
codePrefix: code.substring(0, 10) + '...',
|
||||
oktaDomain: ssoConfig.oktaDomain,
|
||||
clientId: ssoConfig.oktaClientId,
|
||||
hasClientSecret: !!ssoConfig.oktaClientSecret && !ssoConfig.oktaClientSecret.includes('your_okta_client_secret'),
|
||||
oktaDomain: oktaConfigForRequest.domain,
|
||||
clientId: oktaConfigForRequest.clientId,
|
||||
hasClientSecret: !!oktaConfigForRequest.clientSecret && !oktaConfigForRequest.clientSecret.includes('your_okta_client_secret'),
|
||||
});
|
||||
|
||||
const tokenEndpoint = `${ssoConfig.oktaDomain}/oauth2/default/v1/token`;
|
||||
const tokenEndpoint = `${oktaConfigForRequest.domain}/oauth2/default/v1/token`;
|
||||
|
||||
// Exchange authorization code for tokens
|
||||
// redirect_uri here must match the one used when requesting the authorization code
|
||||
@ -901,8 +994,8 @@ export class AuthService {
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: effectiveRedirectUri, // Must match authorize request redirect_uri exactly
|
||||
client_id: ssoConfig.oktaClientId,
|
||||
client_secret: ssoConfig.oktaClientSecret,
|
||||
client_id: oktaConfigForRequest.clientId,
|
||||
client_secret: oktaConfigForRequest.clientSecret,
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
@ -957,7 +1050,7 @@ export class AuthService {
|
||||
|
||||
// Step 1: Try to get user info from Okta Users API (full profile with manager, employeeID, etc.)
|
||||
// First, get oktaSub from userinfo to use as user ID
|
||||
const userInfoEndpoint = `${ssoConfig.oktaDomain}/oauth2/default/v1/userinfo`;
|
||||
const userInfoEndpoint = `${oktaConfigForRequest.domain}/oauth2/default/v1/userinfo`;
|
||||
const userInfoResponse = await axios.get(userInfoEndpoint, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
@ -973,7 +1066,13 @@ export class AuthService {
|
||||
|
||||
// Try Users API first (provides full profile including manager, employeeID, etc.)
|
||||
let userData: SSOUserData | null = null;
|
||||
const usersApiResponse = await this.fetchUserFromOktaUsersAPI(oktaSub, oktaUserInfo.email || '', access_token);
|
||||
const usersApiResponse = await this.fetchUserFromOktaUsersAPI(
|
||||
oktaSub,
|
||||
oktaUserInfo.email || '',
|
||||
access_token,
|
||||
oktaConfigForRequest.domain,
|
||||
oktaConfigForRequest.apiToken
|
||||
);
|
||||
|
||||
if (usersApiResponse) {
|
||||
userData = this.extractUserDataFromUsersAPI(usersApiResponse, oktaSub);
|
||||
@ -1062,8 +1161,38 @@ export class AuthService {
|
||||
throw new Error('Redirect URI is required');
|
||||
}
|
||||
|
||||
const normalize = (s: string) => s.trim().replace(/\/+$/, '');
|
||||
const providedRedirectUri = normalize(redirectUri);
|
||||
const configuredFrontendBases = (process.env.FRONTEND_URL || '')
|
||||
.split(',')
|
||||
.map((s) => normalize(s))
|
||||
.filter(Boolean);
|
||||
const providedOrigin = (() => {
|
||||
try {
|
||||
return normalize(new URL(providedRedirectUri).origin);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
})();
|
||||
const matchingConfiguredBase = configuredFrontendBases.find((base) => {
|
||||
try {
|
||||
return normalize(new URL(base).origin).toLowerCase() === providedOrigin.toLowerCase();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const fallbackConfiguredBase = configuredFrontendBases[0] || '';
|
||||
const selectedFrontendBase = matchingConfiguredBase || fallbackConfiguredBase;
|
||||
const canonicalRedirectUri = selectedFrontendBase ? `${selectedFrontendBase}/login/callback` : providedRedirectUri;
|
||||
const isSecureEnv = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'uat';
|
||||
const effectiveRedirectUri = isSecureEnv ? canonicalRedirectUri : providedRedirectUri;
|
||||
|
||||
logger.info('Exchanging code with Tanflow', {
|
||||
redirectUri,
|
||||
redirectUri: effectiveRedirectUri,
|
||||
providedRedirectUri,
|
||||
canonicalRedirectUri,
|
||||
configuredFrontendBases,
|
||||
selectedFrontendBase,
|
||||
codePrefix: code.substring(0, 10) + '...',
|
||||
tanflowBaseUrl: ssoConfig.tanflowBaseUrl,
|
||||
clientId: ssoConfig.tanflowClientId,
|
||||
@ -1078,7 +1207,7 @@ export class AuthService {
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: redirectUri,
|
||||
redirect_uri: effectiveRedirectUri,
|
||||
client_id: ssoConfig.tanflowClientId!,
|
||||
client_secret: ssoConfig.tanflowClientSecret!,
|
||||
}),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user