From f081767c6066de3af421e59baa9e385ef8379823 Mon Sep 17 00:00:00 2001 From: nickviola Date: Wed, 22 May 2024 13:15:45 -0500 Subject: [PATCH 01/12] Update user model to include oktaID value --- backend/src/models/user.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index 8e996447..36ac721d 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -32,6 +32,12 @@ export class User extends BaseEntity { }) cognitoId: string; + @Index({ unique: true }) + @Column({ + nullable: true + }) + oktaId: string; + @Index({ unique: true }) @Column({ nullable: true From 8b0a9d87de9523934c63705f24412388945041bd Mon Sep 17 00:00:00 2001 From: nickviola Date: Wed, 22 May 2024 16:55:12 -0500 Subject: [PATCH 02/12] Add initial backend callback and frontend login button to redirect to okta cognito user pool authentication --- backend/src/api/app.ts | 223 +++++++++++++++++++-- backend/src/api/auth.ts | 2 +- frontend/src/pages/AuthLogin/AuthLogin.tsx | 20 ++ 3 files changed, 232 insertions(+), 13 deletions(-) diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts index 82ad377c..2568cdca 100644 --- a/backend/src/api/app.ts +++ b/backend/src/api/app.ts @@ -21,11 +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 { User, UserType, connectToDatabase } from '../models'; import * as assessments from './assessments'; +import axios from 'axios'; +import * as jwt from 'jsonwebtoken'; +import { Request, Response, NextFunction } from 'express'; +import { CognitoIdentityServiceProvider } from 'aws-sdk'; const sanitizer = require('sanitizer'); +const cognito = new CognitoIdentityServiceProvider({ + region: process.env.AWS_REGION +}); + if ( (process.env.IS_OFFLINE || process.env.IS_LOCAL) && typeof jest === 'undefined' @@ -107,32 +115,223 @@ 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) => { + // if (!req.isAuthenticated()) { + // console.log('User not authenticated.'); + // return res.status(401).json({ + // message: 'Unauthorized' + // }); + // } else { + // console.log('User Authenticated'); + + // // 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 +} + +// Latest Callback +app.get('/auth/callback', async (req, res) => { + const { code } = req.query; + const clientId = process.env.COGNITO_CLIENT_ID; + const callbackUrl = process.env.COGNITO_CALLBACK_URL; + const domain = process.env.COGNITO_DOMAIN; + + if (!code) { + return res.status(400).json({ message: 'Missing authorization code' }); + } + + try { + console.log('Exchanging code for tokens with the following parameters:'); + console.log(`client_id: ${clientId}`); + console.log(`code: ${code}`); + console.log(`redirect_uri: ${callbackUrl}`); + + if (!callbackUrl) { + throw new Error('callbackUrl is required'); + } + + const response = await axios({ + method: 'post', + url: `https://${domain}/oauth2/token`, + data: `grant_type=authorization_code&client_id=${clientId}&code=${code}&redirect_uri=${encodeURIComponent( + callbackUrl + )}&scope=openid`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }); + + const { id_token, access_token, refresh_token } = response.data; + + if (!id_token) { + throw new Error('ID token is missing in the response'); + } + + // Decode the ID token to get user data and attributes + 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); + + // Verify the ID token before signing a new one + jwt.verify( + id_token, + auth.getKey, + { algorithms: ['RS256'] }, + async (err, payload) => { + if (err) { + console.log('Error: ', err); + return res.status(401).json({ message: 'Invalid ID token' }); + } + + // Connect to the database + await connectToDatabase(); + + // Look up or create user in the database based on the decoded token's information + let user = await User.findOne({ email: decodedToken.email }); + + if (!user) { + user = User.create({ + email: decodedToken.email, + // username: cognitoUsername, + oktaId: oktaId, + firstName: decodedToken.given_name, + lastName: decodedToken.family_name, + invitePending: true + }); + if (user) { + await user.save(); + } + } + + // Set tokens in secure cookies + res.cookie('access_token', access_token, { + httpOnly: true, + secure: true + }); + res.cookie('refresh_token', refresh_token, { + httpOnly: true, + secure: true + }); + + if (user) { + // Update okta id + user.oktaId = decodedToken['custom:OKTA_ID']; + user.save(); + // Ensure JWT_SECRET is defined + if (!process.env.JWT_SECRET) { + throw new Error('JWT_SECRET is not defined'); + } + + // Sign a new token with your application's secret key + const signedToken = await jwt.sign( + { id: user.id, email: user.email }, + process.env.JWT_SECRET, // Ensure JWT_SECRET is defined + { expiresIn: '1d' } + ); + res.cookie('id_token', signedToken, { httpOnly: true, secure: true }); + // res.cookie('crossfeed-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)); -app.post('/auth/callback', handlerToExpress(auth.callback)); +// app.post('/auth/callback', handlerToExpress(auth.callback)); app.post('/users/register', handlerToExpress(users.register)); 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) => { diff --git a/backend/src/api/auth.ts b/backend/src/api/auth.ts index 52c60449..db3c92e0 100644 --- a/backend/src/api/auth.ts +++ b/backend/src/api/auth.ts @@ -51,7 +51,7 @@ const client = jwksClient({ jwksUri: `https://cognito-idp.us-east-1.amazonaws.com/${process.env.REACT_APP_USER_POOL_ID}/.well-known/jwks.json` }); -function getKey(header, callback) { +export function getKey(header, callback) { client.getSigningKey(header.kid, function (err, key) { const signingKey = key?.getPublicKey(); callback(null, signingKey); diff --git a/frontend/src/pages/AuthLogin/AuthLogin.tsx b/frontend/src/pages/AuthLogin/AuthLogin.tsx index 16907dd2..4f133cd9 100644 --- a/frontend/src/pages/AuthLogin/AuthLogin.tsx +++ b/frontend/src/pages/AuthLogin/AuthLogin.tsx @@ -25,6 +25,21 @@ I18n.putVocabulariesForLanguage('en-US', { 'Confirm TOTP Code': 'Enter 2FA Code' }); +const domain = process.env.COGNITO_DOMAIN || 'default_value'; +const clientId = process.env.COGNITO_CLIENT_ID || 'default_value'; + +const LoginButton = () => { + const redirectToAuth = () => { + // Adjust this callback URL once determined + window.location.href = `https://${domain}/oauth2/authorize?client_id=${clientId}&response_type=code&scope=email+openid+profile&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fcallback`; + }; + return ( + + ); +}; + interface Errors extends Partial { global?: string; } @@ -214,6 +229,11 @@ export const AuthLogin: React.FC<{ showSignUp?: boolean }> = ({ + + + + + From 257526e20e518c81b67847ab89dfb821f66e3493 Mon Sep 17 00:00:00 2001 From: nickviola Date: Wed, 22 May 2024 17:56:17 -0500 Subject: [PATCH 03/12] Add initial backend callback and frontend login button to redirect to okta cognito user pool authentication --- frontend/src/pages/AuthLogin/AuthLogin.tsx | 23 +++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/AuthLogin/AuthLogin.tsx b/frontend/src/pages/AuthLogin/AuthLogin.tsx index 4f133cd9..384f81d6 100644 --- a/frontend/src/pages/AuthLogin/AuthLogin.tsx +++ b/frontend/src/pages/AuthLogin/AuthLogin.tsx @@ -25,13 +25,11 @@ I18n.putVocabulariesForLanguage('en-US', { 'Confirm TOTP Code': 'Enter 2FA Code' }); -const domain = process.env.COGNITO_DOMAIN || 'default_value'; -const clientId = process.env.COGNITO_CLIENT_ID || 'default_value'; - +// Replace with commented code below when env vars are set const LoginButton = () => { const redirectToAuth = () => { - // Adjust this callback URL once determined - window.location.href = `https://${domain}/oauth2/authorize?client_id=${clientId}&response_type=code&scope=email+openid+profile&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fcallback`; + window.location.href = + 'https://crossfeed-staging-okta-idp.auth.us-east-1.amazoncognito.com/oauth2/authorize?client_id=481n0fqrjiouharsddrv94c1a2&response_type=code&scope=email+openid+profile&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fcallback'; }; return ( +// ); +// }; + interface Errors extends Partial { global?: string; } From 67ae0126da107eb58df0c5ae5c4ad48c48f7861f Mon Sep 17 00:00:00 2001 From: nickviola Date: Wed, 22 May 2024 18:45:39 -0500 Subject: [PATCH 04/12] Remove logging for client info. --- backend/src/api/app.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts index 2568cdca..3e4d230c 100644 --- a/backend/src/api/app.ts +++ b/backend/src/api/app.ts @@ -175,11 +175,6 @@ app.get('/auth/callback', async (req, res) => { } try { - console.log('Exchanging code for tokens with the following parameters:'); - console.log(`client_id: ${clientId}`); - console.log(`code: ${code}`); - console.log(`redirect_uri: ${callbackUrl}`); - if (!callbackUrl) { throw new Error('callbackUrl is required'); } From bc665b01a58162c271b9a1a6dc3f42268bcf3722 Mon Sep 17 00:00:00 2001 From: nickviola Date: Fri, 24 May 2024 08:28:30 -0500 Subject: [PATCH 05/12] Add frontend session storage for token using new backend callback for okta --- backend/src/api/app.ts | 52 +++++++------------ frontend/src/App.tsx | 2 + frontend/src/pages/AuthLogin/AuthLogin.tsx | 29 ++++------- .../src/pages/OktaCallback/OktaCallback.tsx | 52 +++++++++++++++++++ frontend/src/pages/OktaCallback/index.ts | 1 + frontend/src/pages/index.ts | 1 + 6 files changed, 87 insertions(+), 50 deletions(-) create mode 100644 frontend/src/pages/OktaCallback/OktaCallback.tsx create mode 100644 frontend/src/pages/OktaCallback/index.ts diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts index 3e4d230c..59ea482e 100644 --- a/backend/src/api/app.ts +++ b/backend/src/api/app.ts @@ -27,6 +27,7 @@ import axios from 'axios'; import * as jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; import { CognitoIdentityServiceProvider } from 'aws-sdk'; +import fetch from 'node-fetch'; const sanitizer = require('sanitizer'); @@ -138,6 +139,7 @@ app.use(cookieParser()); app.use(setAuthorizationHeader); app.get('/whoami', (req, res, next) => { + // TODO: Test and determine if this can be removed. // if (!req.isAuthenticated()) { // console.log('User not authenticated.'); // return res.status(401).json({ @@ -163,9 +165,10 @@ interface DecodedToken { [key: string]: any; // Index signature for additional properties } -// Latest Callback -app.get('/auth/callback', async (req, res) => { - const { code } = req.query; +// Okta Callback Handler +app.post('/auth/callback', async (req, res) => { + console.log('Request Body: ', req.body); + const { code } = req.body; const clientId = process.env.COGNITO_CLIENT_ID; const callbackUrl = process.env.COGNITO_CALLBACK_URL; const domain = process.env.COGNITO_DOMAIN; @@ -179,24 +182,22 @@ app.get('/auth/callback', async (req, res) => { throw new Error('callbackUrl is required'); } - const response = await axios({ - method: 'post', - url: `https://${domain}/oauth2/token`, - data: `grant_type=authorization_code&client_id=${clientId}&code=${code}&redirect_uri=${encodeURIComponent( - callbackUrl - )}&scope=openid`, + 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 } = response.data; + const { id_token, access_token, refresh_token } = await response.json(); if (!id_token) { throw new Error('ID token is missing in the response'); } - // Decode the ID token to get user data and attributes const decodedToken = jwt.decode(id_token) as DecodedToken; if (!decodedToken) { throw new Error('Failed to decode ID token'); @@ -210,7 +211,6 @@ app.get('/auth/callback', async (req, res) => { console.log('ID Token:', id_token); console.log('Decoded Token:', decodedToken); - // Verify the ID token before signing a new one jwt.verify( id_token, auth.getKey, @@ -221,27 +221,24 @@ app.get('/auth/callback', async (req, res) => { return res.status(401).json({ message: 'Invalid ID token' }); } - // Connect to the database await connectToDatabase(); - // Look up or create user in the database based on the decoded token's information let user = await User.findOne({ email: decodedToken.email }); if (!user) { user = User.create({ email: decodedToken.email, - // username: cognitoUsername, oktaId: oktaId, firstName: decodedToken.given_name, lastName: decodedToken.family_name, invitePending: true }); - if (user) { - await user.save(); - } + await user.save(); + } else { + user.oktaId = oktaId; + await user.save(); } - // Set tokens in secure cookies res.cookie('access_token', access_token, { httpOnly: true, secure: true @@ -252,25 +249,17 @@ app.get('/auth/callback', async (req, res) => { }); if (user) { - // Update okta id - user.oktaId = decodedToken['custom:OKTA_ID']; - user.save(); - // Ensure JWT_SECRET is defined if (!process.env.JWT_SECRET) { throw new Error('JWT_SECRET is not defined'); } - // Sign a new token with your application's secret key const signedToken = await jwt.sign( { id: user.id, email: user.email }, - process.env.JWT_SECRET, // Ensure JWT_SECRET is defined + process.env.JWT_SECRET, { expiresIn: '1d' } ); + res.cookie('id_token', signedToken, { httpOnly: true, secure: true }); - // res.cookie('crossfeed-token', signedToken, { - // httpOnly: true, - // secure: true - // }); return res.status(200).json({ token: signedToken, @@ -293,7 +282,6 @@ app.get('/auth/callback', async (req, res) => { app.get('/', handlerToExpress(healthcheck)); app.post('/auth/login', handlerToExpress(auth.login)); -// app.post('/auth/callback', handlerToExpress(auth.callback)); app.post('/users/register', handlerToExpress(users.register)); app.post('/readysetcyber/register', handlerToExpress(users.RSCRegister)); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 63cec033..b8b278df 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -22,6 +22,7 @@ import { Domains, Feeds, LoginGovCallback, + OktaCallback, RegionUsers, Reports, Risk, @@ -122,6 +123,7 @@ const App: React.FC = () => ( path="/login-gov-callback" component={LoginGovCallback} /> + { + // TODO: Capture default values here once determined + const domain = process.env.COGNITO_DOMAIN || 'default_value'; + const clientId = process.env.COGNITO_CLIENT_ID || 'default_value'; + const callbackUrl = process.env.COGNITO_CALLBACK_URL || 'default_value'; + const encodedCallbackUrl = encodeURIComponent(callbackUrl); + const redirectToAuth = () => { - window.location.href = - 'https://crossfeed-staging-okta-idp.auth.us-east-1.amazoncognito.com/oauth2/authorize?client_id=481n0fqrjiouharsddrv94c1a2&response_type=code&scope=email+openid+profile&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fcallback'; + // Adjust this callback URL once determined + window.location.href = `https://${domain}/oauth2/authorize?client_id=${clientId}&response_type=code&scope=email+openid+profile&redirect_uri=${encodedCallbackUrl}`; }; + return ( -// ); -// }; - interface Errors extends Partial { global?: string; } diff --git a/frontend/src/pages/OktaCallback/OktaCallback.tsx b/frontend/src/pages/OktaCallback/OktaCallback.tsx new file mode 100644 index 00000000..e2a9fcbe --- /dev/null +++ b/frontend/src/pages/OktaCallback/OktaCallback.tsx @@ -0,0 +1,52 @@ +import React, { useCallback, useEffect } from 'react'; +import { parse } from 'query-string'; +import { useAuthContext } from 'context'; +import { User } from 'types'; +import { useHistory } from 'react-router-dom'; + +type OktaCallbackResponse = { + token: string; + user: User; +}; + +export const OktaCallback: React.FC = () => { + const { apiPost, login } = useAuthContext(); + const history = useHistory(); + + const handleOktaCallback = useCallback(async () => { + const { code } = parse(window.location.search); + console.log('Code: ', code); + const nonce = localStorage.getItem('nonce'); + console.log('Nonce: ', nonce); + + try { + // Pass request to backend callback endpoint + const response = await apiPost('/auth/callback', { + body: { + code: code + } + }); + console.log('Response: ', response); + console.log('token ', response.token); + + // Login + await login(response.token); + + // Storage Management + localStorage.setItem('token', response.token); + localStorage.removeItem('nonce'); + localStorage.removeItem('state'); + + history.push('/'); + } catch (e) { + console.error(e); + history.push('/'); + } + }, [apiPost, history, login]); + + useEffect(() => { + handleOktaCallback(); + }, [handleOktaCallback]); + + return
Loading...
; +}; diff --git a/frontend/src/pages/OktaCallback/index.ts b/frontend/src/pages/OktaCallback/index.ts new file mode 100644 index 00000000..d060d4c8 --- /dev/null +++ b/frontend/src/pages/OktaCallback/index.ts @@ -0,0 +1 @@ +export * from './OktaCallback'; diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts index 1333568e..471a110b 100644 --- a/frontend/src/pages/index.ts +++ b/frontend/src/pages/index.ts @@ -4,6 +4,7 @@ export * from './AuthCreateAccount'; export * from './Domains'; export * from './Domain'; export * from './LoginGovCallback'; +export * from './OktaCallback'; export * from './Scans'; export * from './Search'; export * from './TermsOfUse'; From 99f8c5921e4482a346f040d2fef3b4774651ab70 Mon Sep 17 00:00:00 2001 From: nickviola Date: Fri, 24 May 2024 08:30:18 -0500 Subject: [PATCH 06/12] Remove axios and replace call with fetch --- backend/src/api/app.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts index 59ea482e..b6f91f33 100644 --- a/backend/src/api/app.ts +++ b/backend/src/api/app.ts @@ -23,7 +23,6 @@ import rateLimit from 'express-rate-limit'; import { createProxyMiddleware } from 'http-proxy-middleware'; import { User, UserType, connectToDatabase } from '../models'; import * as assessments from './assessments'; -import axios from 'axios'; import * as jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; import { CognitoIdentityServiceProvider } from 'aws-sdk'; From 37c0ffcc93af84597cb66637def48e9b17c7a73f Mon Sep 17 00:00:00 2001 From: nickviola Date: Fri, 24 May 2024 08:38:51 -0500 Subject: [PATCH 07/12] Remove and clean up logging. --- backend/src/api/app.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts index b6f91f33..16bd38bb 100644 --- a/backend/src/api/app.ts +++ b/backend/src/api/app.ts @@ -140,12 +140,10 @@ app.use(setAuthorizationHeader); app.get('/whoami', (req, res, next) => { // TODO: Test and determine if this can be removed. // if (!req.isAuthenticated()) { - // console.log('User not authenticated.'); // return res.status(401).json({ // message: 'Unauthorized' // }); // } else { - // console.log('User Authenticated'); // // You can log other SAML attributes similarly // // return res.status(200).json({ user: req.user }); From 3e9464f289d619e01ef4c05cb31845c65326475f Mon Sep 17 00:00:00 2001 From: nickviola Date: Fri, 24 May 2024 08:43:33 -0500 Subject: [PATCH 08/12] Remove and clean up logging. --- backend/src/api/app.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts index 16bd38bb..c8d9164b 100644 --- a/backend/src/api/app.ts +++ b/backend/src/api/app.ts @@ -164,7 +164,6 @@ interface DecodedToken { // Okta Callback Handler app.post('/auth/callback', async (req, res) => { - console.log('Request Body: ', req.body); const { code } = req.body; const clientId = process.env.COGNITO_CLIENT_ID; const callbackUrl = process.env.COGNITO_CALLBACK_URL; From 8842ceff9eb27f367d5503cca58a78eabceff191 Mon Sep 17 00:00:00 2001 From: nickviola Date: Fri, 24 May 2024 10:30:44 -0500 Subject: [PATCH 09/12] Update timeout for token to 14m and update env vars for frontend to work with REACT_APP_ leader requirement for project --- backend/src/api/app.ts | 11 ++++++----- frontend/src/pages/AuthLogin/AuthLogin.tsx | 7 ++++--- frontend/src/pages/OktaCallback/OktaCallback.tsx | 11 +++++++---- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts index c8d9164b..e371b3d4 100644 --- a/backend/src/api/app.ts +++ b/backend/src/api/app.ts @@ -163,11 +163,11 @@ interface DecodedToken { } // Okta Callback Handler -app.post('/auth/callback', async (req, res) => { +app.post('/auth/okta-callback', async (req, res) => { const { code } = req.body; - const clientId = process.env.COGNITO_CLIENT_ID; - const callbackUrl = process.env.COGNITO_CALLBACK_URL; - const domain = process.env.COGNITO_DOMAIN; + 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' }); @@ -252,7 +252,7 @@ app.post('/auth/callback', async (req, res) => { const signedToken = await jwt.sign( { id: user.id, email: user.email }, process.env.JWT_SECRET, - { expiresIn: '1d' } + { expiresIn: '14m' } ); res.cookie('id_token', signedToken, { httpOnly: true, secure: true }); @@ -278,6 +278,7 @@ app.post('/auth/callback', async (req, res) => { app.get('/', handlerToExpress(healthcheck)); app.post('/auth/login', handlerToExpress(auth.login)); +app.post('/auth/callback', handlerToExpress(auth.callback)); app.post('/users/register', handlerToExpress(users.register)); app.post('/readysetcyber/register', handlerToExpress(users.RSCRegister)); diff --git a/frontend/src/pages/AuthLogin/AuthLogin.tsx b/frontend/src/pages/AuthLogin/AuthLogin.tsx index 4d153a9a..b3c9769c 100644 --- a/frontend/src/pages/AuthLogin/AuthLogin.tsx +++ b/frontend/src/pages/AuthLogin/AuthLogin.tsx @@ -29,9 +29,10 @@ I18n.putVocabulariesForLanguage('en-US', { // page which will remove the extra unnecessary login screen step. const LoginButton = () => { // TODO: Capture default values here once determined - const domain = process.env.COGNITO_DOMAIN || 'default_value'; - const clientId = process.env.COGNITO_CLIENT_ID || 'default_value'; - const callbackUrl = process.env.COGNITO_CALLBACK_URL || 'default_value'; + const domain = process.env.REACT_APP_COGNITO_DOMAIN || 'default_value'; + const clientId = process.env.REACT_APP_COGNITO_CLIENT_ID || 'default_value'; + const callbackUrl = + process.env.REACT_APP_COGNITO_CALLBACK_URL || 'default_value'; const encodedCallbackUrl = encodeURIComponent(callbackUrl); const redirectToAuth = () => { diff --git a/frontend/src/pages/OktaCallback/OktaCallback.tsx b/frontend/src/pages/OktaCallback/OktaCallback.tsx index e2a9fcbe..f8a9c421 100644 --- a/frontend/src/pages/OktaCallback/OktaCallback.tsx +++ b/frontend/src/pages/OktaCallback/OktaCallback.tsx @@ -21,11 +21,14 @@ export const OktaCallback: React.FC = () => { try { // Pass request to backend callback endpoint - const response = await apiPost('/auth/callback', { - body: { - code: code + const response = await apiPost( + '/auth/okta-callback', + { + body: { + code: code + } } - }); + ); console.log('Response: ', response); console.log('token ', response.token); From 594caf625e4a99d714a545e999c6c378b0f198f1 Mon Sep 17 00:00:00 2001 From: nickviola Date: Tue, 28 May 2024 09:47:17 -0500 Subject: [PATCH 10/12] Add ability to login with old cognito user pool on landing page to prevent login breakage in staging environment --- backend/src/api/app.ts | 2 +- backend/src/api/auth.ts | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts index e371b3d4..201b65c9 100644 --- a/backend/src/api/app.ts +++ b/backend/src/api/app.ts @@ -209,7 +209,7 @@ app.post('/auth/okta-callback', async (req, res) => { jwt.verify( id_token, - auth.getKey, + auth.getOktaKey, { algorithms: ['RS256'] }, async (err, payload) => { if (err) { diff --git a/backend/src/api/auth.ts b/backend/src/api/auth.ts index db3c92e0..c524fcb9 100644 --- a/backend/src/api/auth.ts +++ b/backend/src/api/auth.ts @@ -51,6 +51,10 @@ const client = jwksClient({ jwksUri: `https://cognito-idp.us-east-1.amazonaws.com/${process.env.REACT_APP_USER_POOL_ID}/.well-known/jwks.json` }); +const oktaClient = jwksClient({ + jwksUri: `https://cognito-idp.us-east-1.amazonaws.com/${process.env.REACT_APP_OKTA_USER_POOL_ID}/.well-known/jwks.json` +}); + export function getKey(header, callback) { client.getSigningKey(header.kid, function (err, key) { const signingKey = key?.getPublicKey(); @@ -58,6 +62,13 @@ export function getKey(header, callback) { }); } +export function getOktaKey(header, callback) { + oktaClient.getSigningKey(header.kid, function (err, key) { + const signingKey = key?.getPublicKey(); + callback(null, signingKey); + }); +} + /** * @swagger * From a60012f0a82bcfb9953e7403eb21a156f25f0280 Mon Sep 17 00:00:00 2001 From: nickviola Date: Tue, 28 May 2024 14:40:16 -0500 Subject: [PATCH 11/12] Update name of env variable to reduce duplication --- backend/src/api/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/api/auth.ts b/backend/src/api/auth.ts index c524fcb9..575708ff 100644 --- a/backend/src/api/auth.ts +++ b/backend/src/api/auth.ts @@ -52,7 +52,7 @@ const client = jwksClient({ }); const oktaClient = jwksClient({ - jwksUri: `https://cognito-idp.us-east-1.amazonaws.com/${process.env.REACT_APP_OKTA_USER_POOL_ID}/.well-known/jwks.json` + jwksUri: `https://cognito-idp.us-east-1.amazonaws.com/${process.env.REACT_APP_COGNITO_USER_POOL_ID}/.well-known/jwks.json` }); export function getKey(header, callback) { From 3e7913bfff41788a9562821b41a24f441b915b19 Mon Sep 17 00:00:00 2001 From: nickviola Date: Tue, 28 May 2024 16:07:47 -0500 Subject: [PATCH 12/12] Update dev.env.example to include new user pool config params --- dev.env.example | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dev.env.example b/dev.env.example index 56dc76ad..fee1b25a 100644 --- a/dev.env.example +++ b/dev.env.example @@ -74,6 +74,12 @@ REACT_APP_USER_POOL_ID=us-east-1_uxiY8DOum REACT_APP_USER_POOL_CLIENT_ID=1qf4cii9v0t9hn1hnr54f2ao0j REACT_APP_TOTP_ISSUER=Local Crossfeed +AWS_REGION=us-east-1 +REACT_APP_COGNITO_DOMAIN=crossfeed-staging-okta-idp.auth.us-east-1.amazoncognito.com +REACT_APP_COGNITO_CLIENT_ID=481n0fqrjiouharsddrv94c1a2 +REACT_APP_COGNITO_USER_POOL_ID=us-east-1_iWciADuOe +REACT_APP_COGNITO_CALLBACK_URL=http://localhost/okta-callback + FARGATE_MAX_CONCURRENCY=100 SCHEDULER_ORGS_PER_SCANTASK=2