202 lines
6.7 KiB
TypeScript
202 lines
6.7 KiB
TypeScript
/**
|
|
* Tanflow Authentication Service
|
|
* Handles OAuth flow with Tanflow IAM Suite
|
|
*/
|
|
|
|
import { TokenManager } from '../utils/tokenManager';
|
|
import axios from 'axios';
|
|
|
|
const TANFLOW_BASE_URL = import.meta.env.VITE_TANFLOW_BASE_URL || '';
|
|
const TANFLOW_CLIENT_ID = import.meta.env.VITE_TANFLOW_CLIENT_ID || '';
|
|
const TANFLOW_REDIRECT_URI = `${window.location.origin}/login/callback`;
|
|
|
|
/**
|
|
* Initiate Tanflow SSO login
|
|
* Redirects user to Tanflow authorization endpoint
|
|
*/
|
|
export function initiateTanflowLogin(): void {
|
|
// Check if we're coming from a logout - if so, add prompt=login to force re-authentication
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const isAfterLogout = urlParams.has('logout') || urlParams.has('tanflow_logged_out');
|
|
|
|
// Clear any previous logout flags before starting new login
|
|
if (isAfterLogout) {
|
|
sessionStorage.removeItem('tanflow_logged_out');
|
|
sessionStorage.removeItem('__logout_in_progress__');
|
|
sessionStorage.removeItem('__force_logout__');
|
|
console.log('🚪 Cleared logout flags before initiating Tanflow login');
|
|
}
|
|
|
|
const state = Math.random().toString(36).substring(7);
|
|
// Store provider type and state to identify Tanflow callback
|
|
sessionStorage.setItem('auth_provider', 'tanflow');
|
|
sessionStorage.setItem('tanflow_auth_state', state);
|
|
|
|
let authUrl = `${TANFLOW_BASE_URL}/protocol/openid-connect/auth?` +
|
|
`client_id=${TANFLOW_CLIENT_ID}&` +
|
|
`response_type=code&` +
|
|
`scope=openid&` +
|
|
`redirect_uri=${encodeURIComponent(TANFLOW_REDIRECT_URI)}&` +
|
|
`state=${state}`;
|
|
|
|
// Add prompt=login if coming from logout to force re-authentication
|
|
// This ensures Tanflow requires login even if a session still exists
|
|
if (isAfterLogout) {
|
|
authUrl += `&prompt=login`;
|
|
console.log('🚪 Adding prompt=login to force re-authentication after logout');
|
|
}
|
|
|
|
console.log('🚪 Initiating Tanflow login', { isAfterLogout, hasPrompt: isAfterLogout });
|
|
window.location.href = authUrl;
|
|
}
|
|
|
|
/**
|
|
* Exchange authorization code for tokens via backend
|
|
* Backend handles the token exchange securely with client secret
|
|
*/
|
|
export async function exchangeTanflowCodeForTokens(
|
|
code: string,
|
|
state: string
|
|
): Promise<{
|
|
accessToken: string;
|
|
refreshToken: string;
|
|
idToken: string;
|
|
user: any;
|
|
}> {
|
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
|
|
|
|
try {
|
|
const response = await axios.post(
|
|
`${API_BASE_URL}/auth/tanflow/token-exchange`,
|
|
{
|
|
code,
|
|
redirectUri: TANFLOW_REDIRECT_URI,
|
|
state,
|
|
},
|
|
{
|
|
withCredentials: true,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}
|
|
);
|
|
|
|
const data = response.data?.data || response.data;
|
|
|
|
// Store tokens
|
|
if (data.accessToken) {
|
|
TokenManager.setAccessToken(data.accessToken);
|
|
}
|
|
if (data.refreshToken) {
|
|
TokenManager.setRefreshToken(data.refreshToken);
|
|
}
|
|
if (data.idToken) {
|
|
TokenManager.setIdToken(data.idToken);
|
|
}
|
|
if (data.user) {
|
|
TokenManager.setUserData(data.user);
|
|
}
|
|
|
|
return data;
|
|
} catch (error: any) {
|
|
console.error('❌ Tanflow token exchange failed:', {
|
|
message: error.message,
|
|
response: error.response?.data,
|
|
status: error.response?.status,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh access token using refresh token
|
|
*/
|
|
export async function refreshTanflowToken(): Promise<string> {
|
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
|
|
const refreshToken = TokenManager.getRefreshToken();
|
|
|
|
if (!refreshToken) {
|
|
throw new Error('No refresh token available');
|
|
}
|
|
|
|
try {
|
|
const response = await axios.post(
|
|
`${API_BASE_URL}/auth/tanflow/refresh`,
|
|
{ refreshToken },
|
|
{
|
|
withCredentials: true,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}
|
|
);
|
|
|
|
const data = response.data?.data || response.data;
|
|
const accessToken = data.accessToken;
|
|
|
|
if (accessToken) {
|
|
TokenManager.setAccessToken(accessToken);
|
|
return accessToken;
|
|
}
|
|
|
|
throw new Error('Failed to refresh token');
|
|
} catch (error: any) {
|
|
console.error('❌ Tanflow token refresh failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Logout from Tanflow
|
|
* Uses id_token for logout and redirects back to app
|
|
* Note: This should be called AFTER backend logout API is called and tokens are cleared
|
|
* DO NOT clear tokens here - they should already be cleared by AuthContext
|
|
*/
|
|
export function tanflowLogout(idToken: string): void {
|
|
if (!idToken) {
|
|
console.warn('🚪 No id_token available for Tanflow logout, redirecting to home');
|
|
// Fallback: redirect to home with logout flags (similar to OKTA approach)
|
|
const homeUrl = `${window.location.origin}/?tanflow_logged_out=true&logout=${Date.now()}`;
|
|
window.location.replace(homeUrl);
|
|
return;
|
|
}
|
|
|
|
// Build Tanflow logout URL with redirect back to login callback
|
|
// IMPORTANT: Use the base redirect URI (without query params) to match registered URIs
|
|
// Tanflow requires exact match with registered "Valid Post Logout Redirect URIs"
|
|
// The same URI used for login should be registered for logout
|
|
// Using the base URI ensures it matches what's registered in Tanflow client config
|
|
const postLogoutRedirectUri = TANFLOW_REDIRECT_URI; // Use base URI without query params
|
|
|
|
// Construct logout URL - ensure all parameters are properly encoded
|
|
// Keycloak/Tanflow expects: client_id, id_token_hint, post_logout_redirect_uri
|
|
const logoutUrl = new URL(`${TANFLOW_BASE_URL}/protocol/openid-connect/logout`);
|
|
logoutUrl.searchParams.set('client_id', TANFLOW_CLIENT_ID);
|
|
logoutUrl.searchParams.set('id_token_hint', idToken);
|
|
logoutUrl.searchParams.set('post_logout_redirect_uri', postLogoutRedirectUri);
|
|
|
|
const finalLogoutUrl = logoutUrl.toString();
|
|
|
|
console.log('🚪 Tanflow logout initiated', {
|
|
hasIdToken: !!idToken,
|
|
idTokenPrefix: idToken ? idToken.substring(0, 20) + '...' : 'none',
|
|
postLogoutRedirectUri,
|
|
logoutUrlBase: `${TANFLOW_BASE_URL}/protocol/openid-connect/logout`,
|
|
finalLogoutUrl: finalLogoutUrl.replace(idToken.substring(0, 20), '***'),
|
|
});
|
|
|
|
// DO NOT clear auth_provider here - we need it to detect Tanflow callback
|
|
// The logout flags should already be set by AuthContext
|
|
// Just ensure they're there
|
|
sessionStorage.setItem('__logout_in_progress__', 'true');
|
|
sessionStorage.setItem('__force_logout__', 'true');
|
|
// Don't set tanflow_logged_out here - it will be set when Tanflow redirects back
|
|
|
|
// Redirect to Tanflow logout endpoint
|
|
// Tanflow will clear the session and redirect back to post_logout_redirect_uri
|
|
// The redirect will include tanflow_logged_out=true in the query params
|
|
console.log('🚪 Redirecting to Tanflow logout endpoint...');
|
|
window.location.href = finalLogoutUrl;
|
|
}
|
|
|