react-aad-msal authProvider.getAccessToken() reloads the component indefinitely - reactjs

I have added a axios interceptor within which authProvider.getAccessToken() is called to fetch token and add to header of each request.
Here is my axiosInterceptor.js
import axios from 'axios'
import { authProvider } from '../authProvider'
export const axiosApiIntance = axios.create()
export const axiosInterceptor = axiosApiIntance.interceptors.request.use(async request => {
try {
let token = await authProvider.getAccessToken()
request.headers['Authorization'] = `Bearer ${token.accessToken}`
return request
} catch (err) {
console.log(err)
}
}, error => {
return Promise.reject(error.message)
})
Here is my authProvider.js
import { LoginType, MsalAuthProvider } from 'react-aad-msal'
// The auth provider should be a singleton. Best practice is to only have it ever instantiated once.
// Avoid creating an instance inside the component it will be recreated on each render.
// If two providers are created on the same page it will cause authentication errors.
export const authProvider = new MsalAuthProvider(
{
auth: {
authority: process.env.REACT_APP_AUTHORITY,
clientId: process.env.REACT_APP_CLIENT_ID,
postLogoutRedirectUri: process.env.REACT_APP_URL,
redirectUri: process.env.REACT_APP_URL,
validateAuthority: true,
// After being redirected to the "redirectUri" page, should user
// be redirected back to the Url where their login originated from?
navigateToLoginRequestUrl: false
},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: true
}
},
{
scopes: ['openid', 'profile', 'user.read']
},
{
loginType: LoginType.Redirect,
// When a token is refreshed it will be done by loading a page in an iframe.
// Rather than reloading the same page, we can point to an empty html file which will prevent
// site resources from being loaded twice.
tokenRefreshUri: window.location.origin + '/auth.html'
}
)
authProvider is used in App.js
<AzureAD provider={authProvider} reduxStore={configureStore}>
....
</AzureAD>
axiosInterceptor is also included in App.js.
Please provide suggestion on what could cause the component the reload indifinitely.
I have removed the authProvider.getAccessToken() and verified, it works fine. So the reload is caused due to that.

First, I suggest you to verify the Scope, authority and clientId of your AuthProvider.
I had a similar issue in one project ans I had to add the scope to the getAccessToken() function, even if I never did that in others projects..
See below:
var authenticationParameters = {
scopes: ['openid', 'profile', 'user.read'],
};
axios.interceptors.request.use(function (config): any {
return new Promise(async (resolve: any, reject: any) => {
await authProvider.getAccessToken(authenticationParameters).then((response: any) => {
config.headers["Authorization"] = "Bearer " + response.accessToken;
config.headers["Content-Type"] = "application/json";
config.headers.Accept = "application/json";
resolve(config);
})
.catch((error: any) => {
console.log(error.message);
});
});
});
Hope it help ;)
Regards

Related

How to make simple protected route using nextAuth?

I wanna make simple protected route.
I have credentials provider and nextAuth middleware. I just wanna make simple logic:
if user is logged in he can visit /profile, and if he visits /signup or /signin redirect him to /profile, and if he isnt logged he cant visit /profile and redirect him to /signin
some routes are neutral - for example he can visit /shop while being logged in or not.
there is my [...nextauth].ts
export default NextAuth({
session: {
strategy: 'jwt',
},
providers: [
CredentialsProvider({
type: 'credentials',
async authorize(credentails) {
const { password, email } = credentails as Signin
try {
const client = await connectToDatabase()
if (!client) return
const db = client.db()
const user = await existingUser(email, db)
if (!user) throw new Error('Invalid credentails!')
const isPasswordCorrect = await verifyPassword(password, user.password)
if (!isPasswordCorrect) throw new Error('Invalid credentails!')
return { email: user.email, name: user.name, id: user._id.toString() }
} catch (e: unknown) {
if (e instanceof Error) {
throw new Error(e.message)
}
}
},
}),
],
})
Apart from other answers what you can do is-
At component mount at signin and sign up check user is authenticated or not. If authenticated. use router.push to profile else be at signin/signup.
At profile again check for authentiction at component mount, if not auth push to signin else be at profile. Important thing here is don't show the layout, content of profile page before checking user is authenticated or not. Use a spiner or loader till auth check is going on.
write a middleware
const authorizedRoles = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return next(
// write logic to handle errors
new ErrorHandler(
`Role (${req.user.role}) is not allowed`,
403
)
);
}
next();
};
};
then whichever routes you want to protect, use this middleware. Then on protected pages' getServerSideProps
export async function getServerSideProps(context) {
const session = await getSession({ req: context.req });
if (!session || session.user.role !== "admin") {
return {
redirect: {
destination: "/home",
// permanent - if `true` will use the 308 status code which instructs clients/search engines to cache the redirect forever.
permanent: false,
},
};
}
return {
props: {},
};
}

Handle firebase-authentication callback without firebaseui-web (next-firebase-auth)

I have been dealing with a problem for several days and do not manage to solve it well.
I am using Firebase and NextJs in my current Project.
My current goal is to log in a user with firebase-authentication
.
However, it should also be possible to call the user in getServerSideProps and also to generate the IdToken within this.
The next-firebase-auth package makes life a lot easier. Basically, the package allows you to do just that.
When the user signs in, an endpoint '/api/login' is called to generate a refresh token and store the user info, ID token, and refresh token in cookies. Future requests to SSR pages receive the user info and ID token from cookies, refreshing the ID token as needed.
I am theoretically able to perform user.getIdToken() in getServerSideProps.
There is an example scenario within the repo, but which uses the
firebaseui-web. This enables you to make all the customizations you need.
Within this, inside the callback, signInSuccessWithAuthRes is set to false. And as I understand it, the key is here, as only then there is a redirection to '/api/login' to where the cookie is set.
However, I don't want to use the firebaseui-web as the UI looks bad.
I use the "normal way" of logging in my user with firebase.auth(), but I am not redirected to '/api/login'.
I tried to strive for the route myself, but always got an error
Error: The request is missing an Authorization header value
(I tried to set the Firebase IdToken as Bearer Authorization, but it did not work)
Is there a way to achieve this, without the firebaseui-web ?
My Approach
./pages/api/login
import { setAuthCookies } from 'next-firebase-auth'
import initAuth from '../../initAuth'
initAuth()
const handler = async (req, res) => {
try {
await setAuthCookies(req, res)
} catch (e) {
return res.status(500).json({ error: 'Unexpected error.' })
}
return res.status(200).json({ success: true })
}
export default handler
Part of my loginFuntion
const result = await firebase.auth().signInWithPopup(provider)
const token = await result.user.getIdToken(true)
await axios.post('/api/login', {
headers: {
Authorization: `Bearer ${token}`,
},
})
Example Code from next-firebase-auth GitHub Repo (FirebasAuth.js)
import React, { useEffect, useState } from 'react'
import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth'
import firebase from 'firebase/app'
import 'firebase/auth'
// Note that next-firebase-auth inits Firebase for us,
// so we don't need to.
const firebaseAuthConfig = {
signInFlow: 'popup',
// Auth providers
// https://github.com/firebase/firebaseui-web#configure-oauth-providers
signInOptions: [
{
provider: firebase.auth.EmailAuthProvider.PROVIDER_ID,
requireDisplayName: false,
},
],
signInSuccessUrl: '/',
credentialHelper: 'none',
callbacks: {
// https://github.com/firebase/firebaseui-web#signinsuccesswithauthresultauthresult-redirecturl
signInSuccessWithAuthResult: () =>
// Don't automatically redirect. We handle redirects using
// `next-firebase-auth`.
**// Thats the Part I am not able to implement in my code !!!**
false,
},
}
const FirebaseAuth = () => {
// Do not SSR FirebaseUI, because it is not supported.
// https://github.com/firebase/firebaseui-web/issues/213
const [renderAuth, setRenderAuth] = useState(false)
useEffect(() => {
if (typeof window !== 'undefined') {
setRenderAuth(true)
}
}, [])
return (
<div>
{renderAuth ? (
<StyledFirebaseAuth
uiConfig={firebaseAuthConfig}
firebaseAuth={firebase.auth()}
/>
) : null}
</div>
)
}
export default FirebaseAuth
initAuth.js (if you need)
import { init } from 'next-firebase-auth'
const TWELVE_DAYS_IN_MS = 12 * 60 * 60 * 24 * 1000
const initAuth = () => {
init({
debug: true,
authPageURL: '/auth',
appPageURL: '/',
loginAPIEndpoint: '/api/login', // required
logoutAPIEndpoint: '/api/logout', // required
// firebaseAuthEmulatorHost: 'localhost:9099',
// Required in most cases.
firebaseAdminInitConfig: {
credential: {
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
// Using JSON to handle newline problems when storing the
// key as a secret in Vercel. See:
// https://github.com/vercel/vercel/issues/749#issuecomment-707515089
privateKey: process.env.FIREBASE_PRIVATE_KEY
? JSON.parse(process.env.FIREBASE_PRIVATE_KEY)
: undefined,
},
databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
},
firebaseClientInitConfig: {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
},
cookies: {
name: 'ExampleApp',
keys: [
process.env.NEXT_COOKIE_SECRET_CURRENT,
process.env.NEXT_COOKIE_SECRET_PREVIOUS,
],
httpOnly: true,
maxAge: TWELVE_DAYS_IN_MS,
overwrite: true,
path: '/',
sameSite: 'strict',
secure: process.env.NEXT_PUBLIC_COOKIE_SECURE === 'true',
signed: true,
},
})
}

How to get session in NextJS api route using with-iron-session?

I am using with-iron-session for authentication in my NextJS app however I'm not able to access the session cookie when I make API calls in my getServerSideProps() function. The API route is unable to get the session cookie used for authentication:
Session creation:
// this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions
import { withIronSession } from "next-iron-session";
export default function withSession(handler) {
return withIronSession(handler, {
password: process.env.PASSWORD_HASH,
cookieName: "MYCOOKIE",
cookieOptions: {
// the next line allows to use the session in non-https environements like
// Next.js dev mode (http://localhost:3000)
secure: process.env.NODE_ENV === "production",
httpOnly: false,
},
});
}
My getServerSideProps call:
export const getServerSideProps = withSession(async ({ req, res }) => {
const user = req.session.get("user");
if (!user) {
return {
redirect: {
permanent: false,
destination: "/"
},
props: {}
};
}
// I've replaced 'include' with 'same-origin' but it didn't make a difference
const watchRes = await fetch('/watch',{credentials: 'include'});
const watch = await watchRes.json();
return{
props: {
user,
watch
}
}
}
the api route:
// I've added await before the (req,res) but that was just guessing at this point
export default withSession((req, res) => {
const user = req.session.get("user");
if(user){
res.send("Good");
}else{
res.status(403).end();
}
}
While being logged in and I go to my localhost/api/watch route, it shows "Good" but when I try the fetch request I get the 403.
try to add headers of the getServerSideProps request to the second call.
const headers = req.headers
const watchRes = await fetch('/watch',{headers});
in this way you have injected the cookies

How to use Auth0 with react-admin?

I'm trying to implement authentication using Auth0 in a react-admin v3 app. I need to implement an authProvider that talks with Auth0. This sounds like something that should be available somewhere, but the closest I could find was https://github.com/alexicum/merge-admin/blob/master/src/Auth/index.js, which is about 2 years old (the SDKs have changed since then).
Is there an Auth0 authProvider somewhere I can reuse, or do I have to implement it myself?
Thanks!
for reference sake, here's an example of a way to integrate react admin with auth0-react package
index.js
import { Auth0Provider } from "#auth0/auth0-react";
ReactDOM.render(
<Auth0Provider
domain="XXXXX.auth0.com"
clientId="XXXXX"
audience="https://XXXXX"
redirectUri={window.location.origin}
>
<React.StrictMode>
<App />
</React.StrictMode>
</Auth0Provider>,
document.getElementById("root")
);
App.js
import { withAuth0, withAuthenticationRequired } from "#auth0/auth0-react";
import ApolloClient from "apollo-boost";
// I'm using Hasura w/ JWT Auth, so here's an example of how to set Authorization Header
async componentDidMount() {
const token = await this.props.auth0.getAccessTokenSilently();
const client = new ApolloClient({
uri: "https://HASURA_URL/v1/graphql",
headers: {
Authorization: `Bearer ${token}`
},
});
buildHasuraProvider({ client }).then((dataProvider) =>
this.setState({ dataProvider })
);
}
export default withAuthenticationRequired(withAuth0(App));
I've created a sample application with Auth0 and react-admin way of auth
https://github.com/spintech-software/react-admin-auth0-example
Here is auth provider code for reference
import authConfig from "./authConfig";
import {Auth0Client} from '#auth0/auth0-spa-js';
const auth0 = new Auth0Client({
domain: authConfig.domain,
client_id: authConfig.clientID,
cacheLocation: 'localstorage',
useRefreshTokens: true
});
const CallbackURI = "http://localhost:3000/login"
export default {
// called when the user attempts to log in
login: (url) => {
if (typeof url === 'undefined') {
return auth0.loginWithRedirect({
redirect_uri: CallbackURI
})
}
return auth0.handleRedirectCallback(url.location);
},
// called when the user clicks on the logout button
logout: () => {
return auth0.isAuthenticated().then(function (isAuthenticated) {
if (isAuthenticated) { // need to check for this as react-admin calls logout in case checkAuth failed
return auth0.logout({
redirect_uri: window.location.origin,
federated: true // have to be enabled to invalidate refresh token
});
}
return Promise.resolve()
})
},
// called when the API returns an error
checkError: ({status}) => {
if (status === 401 || status === 403) {
return Promise.reject();
}
return Promise.resolve();
},
// called when the user navigates to a new location, to check for authentication
checkAuth: () => {
return auth0.isAuthenticated().then(function (isAuthenticated) {
if (isAuthenticated) {
return Promise.resolve();
}
return auth0.getTokenSilently({
redirect_uri: CallbackURI
})
})
},
// called when the user navigates to a new location, to check for permissions / roles
getPermissions: () => {
return Promise.resolve()
},
};
My answer is following react-admin approach where I use its authProvider like below. There are two main steps:
Get needed data from useAuth0 hook.
Convert authProvider into function where it takes the above values, and return an object like default.
// In App.js
import authProvider from './providers/authProvider';// my path is changed a bit
const App = () => {
const {
isAuthenticated,
logout,
loginWithRedirect,
isLoading,
error,
user,
} = useAuth0();
const customAuthProvider = authProvider({
isAuthenticated,
loginWithRedirect,
logout,
user,
});
return (
<Admin
{...otherProps}
authProvider={customAuthProvider}
>
{...children}
</Admin>
);
}
// My authProvider.js
const authProvider = ({
isAuthenticated,
loginWithRedirect,
logout,
user,
}) => ({
login: loginWithRedirect,
logout: () => logout({ returnTo: window.location.origin }),
checkError: () => Promise.resolve(),
checkAuth: () => (isAuthenticated ? Promise.resolve() : Promise.reject()),
getPermissions: () => Promise.reject('Unknown method'),
getIdentity: () =>
Promise.resolve({
id: user.id,
fullName: user.name,
avatar: user.picture,
}),
});
export default authProvider;
That's it.
It's more convenient to wrap the react-admin app with auth0 native login, and then provide react-admin dataProvider an http client that reads the jwt token stored in local storage by auth0.

'AADSTS500011' error message returned from API call using adalFetch

I have a React application that is registered in Azure Active Directory. In the API Permissions section, I have added permissions to access the API I am trying to access.
I am using the react-adal package to handle login and storage of access tokens when the user enters the app. My understanding is that the access token for the API is created at this point and adalFetch handles the logistics during the call to the API.
The response from the API is an error object (I replaced the actual id's; yes they match exactly and are correct in AAD):
{
message: "AADSTS500011: The resource principal named https://<domain>.onmicrosoft.com/APP_ID/access_as_user was not found in the tenant named TENANT. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant."
msg: "invalid_resource"
}
I have searched high and low to find a solution to why this isn't working. There is documentation on the API, but none specifying a resource or anything beyond the various endpoints i.e. http://thing-api.azurewebsites.net/api/endpointGoesHere
The API page states:
To use the API, apps need to implement modern authentication (OIDC) using AzureAD (AAD) and then request a token from AAD for the API.
The app id in Azure is https://domain.onmicrosoft.com/APP_ID and requires the “access_as_user” scope.
adalConfig.js
import { AuthenticationContext, adalFetch, withAdalLogin } from 'react-adal';
export const adalConfig = {
clientId: CLIENT_ID,
tenant: TENANT,
endpoints: {
thingApi: 'https://<domain>.onmicrosoft.com/APP_ID/access_as_user',
graphApi: 'https://graph.microsoft.com',
},
cacheLocation: 'localStorage',
};
export const authContext = new AuthenticationContext(adalConfig);
export const adalApiFetch = (fetch, url, options) =>
adalFetch(authContext, adalConfig.endpoints.thingApi, fetch, url, options);
export const adalGraphFetch = (fetch, url, options) =>
adalFetch(authContext, adalConfig.endpoints.graphApi, fetch, url, options);
Function for the API call. Executed in componentDidMount.
TrainLanding.jsx
//Returns error
fetchData = () => {
adalApiFetch(fetch, 'http://thing-api.azurewebsites.net/api/EventGet', {})
.then((response) => {
response.json()
.then((responseJson) => {
this.setState({ apiResponse: JSON.stringify(responseJson, null, 2) }, () => {
console.log(this.state.apiResponse)
})
});
})
.catch((error) => {
console.error(error);
})
}
//works perfectly fine
fetchGraph = () => {
adalGraphFetch(fetch, 'https://graph.microsoft.com/v1.0/me', {})
.then((response) => {
response.json()
.then((responseJson) => {
this.setState({ apiResponse: JSON.stringify(responseJson, null, 2) }, () => {
console.log(this.state.apiResponse)
})
});
})
.catch((error) => {
console.error(error);
})
}
I set up a graph API call in the exact same way to test the method, and it works perfectly fine. So I know adal is set up correctly, I just don't understand the error and where I am going wrong. My googling has not yielded any useful results.
Ok, so if you're here, some things to note:
Don't use ADAL. Use MSAL. ADAL is v1 and does not work. Read here for examples: https://www.npmjs.com/package/react-aad-msal
You should wrap your entire app inside the component you get from above. I will show how I did it below.
You must have already registered your app in Azure Active Directory, configured redirect URLs, and included API permissions.
index.js
import { AzureAD, MsalAuthProviderFactory, LoginType } from 'react-aad-msal';
import { msalConfig, authParams } from './msalConfig';
class Index extends Component {
state = {
userInfo: null,
}
userJustLoggedIn = (accInfo) => {
this.setState({
userInfo: accInfo
})
}
render() {
return(
<AzureAD
provider={
new MsalAuthProviderFactory(msalConfig, authParams, LoginType.Redirect)
}
forceLogin={true}
accountInfoCallback={this.userJustLoggedIn}
>
<HashRouter>
<App userInfo={this.state.userInfo}/>
</HashRouter>
</AzureAD>
);
}
}
ReactDOM.render(
<Index/>, document.getElementById('root')
);
This might not be what your index looks like if you are using the most recent version of Create React App. I converted the Index into a component for a couple of reasons. First, the authentication loop for me was getting stuck 1 refresh short when redirecting. Second, so I could store the logged in user's info in state, update with setState (which forces another render), and then pass it as a prop to the rest of my app.
msalConfig.js
export const msalConfig = {
auth: {
authority: process.env.REACT_APP_AUTHORITY, //this should be "https://login.microsoftonline.com/<your-tenant-id>"
clientId: process.env.REACT_APP_CLIENT_ID, //just "<your-client-id>"
redirectUri: process.env.REACT_APP_REDIRECT //"<url of your app or localhost port you dev on>"
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
export const authParams = {
//can be whatever api scopes you need here **as long as they are from the same API address**
scopes: [
'https://graph.microsoft.com/User.ReadBasic.All',
'https://graph.microsoft.com/email',
'https://graph.microsoft.com/profile',
'https://graph.microsoft.com/User.Read'
],
extraScopesToConsent: [
//any non Microsoft Graph API scopes go here for this example
'any extra strings of APIs to consent to'
]
}
Read above env files and variables here: https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#what-other-env-files-can-be-used
I have a .env.development and a .env.production with the proper redirect URLs for each.
After you have authenticated the user, you can access the API.
You need to acquire a token silently before each API call and use the token in the request. For me it looks like this:
const authProvider = new MsalAuthProviderFactory(msalConfig, authParams);
console.log(authProvider)
authProvider.getAuthProvider().UserAgentApplication.acquireTokenSilent(authParams)
.then((res) => {
axios({
headers: {
'Authorization': 'Bearer ' + res.accessToken
},
method: 'GET',
url: "api address"
})
.then((response) => {
//do stuff with response
console.log(response)
})
.catch((error) => {
console.log('axios fail: ' + error)
})
})
.catch((error) => {
console.log('token fail: ' + error)
})
I put this into a function and called during componentDidMount.
I will update if anything changes. I hope this helps someone.

Resources