OIDC Integration (OpenID) example
Hello,
Because almost all pull requests to this repo stay ignored by maintainer for a long time, I'm creating this as issue, that may helpful for someone else. This is PoC integration oauth2-proxy, that allow to use OpenId providers (list can be found at https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider) with this app. PoC uses docker-compose, but can easy back ported to k8s or etc.
This is steps works on current master revision.
- Application build should be fixed by merging this: https://github.com/timeoff-management/timeoff-management-application/pull/531/files
- If you want to use mysql 8+, then next patch should be applied:
Index: lib/model/db/company.js
===================================================================
diff --git a/lib/model/db/company.js b/lib/model/db/company.js
--- a/lib/model/db/company.js (revision e0ca678927862600b65d22e090be92f608373f7f)
+++ b/lib/model/db/company.js (date 1680865639019)
@@ -84,7 +84,7 @@
comment : "Indicate which mode the company account is in.",
},
timezone : {
- type : DataTypes.TEXT,
+ type : DataTypes.STRING,
allowNull : true,
defaultValue : 'Europe/London',
comment : 'Timezone current company is located in',
- File
lib/middleware/oidc_handler.jsshould be created:
"use strict";
module.exports = async function (req, res, next) {
const oidcEmail = req?.headers?.["x-forwarded-email"].toLowerCase();
const oidcName = req?.headers?.["x-forwarded-user"];
const oidcGroups = req?.headers?.["x-forwarded-groups"]; //May be used to match with DepartmentId
const models = req.app.get('db_model');
if (!req.user) {
let user = await models.User.find_by_email(oidcEmail);
if (user === null) {
const newUser = {
email: oidcEmail,
lastname: oidcName,
name: oidcName,
companyId: 1, //TODO: move to config
DepartmentId: 1, //TODO: map to oidcGroups
password: "null", // it should be string, not null
admin: false,
auto_approve: false,
end_date: null,
start_date: new Date(),
}
await models.User.create(newUser);
user = await models.User.find_by_email(oidcEmail);
}
req.user = await user.reload_with_session_details();
}
next();
};
- Apply patch:
Index: app.js
===================================================================
diff --git a/app.js b/app.js
--- a/app.js (revision e0ca678927862600b65d22e090be92f608373f7f)
+++ b/app.js (date 1680870481183)
@@ -33,8 +33,6 @@
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
-
-
// Setup authentication mechanism
const passport = require('./lib/passport')();
@@ -44,6 +42,7 @@
app.use(passport.initialize());
app.use(passport.session());
+app.use( require('./lib/middleware/oidc_handler') );
// Custom middlewares
-
docker-compose.yamlexample for simples https://oauth2-proxy.github.io/ integration:
version: '3.8'
services:
app:
build: .
oauth2-proxy:
image: bitnami/oauth2-proxy
depends_on:
- app
ports:
- '3000:3000'
command:
- oauth2-proxy
- --upstream=http://app:3000/
- --http-address=:3000
- --scope=email read_api read_user openid profile
- --cookie-secure=true
- --provider=gitlab
- --email-domain=*
- --oidc-issuer-url=https://gitlab.example.com
- --redirect-url=http://localhost:3000/oauth2/callback
# generate cookie-secret according to https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/overview#generating-a-cookie-secret
- --cookie-secret=change_me
- --client-id=change_me_to_gitlab_app_id
- --client-secret=change_me_to_gitlab_app_secret
db:
image: mysql:8
command:
- mysqld
- --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: 'password'
MYSQL_PASSWORD: 'password'
MYSQL_USER: 'user'
MYSQL_DATABASE: 'timeoff'
- replace DB config in config/db.json, and start app
- login directly to timeoff-management app and create first campaign
- on http://localhost:3000 you should see oidc proxy login page
Hope this project will be supported again. Big thanks to authors.
@vsychov Thanks for sharing this. I'm trying to get it working with azure ad but I'm running into a problem with oidc_handler.js.
I'd really appreciate it if you could look at the below and point to what I'm doing wrong.
I'm not sure if the dependencies have changed since you originally did the design but I had to edit the Dockerfile to make it build:
FROM node:18-alpine
EXPOSE 3000
LABEL org.label-schema.schema-version="1.3.0"
LABEL org.label-schema.docker.cmd="docker run -d -p 3000:3000 --name timeoff-management"
RUN apk update
RUN apk upgrade
#Install dependencies
RUN apk add \
git \
make \
python3 \
g++ \
gcc \
libc-dev \
clang
#Add user so it doesn't run as root
RUN adduser --system app --home /app
USER app
WORKDIR /app
#clone app
RUN git clone https://github.com/timeoff-management/application.git timeoff-management
WORKDIR /app/timeoff-management
#Add in OIDC integration
COPY oidc_handler.js /app/timeoff-management/lib/middleware/oidc_handler.js
COPY app.js /app/timeoff-management/
RUN rm package-lock.json
#Update some dependencies
RUN sed -i 's/formidable"\: "~1.0.17/formidable"\: "1.1.1/' package.json
RUN sed -i 's/sqlite3"\: "^4.0.1/sqlite3"\: "^5.1.5/' package.json
RUN sed -i 's/node-sass"\: "^4.5.3/node-sass"\: "^7.0.3/' package.json
RUN sed -i 's/graceful-fs"\: "^4.4.2/graceful-fs"\: "4.4.2/' package.json
#install app
RUN npm install -y
CMD npm start
For simplicities sake I'm sticking with SQLite so haven't applied the MySQL patch.
The container runs but when you access the login page (original inbuilt one) it doesn't load and crashes the container. The logs show the following:
/app/timeoff-management/lib/middleware/oidc_handler.js:4
const oidcEmail = req?.headers?.["x-forwarded-email"].toLowerCase();
^
TypeError: Cannot read properties of undefined (reading 'toLowerCase')
at module.exports (/app/timeoff-management/lib/middleware/oidc_handler.js:4:58)
at Layer.handle [as handle_request] (/app/timeoff-management/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/app/timeoff-management/node_modules/express/lib/router/index.js:328:13)
at /app/timeoff-management/node_modules/express/lib/router/index.js:286:9
at Function.process_params (/app/timeoff-management/node_modules/express/lib/router/index.js:346:12)
at next (/app/timeoff-management/node_modules/express/lib/router/index.js:280:10)
at strategy.pass (/app/timeoff-management/node_modules/passport/lib/middleware/authenticate.js:325:9)
at SessionStrategy.authenticate (/app/timeoff-management/node_modules/passport/lib/strategies/session.js:71:10)
at attempt (/app/timeoff-management/node_modules/passport/lib/middleware/authenticate.js:348:16)
at authenticate (/app/timeoff-management/node_modules/passport/lib/middleware/authenticate.js:349:7)
at Layer.handle [as handle_request] (/app/timeoff-management/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/app/timeoff-management/node_modules/express/lib/router/index.js:328:13)
at /app/timeoff-management/node_modules/express/lib/router/index.js:286:9
at Function.process_params (/app/timeoff-management/node_modules/express/lib/router/index.js:346:12)
at next (/app/timeoff-management/node_modules/express/lib/router/index.js:280:10)
at initialize (/app/timeoff-management/node_modules/passport/lib/middleware/initialize.js:53:5)
at Layer.handle [as handle_request] (/app/timeoff-management/node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (/app/timeoff-management/node_modules/express/lib/router/index.js:328:13)
at /app/timeoff-management/node_modules/express/lib/router/index.js:286:9
at Function.process_params (/app/timeoff-management/node_modules/express/lib/router/index.js:346:12)
at next (/app/timeoff-management/node_modules/express/lib/router/index.js:280:10)
at Model.<anonymous> (/app/timeoff-management/node_modules/express-session/index.js:506:7)
at Model.tryCatcher (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/util.js:16:23)
at Promise.successAdapter [as _fulfillmentHandler0] (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/nodeify.js:23:30)
at Promise._settlePromise (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:601:21)
at Promise._settlePromise0 (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:649:10)
at Promise._settlePromises (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:729:18)
at _drainQueueStep (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/async.js:93:12)
Node.js v18.19.0
If I take out .toLowerCase(); like so: const oidcEmail = req?.headers?.["x-forwarded-email"]; //.toLowerCase(); I get email cannot be null error as follows:
> [email protected] start
> node bin/wwww
node:internal/process/promises:288
triggerUncaughtException(err, true /* fromPromise */);
^
Error [SequelizeValidationError]: notNull Violation: email cannot be null,
notNull Violation: name cannot be null,
notNull Violation: lastname cannot be null
at /app/timeoff-management/node_modules/sequelize/lib/instance-validator.js:74:14
at tryCatcher (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/util.js:16:23)
at Promise._settlePromiseFromHandler (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:547:31)
at Promise._settlePromise (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:604:18)
at Promise._settlePromise0 (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:649:10)
at Promise._settlePromises (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:729:18)
at Promise._fulfill (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:673:18)
at PromiseArray._resolve (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/promise_array.js:127:19)
at PromiseArray._promiseFulfilled (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/promise_array.js:145:14)
at Promise._settlePromise (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:609:26)
at Promise._settlePromise0 (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:649:10)
at Promise._settlePromises (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:729:18)
at _drainQueueStep (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/async.js:93:12)
at _drainQueue (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/async.js:86:9)
at Async._drainQueues (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/async.js:102:5)
at Async.drainQueues [as _onImmediate] (/app/timeoff-management/node_modules/sequelize/node_modules/bluebird/js/release/async.js:15:14)
at process.processImmediate (node:internal/timers:476:21) {
errors: [
{
message: 'email cannot be null',
type: 'notNull Violation',
path: 'email',
value: null
},
{
message: 'name cannot be null',
type: 'notNull Violation',
path: 'name',
value: null
},
{
message: 'lastname cannot be null',
type: 'notNull Violation',
path: 'lastname',
value: null
}
]
}
Node.js v18.19.0
I'm not really sure where to go from here.
@pwpbarney, it's simple, it seems you don't have the x-forwarded-email header, which should always be included, https://github.com/oauth2-proxy/oauth2-proxy/blob/5e30a6fe948fc470108d9e522fe00ea656c94785/docs/docs/configuration/overview.md?plain=1#L145