Release 1.2.0: adopt shared @simpleworkjs/* packages; fix directory envelope drift
Rewire onto the shared @simpleworkjs/oidc-client, /directory-schema, /ldap, and
/app-stack packages (deleting the byte-identical local forks). utils/access.js
now fetches reachable hosts through the shared directory client, which
validates the {results} envelope and treats envelope drift as a failed group
rather than silently returning []. models/user_ldap.js is a thin wrapper over
createLdapClient (loose TLS default preserved). build_info moves to utils/ with
the shared {buildVersion,buildHash,buildYear} shape. Align ldapts ^8.1.8 and
redis ^6.1.0. Lockfile regenerated from the registry (no file:/link:).
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ const router = require('express').Router();
|
||||
const middleware = require('../middleware/auth');
|
||||
|
||||
// Authentication (local login + OIDC handshake). Unauthenticated by design.
|
||||
router.use('/auth', require('./auth'));
|
||||
router.use('/auth', require('../models').authRouter);
|
||||
|
||||
// Who am I — needs a valid session but no admin gate (drives the login state).
|
||||
router.use('/user', middleware.auth, require('./user'));
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const router = require('express').Router();
|
||||
const { rateLimit } = require('express-rate-limit');
|
||||
const conf = require('@simpleworkjs/conf');
|
||||
const { Auth } = require('../models/auth');
|
||||
const { OidcState } = require('../models/oidc_state');
|
||||
const oidc = require('../utils/oidc');
|
||||
const { safeInternalPath } = require('../utils/safe_redirect');
|
||||
|
||||
// Throttle unauthenticated auth endpoints (credential login + the OIDC
|
||||
// handshake) to blunt brute-force / callback abuse. Keyed per IP.
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 60, // 60 attempts per IP per window
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: {name: 'TooManyRequests', message: 'Too many attempts, please try again later.'},
|
||||
});
|
||||
|
||||
|
||||
router.post('/login', authLimiter, async function(req, res, next){
|
||||
try{
|
||||
let auth = await Auth.login(req.body);
|
||||
return res.json({
|
||||
login: true,
|
||||
token: auth.token.token,
|
||||
message:`${req.body.username} logged in!`,
|
||||
});
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.all('/logout', async function(req, res, next){
|
||||
try{
|
||||
if(req.user){
|
||||
await req.user.logout();
|
||||
}
|
||||
|
||||
res.json({message: 'Bye'})
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* OIDC login start: create a PKCE + state challenge, persist it (auto-expiring
|
||||
* via OidcState TTL), and redirect the browser to the SSO authorize endpoint.
|
||||
*/
|
||||
router.get('/oidc/start', authLimiter, async function(req, res, next){
|
||||
try{
|
||||
if(!conf.oidc || !conf.oidc.enabled){
|
||||
let error = new Error('OidcDisabled');
|
||||
error.status = 404;
|
||||
error.message = 'OIDC login is not enabled.';
|
||||
throw error;
|
||||
}
|
||||
|
||||
let {state, codeVerifier, codeChallenge} = oidc.createAuthRequest();
|
||||
await OidcState.create({
|
||||
state,
|
||||
codeVerifier,
|
||||
// Sanitize now so a hostile ?redirect= can't be stored and later
|
||||
// reflected into the login page's navigation.
|
||||
redirect: safeInternalPath(req.query.redirect || '/'),
|
||||
});
|
||||
|
||||
return res.redirect(oidc.buildAuthUrl(state, codeChallenge));
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* OIDC callback: validate state (consuming the one-time record), exchange the
|
||||
* code for tokens, read identity from userinfo, establish a session, and hand
|
||||
* the app token back to the browser via a URL fragment for the login page to
|
||||
* store in localStorage.
|
||||
*/
|
||||
router.get('/oidc/callback', authLimiter, async function(req, res, next){
|
||||
try{
|
||||
let {code, state} = req.query;
|
||||
if(!code || !state){
|
||||
let error = new Error('OidcCallbackInvalid');
|
||||
error.status = 400;
|
||||
error.message = 'Missing code or state.';
|
||||
throw error;
|
||||
}
|
||||
|
||||
// get() throws if the state is unknown or has expired — this both binds
|
||||
// the callback to our request and bounds replay.
|
||||
let saved = await OidcState.get(state);
|
||||
await saved.remove();
|
||||
|
||||
let tokens = await oidc.exchangeCode(code, saved.codeVerifier);
|
||||
let claims = await oidc.fetchUserInfo(tokens.access_token);
|
||||
let identity = oidc.claimsToIdentity(claims);
|
||||
|
||||
let {token} = await Auth.oidcSession(identity);
|
||||
|
||||
let redirect = safeInternalPath(saved.redirect || '/');
|
||||
return res.redirect(
|
||||
`/login#token=${encodeURIComponent(token.token)}&redirect=${encodeURIComponent(redirect)}`
|
||||
);
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -4,8 +4,10 @@ const path = require('path');
|
||||
const express = require('express');
|
||||
const router = require('express').Router();
|
||||
const conf = require('@simpleworkjs/conf');
|
||||
const buildInfo = require('../models/build_info');
|
||||
const buildInfo = require('../utils/build_info');
|
||||
const registry = require('../services/session_registry');
|
||||
const { safeInternalPath } = require('@simpleworkjs/oidc-client');
|
||||
const { mountStaticModules } = require('@simpleworkjs/app-stack');
|
||||
|
||||
const values = {
|
||||
title: conf.environment !== 'production' ? 'dev' : '',
|
||||
@@ -17,15 +19,14 @@ const values = {
|
||||
|
||||
// Serve front-end vendor libraries straight from node_modules (same convention
|
||||
// as the sibling apps), and the app's own JS/CSS/img from public/.
|
||||
const frontEndModules = ['bootstrap', 'mustache', 'jquery', '@fortawesome', 'moment', 'jq-repeat'];
|
||||
frontEndModules.forEach(dep => {
|
||||
router.use(`/static-modules/${dep}`, express.static(path.join(__dirname, `../node_modules/${dep}`), {maxAge: '7d'}));
|
||||
mountStaticModules(router, {
|
||||
root: path.join(__dirname, '..'),
|
||||
deps: ['bootstrap', 'mustache', 'jquery', '@fortawesome', 'moment', 'jq-repeat'],
|
||||
});
|
||||
router.use('/static', express.static(path.join(__dirname, '../public'), {maxAge: '1h'}));
|
||||
|
||||
// Liveness probe — no auth.
|
||||
router.get('/health', (req, res) => {
|
||||
res.json({status: 'ok', activeSessions: registry.count(), version: buildInfo.version, commit: buildInfo.commit});
|
||||
res.json({status: 'ok', activeSessions: registry.count(), buildVersion: buildInfo.buildVersion, buildHash: buildInfo.buildHash});
|
||||
});
|
||||
|
||||
router.get('/', (req, res) => res.redirect(302, '/dashboard'));
|
||||
@@ -36,7 +37,7 @@ router.get('/', (req, res) => res.redirect(302, '/dashboard'));
|
||||
// /login when there's no valid session.
|
||||
router.get('/login', (req, res) => res.render('login', {
|
||||
...values,
|
||||
redirect: '/',
|
||||
redirect: safeInternalPath(req.query.redirect || '/'),
|
||||
oidcEnabled: !!(conf.oidc && conf.oidc.enabled),
|
||||
}));
|
||||
router.get('/dashboard', (req, res) => res.render('dashboard', {...values}));
|
||||
|
||||
Reference in New Issue
Block a user