Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .example.env
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,5 @@ OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_SCOPE=
OIDC_EMAIL_CLAIM=
OIDC_ROLE_CLAIM=roles
OIDC_ADMIN_GROUP=
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ You can use files for each of the variables by appending `_FILE` to the name of
| `OIDC_CLIENT_SECRET` | OIDC client secret | - | `some-secret` |
| `OIDC_SCOPE` | OIDC Scope | `openid profile email` | `openid email` |
| `OIDC_EMAIL_CLAIM` | Name of the field to get user's email from | `email` | `userEmail` |
| `OIDC_ROLE_CLAIM` | Name of the claim containing user roles/groups | `roles` | `groups` |
| `OIDC_ADMIN_GROUP` | Group or role value that grants admin privileges. Admin status is updated on every login to stay in sync with IdP | - | `admin` |
| `REPORT_EMAIL` | The email address that will receive submitted reports | - | `example@yoursite.com` |
| `CONTACT_EMAIL` | The support email address to show on the app | - | `example@yoursite.com` |

Expand Down
2 changes: 2 additions & 0 deletions server/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ const spec = {
OIDC_CLIENT_SECRET: str({ default: "" }),
OIDC_SCOPE: str({ default: "openid profile email" }),
OIDC_EMAIL_CLAIM: str({ default: "email" }),
OIDC_ROLE_CLAIM: str({ default: "roles" }),
OIDC_ADMIN_GROUP: str({ default: "" }),
ENABLE_RATE_LIMIT: bool({ default: false }),
REPORT_EMAIL: str({ default: "" }),
CONTACT_EMAIL: str({ default: "" }),
Expand Down
2 changes: 1 addition & 1 deletion server/handlers/helpers.handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const env = require("../env");
function error(error, req, res, _next) {
if (!(error instanceof CustomError)) {
console.error(error);
} else if (env.isDev) {
Comment thread
smartcoder0777 marked this conversation as resolved.
} else if (process.env.NODE_ENV !== 'production') {
console.error(error.message);
}

Expand Down
45 changes: 40 additions & 5 deletions server/passport.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const bcrypt = require("bcryptjs");

const query = require("./queries");
const env = require("./env");
const utils = require("./utils")
const utils = require("./utils");
const { ROLES } = require("./consts");

const jwtOptions = {
jwtFromRequest: req => req.cookies?.token,
Expand Down Expand Up @@ -74,6 +75,8 @@ passport.use(
})
);

let oidcInitPromise = null;

if (env.OIDC_ENABLED) {
async function enableOIDC() {
const requiredKeys = ["OIDC_ISSUER", "OIDC_CLIENT_ID", "OIDC_CLIENT_SECRET", "OIDC_SCOPE", "OIDC_EMAIL_CLAIM"];
Expand Down Expand Up @@ -108,12 +111,41 @@ if (env.OIDC_ENABLED) {
async (req, tokenset, userinfo, done) => {
try {
const email = userinfo[env.OIDC_EMAIL_CLAIM];

// Check if user should be admin based on OIDC claims
// Claims can be in either the ID token or userinfo, check both
let shouldBeAdmin = false;
if (env.OIDC_ADMIN_GROUP) {
const claims = tokenset.claims();
const roleClaim = claims[env.OIDC_ROLE_CLAIM] || userinfo[env.OIDC_ROLE_CLAIM];
if (process.env.NODE_ENV !== 'production') {
console.log('OIDC admin check:', {
roleClaim,
expectedGroup: env.OIDC_ADMIN_GROUP,
email
});
}
if (roleClaim) {
// Handle both array and string claim values
const roles = Array.isArray(roleClaim) ? roleClaim : [roleClaim];
shouldBeAdmin = roles.includes(env.OIDC_ADMIN_GROUP);
}
}

const desiredRole = shouldBeAdmin ? ROLES.ADMIN : ROLES.USER;
const existingUser = await query.user.find({ email });

// Existing user.
if (existingUser) return done(null, existingUser);
// Existing user - update role if needed
if (existingUser) {
// Update role on every login to stay in sync with IdP
if (existingUser.role !== desiredRole) {
const updatedUser = await query.user.update({ id: existingUser.id }, { role: desiredRole });
return done(null, updatedUser);
}
return done(null, existingUser);
}

// New user.
// New user - create with appropriate role
// Generate a random password which is not supposed to be used directly.
const salt = await bcrypt.genSalt(12);
const password = utils.generateRandomPassword();
Expand All @@ -125,6 +157,7 @@ if (env.OIDC_ENABLED) {
verified: true,
verification_token: null,
verification_expires: null,
role: desiredRole,
});
return done(null, updatedUser);

Expand All @@ -136,5 +169,7 @@ if (env.OIDC_ENABLED) {
);
}

enableOIDC();
oidcInitPromise = enableOIDC();
}

module.exports = { oidcInitPromise };
23 changes: 19 additions & 4 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ if (env.NODE_APP_INSTANCE === 0) {
}

// intialize passport authentication library
require("./passport");
const { oidcInitPromise } = require("./passport");

// create express app
const app = express();
Expand Down Expand Up @@ -86,7 +86,22 @@ app.get("*", renders.notFound);

// handle errors coming from above routes
app.use(helpers.error);

// Start server after OIDC initialization (if enabled)
async function startServer() {
if (oidcInitPromise) {
try {
await oidcInitPromise;
console.log("> OIDC initialized successfully");
} catch (error) {
console.error("Failed to initialize OIDC:", error);
process.exit(1);
}
}

app.listen(env.PORT, () => {
console.log(`> Ready on http://localhost:${env.PORT}`);
});
app.listen(env.PORT, () => {
console.log(`> Ready on http://localhost:${env.PORT}`);
});
}

startServer();
6 changes: 4 additions & 2 deletions server/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,14 @@ function addProtocol(url) {
}

function getSiteURL() {
const protocol = !env.isDev ? "https://" : "http://";
const isDev = process.env.NODE_ENV !== 'production';
const protocol = !isDev ? "https://" : "http://";
return `${protocol}${env.DEFAULT_DOMAIN}`;
}

function getShortURL(address, domain) {
const protocol = (env.CUSTOM_DOMAIN_USE_HTTPS || !domain) && !env.isDev ? "https://" : "http://";
const isDev = process.env.NODE_ENV !== 'production';
const protocol = (env.CUSTOM_DOMAIN_USE_HTTPS || !domain) && !isDev ? "https://" : "http://";
const link = `${domain || env.DEFAULT_DOMAIN}/${address}`;
const url = `${protocol}${link}`;
return { address, link, url };
Expand Down