Skip to content

Commit

Permalink
Update userDetails parsing and logout. (#31)
Browse files Browse the repository at this point in the history
* Update express to work with keycloaks aouth data

* Update packages

* Add jsonwebtokens

* Use keycloak jwts instead of user-details endpoint

Clear session on all cases of logout

* Remove json webtoken, update gatekeeper package

* Move jwt parsing to gatekeeper package

* Update fixtures

* Update env sample

* Update yarn file

* Improve coverage

* Fix mock return values for client-oauth2

* Check idTokenHint is used correctly

* Add profile to scopes

* Ignore coverage from types folder

* Bump gatekeeper package
  • Loading branch information
peterMuriuki authored Aug 24, 2022
1 parent 1b770a1 commit 630d0ed
Show file tree
Hide file tree
Showing 10 changed files with 337 additions and 123 deletions.
5 changes: 3 additions & 2 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ NODE_ENV=development
EXPRESS_OPENSRP_ACCESS_TOKEN_URL=https://reveal-stage.smartregister.org/opensrp/oauth/token
EXPRESS_OPENSRP_AUTHORIZATION_URL=https://reveal-stage.smartregister.org/opensrp/oauth/authorize
EXPRESS_OPENSRP_CALLBACK_URL=http://localhost:3000/oauth/callback/OpenSRP/
EXPRESS_OPENSRP_USER_URL=https://reveal-stage.smartregister.org/opensrp/user-details
EXPRESS_OPENSRP_OAUTH_STATE=opensrp
EXPRESS_OPENSRP_CLIENT_ID=hunter2
EXPRESS_OPENSRP_CLIENT_SECRET=hunter2
Expand All @@ -25,6 +24,7 @@ EXPRESS_ALLOW_TOKEN_RENEWAL=true
EXPRESS_MAXIMUM_SESSION_LIFE_TIME=10800

EXPRESS_SERVER_LOGOUT_URL=http://localhost:3000/logout
# optional -> kills opensrp web server session, for instance not needed when auth server is keycloak
EXPRESS_OPENSRP_LOGOUT_URL=https://reveal-stage.smartregister.org/opensrp/logout.do
EXPRESS_KEYCLOAK_LOGOUT_URL=https://keycloak-stage.smartregister.org/auth/realms/reveal-stage/protocol/openid-connect/logout

Expand All @@ -33,11 +33,12 @@ EXPRESS_MAXIMUM_LOG_FILES_NUMBER=5
EXPRESS_LOGS_FILE_PATH='/home/.express/reveal-express-server.log

# https://github.com/helmetjs/helmet#reference
EXPRESS_CONTENT_SECURITY_POLICY_CONFIG=`{"default-src":["'self'"]}`
EXPRESS_CONTENT_SECURITY_POLICY_CONFIG=`{"default-src":["'self'", "smartregister.org", "github.com"]}`

EXPRESS_REDIS_STAND_ALONE_URL=redis://username:[email protected]:6379/4

EXPRESS_REDIS_SENTINEL_CONFIG='{"name":"master","sentinelUsername":"u_name","sentinelPassword":"pass","db":4,"sentinels":[{"host":"127.0.0.1","port":6379},{"host":"127.0.0.1","port":6379}]}'

# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to. `Map<string, stringifiedJson>`.
EXPRESS_RESPONSE_HEADERS='{"Report-To":"{ \"group\": \"csp-endpoint\", \"max_age\": 10886400, \"endpoints\": [{ \"url\": \"https://example.com/endpoint\" }] }", "Access-Control-Allow-Headers": "GET"}'

2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module.exports = {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
testEnvironment: 'node',
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/tests/', '!src/index.ts'],
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/tests/', '!src/index.ts', '!src/types/index.ts'],
coverageReporters: ['lcov', 'html'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
]
},
"dependencies": {
"@onaio/gatekeeper": "^0.1.2",
"@onaio/gatekeeper": "^0.4.0",
"@onaio/session-reducer": "^0.0.13",
"client-oauth2": "^4.3.3",
"compression": "^1.7.4",
"connect-redis": "^6.1.3",
Expand Down
72 changes: 34 additions & 38 deletions src/app/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { getOpenSRPUserInfo } from '@onaio/gatekeeper';
import ClientOAuth2 from 'client-oauth2';
import compression from 'compression';
import cookieParser from 'cookie-parser';
Expand All @@ -11,9 +10,9 @@ import fetch from 'node-fetch';
import morgan from 'morgan';
import path from 'path';
import querystring from 'querystring';
import request from 'request';
import sessionFileStore from 'session-file-store';
import { parse } from 'url';
import { SessionState } from '@onaio/session-reducer';
import { winstonLogger, winstonStream } from '../configs/winston';
import {
EXPRESS_ALLOW_TOKEN_RENEWAL,
Expand All @@ -28,7 +27,6 @@ import {
EXPRESS_OPENSRP_CLIENT_SECRET,
EXPRESS_OPENSRP_LOGOUT_URL,
EXPRESS_OPENSRP_OAUTH_STATE,
EXPRESS_OPENSRP_USER_URL,
EXPRESS_REACT_BUILD_PATH,
EXPRESS_SERVER_LOGOUT_URL,
EXPRESS_SESSION_FILESTORE_PATH,
Expand All @@ -42,16 +40,15 @@ import {
EXPRESS_RESPONSE_HEADERS,
} from '../configs/envs';
import { SESSION_IS_EXPIRED, TOKEN_NOT_FOUND, TOKEN_REFRESH_FAILED } from '../constants';

type Dictionary = { [key: string]: unknown };
import { parseOauthClientData, sessionLogout } from './utils';

const opensrpAuth = new ClientOAuth2({
accessTokenUri: EXPRESS_OPENSRP_ACCESS_TOKEN_URL,
authorizationUri: EXPRESS_OPENSRP_AUTHORIZATION_URL,
clientId: EXPRESS_OPENSRP_CLIENT_ID,
clientSecret: EXPRESS_OPENSRP_CLIENT_SECRET,
redirectUri: EXPRESS_OPENSRP_CALLBACK_URL,
scopes: ['read', 'write'],
scopes: ['openid', 'profile'],
state: EXPRESS_OPENSRP_OAUTH_STATE,
});
const loginURL = EXPRESS_SESSION_LOGIN_URL;
Expand Down Expand Up @@ -151,7 +148,7 @@ app.use((_, res, next) => {
next();
});

class HttpException extends Error {
export class HttpException extends Error {
public statusCode: number;

public message: string;
Expand Down Expand Up @@ -193,19 +190,23 @@ const oauthLogin = (_: express.Request, res: express.Response) => {
const processUserInfo = (
req: express.Request,
res: express.Response,
authDetails: Dictionary,
userDetails?: Dictionary,
processedUserDetails?: SessionState,
isRefresh?: boolean,
) => {
// get user details from session. will be needed when refreshing token
const userInfo = userDetails ?? req.session.preloadedState?.session?.extraData ?? {};
let userInfo = processedUserDetails ?? req.session.preloadedState?.session?.extraData ?? {};
const date = new Date(Date.now());
const sessionExpiryTime = req.session.preloadedState?.session_expires_at;
const sessionExpiresAt = isRefresh
? sessionExpiryTime
: new Date(date.setSeconds(date.getSeconds() + EXPRESS_MAXIMUM_SESSION_LIFE_TIME)).toISOString();
userInfo.oAuth2Data = authDetails;
const sessionState = getOpenSRPUserInfo(userInfo);
userInfo = {
...userInfo,
extraData: {
...userInfo.extraData,
},
};
const sessionState = userInfo;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (sessionState) {
const gatekeeperState = {
Expand Down Expand Up @@ -265,7 +266,8 @@ const refreshToken = (req: express.Request, res: express.Response, next: express
return token
.refresh()
.then((oauthRes) => {
const preloadedState = processUserInfo(req, res, oauthRes.data, undefined, true);
const opensrpUserInfo = parseOauthClientData(oauthRes);
const preloadedState = processUserInfo(req, res, opensrpUserInfo, true);
return res.json(preloadedState);
})
.catch((error: Error) => {
Expand All @@ -279,27 +281,12 @@ const oauthCallback = (req: express.Request, res: express.Response, next: expres
provider.code
.getToken(req.originalUrl)
.then((user: ClientOAuth2.Token) => {
const url = EXPRESS_OPENSRP_USER_URL;
request.get(
url,
user.sign({
method: 'GET',
url,
}),
(error: Error, _: request.Response, body: string) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (error) {
next(error); // pass error to express
}
let apiResponse: Dictionary;
try {
apiResponse = JSON.parse(body);
processUserInfo(req, res, user.data, apiResponse);
} catch (__) {
res.redirect('/logout?serverLogout=true');
}
},
);
try {
const opensrpUserInfo = parseOauthClientData(user);
processUserInfo(req, res, opensrpUserInfo);
} catch (__) {
res.redirect('/logout?serverLogout=true');
}
})
.catch((e: Error) => {
next(e); // pass error to express
Expand Down Expand Up @@ -337,6 +324,7 @@ const loginRedirect = (req: express.Request, res: express.Response, _: express.N
const logout = async (req: express.Request, res: express.Response) => {
if (req.query.serverLogout) {
const accessToken = req.session.preloadedState?.session?.extraData?.oAuth2Data?.access_token;
const idTokenHint = req.session.preloadedState?.session?.extraData?.oAuth2Data?.id_token;
const payload = {
headers: {
accept: 'application/json',
Expand All @@ -345,14 +333,22 @@ const logout = async (req: express.Request, res: express.Response) => {
},
method: 'GET',
};
if (accessToken) {
if (accessToken && EXPRESS_OPENSRP_LOGOUT_URL) {
await fetch(EXPRESS_OPENSRP_LOGOUT_URL, payload);
}
const keycloakLogoutFullPath = `${EXPRESS_KEYCLOAK_LOGOUT_URL}?redirect_uri=${EXPRESS_SERVER_LOGOUT_URL}`;
let logoutParams = {};
if (idTokenHint) {
logoutParams = {
post_logout_redirect_url: EXPRESS_SERVER_LOGOUT_URL,
id_token_hint: idTokenHint,
};
}
const searchQuery = new URLSearchParams(logoutParams).toString();
const keycloakLogoutFullPath = `${EXPRESS_KEYCLOAK_LOGOUT_URL}?${searchQuery}`;
sessionLogout(req, res);
res.redirect(keycloakLogoutFullPath);
} else {
req.session.destroy(() => undefined);
res.clearCookie(sessionName);
sessionLogout(req, res);
res.redirect(loginURL);
}
};
Expand Down
Loading

0 comments on commit 630d0ed

Please sign in to comment.