syncweek-react-aad / react-aad

A React wrapper for Azure AD using the Microsoft Authentication Library (MSAL). The easiest way to integrate AzureAD with your React for authentication.
MIT License
344 stars 94 forks source link

Forgot password seems to be unimplemented or missing from documentation #105

Open DocCaliban opened 5 years ago

DocCaliban commented 5 years ago

Please provide us with the following information:

This issue is for a: (mark with an x)

- [ ] bug report -> please search issues before submitting
- [ ] feature request
- [X] documentation issue or request
- [ ] regression (a behavior that used to work and stopped in a new release)

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)

AndrewCraswell commented 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.

andylbrummer commented 5 years ago

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
    );
  }
hrc7505 commented 4 years ago

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"));
anderssonola commented 4 years ago

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

causing the refresh token iframe to fail with ` in a frame because it set 'X-Frame-Options' to 'deny'.

Okhrimenko commented 4 years ago

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.

adomrockie commented 4 years ago

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>
yogeshpathade commented 4 years ago

@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?

adomrockie commented 4 years ago

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.

yogeshpathade commented 4 years ago

@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.

Mohammed-El-Nabulsi commented 4 years ago

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.

Premkumar-Shanmugam commented 4 years ago

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'),
);
thiagomeireless commented 4 years ago

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...

voloszad commented 4 years ago

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.

smartameer commented 4 years ago

Hope this solution might help some people till the library gets a fix

  1. When forgot password is clicked at your app use 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
        }
     })
  2. Have the below as your configuration.
    
    cache: {
        ...,
        storeAuthStateInCookie: true // to be able to clear cookies later
    }
  3. After renewal token fail, handle in loginSuccess and check for the payload,
    
    payload: {
        account: {
             idToken: {
                 tfp: 'resetpasswordpolicy'
             }
        }
    }

if tfp satisfies resetpasswordpolicy based token, then use authProvider and make a call authProvider.logout()

  1. Now on click of login or with user action clear all cookies or at least msal*. This is needed otherwise after login you will get 431 error as cookies size will increase because of more iframe requests failed.
  2. Go to login and login with proper username and password and it will work.
Restriction:
brayanL commented 4 years ago

@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.