Skip to content

Commit 1080020

Browse files
committed
lint
1 parent 7190108 commit 1080020

File tree

12 files changed

+149
-105
lines changed

12 files changed

+149
-105
lines changed

src/directives/definedOnlyForAdmins.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { defaultFieldResolver, GraphQLSchema } from 'graphql';
22
import { mapSchema, MapperKind, getDirective } from '@graphql-tools/utils';
33
import { ResolverContextWithUser, UnknownGraphQLResolverResult } from '../types/graphql';
4-
import { ForbiddenError, UserInputError } from 'apollo-server-express';
54
import WorkspaceModel from '../models/workspace';
65

76
/**
@@ -98,4 +97,3 @@ export default function definedOnlyForAdminsDirective(directiveName = 'definedOn
9897
}),
9998
};
10099
}
101-

src/models/user.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,9 +473,17 @@ export default class UserModel extends AbstractModel<Omit<UserDBScheme, '_id'>>
473473
this.identities = {};
474474
}
475475
if (!this.identities[workspaceId]) {
476-
this.identities[workspaceId] = { saml: { id: samlId, email } };
476+
this.identities[workspaceId] = {
477+
saml: {
478+
id: samlId,
479+
email,
480+
},
481+
};
477482
} else {
478-
this.identities[workspaceId].saml = { id: samlId, email };
483+
this.identities[workspaceId].saml = {
484+
id: samlId,
485+
email,
486+
};
479487
}
480488
}
481489

src/resolvers/workspace.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ module.exports = {
4949
/**
5050
* Check if workspace exists and has SSO enabled
5151
*/
52-
if (!workspace || !workspace.sso?.enabled) {
52+
if (!workspace || !(workspace.sso && workspace.sso.enabled)) {
5353
return null;
5454
}
5555

src/sso/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ import { ContextFactories } from '../types/graphql';
1010
*/
1111
export function appendSsoRoutes(app: express.Application, factories: ContextFactories): void {
1212
const samlRouter = createSamlRouter(factories);
13+
1314
app.use('/auth/sso/saml', samlRouter);
1415
}
15-

src/sso/saml/controller.ts

Lines changed: 73 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -23,54 +23,19 @@ export default class SamlController {
2323
*/
2424
private factories: ContextFactories;
2525

26+
/**
27+
* SAML controller constructor used for DI
28+
* @param factories - for working with models
29+
*/
2630
constructor(factories: ContextFactories) {
2731
this.samlService = new SamlService();
2832
this.factories = factories;
2933
}
3034

31-
/**
32-
* Log message with SSO prefix
33-
*
34-
* @param level - log level ('log', 'warn', 'error', 'info', 'success')
35-
* @param args - arguments to log
36-
*/
37-
private log(level: 'log' | 'warn' | 'error' | 'info' | 'success', ...args: unknown[]): void {
38-
const colors = {
39-
log: Effect.ForegroundGreen,
40-
warn: Effect.ForegroundYellow,
41-
error: Effect.ForegroundRed,
42-
info: Effect.ForegroundBlue,
43-
success: [Effect.ForegroundGreen, Effect.Bold],
44-
};
45-
46-
const logger = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
47-
48-
logger(sgr('[SSO]', colors[level]), ...args);
49-
}
50-
51-
/**
52-
* Validate workspace ID format
53-
*
54-
* @param workspaceId - workspace ID to validate
55-
* @returns true if valid, false otherwise
56-
*/
57-
private isValidWorkspaceId(workspaceId: string): boolean {
58-
return ObjectId.isValid(workspaceId);
59-
}
60-
61-
/**
62-
* Compose Assertion Consumer Service URL for workspace
63-
*
64-
* @param workspaceId - workspace ID
65-
* @returns ACS URL
66-
*/
67-
private getAcsUrl(workspaceId: string): string {
68-
const apiUrl = process.env.API_URL || 'https://api.hawk.so';
69-
return `${apiUrl}/auth/sso/saml/${workspaceId}/acs`;
70-
}
71-
7235
/**
7336
* Initiate SSO login (GET /auth/sso/saml/:workspaceId)
37+
* @param req - Express request
38+
* @param res - Express response
7439
*/
7540
public async initiateLogin(req: express.Request, res: express.Response): Promise<void> {
7641
const { workspaceId } = req.params;
@@ -84,6 +49,7 @@ export default class SamlController {
8449
if (!this.isValidWorkspaceId(workspaceId)) {
8550
this.log('warn', 'Invalid workspace ID format:', sgr(workspaceId, Effect.ForegroundRed));
8651
res.status(400).json({ error: 'Invalid workspace ID' });
52+
8753
return;
8854
}
8955

@@ -95,6 +61,7 @@ export default class SamlController {
9561
if (!workspace || !workspace.sso?.enabled) {
9662
this.log('warn', 'SSO not enabled for workspace:', sgr(workspaceId, Effect.ForegroundCyan));
9763
res.status(400).json({ error: 'SSO is not enabled for this workspace' });
64+
9865
return;
9966
}
10067

@@ -107,7 +74,10 @@ export default class SamlController {
10774
/**
10875
* 3. Save RelayState to temporary storage
10976
*/
110-
samlStore.saveRelayState(relayStateId, { returnUrl, workspaceId });
77+
samlStore.saveRelayState(relayStateId, {
78+
returnUrl,
79+
workspaceId,
80+
});
11181

11282
/**
11383
* 4. Generate AuthnRequest
@@ -141,6 +111,7 @@ export default class SamlController {
141111
* 6. Redirect to IdP
142112
*/
143113
const redirectUrl = new URL(workspace.sso.saml.ssoUrl);
114+
144115
redirectUrl.searchParams.set('SAMLRequest', encodedRequest);
145116
redirectUrl.searchParams.set('RelayState', relayStateId);
146117

@@ -167,6 +138,9 @@ export default class SamlController {
167138

168139
/**
169140
* Handle ACS callback (POST /auth/sso/saml/:workspaceId/acs)
141+
* @param req - Express request object
142+
* @param res - Express response object
143+
* @returns void
170144
*/
171145
public async handleAcs(req: express.Request, res: express.Response): Promise<void> {
172146
const { workspaceId } = req.params;
@@ -181,6 +155,7 @@ export default class SamlController {
181155
if (!this.isValidWorkspaceId(workspaceId)) {
182156
this.log('warn', '[ACS] Invalid workspace ID format:', sgr(workspaceId, Effect.ForegroundRed));
183157
res.status(400).json({ error: 'Invalid workspace ID' });
158+
184159
return;
185160
}
186161

@@ -190,6 +165,7 @@ export default class SamlController {
190165
if (!samlResponse) {
191166
this.log('warn', '[ACS] Missing SAML response for workspace:', sgr(workspaceId, Effect.ForegroundCyan));
192167
res.status(400).json({ error: 'SAML response is required' });
168+
193169
return;
194170
}
195171

@@ -201,6 +177,7 @@ export default class SamlController {
201177
if (!workspace || !workspace.sso?.enabled) {
202178
this.log('warn', '[ACS] SSO not enabled for workspace:', sgr(workspaceId, Effect.ForegroundCyan));
203179
res.status(400).json({ error: 'SSO is not enabled for this workspace' });
180+
204181
return;
205182
}
206183

@@ -249,6 +226,7 @@ export default class SamlController {
249226
sgr(samlData.inResponseTo.slice(0, 8), Effect.ForegroundGray)
250227
);
251228
res.status(400).json({ error: 'Invalid SAML response: InResponseTo validation failed' });
229+
252230
return;
253231
}
254232
}
@@ -261,6 +239,7 @@ export default class SamlController {
261239
sgr(error instanceof Error ? error.message : 'Unknown error', Effect.ForegroundRed)
262240
);
263241
res.status(400).json({ error: 'Invalid SAML response' });
242+
264243
return;
265244
}
266245

@@ -310,6 +289,7 @@ export default class SamlController {
310289
*/
311290
const callbackPath = `/login/sso/${workspaceId}`;
312291
const frontendUrl = new URL(callbackPath, process.env.GARAGE_URL || 'http://localhost:3000');
292+
313293
frontendUrl.searchParams.set('access_token', tokens.accessToken);
314294
frontendUrl.searchParams.set('refresh_token', tokens.refreshToken);
315295
frontendUrl.searchParams.set('returnUrl', finalReturnUrl);
@@ -340,6 +320,7 @@ export default class SamlController {
340320
sgr(error.message, Effect.ForegroundRed)
341321
);
342322
res.status(400).json({ error: 'Invalid SAML response' });
323+
343324
return;
344325
}
345326

@@ -354,6 +335,56 @@ export default class SamlController {
354335
}
355336
}
356337

338+
/**
339+
* Log message with SSO prefix
340+
*
341+
* @param level - log level ('log', 'warn', 'error', 'info', 'success')
342+
* @param args - arguments to log
343+
*/
344+
private log(level: 'log' | 'warn' | 'error' | 'info' | 'success', ...args: unknown[]): void {
345+
const colors = {
346+
log: Effect.ForegroundGreen,
347+
warn: Effect.ForegroundYellow,
348+
error: Effect.ForegroundRed,
349+
info: Effect.ForegroundBlue,
350+
success: [Effect.ForegroundGreen, Effect.Bold],
351+
};
352+
353+
let logger: typeof console.log;
354+
355+
if (level === 'error') {
356+
logger = console.error;
357+
} else if (level === 'warn') {
358+
logger = console.warn;
359+
} else {
360+
logger = console.log;
361+
}
362+
363+
logger(sgr('[SSO]', colors[level]), ...args);
364+
}
365+
366+
/**
367+
* Validate workspace ID format
368+
*
369+
* @param workspaceId - workspace ID to validate
370+
* @returns true if valid, false otherwise
371+
*/
372+
private isValidWorkspaceId(workspaceId: string): boolean {
373+
return ObjectId.isValid(workspaceId);
374+
}
375+
376+
/**
377+
* Compose Assertion Consumer Service URL for workspace
378+
*
379+
* @param workspaceId - workspace ID
380+
* @returns ACS URL
381+
*/
382+
private getAcsUrl(workspaceId: string): string {
383+
const apiUrl = process.env.API_URL || 'https://api.hawk.so';
384+
385+
return `${apiUrl}/auth/sso/saml/${workspaceId}/acs`;
386+
}
387+
357388
/**
358389
* Handle user provisioning (JIT or invite-only)
359390
*
@@ -462,4 +493,3 @@ export default class SamlController {
462493
}
463494
}
464495
}
465-

src/sso/saml/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,3 @@ export function createSamlRouter(factories: ContextFactories): express.Router {
3838

3939
return router;
4040
}
41-

src/sso/saml/service.ts

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { SAML, SamlConfig as NodeSamlConfig, Profile } from '@node-saml/node-saml';
2+
import { inflateRawSync } from 'zlib';
23
import { SamlConfig, SamlResponseData } from '../types';
34
import { SamlValidationError, SamlValidationErrorType } from './types';
45
import { extractAttribute } from './utils';
@@ -51,34 +52,6 @@ export default class SamlService {
5152
};
5253
}
5354

54-
/**
55-
* Extract request ID from encoded SAML AuthnRequest
56-
*
57-
* @param encodedRequest - deflated and base64 encoded SAML request
58-
* @returns request ID
59-
*/
60-
private extractRequestIdFromEncodedRequest(encodedRequest: string): string {
61-
const zlib = require('zlib');
62-
63-
/**
64-
* Decode base64 and inflate
65-
*/
66-
const decoded = Buffer.from(encodedRequest, 'base64');
67-
const inflated = zlib.inflateRawSync(decoded).toString('utf-8');
68-
69-
/**
70-
* Extract ID attribute from AuthnRequest XML
71-
* Format: <samlp:AuthnRequest ... ID="_abc123" ...>
72-
*/
73-
const idMatch = inflated.match(/ID="([^"]+)"/);
74-
75-
if (!idMatch || !idMatch[1]) {
76-
throw new Error('Failed to extract request ID from AuthnRequest');
77-
}
78-
79-
return idMatch[1];
80-
}
81-
8255
/**
8356
* Validate and parse SAML Response
8457
*
@@ -179,7 +152,10 @@ export default class SamlService {
179152
throw new SamlValidationError(
180153
SamlValidationErrorType.INVALID_IN_RESPONSE_TO,
181154
`InResponseTo mismatch: expected ${expectedRequestId}, got ${inResponseTo}`,
182-
{ expected: expectedRequestId, received: inResponseTo }
155+
{
156+
expected: expectedRequestId,
157+
received: inResponseTo,
158+
}
183159
);
184160
}
185161

@@ -219,14 +195,40 @@ export default class SamlService {
219195
};
220196
}
221197

198+
/**
199+
* Extract request ID from encoded SAML AuthnRequest
200+
*
201+
* @param encodedRequest - deflated and base64 encoded SAML request
202+
* @returns request ID
203+
*/
204+
private extractRequestIdFromEncodedRequest(encodedRequest: string): string {
205+
/**
206+
* Decode base64 and inflate
207+
*/
208+
const decoded = Buffer.from(encodedRequest, 'base64');
209+
const inflated = inflateRawSync(decoded as unknown as Uint8Array).toString('utf-8');
210+
211+
/**
212+
* Extract ID attribute from AuthnRequest XML
213+
* Format: <samlp:AuthnRequest ... ID="_abc123" ...>
214+
*/
215+
const idMatch = inflated.match(/ID="([^"]+)"/);
216+
217+
if (!idMatch || !idMatch[1]) {
218+
throw new Error('Failed to extract request ID from AuthnRequest');
219+
}
220+
221+
return idMatch[1];
222+
}
223+
222224
/**
223225
* Create node-saml SAML instance with given configuration
224226
*
225227
* @param acsUrl - Assertion Consumer Service URL
226228
* @param samlConfig - SAML configuration from workspace
227229
* @returns configured SAML instance
228230
*/
229-
private createSamlInstance(acsUrl: string, samlConfig: SamlConfig): SAML {
231+
private createSamlInstance(acsUrl: string, samlConfig: SamlConfig): SAML {
230232
const spEntityId = process.env.SSO_SP_ENTITY_ID;
231233

232234
if (!spEntityId) {
@@ -254,4 +256,3 @@ export default class SamlService {
254256
return new SAML(options);
255257
}
256258
}
257-

0 commit comments

Comments
 (0)