'use strict'; const crypto = require('crypto'); const router = require('express').Router(); const {User} = require('../models/user'); const {Auth, AuthToken} = require('../models/auth'); const {PasswordResetToken, ImpersonationToken, OtpToken} = require('../models/token'); const {hashPasswordSSHA512} = require('../models/user_ldap'); const {UserVerification} = require('../models/verification'); const {SMS} = require('../models/sms'); const {Mail} = require('../models/email'); const middleware = require('../middleware/auth'); const rateLimit = require('../middleware/rate_limit'); const permission = require('../utils/permission'); const conf = require('@simpleworkjs/conf'); const metrics = require('../utils/metrics'); async function findUserByLogin(login) { try { return await User.get(login); } catch(e) { if (e.status === 404 || e.name === 'UserNotFound') { return await User.get({searchKey: 'mail', searchValue: login}); } throw e; } } router.get('/username-suggestions', async function(req, res, next) { try { const { givenName, sn, dob } = req.query; if (!givenName || !sn) return res.json({ suggestions: [] }); return res.json({ suggestions: await User.usernameSuggestions(givenName, sn, dob) }); } catch(e) { next(e); } }); router.post('/login', rateLimit.login, async function(req, res, next){ try{ let auth = await Auth.login(req.body); metrics.recordServiceUsage('SSO Web UI', req.body.uid); return res.json({ login: true, token: auth.token.token, message:`${req.body.uid} logged in!`, }); }catch(error){ if (error.name === 'LDAPLoginFailed' || error.status === 401 || error.name === 'UserNotFound') { metrics.recordFailedLogin(req.ip, req.body.uid); } 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); } }); router.post('/resetpassword', rateLimit.passwordReset, async function(req, res, next){ try{ let sent = await User.passwordReset(`${req.protocol}://${req.hostname}`, req.body.mail); console.info('resetpassword for', req.body.mail, 'sent') return res.json({ message: 'If the email address is in our system, you will receive a message.' }); }catch(error){ next(error); } }); router.post('/resetpassword/:token', rateLimit.passwordReset, async function(req, res, next){ try{ let token = await PasswordResetToken.get(req.params.token); if(token.is_valid && 86400000+Number(token.created_on) > (new Date).getTime()){ let user = await User.get(token.created_by); await user.setPassword(req.body); await token.update({is_valid: false}); return res.json({ message: 'Password has been changed.' }); } let error = new Error('TokenExpired'); error.name = 'TokenExpired'; error.message = 'Password reset token is invalid or has expired.'; error.status = 401; throw error; }catch(error){ next(error); } }); router.post('/invite/:token/:mailToken', rateLimit.invite, async function(req, res, next) { try{ req.body.token = req.params.token; req.body.mailToken = req.params.mailToken; let user = await User.addByInvite(req.body); let token = await AuthToken.create(user); return res.json({ user: user.uid, token: token.token }); }catch(error){ next(error); } }); router.post('/invite/:token', rateLimit.invite, async function(req, res, next){ try{ let data = { token: req.params.token, url: `${req.protocol}://${req.hostname}`, mail: req.body.mail, } await User.verifyEmail(data); return res.send({message: 'sent'}); }catch(error){ next(error) } }); router.post('/otp/request', rateLimit.otpRequest, async function(req, res, next) { try { const {login, method} = req.body; if (!login || !method) { return res.status(400).json({message: 'login and method are required'}); } const user = await findUserByLogin(login); if (method === 'sms' && !user.mobile) { return res.status(400).json({message: 'No phone number on file for this account'}); } const otp = await OtpToken.issue(user.uid, method); if (method === 'email') { await Mail.sendTemplate(user.mail, 'otp_code', {givenName: user.givenName, code: otp.code}); } else if (method === 'sms') { try { await SMS.send(user.mobile, `Your ${conf.name} login code: ${otp.code}`); } catch (smsErr) { const err = new Error('SMS delivery failed. Please try email or contact your administrator.'); err.status = 502; throw err; } } else { return res.status(400).json({message: 'method must be email or sms'}); } return res.json({message: 'Code sent', method, expires_at: otp.expires_at}); } catch(error) { next(error); } }); router.post('/otp/verify', rateLimit.otpVerify, async function(req, res, next) { try { const {login, code} = req.body; if (!login || !code) { return res.status(400).json({message: 'login and code are required'}); } const user = await findUserByLogin(login); const otp = await OtpToken.verify(user.uid, String(code)); if (!otp) { const error = new Error('Invalid or expired code'); error.status = 401; throw error; } const verif = await UserVerification.getOrCreate(user.uid); if (otp.method === 'email') await verif.markEmailVerified(); if (otp.method === 'sms') await verif.markPhoneVerified(); const authToken = await AuthToken.create(user); return res.json({login: true, token: authToken.token}); } catch(error) { next(error); } }); router.post('/impersonate/:uid', middleware.auth, async function(req, res, next) { try { await permission.byGroup(req.user, ['app_sso_admin']); const target = await User.get(req.params.uid); // Clean up any existing impersonation for this target const existing = await ImpersonationToken.list({ where: { target_uid: target.uid } }); for (const old of existing) { if (old.is_valid && !old.isExpired) { try { await target.removeTempPassword(old.temp_hash); } catch(_) {} } await old.update({ is_valid: false }); } const tempPassword = crypto.randomBytes(16).toString('base64url'); const tempHash = hashPasswordSSHA512(tempPassword); await target.addTempPassword(tempHash); const token = await ImpersonationToken.add({ admin_uid: req.user.uid, target_uid: target.uid, temp_hash: tempHash, }); return res.json({ uid: target.uid, temp_password: tempPassword, expires_at: token.expires_at, }); } catch(error) { next(error); } }); router.delete('/impersonate/:uid', middleware.auth, async function(req, res, next) { try { await permission.byGroup(req.user, ['app_sso_admin']); const target = await User.get(req.params.uid); const existing = await ImpersonationToken.list({ where: { target_uid: target.uid } }); let revoked = 0; for (const token of existing) { if (token.is_valid) { try { await target.removeTempPassword(token.temp_hash); } catch(_) {} await token.update({ is_valid: false }); revoked++; } } return res.json({ message: `Impersonation ended for ${target.uid}`, revoked }); } catch(error) { next(error); } }); module.exports = router; /* verify public ssh key */ // router.post('/verifykey', async function(req, res){ // let key = req.body.key; // try{ // return res.json({ // info: await Users.verifyKey(key) // }); // }catch(error){ // return res.status(400).json({ // message: 'Key is not a public key file!' // }); // } // });