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 | |
| 16 | import { Hono } from 'hono'; |
| 17 | import { setCookie } from 'hono/cookie'; |
| 18 | |
| 19 | import { |
| 20 | requireAuthCoreDashboard, |
| 21 | requireAuthCoreProject, |
| 22 | } from '../middleware/auth-core-guard.js'; |
| 23 | import { BRIVEN_ENGINE_ID, isAuthCoreInitialized } from '../services/auth-core/engine.js'; |
| 24 | import { |
| 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'; |
| 38 | import type { AppEnv } from '../types/app-env.js'; |
| 39 | |
| 40 | export const authCoreSsoRouter = new Hono<AppEnv>(); |
| 41 | |
| 42 | authCoreSsoRouter.use( |
| 43 | '/v1/auth-core/projects/:projectId/sso/*', |
| 44 | ...requireAuthCoreProject('admin'), |
| 45 | ); |
| 46 | |
| 47 | // ─── Admin CRUD ─────────────────────────────────────────────────────────── |
| 48 | |
| 49 | authCoreSsoRouter.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 | |
| 71 | authCoreSsoRouter.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 | |
| 123 | authCoreSsoRouter.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 | |
| 154 | authCoreSsoRouter.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) |
| 169 | authCoreSsoRouter.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 | |
| 193 | authCoreSsoRouter.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 | |
| 209 | authCoreSsoRouter.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 | |
| 229 | authCoreSsoRouter.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 | |
| 277 | authCoreSsoRouter.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 | |
| 293 | authCoreSsoRouter.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 | ); |