{"id":23819833,"url":"https://github.com/kth/kth-node-passport-oidc","last_synced_at":"2025-07-13T17:09:28.838Z","repository":{"id":45639717,"uuid":"349203227","full_name":"KTH/kth-node-passport-oidc","owner":"KTH","description":null,"archived":false,"fork":false,"pushed_at":"2025-05-01T04:25:09.000Z","size":1288,"stargazers_count":0,"open_issues_count":7,"forks_count":0,"subscribers_count":12,"default_branch":"main","last_synced_at":"2025-07-06T09:50:29.942Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/KTH.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2021-03-18T20:05:12.000Z","updated_at":"2025-02-12T11:50:24.000Z","dependencies_parsed_at":"2024-08-01T15:41:23.472Z","dependency_job_id":"fc2fc7fb-3437-4315-a7b3-5641273c6bc8","html_url":"https://github.com/KTH/kth-node-passport-oidc","commit_stats":{"total_commits":193,"total_committers":11,"mean_commits":"17.545454545454547","dds":"0.36269430051813467","last_synced_commit":"2bfd7a25bc4542047d25dfc9131d7ecce1d6f93a"},"previous_names":[],"tags_count":1,"template":false,"template_full_name":"KTH/npm-template","purl":"pkg:github/KTH/kth-node-passport-oidc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KTH%2Fkth-node-passport-oidc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KTH%2Fkth-node-passport-oidc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KTH%2Fkth-node-passport-oidc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KTH%2Fkth-node-passport-oidc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/KTH","download_url":"https://codeload.github.com/KTH/kth-node-passport-oidc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KTH%2Fkth-node-passport-oidc/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265175567,"owners_count":23722661,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-01-02T07:16:03.705Z","updated_at":"2025-07-13T17:09:28.818Z","avatar_url":"https://github.com/KTH.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# KTH Node Passport OpenID Connect\n\nSimple and configurable package for OpenID Connect authentication. Based on [node-openid-client](https://github.com/panva/node-openid-client) and gives you Express middleware based on Passport.\n\n## Quick start\n\n```bash\n$ npm install @kth/kth-node-passport-oidc\n```\n\n```js\nconst { OpenIDConnect, hasGroup } = require('@kth/kth-node-passport-oidc')\n\nconst oidc = new OpenIDConnect(server, passport, {\n  ...config.oidc,\n  appCallbackLoginUrl: _addProxy('/auth/login/callback'),\n  appCallbackLogoutUrl: _addProxy('/auth/logout/callback'),\n  appCallbackSilentLoginUrl: _addProxy('/auth/silent/callback'),\n  defaultRedirect: _addProxy(''),\n  extendUser: (user, claims) =\u003e {\n    user.isAdmin = hasGroup(config.auth.adminGroup, user)\n  },\n  log,\n})\n\n// And use the middleware with your routes\nappRoute.get('node.page', _addProxy('/silent'), oidc.silentLogin, Sample.getIndex)\nappRoute.get('node.index', _addProxy('/'), oidc.login, Sample.getIndex)\n```\n\n## The basics\n\nThere are three basic OIDC functions\n\n### **login**\n\nA normal login. Use this middleware to force the user to login into the OpenID Connect server.\n\nAfter a successful login a user object can be found in req.user.\n\nIf not, the user may not visit the route\n\n### **silentLogin**\n\nA silent login. Basically the user is allowed to be anonymous.\n\nUse this middleware to check if the user is logged into the OpenID Connect server.\n\nIf the user is logged in a user object can be found in req.user.\n\nIf not, the user is anonymous and the req.user will be undefined. The silent login will occur again after `anonymousCookieMaxAge` expires.\n\n### **logout**\n\nLogs out the user from both the OpenID Connect server and this app.\n\n## Configuration\n\nConfiguration for each OIDC function comes in pairs. A pair consist of the same URL in two different formats.\n\nOne that is configured directly into the OpenID Connect client and the other is used to set up the URL in our app through a Express route.\n\nThese URLs are used by the OpenID Connect server to communicate with our app during authentication.\n\n\u003e Note: Only the login configuration is required but you will most likely want at least also the logout\n\n## req.user\n\nOn a successful login passport will add a user object on the request object. By default this object will have the following properties:\n\n| Property    | Type   | Example                               | Description                                                            |\n| ----------- | ------ | ------------------------------------- | ---------------------------------------------------------------------- |\n| username    | string | johnd                                 | KTH Username                                                           |\n| displayName | string | John Doe                              | Users full name                                                        |\n| email       | string |                                       | KTH email address. This requires higher security clearance             |\n| memberOf    | array  | ['app.myApp.user', 'app.myApp.admin'] | Groups connected to this user. This requires higher security clearance |\n\nIf you would like to add properties to the user object you can do this by adding a function called `extendUser` when instantiating OpenIDConnect. The function can also be async.\n\nThe function makes changes directly to the user object and must have this signature:\n\n```js\n;(user, claims) =\u003e {\n  user.isAwesome = true\n}\n```\n\n\u003e The claims argument is the full response from the OpenID Connect server\n\n### Parameters\n\n| Param                             | Type                  | Description                                                                                                                                                                                                                               |\n| --------------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| expressApp                        | \u003ccode\u003eObject\u003c/code\u003e   | The express app instance                                                                                                                                                                                                                  |\n| passport                          | \u003ccode\u003eObject\u003c/code\u003e   | The passport instance                                                                                                                                                                                                                     |\n| config                            | \u003ccode\u003eObject\u003c/code\u003e   | Configuration object                                                                                                                                                                                                                      |\n| config.configurationUrl           | \u003ccode\u003estring\u003c/code\u003e   | Url to OpenID Connect server Example: https://myOpenIDServer.com/adfs/.well-known/openid-configuration                                                                                                                                    |\n| config.clientId                   | \u003ccode\u003estring\u003c/code\u003e   | This apps clientID                                                                                                                                                                                                                        |\n| config.clientSecret               | \u003ccode\u003estring\u003c/code\u003e   | This apps client secret                                                                                                                                                                                                                   |\n| config.tokenSecret                | \u003ccode\u003estring\u003c/code\u003e   | This apps token secret, used for encrypting token for session storage                                                                                                                                                                     |\n| config.callbackLoginUrl           | \u003ccode\u003estring\u003c/code\u003e   | This apps full URL to callback function for standard login. Example: http://localhost:3000/node/auth/login/callback                                                                                                                       |\n| config.callbackLoginRoute         | \u003ccode\u003estring\u003c/code\u003e   | The callback route used for setting up the express route. Same as config.callbackUrl without host. Example: /node/auth/login/callback                                                                                                     |\n| [config.callbackSilentLoginUrl]   | \u003ccode\u003estring\u003c/code\u003e   | Optional This apps full URL to callback function for silent login. Example: http://localhost:3000/node/auth/silent/callback                                                                                                               |\n| [config.callbackSilentLoginRoute] | \u003ccode\u003estring\u003c/code\u003e   | Optional The silent callback route used for setting up the express route. Same as config.callbackUrl without host. Example: /node/auth/silent/callback                                                                                    |\n| [config.callbackLogoutUrl]        | \u003ccode\u003estring\u003c/code\u003e   | Optional This apps full URL to callback function for logout. Example: http://localhost:3000/node/auth/logout/callback                                                                                                                     |\n| [config.callbackLogoutRoute]      | \u003ccode\u003estring\u003c/code\u003e   | Optional The logout callback route used for setting up the express route. Same as config.callbackUrl without host. Example: /node/auth/logout/callback                                                                                    |\n| config.defaultRedirect            | \u003ccode\u003estring\u003c/code\u003e   | Fallback if no next url is supplied to login or on logout                                                                                                                                                                                 |\n| [config.extendUser]               | \u003ccode\u003efunction\u003c/code\u003e | Optional Function which gives you the possibility to add custom properties to the user object. The supplied function can be a async. Example: (user, claims) =\u003e { user.isAwesome = true } or async (user, claims) =\u003e { // do a api call } |\n| [config.log]                      | \u003ccode\u003eObject\u003c/code\u003e   | Optional Logger object which should have logging functions. Used for logging in this module. Example: logger.error('Error message')                                                                                                       |\n| [config.setIsOwner]               | \u003ccode\u003eboolean\u003c/code\u003e  | Optional flag with false as default. When used with requireRole, user object includes the property isOwner which is set to true only if req.parameter contains the same username as the logged in username.                               |\n|                                   |\n\n### Properties on the created OIDC\n\n\u003ca name=\"login\"\u003e\u003c/a\u003e\n\n## login(req, res, next) ⇒ \u003ccode\u003ePromise.\u0026lt;Middleware\u0026gt;\u003c/code\u003e\n\n**Kind**: global function  \n**Summary**: Check if the user it authenticated or else redirect to OpenID Connect server\nfor authentication  \n**Returns**: \u003ccode\u003ePromise.\u0026lt;Middleware\u0026gt;\u003c/code\u003e - A promise which resolves to a middleware which ensures a logged in user\n\n| Param | Type                  | Description                      |\n| ----- | --------------------- | -------------------------------- |\n| req   | \u003ccode\u003eObject\u003c/code\u003e   | Express request object           |\n| res   | \u003ccode\u003eObject\u003c/code\u003e   | Express response object          |\n| next  | \u003ccode\u003efunction\u003c/code\u003e | Express next middleware function |\n\n**Example**\n\n```js\noidc.login\n```\n\n\u003ca name=\"silentLogin\"\u003e\u003c/a\u003e\n\n## silentLogin(req, res, next) ⇒ \u003ccode\u003ePromise.\u0026lt;Middleware\u0026gt;\u003c/code\u003e\n\n**Kind**: global function  \n**Summary**: Check if the user is anonymous or authenticated, known as a \"silent login\"\nfor authentication  \n**Returns**: \u003ccode\u003ePromise.\u0026lt;Middleware\u0026gt;\u003c/code\u003e - A promise which resolves to a middleware which ensures a silent authenticated user\n\n| Param | Type                  | Description                      |\n| ----- | --------------------- | -------------------------------- |\n| req   | \u003ccode\u003eObject\u003c/code\u003e   | Express request object           |\n| res   | \u003ccode\u003eObject\u003c/code\u003e   | Express response object          |\n| next  | \u003ccode\u003efunction\u003c/code\u003e | Express next middleware function |\n\n**Example**\n\n```js\noidc.silentLogin\n```\n\n\u003ca name=\"logout\"\u003e\u003c/a\u003e\n\n## logout(req, res) ⇒ \u003ccode\u003ePromise.\u0026lt;Middleware\u0026gt;\u003c/code\u003e\n\n**Kind**: global function  \n**Summary**: Express Middleware that logs out the user from both the OpenID Connect server and this app. Note: The user is redirected to the config.defaultRedirect after a successful logout.  \n**Returns**: \u003ccode\u003ePromise.\u0026lt;Middleware\u0026gt;\u003c/code\u003e - A promise which resolves to a middleware which logs out the current user\n\n| Param | Type                | Description             |\n| ----- | ------------------- | ----------------------- |\n| req   | \u003ccode\u003eObject\u003c/code\u003e | Express request object  |\n| res   | \u003ccode\u003eObject\u003c/code\u003e | Express response object |\n\n**Example**\n\n```js\noidc.logout\n```\n\n\u003ca name=\"requireRole\"\u003e\u003c/a\u003e\n\n## requireRole(roles) ⇒ \u003ccode\u003eMiddleware\u003c/code\u003e\n\n**Kind**: global function  \n**Summary**: Express Middleware that checks if the req.user has this/these roles.  \n**Returns**: \u003ccode\u003eMiddleware\u003c/code\u003e - A Express middleware\n\nA role is a property found on the user object and has most\nlikely been added through the optional extendUser function parameter. @see {config.extendUser}  \n**Api**: public\n\n| Param | Type                              | Description                                                        |\n| ----- | --------------------------------- | ------------------------------------------------------------------ |\n| roles | \u003ccode\u003eArray.\u0026lt;string\u0026gt;\u003c/code\u003e | Array of roles to be compared with the ones on the req.user object |\n\n**Example**\n\n```js\noidc.requireRole('isAdmin', 'isEditor')\n```\n\n## Versions and Migrating\n\n### v4\n\n**Important**: SilentLogin now uses the toolbar cookie KTH_SSO_START for deciding if the user should be silently logged in.\n\nBasically: if the cookie exists we will try to silently login in the user.\n\nThis eliminates a strange behavior for our users.\nMost users considers the Social toolbar as the main place to log in into KTH. But if they used one app before they logged into the toolbar, a session was already created in that app which was anonymous.\n\nBy letting the app know, through the KTH_SSO_START cookie, that it should try again to log in the user, we get a better flow for the user.\n\n### v3\n\nChanges how the data in the session is stored during login.\n\n### v2\n\nChanges how the data in the session is stored during login. Also adds the possibility to configure a logger which can be used to debug.\n\n### v1\n\nThe original :-)\n\n## Troubleshooting\n\n### I get a 403 Unauthorized when trying to login\n\nIf you get this message after you logged into the ADFS server it might be that your applications local time differs from the one on the ADFS server.\n\nAfter you logged in, the client (browser) is trying to call the callback-route in your application.\n\nThe reason for this is that the JWT information contains a timestamp. If the timestamp differs to much the JWT will be refused and you get a 403.\n\nCheck your time settings. Are you synching with a time server? Try to change this to `ntp.kth.se`\n\n### Handling multiple simultaneous requests\n\nIf your app gets multiple simultaneous requests it will break. This is mainly because our session store does not work well with multiple app instances. It can overwrite session data if requests reach the two app instances at the same time.\n\nAnd since we store ongoing auth information in the session, it will break most of the time. The last request will most likely succeed.\n\nOne way to handle this is to ensure that a login has been made before all the requests are made.\n\n#### Example\n\nThis solution is currently used in directory-web and files-web.\n\nIn Directory-web, the app that makes multiple calls for avatar images, use this middleware on its public routes. It bounces the incoming page requests, if needed, against files-web.\n\n\u003e Note: Working locally, localhost:3000 and so on, can be problematic. The KTH_SSO_START cookie has a domain set which will not work with localhost. One way is to simply create a \"fake cookie\" with the same name.\n\n```javascript\nconst bounceOnFiles = (req, res, next) =\u003e {\n  const cookies = Object.keys(req.cookies)\n  const filesAuthHandling = `${cookies.includes('files-web.sid')}${cookies.includes('KTH_SSO_START')}`\n\n  // By adding the existence of the two cookies above we create a state. The state\n  // shows if a files-cookie exists and if a KTH_SSO_START cookie exists. If the\n  // state changes during calls we bounce against files-web again to get a correct state\n\n  if (req.session.filesAuthHandling === filesAuthHandling) {\n    return next()\n  }\n\n  req.session.filesAuthHandling = filesAuthHandling\n  const nextUrl = encodeURIComponent(req.protocol + '://' + req.get('host') + req.originalUrl)\n  // config.files.url = https://www-r.referens.sys.kth.se/files\n  return res.redirect(`${config.files.url}/auth/silent/bounce?nextUrl=${nextUrl}`)\n}\n```\n\n**serverSettings.js**\n\n```javascript\nfiles: {\n  url: getEnv('FILES_URL', devDefaults('http://localhost:3003/files')),\n}\n```\n\nIn Files-web, the app that serves avatar images has this routes which handles the bounce request. As you may notice there is a simple whitelisting of the accepted urls to be redirected.\n\n```javascript\nconst urlWhitelist = ['localhost', '.kth.se']\n\nserver.get(_addProxy('/auth/silent/bounce'), oidc.silentLogin, async (req, res, next) =\u003e {\n  const nextUrl = req.query.nextUrl\n\n  if (!nextUrl) {\n    return res.status(400).send('Missing nextUrl param')\n  }\n\n  if (!urlWhitelist.find(whitelisted =\u003e nextUrl.includes(whitelisted))) {\n    return res.status(400).send('Not an accepted url')\n  }\n\n  return res.redirect(nextUrl)\n})\n```\n\n## Development\n\n1. Clone the repo\n   ```bash\n   $ npm clone git@github.com:KTH/kth-node-passport-oidc.git\n   ```\n2. Install dependencies\n   ```bash\n   $ npm install\n   ```\n\n### Generate API documentation\n\nThis project includes a simple generation of Markdown documentation from the JS-doc in our code.\n\nTo run this:\n\n```bash\n$ npm run buildApiDocs\n```\n\nNow you have a `api.md` file in the root of the project. Use this to update the main `README.md`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkth%2Fkth-node-passport-oidc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkth%2Fkth-node-passport-oidc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkth%2Fkth-node-passport-oidc/lists"}