Open DocCaliban opened 5 years ago
My understanding is that MSAL hasn't fully addressed the forgot password workflows at the moment. My knowledge could be outdated, but last I read the most reasonable approach seemed to be logging the user out when their token failed to be renewed. https://github.com/Azure-Samples/active-directory-b2c-javascript-msal-singlepageapp/issues/9
I'm not very familiar with this workflow though, so I'm not the best person to comment. @DocCaliban, if you haven't already, would you be willing to scan the MSAL issues and assess what the current state of this is? If they have something ready then I think it could be included back into the AzureAD
component.
I just ended up writing this code to hack my way around resetPassword handling for the redirect loginType:
if (window.location.href.indexOf('AADB2C90118') > -1) {
window.localStorage.setItem('resetPassword', true)
providerFactory = new MsalAuthProvider(
siteData.msal.config,
siteData.msal.authenticationParameters,
LoginType.Redirect
);
} else if (window.localStorage.getItem('resetPassword')) {
window.localStorage.removeItem('resetPassword');
const config = { ...siteData.msal.config, auth: { ...siteData.msal.config.auth, authority: "https://sbnkfm.b2clogin.com/tfp/sbnkfm.onmicrosoft.com/B2C_1_pwd" } };
providerFactory = new MsalAuthProvider(
config,
siteData.msal.authenticationParameters,
LoginType.Redirect
);
} else {
providerFactory = new MsalAuthProvider(
siteData.msal.config,
siteData.msal.authenticationParameters,
LoginType.Redirect
);
}
I ended up with following solution,
ReactDOM.render(
<Router>
<Route exact path="/" render={(): JSX.Element => (
// Login/SignUp
<AzureAD provider={authProvider.authProviderWithSignUpSignInPolicy}>
{(props: IAzureADFunctionProps): JSX.Element => {
if (props.error && props.error.errorMessage.search("AADB2C90118") !== -1) {
// Forgot password
return <AzureAD forceLogin provider={authProvider.authProviderWithPassResetPolicy} />;
}
return <App {...props} />;
}}
</AzureAD>
)} />
</Router>,
document.getElementById("root"));
Has anyone found a workaround for this issue when using the redirect flow. When the reset password flow is finished on the azure side, it redirects back to my React app
/b2c_1_passwordreset/oauth2/v2.0/authorize
/b2c_1_signin/oauth2/v2.0/authorize
causing the refresh token iframe to fail with ` in a frame because it set 'X-Frame-Options' to 'deny'.
I ended up with following solution,
ReactDOM.render( <Router> <Route exact path="/" render={(): JSX.Element => ( // Login/SignUp <AzureAD provider={authProvider.authProviderWithSignUpSignInPolicy}> {(props: IAzureADFunctionProps): JSX.Element => { if (props.error && props.error.errorMessage.search("AADB2C90118") !== -1) { // Forgot password return <AzureAD forceLogin provider={authProvider.authProviderWithPassResetPolicy} />; } return <App {...props} />; }} </AzureAD> )} /> </Router>, document.getElementById("root"));
HI, thanks for you answer. The code above doesn't work for me. It couldn't redirect to Password Reset page, it redirects back to the login page. My solution is:
const storageError = window.localStorage.getItem("msal.error.description");
if (storageError && storageError.search("AADB2C90118") !== -1) {
// Forgot password
return <AzureAD forceLogin provider={AuthProvider.buildPasswordResetAuthority(authInfo)} />;
}
Maybe you have any thoughts about this behavior.
For anyone looking for a solution, this is what I did index.js
let providerFactory;
if (window.localStorage.getItem("resetPassword")) {
window.localStorage.removeItem("resetPassword");
providerFactory = authProvider.authProviderWithPassResetPolicy;
} else {
providerFactory = authProvider.authProviderWithSignUpSignInPolicy;
}
<AzureAD provider={providerFactory} reduxStore={store} forceLogin={true}>
{props => {
const { authenticationState, error } = props;
if (error && error.errorMessage.search("AADB2C90118") !== -1) {
window.localStorage.setItem("resetPassword", true);
}
switch (authenticationState) {
case AuthenticationState.Authenticated:
return (
<PersistGate loading={null} persistor={persistor}>
<ConnectedRouter history={history}>
<App {...props} />
</ConnectedRouter>
</PersistGate>
);
case AuthenticationState.InProgress:
return <p>...loading</p>;
default:
return <p></p>;
}
}
}
</AzureAD>
@adomrockie I have similar implementation to perform reset password. However at the end of the successful reset password I want the user to get into my portal and start using it without having to go back to Sign In page.
The problem seems to be the auto renewal of token is failing with following error
Refused to display 'https://testdomainmasked.b2clogin.com/testdomainmasked.onmicrosoft.com/b2c_1a_passwordreset/oauth2/v2.0/authorize?response_type=token&scope=https%3A%2F%2Ftestdomainmasked.onmicrosoft.com%2Fapi%2Fuser_impersonation%20openid%20profile&client_id=masked5&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Freset.html&state=eyJpZCI6ImJmNzVmMjFmLWE1N2UtNDk0Ny04MmRlLWJmMDA3MGNjMGI3MyIsInRzIjoxNTkwMzcyNjMwLCJtZXRob2QiOiJzaWxlbnRJbnRlcmFjdGlvbiJ9&nonce=36ab17e9-30b6-4a41-b8d1-150e8a4991bc&client_info=1&x-client-SKU=MSAL.JS&x-client-Ver=1.3.1&client-request-id=7c84b816-7946-4bff-9097-8959a5b29543&prompt=none&response_mode=fragment' in a frame because it set 'X-Frame-Options' to 'deny'.
And then fails with [ERROR] ClientAuthError: URL navigated to is https://testdomainmasked.b2clogin.com/testdomainmasked.onmicrosoft.com/b2c_1a_localdev_passwordreset/oauth2/v2.0/authorize?response_type=token&scope=https%3A%2F%2Ftestdomainmasked.onmicrosoft.com%2Fapi%2Fuser_impersonation%20openid%20profile&client_id=masked&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Freset.html&state=eyJpZCI6ImJmNzVmMjFmLWE1N2UtNDk0Ny04MmRlLWJmMDA3MGNjMGI3MyIsInRzIjoxNTkwMzcyNjMwLCJtZXRob2QiOiJzaWxlbnRJbnRlcmFjdGlvbiJ9&nonce=36ab17e9-30b6-4a41-b8d1-150e8a4991bc&client_info=1&x-client-SKU=MSAL.JS&x-client-Ver=1.3.1&client-request-id=7c84b816-7946-4bff-9097-8959a5b29543&prompt=none&response_mode=fragment, Token renewal operation failed due to timeout.
Has anyone done reset password/forgot password through the library without having to redirect the user to Sign in page after resetting the password?
I think there is a bug in the getAccessToken call, if you remove this function from your api function, you will notice your current function works fine. There are a few raised bug in regards with this issue. Hope it helps.
@adomrockie Thanks. You are right the getAccessToken does trigger the refresh/renewal flow and gets into the errors listed before. I have seen similar issues raised in the MSAL repo. I might chase up some relevant one there. Fixing this in MSAL would make the user experience whole lot better without having to redirect user to Sign In after successful reset password.
My solution is just to dig into the exposed MSAL functions and do the following:
authProvider.handleRedirectCallback((error, response) => {
if (error && error.errorMessage.indexOf('AADB2C90118') > -1) {
authProvider.loginRedirect({
authority: 'https://<tenant>.b2clogin.com/<tenant>.onmicrosoft.com/B2C_1A_PasswordReset',
redirectUri: window.location.origin,
extraQueryParameters: {
// eslint-disable-next-line @typescript-eslint/camelcase
ui_locales: 'de',
},
});
}
});
I think my sample should be extended to catch the returned token after Password Reset to prevent a second login prompt to the user. Maybe I'll have time to look at that within the next few days.
Hope that helps.
I used the functions exposed by MsalAuthProvider.
// authProvider.js
import { MsalAuthProvider } from 'react-aad-msal';
// Msal Configurations
const config = { ... }
// Authentication Parameters
const authenticationParameters = { ... }
// Options
const options = { ... }
// Forgot Password Handler
function handleForgotPassword(error) {
if (error && error.errorMessage.indexOf('AADB2C90118') > -1) {
authProvider.setAuthenticationParameters ({authority: <PASSWORD RESET AUTHORITY>})
authProvider.login()
}
}
const authProvider = new MsalAuthProvider(config, authenticationParameters, options)
authProvider.registerErrorHandler(handleForgotPassword)
export default authProvider
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { AzureAD } from 'react-aad-msal';
import App from './App';
import authProvider from './authProvider';
authProvider.setAuthenticationParameters ({authority: <SISO AUTHORITY>})
ReactDOM.render(
<AzureAD provider={authProvider} >
<App />
</AzureAD>,
document.getElementById('root'),
);
Thank you very much for your workaround @Premkumar-Shanmugam, it works perfectly.
I just wish we had a way to do it 'natively' without having so many redirects back and forth...
Thank you for the examples, @Premkumar-Shanmugam approach works well, although I needed to extend the handleError function with another if:
if (error && error.errorMessage.indexOf('AADB2C90091') > -1) {
authProvider.setAuthenticationParameters({
authority: b2cPolicies.authorities.signIn.authority,
});
authProvider.login();
}
Without this, when user tried to cancel the reset flow msal redirected back to a blank page without calling the login again.
Hope this solution might help some people till the library gets a fix
authProvider.registerErrorHandler
and change authority
to reset password policy by using authProvider.setAuthenticationParameters({ authority: resetPolicyURL})
.
authProvider.registerErrorHandler(error => {
if ( error.errorMessage.indexOf('AADB2C90091') > -1 ) {
// reset authority to reset password policy
}
if ( error.errorMessage.indexOf('AADB2C90077') > -1 ) { // may be may not be needed
// reset authority to signin policy
}
if (error.errorCode === 'token_renewal_error') {
// reset authority to signin policy
authProvider.signin() // if you want user to directly go to sign in
window.location.reload() // if you want user to click sign in manually by clicking
}
})
cache: {
...,
storeAuthStateInCookie: true // to be able to clear cookies later
}
payload: {
account: {
idToken: {
tfp: 'resetpasswordpolicy'
}
}
}
if tfp satisfies resetpasswordpolicy based token, then use authProvider and make a call authProvider.logout()
msal*
. This is needed otherwise after login you will get 431 error as cookies size will increase because of more iframe requests failed.Allow all third party cookies
Prevent cross-site tracking
@Premkumar-Shanmugam and @smartameer your answers together saved my life thanks a lot. This is the key when azure throw token_renewal_error.
if (error.errorCode === 'token_renewal_error') {
// reset authority to signin policy
authProvider.signin() // if you want user to directly go to sign in
window.location.reload() // if you want user to click sign in manually by clicking
}
I achieved a flow with login an reset password works like a charm.
This issue is for a: (mark with an
x
)Minimal steps to reproduce
Configure Azure with a signin sign up user flow Configure Azure with a Password reset user flow Look for documentation on where to set the forgot password user flow
Results: Nothing found
Any log messages given by the failure
Not applicable
Expected/desired behavior
I would expecty full documentation on how to configure
OS and Version?
Not applicable
Versions
0.4.9
Mention any other details that might be useful
I briefly scanned through the code and couldn't see anything that looked applicable to the forgot password user flow, I recognize I might just not have looked hard enough, but regardless. I think this should be documented (or implemented if it's not)