Azure B2C: KMSI feature does not work with custom policy - azure

Our SPA uses Azure B2C and MSAL (React) for user authentication. There are other requirements so we use custom policies instead of predefined user flows. But I struggle to implement Keep Me Signed In (KMSI) feature following these instructions.
I used custom policies from the starter pack: Phone_Email_Base.xml and SignUpOrSignInWithPhoneOrEmail.xml
Added <Item Key="setting.enableRememberMe">True</Item> entry to <TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Phone-Email">
Updated relying party policy file with this:
<UserJourneyBehaviors>
<SingleSignOn Scope="Tenant" KeepAliveInDays="30" />
<SessionExpiryType>Absolute</SessionExpiryType>
<SessionExpiryInSeconds>1200</SessionExpiryInSeconds>
<ScriptExecution>Allow</ScriptExecution>
</UserJourneyBehaviors>
Set up MSAL instance in my index.tsx following this. Lib versions: "#azure/msal-browser": "^2.14.2", "#azure/msal-react": "^1.0.0"
Tried to obtain access token:
msalInstance
.acquireTokenSilent(accessTokenRequest)
.then((response) => {
// use response.accessToken here
...
})
.catch((e) => {
console.error(e);
if (e instanceof InteractionRequiredAuthError) {
instance.acquireTokenRedirect(accessTokenRequest);
}
});
The problem is MSAL cannot retrieve access token silently after 24 hours from user logged in (i.e. once refresh token is expired) and requires user to re-login.
To make sure that my application code is Ok, I tried to use predefined user flow (combined B2C_1_SignUpSignIn) with KMSI feature enabled. And in this case, my application is able obtain access token silently after 24 hours. So KMSI works perfectly with user flow, but doesn't with custom policy.
Crawled through docs and examples for days, but still can't find any clues what else needs to be done here. Any help would be appreciated.

When acquireTokenSilent() fails, MSAL will call ssoSilent(). This will launch a hidden iframe to try to get a token using cookie based SSO.
When this fails, a final error will come back. You must catch this error and call acquireTokenRedirect(). Now if your session setup for your technical profiles is setup properly, and a valid session cookie exists, you’ll get SSO.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/custom-policy-reference-sso
If you are actually seeing a prompt for user input, your session setup is not correct for that particular techical profile. This is the real reason why ssoSilent() failed.
Your problem is not KMSI. To prove it, remove KMSI config, sign in to your app, remove the MSAL objects from the LocalStorage, force a token renewal. You’ll reproduce the issue you described, even without KMSI, and just after a few minutes of logging in.

Well, eventually it turned out that this solution actually works. Still not sure why it failed after the first 24 hours after the custom policy was applied. So I was forced to re-login after the first 24 hours but when the other 24 hours passed, my application was able to get a new access token without providing credentials by the user.

Related

ADB2C and MSAL with separate signup and signin policies causing issues

I'm trying to use ADB2C custom policies with the MSAL.js library on a static web app
Our policies are reasonably complex, so we've split the 'signup' into it's own flow, but now I'm having trouble with the handover from signup to signin. From what I'm reading it sounds like each IEF policy assigns its own tokens, and so a token generated from a signup flow cannot be used to signin? Even if they're both associated with the same ADB2C tenant?
Is this correct? It seems odd if so, as the keys (behind the discovery document/jwks_uri) are identical on both policies, as is the issuer.
The error that msal reports is during an 'acquireTokenFromNetworkStart' request which returns a 400 Bad Request Silent SSO could not be completed - insufficient information was provided. Please provide either a loginHint or sid.
So perhaps I just need to adjust the session management on the ADB2C policies? Do I need to emit the sid (session id) and then use that with msal when re-acquiring tokens?
Any advice would be most welcome, I cannot find a well documented example that puts all of this together.
Please check if it can be worked around like below .
Try to include your scope in the initial loginRedirect or loginPopup or while calling acquireTokenRedirect or acquireTokenPopup before calling acquireTokenSilent.
Scopes that is created in expose an api like "user.read", "mail.send" in your login request in code and grant consent for the same .
That means we need to Call acquireTokenSilent with your resource scope.
var loginRequest = {
scopes: ["openid","user.read", "mail.send"]
};
try {
msalInstance.loginRedirect(loginRequest);
} catch (err) {
// handle error
}
In B2C ,your tenant will need to be configured to return the emails claim on idTokens .Reference
Also try to include application Id or app id uri in the scopes .
References:
microsoft-authentication-library-for-js/issues
cannot-get-access-token-in-react-app/SO Reference

SaaS App with AzureADB2C: User Flows for Client SSO?

We have a SaaS web app and our clients are requiring SSO authentication for each of them. We are using AzureADB2C and it works great, but now are looking at adding SSO.
I put in the SSO setup into the B2C tenet and it works great, but really messed up our login screen with a "MyCompanySSO" button to log in with, on our customer-facing login screen.
So now my idea is to have a separate user flow that handles each SSO setup. Starting with us. We'd go to MyCompany.OurSaaSApp.us and that'd forward them directly to the user flow endpoint and prompt them to login with their SSO account (AzureAD).
This all seems to try to work, but I'm getting these errors within the AzureADB2C middleware:
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler:Warning: .AspNetCore.Correlation. state property not found.
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler:Information: Error from RemoteAuthentication: Correlation failed..
Then I get pumped out onto a error page and the login fails.
So 2 things...
1.) Am I going in the right direction knowing what we're wanting to accomplish
2.) What do we need to do to resolve this?
Thanks everyone for the help, it's been greatly appreciated.
(note:)
Just to reiterate. The SSO works properly when the custom identity provider is attached to the existing SignUpOrIn UserFlow I have configured in the app. I'm only getting this error when I try to use another UserFlow that I want to use specifically for this SSO.
I'm not sure about that specific error, although "state" parameter is a parameter that your app sends in the request that will be returned in the token for correlation purposes.
Using and different policy for each federation sounds like the right approach, but if you are doing from a single instance of your app, you'll need to modify the OIDC protocol message with the correct authority (ie policy) on redirect.
In your OIDC middleware configuration, set up a handler for the RedirectToIdentityProvider notification. Then handle it with something like:
private Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
//var policy = notification.OwinContext.Get<string>("Policy");
var tenantSegment = notification.Request.Path.Value.Split(new char [] { '/'}, StringSplitOptions.RemoveEmptyEntries)[0];
if (!string.IsNullOrEmpty(tenantSegment) && !tenantSegment.Equals(DefaultPolicy))
{
notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.ToLower().Replace(DefaultPolicy.ToLower(), $"B2C_1A_{tenantSegment.ToLower()}_SignUpSignInPolicy");
}
return Task.FromResult(0);
}
If you need to inject anything else tenant-related, that would be the place to do it.

GetExternalLoginInfoAsync always returns null when using Okta for authentication

I'm currently trying to get Okta to work with our MVC based application. Unfortunatly I am not the original designer or author of original code. However after some digging I have found that my predecessors work was based on the sustainsys example app "SampleOwinApplication" and this certainly seems to provided the functionality that we require. So I have based my query on this sample that can be obtained from https://github.com/Sustainsys/Saml2
This works with the sustainsys saml stub but now a genuine authentication provider (in this case Okta)
If I configure the application to use the sustainsys stub authentication provider and using Chrome with a plugin to view SAML tokens. I can see the SAML token come back and is presented to the call back as expected:
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
and when it runs loginInfo gets filed in and all works as expetced.
However when I change to configuration to use my Okta app, I get redirected to log in to Okta (as expecected) and I can see successful authentication and a SAML token comes back to my application (as seen in the Chrome plugin). However the above consumer for the callback ends up with a null value in loginInfo.
Further digging into the issue shows that in the Statup.Auth.cs there is the following code:
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
and then the Saml2 authentication is added
app.UseSaml2Authentication(CreateSaml2Options());
So it looks like cookie authentication is being used rather than saml2. If I check for cookies from the sustainsys site I can see them added to the browser and authentication works as expected. However, if I use Okta for authentication, no such cookies get set and the authentication fails.
Removing all the cookie authentication references results in:
A default value for SignInAsAuthenticationType was not found in IAppBuilder Properties. This can happen if your authentication middleware are added in the wrong order, or if one is missing.
So clearly this is required, shifting the call to app.UseSaml2Authentication(CreateSaml2Options()); before app.UseCookieAuthentication in the vain hope of it changing the priority and therefore picking up the SAML token fails and whilst the code runs authentication and the call to AuthenticationManager.GetExternalLoginInfoAsync, always results in a null value being returned regardless of the authentication provider.
So I either need to completely remove the cookie authentication so it is forced to use the saml packet, get Okta to set the necessary cookies or be able to parse the saml 2 token independently rather than relying on AuthenticationManager.GetExternalLoginInfoAsync to do the work.
Any clues/advice is appreciated
See the working configuration I am currently using successfully with Okta for a service provider initiated login:
AuthenticateResult.Succeeded is false with Okta and Sustainsys.SAML2
Unfortunately, it is still not working with an identity provider initiated login.
See: IdP Initiated Login with Sustainsys.SAML2 - AuthenticateResult Has No Information

Azure B2C Sample Custom Policy, When SignIng In Shows 'Your password is incorrect'

I have followed the Getting Started workflow here:
https://learn.microsoft.com/azure/active-directory-b2c/active-directory-b2c-get-started-custom
I followed it from scratch, twice, and have both times when trying to sign in with the sign-in & sign-up custom policy I am prompted with 'Your password is incorrect'. How can I properly authenticate?
Things that are working:
When I do signup the user journey completes, and the user is added to the directory
Using built in policies I can sign in and sign up
Using built in policies I can sign in with a user who I used the custom policy to sign up for
Using Application Insights I can see the following errors. (some sanitation applied)
"ValidationRequest":{
"ContentType":"Unspecified",
"Created":"2017-10-06T17:19:34.3995426Z",
"Key":"ValidationRequest",
"Persistent":true,
"Value":"client_id=55555555-5555-5555-5555-555555555555&resource=cccccccc-cccc-cccc-cccc-cccccccccccc&username=MYUSERNAME&password=PASSWORDIENTEREDONSCREEN&grant_type=password&scope=openid&nca=1;1;login-NonInteractive;False"
},
"ValidationResponse":{
"ContentType":"Json",
"Created":"2017-10-06T17:19:34.3995426Z",
"Key":"ValidationResponse",
"Persistent":true,
"Value":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS70002: The request body must contain the following parameter: 'client_secret or client_assertion'.\\r\\nTrace ID: 55555555-cccc-cccc-cccc-555555555555\\r\\nCorrelation ID: 77777777-7777-7777-7777-777777777777\\r\\nTimestamp: 2017-10-06 17:19:33Z\",\"error_codes\":[70002],\"timestamp\":\"2017-10-06 17:19:33Z\",\"trace_id\":\"55555555-cccc-cccc-cccc-555555555555\",\"correlation_id\":\"77777777-7777-7777-7777-777777777777\"};1;login-NonInteractive;False"
},
The important bit seems to be:
"AADSTS70002: The request body must contain the following parameter: 'client_secret or client_assertion"
Also later it says:
"Exception of type 'Web.TPEngine.Providers.BadArgumentRetryNeededException' was thrown."
Some things I've double checked:
In my Identity Experience Framework > Policy Keys, I have the following 4 keys after following the steps in the guide:
B2C_1A_AdminClientEncryptionKeyContainer
B2C_1A_FacebookSecret
B2C_1A_TokenEncryptionKeyContainer
B2C_1A_TokenSigningKeyContainer
In the regular AAD > App registrations, I have the applications:
IdentityExperienceFramework
ProxyIdentityExperienceFramework
And ProxyIdentityExperienceFramework has delegated Access IdentityExperienceFramework permissions from IdentityExperienceFramework.
And I've hit Grant permissions for both apps.
Both application Ids were properly substituted in the TrustFrameworkExtensions.xml, two places each
Any help is much appreciated. Thank you.
Your ProxyIdentityExperienceFramework app was incorrectly created as a Web App/API. You need to recreate it as a Native app. Make sure you don't forget to update the client_ids in your base policy to the new ProxyIdentityExperienceFramework accordingly.
The error AADSTS70002: The request body must contain the following parameter: 'client_secret or client_assertion occurs when you try to obtain a token using a client_id for an application that was registered as a Web App/API but are not providing the required client_secret. In the case of Native app, there is no client_secret required.
When sign in, Azure AD B2C's trust framework (the thing that executes custom policies) internally attempts to obtain a token for the IdentityExperienceFramework app (Web API) using the ProxyIdentityExperienceFramework app (Native app). If you incorrectly create the latter as a Web App/API, B2C's policy engine will fail to obtain the token which manifests itself as a Your password is incorrect error message to the user.

chrome.identity.LaunchWebAuthFlow: How to implement logout in a web app using oauth2

I am working on some client side web app like a chrome extension that needs access to outlook mail and calendar. I followed the instruction on https://dev.outlook.com/RestGettingStarted and successfully got access and refresh tokens to retrieve data.
However, I cannot find any way of implementing "logout". The basic idea is to let user sign out and login with a different outlook account. In order to do that, I removed cached tokens, requested access tokens in interactive mode. The login window did pop out, but it took any valid email address, didn't let me input password and finally returned tokens for previous account. So I was not able to really use a different account until the old token expired.
Can anyone please tell me if it is possible to send a request to revoke the tokens so people can use a different account? Thanks!
=========================================================
Update:
Actually it is the fault of chrome.identity api. I used chrome.identity.LaunchWebAuthFlow to init the auth flow. It caches user's identity but no way to remove it. So we cannot really "logout" if using this api.
I used two logouts via launchWebAuthFlow - first I called the logout link to my app, then secondly, I called the logout link to Google.
var options = {
'interactive': false,
'url': 'https://localhost:44344/Account/Logout'
}
chrome.identity.launchWebAuthFlow(options, function(redirectUri) {});
options = {
'interactive': false,
'url': 'https://accounts.google.com/logout'
}
chrome.identity.launchWebAuthFlow(options, function(redirectUri) {});

Resources