diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts
index 82ad377c..201b65c9 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 * as jwt from 'jsonwebtoken';
+import { Request, Response, NextFunction } from 'express';
+import { CognitoIdentityServiceProvider } from 'aws-sdk';
+import fetch from 'node-fetch';
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,10 +115,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));
@@ -121,18 +285,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) => {
diff --git a/backend/src/api/auth.ts b/backend/src/api/auth.ts
index 52c60449..575708ff 100644
--- a/backend/src/api/auth.ts
+++ b/backend/src/api/auth.ts
@@ -51,13 +51,24 @@ 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) {
+const oktaClient = jwksClient({
+ 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) {
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
*
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
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
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.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 = () => {
+ // 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;
}
@@ -214,6 +236,11 @@ export const AuthLogin: React.FC<{ showSignUp?: boolean }> = ({
+
+
+
+
+
diff --git a/frontend/src/pages/OktaCallback/OktaCallback.tsx b/frontend/src/pages/OktaCallback/OktaCallback.tsx
new file mode 100644
index 00000000..f8a9c421
--- /dev/null
+++ b/frontend/src/pages/OktaCallback/OktaCallback.tsx
@@ -0,0 +1,55 @@
+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/okta-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';