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 -->
|
<!-- Preload essential fonts and icons -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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/charts-vendor-CmYZJIYl.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CLtqm-Ae.js">
|
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CLtqm-Ae.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
|
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
|
||||||
|
|||||||
@ -30,14 +30,54 @@ function parseDeviceFromUserAgent(ua?: string): string {
|
|||||||
|
|
||||||
|
|
||||||
export class AuthService {
|
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.)
|
* 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
|
* 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 {
|
try {
|
||||||
|
const oktaDomain = oktaDomainOverride || ssoConfig.oktaDomain;
|
||||||
|
const oktaApiToken = oktaApiTokenOverride || ssoConfig.oktaApiToken;
|
||||||
// Check if API token is configured
|
// 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');
|
logger.info('OKTA_API_TOKEN not configured, will use userinfo endpoint as fallback');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -48,17 +88,17 @@ export class AuthService {
|
|||||||
|
|
||||||
// First attempt: Use email (preferred method as shown in curl example)
|
// First attempt: Use email (preferred method as shown in curl example)
|
||||||
if (email) {
|
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)', {
|
logger.info('Fetching user from Okta Users API (using email)', {
|
||||||
endpoint: usersApiEndpoint.replace(email, email.substring(0, 5) + '...'),
|
endpoint: usersApiEndpoint.replace(email, email.substring(0, 5) + '...'),
|
||||||
hasApiToken: !!ssoConfig.oktaApiToken,
|
hasApiToken: !!oktaApiToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(usersApiEndpoint, {
|
const response = await axios.get(usersApiEndpoint, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `SSWS ${ssoConfig.oktaApiToken}`,
|
'Authorization': `SSWS ${oktaApiToken}`,
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
},
|
},
|
||||||
validateStatus: (status) => status < 500, // Don't throw on 4xx errors
|
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
|
// Second attempt: Use oktaSub (user ID) if email lookup failed
|
||||||
if (oktaSub) {
|
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)', {
|
logger.info('Fetching user from Okta Users API (using oktaSub)', {
|
||||||
endpoint: usersApiEndpoint.replace(oktaSub, oktaSub.substring(0, 10) + '...'),
|
endpoint: usersApiEndpoint.replace(oktaSub, oktaSub.substring(0, 10) + '...'),
|
||||||
hasApiToken: !!ssoConfig.oktaApiToken,
|
hasApiToken: !!oktaApiToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(usersApiEndpoint, {
|
const response = await axios.get(usersApiEndpoint, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `SSWS ${ssoConfig.oktaApiToken}`,
|
'Authorization': `SSWS ${oktaApiToken}`,
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
},
|
},
|
||||||
validateStatus: (status) => status < 500,
|
validateStatus: (status) => status < 500,
|
||||||
@ -269,6 +309,20 @@ export class AuthService {
|
|||||||
throw new Error('Email and Okta sub are required');
|
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
|
// Prepare user data with defaults for missing fields
|
||||||
// If firstName/lastName are missing, try to extract from displayName
|
// If firstName/lastName are missing, try to extract from displayName
|
||||||
let firstName = userData.firstName || '';
|
let firstName = userData.firstName || '';
|
||||||
@ -294,13 +348,17 @@ export class AuthService {
|
|||||||
displayName = userData.email.split('@')[0] || 'User';
|
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 sessionToken = uuidv4();
|
||||||
const lastLoginDevice = parseDeviceFromUserAgent(userAgent);
|
const lastLoginDevice = parseDeviceFromUserAgent(userAgent);
|
||||||
|
|
||||||
// Prepare update/create data - always include required fields
|
// Prepare update/create data - always include required fields
|
||||||
const userUpdateData: any = {
|
const userUpdateData: any = {
|
||||||
email: userData.email,
|
email: normalizedEmail,
|
||||||
oktaSub: userData.oktaSub,
|
oktaSub: normalizedOktaSub,
|
||||||
lastLogin: new Date(),
|
lastLogin: new Date(),
|
||||||
sessionToken,
|
sessionToken,
|
||||||
lastLoginDevice,
|
lastLoginDevice,
|
||||||
@ -311,25 +369,36 @@ export class AuthService {
|
|||||||
if (firstName) userUpdateData.firstName = firstName;
|
if (firstName) userUpdateData.firstName = firstName;
|
||||||
if (lastName) userUpdateData.lastName = lastName;
|
if (lastName) userUpdateData.lastName = lastName;
|
||||||
if (displayName) userUpdateData.displayName = displayName;
|
if (displayName) userUpdateData.displayName = displayName;
|
||||||
if (userData.employeeId) userUpdateData.employeeId = userData.employeeId; // Optional
|
if (limit(userData.employeeId, 50)) userUpdateData.employeeId = limit(userData.employeeId, 50); // Optional
|
||||||
if (userData.department) userUpdateData.department = userData.department;
|
if (limit(userData.department, 100)) userUpdateData.department = limit(userData.department, 100);
|
||||||
if (userData.designation) userUpdateData.designation = userData.designation;
|
if (limit(userData.designation, 100)) userUpdateData.designation = limit(userData.designation, 100);
|
||||||
if (userData.phone) userUpdateData.phone = userData.phone;
|
if (limit(userData.phone, 20)) userUpdateData.phone = limit(userData.phone, 20);
|
||||||
if (userData.manager) userUpdateData.manager = userData.manager; // Manager name from SSO
|
if (limit(userData.manager, 200)) userUpdateData.manager = limit(userData.manager, 200); // Manager name from SSO
|
||||||
if (userData.jobTitle) userUpdateData.jobTitle = userData.jobTitle; // Job title from SSO
|
if (limit(userData.jobTitle, 3000)) userUpdateData.jobTitle = limit(userData.jobTitle, 3000); // Job title from SSO
|
||||||
if (userData.postalAddress) userUpdateData.postalAddress = userData.postalAddress; // Address from SSO
|
if (limit(userData.postalAddress, 500)) userUpdateData.postalAddress = limit(userData.postalAddress, 500); // Address from SSO
|
||||||
if (userData.mobilePhone) userUpdateData.mobilePhone = userData.mobilePhone; // Mobile phone from SSO
|
if (limit(userData.mobilePhone, 20)) userUpdateData.mobilePhone = limit(userData.mobilePhone, 20); // Mobile phone from SSO
|
||||||
if (userData.employeeNumber || userData.dealerCode) {
|
if (limit(userData.secondEmail, 255)) userUpdateData.secondEmail = limit(userData.secondEmail, 255);
|
||||||
userUpdateData.employeeNumber = userData.employeeNumber || userData.dealerCode;
|
const employeeNumber = limit(userData.employeeNumber || userData.dealerCode, 50);
|
||||||
|
if (employeeNumber) {
|
||||||
|
userUpdateData.employeeNumber = employeeNumber;
|
||||||
}
|
}
|
||||||
if (userData.adGroups && Array.isArray(userData.adGroups) && userData.adGroups.length > 0) {
|
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({
|
let user = await User.findOne({
|
||||||
where: { email: userData.email }
|
where: { oktaSub: normalizedOktaSub }
|
||||||
});
|
});
|
||||||
|
if (!user) {
|
||||||
|
user = await User.findOne({
|
||||||
|
where: { email: normalizedEmail }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
// Update existing user - update oktaSub if different, and other fields
|
// Update existing user - update oktaSub if different, and other fields
|
||||||
@ -353,21 +422,22 @@ export class AuthService {
|
|||||||
} else {
|
} else {
|
||||||
// Create new user with required fields (email and oktaSub)
|
// Create new user with required fields (email and oktaSub)
|
||||||
user = await User.create({
|
user = await User.create({
|
||||||
email: userData.email,
|
email: normalizedEmail,
|
||||||
oktaSub: userData.oktaSub,
|
oktaSub: normalizedOktaSub,
|
||||||
employeeId: userData.employeeId || null, // Optional
|
employeeId: userData.employeeId || null, // Optional
|
||||||
firstName: firstName || null,
|
firstName: firstName || null,
|
||||||
lastName: lastName || null,
|
lastName: lastName || null,
|
||||||
displayName: displayName,
|
displayName: displayName,
|
||||||
department: userData.department || null,
|
department: limit(userData.department, 100) || null,
|
||||||
designation: userData.designation || null,
|
designation: limit(userData.designation, 100) || null,
|
||||||
phone: userData.phone || null,
|
phone: limit(userData.phone, 20) || null,
|
||||||
manager: userData.manager || null, // Manager name from SSO
|
manager: limit(userData.manager, 200) || null, // Manager name from SSO
|
||||||
jobTitle: userData.jobTitle || null, // Job title from SSO
|
jobTitle: limit(userData.jobTitle, 3000) || null, // Job title from SSO
|
||||||
postalAddress: userData.postalAddress || null, // Address from SSO
|
postalAddress: limit(userData.postalAddress, 500) || null, // Address from SSO
|
||||||
mobilePhone: userData.mobilePhone || null,
|
mobilePhone: limit(userData.mobilePhone, 20) || null,
|
||||||
adGroups: userData.adGroups && Array.isArray(userData.adGroups) && userData.adGroups.length > 0 ? userData.adGroups : null,
|
secondEmail: limit(userData.secondEmail, 255) || null,
|
||||||
employeeNumber: userData.employeeNumber || userData.dealerCode || 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,
|
isActive: true,
|
||||||
role: 'USER',
|
role: 'USER',
|
||||||
lastLogin: new Date(),
|
lastLogin: new Date(),
|
||||||
@ -860,11 +930,12 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
async exchangeCodeForTokens(code: string, redirectUri: string, userAgent?: string): Promise<LoginResponse> {
|
async exchangeCodeForTokens(code: string, redirectUri: string, userAgent?: string): Promise<LoginResponse> {
|
||||||
try {
|
try {
|
||||||
|
const oktaConfigForRequest = this.resolveOktaConfigForRedirectUri(redirectUri);
|
||||||
// Validate configuration
|
// 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.');
|
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.');
|
throw new Error('OKTA_CLIENT_SECRET is not configured. Please set it in your .env file.');
|
||||||
}
|
}
|
||||||
if (!code || code.trim() === '') {
|
if (!code || code.trim() === '') {
|
||||||
@ -876,8 +947,27 @@ export class AuthService {
|
|||||||
|
|
||||||
const normalize = (s: string) => s.trim().replace(/\/+$/, '');
|
const normalize = (s: string) => s.trim().replace(/\/+$/, '');
|
||||||
const providedRedirectUri = normalize(redirectUri);
|
const providedRedirectUri = normalize(redirectUri);
|
||||||
const frontendBase = process.env.FRONTEND_URL ? normalize(process.env.FRONTEND_URL) : '';
|
const configuredFrontendBases = (process.env.FRONTEND_URL || '')
|
||||||
const canonicalRedirectUri = frontendBase ? `${frontendBase}/login/callback` : providedRedirectUri;
|
.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 isSecureEnv = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'uat';
|
||||||
const effectiveRedirectUri = isSecureEnv ? canonicalRedirectUri : providedRedirectUri;
|
const effectiveRedirectUri = isSecureEnv ? canonicalRedirectUri : providedRedirectUri;
|
||||||
|
|
||||||
@ -885,13 +975,16 @@ export class AuthService {
|
|||||||
redirectUri: effectiveRedirectUri,
|
redirectUri: effectiveRedirectUri,
|
||||||
providedRedirectUri,
|
providedRedirectUri,
|
||||||
canonicalRedirectUri,
|
canonicalRedirectUri,
|
||||||
|
configuredFrontendBases,
|
||||||
|
selectedFrontendBase,
|
||||||
|
oktaProfile: oktaConfigForRequest.profile,
|
||||||
codePrefix: code.substring(0, 10) + '...',
|
codePrefix: code.substring(0, 10) + '...',
|
||||||
oktaDomain: ssoConfig.oktaDomain,
|
oktaDomain: oktaConfigForRequest.domain,
|
||||||
clientId: ssoConfig.oktaClientId,
|
clientId: oktaConfigForRequest.clientId,
|
||||||
hasClientSecret: !!ssoConfig.oktaClientSecret && !ssoConfig.oktaClientSecret.includes('your_okta_client_secret'),
|
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
|
// Exchange authorization code for tokens
|
||||||
// redirect_uri here must match the one used when requesting the authorization code
|
// redirect_uri here must match the one used when requesting the authorization code
|
||||||
@ -901,8 +994,8 @@ export class AuthService {
|
|||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
code,
|
code,
|
||||||
redirect_uri: effectiveRedirectUri, // Must match authorize request redirect_uri exactly
|
redirect_uri: effectiveRedirectUri, // Must match authorize request redirect_uri exactly
|
||||||
client_id: ssoConfig.oktaClientId,
|
client_id: oktaConfigForRequest.clientId,
|
||||||
client_secret: ssoConfig.oktaClientSecret,
|
client_secret: oktaConfigForRequest.clientSecret,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
headers: {
|
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.)
|
// 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
|
// 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, {
|
const userInfoResponse = await axios.get(userInfoEndpoint, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${access_token}`,
|
Authorization: `Bearer ${access_token}`,
|
||||||
@ -973,7 +1066,13 @@ export class AuthService {
|
|||||||
|
|
||||||
// Try Users API first (provides full profile including manager, employeeID, etc.)
|
// Try Users API first (provides full profile including manager, employeeID, etc.)
|
||||||
let userData: SSOUserData | null = null;
|
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) {
|
if (usersApiResponse) {
|
||||||
userData = this.extractUserDataFromUsersAPI(usersApiResponse, oktaSub);
|
userData = this.extractUserDataFromUsersAPI(usersApiResponse, oktaSub);
|
||||||
@ -1062,8 +1161,38 @@ export class AuthService {
|
|||||||
throw new Error('Redirect URI is required');
|
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', {
|
logger.info('Exchanging code with Tanflow', {
|
||||||
redirectUri,
|
redirectUri: effectiveRedirectUri,
|
||||||
|
providedRedirectUri,
|
||||||
|
canonicalRedirectUri,
|
||||||
|
configuredFrontendBases,
|
||||||
|
selectedFrontendBase,
|
||||||
codePrefix: code.substring(0, 10) + '...',
|
codePrefix: code.substring(0, 10) + '...',
|
||||||
tanflowBaseUrl: ssoConfig.tanflowBaseUrl,
|
tanflowBaseUrl: ssoConfig.tanflowBaseUrl,
|
||||||
clientId: ssoConfig.tanflowClientId,
|
clientId: ssoConfig.tanflowClientId,
|
||||||
@ -1078,7 +1207,7 @@ export class AuthService {
|
|||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
code,
|
code,
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: effectiveRedirectUri,
|
||||||
client_id: ssoConfig.tanflowClientId!,
|
client_id: ssoConfig.tanflowClientId!,
|
||||||
client_secret: ssoConfig.tanflowClientSecret!,
|
client_secret: ssoConfig.tanflowClientSecret!,
|
||||||
}),
|
}),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user