Skip to content

Commit

Permalink
refactoring auth with auto refresh token and api-client
Browse files Browse the repository at this point in the history
  • Loading branch information
fcaps committed Nov 27, 2023
1 parent 9d5de76 commit 8f0694c
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 70 deletions.
1 change: 1 addition & 0 deletions config/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const appConfig = {
tokenLifespan: process.env.TOKEN_LIFESPAN || 43200
},
oauth: {
strategy: 'faforever',
clientId: process.env.OAUTH_CLIENT_ID || '12345',
clientSecret: process.env.OAUTH_CLIENT_SECRET || '12345',
url: oauthUrl,
Expand Down
48 changes: 46 additions & 2 deletions fafApp.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const appConfig = require("./config/app")
const appConfig = require('./config/app')
const express = require('express')
const bodyParser = require('body-parser')
const session = require('express-session')
Expand All @@ -15,6 +15,9 @@ const clanRouter = require("./routes/views/clanRouter")
const accountRouter = require("./routes/views/accountRouter")
const dataRouter = require('./routes/views/dataRouter');
const setupCronJobs = require("./scripts/cron-jobs")
const OidcStrategy = require('passport-openidconnect')
const axios = require('axios')
const refresh = require('passport-oauth2-refresh')

const copyFlashHandler = (req, res, next) => {
res.locals.message = req.flash();
Expand All @@ -33,6 +36,44 @@ const errorHandler = (err, req, res, next) => {
res.status(500).render('errors/500');
}

const configureAuth = () => {
passport.serializeUser((user, done) => done(null, user))
passport.deserializeUser((user, done) => done(null, user))

const authStrategy = new OidcStrategy({
issuer: appConfig.oauth.url + '/',
tokenURL: appConfig.oauth.url + '/oauth2/token',
authorizationURL: appConfig.oauth.publicUrl + '/oauth2/auth',
userInfoURL: appConfig.oauth.url + '/userinfo?schema=openid',
clientID: appConfig.oauth.clientId,
clientSecret: appConfig.oauth.clientSecret,
callbackURL: `${appConfig.host}/${appConfig.oauth.callback}`,
scope: ['openid', 'offline', 'public_profile', 'write_account_data']
}, function (iss, sub, profile, jwtClaims, accessToken, refreshToken, params, verified) {
axios.get(
appConfig.apiUrl + '/me',
{
headers: { Authorization: `Bearer ${accessToken}` }
}).then((res) => {
const user = res.data
user.token = accessToken
user.refreshToken = refreshToken
user.data.attributes.token = accessToken
user.data.id = user.data.attributes.userId

return verified(null, user)
}).catch(e => {
console.error('[Error] views/auth.js::passport::verify failed with "' + e.toString() + '"')

return verified(null, null)
})
}
)

passport.use(appConfig.oauth.strategy, authStrategy)
refresh.use(appConfig.oauth.strategy, authStrategy)
}

module.exports.setupCronJobs = () => {
setupCronJobs()
}
Expand Down Expand Up @@ -64,7 +105,6 @@ module.exports.setup = (app) => {
app.set('view engine', 'pug')
app.set('port', appConfig.expressPort)

app.use(middleware.injectServices)
app.use(middleware.initLocals)

app.use(express.static('public', {
Expand Down Expand Up @@ -93,6 +133,10 @@ module.exports.setup = (app) => {
}))
app.use(passport.initialize())
app.use(passport.session())
configureAuth()

app.use(middleware.injectServices)

app.use(flash())
app.use(middleware.username)
app.use(middleware.webpackAsset)
Expand Down
1 change: 1 addition & 0 deletions lib/ApiErrors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports.AuthFailed = class AuthFailed extends Error {}
57 changes: 57 additions & 0 deletions lib/JavaApiClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const { Axios } = require('axios')
const refresh = require('passport-oauth2-refresh')
const { AuthFailed } = require('./ApiErrors')
const appConfig = require('../config/app')

const getRefreshToken = (user) => {
return new Promise((resolve, reject) => {
refresh.requestNewAccessToken(appConfig.oauth.strategy, user.refreshToken, function (err, accessToken, refreshToken) {
if (err || !accessToken || !refreshToken) {
return reject(new AuthFailed('Failed to refresh token'))
}

return resolve([accessToken, refreshToken])
})
})
}

module.exports = (javaApiBaseURL, user) => {
let tokenRefreshRunning = null
const client = new Axios({
baseURL: javaApiBaseURL
})

client.interceptors.request.use(
async config => {
config.headers = {
Authorization: `Bearer ${user.token}`
}

return config
})

client.interceptors.response.use(async (res) => {
if (!res.config._refreshTokenRequest && res.config && res.status === 401) {
res.config._refreshTokenRequest = true

if (!tokenRefreshRunning) {
tokenRefreshRunning = getRefreshToken(user)
}

const [token, refreshToken] = await tokenRefreshRunning

user.token = token
user.refreshToken = refreshToken

return client.request(res.config)
}

if (res.status === 401) {
throw new AuthFailed('Token no longer valid and refresh did not help')
}

return res
})

return client
}
19 changes: 12 additions & 7 deletions lib/LeaderboardRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,18 @@ class LeaderboardRepository {
let leaderboardData = []

data.data.forEach((item, index) => {
leaderboardData.push({
rating: item.attributes.rating,
totalgames: item.attributes.totalGames,
wonGames: item.attributes.wonGames,
date: item.attributes.updateTime,
label: data.included[index].attributes.login,
})
try {
leaderboardData.push({
rating: item.attributes.rating,
totalgames: item.attributes.totalGames,
wonGames: item.attributes.wonGames,
date: item.attributes.updateTime,
label: data.included[index]?.attributes.login || 'unknown user',
})
} catch (e) {
console.error('LeaderboardRepository::mapResponse failed on item with "' + e.toString() + '"')
}

})

return leaderboardData
Expand Down
14 changes: 0 additions & 14 deletions lib/LeaderboardServiceFactory.js

This file was deleted.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"node-fetch": "^2.6.7",
"npm-check": "^6.0.1",
"passport": "^0.6.0",
"passport-oauth2-refresh": "^2.2.0",
"passport-openidconnect": "^0.1.1",
"pug": "3.0.2",
"request": "2.88.2",
Expand All @@ -42,6 +43,7 @@
"jest": "^29.7.0",
"load-grunt-config": "4.0.1",
"load-grunt-tasks": "5.1.0",
"nock": "^13.3.8",
"octokit": "^3.1.2",
"supertest": "^6.3.3",
"webpack": "^5.89.0",
Expand Down
8 changes: 8 additions & 0 deletions routes/middleware.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const WordpressServiceFactory = require("../lib/WordpressServiceFactory");
const JavaApiClientFactory = require('../lib/JavaApiClient')
const LeaderboardService = require('../lib/LeaderboardService')
const cacheService = require('../lib/CacheService')
const appConfig = require("../config/app");
const wordpressService = WordpressServiceFactory(appConfig.wordpressUrl)
const fs = require('fs');
Expand Down Expand Up @@ -58,5 +61,10 @@ exports.injectServices = function(req, res, next) {
wordpressService: wordpressService
}

if (req.isAuthenticated()) {
req.services.javaApiClient = JavaApiClientFactory(appConfig.apiUrl, req.user)
req.services.leaderboardService = new LeaderboardService(cacheService, new LeaderboardRepository(req.services.javaApiClient))
}

next()
}
45 changes: 5 additions & 40 deletions routes/views/auth.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,9 @@
const appConfig = require('../../config/app')
const passport = require('passport');
const OidcStrategy = require('passport-openidconnect');
const express = require("express");
const axios = require("axios");
const router = express.Router();
const passport = require('passport')
const express = require("express")
const router = express.Router()

passport.serializeUser((user, done) => done(null, user))
passport.deserializeUser((user, done) => done(null, user))

passport.use('faforever', new OidcStrategy({
issuer: appConfig.oauth.url + '/',
tokenURL: appConfig.oauth.url + '/oauth2/token',
authorizationURL: appConfig.oauth.publicUrl + '/oauth2/auth',
userInfoURL: appConfig.oauth.url + '/userinfo?schema=openid',
clientID: appConfig.oauth.clientId,
clientSecret: appConfig.oauth.clientSecret,
callbackURL: `${appConfig.host}/${appConfig.oauth.callback}`,
scope: ['openid', 'public_profile', 'write_account_data']
}, function (iss, sub, profile, jwtClaims, accessToken, refreshToken, params, verified) {

axios.get(
appConfig.apiUrl + '/me',
{
headers: {'Authorization': `Bearer ${accessToken}`}
}).then((res) => {
const user = res.data
user.token = accessToken
user.data.attributes.token = accessToken;
user.data.id = user.data.attributes.userId;

return verified(null, user);
}).catch(e => {
console.error('[Error] views/auth.js::passport::verify failed with "' + e.toString() + '"');

return verified(null, null);
});
}
));

router.get('/login', passport.authenticate('faforever'));
router.get('/login', passport.authenticate(appConfig.oauth.strategy));

router.get(
'/' + appConfig.oauth.callback,
Expand All @@ -47,7 +12,7 @@ router.get(

return next()
},
passport.authenticate('faforever', {failureRedirect: '/login', failureFlash: true}),
passport.authenticate(appConfig.oauth.strategy, {failureRedirect: '/login', failureFlash: true}),
(req, res) => {
res.redirect(res.locals.returnTo || '/')
}
Expand Down
18 changes: 12 additions & 6 deletions routes/views/leaderboardRouter.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
const appConfig = require('../../config/app')
const express = require('express');
const router = express.Router();
const LeaderboardServiceFactory = require('../../lib/LeaderboardServiceFactory')
const {AcquireTimeoutError} = require('../../lib/MutexService');
const middlewares = require('../middleware')
const {AuthFailed} = require('../../lib/ApiErrors')


const getLeaderboardId = (leaderboardName) => {
Expand Down Expand Up @@ -33,14 +32,21 @@ router.get('/:leaderboard.json', middlewares.isAuthenticated(null, true), async
return res.status(404).json({error: 'Leaderboard "' + req.params.leaderboard + '" does not exist'})
}

const token = req.user.data.attributes.token
const leaderboardService = LeaderboardServiceFactory(appConfig.apiUrl, token)

return res.json(await leaderboardService.getLeaderboard(leaderboardId))
return res.json(await req.services.leaderboardService.getLeaderboard(leaderboardId))
} catch (e) {
if (e instanceof AcquireTimeoutError) {
return res.status(503).json({error: 'timeout reached'})
}

if (e instanceof AuthFailed) {
req.logout(function(err) {
if (err) {
throw err
}
})

return res.status(400).json({error: 'authentication failed, reload site'})
}

console.error('[error] leaderboardRouter::get:leaderboard.json failed with "' + e.toString() + '"')

Expand Down
5 changes: 5 additions & 0 deletions src/frontend/js/entrypoint/leaderboards.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ let currentDate = new Date(minusTimeFilter).toISOString();
async function leaderboardOneJSON(leaderboardFile) {
//Check which category is active
const response = await fetch(`leaderboards/${leaderboardFile}.json`);

if (response.status === 400) {
window.location.href = '/leaderboards'
}

currentLeaderboard = leaderboardFile;
const data = await response.json();
return await data;
Expand Down

0 comments on commit 8f0694c

Please sign in to comment.