Skip to content
Snippets Groups Projects
Commit 89fedbd2 authored by Alexandros Georgantas's avatar Alexandros Georgantas Committed by Yannis Barlas
Browse files

fix(server): handle access token becoming invalid before expiration

parent 9b91eacc
No related branches found
No related tags found
1 merge request!84fix(server): handle invalid access token case even before expiration
const { boss } = require('pubsweet-server/src/jobs')
const logger = require('@pubsweet/logger')
const moment = require('moment')
const { pubsubManager } = require('pubsweet-server')
const { jobs } = require('./services')
......@@ -9,6 +10,7 @@ const {
} = require('./models/user/constants')
const { getUser } = require('./models/user/user.controller')
const Identity = require('./models/identity/identity.model')
/**
* Add a list of jobs to the job queue. If no jobs are specified, subscribe all
......@@ -97,15 +99,36 @@ const defaultJobs = [
callback: async job => {
try {
const pubsub = await pubsubManager.getPubsub()
const { userId } = job.data
const { userId, providerLabel } = job.data
const updatedUser = await getUser(userId)
pubsub.publish(USER_UPDATED, {
userUpdated: updatedUser,
const providerUserIdentity = await Identity.findOne({
userId,
provider: providerLabel,
})
if (!providerUserIdentity) {
throw new Error(
`identity for user with id ${userId} does not exist for provider ${providerLabel}`,
)
}
const { oauthRefreshTokenExpiration } = providerUserIdentity
const UTCNowTimestamp = moment().utc().toDate().getTime()
const refreshTokenExpired =
oauthRefreshTokenExpiration.getTime() < UTCNowTimestamp
if (refreshTokenExpired) {
pubsub.publish(USER_UPDATED, {
userUpdated: updatedUser,
})
}
job.done()
} catch (e) {
job.done(e)
logger.error(`Job ${jobs.REFRESH_TOKEN_EXPIRED}: defer error:`, e)
throw e
}
......
......@@ -4,7 +4,12 @@ const { URLSearchParams: UnpackedParams } = require('url')
const { createUser } = require('./helpers/users')
const { createOAuthIdentity } = require('../identity/identity.controller')
const {
createOAuthIdentity,
invalidateProviderAccessToken,
invalidateProviderTokens,
} = require('../identity/identity.controller')
const { User, Identity } = require('../index')
const clearDb = require('./_clearDb')
......@@ -104,6 +109,8 @@ const timeLeft = dateTime => {
}
jest.mock('axios')
const specificDate = new Date()
Date.now = jest.fn(() => specificDate)
describe('Identity Controller', () => {
beforeEach(() => clearDb())
......@@ -113,11 +120,11 @@ describe('Identity Controller', () => {
knex.destroy()
})
it('authorises access and inserts the Oauth tokens', async () => {
it('authorizes access and inserts the Oauth tokens', async () => {
axios.mockImplementationOnce(fakePostResponse)
const user = await createUser()
// Mock authorisation
// Mock authorization
await createOAuthIdentity(
user.id,
'test',
......@@ -157,4 +164,50 @@ describe('Identity Controller', () => {
}) // 360000 - 86400
expect(data).toEqual({ providerLabel: 'test', userId: user.id })
})
it('invalidates access token', async () => {
axios.mockImplementationOnce(fakePostResponse)
const user = await createUser()
// Mock authorization
await createOAuthIdentity(
user.id,
'test',
'fake-session-state',
'fake-code',
)
await invalidateProviderAccessToken(user.id, 'test')
const { oauthAccessTokenExpiration } = await Identity.findOne({
userId: user.id,
provider: 'test',
})
expect(oauthAccessTokenExpiration).toEqual(specificDate)
})
it('invalidates provider tokens', async () => {
axios.mockImplementationOnce(fakePostResponse)
const user = await createUser()
// Mock authorization
await createOAuthIdentity(
user.id,
'test',
'fake-session-state',
'fake-code',
)
await invalidateProviderTokens(user.id, 'test')
const { oauthAccessTokenExpiration, oauthRefreshTokenExpiration } =
await Identity.findOne({
userId: user.id,
provider: 'test',
})
expect(oauthAccessTokenExpiration).toEqual(specificDate)
expect(oauthRefreshTokenExpiration).toEqual(specificDate)
})
})
const logger = require('@pubsweet/logger')
const { pubsubManager } = require('pubsweet-server')
const axios = require('axios')
const config = require('config')
......@@ -6,6 +7,11 @@ const moment = require('moment')
const { getExpirationTime } = require('../../utils/tokens')
const { jobs } = require('../../services')
const {
subscriptions: { USER_UPDATED },
} = require('../user/constants')
const { getUser } = require('../user/user.controller')
const Identity = require('./identity.model')
const {
......@@ -139,8 +145,20 @@ const authorizeOAuth = async (provider, sessionState, code) => {
/* eslint-disable camelcase */
const { access_token, expires_in, refresh_token, refresh_expires_in } = data
if (!access_token || !expires_in || !refresh_token || !refresh_expires_in) {
throw new Error('Missing data from response!')
if (!access_token) {
throw new Error('Missing access_token from response!')
}
if (!expires_in) {
throw new Error('Missing expires_in from response!')
}
if (!refresh_token) {
throw new Error('Missing refresh_token from response!')
}
if (!refresh_expires_in) {
throw new Error('Missing refresh_expires_in from response!')
}
return {
......@@ -152,9 +170,50 @@ const authorizeOAuth = async (provider, sessionState, code) => {
/* eslint-enable camelcase */
}
const invalidateProviderAccessToken = async (userId, providerLabel) => {
const providerUserIdentity = await Identity.findOne({
userId,
provider: providerLabel,
})
await Identity.patchAndFetchById(providerUserIdentity.id, {
oauthAccessTokenExpiration: moment().utc().toDate(),
})
logger.info(
`access token for provider ${providerLabel} became invalid, trying to get a new one via the refresh token`,
)
}
const invalidateProviderTokens = async (userId, providerLabel) => {
const pubsub = await pubsubManager.getPubsub()
const updatedUser = await getUser(userId)
const providerUserIdentity = await Identity.findOne({
userId,
provider: providerLabel,
})
await Identity.patchAndFetchById(providerUserIdentity.id, {
oauthAccessTokenExpiration: moment().utc().toDate(),
oauthRefreshTokenExpiration: moment().utc().toDate(),
})
pubsub.publish(USER_UPDATED, {
userUpdated: updatedUser,
})
logger.error(
`refresh token for provider ${providerLabel} became invalid, authorization flow (provider login) should be followed by the user`,
)
}
module.exports = {
createOAuthIdentity,
getUserIdentities,
getDefaultIdentity,
hasValidRefreshToken,
invalidateProviderAccessToken,
invalidateProviderTokens,
}
......@@ -7,6 +7,14 @@ jest.mock('../tokens', () => {
}
})
jest.mock('../../models/identity/identity.controller.js')
const {
invalidateProviderAccessToken,
} = require('../../models/identity/identity.controller')
const { getAuthTokens } = require('../tokens')
jest.mock('axios')
describe('Authenticated call', () => {
......@@ -15,4 +23,13 @@ describe('Authenticated call', () => {
const res = await authenticatedCall('123', 'lulu', {})
expect(res).toBe(true)
})
it('fetches a new token when expired', async () => {
axios.mockResolvedValueOnce({ status: 401 }).mockResolvedValue(true)
const res = await authenticatedCall('123', 'lulu', {})
expect(invalidateProviderAccessToken).toHaveBeenCalled()
expect(getAuthTokens).toHaveBeenCalled()
expect(res).toBe(true)
})
})
const {
invalidateProviderAccessToken,
} = require('../models/identity/identity.controller')
const makeCall = require('./makeCall')
const { getAuthTokens } = require('./tokens')
......@@ -7,7 +11,18 @@ const authenticatedCall = async (userId, providerLabel, callParameters) => {
const accessToken = await getAuthTokens(userId, providerLabel)
return makeCall(callParameters, accessToken)
const response = await makeCall(callParameters, accessToken)
if (response.status === 401) {
// for the case that something happened and accessToken become invalid -> set that expired
await invalidateProviderAccessToken(userId, providerLabel)
const freshAccessToken = await getAuthTokens(userId, providerLabel)
return makeCall(callParameters, freshAccessToken)
}
return response
} catch (e) {
throw new Error(e)
}
......
......@@ -9,10 +9,15 @@ const {
subscriptions: { USER_UPDATED },
} = require('../models/user/constants')
const { Identity, ServiceCredential } = require('../models')
const Identity = require('../models/identity/identity.model')
const ServiceCredential = require('../models/serviceCredential/serviceCredential.model')
const { getUser } = require('../models/user/user.controller')
const {
invalidateProviderTokens,
} = require('../models/identity/identity.controller')
const getAuthTokens = async (userId, providerLabel) => {
return requestTokensFromProvider(userId, providerLabel, {
checkAccessToken: true,
......@@ -114,27 +119,26 @@ const requestTokensFromProvider = async (
if (status === 401) {
// for the case that something happened and refreshToken become invalid -> set that expired
const updatedUser = await getUser(userId)
await Identity.patchAndFetchById(providerUserIdentity.id, {
oauthRefreshTokenExpiration: moment().utc().toDate(),
})
pubsub.publish(USER_UPDATED, {
userUpdated: updatedUser,
})
// logger.error(
// `refresh token for provider ${providerLabel} expired, authorization flow should (provider login) be followed by the user`,
// )
// return false
throw new Error(
`refresh token for provider ${providerLabel} expired, authorization flow should (provider login) be followed by the user`,
)
await invalidateProviderTokens(userId, providerLabel)
}
/* eslint-disable camelcase */
const { access_token, expires_in, refresh_token, refresh_expires_in } = data
if (!access_token || !expires_in || !refresh_token || !refresh_expires_in) {
throw new Error('Missing data from response!')
if (!access_token) {
throw new Error('Missing access_token from response!')
}
if (!expires_in) {
throw new Error('Missing expires_in from response!')
}
if (!refresh_token) {
throw new Error('Missing refresh_token from response!')
}
if (!refresh_expires_in) {
throw new Error('Missing refresh_expires_in from response!')
}
await Identity.patchAndFetchById(providerUserIdentity.id, {
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment