auth-core-sso.ts337 lines · main
1/**
2 * briven-engine enterprise SSO routes (Phase enterprise).
3 *
4 * Admin (dashboard session + project admin):
5 * GET/POST /v1/auth-core/projects/:projectId/sso/connections
6 * PATCH/DELETE .../sso/connections/:connectionId
7 *
8 * Login (public ACS / OIDC callback):
9 * GET /v1/auth-core/sso/saml/:connectionId
10 * POST /v1/auth-core/sso/saml/:connectionId/acs
11 * GET /v1/auth-core/sso/saml/:connectionId/metadata
12 * GET /v1/auth-core/sso/oidc/:connectionId
13 * GET /v1/auth-core/sso/oidc/:connectionId/callback
14 */
15
16import { Hono } from 'hono';
17import { setCookie } from 'hono/cookie';
18
19import {
20 requireAuthCoreDashboard,
21 requireAuthCoreProject,
22} from '../middleware/auth-core-guard.js';
23import { BRIVEN_ENGINE_ID, isAuthCoreInitialized } from '../services/auth-core/engine.js';
24import {
25 completeOidcLogin,
26 completeSamlLogin,
27 createEngineSsoConnection,
28 deactivateEngineSsoConnection,
29 generateSamlMetadataXml,
30 getEngineSsoConnection,
31 listEngineSsoConnections,
32 publicSsoConnection,
33 startOidcLogin,
34 startSamlLogin,
35 updateEngineSsoConnection,
36 type SsoProviderType,
37} from '../services/auth-core/sso.js';
38import type { AppEnv } from '../types/app-env.js';
39
40export const authCoreSsoRouter = new Hono<AppEnv>();
41
42authCoreSsoRouter.use(
43 '/v1/auth-core/projects/:projectId/sso/*',
44 ...requireAuthCoreProject('admin'),
45);
46
47// ─── Admin CRUD ───────────────────────────────────────────────────────────
48
49authCoreSsoRouter.get(
50 '/v1/auth-core/projects/:projectId/sso/connections',
51 async (c) => {
52 if (!isAuthCoreInitialized()) {
53 return c.json(
54 { engine: BRIVEN_ENGINE_ID, code: 'auth_core_sdk_not_ready', connections: [] },
55 503,
56 );
57 }
58 const projectId = c.req.param('projectId');
59 const connections = await listEngineSsoConnections(projectId);
60 return c.json({
61 engine: BRIVEN_ENGINE_ID,
62 storage: 'doltgres',
63 projectId,
64 connections: connections.map(publicSsoConnection),
65 productNote:
66 'SAML + OIDC enterprise SSO on briven-engine. productionReady=true when IdP fields are complete.',
67 });
68 },
69);
70
71authCoreSsoRouter.post(
72 '/v1/auth-core/projects/:projectId/sso/connections',
73 async (c) => {
74 const projectId = c.req.param('projectId');
75 let body: {
76 name?: string;
77 providerType?: SsoProviderType;
78 domains?: string[];
79 config?: Record<string, unknown>;
80 jitEnabled?: boolean;
81 } = {};
82 try {
83 body = await c.req.json();
84 } catch {
85 body = {};
86 }
87 if (!body.name || !body.providerType) {
88 return c.json(
89 {
90 engine: BRIVEN_ENGINE_ID,
91 code: 'bad_request',
92 message: 'name and providerType (saml|oidc) required',
93 },
94 400,
95 );
96 }
97 try {
98 const connection = await createEngineSsoConnection({
99 projectId,
100 name: body.name,
101 providerType: body.providerType,
102 domains: body.domains,
103 config: body.config,
104 jitEnabled: body.jitEnabled,
105 });
106 return c.json({
107 engine: BRIVEN_ENGINE_ID,
108 connection: publicSsoConnection(connection),
109 });
110 } catch (err) {
111 return c.json(
112 {
113 engine: BRIVEN_ENGINE_ID,
114 code: 'create_failed',
115 message: err instanceof Error ? err.message : String(err),
116 },
117 400,
118 );
119 }
120 },
121);
122
123authCoreSsoRouter.patch(
124 '/v1/auth-core/projects/:projectId/sso/connections/:connectionId',
125 async (c) => {
126 const connectionId = c.req.param('connectionId');
127 const projectId = c.req.param('projectId');
128 const existing = await getEngineSsoConnection(connectionId);
129 if (!existing || existing.projectId !== projectId) {
130 return c.json({ engine: BRIVEN_ENGINE_ID, code: 'not_found' }, 404);
131 }
132 let body: {
133 name?: string;
134 domains?: string[];
135 config?: Record<string, unknown>;
136 jitEnabled?: boolean;
137 } = {};
138 try {
139 body = await c.req.json();
140 } catch {
141 body = {};
142 }
143 const updated = await updateEngineSsoConnection(connectionId, body);
144 if (!updated) {
145 return c.json({ engine: BRIVEN_ENGINE_ID, code: 'not_found' }, 404);
146 }
147 return c.json({
148 engine: BRIVEN_ENGINE_ID,
149 connection: publicSsoConnection(updated),
150 });
151 },
152);
153
154authCoreSsoRouter.delete(
155 '/v1/auth-core/projects/:projectId/sso/connections/:connectionId',
156 async (c) => {
157 const connectionId = c.req.param('connectionId');
158 const projectId = c.req.param('projectId');
159 const existing = await getEngineSsoConnection(connectionId);
160 if (!existing || existing.projectId !== projectId) {
161 return c.json({ engine: BRIVEN_ENGINE_ID, code: 'not_found' }, 404);
162 }
163 await deactivateEngineSsoConnection(connectionId);
164 return c.json({ engine: BRIVEN_ENGINE_ID, ok: true, connectionId });
165 },
166);
167
168// Dashboard-only list all ready connections across tenants (operator overview)
169authCoreSsoRouter.get(
170 '/v1/auth-core/sso/status',
171 requireAuthCoreDashboard(),
172 async (c) => {
173 return c.json({
174 engine: BRIVEN_ENGINE_ID,
175 storage: 'doltgres',
176 product: 'Briven Auth enterprise SSO',
177 saml: {
178 start: 'GET /v1/auth-core/sso/saml/:connectionId',
179 acs: 'POST /v1/auth-core/sso/saml/:connectionId/acs',
180 metadata: 'GET /v1/auth-core/sso/saml/:connectionId/metadata',
181 },
182 oidc: {
183 start: 'GET /v1/auth-core/sso/oidc/:connectionId',
184 callback: 'GET /v1/auth-core/sso/oidc/:connectionId/callback',
185 },
186 note: 'Configure connections under Enterprise tab. productionReady when IdP SSO URL+cert (SAML) or client id/secret+issuer/URLs (OIDC) are set.',
187 });
188 },
189);
190
191// ─── Public login ─────────────────────────────────────────────────────────
192
193authCoreSsoRouter.get('/v1/auth-core/sso/saml/:connectionId/metadata', async (c) => {
194 try {
195 const xml = await generateSamlMetadataXml(c.req.param('connectionId'));
196 return c.body(xml, 200, { 'content-type': 'application/xml; charset=utf-8' });
197 } catch (err) {
198 return c.json(
199 {
200 engine: BRIVEN_ENGINE_ID,
201 code: 'metadata_failed',
202 message: err instanceof Error ? err.message : String(err),
203 },
204 400,
205 );
206 }
207});
208
209authCoreSsoRouter.get('/v1/auth-core/sso/saml/:connectionId', async (c) => {
210 try {
211 const relayState = c.req.query('relayState') ?? undefined;
212 const { redirectUrl } = await startSamlLogin(
213 c.req.param('connectionId'),
214 relayState,
215 );
216 return c.redirect(redirectUrl, 302);
217 } catch (err) {
218 return c.json(
219 {
220 engine: BRIVEN_ENGINE_ID,
221 code: 'saml_start_failed',
222 message: err instanceof Error ? err.message : String(err),
223 },
224 400,
225 );
226 }
227});
228
229authCoreSsoRouter.post('/v1/auth-core/sso/saml/:connectionId/acs', async (c) => {
230 try {
231 const body = await c.req.parseBody();
232 const samlResponse =
233 typeof body.SAMLResponse === 'string' ? body.SAMLResponse : '';
234 if (!samlResponse) {
235 return c.json(
236 { engine: BRIVEN_ENGINE_ID, code: 'SAMLResponse_required' },
237 400,
238 );
239 }
240 const result = await completeSamlLogin({
241 connectionId: c.req.param('connectionId'),
242 samlResponse,
243 });
244 setCookie(c, 'sAccessToken', result.accessToken, {
245 httpOnly: true,
246 secure: true,
247 sameSite: 'Lax',
248 path: '/',
249 maxAge: 60 * 60 * 24 * 30,
250 });
251 const relay =
252 typeof body.RelayState === 'string' && body.RelayState.startsWith('http')
253 ? body.RelayState
254 : null;
255 if (relay) return c.redirect(relay, 302);
256 return c.json({
257 engine: BRIVEN_ENGINE_ID,
258 status: 'OK',
259 userId: result.userId,
260 email: result.email,
261 projectId: result.projectId,
262 tenantId: result.tenantId,
263 sessionHandle: result.sessionHandle,
264 });
265 } catch (err) {
266 return c.json(
267 {
268 engine: BRIVEN_ENGINE_ID,
269 code: 'saml_acs_failed',
270 message: err instanceof Error ? err.message : String(err),
271 },
272 400,
273 );
274 }
275});
276
277authCoreSsoRouter.get('/v1/auth-core/sso/oidc/:connectionId', async (c) => {
278 try {
279 const { redirectUrl } = await startOidcLogin(c.req.param('connectionId'));
280 return c.redirect(redirectUrl, 302);
281 } catch (err) {
282 return c.json(
283 {
284 engine: BRIVEN_ENGINE_ID,
285 code: 'oidc_start_failed',
286 message: err instanceof Error ? err.message : String(err),
287 },
288 400,
289 );
290 }
291});
292
293authCoreSsoRouter.get(
294 '/v1/auth-core/sso/oidc/:connectionId/callback',
295 async (c) => {
296 try {
297 const code = c.req.query('code');
298 const state = c.req.query('state');
299 if (!code || !state) {
300 return c.json(
301 { engine: BRIVEN_ENGINE_ID, code: 'code_and_state_required' },
302 400,
303 );
304 }
305 const result = await completeOidcLogin({
306 connectionId: c.req.param('connectionId'),
307 code,
308 state,
309 });
310 setCookie(c, 'sAccessToken', result.accessToken, {
311 httpOnly: true,
312 secure: true,
313 sameSite: 'Lax',
314 path: '/',
315 maxAge: 60 * 60 * 24 * 30,
316 });
317 return c.json({
318 engine: BRIVEN_ENGINE_ID,
319 status: 'OK',
320 userId: result.userId,
321 email: result.email,
322 projectId: result.projectId,
323 tenantId: result.tenantId,
324 sessionHandle: result.sessionHandle,
325 });
326 } catch (err) {
327 return c.json(
328 {
329 engine: BRIVEN_ENGINE_ID,
330 code: 'oidc_callback_failed',
331 message: err instanceof Error ? err.message : String(err),
332 },
333 400,
334 );
335 }
336 },
337);