Skip to content

Commit 3fbee9f

Browse files
authored
feat(meetings): add join URL endpoint and simplify auth middleware (#79)
* feat(meetings): add join URL endpoint and simplify auth middleware - Add GET /api/meetings/:uid/join-url endpoint for fetching meeting join URLs - Create MeetingJoinURL interface for type safety - Update public meeting controller to fetch join URLs for public meetings - Simplify authentication middleware by removing redundant authContext - Create unified authMiddleware replacing dual middleware system - Use req.oidc directly from OIDC middleware - Add comprehensive debug logging for authentication troubleshooting - Remove obsolete auth-token.middleware.ts and protected-routes.middleware.ts LFXV2-417 LFXV2-452 Signed-off-by: Asitha de Silva <asithade@gmail.com> * fix(auth): prevent authentication bypass and fix Authorization header - Fix critical security vulnerability where non-GET requests to protected SSR routes bypassed authentication - Add proper 401 error handling for non-GET requests to protected SSR routes - Fix Authorization header being set to 'Bearer undefined' when bearerToken is not provided - Make Authorization header conditional in ApiClientService based on bearerToken presence LFXV2-417 Signed-off-by: Asitha de Silva <asithade@gmail.com> * fix(server): remove deprecated function Signed-off-by: Asitha de Silva <asithade@gmail.com> * fix(auth): added ui auth guards Signed-off-by: Asitha de Silva <asithade@gmail.com> * fix(auth): simplify authentication check and improve performance - Remove async checkAuthentication function to simplify auth flow - Remove user from AuthMiddlewareResult interface for cleaner separation - Optimize user info fetching in server.ts with fallback logic - Remove unnecessary build step from e2e-tests workflow - Clean up imports and debug logging in meeting service - Add debug logging to proxy service for better observability LFXV2-417 Signed-off-by: Asitha de Silva <asithade@gmail.com> * fix(test): revert build step removal Signed-off-by: Asitha de Silva <asithade@gmail.com> --------- Signed-off-by: Asitha de Silva <asithade@gmail.com>
1 parent 60b83fa commit 3fbee9f

File tree

19 files changed

+683
-196
lines changed

19 files changed

+683
-196
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,6 @@
6161
},
6262
"[markdown]": {
6363
"editor.defaultFormatter": "esbenp.prettier-vscode"
64-
}
64+
},
65+
"angular.enable-strict-mode-prompt": false
6566
}

apps/lfx-pcc/src/app/app.routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33

44
import { Routes } from '@angular/router';
55

6+
import { authGuard } from './shared/guards/auth.guard';
7+
68
export const routes: Routes = [
79
{
810
path: '',
911
loadComponent: () => import('./modules/pages/home/home.component').then((m) => m.HomeComponent),
12+
canActivate: [authGuard],
1013
},
1114
{
1215
path: 'meetings/:id',
@@ -16,6 +19,7 @@ export const routes: Routes = [
1619
path: 'project/:slug',
1720
loadComponent: () => import('./layouts/project-layout/project-layout.component').then((m) => m.ProjectLayoutComponent),
1821
loadChildren: () => import('./modules/project/project.routes').then((m) => m.PROJECT_ROUTES),
22+
canActivate: [authGuard],
1923
data: { preload: true, preloadDelay: 1000 }, // Preload after 1 second for likely navigation
2024
},
2125
];

apps/lfx-pcc/src/app/modules/meeting/meeting.component.html

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,14 @@ <h4 class="font-medium text-gray-900 font-sans">Enter your information</h4>
257257
</div>
258258
</div>
259259

260-
<div class="flex items-center justify-between pt-3">
261-
<p class="text-sm text-gray-500 font-sans">Meeting invites and updates will be sent to your email</p>
262-
<lfx-button size="small" [href]="meeting().join_url" severity="primary" label="Join Meeting" icon="fa-light fa-sign-in"></lfx-button>
260+
<div class="flex items-center justify-end pt-3">
261+
<lfx-button
262+
size="small"
263+
[href]="meeting().join_url"
264+
severity="primary"
265+
label="Join Meeting"
266+
icon="fa-light fa-sign-in"
267+
[disabled]="joinForm.invalid"></lfx-button>
263268
</div>
264269
</form>
265270
</div>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
import { isPlatformBrowser } from '@angular/common';
5+
import { inject, PLATFORM_ID } from '@angular/core';
6+
import { CanActivateFn } from '@angular/router';
7+
8+
import { UserService } from '../services/user.service';
9+
10+
/**
11+
* Authentication guard that protects routes requiring user authentication.
12+
*
13+
* Redirects unauthenticated users to the login page with a returnTo parameter
14+
* containing the originally requested URL.
15+
*/
16+
export const authGuard: CanActivateFn = (_route, state) => {
17+
const userService = inject(UserService);
18+
const platformId = inject(PLATFORM_ID);
19+
20+
// Check if user is authenticated using the signal
21+
if (userService.authenticated()) {
22+
return true;
23+
}
24+
25+
// Redirect to login with returnTo parameter using Router for SSR compatibility
26+
const returnTo = encodeURIComponent(state.url);
27+
if (isPlatformBrowser(platformId)) {
28+
window.location.href = `/login?returnTo=${returnTo}`;
29+
}
30+
31+
return false;
32+
};

apps/lfx-pcc/src/app/shared/interceptors/authentication.interceptor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const authenticationInterceptor: HttpInterceptorFn = (req, next) => {
1616
const cookieService = inject(SsrCookieService);
1717
const userService = inject(UserService);
1818

19-
if (req.url.startsWith('/api/') && userService.authenticated()) {
19+
if ((req.url.startsWith('/api/') || req.url.startsWith('/public/api/')) && userService.authenticated()) {
2020
const authenticatedReq = req.clone({
2121
withCredentials: true,
2222
headers: req.headers.append(

apps/lfx-pcc/src/server/controllers/meeting.controller.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,49 @@ export class MeetingController {
633633
}
634634
}
635635

636+
/**
637+
* GET /meetings/:uid/join-url
638+
*/
639+
public async getMeetingJoinUrl(req: Request, res: Response, next: NextFunction): Promise<void> {
640+
const { uid } = req.params;
641+
const startTime = Logger.start(req, 'get_meeting_join_url', {
642+
meeting_uid: uid,
643+
});
644+
645+
try {
646+
// Check if the meeting UID is provided
647+
if (
648+
!validateUidParameter(uid, req, next, {
649+
operation: 'get_meeting_join_url',
650+
service: 'meeting_controller',
651+
logStartTime: startTime,
652+
})
653+
) {
654+
return;
655+
}
656+
657+
// Get the meeting join URL
658+
const joinUrlData = await this.meetingService.getMeetingJoinUrl(req, uid);
659+
660+
// Log the success
661+
Logger.success(req, 'get_meeting_join_url', startTime, {
662+
meeting_uid: uid,
663+
has_join_url: !!joinUrlData.join_url,
664+
});
665+
666+
// Send the join URL data to the client
667+
res.json(joinUrlData);
668+
} catch (error) {
669+
// Log the error
670+
Logger.error(req, 'get_meeting_join_url', startTime, error, {
671+
meeting_uid: uid,
672+
});
673+
674+
// Send the error to the next middleware
675+
next(error);
676+
}
677+
}
678+
636679
/**
637680
* Private helper to process registrant operations with fail-fast for 403 errors
638681
*/

apps/lfx-pcc/src/server/controllers/public-meeting.controller.ts

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Logger } from '../helpers/logger';
99
import { validateUidParameter } from '../helpers/validation.helper';
1010
import { MeetingService } from '../services/meeting.service';
1111
import { ProjectService } from '../services/project.service';
12-
import { validatePasscode } from '../utils/security.util';
12+
import { validatePassword } from '../utils/security.util';
1313

1414
/**
1515
* Controller for handling public meeting HTTP requests (no authentication required)
@@ -42,8 +42,8 @@ export class PublicMeetingController {
4242
// TODO: Generate an M2M token
4343

4444
// Get the meeting by ID using the existing meeting service
45-
const meeting = await this.meetingService.getMeetingById(req, id);
46-
const project = await this.projectService.getProjectById(req, meeting.project_uid);
45+
const meeting = await this.meetingService.getMeetingById(req, id, 'meeting', false);
46+
const project = await this.projectService.getProjectById(req, meeting.project_uid, false);
4747

4848
if (!project) {
4949
throw new ResourceNotFoundError('Project', meeting.project_uid, {
@@ -60,22 +60,83 @@ export class PublicMeetingController {
6060
title: meeting.title,
6161
});
6262

63-
// Check if the meeting visibility is public, if so, return the meeting and project
63+
// Check if the meeting visibility is public, if so, get join URL and return the meeting and project
6464
if (meeting.visibility === MeetingVisibility.PUBLIC) {
65-
res.json({ meeting, project });
65+
try {
66+
// Get the meeting join URL for public meetings
67+
req.log.debug(
68+
Logger.sanitize({
69+
meeting_uid: id,
70+
isAuthenticated: req.oidc?.isAuthenticated(),
71+
hasOidc: !!req.oidc,
72+
user: req.oidc?.user,
73+
accessToken: req.oidc?.accessToken ? 'present' : 'missing',
74+
cookies: Object.keys(req.cookies || {}),
75+
headers: {
76+
cookie: req.headers.cookie ? 'present' : 'missing',
77+
},
78+
}),
79+
'OIDC Authentication Debug - Getting join URL for public meeting'
80+
);
81+
const joinUrlData = await this.meetingService.getMeetingJoinUrl(req, id);
82+
meeting.join_url = joinUrlData.join_url;
83+
84+
req.log.debug(
85+
Logger.sanitize({
86+
meeting_uid: id,
87+
has_join_url: !!joinUrlData.join_url,
88+
}),
89+
'Fetched join URL for public meeting'
90+
);
91+
} catch (error) {
92+
// Log the error but don't fail the request - join URL is optional
93+
req.log.warn(
94+
{
95+
error: error instanceof Error ? error.message : error,
96+
meeting_uid: id,
97+
has_token: !!req.bearerToken,
98+
},
99+
'Failed to fetch join URL for public meeting, continuing without it'
100+
);
101+
}
102+
103+
res.json({ meeting, project: { name: project.name, slug: project.slug, logo_url: project.logo_url } });
66104
return;
67105
}
68106

69107
// Check if the user has passed in a password, if so, check if it's correct
70108
const { password } = req.query;
71-
if (!password || !validatePasscode(password as string, meeting.password)) {
109+
if (!password || !validatePassword(password as string, meeting.password)) {
72110
throw new AuthenticationError('Invalid password', {
73111
operation: 'get_public_meeting_by_id',
74112
service: 'public_meeting_controller',
75113
path: `/meetings/${id}`,
76114
});
77115
}
78116

117+
// Get the meeting join URL for password-protected meetings
118+
try {
119+
const joinUrlData = await this.meetingService.getMeetingJoinUrl(req, id);
120+
meeting.join_url = joinUrlData.join_url;
121+
122+
req.log.debug(
123+
{
124+
meeting_uid: id,
125+
has_join_url: !!joinUrlData.join_url,
126+
},
127+
'Fetched join URL for password-protected meeting'
128+
);
129+
} catch (error) {
130+
// Log the error but don't fail the request - join URL is optional
131+
req.log.warn(
132+
{
133+
error: error instanceof Error ? error.message : error,
134+
meeting_uid: id,
135+
},
136+
'Failed to fetch join URL for password-protected meeting, continuing without it'
137+
);
138+
}
139+
79140
// Send the meeting and project data to the client
80141
res.json({ meeting, project: { name: project.name, slug: project.slug, logo_url: project.logo_url } });
81142
} catch (error) {

apps/lfx-pcc/src/server/middleware/auth-token.middleware.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.

0 commit comments

Comments
 (0)