Skip to content

Commit

Permalink
Merge pull request #11 from onaio/refresh-token
Browse files Browse the repository at this point in the history
Refresh access token
  • Loading branch information
ukanga authored Jan 13, 2021
2 parents 3ff7989 + 3b19eb8 commit ae7f938
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 77 deletions.
2 changes: 2 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
process.env.TZ = 'EAT';

module.exports = {
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'],
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
]
},
"dependencies": {
"@onaio/gatekeeper": "^0.0.21",
"@onaio/gatekeeper": "^0.1.1",
"compression": "^1.7.4",
"cookie-parser": "^1.4.4",
"dotenv": "^8.2.0",
Expand Down Expand Up @@ -66,6 +66,7 @@
"eslint-config-prettier": "^6.10.1",
"eslint-plugin-prettier": "^3.1.2",
"jest": "^25.1.0",
"mockdate": "^3.0.2",
"nock": "^12.0.1",
"nodemon": "^2.0.2",
"npm-run-all": "^4.1.5",
Expand Down
98 changes: 69 additions & 29 deletions src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {
EXPRESS_SESSION_SECRET,
} from '../configs/envs';

type dictionary = { [key: string]: any };

const opensrpAuth = new ClientOAuth2({
accessTokenUri: EXPRESS_OPENSRP_ACCESS_TOKEN_URL,
authorizationUri: EXPRESS_OPENSRP_AUTHORIZATION_URL,
Expand Down Expand Up @@ -111,6 +113,71 @@ const oauthLogin = (_: express.Request, res: express.Response) => {
res.redirect(uri);
};

const processUserInfo = (
req: express.Request,
res: express.Response,
authDetails: dictionary,
userDetails?: dictionary,
isRefresh?: boolean
) => {
let userInfo = userDetails
if(!userDetails) {
// get user details from session. will be needed when refreshing token
userInfo = req.session.preloadedState?.session?.extraData || {};
}
userInfo.oAuth2Data = authDetails;
const sessionState = getOpenSRPUserInfo(userInfo);
if (sessionState) {
const gatekeeperState = {
success: true,
result: sessionState.extraData,
};
const preloadedState = {
gatekeeper: gatekeeperState,
session: sessionState,
};
req.session.preloadedState = preloadedState;
const expireAfterMs = sessionState.extraData.oAuth2Data.refresh_expires_in * 1000;
req.session.cookie.maxAge = expireAfterMs;
// you have to save the session manually for POST requests like this one
req.session.save(() => void 0);
// when refreshing token we only need the preloaded state
if(isRefresh) {
return preloadedState
}
if (nextPath) {
/** reset nextPath to undefined; its value once set should only be used
* once and invalidated after being used, which is here. Failing to invalidate the previous value
* would result in the user being redirected to the same url the next time they pass through
* here irrespective of whether they should or shouldn't
*/
const localNextPath = nextPath;
nextPath = undefined;
return res.redirect(localNextPath);
}
return res.redirect(EXPRESS_FRONTEND_OPENSRP_CALLBACK_URL);
}
}

const refreshToken = (req: express.Request, res: express.Response) => {
const provider = opensrpAuth;
const accessToken = req.session.preloadedState?.session?.extraData?.oAuth2Data?.access_token;
const refreshToken = req.session.preloadedState?.session?.extraData?.oAuth2Data?.refresh_token;
if(!accessToken || !refreshToken) {
return res.json({error: 'Access token or Refresh token not found'});
}
// re-create an access token instance
const token = provider.createToken(accessToken, refreshToken)
return token.refresh()
.then(oauthRes => {
const preloadedState = processUserInfo(req, res, oauthRes.data, undefined, true);
return res.json(preloadedState)
})
.catch((error) => {
return res.json({error: error.message || 'Failed to refresh token'});
});
}

const oauthCallback = (req: express.Request, res: express.Response, next: express.NextFunction) => {
const provider = opensrpAuth;
provider.code
Expand All @@ -128,35 +195,7 @@ const oauthCallback = (req: express.Request, res: express.Response, next: expres
next(error); // pass error to express
}
const apiResponse = JSON.parse(body);
apiResponse.oAuth2Data = user.data;

const sessionState = getOpenSRPUserInfo(apiResponse);
if (sessionState) {
const gatekeeperState = {
success: true,
result: sessionState.extraData,
};
const preloadedState = {
gatekeeper: gatekeeperState,
session: sessionState,
};
req.session.preloadedState = preloadedState;
const expireAfterMs = sessionState.extraData.oAuth2Data.expires_in * 1000;
req.session.cookie.maxAge = expireAfterMs;
// you have to save the session manually for POST requests like this one
req.session.save(() => void 0);
if (nextPath) {
/** reset nextPath to undefined; its value once set should only be used
* once and invalidated after being used, which is here. Failing to invalidate the previous value
* would result in the user being redirected to the same url the next time they pass through
* here irrespective of whether they should or shouldn't
*/
const localNextPath = nextPath;
nextPath = undefined;
return res.redirect(localNextPath);
}
return res.redirect(EXPRESS_FRONTEND_OPENSRP_CALLBACK_URL);
}
processUserInfo(req, res, user.data, apiResponse);
},
);
})
Expand Down Expand Up @@ -199,6 +238,7 @@ const router = express.Router();
router.use('/oauth/opensrp', oauthLogin);
router.use('/oauth/callback/OpenSRP', oauthCallback);
router.use('/oauth/state', oauthState);
router.use('/refresh/token', refreshToken);
// handle login
router.use(loginURL, loginRedirect);
// logout
Expand Down
6 changes: 6 additions & 0 deletions src/app/tests/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ export const oauthState = {
access_token: '64dc9918-fa1c-435d-9a97-ddb4aa1a8316',
expires_in: 3221,
refresh_token: '808f060c-be93-459e-bd56-3074d9b96229',
refresh_expires_in: 2592000,
refresh_expires_at: '2020-01-31T00:00:00.000Z',
scope: 'read write',
token_expires_at: '2020-01-01T00:53:41.000Z',
token_type: 'bearer',
},
preferredName: 'Superset User',
Expand All @@ -28,7 +31,10 @@ export const oauthState = {
access_token: '64dc9918-fa1c-435d-9a97-ddb4aa1a8316',
expires_in: 3221,
refresh_token: '808f060c-be93-459e-bd56-3074d9b96229',
refresh_expires_in: 2592000,
refresh_expires_at: '2020-01-31T00:00:00.000Z',
scope: 'read write',
token_expires_at: '2020-01-01T00:53:41.000Z',
token_type: 'bearer',
},
preferredName: 'Superset User',
Expand Down
37 changes: 33 additions & 4 deletions src/app/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/camelcase */
import MockDate from 'mockdate';
import ClientOauth2 from 'client-oauth2';
import nock from 'nock';
import request from 'supertest';
Expand Down Expand Up @@ -42,6 +43,7 @@ jest.mock('client-oauth2', () => {
return {
access_token: '64dc9918-fa1c-435d-9a97-ddb4aa1a8316',
expires_in: 3221,
refresh_expires_in: 2592000,
refresh_token: '808f060c-be93-459e-bd56-3074d9b96229',
scope: 'read write',
token_type: 'bearer',
Expand All @@ -56,6 +58,9 @@ jest.mock('client-oauth2', () => {
public sign(_: any) {
return { url: 'http://someUrl.com' };
}
public async refresh() {
return {data: this.data};
}
}

// tslint:disable-next-line: max-classes-per-file
Expand All @@ -72,6 +77,9 @@ jest.mock('client-oauth2', () => {
this.options = options;
this.request = req;
}
public createToken = (() => {
return this.token
});
};
});

Expand Down Expand Up @@ -112,16 +120,14 @@ describe('src/index.ts', () => {
});

it('E2E: oauth/opensrp/callback works correctly', async (done) => {
MockDate.set('1/1/2020');
JSON.parse = (body) => {
if (body === '{}') {
return parsedApiResponse;
}
};
nock('http://reveal-stage.smartregister.org').get(`/opensrp/user-details`).reply(200, {});

/** PS: This test will start failing on Fri, 14 May 3019 11:55:39 GMT */
jest.spyOn(global.Date, 'now').mockImplementationOnce(() => new Date('3019-05-14T11:01:58.135Z').valueOf());

request(app)
.get(oauthCallbackUri)
.end((err, res: request.Response) => {
Expand All @@ -133,7 +139,7 @@ describe('src/index.ts', () => {
cookie = extractCookies(res.header);
// expect that cookie will expire in: now(a date mocked to be in the future) + token.expires_in
expect(cookie['reveal-session'].flags).toEqual({
Expires: 'Fri, 14 May 3019 11:55:39 GMT',
Expires: 'Fri, 31 Jan 2020 00:00:00 GMT',
HttpOnly: true,
Path: '/',
});
Expand All @@ -152,6 +158,7 @@ describe('src/index.ts', () => {
});

it('/oauth/state works correctly with cookie', (done) => {
MockDate.set('1/1/2020');
request(app)
.get('/oauth/state')
.set('cookie', sessionString)
Expand All @@ -163,6 +170,28 @@ describe('src/index.ts', () => {
});
});

it('/refresh/token works correctly', (done) => {
MockDate.set('1/1/2020');
// when no session is found
request(app)
.get('/refresh/token')
.end((err: Error, res: request.Response) => {
panic(err, done);
expect(res.body).toEqual({error: 'Access token or Refresh token not found'});
done();
});

// call refresh token
request(app)
.get('/refresh/token')
.set('cookie', sessionString)
.end((err: Error, res: request.Response) => {
panic(err, done);
expect(res.body).toEqual(oauthState);
done();
});
});

it('Accessing login url when next path is undefined and logged in', (done) => {
// when logged in and nextPath is not provided, redirect to home
request(app)
Expand Down
Loading

0 comments on commit ae7f938

Please sign in to comment.