Skip to content

Commit d9a9c51

Browse files
Add OIDC middleware (#599)
* Add OIDC middleware * use refresh token * address comments * fix lint issues
1 parent 0ec89b1 commit d9a9c51

File tree

3 files changed

+235
-2
lines changed

3 files changed

+235
-2
lines changed

package-lock.json

Lines changed: 106 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@
6161
"koa-bodyparser": "^4.2.0",
6262
"koa-compress": "^2.0.0",
6363
"koa-onerror": "^3.1.0",
64+
"koa-passport": "^6.0.0",
6465
"koa-router": "^7.2.1",
6566
"koa-send": "^4.1.1",
67+
"koa-session": "^6.4.0",
6668
"koa-static": "^4.0.1",
6769
"koa-webpack": "^2.0.3",
6870
"lodash-es": "^4.17.4",
@@ -77,6 +79,7 @@
7779
"lossless-json": "^1.0.2",
7880
"moment": "^2.29.4",
7981
"napa": "^3.0.0",
82+
"openid-client": "^5.6.5",
8083
"prismjs": "^1.14.0",
8184
"promise.prototype.finally": "^3.1.0",
8285
"stylus": "^0.54.5",

server/middleware/oidc/index.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright (c) 2024 Uber Technologies Inc.
2+
//
3+
//
4+
// Permission is hereby granted, free of charge, to any person obtaining a copy
5+
// of this software and associated documentation files (the "Software"), to deal
6+
// in the Software without restriction, including without limitation the rights
7+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
// copies of the Software, and to permit persons to whom the Software is
9+
// furnished to do so, subject to the following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included in
12+
// all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
// THE SOFTWARE.
21+
22+
const OpenIDClient = require('openid-client');
23+
const passport = require('koa-passport');
24+
const session = require('koa-session');
25+
const allowUrl = ['/login', '/logout', '/oauth2/redirect'];
26+
27+
let settings = {};
28+
let oiClient;
29+
30+
try {
31+
settings = require('../../config/oidc.js');
32+
} catch (e) {
33+
console.log('OIDC configuration file not found, using ENV variables');
34+
}
35+
36+
const clientID = settings.clientID || process.env.OPENID_CLIENT_ID;
37+
const clientSecret = settings.clientSecret || process.env.OPENID_CLIENT_SECRET;
38+
const callbackURL = settings.callbackURL || process.env.OPENID_CALLBACK_URL;
39+
const discoverURL = settings.discoverURL || process.env.OPENID_DISCOVER_URL;
40+
const scope = settings.scope || process.env.OPENID_SCOPE || 'openid';
41+
42+
function verifyCallback(tokenSet, user, done) {
43+
let email = '';
44+
45+
if (tokenSet.claims().email) {
46+
email = tokenSet.claims().email;
47+
}
48+
49+
return done(null, {
50+
accessToken: tokenSet.access_token,
51+
refreshToken: tokenSet.refresh_token,
52+
exp: tokenSet.expires_at,
53+
email: email,
54+
});
55+
}
56+
57+
const middleware = async function(ctx, next) {
58+
if (allowUrl.includes(ctx.path)) {
59+
return next();
60+
}
61+
62+
if (ctx.isUnauthenticated()) {
63+
return ctx.redirect('/login');
64+
}
65+
66+
// refresh token when access token is expired
67+
if (
68+
ctx.state.user.exp !== undefined &&
69+
ctx.state.user.exp < Date.now() / 1000
70+
) {
71+
const ts = await oiClient.refresh(ctx.state.user.refreshToken);
72+
73+
ctx.state.user.exp = ts.expires_at;
74+
ctx.state.user.accessToken = ts.access_token;
75+
}
76+
77+
ctx.authTokenHeaders = ctx.authTokenHeaders || {};
78+
ctx.authTokenHeaders['cadence-authorization'] = ctx.state.user.accessToken;
79+
80+
await next();
81+
};
82+
83+
const setupAuth = async function(app, router) {
84+
app.keys = ['secret'];
85+
app.use(session(app));
86+
app.use(passport.initialize());
87+
app.use(passport.session());
88+
89+
router.get('/login', passport.authenticate('oidc'));
90+
router.get('/logout', async ctx => {
91+
ctx.logout();
92+
ctx.redirect('/');
93+
});
94+
router.get(
95+
'/oauth2/redirect',
96+
passport.authenticate('oidc', {
97+
successReturnToOrRedirect: '/',
98+
failureRedirect: '/login',
99+
})
100+
);
101+
passport.serializeUser((user, done) => done(null, user));
102+
passport.deserializeUser((user, done) => done(null, user));
103+
104+
const discovered = await OpenIDClient.Issuer.discover(discoverURL);
105+
106+
oiClient = new discovered.Client({
107+
client_id: clientID,
108+
client_secret: clientSecret,
109+
});
110+
111+
const strategyOptions = {
112+
client: oiClient,
113+
params: {
114+
redirect_uri: callbackURL,
115+
scope: scope,
116+
},
117+
passReqToCallback: false,
118+
};
119+
120+
passport.use('oidc', OpenIDClient.Strategy(strategyOptions, verifyCallback));
121+
};
122+
123+
module.exports = {
124+
setupAuth: setupAuth,
125+
middleware: middleware,
126+
};

0 commit comments

Comments
 (0)