Skip to content

Commit

Permalink
Merge branch 'develop' into 20-merge-lz-infra
Browse files Browse the repository at this point in the history
  • Loading branch information
aloftus23 committed May 29, 2024
2 parents af68221 + 62a2d62 commit 4547c00
Show file tree
Hide file tree
Showing 21 changed files with 528 additions and 74 deletions.
2 changes: 1 addition & 1 deletion backend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ module.exports = {
},
coverageThreshold: {
global: {
branches: 50
branches: 45
}
}
};
236 changes: 214 additions & 22 deletions backend/src/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,19 @@ import * as reports from './reports';
import * as savedSearches from './saved-searches';
import rateLimit from 'express-rate-limit';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { UserType } from '../models';
import logger from '../tools/lambda-logger';
import { User, UserType, connectToDatabase } from '../models';
import * as assessments from './assessments';
import { sanitize } from 'dompurify';
import * as jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import { CognitoIdentityServiceProvider } from 'aws-sdk';
import fetch from 'node-fetch';
import logger from '../tools/lambda-logger';

const sanitizer = require('sanitizer');

const cognito = new CognitoIdentityServiceProvider({
region: process.env.AWS_REGION
});

if (
(process.env.IS_OFFLINE || process.env.IS_LOCAL) &&
Expand Down Expand Up @@ -55,12 +64,12 @@ const handlerToExpress = (handler) => async (req, res, next) => {
res.setHeader('Strict-Transport-Security', 'max-age=31536000');

try {
const parsedBody = JSON.parse(body);
const parsedBody = JSON.parse(sanitizer.sanitize(body));
res.status(statusCode).json(parsedBody);
} catch (e) {
// Not a JSON body
res.setHeader('content-type', 'text/plain');
res.status(statusCode).send(sanitize(body));
res.status(statusCode).send(sanitizer.sanitize(body));
}
};

Expand Down Expand Up @@ -99,30 +108,42 @@ app.use(

app.use(express.json({ strict: false }));

// These CORS origins work in all Crossfeed environments
app.use(
cors({
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
origin: [
'http://localhost',
/^https:\/\/(.*\.)?crossfeed\.cyber\.dhs\.gov$/,
/^https:\/\/(.*\.)?readysetcyber\.cyber\.dhs\.gov$/
],
methods: 'GET,POST,PUT,DELETE,OPTIONS'
})
);

// The API URLs are different in each environment
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: [
"'self'",
`'${process.env.COGNITO_URL}'`,
`'${process.env.BACKEND_DOMAIN}'`
`${process.env.COGNITO_URL}`,
`${process.env.BACKEND_DOMAIN}`
],
frameSrc: ["'self'", 'https://www.dhs.gov/ntas/'],
imgSrc: [
"'self'",
'data:',
`${process.env.FRONTEND_DOMAIN}`,
'https://www.dhs.gov'
],
objectSrc: ["'none'"],
scriptSrc: [
"'self'",
`'${process.env.BACKEND_DOMAIN}'`
// Add any other allowed script sources here
`${process.env.BACKEND_DOMAIN}`,
'https://www.dhs.gov'
],
frameAncestors: ["'none'"]
// Add other directives as needed
}
},
hsts: {
Expand All @@ -135,10 +156,166 @@ app.use(

app.use((req, res, next) => {
res.setHeader('X-XSS-Protection', '0');
// Okta header
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});

const setAuthorizationHeader = (
req: Request,
res: Response,
next: NextFunction
) => {
const accessToken = req.cookies.access_token;

if (accessToken) {
req.headers.authorization = `Bearer ${accessToken}`;
}

next();
};

app.use(cookieParser());
app.use(setAuthorizationHeader);

app.get('/whoami', (req, res, next) => {
// TODO: Test and determine if this can be removed.
// if (!req.isAuthenticated()) {
// return res.status(401).json({
// message: 'Unauthorized'
// });
// } else {

// // You can log other SAML attributes similarly
// // return res.status(200).json({ user: req.user });
// }
return next();
});

interface DecodedToken {
sub: string;
email: string;
'cognito:username': string;
'custom:OKTA_ID': string;
given_name: string;
family_name: string;
email_verified: boolean;
[key: string]: any; // Index signature for additional properties
}

// Okta Callback Handler
app.post('/auth/okta-callback', async (req, res) => {
const { code } = req.body;
const clientId = process.env.REACT_APP_COGNITO_CLIENT_ID;
const callbackUrl = process.env.REACT_APP_COGNITO_CALLBACK_URL;
const domain = process.env.REACT_APP_COGNITO_DOMAIN;

if (!code) {
return res.status(400).json({ message: 'Missing authorization code' });
}

try {
if (!callbackUrl) {
throw new Error('callbackUrl is required');
}

const tokenEndpoint = `https://${domain}/oauth2/token`;
const tokenData = `grant_type=authorization_code&client_id=${clientId}&code=${code}&redirect_uri=${callbackUrl}&scope=openid`;

const response = await fetch(tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: tokenData
});
const { id_token, access_token, refresh_token } = await response.json();

if (!id_token) {
throw new Error('ID token is missing in the response');
}

const decodedToken = jwt.decode(id_token) as DecodedToken;
if (!decodedToken) {
throw new Error('Failed to decode ID token');
}

const cognitoUsername = decodedToken['cognito:username'];
const oktaId = decodedToken['custom:OKTA_ID'];
console.log('Cognito Username:', cognitoUsername);
console.log('Cognito OKTA_ID:', oktaId);

console.log('ID Token:', id_token);
console.log('Decoded Token:', decodedToken);

jwt.verify(
id_token,
auth.getOktaKey,
{ algorithms: ['RS256'] },
async (err, payload) => {
if (err) {
console.log('Error: ', err);
return res.status(401).json({ message: 'Invalid ID token' });
}

await connectToDatabase();

let user = await User.findOne({ email: decodedToken.email });

if (!user) {
user = User.create({
email: decodedToken.email,
oktaId: oktaId,
firstName: decodedToken.given_name,
lastName: decodedToken.family_name,
invitePending: true
});
await user.save();
} else {
user.oktaId = oktaId;
await user.save();
}

res.cookie('access_token', access_token, {
httpOnly: true,
secure: true
});
res.cookie('refresh_token', refresh_token, {
httpOnly: true,
secure: true
});

if (user) {
if (!process.env.JWT_SECRET) {
throw new Error('JWT_SECRET is not defined');
}

const signedToken = await jwt.sign(
{ id: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '14m' }
);

res.cookie('id_token', signedToken, { httpOnly: true, secure: true });

return res.status(200).json({
token: signedToken,
user: user
});
}
}
);
} catch (error) {
console.error(
'Token exchange error:',
error.response ? error.response.data : error.message
);
res.status(500).json({
message: 'Authentication failed',
error: error.response ? error.response.data : error.message
});
}
});

app.get('/', handlerToExpress(healthcheck));
app.post('/auth/login', handlerToExpress(auth.login));
Expand All @@ -149,18 +326,33 @@ app.post('/readysetcyber/register', handlerToExpress(users.RSCRegister));
app.get('/notifications', handlerToExpress(notifications.list));

const checkUserLoggedIn = async (req, res, next) => {
req.requestContext = {
authorizer: await auth.authorize({
authorizationToken: req.headers.authorization
})
};
if (
!req.requestContext.authorizer.id ||
req.requestContext.authorizer.id === 'cisa:crossfeed:anonymous'
) {
console.log('Checking if user is logged in.');

const authorizationHeader = req.headers.authorization;

if (!authorizationHeader) {
return res.status(401).send('Not logged in');
}
return next();

try {
req.requestContext = {
authorizer: await auth.authorize({
authorizationToken: authorizationHeader
})
};

if (
!req.requestContext.authorizer.id ||
req.requestContext.authorizer.id === 'cisa:crossfeed:anonymous'
) {
return res.status(401).send('Not logged in');
}

return next();
} catch (error) {
console.error('Error authorizing user:', error);
return res.status(500).send('Internal server error');
}
};

const checkUserSignedTerms = (req, res, next) => {
Expand Down
52 changes: 42 additions & 10 deletions backend/src/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
OrganizationTag,
UserType
} from '../models';
import { getRegion } from './organizations';
import * as jwt from 'jsonwebtoken';
import { APIGatewayProxyEvent } from 'aws-lambda';
import * as jwksClient from 'jwks-rsa';
Expand Down Expand Up @@ -54,13 +55,24 @@ const client = jwksClient({
}
});

function getKey(header, callback) {
const oktaClient = jwksClient({
jwksUri: `${process.env.COGNITO_URL}/${process.env.REACT_APP_COGNITO_USER_POOL_ID}/.well-known/jwks.json`
});

export function getKey(header, callback) {
client.getSigningKey(header.kid, function (err, key) {
const signingKey = key?.getPublicKey();
callback(null, signingKey);
});
}

export function getOktaKey(header, callback) {
oktaClient.getSigningKey(header.kid, function (err, key) {
const signingKey = key?.getPublicKey();
callback(null, signingKey);
});
}

/**
* @swagger
*
Expand Down Expand Up @@ -259,28 +271,48 @@ export const authorize = async (event) => {

/** Check if a user has global write admin permissions */
export const isGlobalWriteAdmin = (event: APIGatewayProxyEvent) => {
return event.requestContext.authorizer &&
return !!(
event.requestContext.authorizer &&
event.requestContext.authorizer.userType === UserType.GLOBAL_ADMIN
? true
: false;
);
};

/** Check if a user has global view permissions */
export const isGlobalViewAdmin = (event: APIGatewayProxyEvent) => {
return event.requestContext.authorizer &&
return !!(
event.requestContext.authorizer &&
(event.requestContext.authorizer.userType === UserType.GLOBAL_VIEW ||
event.requestContext.authorizer.userType === UserType.GLOBAL_ADMIN)
? true
: false;
);
};

/** Check if a user has regionalAdmin view permissions */
export const isRegionalAdmin = (event: APIGatewayProxyEvent) => {
return event.requestContext.authorizer &&
return !!(
event.requestContext.authorizer &&
(event.requestContext.authorizer.userType === UserType.REGIONAL_ADMIN ||
event.requestContext.authorizer.userType === UserType.GLOBAL_ADMIN)
? true
: false;
);
};

/** Check if user is a regional admin and if a selected organization belongs to their region */
export const isRegionalAdminForOrganization = (
event: APIGatewayProxyEvent,
organizationId?: string
) => {
if (!event.requestContext.authorizer || !organizationId) return false;
return (
isRegionalAdmin(event) &&
matchesRegion(event.requestContext.authorizer.regionId, organizationId)
);
};

export const matchesRegion = async (
regionId?: string,
organizationId?: string
) => {
if (!organizationId || !regionId) return false;
return regionId === (await getRegion(organizationId));
};

/** Checks if the current user is allowed to access (modify) a user with id userId */
Expand Down
Loading

0 comments on commit 4547c00

Please sign in to comment.