timeoff-management-application icon indicating copy to clipboard operation
timeoff-management-application copied to clipboard

OIDC Integration (OpenID) example

Open vsychov opened this issue 3 years ago • 2 comments

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.js should 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.yaml example 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 avatar Apr 07 '23 13:04 vsychov

@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 avatar Dec 07 '23 16:12 pwpbarney

@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

vsychov avatar Dec 07 '23 21:12 vsychov