Skip to content

Commit

Permalink
token refresh oauth clients (#492)
Browse files Browse the repository at this point in the history
init global api-client with auto-refresh token, loggedInUserService and some minor fixes
  • Loading branch information
fcaps authored Nov 29, 2023
1 parent beaa488 commit 3f1ea05
Show file tree
Hide file tree
Showing 29 changed files with 501 additions and 155 deletions.
4 changes: 3 additions & 1 deletion 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 All @@ -20,7 +21,8 @@ const appConfig = {
apiUrl: process.env.API_URL || 'https://api.faforever.com',
wordpressUrl: process.env.WP_URL || 'https://direct.faforever.com',
extractorInterval: process.env.EXTRACTOR_INTERVAL || 5,
playerCountInterval: process.env.PLAYER_COUNT_INTERVAL || 15
playerCountInterval: process.env.PLAYER_COUNT_INTERVAL || 15,
recaptchaKey: process.env.RECAPTCHA_SITE_KEY || 'test'
}

module.exports = appConfig
54 changes: 49 additions & 5 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,10 @@ 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 refresh = require('passport-oauth2-refresh')
const {JavaApiClientFactory} = require('./lib/JavaApiClientFactory')
const UserRepository = require('./lib/UserRepository')

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

const configureAuth = (app) => {
app.use(passport.initialize())
app.use(passport.session())

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']
}, async function (iss, sub, profile, jwtClaims, token, refreshToken, params, verified) {
const oAuthPassport = {
token,
refreshToken
}

const apiClient = JavaApiClientFactory(appConfig.apiUrl, oAuthPassport)
const userRepository = new UserRepository(apiClient)

userRepository.fetchUser(oAuthPassport).then(user => {
verified(null, user)
}).catch(e => {
console.error('[Error] oAuth verify failed with "' + e.toString() + '"')
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 +106,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 All @@ -91,10 +132,13 @@ module.exports.setup = (app) => {
secret: appConfig.session.key
})
}))
app.use(passport.initialize())
app.use(passport.session())

configureAuth(app)

app.use(middleware.injectServices)

app.use(flash())
app.use(middleware.username)
app.use(middleware.populatePugGlobals)
app.use(middleware.webpackAsset)
app.use(copyFlashHandler)
}
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 {}
69 changes: 69 additions & 0 deletions lib/JavaApiClientFactory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const { Axios } = require('axios')
const refresh = require('passport-oauth2-refresh')
const { AuthFailed } = require('./ApiErrors')
const appConfig = require('../config/app')

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

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

module.exports.JavaApiClientFactory = (javaApiBaseURL, oAuthPassport) => {
if (typeof oAuthPassport !== "object") {
throw new Error("oAuthPassport not an object");
}

if (typeof oAuthPassport.refreshToken !== "string") {
throw new Error("oAuthPassport.refreshToken not a string")
}

if (typeof oAuthPassport.token !== "string") {
throw new Error("oAuthPassport.token not a string")
}

let tokenRefreshRunning = null
const client = new Axios({
baseURL: javaApiBaseURL
})

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

return config
})

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

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

return tokenRefreshRunning.then(([token, refreshToken]) => {
oAuthPassport.token = token
oAuthPassport.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.

15 changes: 15 additions & 0 deletions lib/LoggedInUserService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class LoggedInUserService {
constructor (userRepository, request) {
this.request = request

if (typeof this.request.user !== "object") {
throw new Error("request.user not an object");
}
}

getUser() {
return this.request.user
}
}

module.exports = LoggedInUserService
32 changes: 32 additions & 0 deletions lib/UserRepository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class UserRepository {
constructor (javaApiClient) {
this.javaApiClient = javaApiClient
}

fetchUser (oAuthPassport) {
return this.javaApiClient.get('/me').then(response => {
const rawUser = JSON.parse(response.data).data

let clan = null

if (rawUser.attributes.clan) {
clan = {
id: rawUser.attributes.clan.id,
membershipId: rawUser.attributes.clan.membershipId,
tag: rawUser.attributes.clan.tag,
name: rawUser.attributes.clan.name
}
}

return {
id: rawUser.id,
name: rawUser.attributes.userName,
email: rawUser.attributes.email,
clan,
oAuthPassport
}
})
}
}

module.exports = UserRepository
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 Down Expand Up @@ -44,6 +45,7 @@
"jquery": "^3.7.1",
"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
43 changes: 29 additions & 14 deletions routes/middleware.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
const WordpressServiceFactory = require("../lib/WordpressServiceFactory");
const {JavaApiClientFactory} = require('../lib/JavaApiClientFactory')
const LeaderboardService = require('../lib/LeaderboardService')
const LeaderboardRepository = require('../lib/LeaderboardRepository')
const cacheService = require('../lib/CacheService')
const appConfig = require("../config/app");
const wordpressService = WordpressServiceFactory(appConfig.wordpressUrl)
const fs = require('fs');
const webpackManifestJS = JSON.parse(fs.readFileSync('dist/js/manifest.json', 'utf8'));
const LoggedInUserService = require('../lib/LoggedInUserService')
const UserRepository = require('../lib/UserRepository');


exports.initLocals = function(req, res, next) {
let locals = res.locals;
Expand All @@ -23,17 +30,16 @@ exports.webpackAsset = (req, res, next) => {
next()
}

exports.username = function(req, res, next) {
var locals = res.locals;

if (req.isAuthenticated()) {
locals.username = req.user.data.attributes.userName;
locals.hasClan =
req.user && req.user.data.attributes.clan;
}

next();
};
exports.populatePugGlobals = function (req, res, next) {
res.locals.appGlobals = {
loggedInUser: null
}

if (req.isAuthenticated()) {
res.locals.appGlobals.loggedInUser = req.services.userService.getUser()
}
next()
}

exports.isAuthenticated = (redirectUrlAfterLogin = null, isApiRequest = false) => {
return (req, res, next) => {
Expand All @@ -53,9 +59,18 @@ exports.isAuthenticated = (redirectUrlAfterLogin = null, isApiRequest = false) =
}
}

exports.injectServices = function(req, res, next) {
req.services = {
wordpressService: wordpressService
exports.injectServices = (req, res, next) => {
req.services = {}
req.services.wordpressService = wordpressService

if (req.isAuthenticated()) {
try {
req.services.javaApiClient = JavaApiClientFactory(appConfig.apiUrl, req.user.oAuthPassport)
req.services.userService = new LoggedInUserService(new UserRepository(req.services.javaApiClient), req)
req.services.leaderboardService = new LeaderboardService(cacheService, new LeaderboardRepository(req.services.javaApiClient))
} catch (e) {
req.logout(() => next(e))
}
}

next()
Expand Down
2 changes: 1 addition & 1 deletion routes/views/account/get/connectSteam.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ exports = module.exports = function (req, res) {

request.post({
'url': process.env.API_URL + '/users/buildSteamLinkUrl',
'headers': {'Authorization': 'Bearer ' + req.user.data.attributes.token},
'headers': {'Authorization': 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token},
form: {callbackUrl: req.protocol + '://' + req.get('host') + '/account/link?done'}
}, function (err, res, body) {
//Must not be valid, check to see if errors, otherwise return generic error.
Expand Down
2 changes: 1 addition & 1 deletion routes/views/account/get/linkGog.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ exports = module.exports = function (req, res) {

request.get({
url: process.env.API_URL + '/users/buildGogProfileToken',
headers: {'Authorization': 'Bearer ' + req.user.data.attributes.token},
headers: {'Authorization': 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token},
form: {}
}, function (err, res, body) {
locals.gogToken = 'unable to obtain token';
Expand Down
2 changes: 1 addition & 1 deletion routes/views/account/get/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ exports = module.exports = function (req, res) {
{
url: process.env.API_URL + '/data/moderationReport?include=reportedUsers,lastModerator&sort=-createTime',
headers: {
'Authorization': 'Bearer ' + req.user.data.attributes.token
'Authorization': 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token
}
},
function (err, childRes, body) {
Expand Down
Loading

0 comments on commit 3f1ea05

Please sign in to comment.