I have a requirement where I want to fetch the Authorization Code for Azure login for each request in web API. As of now once the user signs in to Azure, after that I am not getting the authorization code as the user is already signed in.
How can I force the user to sign in again? This is the code I have been using as of now in the owin_startup file in web API?
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
CookieSecure = (CookieSecureOption)Convert.ToInt32(cookieSecure), // CookieSecureOption.NeverAlways
CookieManager = new SystemWebCookieManager(),
CookieHttpOnly = false,
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
RedirectUri = RedirectUri,
});
According to the code and the cases you post before, I think it it not about Azure ad b2c, so here I will give a reply for azure ad.
When you request an authorization code, there is a prompt=login property which is indicate the user should be prompted to reauthenticate.
Also here is an article about Forcing reauthentication with Azure AD which suggest use Token Max Age to achieve it.
You can append max_age= to the authorization URL (or just put 0 to force password authentication at all times). So once user gets redirected to the URL, they will be presented with an information to login again.
public class RequireReauthenticationAttribute : Attribute, IAsyncResourceFilter
{
private int _timeElapsedSinceLast;
public RequireReauthenticationAttribute(int timeElapsedSinceLast)
{
_timeElapsedSinceLast = timeElapsedSinceLast;
}
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
{
var foundAuthTime = int.TryParse(context.HttpContext.User.FindFirst(AppClaimTypes.AuthTime)?.Value, out int authTime);
if (foundAuthTime && DateTime.UtcNow.ToUnixTimestamp() - authTime < _timeElapsedSinceLast)
{
await next();
}
else
{
var state = new Dictionary<string, string> { { "reauthenticate", "true" } };
await context.HttpContext.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties(state)
{
RedirectUri = context.HttpContext.Request.Path
}, ChallengeBehavior.Unauthorized);
}
}
}
It appears that the reason you wish to re-authenticate the user is to get/refresh the token in memory even when the auth cookie is present. Simplest way to achieve this would be decorate your controller with the AuthorizeForScopes attribute. You can check the sample project on Github here
Related
I have created one b2c-dotnet-webapp-and-webapi type application. But After 20 min or after sometime(nearly 30 min not sure) My WebApp is throwing exception during the Ajax call saying 401(Unauthorized). This exception is coming when ajax call hitting the WebApp controller So this error is coming from OWIN middleware not sure why.
My Startup.cs settings are
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(_aadB2CPasswordResetPolicy));
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(_aadB2CSignInPolicy));
}
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
{
context.HandleResponse();
if (context.Exception is OpenIdConnectProtocolInvalidNonceException &&
context.Exception.Message.Contains("IDX10316"))
{
// Redirect to the originally requested URL
context.Response.Redirect(context.Request.Uri.PathAndQuery);
}
else
{
var trackingId = Guid.NewGuid().ToString("N");
_telemetry.TrackException(
context.Exception,
new Dictionary<string, string> {{"SignInErrorTrackingId", trackingId}});
context.Response.Redirect($"/Home/SignInError?trackingId={trackingId}");
}
return Task.FromResult(0);
}
private OpenIdConnectAuthenticationOptions CreateOptionsFromPolicy(string policy)
{
return new OpenIdConnectAuthenticationOptions
{
// For each policy, give OWIN the policy-specific metadata address, and
// set the authentication type to the id of the policy
MetadataAddress = string.Format(_aadInstance, _tenant, policy),
AuthenticationType = policy,
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = _clientId,
RedirectUri = _redirectUri,
PostLogoutRedirectUri = _redirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
},
Scope = "openid",
ResponseType = "id_token",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
SaveSigninToken = true,
},
};
}
}
If I am modifying the code
app.UseCookieAuthentication(new CookieAuthenticationOptions { SlidingExpiration = true, ExpireTimeSpan = TimeSpan.FromMinutes(60) });,
And adding the below setting in OpenIdConnectAuthenticationOptions
UseTokenLifetime = false,
Then My WebApp is working for 1 hour and after that again I am facing the 401 Unauthorized. This time my WEAPI giving this error because by default token is valid for 1 hour I guess.
Question : How can I manage the token issues if the token is expired after 1 hour during ajax call ? and What is best setting that I should have So that my middleware will not give me 401 after 20 min or some random time ?
Kindly Ignore if something I am doing terribly wrong. I am very new to this and don't have much idea.
You're on the right track! There are a couple things that you need to be aware of:
All tokens returned by Azure AD B2C have an expiration time. After x minutes, your id token expired. You need to get a new id token again by going through the sign-in process again.
Because it is annoying to sign in every x minutes, you can request a refresh token when a user logs in for the first time. When the id token expires, you can send the refresh token to Azure AD B2C to get a new id token.
Part of the refresh token implementation is automated by the library, but this is what you need to implement:
Request a code in addition to an id token by adding 'code' in the responseType variable, and adding 'offline_access' in the scope variable.
Exchange the code for a refresh token and store the token inside a cache.
Get the token from the cache before making a call to the web api. If the token is expired, the library will automatically renew it for you before pulling it out of the cache.
All of this is done in this example. You just need to update the scope (the default responseType when you don't specify it is "code id_token").
Note: We enabled the use of access tokens and updated the sample with it. Please use this updated sample. Your code reflects the old sample. I also recommend using access tokens instead of id tokens when calling the web API.
In the Azure B2C, I used to be able to get a "groups" claim in my JWT tokens by following Retrieving Azure AD Group information with JWT:
Open the old-school Azure manager (https://manage.windowsazure.com)
Register my application with B2C
Download the B2C manifest for the application
In the manifest, change the "groupMembershipClaims" entry to
"groupMembershipClaims": "SecurityGroup",
Upload the changed B2C manifest again
The problem
This has worked well in the past (about a month ago, I believe...) but it doesn't anymore. See below for details...
What I've tried sofar
Plan A: Use Azure Manager
Follow the known-good recipe above.
Unfortunately that doesn't work anymore - I get the following error when this client tries to authenticate me with B2C:
AADB2C90068: The provided application with ID '032fe196-e17d-4287-9cfd-25386d49c0d5' is not valid against this service. Please use an application created via the B2C portal and try again"
OK, fair enough - they're moving us to the new Portal.
Plan B: Use Azure Portal
Follow the good old recipe, using the new Portal.
But that doesn't work either - when I get to the "download manifest" part, I cannot find any way to access the manifest (and Googling tells me it's probably gone for good...).
Plan C: Mix Azure Portal and manager
Getting a little desperate, I tried mixing plans A and B: register the app using the new Portal, then change the manifest using the old Azure Manager.
But no luck - when I try to upload the manifest, it fails with the message
ParameterValidationException=Invalid parameters provided; BadRequestException=Updates to converged applications are not allowed in this version.
Plan Z: Use the Graph API to retrieve group membership data
Just give up the "group" claim - instead, whenever I need group info, just query the B2C server using the Graph API.
I really, really don't want to do this - it would ruin the self-contained-ness of the access token, and make the system very "chatty".
But I've included it as a plan Z here, just to say: yes, I know the option exists, no I haven't tried it - and I'd prefer not to.
The question:
How do I get the "group" claim in my JWT token these days?
Plan Z it is I'm afraid. I don't know why they don't return it, but it's currently marked as planned on their Feedback Portal (it's the highest rated item).
This is how I'm doing it. Querying the groups when the user is authenticated, you can do it your way as well - just query as and when you need to. Depends on your use case.
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
LoginPath = new PathString("/account/unauthorised"),
CookieSecure = CookieSecureOption.Always,
ExpireTimeSpan = TimeSpan.FromMinutes(20),
SlidingExpiration = true,
CookieHttpOnly = true
});
// Configure OpenID Connect middleware for each policy
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(Globals.SignInPolicyId));
}
private OpenIdConnectAuthenticationOptions CreateOptionsFromPolicy(string policy)
{
return new OpenIdConnectAuthenticationOptions
{
// For each policy, give OWIN the policy-specific metadata address, and
// set the authentication type to the id of the policy
MetadataAddress = string.Format(Globals.AadInstance, Globals.TenantName, policy),
AuthenticationType = policy,
AuthenticationMode = AuthenticationMode.Active,
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = Globals.ClientIdForLogin,
RedirectUri = Globals.RedirectUri,
PostLogoutRedirectUri = Globals.RedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = AuthenticationFailed,
SecurityTokenValidated = SecurityTokenValidated
},
Scope = "openid",
ResponseType = "id_token",
// This piece is optional - it is used for displaying the user's name in the navigation bar.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
}
};
}
private async Task SecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> token)
{
var groups = await _metaDataService.GetGroups(token.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value);
if (groups?.Value != null && groups.Value.Any())
{
foreach (IGroup group in groups.Value.ToList())
{
token.AuthenticationTicket.Identity.AddClaim(
new Claim(ClaimTypes.Role, group.DisplayName, ClaimValueTypes.String, "GRAPH"));
}
}
}
// Used for avoiding yellow-screen-of-death
private Task AuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
if (notification.Exception.Message == "access_denied")
{
notification.Response.Redirect("/");
}
else
{
notification.Response.Redirect("/error?message=" + notification.Exception.Message);
}
return Task.FromResult(0);
}
}
My GetGroups method just queries the getMemberGroups method on the Users API
Then I have a simple helper method to determine whether the user is in a role:
public static bool UserIsInRole(IPrincipal user, string roleName)
{
var claims = user.Identity as ClaimsIdentity;
if (claims == null) return false;
return claims.FindAll(x => x.Type == ClaimTypes.Role).Any(x => x.Value == roleName);
}
I'm struggling to understand how IdentityServer3, AzureAD and a Private Database all work together. The biggest problem is how the Redirect URIs are being handled.
My scenario is I have a stand alone IdentityServer3. It's job is to authenticate users against either AzureAD or a private DB. Within the Startup.cs file on the ID3 server, I have the following OpenID Connect code:
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Map("/identity", s3App =>
{
s3App.UseIdentityServer(new IdentityServerOptions
{
SiteName = "3S",
SigningCertificate = Certificate.Load(),
Factory = new IdentityServerServiceFactory()
.UseInMemoryUsers(InMemoryUsers.Get())
.UseInMemoryClients(InMemoryClients.Get())
.UseInMemoryScopes(InMemoryScopes.Get()),
AuthenticationOptions = new AuthenticationOptions
{
EnablePostSignOutAutoRedirect = true,
EnableSignOutPrompt = false,
IdentityProviders = ConfigureAdditionalIdentityProviders
}
});
});
}
public static void ConfigureAdditionalIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "AzureAd",
Caption = "Login",
ClientId = "4613ed32-xxxx-xxxx-xxxx-xxxxxxxxxxxx", // GUID of registered application on Azure
Authority = "https://login.microsoftonline.com/our-tenant-id/",
PostLogoutRedirectUri = "https://localhost:44348/identity",
RedirectUri = "https://localhost:44348/identity",
Scope = "openid email profile",
ResponseType = "id_token",
AuthenticationMode = AuthenticationMode.Passive,
SignInAsAuthenticationType = signInAsType,
TokenValidationParameters = new TokenValidationParameters
{
AuthenticationType = Constants.ExternalAuthenticationType,
ValidateIssuer = false
}
});
}
I don't understand why the ID3 Server would need to have either RedirectUri or PostLogoutRedirectUri...shouldn't that be "passed through" from the application requesting the authentication? After all, we want to get back to the application, not the ID3 Server. Granted, I don't think this is what's causing my problem, but it would be nice to understand why these are here.
I will say, I've gotten "close" to this working.
When my application requiring authentication requests authentication against AzureAD, I'm redirected to the Microsoft Account login screen to enter my username/password for my work account. I submit my credentials and then get redirected back to either the ID3 server or my application, depending on which RedirectUri has been used in the above code.
For the sake of argument, let's say I use my application for the RedirectUri. I will be sent back to the application, but not to the page that initially prompted the authentication challenge, and if I click on a page that requires authentication, I'm sent back to the AzureAD server to log in again, only this time AzureAD recognizes me as already logged in.
Unfortunately, it doesn't appear that the SecurityTokenValidated notification is being acknowledged/set after the redirect from AzureAD.
Here's the code found in the application Startup.cs:
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44348/identity",
ClientId = "3af8e3ba-5a04-4acc-8c51-1d30f8587ced", // Local ClientID registered as part of the IdentityServer3 InMemoryClients
Scope = "openid profile roles",
RedirectUri = "http://localhost:52702/",
PostLogoutRedirectUri = "http://localhost:52702/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n =>
{
var id = n.AuthenticationTicket.Identity;
var givenName = id.FindFirst(Constants.ClaimTypes.GivenName);
var familyName = id.FindFirst(Constants.ClaimTypes.FamilyName);
var sub = id.FindFirst(Constants.ClaimTypes.Subject);
var roles = id.FindAll(Constants.ClaimTypes.Role);
var nid = new ClaimsIdentity(
id.AuthenticationType,
Constants.ClaimTypes.GivenName,
Constants.ClaimTypes.Role
);
nid.AddClaim(givenName);
nid.AddClaim(familyName);
nid.AddClaim(sub);
nid.AddClaims(roles);
nid.AddClaim(new Claim("application_specific", "Some data goes here. Not sure what, though."));
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
n.AuthenticationTicket = new AuthenticationTicket(nid, n.AuthenticationTicket.Properties);
return Task.FromResult(0);
},
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType != OpenIdConnectRequestType.LogoutRequest)
return Task.FromResult(0);
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.HandleResponse();
context.Response.Redirect("/Error/message=" + context.Exception.Message);
//Debug.WriteLine("*** AuthenticationFailed");
return Task.FromResult(0);
},
}
});
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
}
}
You'll notice that the OpenIdConnectAuthenticationOptions also contain a RedirectUri and a PostLogoutRedirectUri which point to the application, but those don't seem to matter.
Of course, everything works perfectly when I'm logging in using the "cookies" side of things - I see all of my claims for the user. And, spending some time on the phone with Microsoft, they proposed a solution outside of ID3 which worked, but is not the way we need to go. We will have multiple applications authenticating against our ID3 so we need to contain and control the flow internally.
I really need some help trying to figure out this last mile issue. I know I'm close, I've just been staring at this so long that I'm probably staring right at my error and not seeing it.
10/22/2016 Edit
Further testing and enabling Serilog revealed an issue with the RedirectUri and PostLogoutRedirectUri resulted in my adding the /identity to the end of the URIs which corresponds to the value set in app.Map. This resolved the issue of my being returned to the "blank" page of IdentityServer3, I'm now returned to the IdentityServer3 login screen. Azure AD still thinks I'm logged in, I'm just not getting the tokens set properly in my application.
Since the authenticate flow is a little complex, I am trying illustrate it using a figure below:
First the redirect URL need to register to the identity provider, so the server will match the RedirectURL in request to ensure that the response is redirected as expected instead of redirect to like Phishing site( security consideration).
And as the figure demonstrate, to use the Azure AD as the external identity provider for IdentityServer3, we need to register the apps on Azure AD. However since the app is used to communicate with Identity Server, the redirect URL register on the Azure Portal should redirect to the IdentityServer3 instead of URL of app.
For example, the URL of my identity server 3 is https://localhost:44333 then I use code below to add the additional identity providers. And this URL is the redirect URL on the Azure portal:
public static void ConfigureAdditionalIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "aad",
Caption = "Azure AD",
SignInAsAuthenticationType = signInAsType,
Authority = "https://login.microsoftonline.com/04e14a2c-0e9b-42f8-8b22-3c4a2f1d8800",
ClientId = "eca61fd9-f491-4f03-a622-90837bbc1711",
RedirectUri = "https://localhost:44333/core/aadcb",
});
}
And the URL of my app is http://localhost:1409/ which is register on the IdentyServer3 and below code is the web app use OWIN OpenId Connect to add the IdentyServer3 as the identity data provider:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "oidc",
SignInAsAuthenticationType = "cookies",
Authority = "https://localhost:44333",
ClientId = "mvc",
RedirectUri = "http://localhost:1409/",
ResponseType = "id_token",
Scope = "openid profile email"
});
Im trying Azure AD B2C and I've added Google and Microsoft Identity Providers through Azure Portal.
When i try to login with Microsoft OR Google IP, i always receive following error message in the OnAuthenticationFailed-Handler:
AADB2C99002: User does not exist. Please sign up before you can sign in.
But when i'm using the "Local Account SignIn" Provided by Azure B2C everything is working fine. Do i missing something in my configuration ?
The following code snippet shows my OWIN Configuration.
private void ConfigureAuthentication(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
OpenIdConnectAuthenticationOptions options = new OpenIdConnectAuthenticationOptions
{
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = clientId,
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
SecurityTokenValidated = context => {
return null;
}
},
Scope = "openid offline_access",
// The PolicyConfigurationManager takes care of getting the correct Azure AD authentication
// endpoints from the OpenID Connect metadata endpoint. It is included in the PolicyAuthHelpers folder.
ConfigurationManager = new PolicyConfigurationManager(
String.Format(CultureInfo.InvariantCulture, aadInstance, tenant, "/v2.0", OIDCMetadataSuffix),
new string[] { SignUpPolicyId, SignInPolicyId, ProfilePolicyId }),
// This piece is optional - it is used for displaying the user's name in the navigation bar.
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
NameClaimType = "name",
},
};
app.UseOpenIdConnectAuthentication(options);
}
// This notification can be used to manipulate the OIDC request before it is sent. Here we use it to send the correct policy.
private async Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
PolicyConfigurationManager mgr = notification.Options.ConfigurationManager as PolicyConfigurationManager;
if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
OpenIdConnectConfiguration config = await mgr.GetConfigurationByPolicyAsync(CancellationToken.None, notification.OwinContext.Authentication.AuthenticationResponseRevoke.Properties.Dictionary[AzureB2C.PolicyKey]);
notification.ProtocolMessage.IssuerAddress = config.EndSessionEndpoint;
}
else
{
OpenIdConnectConfiguration config = await mgr.GetConfigurationByPolicyAsync(CancellationToken.None, notification.OwinContext.Authentication.AuthenticationResponseChallenge.Properties.Dictionary[AzureB2C.PolicyKey]);
notification.ProtocolMessage.IssuerAddress = config.AuthorizationEndpoint;
}
}
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
// The user's objectId is extracted from the claims provided in the id_token, and used to cache tokens in ADAL
// The authority is constructed by appending your B2C directory's name to "https://login.microsoftonline.com/"
// The client credential is where you provide your application secret, and is used to authenticate the application to Azure AD
string userObjectID = notification.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant, string.Empty, string.Empty);
ClientCredential credential = new ClientCredential(clientId, clientSecret);
// We don't care which policy is used to access the TaskService, so let's use the most recent policy
string mostRecentPolicy = notification.AuthenticationTicket.Identity.FindFirst(AzureB2C.AcrClaimType).Value;
// The Authentication Context is ADAL's primary class, which represents your connection to your B2C directory
// ADAL uses an in-memory token cache by default. In this case, we've extended the default cache to use a simple per-user session cache
AuthenticationContext authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectID));
// Here you ask for a token using the web app's clientId as the scope, since the web app and service share the same clientId.
// The token will be stored in the ADAL token cache, for use in our controllers
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(notification.Code, new Uri(redirectUri), credential, new string[] { clientId }, mostRecentPolicy);
}
// Used for avoiding yellow-screen-of-death
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
_log.Error("AuthenticationFailed!\r\nError={0}\r\nErrorDescription={1}\r\n{0}",
notification.ProtocolMessage.Error,
notification.ProtocolMessage.ErrorDescription,
notification.Exception.ToString());
notification.HandleResponse();
notification.Response.Redirect("/Home/OpenIdError?message=" + notification.ProtocolMessage.ErrorDescription);
return Task.FromResult(0);
}
}
External identities first need to 'sign up' as well before signing in. During sign up the external identity is linked to B2C.
In the sign up page you can ask additional attributes for your users, like a customer number. You need this for external identies and for the Local Account users in B2C, no difference between the two.
This is different behaviour compared to adding an identity provider without B2C, where every login just works.
Edit: Like Konstantin mentioned, the new combined sign-up or sign-in policy solves this problem:
https://azure.microsoft.com/en-us/documentation/articles/active-directory-b2c-reference-policies/#create-a-sign-up-or-sign-in-policy
I was running into the same issue, but was able to circumvent the user "Sign-Up" after user insertion. The issue turned out to be, that to have proper federation occur, the proper values need to be in place.
"identities": [
{
"signInType": "federated",
"issuer": "https://login.microsoftonline.com/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX/v2.0",
"issuerAssignedId": "YYYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"
What was happening was that I was using "issuer": "myDomain.com" which was not resolving correctly to do a login; to which the user then had to "SignUp" via the federated IP.
By changing that from DNS readable name, to the MS login with my AD directories ID (the number provided when switching domain in Azure, XXXX-XXX ...) and the proper issuerAssignedId, from the originating AD issuer, it worked and the user was added.
I am following the below GitHub sample for implementing Authentication mechanism across WebApp and WebApi.
https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet
I am using a single App registration for both WebApp and WebApi, get a access token for "https://abc.onmicrosoft.com/App" and pass it on to WebApi. I am attaching the token to the HTTPS headers with the name "Bearer". I have the below in the WebApi Owin Startup class to validate the token for the Audience and Tenant, but does not actually validate the token for these as expected.
A couple of questions:
1. What triggers the below handler to validate the token for the tenant and audience? Is it the [Authorize] attribute on the Controller class?
2. How does it where to find the token to execute the handler?
3. Setting the SaveSigninToken to true saves the token. How can I retrieve the token and also Acquire access token for Graph API from this token?
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = "abc.onmicrosoft.com",
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = "https://abc.onmicrosoft.com/App",
SaveSigninToken = true,
}
});
Please advise. Thanks in advance!
What triggers the below handler to validate the token for the tenant and audience?
The middleware runs in Active mode by default, so it will attempt to find a token in every request. If it finds one, it will attempt to validate it. If it finds that it is valid, a ClaimsPrincipal is created which is accessible in further OWIN middleware and Web API components.
It also downloads the public keys with which it checks the token signature on app startup from Azure AD. You can see this if you use a tool like Fiddler.
How does it where to find the token to execute the handler?
I'm not sure if I am understanding this question, I hope my answer above clarified the process.
Setting the SaveSigninToken to true saves the token. How can I retrieve the token and also Acquire access token for Graph API from this token?
What you are trying to do is call an API using the on-behalf-of flow. You can find an example app here: https://github.com/Azure-Samples/active-directory-dotnet-webapi-onbehalfof. More specifically this part should be of interest to you: https://github.com/Azure-Samples/active-directory-dotnet-webapi-onbehalfof/blob/master/TodoListService/Controllers/TodoListController.cs#L133.
ClientCredential clientCred = new ClientCredential(clientId, appKey);
var bootstrapContext = ClaimsPrincipal.Current.Identities.First().BootstrapContext as System.IdentityModel.Tokens.BootstrapContext;
string userName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn) != null ? ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value : ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;
string userAccessToken = bootstrapContext.Token;
UserAssertion userAssertion = new UserAssertion(bootstrapContext.Token, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);
string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
string userId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
AuthenticationContext authContext = new AuthenticationContext(authority, new DbTokenCache(userId));
// In the case of a transient error, retry once after 1 second, then abandon.
// Retrying is optional. It may be better, for your application, to return an error immediately to the user and have the user initiate the retry.
bool retry = false;
int retryCount = 0;
do
{
retry = false;
try
{
result = await authContext.AcquireTokenAsync(graphResourceId, clientCred, userAssertion);
accessToken = result.AccessToken;
}
catch (AdalException ex)
{
if (ex.ErrorCode == "temporarily_unavailable")
{
// Transient error, OK to retry.
retry = true;
retryCount++;
Thread.Sleep(1000);
}
}
} while ((retry == true) && (retryCount < 1));
The [Authorize] decoration in the controller or whichever method we specify triggers the Owin security handler to validate the token and generates the claims.