Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions .example.env
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,10 @@ OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_SCOPE=
OIDC_EMAIL_CLAIM=
# Optional - Claim name containing user roles/groups (default: roles)
Comment thread
smartcoder0777 marked this conversation as resolved.
Outdated
OIDC_ROLE_CLAIM=roles
# Optional - Group or role value that grants admin privileges
# Example: "admin" or "kutt-admins"
# If set, users with this group/role will be granted admin status
# Admin status is updated on every login to stay in sync with IdP
OIDC_ADMIN_GROUP=
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
36 changes: 31 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,32 @@ 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
let shouldBeAdmin = false;
if (env.OIDC_ADMIN_GROUP) {
const roleClaim = userinfo[env.OIDC_ROLE_CLAIM];
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 +148,7 @@ if (env.OIDC_ENABLED) {
verified: true,
verification_token: null,
verification_expires: null,
role: desiredRole,
});
return done(null, updatedUser);

Expand All @@ -136,5 +160,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