We've implemented Azure AD B2C in Umbraco on the front end using Microsofts webapp sample https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi
Most of the time this is generally working, but after a while everyone starts getting hit by a redirect loop. Restating the website then clears the issue.
It seems to be something causing the .AspNet.Cookies cookie to stop being set when the user is redirected back to the site with an id token.
Any ideas?
For the folks that will run into the same problem and find this question, I wanted to share what caused this in my case and how I resolved it.
The AD B2C App Registration expects to have a RedirectURI. I forgot to put signin-oidc
So changing:
https://localhost:5000
To
https://localhost:5000/signin-oidc
resolved my problem.
This is the default value - /signin-oidc - unless something else is explicitly set.
I had infinite loop issue at logout and it was because of missing support of Razor pages. The default Microsoft.Identity.Web.UI SignOut action uses /Account/SignedOut Razor page as callback url.
var callbackUrl = Url.Page("/Account/SignedOut", pageHandler: null, values: null, protocol: Request.Scheme);
I added Razor support in my Asp.Net core web app and it fixed the issue.
services.AddRazorPages();
and
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
Thanks.
Please ensure that your Reply URL in your application registration matches your Redirect URI in the web.config. Try setting both of these to your main homepage URL to ensure that your app is registered properly. Also make sure that the Application ID and the Client ID are matching and the right tenant is set in your web config. This needs to be the onmicrosoft.com tenant. Also, ensure that your users have the right permissions for the application.
Please follow the instructions in my blog and video to ensure that these are set properly.
https://medium.com/#marilee.turscak/reply-urls-vs-postlogoutredirecturis-in-azure-active-directory-aad-20f57a03267b
https://www.youtube.com/watch?v=A9U1VGyztEM
You can also try deleting the application and republishing it. If none of these things work, it may actually be an issue with the platform itself.
enabled HTTPS only under TLS/SSL settings in web app .
For me, it was because I didn't have the scope defined in my b2c configuration settings, like this:
"Resources": {
"myApi": {
"ResourceUri": "https://localhost:44361",//"https://my.ui.com",
"ResourceScopes": [
"https://myapp.onmicrosoft.com/my-api/Admin.Read.Write" // this was wrong, which caused my looping
]
}
}
I was also getting a logout redirect loop. It would actually log out, but just get stuck in a loop. In my case, the redirect URL I had configured in Azure was fine (I had /signin-oidc).
I followed the guide on adding my own account controller action rather than using the built in 'MicrosoftIdentity/Account/SignOut' (while also adding the 'id_token' validation to secure the logout): https://learn.microsoft.com/en-us/azure/active-directory-b2c/enable-authentication-web-application-options#secure-your-logout-redirect
My startup.cs code is per the documentation, my controller code looks like this (the documentation code is missing 'AuthenticationProperties' variable):
namespace Cosmos.WebPortal.Controllers;
[AllowAnonymous]
[Area("MicrosoftIdentity")]
[Route("[area]/[controller]/[action]")]
public class MyAccountController : Controller
{
[HttpGet("{scheme?}")]
public async Task<IActionResult> SignOutAsync([FromRoute] string scheme)
{
scheme ??= OpenIdConnectDefaults.AuthenticationScheme;
var redirectUrl = Url.Content("~/");
var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
//obtain the id_token
var idToken = await HttpContext.GetTokenAsync("id_token");
//send the id_token value to the authentication middleware
properties.Items["id_token_hint"] = idToken;
return SignOut(properties, CookieAuthenticationDefaults.AuthenticationScheme, scheme);
}
}
So my logout link is now to this controller instead e.g. 'MicrosoftIdentity/MyAccount/SignOut'
That seems to work fine, no infinite loop. A bit frustrating as I don't really understand the cause or difference, but it works.
For me, it was an expired secret/certificate in Azure B2C. It's important to look at the network log to see if any message, thankfully there was message telling me exactly where to look
Related
So i am basically following this auth0 tutorial:
https://auth0.com/docs/quickstart/webapp/aspnet-core/01-login
https://github.com/auth0-samples/auth0-aspnetcore-mvc-samples/tree/master/Quickstart/01-Login
on my localhost and my test-environment the login completely works.
Although the error i am getting now on my staging environment also rings a bell, and had that before. But I do not know how I fixed it at that point.
During logging in it gets redirect to the callback url => callback, and not following my redirect url as i am setting it.
as in the accountcontroller
[HttpGet]
[Route("login")]
public async Task Login(string returnUrl = "/home")
{
await HttpContext.ChallengeAsync(Auth0Constants.Auth0Scheme, new AuthenticationProperties
{
RedirectUri = returnUrl
});
}
so the big question; what am I missing?
EDIT:
looking at the network traffic:
the one that does work:
I see that I have 2 authorize calls (not sure why?).
and in the one that is not working:
where does the second Login call come from..?
it was due to the encryption. When running multiple instances the encryption key is different:
when keeping track of the key it is fixed:
https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/key-storage-providers?view=aspnetcore-2.2&tabs=visual-studio
I don't know much about Auth0 but does it have the URL for each environment?
Does anyone have a working sample for Sustainsys Saml2 library for ASP.NET Core WebAPI only project (no Mvc) and what's more important without ASP Identity? The sample provided on github strongly relies on MVC and SignInManager which I do not need nor want to use.
I added Saml2 authentication and at first it worked fine with my IdP (I also checked the StubIdP provided by Sustainsys) for first few steps so:
IdP metadata get properly loaded
My API properly redirects to sign-in page
Sign-in page redirects to /Saml2/Acs page, and I see in the logs that it parses the result successfully
However I don't know how to move forward from there and extract user login and additional claims (my IdP provided also an e-mail, and it is included in SAML response which I confirmed in the logs).
Following some samples found on the web and modyfing a little bit the MVC Sample from GitHub I did the following:
In Startup.cs:
...
.AddSaml2(Saml2Defaults.Scheme,
options =>
{
options.SPOptions.EntityId = new EntityId("...");
options.SPOptions.ServiceCertificates.Add(...));
options.SPOptions.Logger = new SerilogSaml2Adapter();
options.SPOptions.ReturnUrl = new Uri(Culture.Invariant($"https://localhost:44364/Account/Callback?returnUrl=%2F"));
var idp =
new IdentityProvider(new EntityId("..."), options.SPOptions)
{
LoadMetadata = true,
AllowUnsolicitedAuthnResponse = true, // At first /Saml2/Acs page throwed an exception that response was unsolicited so I set it to true
MetadataLocation = "...",
SingleSignOnServiceUrl = new Uri("...") // I need to set it explicitly because my IdP returns different url in the metadata
};
options.IdentityProviders.Add(idp);
});
In AccountContoller.cs (I tried to follow a somewhat similar situation described at how to implement google login in .net core without an entityframework provider):
[Route("[controller]")]
[ApiController]
public class AccountController : ControllerBase
{
private readonly ILog _log;
public AccountController(ILog log)
{
_log = log;
}
[HttpGet("Login")]
[AllowAnonymous]
public IActionResult Login(string returnUrl)
{
return new ChallengeResult(
Saml2Defaults.Scheme,
new AuthenticationProperties
{
// It looks like this parameter is ignored, so I set ReturnUrl in Startup.cs
RedirectUri = Url.Action(nameof(LoginCallback), new { returnUrl })
});
}
[HttpGet("Callback")]
[AllowAnonymous]
public async Task<IActionResult> LoginCallback(string returnUrl)
{
var authenticateResult = await HttpContext.AuthenticateAsync(Constants.Auth.Schema.External);
_log.Information("Authenticate result: {#authenticateResult}", authenticateResult);
// I get false here and no information on claims etc.
if (!authenticateResult.Succeeded)
{
return Unauthorized();
}
// HttpContext.User does not contain any data either
// code below is not executed
var claimsIdentity = new ClaimsIdentity(Constants.Auth.Schema.Application);
claimsIdentity.AddClaim(authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier));
_log.Information("Logged in user with following claims: {#Claims}", authenticateResult.Principal.Claims);
await HttpContext.SignInAsync(Constants.Auth.Schema.Application, new ClaimsPrincipal(claimsIdentity));
return LocalRedirect(returnUrl);
}
TLDR: Configuration for SAML in my ASP.NET Core WebApi project looks fine, and I get success response with proper claims which I checked in the logs. I do not know how to extract this data (either return url is wrong or my callback method should work differently). Also, it is puzzling why successfuly redirect from SSO Sign-In page is treated as "unsolicited", maybe this is the problem?
Thanks for any assistance
For anyone who still needs assistance on this issue, I pushed a full working example to github which uses a .Net Core WebAPI for backend and an Angular client using the WebAPI. you can find the example from here:
https://github.com/hmacat/Saml2WebAPIAndAngularSpaExample
As it turned out, the various errors I've been getting were due to my solution being hosted inside docker container. This caused a little malfunction in internal aspnet keychain. More details can be found here (docker is mentioned almost at the end of the article):
https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?tabs=aspnetcore2x&view=aspnetcore-2.2
Long story short, for the code to be working I had to add only these lines:
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("/some/volume/outside/docker")); // it needs to be outside container, even better if it's in redis or other common resource
It fixed everything, which includes:
Sign-in action to external cookie
Unsolicited SSO calls
Exceptions with data protection key chain
So it was very difficult to find, since exceptions thrown by the code didn't point out what's going on (and the unsolicited SSO calls made me think that the SSO provider was wrongly configured). It was only when I disassembled the Saml2 package and tried various code pieces one by one I finally encoutered proper exception (about the key chain) which in turned led me to an article about aspnet data protection.
I provide this answer so that maybe it will help someone, and I added docker tag for proper audience.
I'm creating a webapi project with oauth bearer token authenthication and external login providers (google, twitter, facebook etc.). I started with the basic VS 2013 template and got everything to work fine!
However, after a user successfully logs is, the owin infrastructure creates a redirect with the folllowing structure:
http://some.url/#access_token=<the access token>&token_type=bearer&expires_in=1209600
In my server code I want to add an additional parameter to this redirect because in the registration process of my app, a new user needs to first confirm and accept the usage license before he/she is registered as a user. Therefore I want to add the parameter "requiresConfirmation=true" to the redirect. However, I've no clue about how to do this. I tried setting AuthenticationResponseChallenge.Properties.RedirectUri of the AuthenticationManager but this doesn't seem to have any affect.
Any suggestions would be greatly appreciated!
It should be relatively easy with the AuthorizationEndpointResponse notification:
In your custom OAuthAuthorizationServerProvider implementation, simply override AuthorizationEndpointResponse to extract your extra parameter from the ambient response grant, which is created when you call IOwinContext.Authentication.SignIn(properties, identity).
You can then add a custom requiresConfirmation parameter to AdditionalResponseParameters: it will be automatically added to the callback URL (i.e in the fragment when using the implicit flow):
public override Task AuthorizationEndpointResponse(OAuthAuthorizationEndpointResponseContext context) {
var requiresConfirmation = bool.Parse(context.OwinContext.Authentication.AuthenticationResponseGrant.Properties.Dictionary["requiresConfirmation"]);
if (requiresConfirmation) {
context.AdditionalResponseParameters.Add("requiresConfirmation", true);
}
return Task.FromResult<object>(null);
}
In your code calling SignIn, determine whether the user is registered or not and add requiresConfirmation to the AuthenticationProperties container:
var properties = new AuthenticationProperties();
properties.Dictionary.Add("requiresConfirmation", "true"/"false");
context.Authentication.SignIn(properties, identity);
Feel free to ping me if you need more details.
Before trying MVC5, I had a go at using OpenAM with MVC4, with slightly better results. I need to authenticate from an asp.net application using OpenAM, and don't fancy the Fedlet route - I can see anybody else who has ever tried that
So, this was my starting point. This shows how to use Google and Facebook, and it works a treat. This goes on to show how to use other providers. I'm using OpenAM with OAuth2, so created "OpenAMClient", inheriting from DotNetOpenAuth.AspNet.Clients.OAuth2Client, and registered this in AuthConfig.RegisterAuth:
OAuthWebSecurity.RegisterClient(new OpenAMClient("consumerKey", "consumerSecret"));
This is great, because it now appears automatically on the login page, and pretty much worked perfectly until I started to use it for authentication. In the GetServiceLoginUrl override, I constructed a path to my OpenAM server, appending the returnURL that had been generated by the application and passed in as a parameter:
protected override Uri GetServiceLoginUrl(Uri returnUrl)
{
var response =
string.Format(
"http://localopenam.hibu.com:9080/openam_10.1.0/oauth2/authorize?client_id={0}&response_type=code&scope=email&redirect_uri={1}",
_consumerKey, returnUrl);
return new Uri(response);
}
This got me to my OpenAM server login page, but after authenticating, I got an error saying that the redirection URI wasn't acceptable. Debugging the code, I can see that the ReturnURL starts off in the ExternalLoginResult as "/Account/ExternalLoginCallback", but by the time it reaches GetServiceLoginUrl, it has become:
http://localhost:60448/Account/ExternalLoginCallback?__provider__=OpenAM&__sid__=12e299cbac474b60a935f946f69d04a8
OpenAM isn't having any of that, as the "sid" parameter is dynamic, and it doesn't seem to acccept wildcards - it won't allow the returnURL provided by OAuthWebSecurity.
As a workaround, I intercept the ReturnURL, and switch to a new AccountController method:
protected override Uri GetServiceLoginUrl(Uri returnUrl)
{
var workingUrl = "http://localhost:60448/Account/OpenAMCallback";
var response =
string.Format(
"http://localopenam.hibu.com:9080/openam_10.1.0/oauth2/authorize?client_id={0}&response_type=code&scope=email&redirect_uri={1}",
_consumerKey, workingUrl);
return new Uri(response);
}
I add http://localhost:60448/Account/OpenAMCallback as a redirectURL in OpenAM, then added AccountController OpenAMCallback:
public ActionResult OpenAMCallback(string code)
{
Console.WriteLine(code );
//use the code to get the token, then user details etc
return RedirectToLocal(null);
}
This is great, because from here I get the access code, so I can make more requests for the token, get all the allowed user details, all of that kind of thing, but ... I'm jealous of the original ExternalLoginCallback method I've subverted away from, that all the other cool authentication servers use. I want to use OAuthWebSecurity.VerifyAuthentication and OAuthWebSecurity.GetOAuthClientData, but VerifyAuthentication is coming back as null, so that stops that party
I can use http://dotnetopenauth.net/ and do it by hand, but I'd rather use a framework so there's less to maintain. Am I missing something obvious, or is OAuthWebSecurity not really up to this so I should stick with dotnetopenauth?
thanks
Trying to use the StartSignInWithTwitter method. When the method is called soon after an exception is thrown. This is using the latest version of DotNetOpenAuth. Would it have anything to do with me developing and running with locally? (VS2010) Is this how I should be doing authentication in the first place? I do see some different ways in the Samples pack that is included with the source.
{"The remote server returned an error: (401) Unauthorized."}
My code looks like below:
public void TwitAuthInit()
{
TwitterConsumer.StartSignInWithTwitter(false).Send();
}
public ActionResult TwitAuth()
{
if (TwitterConsumer.IsTwitterConsumerConfigured)
{
string screenName;
int userId;
if (TwitterConsumer.TryFinishSignInWithTwitter(out screenName, out userId))
{
FormsAuthentication.SetAuthCookie(screenName, false);
return RedirectToAction("Home", "Index");
}
}
return View();
}
To answer your question about "Is this how I should be doing authentication in the first place?":
You probably shouldn't be calling SetAuthCookie(screenName, false) with your screenName, since screen names (I believe) can be recycled. You should instead log the user in using a unique ID, either one you create in your own user database or Twitter's, and then use the screen name only as an alias that is displayed to the user (and perhaps other users if this user were to post something for public viewing). Otherwise, when Twitter recycles a username, that user will inherit all the data from the old user on your site -- not good.
Wanted to confirm that the 401 error is indeed solved by setting a non-empty callback URL on the twitter app config page.
From the Application Type block of the settings page:
To restrict your application from using callbacks, leave this field
blank.
You have to go into TwitterConsumer.cs and change the following URLs:
Request token URL https://api.twitter.com/oauth/request_token
Authorize URL https://api.twitter.com/oauth/authorize
Access token URL https://api.twitter.com/oauth/access_token
As Twitter changed their URLs. I didn't get the memo and spent way too much time debugging this.