Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions frontend/app/components/modules/auth/components/login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ import LfxMenuButton from '~/components/uikit/menu-button/menu-button.vue';
import LfxIcon from '~/components/uikit/icon/icon.vue';
import { links } from '~/config/links';

const { isAuthenticated, user, token, isLoading, login, logout } = useAuth();
const { isAuthenticated, user, isLoading, login, logout } = useAuth();
const authStore = useAuthStore();

const isOpen = ref(false);
Expand All @@ -91,10 +91,9 @@ const logoutHandler = async () => {

// Update auth store when authentication state changes
watch(
[isAuthenticated, token],
([newAuthVal, newToken]) => {
isAuthenticated,
(newAuthVal) => {
authStore.isAuthenticated = newAuthVal;
authStore.token = newToken || '';
authStore.user = user.value;
},
{ immediate: true },
Comment on lines 92 to 99
Expand Down
2 changes: 0 additions & 2 deletions frontend/app/components/modules/auth/store/auth.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ import { type User } from '~~/types/auth/auth-user.types';

export const useAuthStore = defineStore('auth', () => {
const isAuthenticated = ref(false);
const token = ref('');
const user = ref<User | null>(null);
const hasLfxInsightsPermission = computed(() => user.value?.hasLfxInsightsPermission || false);

return {
isAuthenticated,
token,
user,
hasLfxInsightsPermission,
};
Expand Down
1 change: 0 additions & 1 deletion frontend/app/plugins/auth.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export default defineNuxtPlugin(() => {
default: () => ({
isAuthenticated: false,
user: null,
token: null,
}),
server: false,
lazy: true,
Expand Down
4 changes: 0 additions & 4 deletions frontend/composables/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ declare const window: Window & typeof globalThis;
export const authState = ref<AuthData>({
isAuthenticated: false,
user: null,
token: null,
});

export const isAuthLoading = ref(false);
Expand Down Expand Up @@ -105,7 +104,6 @@ export const logout = async () => {
authState.value = {
isAuthenticated: false,
user: null,
token: null,
};

// Clear liked collections
Expand All @@ -131,12 +129,10 @@ export const logout = async () => {
export const useAuth = () => {
const isAuthenticated = computed(() => authState.value.isAuthenticated);
const user = computed(() => authState.value.user);
const token = computed(() => authState.value.token);

return {
isAuthenticated,
user,
token,
isLoading: isAuthLoading,
isReady: isAuthReady,
login,
Expand Down
8 changes: 4 additions & 4 deletions frontend/server/api/auth/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,15 +136,15 @@ export default defineEventHandler(async (event) => {
email_verified: decodedIdToken.email_verified,
updated_at: decodedIdToken.updated_at,
iss: config.public.auth0Domain,
// aud: config.public.auth0ClientId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (tokenResponse.expires_in || 86400),
claims,
hasLfxInsightsPermission: hasLfxInsightsPermission(claims as string[]),
isLfInsightsTeamMember: isLfInsightsTeamMember(decodedIdToken.email || ''),
// Include original tokens for reference if needed
// original_access_token: tokenResponse.access_token,
original_id_token: tokenResponse.id_token,
username: decodedIdToken['https://sso.linuxfoundation.org/claims/username'] as
| string
| undefined,
intercomJwt: decodedIdToken['http://lfx.dev/claims/intercom'] as string | undefined,
};

// Sign the custom OpenID Connect token with client secret
Expand Down
106 changes: 23 additions & 83 deletions frontend/server/api/auth/logout.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
// SPDX-License-Identifier: MIT

// Auth0 logout endpoint - no longer using OIDC discovery since Auth0 uses proprietary /v2/logout
import { getCookie, deleteCookie } from 'h3';
import jwt from 'jsonwebtoken';
import { deleteCookie } from 'h3';
import type { H3Event } from 'h3';
import { Pool } from 'pg';
import { isValidRedirectUrl } from '../../utils/redirect';
import { SecurityAuditRepository } from '../../repo/securityAudit.repo';
import type { DecodedOidcToken } from '~~/types/auth/auth-jwt.types';

const isProduction = process.env.NUXT_APP_ENV === 'production';

Expand Down Expand Up @@ -66,89 +64,31 @@ export default defineEventHandler(async (event) => {
}

try {
// Get the OIDC token for logout (don't delete yet - we need it for proper logout)
const oidcToken = getCookie(event, 'auth_oidc_token');

// If we have an OIDC token, extract the original ID token for proper Auth0 logout
if (oidcToken && config.auth0ClientSecret) {
try {
// Verify and decode the OIDC token to get the original ID token
const decodedToken = jwt.verify(oidcToken, config.auth0ClientSecret, {
algorithms: ['HS256'],
}) as DecodedOidcToken;

// Use the original ID token for Auth0 logout if available
const originalIdToken = decodedToken.original_id_token;

if (originalIdToken) {
// Skip OIDC discovery for Auth0 and construct logout URL manually
// Auth0 uses /v2/logout, not the standard OIDC /oidc/logout endpoint
const isProduction = process.env.NUXT_APP_ENV === 'production';
let logoutUrl: string;

// Strictly check the hostname using the URL API
let parsedAuth0Domain: URL;
try {
parsedAuth0Domain = new URL(
config.public.auth0Domain.startsWith('http')
? config.public.auth0Domain
: `https://${config.public.auth0Domain}`,
);
} catch {
parsedAuth0Domain = { hostname: '' } as URL; // fallback in case parsing fails
}

if (isProduction && parsedAuth0Domain.hostname === 'sso.linuxfoundation.org') {
// For Linux Foundation SSO, use their logout endpoint with ID token hint
const logoutParams = new URLSearchParams({
returnTo: returnToUrl,
client_id: config.public.auth0ClientId,
});

// Add ID token hint if available for proper SSO logout
if (originalIdToken) {
logoutParams.set('id_token_hint', originalIdToken);
}

logoutUrl = `https://sso.linuxfoundation.org/v2/logout?${logoutParams.toString()}`;
} else {
// For standard Auth0 domains, use the standard logout endpoint
const auth0Domain = config.public.auth0Domain.replace('https://', '');
const logoutParams = new URLSearchParams({
returnTo: returnToUrl,
client_id: config.public.auth0ClientId,
});

// Add ID token hint if available
if (originalIdToken) {
logoutParams.set('id_token_hint', originalIdToken);
}
// Construct Auth0 logout URL
let parsedAuth0Domain: URL;
try {
parsedAuth0Domain = new URL(
config.public.auth0Domain.startsWith('http')
? config.public.auth0Domain
: `https://${config.public.auth0Domain}`,
);
} catch {
parsedAuth0Domain = { hostname: '' } as URL;
}

logoutUrl = `https://${auth0Domain}/v2/logout?${logoutParams.toString()}`;
}
const logoutParams = new URLSearchParams({
returnTo: returnToUrl,
client_id: config.public.auth0ClientId,
});

// Clear all auth cookies after successful logout URL generation
if (isProduction) {
setOIDCCookie(event);
} else {
deleteCookie(event, 'auth_oidc_token');
}
deleteCookie(event, 'auth_refresh_token');
deleteCookie(event, 'auth_pkce');
deleteCookie(event, 'auth_redirect_to');
const auth0Base =
isProduction && parsedAuth0Domain.hostname === 'sso.linuxfoundation.org'
? 'https://sso.linuxfoundation.org'
: `https://${config.public.auth0Domain.replace('https://', '')}`;

return {
success: true,
logoutUrl,
};
}
} catch (tokenError) {
console.error('Error decoding OIDC token for logout:', tokenError);
// Continue with fallback logout
}
}
const logoutUrl = `${auth0Base}/v2/logout?${logoutParams.toString()}`;
Comment on lines +84 to +89

// Clear all auth cookies for fallback case
// Clear all auth cookies
if (isProduction) {
setOIDCCookie(event);
} else {
Expand All @@ -160,7 +100,7 @@ export default defineEventHandler(async (event) => {

return {
success: true,
logoutUrl: returnToUrl,
logoutUrl,
};
} catch (error) {
console.error('Auth logout error:', error);
Expand Down
21 changes: 2 additions & 19 deletions frontend/server/api/auth/user.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// SPDX-License-Identifier: MIT

import { getCookie } from 'h3';
import { jwtDecode } from 'jwt-decode';
import { verifyOrRefreshOidcToken } from '~~/server/utils/auth-refresh';

export default defineEventHandler(async (event) => {
Expand All @@ -27,24 +26,10 @@ export default defineEventHandler(async (event) => {
return {
isAuthenticated: false,
user: null,
token: null,
shouldAttemptSilentLogin,
};
}

// Extract Intercom claims from the original Auth0 ID token
let intercomJwt: string | undefined;
let username: string | undefined;
if (decodedToken.original_id_token) {
try {
const idTokenClaims = jwtDecode<Record<string, string>>(decodedToken.original_id_token);
intercomJwt = idTokenClaims['http://lfx.dev/claims/intercom'];
username = idTokenClaims['https://sso.linuxfoundation.org/claims/username'];
} catch (error) {
console.error('Intercom: Boot failed', error);
}
}

return {
isAuthenticated: true,
user: {
Expand All @@ -56,17 +41,15 @@ export default defineEventHandler(async (event) => {
updated_at: decodedToken.updated_at,
hasLfxInsightsPermission: decodedToken.hasLfxInsightsPermission,
isLfInsightsTeamMember: decodedToken.isLfInsightsTeamMember,
username,
intercomJwt,
username: decodedToken.username,
intercomJwt: decodedToken.intercomJwt,
},
token: decodedToken.original_id_token,
};
} catch (error) {
console.error('Auth user error:', error);
return {
isAuthenticated: false,
user: null,
token: null,
};
}
});
12 changes: 0 additions & 12 deletions frontend/server/middleware/jwt-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@
import { isLocal } from '../utils/common';
import { verifyOrRefreshOidcToken } from '../utils/auth-refresh';

const isJWT = (token: string) => {
const parts = token.split('.');
return parts.length === 3;
};

export default defineEventHandler(async (event) => {
const url = getRouterParam(event, '_') || event.node.req.url || '';

Expand Down Expand Up @@ -57,13 +52,6 @@ export default defineEventHandler(async (event) => {
});
}

if (!decodedToken.original_id_token || !isJWT(decodedToken.original_id_token)) {
throw createError({
statusCode: 401,
statusMessage: 'Invalid token format',
});
}

event.context.user = decodedToken;

if (!isLocal && isPermissionRequired && !decodedToken.hasLfxInsightsPermission) {
Expand Down
5 changes: 4 additions & 1 deletion frontend/server/utils/auth-refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ const callAuth0Refresh = async (refreshToken: string): Promise<RawRefresh | null
claims,
hasLfxInsightsPermission: hasLfxInsightsPermission(claims as string[]),
isLfInsightsTeamMember: isLfInsightsTeamMember(decodedIdToken.email || ''),
original_id_token: tokenResponse.id_token,
username: decodedIdToken['https://sso.linuxfoundation.org/claims/username'] as
| string
| undefined,
intercomJwt: decodedIdToken['http://lfx.dev/claims/intercom'] as string | undefined,
};

const oidcToken = jwt.sign(oidcTokenPayload, config.auth0ClientSecret, {
Expand Down
9 changes: 1 addition & 8 deletions frontend/server/utils/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import type { H3Event } from 'h3';
import { getCookie } from 'h3';
import type { DecodedOidcToken } from '~~/types/auth/auth-jwt.types';

const isJWT = (token: string) => token.split('.').length === 3;

/**
* Auth middleware for static jwt - supports both Authorization header and auth query parameter
* @param event - H3 event object
Expand Down Expand Up @@ -61,14 +59,9 @@ export function getOptionalUser(event: H3Event): DecodedOidcToken | null {

try {
const config = useRuntimeConfig();
const decoded = jwt.verify(oidcToken, config.auth0ClientSecret, {
return jwt.verify(oidcToken, config.auth0ClientSecret, {
algorithms: ['HS256'],
}) as DecodedOidcToken;

if (decoded.original_id_token && isJWT(decoded.original_id_token)) {
return decoded;
}
return null;
} catch {
return null;
}
Expand Down
3 changes: 2 additions & 1 deletion frontend/types/auth/auth-jwt.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export interface DecodedOidcToken {
aud: string;
iat: number;
exp: number;
original_id_token?: string;
username?: string;
intercomJwt?: string;
hasLfxInsightsPermission?: boolean;
isLfInsightsTeamMember?: boolean;
}
Expand Down
1 change: 0 additions & 1 deletion frontend/types/auth/auth-user.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,5 @@ export interface User {
export interface AuthData {
isAuthenticated: boolean;
user: User | null;
token: string | null;
shouldAttemptSilentLogin?: boolean;
}
Loading