chore: update CHANGELOG.md to reflect improvements in authentication flow, including seamless sign-in from the Ciphera portal and enhanced cookie management for better security and user experience
This commit is contained in:
@@ -32,8 +32,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Seamless sign-in from Auth.** When you click "Sign in" on Pulse and complete authentication in the Ciphera Auth portal, you now return to Pulse fully authenticated without any loading loops or errors. We fixed CSRF handling and cookie forwarding issues that were causing 403 errors after OAuth callback, so the transition between apps is now smooth and reliable.
|
||||
- **Sign in after inactivity.** Clicking "Sign in" after a period of inactivity no longer does nothing. Previously, stale refresh cookies caused the middleware to redirect away from the login page; now only a valid access token triggers that redirect, so you can complete OAuth sign-in when your session has expired.
|
||||
- **Seamless sign-in from the Ciphera portal.** When you click "Sign in" on Pulse and authenticate through the Ciphera Auth portal, you now return to Pulse fully logged in without any loading loops or error messages. Previously, you might see console errors or have to sign in twice—now the handoff between apps is smooth and reliable.
|
||||
- **Sign in after inactivity.** Clicking "Sign in" after a period of inactivity no longer does nothing. Previously, you could get stuck in a redirect loop and never reach the login page; now you can always complete sign-in, even when your session has expired.
|
||||
- **Frequent re-login.** You no longer have to sign in multiple times a day. When the access token expires after 15 minutes of inactivity, the app now automatically refreshes it using your refresh token on the next page load, so you stay logged in for up to 30 days.
|
||||
- **2FA disable now requires password confirmation.** Disabling 2FA sends the derived password to the backend for verification. This prevents an attacker with a hijacked session from stripping 2FA.
|
||||
- **More accurate visitor tracking.** We fixed rare edge cases where visitor counts could be slightly off during busy traffic spikes. Previously, the timestamp-based session ID generation could occasionally create overlapping identifiers. Every visitor now gets a truly unique UUID that never overlaps with others, ensuring your analytics are always precise.
|
||||
|
||||
@@ -35,11 +35,15 @@ export type AuthExchangeErrorType = 'network' | 'expired' | 'invalid' | 'server'
|
||||
|
||||
export async function exchangeAuthCode(code: string, codeVerifier: string | null, redirectUri: string) {
|
||||
try {
|
||||
// * IMPORTANT: credentials: 'include' is required to receive httpOnly cookies from Auth API
|
||||
// * The Auth API sets access_token, refresh_token, and csrf_token as httpOnly cookies
|
||||
// * We must forward these to the browser for cross-subdomain auth to work
|
||||
const res = await fetch(`${AUTH_API_URL}/oauth/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include', // * Critical: receives httpOnly cookies from Auth API
|
||||
body: JSON.stringify({
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
@@ -91,10 +95,40 @@ export async function exchangeAuthCode(code: string, codeVerifier: string | null
|
||||
maxAge: 60 * 60 * 24 * 30 // 30 days
|
||||
})
|
||||
|
||||
// * Note: CSRF token should be set by Auth API login flow and available via cookie
|
||||
// * If the Auth API returns a CSRF token in header, we forward it
|
||||
// * Forward cookies from Auth API response to browser
|
||||
// * The Auth API sets httpOnly cookies on auth.ciphera.net - we need to mirror them on pulse.ciphera.net
|
||||
const setCookieHeaders = res.headers.getSetCookie()
|
||||
if (setCookieHeaders && setCookieHeaders.length > 0) {
|
||||
for (const cookieStr of setCookieHeaders) {
|
||||
// * Parse Set-Cookie header (format: name=value; attributes...)
|
||||
const [nameValue] = cookieStr.split(';')
|
||||
const [name, value] = nameValue.trim().split('=')
|
||||
|
||||
if (name && value) {
|
||||
// * Determine if httpOnly (default true for security)
|
||||
const isHttpOnly = cookieStr.toLowerCase().includes('httponly')
|
||||
// * Determine sameSite (default lax)
|
||||
const sameSiteMatch = cookieStr.match(/samesite=(\w+)/i)
|
||||
const sameSite = (sameSiteMatch?.[1]?.toLowerCase() as 'strict' | 'lax' | 'none') || 'lax'
|
||||
// * Extract max-age if present
|
||||
const maxAgeMatch = cookieStr.match(/max-age=(\d+)/i)
|
||||
const maxAge = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) : 60 * 60 * 24 * 30
|
||||
|
||||
cookieStore.set(name.trim(), decodeURIComponent(value.trim()), {
|
||||
httpOnly: isHttpOnly,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: sameSite,
|
||||
path: '/',
|
||||
domain: cookieDomain,
|
||||
maxAge: maxAge
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// * Also check for CSRF token in response header (fallback)
|
||||
const csrfToken = res.headers.get('X-CSRF-Token')
|
||||
if (csrfToken) {
|
||||
if (csrfToken && !cookieStore.get('csrf_token')) {
|
||||
cookieStore.set('csrf_token', csrfToken, {
|
||||
httpOnly: false, // * Must be readable by JS for CSRF protection
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
|
||||
Reference in New Issue
Block a user