EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
sso.js
Go to the documentation of this file.
1/**
2 * @file sso.js
3 * @module routes/sso
4 * @description Single Sign-On (SSO) integration endpoints. Supports per-tenant and per-user SSO
5 * configuration with OAuth2 and SAML protocols. Includes Azure AD integration with MSAL.
6 * @requires express
7 * @requires services/db
8 * @requires @azure/msal-node
9 * @author RMM-PSA Development Team
10 * @copyright 2026 RMM-PSA Platform
11 * @license Proprietary
12 */
13
14/**
15 * @apiDefine SSO Single Sign-On
16 * SSO integration for OAuth2/SAML authentication
17 */
18
19// SSO Integration Route: Per-tenant and per-user SSO (OAuth2/SAML)
20const express = require('express');
21const router = express.Router();
22const pool = require('../services/db');
23const msal = require('@azure/msal-node');
24
25const azureConfig = {
26 auth: {
27 clientId: process.env.AZURE_AD_CLIENT_ID,
28 authority: 'https://login.microsoftonline.com/common',
29 clientSecret: process.env.AZURE_AD_CLIENT_SECRET
30 }
31};
32const azureRedirectUri = process.env.AZURE_AD_REDIRECT_URI || 'https://yourdomain.com/api/sso/azure/callback';
33
34/**
35 * @api {get} /api/sso/settings/:tenant_id Get tenant SSO config
36 * @apiName GetTenantSSOSettings
37 * @apiGroup SSO
38 * @apiDescription Retrieve SSO configuration for a tenant. Returns config object containing
39 * OAuth2 or SAML provider settings.
40 * @apiParam {string} tenant_id Tenant ID
41 * @apiSuccess {object} config SSO configuration object
42 * @apiError {string} error="No SSO config found" (404) Tenant has no SSO configuration
43 * @apiError {string} error="Server error" (500) Database query failed
44 * @apiExample {curl} Example:
45 * curl -X GET http://localhost:3000/api/sso/settings/123
46 * @apiSuccessExample {json} Success-Response:
47 * HTTP/1.1 200 OK
48 * {
49 * "provider": "azure",
50 * "clientId": "abc123-def456",
51 * "tenantId": "xyz789"
52 * }
53 */
54router.get('/settings/:tenant_id', async (req, res) => {
55 try {
56 const result = await pool.query('SELECT config FROM integrations WHERE tenant_id = $1 AND integration_type = $2', [req.params.tenant_id, 'sso']);
57 if (!result.rows.length) return res.status(404).json({ error: 'No SSO config found' });
58 res.json(result.rows[0].config || {});
59 } catch (err) {
60 res.status(500).json({ error: 'Server error' });
61 }
62});
63
64/**
65 * @api {post} /api/sso/settings/:tenant_id Save tenant SSO config
66 * @apiName SaveTenantSSOSettings
67 * @apiGroup SSO
68 * @apiDescription Save or update SSO configuration for a tenant. Upserts config in integrations
69 * table with integration_type='sso'. Config object should contain provider-specific settings.
70 * @apiParam {string} tenant_id Tenant ID
71 * @apiParam {object} config SSO configuration object (varies by provider)
72 * @apiSuccess {boolean} success=true Configuration saved successfully
73 * @apiError {string} error="Server error" (500) Database save failed
74 * @apiExample {curl} Example:
75 * curl -X POST http://localhost:3000/api/sso/settings/123 \
76 * -H "Content-Type: application/json" \
77 * -d '{"provider": "azure", "clientId": "abc123", "tenantId": "xyz789"}'
78 * @apiSuccessExample {json} Success-Response:
79 * HTTP/1.1 200 OK
80 * {
81 * "success": true
82 * }
83 */
84router.post('/settings/:tenant_id', async (req, res) => {
85 try {
86 const config = req.body;
87 // Upsert SSO config in integrations table
88 await pool.query(`INSERT INTO integrations (tenant_id, integration_type, config, is_active, created_at, updated_at)
89 VALUES ($1, $2, $3, true, NOW(), NOW())
90 ON CONFLICT (tenant_id, integration_type) DO UPDATE SET config = $3, updated_at = NOW()`,
91 [req.params.tenant_id, 'sso', JSON.stringify(config)]);
92 res.json({ success: true });
93 } catch (err) {
94 res.status(500).json({ error: 'Server error' });
95 }
96});
97
98/**
99 * @api {get} /api/sso/oauth/:tenant_id/start Start OAuth2 flow
100 * @apiName StartOAuth2Flow
101 * @apiGroup SSO
102 * @apiDescription Start OAuth2 authentication flow for tenant SSO. Currently not implemented.
103 * Placeholder for future OAuth2 provider support (Google, Okta, etc).
104 * @apiParam {string} tenant_id Tenant ID
105 * @apiSuccess {string} status="not_implemented"
106 * @apiSuccess {string} message Implementation status
107 * @apiExample {curl} Example:
108 * curl -X GET http://localhost:3000/api/sso/oauth/123/start
109 * @apiSuccessExample {json} Success-Response:
110 * HTTP/1.1 200 OK
111 * {
112 * "status": "not_implemented",
113 * "message": "OAuth2 flow not yet implemented."
114 * }
115 */
116router.get('/oauth/:tenant_id/start', (req, res) => {
117 // TODO: Implement OAuth2 flow (e.g., Azure AD, Google, Okta)
118 res.json({ status: 'not_implemented', message: 'OAuth2 flow not yet implemented.' });
119});
120
121/**
122 * @api {post} /api/sso/saml/:tenant_id/assert Handle SAML assertion
123 * @apiName HandleSAMLAssertion
124 * @apiGroup SSO
125 * @apiDescription Handle SAML assertion for tenant SSO. Currently not implemented.
126 * Placeholder for future SAML provider support.
127 * @apiParam {string} tenant_id Tenant ID
128 * @apiSuccess {string} status="not_implemented"
129 * @apiSuccess {string} message Implementation status
130 * @apiExample {curl} Example:
131 * curl -X POST http://localhost:3000/api/sso/saml/123/assert
132 * @apiSuccessExample {json} Success-Response:
133 * HTTP/1.1 200 OK
134 * {
135 * "status": "not_implemented",
136 * "message": "SAML assertion not yet implemented."
137 * }
138 */
139router.post('/saml/:tenant_id/assert', (req, res) => {
140 // TODO: Implement SAML assertion handling
141 res.json({ status: 'not_implemented', message: 'SAML assertion not yet implemented.' });
142});
143
144/**
145 * @api {get} /api/sso/user/:user_id Get user SSO info
146 * @apiName GetUserSSOInfo
147 * @apiGroup SSO
148 * @apiDescription Get SSO provider and ID for a user. Returns sso_provider and sso_id fields
149 * from users table.
150 * @apiParam {number} user_id User ID
151 * @apiSuccess {string} sso_provider SSO provider name (azure, google, okta, etc)
152 * @apiSuccess {string} sso_id Provider-specific user identifier
153 * @apiError {string} error="No SSO info found for user" (404) User not found or no SSO linked
154 * @apiError {string} error="Server error" (500) Database query failed
155 * @apiExample {curl} Example:
156 * curl -X GET http://localhost:3000/api/sso/user/456
157 * @apiSuccessExample {json} Success-Response:
158 * HTTP/1.1 200 OK
159 * {
160 * "sso_provider": "azure",
161 * "sso_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
162 * }
163 */
164router.get('/user/:user_id', async (req, res) => {
165 try {
166 const result = await pool.query('SELECT sso_provider, sso_id FROM users WHERE user_id = $1', [req.params.user_id]);
167 if (!result.rows.length) return res.status(404).json({ error: 'No SSO info found for user' });
168 res.json(result.rows[0]);
169 } catch (err) {
170 res.status(500).json({ error: 'Server error' });
171 }
172});
173
174/**
175 * @api {post} /api/sso/user/:user_id Link user to SSO provider
176 * @apiName LinkUserToSSO
177 * @apiGroup SSO
178 * @apiDescription Link a user account to an SSO provider. Updates sso_provider and sso_id
179 * fields in users table.
180 * @apiParam {number} user_id User ID
181 * @apiParam {string} sso_provider SSO provider name
182 * @apiParam {string} sso_id Provider-specific user identifier
183 * @apiSuccess {boolean} success=true User linked successfully
184 * @apiError {string} error="Server error" (500) Database update failed
185 * @apiExample {curl} Example:
186 * curl -X POST http://localhost:3000/api/sso/user/456 \
187 * -H "Content-Type: application/json" \
188 * -d '{"sso_provider": "azure", "sso_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}'
189 * @apiSuccessExample {json} Success-Response:
190 * HTTP/1.1 200 OK
191 * {
192 * "success": true
193 * }
194 */
195router.post('/user/:user_id', async (req, res) => {
196 try {
197 const { sso_provider, sso_id } = req.body;
198 await pool.query('UPDATE users SET sso_provider = $1, sso_id = $2 WHERE user_id = $3', [sso_provider, sso_id, req.params.user_id]);
199 res.json({ success: true });
200 } catch (err) {
201 res.status(500).json({ error: 'Server error' });
202 }
203});
204
205/**
206 * @api {get} /api/sso/azure/:tenant_id/start Start Azure AD OAuth2 flow
207 * @apiName StartAzureADFlow
208 * @apiGroup SSO
209 * @apiDescription Initialize Azure AD OAuth2 authentication flow using MSAL. Returns authorization
210 * URL for redirecting user to Microsoft login. Tenant ID passed via state parameter.
211 * @apiParam {string} tenant_id Tenant ID (passed as state parameter)
212 * @apiSuccess {string} url Azure AD authorization URL
213 * @apiError {string} error="Failed to generate Azure AD auth URL" (500) MSAL error
214 * @apiExample {curl} Example:
215 * curl -X GET http://localhost:3000/api/sso/azure/123/start
216 * @apiSuccessExample {json} Success-Response:
217 * HTTP/1.1 200 OK
218 * {
219 * "url": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=..."
220 * }
221 */
222router.get('/azure/:tenant_id/start', async (req, res) => {
223 const msalClient = new msal.ConfidentialClientApplication(azureConfig);
224 const authCodeUrlParameters = {
225 scopes: ['openid', 'profile', 'email'],
226 redirectUri: azureRedirectUri,
227 state: req.params.tenant_id
228 };
229 try {
230 const url = await msalClient.getAuthCodeUrl(authCodeUrlParameters);
231 res.json({ url });
232 } catch (err) {
233 res.status(500).json({ error: 'Failed to generate Azure AD auth URL' });
234 }
235});
236
237/**
238 * @api {get} /api/sso/azure/callback Azure AD OAuth2 callback
239 * @apiName AzureADCallback
240 * @apiGroup SSO
241 * @apiDescription Handle Azure AD OAuth2 callback. Exchanges authorization code for tokens,
242 * extracts user info from ID token, and updates user SSO info in database. Also stores tokens
243 * in tenant integration config.
244 * @apiParam {string} code Authorization code from Azure AD (query param)
245 * @apiParam {string} state Tenant ID (query param)
246 * @apiSuccess {boolean} success=true Authentication successful
247 * @apiSuccess {object} user User info from ID token
248 * @apiSuccess {string} user.email User email
249 * @apiSuccess {string} user.oid User object ID (Azure AD)
250 * @apiSuccess {string} user.name User display name
251 * @apiError {string} error="Missing code or tenant_id" (400) Required query params missing
252 * @apiError {string} error="Azure AD callback failed" (500) Token exchange or DB update failed
253 * @apiExample {curl} Example:
254 * curl -X GET "http://localhost:3000/api/sso/azure/callback?code=abc123&state=123"
255 * @apiSuccessExample {json} Success-Response:
256 * HTTP/1.1 200 OK
257 * {
258 * "success": true,
259 * "user": {
260 * "email": "user@example.com",
261 * "oid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
262 * "name": "John Doe"
263 * }
264 * }
265 */
266router.get('/azure/callback', async (req, res) => {
267 const code = req.query.code;
268 const tenant_id = req.query.state;
269 if (!code || !tenant_id) return res.status(400).json({ error: 'Missing code or tenant_id' });
270 const msalClient = new msal.ConfidentialClientApplication(azureConfig);
271 try {
272 const tokenResponse = await msalClient.acquireTokenByCode({
273 code,
274 scopes: ['openid', 'profile', 'email'],
275 redirectUri: azureRedirectUri
276 });
277 // Extract user info from ID token
278 const idToken = tokenResponse.idTokenClaims;
279 // Upsert user SSO info
280 await pool.query(
281 'UPDATE users SET sso_provider = $1, sso_id = $2 WHERE email = $3',
282 ['azure', idToken.oid || idToken.sub, idToken.email]
283 );
284 // Optionally store tokens in tenant SSO config
285 await pool.query(
286 "UPDATE integrations SET config = jsonb_set(coalesce(config, '{}'), '{azure}', $1::jsonb) WHERE tenant_id = $2 AND integration_type = $3",
287 [JSON.stringify(tokenResponse), tenant_id, 'sso']
288 );
289 res.json({ success: true, user: idToken });
290 } catch (err) {
291 res.status(500).json({ error: 'Azure AD callback failed', details: err.message });
292 }
293});
294
295module.exports = router;