|
| 1 | +// ChulaSSO 2.0 authentication (custom ticket flow — NOT OIDC). |
| 2 | +// Docs: https://account.it.chula.ac.th/html/docs |
| 3 | +// 1. Redirect the browser to {BASE}/login?service={callback}&app_id={id} |
| 4 | +// 2. ChulaSSO redirects back to {callback}?ticket={id} |
| 5 | +// 3. Validate server-side: POST {BASE}/serviceValidation with headers |
| 6 | +// DeeAppId / DeeAppSecret / DeeTicket -> JSON profile { email, roles, ... } |
| 7 | +// On success we find-or-create a verified Kutt user (mirrors the OIDC handler |
| 8 | +// in passport.js) and issue the normal Kutt session cookie. |
| 9 | +const bcrypt = require("bcryptjs"); |
| 10 | + |
| 11 | +const { roleAllowed } = require("./chulasso.util"); |
| 12 | +const query = require("../queries"); |
| 13 | +const utils = require("../utils"); |
| 14 | +const env = require("../env"); |
| 15 | + |
| 16 | +const CustomError = utils.CustomError; |
| 17 | + |
| 18 | +// Step 1 — send the browser to ChulaSSO with our callback as `service`. |
| 19 | +async function start(req, res) { |
| 20 | + const params = new URLSearchParams({ |
| 21 | + service: utils.getSiteURL() + "/login/chulasso", |
| 22 | + app_id: env.CHULASSO_APP_ID, |
| 23 | + serviceName: env.CHULASSO_SERVICE_NAME || env.SITE_NAME, |
| 24 | + }); |
| 25 | + res.redirect(`${env.CHULASSO_BASE}/login?${params.toString()}`); |
| 26 | +} |
| 27 | + |
| 28 | +// Step 3 — validate the returned ticket and log the user in. |
| 29 | +async function callback(req, res) { |
| 30 | + const ticket = req.query.ticket; |
| 31 | + if (!ticket) { |
| 32 | + throw new CustomError("ChulaSSO did not return a ticket.", 400); |
| 33 | + } |
| 34 | + |
| 35 | + const response = await fetch(`${env.CHULASSO_BASE}/serviceValidation`, { |
| 36 | + method: "POST", |
| 37 | + headers: { |
| 38 | + DeeAppId: env.CHULASSO_APP_ID, |
| 39 | + DeeAppSecret: env.CHULASSO_APP_SECRET, |
| 40 | + DeeTicket: String(ticket), |
| 41 | + }, |
| 42 | + }); |
| 43 | + |
| 44 | + if (!response.ok) { |
| 45 | + throw new CustomError("ChulaSSO ticket validation failed.", 401); |
| 46 | + } |
| 47 | + |
| 48 | + const profile = await response.json(); |
| 49 | + const email = profile && profile.email; |
| 50 | + if (!email) { |
| 51 | + throw new CustomError("ChulaSSO profile did not include an email.", 400); |
| 52 | + } |
| 53 | + |
| 54 | + if (!roleAllowed(profile.roles, env.CHULASSO_ALLOWED_ROLES)) { |
| 55 | + throw new CustomError("Your Chula account is not permitted to use this service.", 403); |
| 56 | + } |
| 57 | + |
| 58 | + let user = await query.user.find({ email }); |
| 59 | + |
| 60 | + // First login for this account: create it, pre-verified (no email/password flow). |
| 61 | + if (!user) { |
| 62 | + const salt = await bcrypt.genSalt(12); |
| 63 | + const password = await bcrypt.hash(utils.generateRandomPassword(), salt); |
| 64 | + const created = await query.user.add({ email, password }); |
| 65 | + user = await query.user.update(created, { |
| 66 | + verified: true, |
| 67 | + verification_token: null, |
| 68 | + verification_expires: null, |
| 69 | + }); |
| 70 | + } |
| 71 | + |
| 72 | + if (user.banned) { |
| 73 | + throw new CustomError("You're banned from using this website.", 403); |
| 74 | + } |
| 75 | + |
| 76 | + const token = utils.signToken(user); |
| 77 | + utils.setToken(res, token); |
| 78 | + res.redirect("/"); |
| 79 | +} |
| 80 | + |
| 81 | +module.exports = { start, callback, roleAllowed }; |
0 commit comments