@@ -7,6 +7,7 @@ import { ContextFactories } from '../../types/graphql';
77import { SamlResponseData } from '../types' ;
88import WorkspaceModel from '../../models/workspace' ;
99import UserModel from '../../models/user' ;
10+ import { sgr , Effect } from '../../utils/ansi' ;
1011
1112/**
1213 * Controller for SAML SSO endpoints
@@ -27,6 +28,26 @@ export default class SamlController {
2728 this . factories = factories ;
2829 }
2930
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+
3051 /**
3152 * Validate workspace ID format
3253 *
@@ -52,14 +73,16 @@ export default class SamlController {
5273 * Initiate SSO login (GET /auth/sso/saml/:workspaceId)
5374 */
5475 public async initiateLogin ( req : express . Request , res : express . Response ) : Promise < void > {
76+ const { workspaceId } = req . params ;
77+
5578 try {
56- const { workspaceId } = req . params ;
5779 const returnUrl = ( req . query . returnUrl as string ) || `/workspace/${ workspaceId } ` ;
5880
5981 /**
6082 * Validate workspace ID format
6183 */
6284 if ( ! this . isValidWorkspaceId ( workspaceId ) ) {
85+ this . log ( 'warn' , 'Invalid workspace ID format:' , sgr ( workspaceId , Effect . ForegroundRed ) ) ;
6386 res . status ( 400 ) . json ( { error : 'Invalid workspace ID' } ) ;
6487 return ;
6588 }
@@ -70,6 +93,7 @@ export default class SamlController {
7093 const workspace = await this . factories . workspacesFactory . findById ( workspaceId ) ;
7194
7295 if ( ! workspace || ! workspace . sso ?. enabled ) {
96+ this . log ( 'warn' , 'SSO not enabled for workspace:' , sgr ( workspaceId , Effect . ForegroundCyan ) ) ;
7397 res . status ( 400 ) . json ( { error : 'SSO is not enabled for this workspace' } ) ;
7498 return ;
7599 }
@@ -107,12 +131,23 @@ export default class SamlController {
107131 redirectUrl . searchParams . set ( 'SAMLRequest' , encodedRequest ) ;
108132 redirectUrl . searchParams . set ( 'RelayState' , relayStateId ) ;
109133
134+ this . log (
135+ 'log' ,
136+ 'Initiating SSO login for workspace:' ,
137+ sgr ( workspaceId , [ Effect . ForegroundCyan , Effect . Bold ] ) ,
138+ '| Request ID:' ,
139+ sgr ( requestId . slice ( 0 , 8 ) , Effect . ForegroundGray )
140+ ) ;
141+
110142 res . redirect ( redirectUrl . toString ( ) ) ;
111143 } catch ( error ) {
112- console . error ( 'SSO initiation error:' , {
113- workspaceId : req . params . workspaceId ,
114- error : error instanceof Error ? error . message : 'Unknown error' ,
115- } ) ;
144+ this . log (
145+ 'error' ,
146+ 'SSO initiation error for workspace:' ,
147+ sgr ( workspaceId , Effect . ForegroundCyan ) ,
148+ '|' ,
149+ sgr ( error instanceof Error ? error . message : 'Unknown error' , Effect . ForegroundRed )
150+ ) ;
116151 res . status ( 500 ) . json ( { error : 'Failed to initiate SSO login' } ) ;
117152 }
118153 }
@@ -121,15 +156,17 @@ export default class SamlController {
121156 * Handle ACS callback (POST /auth/sso/saml/:workspaceId/acs)
122157 */
123158 public async handleAcs ( req : express . Request , res : express . Response ) : Promise < void > {
159+ const { workspaceId } = req . params ;
160+
124161 try {
125- const { workspaceId } = req . params ;
126162 const samlResponse = req . body . SAMLResponse as string ;
127163 const relayStateId = req . body . RelayState as string ;
128164
129165 /**
130166 * Validate workspace ID format
131167 */
132168 if ( ! this . isValidWorkspaceId ( workspaceId ) ) {
169+ this . log ( 'warn' , '[ACS] Invalid workspace ID format:' , sgr ( workspaceId , Effect . ForegroundRed ) ) ;
133170 res . status ( 400 ) . json ( { error : 'Invalid workspace ID' } ) ;
134171 return ;
135172 }
@@ -138,6 +175,7 @@ export default class SamlController {
138175 * Validate required SAML response
139176 */
140177 if ( ! samlResponse ) {
178+ this . log ( 'warn' , '[ACS] Missing SAML response for workspace:' , sgr ( workspaceId , Effect . ForegroundCyan ) ) ;
141179 res . status ( 400 ) . json ( { error : 'SAML response is required' } ) ;
142180 return ;
143181 }
@@ -148,6 +186,7 @@ export default class SamlController {
148186 const workspace = await this . factories . workspacesFactory . findById ( workspaceId ) ;
149187
150188 if ( ! workspace || ! workspace . sso ?. enabled ) {
189+ this . log ( 'warn' , '[ACS] SSO not enabled for workspace:' , sgr ( workspaceId , Effect . ForegroundCyan ) ) ;
151190 res . status ( 400 ) . json ( { error : 'SSO is not enabled for this workspace' } ) ;
152191 return ;
153192 }
@@ -171,6 +210,14 @@ export default class SamlController {
171210 workspace . sso . saml
172211 ) ;
173212
213+ this . log (
214+ 'log' ,
215+ '[ACS] SAML response validated for workspace:' ,
216+ sgr ( workspaceId , Effect . ForegroundCyan ) ,
217+ '| User:' ,
218+ sgr ( samlData . email , [ Effect . ForegroundMagenta , Effect . Bold ] )
219+ ) ;
220+
174221 /**
175222 * Validate InResponseTo against stored AuthnRequest
176223 */
@@ -181,15 +228,25 @@ export default class SamlController {
181228 ) ;
182229
183230 if ( ! isValidRequest ) {
231+ this . log (
232+ 'error' ,
233+ '[ACS] InResponseTo validation failed for workspace:' ,
234+ sgr ( workspaceId , Effect . ForegroundCyan ) ,
235+ '| Request ID:' ,
236+ sgr ( samlData . inResponseTo . slice ( 0 , 8 ) , Effect . ForegroundGray )
237+ ) ;
184238 res . status ( 400 ) . json ( { error : 'Invalid SAML response: InResponseTo validation failed' } ) ;
185239 return ;
186240 }
187241 }
188242 } catch ( error ) {
189- console . error ( 'SAML validation error:' , {
190- workspaceId,
191- error : error instanceof Error ? error . message : 'Unknown error' ,
192- } ) ;
243+ this . log (
244+ 'error' ,
245+ '[ACS] SAML validation error for workspace:' ,
246+ sgr ( workspaceId , Effect . ForegroundCyan ) ,
247+ '|' ,
248+ sgr ( error instanceof Error ? error . message : 'Unknown error' , Effect . ForegroundRed )
249+ ) ;
193250 res . status ( 400 ) . json ( { error : 'Invalid SAML response' } ) ;
194251 return ;
195252 }
@@ -203,7 +260,22 @@ export default class SamlController {
203260 /**
204261 * JIT provisioning or invite-only policy
205262 */
263+ this . log (
264+ 'info' ,
265+ '[ACS] User not found, starting provisioning:' ,
266+ sgr ( samlData . email , Effect . ForegroundMagenta ) ,
267+ '| Workspace:' ,
268+ sgr ( workspaceId , Effect . ForegroundCyan )
269+ ) ;
206270 user = await this . handleUserProvisioning ( workspaceId , samlData , workspace ) ;
271+ } else {
272+ this . log (
273+ 'log' ,
274+ '[ACS] Existing user found:' ,
275+ sgr ( samlData . email , Effect . ForegroundMagenta ) ,
276+ '| User ID:' ,
277+ sgr ( user . _id . toString ( ) . slice ( 0 , 8 ) , Effect . ForegroundGray )
278+ ) ;
207279 }
208280
209281 /**
@@ -225,24 +297,40 @@ export default class SamlController {
225297 frontendUrl . searchParams . set ( 'access_token' , tokens . accessToken ) ;
226298 frontendUrl . searchParams . set ( 'refresh_token' , tokens . refreshToken ) ;
227299
300+ this . log (
301+ 'success' ,
302+ '[ACS] ✓ SSO login successful:' ,
303+ sgr ( samlData . email , [ Effect . ForegroundMagenta , Effect . Bold ] ) ,
304+ '| Workspace:' ,
305+ sgr ( workspaceId , Effect . ForegroundCyan ) ,
306+ '| Redirecting to:' ,
307+ sgr ( finalReturnUrl , Effect . ForegroundGray )
308+ ) ;
309+
228310 res . redirect ( frontendUrl . toString ( ) ) ;
229311 } catch ( error ) {
230312 /**
231313 * Handle specific error types
232314 */
233315 if ( error instanceof Error && error . message . includes ( 'SAML' ) ) {
234- console . error ( 'SAML processing error:' , {
235- workspaceId : req . params . workspaceId ,
236- error : error . message ,
237- } ) ;
316+ this . log (
317+ 'error' ,
318+ '[ACS] SAML processing error for workspace:' ,
319+ sgr ( workspaceId , Effect . ForegroundCyan ) ,
320+ '|' ,
321+ sgr ( error . message , Effect . ForegroundRed )
322+ ) ;
238323 res . status ( 400 ) . json ( { error : 'Invalid SAML response' } ) ;
239324 return ;
240325 }
241326
242- console . error ( 'ACS callback error:' , {
243- workspaceId : req . params . workspaceId ,
244- error : error instanceof Error ? error . message : 'Unknown error' ,
245- } ) ;
327+ this . log (
328+ 'error' ,
329+ '[ACS] ACS callback error for workspace:' ,
330+ sgr ( workspaceId , Effect . ForegroundCyan ) ,
331+ '|' ,
332+ sgr ( error instanceof Error ? error . message : 'Unknown error' , Effect . ForegroundRed )
333+ ) ;
246334 res . status ( 500 ) . json ( { error : 'Failed to process SSO callback' } ) ;
247335 }
248336 }
@@ -260,44 +348,99 @@ export default class SamlController {
260348 samlData : SamlResponseData ,
261349 workspace : WorkspaceModel
262350 ) : Promise < UserModel > {
263- /**
264- * Find user by email
265- */
266- let user = await this . factories . usersFactory . findByEmail ( samlData . email ) ;
267-
268- if ( ! user ) {
351+ try {
269352 /**
270- * Create new user (JIT provisioning)
271- * Password is not set - only SSO login is allowed
353+ * Find user by email
272354 */
273- user = await this . factories . usersFactory . create ( samlData . email , undefined , undefined ) ;
274- }
355+ let user = await this . factories . usersFactory . findByEmail ( samlData . email ) ;
275356
276- /**
277- * Link SAML identity to user
278- */
279- await user . linkSamlIdentity ( workspaceId , samlData . nameId , samlData . email ) ;
280-
281- /**
282- * Check if user is a member of the workspace
283- */
284- const member = await workspace . getMemberInfo ( user . _id . toString ( ) ) ;
357+ if ( ! user ) {
358+ /**
359+ * Create new user (JIT provisioning)
360+ * Password is not set - only SSO login is allowed
361+ */
362+ this . log (
363+ 'info' ,
364+ '[Provisioning] Creating new user:' ,
365+ sgr ( samlData . email , [ Effect . ForegroundMagenta , Effect . Bold ] ) ,
366+ '| Workspace:' ,
367+ sgr ( workspaceId , Effect . ForegroundCyan )
368+ ) ;
369+ user = await this . factories . usersFactory . create ( samlData . email , undefined , undefined ) ;
370+ }
285371
286- if ( ! member ) {
287372 /**
288- * Add user to workspace (JIT provisioning)
373+ * Link SAML identity to user
289374 */
290- await workspace . addMember ( user . _id . toString ( ) ) ;
291- await user . addWorkspace ( workspaceId ) ;
292- } else if ( WorkspaceModel . isPendingMember ( member ) ) {
375+ this . log (
376+ 'info' ,
377+ '[Provisioning] Linking SAML identity for user:' ,
378+ sgr ( samlData . email , Effect . ForegroundMagenta ) ,
379+ '| NameID:' ,
380+ sgr ( samlData . nameId . slice ( 0 , 16 ) + '...' , Effect . ForegroundGray )
381+ ) ;
382+ await user . linkSamlIdentity ( workspaceId , samlData . nameId , samlData . email ) ;
383+
293384 /**
294- * Confirm pending membership
385+ * Check if user is a member of the workspace
295386 */
296- await workspace . confirmMembership ( user ) ;
297- await user . confirmMembership ( workspaceId ) ;
298- }
387+ const member = await workspace . getMemberInfo ( user . _id . toString ( ) ) ;
299388
300- return user ;
389+ if ( ! member ) {
390+ /**
391+ * Add user to workspace (JIT provisioning)
392+ */
393+ this . log (
394+ 'log' ,
395+ '[Provisioning] Adding user to workspace:' ,
396+ sgr ( samlData . email , Effect . ForegroundMagenta ) ,
397+ '| Workspace:' ,
398+ sgr ( workspaceId , Effect . ForegroundCyan )
399+ ) ;
400+ await workspace . addMember ( user . _id . toString ( ) ) ;
401+ await user . addWorkspace ( workspaceId ) ;
402+ } else if ( WorkspaceModel . isPendingMember ( member ) ) {
403+ /**
404+ * Confirm pending membership
405+ */
406+ this . log (
407+ 'log' ,
408+ '[Provisioning] Confirming pending membership:' ,
409+ sgr ( samlData . email , Effect . ForegroundMagenta ) ,
410+ '| Workspace:' ,
411+ sgr ( workspaceId , Effect . ForegroundCyan )
412+ ) ;
413+ await workspace . confirmMembership ( user ) ;
414+ await user . confirmMembership ( workspaceId ) ;
415+ } else {
416+ this . log (
417+ 'log' ,
418+ '[Provisioning] User already member of workspace:' ,
419+ sgr ( samlData . email , Effect . ForegroundMagenta )
420+ ) ;
421+ }
422+
423+ this . log (
424+ 'success' ,
425+ '[Provisioning] ✓ User provisioning completed:' ,
426+ sgr ( samlData . email , [ Effect . ForegroundMagenta , Effect . Bold ] ) ,
427+ '| User ID:' ,
428+ sgr ( user . _id . toString ( ) , Effect . ForegroundGray )
429+ ) ;
430+
431+ return user ;
432+ } catch ( error ) {
433+ this . log (
434+ 'error' ,
435+ '[Provisioning] Provisioning error for user:' ,
436+ sgr ( samlData . email , Effect . ForegroundMagenta ) ,
437+ '| Workspace:' ,
438+ sgr ( workspaceId , Effect . ForegroundCyan ) ,
439+ '|' ,
440+ sgr ( error instanceof Error ? error . message : 'Unknown error' , Effect . ForegroundRed )
441+ ) ;
442+ throw error ;
443+ }
301444 }
302445}
303446
0 commit comments