Prevent msal-react from loading components multiple times - reactjs

I'm not too far into the implementation and have it pretty bear bones and not refactored (and not great coding practices).
This is an Admin portal that uses the company's Azure Active Directory to validate users as opposed to username/password, etc. The flow is this:
User navigates to /admin.
useEffect() starts checking if user isAuthenticated.
If not, they are automatically redirected to login.
Once they have logged in, or are verified as already being isAuthenticated, they can now access components that are children of <AuthenticatedTemplate>.
The only child component currently is <Token>, which for testing purposes, just prints out the JWT token returned from the API. Essentially what happens here is:
The accessToken, idToken, oid are automatically sent to my API for validation server-side as well.
When they are validated, the user is checked against the DB... if they exist, send a JWT... if not, add them and send a JWT.
JWT is saved to sessionStorage and is used subsequently for client-API communication.
The problem I'm running into my API is getting queried four or more times:
This is what I have so far:
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import { PublicClientApplication } from '#azure/msal-browser';
import { MsalProvider } from '#azure/msal-react';
import { msalConfig } from './utils/authConfig';
// Instantiate MSAL that encapsulates the entire app
const msalInstance = new PublicClientApplication(msalConfig);
ReactDOM.render(
<React.StrictMode>
<MsalProvider instance={msalInstance}>
<App />
</MsalProvider>
</React.StrictMode>,
document.getElementById('root')
);
// App.tsx
import React, { useEffect } from 'react';
import {
AuthenticatedTemplate,
useMsal,
useAccount,
useIsAuthenticated,
} from '#azure/msal-react';
import { graphConfig, loginRequest } from '../utils/authConfig';
import { InteractionStatus } from '#azure/msal-browser';
import axios from 'axios';
const Token = (props: any) => {
if (props.account) {
props.instance
.acquireTokenSilent({
...loginRequest,
account: props.account,
})
.then((response: any) => {
axios
.post('/api/v2/auth/aad_token/validate/', {
access_token: response.accessToken,
id_token: response.idToken,
oid: response.uniqueId,
})
.then((response) => {
console.log(response.data);
})
.catch((error) => {
console.log(error);
});
});
}
return <div>Test</div>;
};
const App = () => {
const isAuthenticated = useIsAuthenticated();
const { instance, inProgress, accounts } = useMsal();
const account = useAccount(accounts[0] || {});
useEffect(() => {
if (inProgress === InteractionStatus.None && !isAuthenticated) {
instance.loginRedirect(loginRequest);
}
});
return (
<div className='App'>
<AuthenticatedTemplate>
<Token account={account} instance={instance} />
</AuthenticatedTemplate>
</div>
);
};
export default App;
Suggestions for how to limit this?

This fixed it for me... forgot the dependency array:
const Token = () => {
const { accounts, instance } = useMsal();
const account = useAccount(accounts[0] || {});
useEffect(() => {
// Check if account object exists
if (account) {
// If it does then acquire an accessToken
instance
.acquireTokenSilent({
...loginRequest,
account: account,
})
.then((response: any) => {
axios
.post('/api/v2/auth/aad_token/validate/', {
access_token: response.accessToken,
id_token: response.idToken,
oid: response.uniqueId,
})
.then((response) => {
console.log(response.data);
})
.catch((error) => {
console.log(error);
});
});
}
}, []);
};

Related

How can I setup a whitelist of users for access to a React website?

I have created a website for my son's school class. I am using React with Firebase and have got all the authentication through Social Media sorted.
However I want to have a table in Firebase of permitted users, which will check the social media login and see if that person is able to access the website. I have the table called permitted (which is just a list of usernames) and I created the following function to check to see if that user is authorized to access the site:
const isWhitelisted = async (username: string) => {
let result: boolean = false;
if (username)
{
// check the user against the whitelist of approved people
projectFirestore.collection("permitted")
.where("username", "==", username)
.get()
.then((snapshot) => {
snapshot.forEach((doc) => {
if (doc.data().enabled)
{
result = true;
}
})
})
}
return result;
}
The problem I have is that I am not sure when to call this function after authentication.
I have an auth module which exports SignIn:
import firebase from 'firebase';
import {auth} from '../../config/firebase';
const SignIn = (provider: firebase.auth.AuthProvider) =>
new Promise<firebase.auth.UserCredential>((resolve, reject) => {
auth.signInWithPopup(provider)
.then(result => resolve(result))
.catch(error => reject(error));
});
export {SignIn};
And a LoginPage.tsx that allows people to log into the site:
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import firebase from "firebase";
import IPageProps from "../../interfaces/page.interface";
import { SignIn } from "../../modules/auth";
import { Providers } from "../../config/firebase";
import Title from '../../comps/Title';
import SiteNavbar from "../../comps/Navbar";
import { projectFirestore } from "../../config/firebase";
const LoginPage: React.FC<IPageProps> = props => {
const [authenticating, setAuthenticating] = useState<boolean>(false);
const [error, setError] = useState<string>('');
const history = useHistory();
const signIn = (provider: firebase.auth.AuthProvider) => {
if (error !== "") setError("");
setAuthenticating(true);
SignIn(provider)
.then(result => {
history.push("/photos");
})
.catch(error => {
setAuthenticating(false);
setError(error.message);
})
}
return (
<div>
<SiteNavbar />
<div className="AuthLogin">
<div className="auth-main-container">
<div>
<h1>Welcome to Website</h1>
</div>
<div className="auth-btn-wrapper">
<button
disabled={authenticating}
onClick={() => signIn(Providers.google)}
>
Login in with Google
</button>
</div>
</div>
</div>
</div>
)
}
export default LoginPage;
And then I have a UserPrivider.tsx that handles the authentication state change:
import React, { Component, createContext } from "react";
import firebase from "firebase/app";
import { auth, generateUserDocument } from "../config/firebase";
const UserContext = createContext<firebase.User | null>(null);
class UserProvider extends Component {
state = {
user: null
};
componentDidMount = async () => {
auth.onAuthStateChanged(async userAuth => {
const user = await generateUserDocument(userAuth);
this.setState({
user
})
})
}
render() {
const { user } = this.state;
return (
<UserContext.Provider value={user}>
{this.props.children}
</UserContext.Provider>
)
}
}
export { UserProvider, UserContext };
I thought the best place to put the isWhitelist would be after the then(result) function in the LoginPage.tsx but then I have bumped into asynchronous issues.
Have I taken the correct approach here is is there a better more recognosed way of dealing with authorization that I have completely missed?
You cannot prevent anyone from logging in but you can restrict their access to your Firestore using security rules. You can check if the user is present in your permitted collection as shown below.
service cloud.firestore {
match /databases/{database}/documents {
match /collection/{doc} {
allow create: if request.auth != null && exists(/databases/$(database)/documents/permitted/$(request.auth.uid))
}
}
}
This rule will allow user to create a document in the collection only if a document with user's UID as the key in "permitted" collection.
However you cannot check this in storage rules or realtime databases so I'd recommend using Custom Claims. You can access these in security rules by using request.auth.token.<claim_name>.
All this is necessary as any frontend redirects or conditional rendering can be bypassed. On the frontend, the best you can do is when the user logs in, check if their UID is present in your permitted collection or if they have the permitted custom claim which can be done like this:
firebase.auth().onAuthStateChanged(async (user) => {
if (user) {
// User is signed in,
// 1] Checking Custom Claims
// const {claims} = await user.getIdTokenResult()
// if (!claims.permitted) { // redirect and force logout }
// 2] checking in permitted collection
const permittedRef = firebase.firestore().collection("permitted").doc(user.uid)
if (!(await permittedRef.get()).exists) { // Redirect and force logout }
} else {
// User is signed out
// Redirect to login page
}
});
You can use the get() method in Firestore security rules to read document and validate the data.
allow read: get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true;
If you want to read data from the document that you are reading, then you can access the data like this:
match /collection/{docID} {
allow read: if resource.data.field == 'value';
}

how to successfully login to application using okta idp initiated login

I have created react application and using okta sample provided at below for login using okta.
https://developer.okta.com/code/react/okta_react/#add-an-openid-connect-client-in-okta
I am able to login to application successfully from my application by entering credentials explictly and click on login button.
but if i login to okta dev account and then if I navigate to my application my application is not recognizing the existing session.
below is my login component,
import React, { useEffect } from 'react';
import { Redirect } from 'react-router-dom';
import { useSelector, useDispatch } from "react-redux";
import OktaSignInWidget from '../../Shared/oktaSignInWidget/OktaSignInWidget';
import { useOktaAuth } from '#okta/okta-react';
import * as sharedActions from '../../Shared/data/actions';
const Login = ({ config }) => {
const { oktaAuth, authState } = useOktaAuth();
const dispatch = useDispatch();
useEffect(() => {
dispatch(sharedActions.setCurrentComponent('login'));
// console.log('authState.isPending :', authState.isPending);
// console.log('authState.isAuthenticated :', authState.isAuthenticated);
}, []);
useEffect(() => {
if (!authState.isPending) {
console.log(' authState.isPending :', authState.isPending);
console.log(' authState.isAuthenticated :', authState.isAuthenticated);
}
}, [authState]);
const onSuccess = (tokens) => {
console.log('tokens :', tokens);
oktaAuth.handleLoginRedirect(tokens);
};
const onError = (err) => {
console.log('error logging in', err);
};
if (authState.isPending) return null;
return authState.isAuthenticated ?
<Redirect to={{ pathname: '/' }} /> :
<OktaSignInWidget
config={config}
onSuccess={onSuccess}
onError={onError} />;
};
export default Login;
useEffect(() => {
// check to see if the user already has an okta session
oktaAuth.session.exists().then((response) => {
console.log(' response : ', response);
setCheckingSession(false);
if (response) {
// oktaAuth.token.getWithRedirect();
oktaAuth.token.getWithoutPrompt().then((response) => {
console.log('response tokens : ', response);
oktaAuth.tokenManager.setTokens(response.tokens);
});
}
});
}, [oktaAuth.session, oktaAuth.token]);

React Hook and Context not persisting on redirect

I am still navigating React hooks and contexts and have been attempting to use both to store user information for further usage in other portions of my app after successful authentication from an axios request. With my code that follows, I successfully set the state used in the context, but when the state is accessed following a redirect that occurs directly after setting the value, it comes back as undefined and I'm not sure what is preventing the value from being stored.
Provided is my context and hook (AppSession.js):
import React, { createContext, useContext, useState } from 'react'
export const SessionContext = createContext(null);
const AppSession = ({ children }) => {
const [user, setUser] = useState()
if (user){
console.log("useState: Authenticated")
console.log(user)
} else {
console.log("useState: Not authenticated")
console.log(user)
}
return (
<SessionContext.Provider value={{user, setUser}}>
{children}
</SessionContext.Provider>
)
}
export const getUserState = () => {
const { user } = useContext(SessionContext)
return user;
}
export const updateUserState = () => {
const { setUser } = useContext(SessionContext)
return (user) => {
setUser(user);
}
}
export default AppSession;
**Provided is the axios request and console logs upon successful response (login.js):**
axios.post(
'/api/auth/signin/',
{ email, password },
{
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
}).then((res) => {
console.log(res.data) // {authenticated: true, user_id: "071c7b80-6b4d-462c-8c4a-4fa613a7e8b6", user_email: "Alysson_Runolfsdottir#yahoo.com"}
const data = res.data; //
console.log("updateUserState")
setUser(data)
}).then(()=> {
return window.location = '/app/profile/'
}).catch((err) => {
console.log(err)
})
// Console.logs
{ authenticated: true, user_id: "071c7b80-6b4d-462c-8c4a-4fa613a7e8b6", user_email: "Alysson_Runolfsdottir#yahoo.com" } // login.js
updateUserState // login.js
useState: Authenticated // AppSession.js
{ authenticated: true, user_id: "071c7b80-6b4d-462c-8c4a-4fa613a7e8b6", user_email: "Alysson_Runolfsdottir#yahoo.com" } // AppSession.js
Then the code for profile.js which is the result of redirect to /app/profile with console logs:
import React from 'react'
import { getUserState } from '../../contexts/AppSession'
import Layout from '../../components/Universal/Layout'
export default function Profile(props) {
const checkUser = getUserState()
console.log(checkUser)
console.log(props)
return (
<Layout
title="Signin"
description="TEST"
>
<h1>Protected Page</h1>
<p>You can view this page because you are signed in.</p>
<br />
<b>Check User: {checkUser}</b>
</Layout>
)
}
// Console.logs
useState: Not authenticated // AppSession.js
undefined // AppSession.js (console.log(user))
undefied // profile.js (console.log(checkUser))
As you can see the storage is short-lives as the subsuquent page that loads upon redirect access the user state and it is undefined. Any idea why this might be?

aws cognito - how to keep the id token refresh at the right time in frontend

Problem:
Every time when I log in, the id token which is obtained by Auth.signIn will be store in localStorage.
After I login, UI make requests which require Authorization(use id token),
but it failed every time.
I tried to copy the id token in localStorage and tried the same API request in Postman,
below error message shown.
the incoming token has expired
But When I reload the page, the request is sent successfully and receive ok response.
I am not sure whether it's because the token refreshing logic is not correct in my code.
I just put the token refreshing logic in App.js componentDidMount().
The logic is based on below post.
how handle refresh token service in AWS amplify-js
Can someone let me know what's wrong of my code?
Index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
//aws
import Amplify from 'aws-amplify';
import config from './config.json'
const Index = () => {
Amplify.configure({
Auth: {
mandatorySignId: true,
region: config.cognito.REGION,
userPoolId: config.cognito.USER_POOL_ID,
userPoolWebClientId: config.cognito.APP_CLIENT_ID
}
});
return(
<React.StrictMode>
<App/>
</React.StrictMode>
)
}
ReactDOM.render(
<Index />,
document.getElementById('root')
);
serviceWorker.unregister();
App.js
import React, { Component } from 'react';
import { Switch, Route } from 'react-router-dom';
import { Redirect } from 'react-router';
import { withRouter } from 'react-router-dom';
import config from './config.json'
//Screen
import Login from './screen/auth/Login'
import Drawer from './components/Drawer'
import { Auth } from 'aws-amplify';
const AmazonCognitoIdentity = require('amazon-cognito-identity-js');
const CognitoUserPool = AmazonCognitoIdentity.CognitoUserPool;
class App extends Component {
state = {
isAuthenticated: false,
isAuthenticating: true,
user: null
}
setAuthStatus = authenticated =>{
this.setState({isAuthenticated: authenticated})
}
setUser = user =>{
this.setState({ user: user})
}
handleLogout = async () =>{
try{
Auth.signOut();
this.setAuthStatus(false);
this.setUser(null)
localStorage.removeItem('jwtToken')
localStorage.removeItem('idToken')
this.props.history.push('/')
}catch(error){
console.log(error)
}
}
tokenRefresh(){
const poolData = {
UserPoolId : config.cognito.USER_POOL_ID, // Your user pool id here,
ClientId : config.cognito.APP_CLIENT_ID// Your client id here
};
const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
const cognitoUser = userPool.getCurrentUser();
cognitoUser.getSession((err, session) =>{
const refresh_token = session.getRefreshToken();
cognitoUser.refreshSession(refresh_token, (refErr, refSession) => {
if (refErr) {
throw refErr;
}
else{
localStorage.setItem('jwtToken',refSession.idToken.jwtToken)
localStorage.setItem('idToken',JSON.stringify(refSession.idToken))
}
});
})
}
async componentDidMount(){
try{
const session = await Auth.currentSession();
this.setAuthStatus(true);
const user = await Auth.currentAuthenticatedUser();
this.setUser(user);
}catch(error){
console.log(error);
}
// check if the token need refresh
this.setState({isAuthenticating: false})
let getIdToken = localStorage.getItem('idToken');
if(getIdToken !== null){
let newDateTime = new Date().getTime()/1000;
const newTime = Math.trunc(newDateTime);
const splitToken = getIdToken.split(".");
const decodeToken = atob(splitToken[1]);
const tokenObj = JSON.parse(decodeToken);
const newTimeMin = ((newTime) + (5 * 60)); //adding 5min faster from current time
if(newTimeMin > tokenObj.exp){
this.tokenRefresh();
}
}
}
render(){
const authProps = {
isAuthenticated: this.state.isAuthenticated,
user: this.state.user,
setAuthStatus: this.setAuthStatus,
setUser: this.setUser
}
return (
!this.state.isAuthenticating &&
<React.Fragment>
{this.state.isAuthenticated ?
<Drawer props={this.props} auth={authProps} handleLogout={this.handleLogout} onThemeChange={this.props.onThemeChange} /> :
<Switch>
<Redirect exact from='/' to='/login'/>
<Route path='/login' render={(props)=> <Login {...props} auth={authProps}/>} />
</Switch>
}
</React.Fragment>
);
}
}
export default withRouter(App);
Login.js
import React, { useState } from 'react';
import TextField from '#material-ui/core/TextField';
import withStyles from '#material-ui/core/styles/withStyles';
import _ from 'lodash';
import { Auth } from "aws-amplify";
function Login(props) {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (event) => {
event.preventDefault();
const payload = {
"username": username,
"password": password
}
// aws login
try{
const signInResponse = await Auth.signIn(payload.username,payload.password)
console.log(signInResponse)
props.history.push("/home")
props.auth.setAuthStatus(true)
props.auth.setUser(signInResponse)
localStorage.setItem('jwtToken',signInResponse.signInUserSession.idToken.jwtToken)
localStorage.setItem('idToken',JSON.stringify(signInResponse.signInUserSession.idToken))
}catch(error){
console.log(error)
}
}
return(
<form onSubmit={handleSubmit}>
<TextField
name='username'
value="username"
...
/>
<TextField
name='password'
value="password"
...
/>
</form>
);
}
export default withStyles(styles)(Login);
Why do you want to refresh token yourself as AWS Amplify handle it for you?
The documentation states that:
When using Authentication with AWS Amplify, you don’t need to refresh
Amazon Cognito tokens manually. The tokens are automatically refreshed
by the library when necessary.
Amplify automatically tries to refresh if the access token has timed out (which happens after an hour). You configure the refresh token expiration in the Cognito User Pools console.
import { Auth } from 'aws-amplify';
Auth.currentSession()
.then(data => console.log(data))
.catch(err => console.log(err));
Auth.currentSession() returns a CognitoUserSession object which contains JWT accessToken, idToken, and refreshToken.
This method will automatically refresh the accessToken and idToken if tokens are expired and a valid refreshToken presented. So you can use this method to refresh the session if needed.
https://docs.amplify.aws/lib/auth/manageusers/q/platform/js#managing-security-tokens
https://docs.amplify.aws/lib/auth/manageusers/q/platform/js#retrieve-current-session
I built an app with the tutorial here: https://aws.amazon.com/getting-started/hands-on/build-serverless-web-app-lambda-apigateway-s3-dynamodb-cognito.
In the example application provided at s3://wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website there is a file cognito-auth.js. In that file there is:
YourApp.authToken = new Promise(function fetchCurrentAuthToken(resolve, reject) {…
This does not refresh the auth token until the entire page is reloaded. I ran into issues when the page was loaded for a long time (hours) and only ajax calls were made. After the auth token that was loaded at the page load expired, the authorized ajax calls to my API returned 401 errors.
To resolve this, I added an async function wrapper to the promise:
YourApp.authToken = async function () {
return await new Promise(function fetchCurrentAuthToken(resolve, reject) {…
Then changed the app references from
YourApp.authToken.then(function (token) {…
to
YourApp.authToken().then(function (token) {…

How to provide functions via context in ReactJS?

I have a React UI kit and want to get some functionality to it. I also have some functionality without the UI. Both are working separately, but I cannot manage to work together due to the error
TypeError: Cannot destructure property 'authenticate' of
'Object(...)(...)' as it is undefined.
I have an account object which is the context provider (Accounts.js, shortened for brevity):
import React, { createContext } from 'react'
import { CognitoUser, AuthenticationDetails } from 'amazon-cognito-identity-js'
import Pool from 'UserPool'
const AccountContext = createContext()
const Account = (props) => {
const getSession = async () =>
await new Promise((resolve, reject) => {
...
})
const authenticate = async (Email, Password) =>
await new Promise((resolve, reject) => {
...
})
const logout = () => {
const user = Pool.getCurrentUser()
if (user) {
user.signOut()
}
}
return (
<AccountContext.Provider
value={{
authenticate,
getSession,
logout
}}
>
{props.children}
</AccountContext.Provider>
)
}
export { Account, AccountContext }
And I have SignIn.js Component which throws the error (also shortened):
import React, { useState, useEffect, useContext } from 'react';
import { Link as RouterLink, withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import validate from 'validate.js';
import { AccountContext } from 'Accounts.js';
const SignIn = props => {
const { history } = props;
const [status, setStatus] = useState(false);
const { authenticate, getSession } = useContext(AccountContext);
const classes = useStyles();
const [formState, setFormState] = useState({
isValid: false,
values: {},
touched: {},
errors: {}
});
useEffect(() => {
const errors = validate(formState.values, schema);
setFormState(formState => ({
...formState,
isValid: errors ? false : true,
errors: errors || {}
}));
getSession()
.then(session => {
console.log('Session:', session);
setStatus(true);
});
}, [formState.values]);
const handleSignIn = event => {
event.preventDefault();
authenticate(formState.values.email, formState.values.password)
.then(data => {
console.log('Logged in!', data);
//setStatus(true);
})
.catch(err => {
console.error('Failed to login!', err);
//setStatus(false);
})
history.push('/');
};
return (
<div className={classes.root}>
</div>
);
};
SignIn.propTypes = {
history: PropTypes.object
};
export default withRouter(SignIn);
I guess something is wrong with the Accounts.js because the SignIn.js cannot use the authenticate or getSession functions. I need those in the context because other components will render differently when a user is signed in and getSession exactly retrieves this info. Accounts.js is calling against AWS Cognito. I understand how to use variables or states in context but functions seem to work differently. How do I define the functions in Accounts.js to add them to the context so that I can use them in other components as well?
I have tried similar approach in my application.
As per your code, everything is looking fine. The error you have mentioned can be because of wrapping SignIn component wrongly in Provider i.e Account.
Try wrapping SignIn Component inside Account Provider like below:
Import {Account} from './Accounts.js' // Path of Account.js file
<Account> // Account act as Provider as per your code
<SignIn />
...
</Account>
Rest of your code seems fine.

Resources