User.Identity.IsAuthenticated returns false after login while using OpenId Connect with Auth0 - authentication

I am trying to implement user authentication in an ASP.Net Core (v2.1) MVC application using OpenId Connect and Auth0. I have the required configurations stored in the AppSettings files and application runs well till the Auth0 login page comes. Post login it hits the Callback URL which basically invokes a method (method name is Callback) in my Account Controller. In the callback method I am trying to get the access token if the user is authenticated. However, the User.Identity.IsAuthenticated returns false. Here is my code in the Startup.cs file--
public void ConfigureServices(IServiceCollection services)
{
//Set Cookie Policy
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
// Add authentication services
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect("Auth0", options => {
options.Authority = $"https://{Configuration["Auth0:Domain"]}";
options.ClientId = Configuration["Auth0:ClientId"];
options.ClientSecret = Configuration["Auth0:ClientSecret"];
options.ResponseType = "code";
options.Scope.Clear();
options.Scope.Add("openid");
options.CallbackPath = new PathString("/oauth/callback");
options.ClaimsIssuer = "Auth0";
options.SaveTokens = true;
options.Events = new OpenIdConnectEvents
{
// handle the logout redirection
OnRedirectToIdentityProviderForSignOut = (context) =>
{
var logoutUri = $"https://{Configuration["Auth0:Domain"]}/v2/logout?client_id={Configuration["Auth0:ClientId"]}";
var postLogoutUri = context.Properties.RedirectUri;
if (!string.IsNullOrEmpty(postLogoutUri))
{
if (postLogoutUri.StartsWith("/"))
{
// transform to absolute
var request = context.Request;
postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
}
logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri)}";
}
context.Response.Redirect(logoutUri);
context.HandleResponse();
return Task.CompletedTask;
}
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
And here is my code in the Account Controller
public class AccountController : Controller
{
public async Task Login(string returnUrl = "/")
{
await HttpContext.ChallengeAsync("Auth0", new AuthenticationProperties() { RedirectUri = returnUrl });
}
[Authorize]
public async Task Logout()
{
await HttpContext.SignOutAsync("Auth0", new AuthenticationProperties
{
RedirectUri = Url.Action("Index", "Home")
});
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
public IActionResult AccessDenied()
{
return View();
}
[Authorize]
public IActionResult Claims()
{
return View();
}
[Route("/oauth/callback")]
public async Task<ActionResult> CallbackAsync()
{
if (User.Identity.IsAuthenticated)
{
string accessToken = await HttpContext.GetTokenAsync("access_token");
}
return RedirectToAction("Claims", "Account");
}
}
Please help. Any help will be appreciated.
Thanks,
Amit Anand

In fact i'm not sure why your custom CallbackAsync method fires during OIDC login . The callback url of OIDC middleware will handle token valiation ,token decode,exchange token and finally fill the user principle . You shouldn't handle the process and let OIDC middlware handle it , so change the route of the CallbackAsync method(or change the CallbackPath in OIDC middleware , but of course the url should match the url config in Auth0's portal ) , for example : [Route("/oauth/callbackAfterLogin")] .
After change that , the process will be : user will be redirect to Auth0 for login -->Auth0 validate the user's credential and redirect user back to url https://localhost:xxx/oauth/callback-->OIDC middlware handle token --> authentication success . If you want to redirect to CallbackAsync(route is /oauth/callbackAfterLogin) and get tokens there , you can directly pass the url in ChallengeAsync method when login :
await HttpContext.ChallengeAsync("Auth0",
new AuthenticationProperties() { RedirectUri = "/oauth/callbackAfterLogin"});

Related

ASP.NET Core: OpenIDConnect: Keycloak: Logout: not redirecting to keycloak login page

I am implementing openidconnect authentication using keycloak server in my asp.net core application. The login works ok, but I'm not able to implement logout. The behavior is that it takes me to the home page of my web app; instead it should take me to the keycloak login page. Below is my code:
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
// Store the session to cookies
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// OpenId authentication
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(cookie =>
{
cookie.Cookie.Name = "keycloak.cookie";
cookie.Cookie.MaxAge = TimeSpan.FromMinutes(60);
cookie.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
cookie.SlidingExpiration = true;
})
.AddOpenIdConnect(options =>
{
options.Authority = Configuration.GetSection("Keycloak")["Authority"];
//Keycloak client ID
options.ClientId = Configuration.GetSection("Keycloak")["ClientId"];
//Keycloak client secret
options.ClientSecret = Configuration.GetSection("Keycloak")["ClientSecret"];
// For testing we disable https (should be true for production)
options.RequireHttpsMetadata = false;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
// OpenID flow to use
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.Events.OnSignedOutCallbackRedirect += context =>
{
context.Response.Redirect(context.Options.SignedOutRedirectUri);
context.HandleResponse();
return Task.CompletedTask;
};
});
}
Index.shtml:
<a class="nav-link text-light" asp-page-handler="Logout" asp-page="/Pages/Index">Sign out</a>
Index.cshtml.cs:
public IActionResult Logout()
{
return new SignOutResult(
new[] {
OpenIdConnectDefaults.AuthenticationScheme,
CookieAuthenticationDefaults.AuthenticationScheme
});
}
You are not supposed to return anything from the Logout method, because SignOutAsync genereates its own response.
So, this is how I do logouts in my applications:
/// <summary>
/// Do the logout
/// </summary>
/// <returns></returns>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
//Important, this method should never return anything.
}

Storing claims on cookie while redirecting to other URL and also without identity authentication

I just need advise if this is feasible. I am developing an authorization for my Shopify app and I need to somewhat store the access token from shopify auth for future verification of my front-end app.
So the first end-point the shopify is calling is this one:
[HttpGet("install")]
public async Task<IActionResult> Install()
{
try
{
if (ModelState.IsValid)
{
var queryString = Request.QueryString.Value;
var isValid = _shopifyService.VerifyRequest(queryString);
if (isValid)
{
var shopifyUrl = Request.Query["shop"];
var authUrl = _shopifyService.BuildAuthUrl(shopifyUrl,
$"{Request.Scheme}://{Request.Host.Value}/api/shopify/authorize",
Program.Settings.Shopify.AuthorizationScope);
return Redirect(authUrl);
}
}
}
catch (Exception ex)
{
var exceptionMessage = await ApiHelpers.GetErrors(ex, _localizer).ConfigureAwait(false);
ModelState.AddModelError(new ValidationResult(exceptionMessage));
}
ModelState.AddModelError(new ValidationResult(_localizer["InvalidAuthStore"]));
return BadRequest(ModelState.GetErrors());
}
This works fine and the result of this api call will actually redirect to same link to my api, but this one will authorize the app:
[HttpGet("authorize")]
public async Task<IActionResult> AuthorizeStore()
{
try
{
if (ModelState.IsValid)
{
var code = Request.Query["code"];
var shopifyUrl = Request.Query["shop"];
var accessToken = await _shopifyService.AuthorizeStore(code, shopifyUrl).ConfigureAwait(false);
var identity = User.Identity as ClaimsIdentity;
identity.AddClaim(new Claim(Constants.Claims.AccessToken, accessToken));
// genereate the new ClaimsPrincipal
var claimsPrincipal = new ClaimsPrincipal(identity);
// store the original tokens in the AuthenticationProperties
var props = new AuthenticationProperties {
AllowRefresh = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddDays(1),
IsPersistent = false,
IssuedUtc = DateTimeOffset.UtcNow,
};
// sign in using the built-in Authentication Manager and ClaimsPrincipal
// this will create a cookie as defined in CookieAuthentication middleware
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal, props).ConfigureAwait(false);
Uri uri = new Uri($"{Program.Settings.Shopify.RedirectUrl}?token={accessToken}");
return Redirect(uri.ToString());
}
}
catch (Exception ex)
{
var exceptionMessage = await ApiHelpers.GetErrors(ex, _localizer).ConfigureAwait(false);
ModelState.AddModelError(new ValidationResult(exceptionMessage));
}
ModelState.AddModelError(new ValidationResult(_localizer["InvalidAuthStore"]));
return BadRequest(ModelState.GetErrors());
}
So the above api will authorize my app in shopify and will return an access token. The accessToken is the one I want to save in the claims identity with Cookie authentication type(this is without authorizing user credentials). Still no errors at that point and after calling the HttpContext.SignInAsync function, I can still view using debugger the newly added claims.
As, you can see in the code, after assigning claims, I call to redirect the app to front-end link(Note: front-end and back-end has different url)
In my front-end app, I have a Nuxt middleware that I put a logic to check the token received from back-end since I only pass the token to the front-end app using query params. Here's my middleware code:
export default function ({ app, route, next, store, error, req }) {
if (process.browser) {
const shopifyAccessToken = store.get('cache/shopifyAccessToken', null)
if (!shopifyAccessToken && route.query.token) {
// if has token on query params but not yet in cache, store token and redirect
store.set('cache/shopifyAccessToken', route.query.token)
app.router.push({
path: '/',
query: {}
})
// verify access token on the route
app.$axios
.get(`/shopify/verifyaccess/${route.query.token}`)
.catch((err) => {
error(err)
})
} else if (!shopifyAccessToken && !route.query.token) {
// if does not have both, throw error
error({
statusCode: 401,
message: 'Unauthorized access to this app'
})
}
} else {
next()
}
}
In my middleware, when the route has query params equal to token= It calls another api to verify the accessToken saved in my claims identity:
[HttpGet("verifyaccess/{accessToken}")]
public async Task<IActionResult> VerifyAccess(string accessToken)
{
try
{
if (ModelState.IsValid)
{
var principal = HttpContext.User;
if (principal?.Claims == null)
return Unauthorized(_localizer["NotAuthenticated"]);
var accessTokenClaim = principal.FindFirstValue(Constants.Claims.AccessToken);
if (accessToken == accessTokenClaim)
{
return Ok();
}
else
{
return Unauthorized(_localizer["NotAuthenticated"]);
}
}
}
catch (Exception ex)
{
var exceptionMessage = await ApiHelpers.GetErrors(ex, _localizer).ConfigureAwait(false);
ModelState.AddModelError(new ValidationResult(exceptionMessage));
}
ModelState.AddModelError(new ValidationResult(_localizer["InvalidAuthStore"]));
return BadRequest(ModelState.GetErrors());
}
Looking at the code above, it always fails me because the claims identity that I saved on the authorize endpoint was not there or in short the ClaimsIdentity is always empty.
Here's how I register the Cookie config:
private void ConfigureAuthCookie(IServiceCollection services)
{
services.AddAuthentication(option =>
{
option.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
option.RequireAuthenticatedSignIn = false;
})
.AddCookie(options => {
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
options.SlidingExpiration = true;
options.Cookie.Name = "shopifytoken";
});
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
});
}
and I also put a app.UseAuthentication() and app.UseAuthorization() on my Startup.Configure
Please let me know if this seems confusing so I can revised it. My main goal here is to be able to access that accessToken that I saved in the ClaimsIdentity so that I can verify the token. The reason why I did this because currently the shopify does not have an API for verifying access token. So when a user access my app link like this one http://example.com/?token=<any incorrect token> then they can already access my app.

Error from RemoteAuthentication: OpenIdConnectAuthenticationHandler: message.State is null or empty

Error from RemoteAuthentication: OpenIdConnectAuthenticationHandler: message.State is null or empty.. even after getting the code, id_token and token successfully.
I am using Razor pages along with .netcore and have registered the required middleware in startup.cs which you will found below.
ConfigureServices Function
public void ConfigureServices(IServiceCollection services)
{
RegisterRazorPages(services);
RegisterCoreServices(services);
RegisterDataServices(services);
RegisterVersioningServices(services);
RegisterAntiforegery(services);
}
private void RegisterCoreServices(IServiceCollection services)
{
services.AddSingleton(Configuration);
services.AddControllers(opts =>
{
opts.ModelBinderProviders.Insert(0, new DateTimeModelBinderProvider());
opts.RequireHttpsPermanent = true;
})
.AddNewtonsoftJson(opts =>
{
opts.SerializerSettings.DateFormatString = "yyyyMMdd";
opts.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
// Add authentication services
services.AddAuthentication(options => {
//options.DefaultAuthenticateScheme = OpenIdConnectDefaults.AuthenticationScheme;
//options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.IsEssential = true;
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => {
//options.SignInScheme = "Cookies";
// Set the authority to your Auth0 domain
options.Authority = $"https://{Configuration["OpenIdConnect:Domain"]}";
options.RequireHttpsMetadata = true;
options.MetadataAddress = $"https://{Configuration["OpenIdConnect:Domain"]}/.well-known/openid-configuration";
options.UseTokenLifetime = true;
// Configure the Auth0 Client ID and Client Secret
options.ClientId = Configuration["OpenIdConnect:ClientId"];
options.ClientSecret = Configuration["OpenIdConnect:ClientSecret"];
// Set response type to code
options.ResponseType = OpenIdConnectResponseType.CodeIdTokenToken;
options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
options.GetClaimsFromUserInfoEndpoint = true;
options.UsePkce = true;
// Configure the scope
options.Scope.Clear();
options.Scope.Add("openid");
//options.Scope.Add("profile");
options.Scope.Add("siam");
options.SecurityTokenValidator = new JwtSecurityTokenHandler
{
// Disable the built-in JWT claims mapping feature.
InboundClaimTypeMap = new Dictionary<string, string>()
};
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "role";
// Set the callback path, so Auth0 will call back to http://localhost:3000/callback
// Also ensure that you have added the URL as an Allowed Callback URL in your Auth0 dashboard
options.CallbackPath = new PathString("/Default");
// Configure the Claims Issuer to be Auth0
options.ClaimsIssuer = OpenIdConnectDefaults.AuthenticationScheme;
options.SaveTokens = true;
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.SetParameter("audience", "http://localhost:3000/");
return Task.FromResult(0);
},
// handle the logout redirection
OnRedirectToIdentityProviderForSignOut = (context) =>
{
var logoutUri = $"https://{Configuration["Siam:Domain"]}/v2/logout?client_id={Configuration["Siam:ClientId"]}";
var postLogoutUri = context.Properties.RedirectUri;
if (!string.IsNullOrEmpty(postLogoutUri))
{
if (postLogoutUri.StartsWith("/"))
{
// transform to absolute
var request = context.Request;
postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
}
logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri)}";
}
context.Response.Redirect(logoutUri);
context.HandleResponse();
return Task.CompletedTask;
}
};
});
services.AddAuthorization();
services.AddHttpClient();
services.AddHealthChecks()
.AddCheck<AuthEndpointCheck>("auth_endpoint_check")
.AddCheck<DbHealthCheck>("db_health_check");
}
Configure function
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider apiVersionDescriptionProvider)
{
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
RequireHeaderSymmetry = false,
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseErrorHandlingMiddleware();
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
//app.UseCookiePolicy(new CookiePolicyOptions()
//{
// HttpOnly = HttpOnlyPolicy.Always,
// Secure = CookieSecurePolicy.Always,
// MinimumSameSitePolicy = SameSiteMode.Strict
//});
app.UseRouting();
// keep both between UseRouting() and UseEndpoints()
app.UseAuthentication();
app.UseAuthorization();
app.UseHttpMetrics(options =>
{
options.RequestDuration.Histogram = Metrics.CreateHistogram("CCR_http_request_duration_seconds", string.Empty,
new HistogramConfiguration
{
Buckets = Histogram.LinearBuckets(
start: Convert.ToDouble(Configuration["Prometheus:Start"]),
width: Convert.ToDouble(Configuration["Prometheus:Width"]),
count: Convert.ToInt32(Configuration["Prometheus:Count"])),
LabelNames = new[] { "code", "method" }
});
});
app.UseMetricServer();
app.UseSitHealthChecks();
app.UseSwagger();
app.UseSwaggerUI(opts =>
{
// build a swagger endpoint for each discovered API version
foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
{
opts.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
}
opts.RoutePrefix = string.Empty;
});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers().RequireAuthorization();
endpoints.MapHealthChecks("/hc", new HealthCheckOptions() { }).RequireAuthorization();
endpoints.MapMetrics().RequireAuthorization();
endpoints.MapRazorPages();
});
IdentityModelEventSource.ShowPII = true;
}
Problem Description
In the startup.cs file, i have set callback url to the protected homepage. When the application is stated, it will challange the oauth and here is the challenge code of indexPage. After finishing this challenge, the page should redirect to Default Page which is the home page of Application and is protected.
public async Task OnGetAsync()
{
if (User.Identity.IsAuthenticated)
{
string accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
// if you need to check the Access Token expiration time, use this value
// provided on the authorization response and stored.
// do not attempt to inspect/decode the access token
DateTime accessTokenExpiresAt = DateTime.Parse(
await HttpContext.GetTokenAsync("expires_at"),
CultureInfo.InvariantCulture,
DateTimeStyles.RoundtripKind);
string idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
}
else
{
string accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
string returnUrl = "/Default";
await HttpContext.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties() { RedirectUri = returnUrl });
//Challenge(OpenIdConnectDefaults.AuthenticationScheme);
}
}
and in the response following output has been generated from the brower.
In 4th call i am getting the id_token, token and code in response and after that app is redirecting to the mentioned /Default route in 5th call, where again some redirect occus in 6th call which i dont understand.
In the 6th call i am loosing all the parameter, and i dont have any cookies anymore. The logs are then showing the following exception.
2020-08-17 14:38:11.337 +02:00 [INF] Error from RemoteAuthentication: OpenIdConnectAuthenticationHandler: message.State is null or empty..
2020-08-17 14:38:11.381 +02:00 [ERR] An error was encountered while handling the remote login.
System.Exception: An error was encountered while handling the remote login.
---> System.Exception: OpenIdConnectAuthenticationHandler: message.State is null or empty.
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at SIT.WebApi.Infrastructure.Middleware.ErrorHandlingMiddleware.Invoke(HttpContext context)
2020-08-17 14:38:11.397 +02:00 [INF] Request finished in 62.128ms 500 application/json
Question
Why i need give callback url, while my server is automatically
redirecting and authenticating the users upon hitting the
authorization endpoint. Server is using kerberos windows
authentication.
What is the different between callback url in startup.cs, and the redirect url in index page.
If i am not mentioning the callback url, my app is by default redirecting toward /signin-oidc route, why?
How should i overcome this error?
How can i store the user information into HttpContext.User after getting the tokens, code and id_token etc.

Signing in an user after a successful external external provider challenge in serverside blazor

I'm struggling to configure my serverside web-app to sign-in using an external log-in provider (Discord in this case). I'm using https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers for my oauth providers. It's worth noting that this application is meant to only use the discord external provider, there is no regular log-in functionality using username/password.
This is what I have in my Startup.cs's ConfigureServices method to set up authentication, authorization and the discord oauth middleware:
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.User.RequireUniqueEmail = false;
options.Password.RequireDigit = false;
options.Password.RequiredLength = 4;
options.Password.RequiredUniqueChars = 0;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Lockout.AllowedForNewUsers = false;
}).AddEntityFrameworkStores<ApplicationDbContext>();
services.AddAuthorization();
services.AddAuthentication().AddDiscord(x =>
{
x.ClientId = Configuration["AppId"];
x.ClientSecret = Configuration["AppSecret"];
x.Scope.Add("guilds");
x.Scope.Add("identify");
x.Validate();
});
and this is what i have in my Configure method
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// environment stuff
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
For whatever reason, whenever I perform perform a challenge (which successfully redirects to the discord log-in which then redirects back to /sigin-discord) it is not logging in the user.
This here is my identity controller that handles the login, after the challenge happens, the user is sent to /sigin-discord which then redirects to /sigin in order to either log-in the user or create an account for them if they don't have one yet.
[HttpGet("login")]
public async Task login(string redirectUrl)
{
string redirectUri = Url.Action("ExternalLoginCallback","Identity",new { returnUrl = redirectUrl});
var properties = await signInManager.GetExternalAuthenticationSchemesAsync();
await HttpContext.ChallengeAsync(properties.FirstOrDefault().Name, new AuthenticationProperties { RedirectUri = redirectUri });
}
[HttpGet("signin")]
public async Task ExternalLoginCallback(string returnUrl)
{
returnUrl = returnUrl ?? "/";
var info = await signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
HttpContext.Response.Redirect(returnUrl);
}
var result = await signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, true);
if (result.Succeeded)
{
HttpContext.Response.Redirect(returnUrl);
}
else
{
var username = info.Principal.FindFirstValue(ClaimTypes.Name);
if (username != null)
{
var user = await UserManager.FindByNameAsync(info.Principal.Identity.Name);
if (user == null)
{
user = new ApplicationUser(info.Principal.FindFirstValue(ClaimTypes.Name), Convert.ToUInt64(info.ProviderKey));
await UserManager.CreateAsync(user);
}
await UserManager.AddLoginAsync(user, info);
await signInManager.SignInAsync(user, true);
}
HttpContext.Response.Redirect(returnUrl);
}
However. The signInManager.GetExternalLoginInfoAsync() always returns null, even though I successfully did the challenge.
Has anyone attempted to use an external sign-in provider with blazor yet? If so, does anyone have an idea why this is not working?

Asp.Net core web API anti-forgery validation fails if used along with JWT bearer authentication

I am trying to use Anti-forgery along with jwt bearer authentication in Asp.net core 3.0 web API. The weird problem that I am facing is that anti-forgery works perfectly fine, but if I try to add an [Authorize] filter to the controller action along with [ValidateAntiForgeryToken], then AntiForgery validation fails with Http 400 error.
startup.cs :
services.AddCors();
services.AddControllers();
services.AddMvc();
services.AddAntiforgery
(
options =>
{
options.HeaderName = "X-XSRF-TOKEN";
options.Cookie = new Microsoft.AspNetCore.Http.CookieBuilder()
{ Name = "X-XSRF-COOKIE" };
}
);
// configure strongly typed settings objects
var appSettingsSection = Configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);
// configure jwt authentication
var appSettings = appSettingsSection.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
UsersController.cs :
[AllowAnonymous]
[IgnoreAntiforgeryToken]
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]AuthenticateModel model)
{
var user = _userService.Authenticate(model.Username, model.Password);
var tokens = _antiforgery.GetAndStoreTokens(HttpContext);
Response.Cookies.Append("X-XSRF-TOKEN", tokens.RequestToken, new Microsoft.AspNetCore.Http.CookieOptions
{
HttpOnly = false
});
if (user == null)
return BadRequest(new { message = "Username or password is incorrect" });
return Ok(user);
}
If I use [Authorize] filter on this below action Antiforgery validation fails.If I remove it Antiforgery validation seems to be working fine.
UsersController.cs :
[HttpGet]
[Authorize]
[ValidateAntiForgeryToken]
public IActionResult GetAll()
{
var users = _userService.GetAll();
return Ok(users);
}
and this is how I am generating JWT the token :
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
user.Token = tokenHandler.WriteToken(token);
servers logs :
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[3]
Route matched with {action = "GetAll", controller = "Users"}. Executing controller action with signature Microsoft.AspNetCore.Mvc.IActionResult GetAll() on controller WebApi.Controllers.UsersController (WebApi).
info: Microsoft.AspNetCore.Mvc.ViewFeatures.Filters.ValidateAntiforgeryTokenAuthorizationFilter[1]
Antiforgery token validation failed. The provided antiforgery token was meant for a different claims-based user than the current user.
Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException: The provided antiforgery token was meant for a different claims-based user than the current user.
at Microsoft.AspNetCore.Antiforgery.DefaultAntiforgery.ValidateTokens(HttpContext httpContext, AntiforgeryTokenSet antiforgeryTokenSet)
at Microsoft.AspNetCore.Antiforgery.DefaultAntiforgery.ValidateRequestAsync(HttpContext httpContext)
at Microsoft.AspNetCore.Mvc.ViewFeatures.Filters.ValidateAntiforgeryTokenAuthorizationFilter.OnAuthorizationAsync(AuthorizationFilterContext context)
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[3]
Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.ViewFeatures.Filters.ValidateAntiforgeryTokenAuthorizationFilter'.
info: Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor[1]
Executing ObjectResult, writing value of type 'Microsoft.AspNetCore.Mvc.ProblemDetails'.
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[2]
Executed action WebApi.Controllers.UsersController.GetAll (WebApi) in 28.5729ms
Tried setting HttpContext.user before calling _antiforgery.GetAndStoreTokens(HttpContext) but it did not worked.
You need to setup antiforgeryvalidation in the startup file before going into controller. See the below code snippet:
public void Configure(IApplicationBuilder app, IAntiforgery antiforgery)
{
app.Use(next => context =>
{
string path = context.Request.Path.Value;
if (
string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(path, "/index.html", StringComparison.OrdinalIgnoreCase))
{
// The request token can be sent as a JavaScript-readable cookie,
// and Angular uses it by default.
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
return next(context);
});
}