OAuth .NET Core - to many redirects after sign in with custom options - Multi Tenant - asp.net-core

I'm trying to implement OAuth per tenant. Each tenant has their own OAuthOptions.
I overwritten OAuthOptions and with IOptionsMonitor i resolve the OAuthOptions every time. Based on this answer: openid connect - identifying tenant during login
But right now after log in on external, the callback to signin ends up in to many redirects.
Signin successfull -> redirect to app -> app says not authenticated -> redirect to external login -> repeat.
The code:
My Startup.ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddAuthentication(options =>
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "OAuth";
})
.AddCookie("OAuth.Cookie", options =>
{
options.Cookie.Name = "OAuth-cookiename";
options.Cookie.SameSite = SameSiteMode.None;
options.LoginPath = "/account/login";
options.AccessDeniedPath = "/account/login";
})
.AddOAuth<MyOptions, OAuthHandler<MyOptions>>("OAuth", options =>
{
// All options are set at runtime by tenant settings
});
services.AddScoped<IOptionsMonitor<MyOptions>, MyOptionsMonitor>();
services.AddScoped<IConfigureOptions<MyOptions>, ConfigureMyOptions>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
Startup.Configure:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
ConfigureMyOptions.cs
public class ConfigureMyOptions : IConfigureNamedOptions<MyOptions>
{
private HttpContext _httpContext;
private IDataProtectionProvider _dataProtectionProvider;
private MyOptions myCurrentOptions;
public ConfigureMyOptions(IHttpContextAccessor contextAccessor, IDataProtectionProvider dataProtectionProvider)
{
_httpContext = contextAccessor.HttpContext;
_dataProtectionProvider = dataProtectionProvider;
}
public void Configure(string name, MyOptions options)
{
//var tenant = _httpContext.ResolveTenant();
// in my code i use tenant.Settings for these:
options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
options.TokenEndpoint = "https://github.com/login/oauth/access_token";
options.UserInformationEndpoint = "https://api.github.com/user";
options.ClientId = "redacted";
options.ClientSecret = "redacted";
options.Scope.Add("openid");
options.Scope.Add("write:gpg_key");
options.Scope.Add("repo");
options.Scope.Add("read:user");
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
options.ClaimActions.MapJsonKey("External", "id");
options.SignInScheme = "OAuth.Cookie";
options.CallbackPath = new PathString("/signin");
options.SaveTokens = true;
options.Events = new OAuthEvents
{
OnCreatingTicket = _onCreatingTicket,
OnTicketReceived = _onTicketReceived
};
myCurrentOptions = options;
}
public void Configure(MyOptions options) => Configure(Options.DefaultName, options);
private static async Task _onCreatingTicket(OAuthCreatingTicketContext context)
{
// Get the external user id and set it as a claim
using (var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint))
{
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
using (var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted))
{
response.EnsureSuccessStatusCode();
var user = JObject.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(user);
}
}
}
private static Task _onTicketReceived(TicketReceivedContext context)
{
context.Properties.IsPersistent = true;
context.Properties.AllowRefresh = true;
context.Properties.ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(30);
return Task.CompletedTask;
}
}
MyOptions:
// Overwritten to by pass validate
public class MyOptions : OAuthOptions
{
public override void Validate()
{
return;
}
public override void Validate(string scheme)
{
return;
}
}
MyOptionsMonitor:
// TODO caching
public class MyOptionsMonitor : IOptionsMonitor<MyOptions>
{
// private readonly TenantContext<Tenant> _tenantContext;
private readonly IOptionsFactory<MyOptions> _optionsFactory;
public MyOptionsMonitor(
// TenantContext<Tenant> tenantContext,
IOptionsFactory<MyOptions> optionsFactory)
{
// _tenantContext = tenantContext;
_optionsFactory = optionsFactory;
}
public MyOptions CurrentValue => Get(Options.DefaultName);
public MyOptions Get(string name)
{
return _optionsFactory.Create($"{name}");
}
public IDisposable OnChange(Action<MyOptions, string> listener)
{
return null;
}
}

Related

How to get identity values from google external provider in ASP.NET Core Web API

[ApiController]
[Route("[controller]")]
public class AuthController : ControllerBase
{
private readonly SignInManager<IdentityUser> _signInManager;
public AuthController(SignInManager<IdentityUser> signInManager)
{
_signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
}
[HttpGet("token")]
public ChallengeResult Token()
{
var properties = new GoogleChallengeProperties
{
RedirectUri = "/auth/retrieve",
AllowRefresh = true,
};
return Challenge(properties, "Google");
}
[HttpGet("[action]")]
public async Task Retrieve()
{
var token = await HttpContext.GetTokenAsync("access_token");
var externalLoginInfoAsync = await _signInManager.GetExternalLoginInfoAsync();
var identityName = User?.Identity?.Name;
var authenticateResult = await HttpContext.AuthenticateAsync();
}
}
I direct the user to /auth/token, where he is redirected to the Google Oauth Page, if successful, he is redirected to /auth/retrieve, where I expect the user data, but token, externalLoginInfoAsync, identityName, authenticateResult is null
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString("Default")));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication()
.AddCookie()
.AddGoogle(options =>
{
options.Scope.Add("https://www.googleapis.com/auth/gmail.settings.basic");
options.AccessType = "offline";
options.SaveTokens = true;
options.SignInScheme = IdentityConstants.ExternalScheme;
options.Events.OnCreatingTicket = ctx =>
{
var identityName = ctx.Identity.Name;
return Task.CompletedTask;
};
options.ClientId = "SMTH_VALUE";
options.ClientSecret = "SMTH_VALUE";
});
services.AddControllers();
}
I debug the google provider and found the user values in the Events - identityName is not null.
How i can get this value in the controller?
You could refer the following code to configure Google authentication in Startup.ConfigureServices method:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddAuthentication()
.AddGoogle(opt =>
{
opt.ClientId = "620831551062-rcvu44q4rhr5d8ossu3m0163jqbjdji0.apps.googleusercontent.com";
opt.ClientSecret = "GXFN0cHBbUlZ6nYLD7a7-cT8";
opt.SignInScheme = IdentityConstants.ExternalScheme;
});
services.AddControllersWithViews();
services.AddRazorPages();
}
Then, use the following sample to login using Google and get user information:
[Authorize]
public class AccountController : Controller
{
private UserManager<ApplicationUser> userManager;
private SignInManager<ApplicationUser> signInManager;
public AccountController(UserManager<ApplicationUser> userMgr, SignInManager<ApplicationUser> signinMgr)
{
userManager = userMgr;
signInManager = signinMgr;
}
// other methods
public IActionResult AccessDenied()
{
return View();
}
[AllowAnonymous]
public IActionResult GoogleLogin()
{
string redirectUrl = Url.Action("GoogleResponse", "Account");
var properties = signInManager.ConfigureExternalAuthenticationProperties("Google", redirectUrl);
return new ChallengeResult("Google", properties);
}
public IActionResult Login()
{
return View();
}
[AllowAnonymous]
public async Task<IActionResult> GoogleResponse()
{
ExternalLoginInfo info = await signInManager.GetExternalLoginInfoAsync();
if (info == null)
return RedirectToAction(nameof(Login));
var result = await signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, false);
string[] userInfo = { info.Principal.FindFirst(ClaimTypes.Name).Value, info.Principal.FindFirst(ClaimTypes.Email).Value };
if (result.Succeeded)
return View(userInfo);
else
{
ApplicationUser user = new ApplicationUser
{
Email = info.Principal.FindFirst(ClaimTypes.Email).Value,
UserName = info.Principal.FindFirst(ClaimTypes.Email).Value
};
IdentityResult identResult = await userManager.CreateAsync(user);
if (identResult.Succeeded)
{
identResult = await userManager.AddLoginAsync(user, info);
if (identResult.Succeeded)
{
await signInManager.SignInAsync(user, false);
return View(userInfo);
}
}
return AccessDenied();
}
}
}
The result like this:
More detail information, see How to integrate Google login feature in ASP.NET Core Identity and Google external login setup in ASP.NET Core

Custom Authorization attribute doesn't allow authorize in asp.net core 3

.Net core app authenticates with IdentityServer. I'm working without https.
I created custom authorization attribute, but it doesn't allow authorize. And I can't catch a problem.
Logs:
dbug: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[8]
AuthenticationScheme: Cookies was successfully authenticated.
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler: Debug: AuthenticationScheme: Cookies was successfully authenticated.
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService: Information: Authorization failed.
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[13]
AuthenticationScheme: Cookies was forbidden.
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler: Information: AuthenticationScheme: Cookies was forbidden.
info: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[13]
AuthenticationScheme: oidc was forbidden.
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler: Information: AuthenticationScheme: oidc was forbidden.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddAntiforgery(options =>
{
options.Cookie.HttpOnly = false;
options.HeaderName = "XSRF-TOKEN";
});
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies", options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.Name = "app";
options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
options.SlidingExpiration = true;
})
.AddOpenIdConnect("oidc", options =>
{
options.Authority = $"{OpenId["ServerUrl"]}";
options.RequireHttpsMetadata = false;
options.ClientId = OpenId["ClientId"];
options.ClientSecret = OpenId["ClientSecret"];
options.ResponseType = "code";
options.UsePkce = true;
options.SignInScheme = "Cookies";
options.CallbackPath = "/signin-callback";
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("offline_access");
options.Scope.Add("profile");
options.Scope.Add("role");
options.Scope.Add("myapp");
options.ClaimActions.MapJsonKey("website", "website");
options.ClaimActions.MapJsonKey(UserClaimTypes.Role, UserClaimTypes.Role);
options.ClaimActions.MapJsonKey(UserClaimTypes.UserId, UserClaimTypes.UserId);
options.SaveTokens = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = UserClaimTypes.Role
};
options.Events = new OpenIdConnectEvents
{
OnTicketReceived = (e) =>
{
e.Properties.IsPersistent = true;
e.Properties.ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(60);
return Task.CompletedTask;
}
};
});
services.AddSingleton<IAuthorizationPolicyProvider, AuthPolicyProvider>();
services.AddSingleton<IAuthorizationHandler, ArticlePermissionHandler>();
services.AddControllersWithViews(o =>
{
o.Filters.Add(typeof(ModelStateValidator));
o.InputFormatters.Insert(0, GetJsonPatchInputFormatter());
})
.AddNewtonsoftJson();
}
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
IDistributedCache cache,
IHttpContextAccessor httpContextAccessor,
ILogger<Startup> logger,
IAntiforgery antiforgery)
{
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllers();
});
ArticlePermissionRequirement.cs
using System;
using Microsoft.AspNetCore.Authorization;
namespace App.Application.Services.Authorization.Policies.Requirements
{
public class ArticlePermissionRequirement : IAuthorizationRequirement
{
public string PermissionName { get; }
public ArticlePermissionRequirement(string permissionName)
{
PermissionName = permissionName ?? throw new ArgumentNullException(nameof(permissionName));
}
}
}
AuthPolicyProvider.cs
using System.Threading.Tasks;
using App.Application.Services.Authorization.Operations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
namespace App.Application.Services.Authorization.Policies.Providers
{
public class AuthPolicyProvider : DefaultAuthorizationPolicyProvider
{
public AuthPolicyProvider(IOptions<AuthorizationOptions> options) : base(options)
{
}
public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
var policy = await base.GetPolicyAsync(policyName);
if (policy == null)
{
policy = new AuthorizationPolicyBuilder()
.AddRequirements(RequirementOperations.Requirement(policyName))
.Build();
}
return policy;
}
}
}
ArticlePermissionHandler.cs
namespace App.Application.Services.Authorization.Policies.Handlers
{
public class ArticlePermissionHandler : AuthorizationHandler<ArticlePermissionRequirement, Owner>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ArticlePermissionRequirement requirement,
Owner resource)
{
// for test purpose (always true) I putted this
context.Succeed(requirement);
var user = context.User;
if (user == null)
{
return Task.CompletedTask;
}
var roleClaim = user.Claims.FirstOrDefault(
c => c.Type == ClaimTypes.Role
)?.Value;
if (roleClaim == null)
{
return Task.CompletedTask;
}
// Administrator and Manager can do all things
if (user.IsInRole(Roles.Administrator) || user.IsInRole(Roles.Manager))
{
context.Succeed(requirement);
}
// Moderator can read, create, update all articles
else if (Equals(requirement.PermissionName, Permissions.ReadArticle)
|| Equals(requirement.PermissionName, Permissions.CreateArticle)
|| Equals(requirement.PermissionName, Permissions.UpdateArticle))
{
if (user.IsInRole(Roles.Moderator))
{
context.Succeed(requirement);
}
}
// Writer can create new article
else if (Equals(requirement.PermissionName, Permissions.CreateArticle))
{
if (user.IsInRole(Roles.Writer))
{
context.Succeed(requirement);
}
}
// Onwer can read, update, delete self article
else if (Equals(requirement.PermissionName, Permissions.ReadArticle)
|| Equals(requirement.PermissionName, Permissions.UpdateArticle)
|| Equals(requirement.PermissionName, Permissions.DeleteArticle))
{
ClaimsPrincipal claimUser = context.User;
var userClaimExtractor = new UserClaimExtractor(claimUser);
var userId = userClaimExtractor.GetId();
if (resource.OwnerId == userId)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
}
HasPermissionAttribute.cs
using Microsoft.AspNetCore.Authorization;
namespace App.Application.Services.Authorization.Policies.Attributes
{
public class HasPermissionAttribute : AuthorizeAttribute
{
public HasPermissionAttribute(string permissionName)
{
PermissionName = permissionName;
}
public string PermissionName
{
get
{
return Policy;
}
set
{
Policy = value;
}
}
}
}
Controller
public class DraftController : ApiController
{
public DraftController(IMediator mediator,
IHttpContextAccessor context) : base(mediator, context)
{
}
[HasPermission(Permissions.CreateArticle)]
[HttpPost("drafts")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Drafts([FromBody] JsonPatchDocument<DraftDto> patchDoc)
{
...
}
But there is an interest thing - when I rewrite attribute with authorize:
[Authorize]
[HttpPost("drafts")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Drafts(
Then app is authorized, and I can work with action Drafts. But with HasPermission attribute authorization always failed.
Where I can find a problem? Why the log writes: "Cookies was successfully authenticated" and then "Cookies was forbidden"?
Yes! I got! Problem was with resource Owner.
AuthorizationHandler<ArticlePermissionRequirement, Owner>
After remove this resource ArticlePermissionHandler started to work.
AuthorizationHandler<ArticlePermissionRequirement>
This info helped understand the problem.

How to delete cookie on browser close

I'm implementing asp.net core project and I authenticate the user via ldap by using Novell.Directory.Ldap.NETStandard. Now my problem is How can I delete cookie after the user close the browser. I want whenever the user close the browser and opens the system again, he confronts with login page. here below is what I've tried in startup for setting cookie:
services.AddScoped<Ldap.IAuthenticationService, LdapAuthenticationService>();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(15);
options.LoginPath = "/Account/Login";
options.SlidingExpiration = true;
});
And here is my AccountController:
public class AccountController : Controller {
private readonly Ldap.IAuthenticationService _authenticationService;
private readonly IHttpContextAccessor _httpContext;
public AccountController(Ldap.IAuthenticationService authenticationService,IHttpContextAccessor context)
{
_httpContext = context;
_authenticationService = authenticationService;
}
public IActionResult Login()
{
return View();
}
[HttpPost]
// [ChildActionOnly]
public async Task<IActionResult> Login(LoginModel model)
{
LdapEntry result1 = null;
var result = _authenticationService.ValidateUser1("mm.fr", model.UserName, model.Password);
if (result.Equals(true))
{
result1 = _authenticationService.GetLdapUserDetail("mm.fr", model.UserName, model.Password);
ViewBag.Username = result1.GetAttribute("CN").StringValue;
Index(result1.GetAttribute("CN").StringValue);
if (result != null)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, model.UserName),
new Claim(ClaimTypes.Role, "Administrator"),
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
}
}
else
{
this.TempData["ErrorMessage"] = "Password is incorrect";
}
return RedirectToAction(nameof(MainDashboardController.Index), "MainDashboard");
}
public IActionResult Index(string str)
{
_httpContext.HttpContext.Session.SetString("mystr", str);
return View();
}
}
Here below is what I wrote in Startup:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContext<CSDDashboardContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("CSDDashboardContext")));
//Notice this is NOT the same class... Assuming this is a valid DBContext. You need to add this class as well.
//services.AddDbContext<CSSDDashboardContext>(options =>
// options.UseSqlServer(Configuration.GetConnectionString("CSDDashboardContext")));
//*---------------------------New Active Directory--------------------------------
services.AddScoped<IAuthenticationService, LdapAuthenticationService>();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(15);
options.LoginPath = "/Account/Login";
options.SlidingExpiration = true;
});
services.AddSession();
services.AddSingleton<MySharedDataViewComponent>();
services.AddHttpContextAccessor();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseSession();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
function deleteCookie(name) {
setCookie(name,"",-1);
}
function setCookie(name,value,days) {
if (days) {
var date = new Date();
date.setTime(date.getTime()+(days*24*60*60*1000));
var expires = "; expires="+date.toGMTString();
}
else expires = "";
document.cookie = name+"="+value+expires+"; path=/";
}
$(window).unload(function() {
deleteCookie('Your cookie to delete name');
});

Error trying to get authenticate with google in ASP.NET Core 3.1 No authentication handler is registered

The Error
This is my startup class
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// Demand authentication in the whole application
services.AddControllersWithViews(o => o.Filters.Add(new AuthorizeFilter()));
services.AddScoped<IConferenceRepository, ConferenceRepository>();
services.AddScoped<IProposalRepository, ProposalRepository>();
services.AddScoped<IAttendeeRepository, AttendeeRepository>();
services.AddScoped<IUserRepository, UserRepository>();
services.AddDbContext<ConfArchDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
assembly =>
assembly.MigrationsAssembly(typeof(ConfArchDbContext).Assembly.FullName)));
services
.AddAuthentication(options =>
{
options.DefaultScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
GoogleDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.IsEssential = true;
//options.Cookie.SameSite = SameSiteMode.None;
})
.AddGoogle(options =>
{
options.SignInScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
options.ClientId =
Configuration["Authentication:Google:ClientId"];
options.ClientSecret =
Configuration["Authentication:Google:ClientSecret"];
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
//app.UseIdentity();
// app.UseAuthentication();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Conference}/{action=Index}/{id?}");
});
}
}
this is my controller, the error is thrown in the line: var result = await HttpContext.AuthenticateAsync(
public class AccountController : Controller
{
private readonly IUserRepository userRepository;
public AccountController(IUserRepository userRepository)
{
this.userRepository = userRepository;
}
// This method must be anonymous to allow access to not logged users
[AllowAnonymous]
public IActionResult Login(string returnUrl = "/")
{
return View(new LoginModel { ReturnUrl = returnUrl });
}
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginModel model)
{
// Looking for user in local repository in the class userrepository
var user = userRepository.GetByUsernameAndPassword(model.Username, model.Password);
if (user == null)
return Unauthorized();
// Data of the user, claims class is used to represent the data user
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.Role, user.Role),
new Claim("FavoriteColor", user.FavoriteColor)
};
// Object to save in Identity object type ClaimsIdentity
var identity = new ClaimsIdentity(claims,
CookieAuthenticationDefaults.AuthenticationScheme);
// Create claims principal object with the Identity
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
principal,
new AuthenticationProperties { IsPersistent =
model.RememberLogin,
ExpiresUtc= DateTime.UtcNow.AddMinutes(10)
});
return LocalRedirect(model.ReturnUrl);
}
[AllowAnonymous]
public IActionResult LoginWithGoogle(string returnUrl = "/")
{
var props = new AuthenticationProperties
{
RedirectUri = Url.Action("GoogleLoginCallback"),
Items =
{
{ "returnUrl", returnUrl }
}
};
return Challenge(props, GoogleDefaults.AuthenticationScheme);
}
[AllowAnonymous]
public async Task<IActionResult> GoogleLoginCallback()
{
// read google identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(
ExternalAuthenticationDefaults.AuthenticationScheme);
var externalClaims = result.Principal.Claims.ToList();
var subjectIdClaim = externalClaims.FirstOrDefault(
x => x.Type == ClaimTypes.NameIdentifier);
var subjectValue = subjectIdClaim.Value;
var user = userRepository.GetByGoogleId(subjectValue);
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.Role, user.Role),
new Claim("FavoriteColor", user.FavoriteColor)
};
var identity = new ClaimsIdentity(claims,
CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
// delete temporary cookie used during google authentication
await HttpContext.SignOutAsync(
ExternalAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme, principal);
return LocalRedirect(result.Properties.Items["returnUrl"]);
}
// Action to logout
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Redirect("/");
}
}
I figure out the answer, the in the configure services code should be so:
// Demand authentication in the whole application
services.AddControllersWithViews(o => o.Filters.Add(new
AuthorizeFilter()));
services.AddScoped<IConferenceRepository, ConferenceRepository>();
services.AddScoped<IProposalRepository, ProposalRepository>();
services.AddScoped<IAttendeeRepository, AttendeeRepository>();
services.AddScoped<IUserRepository, UserRepository>();
services.AddDbContext<ConfArchDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
assembly =>
assembly.MigrationsAssembly(typeof(ConfArchDbContext).Assembly.FullName)));
services.AddAuthentication(o => {
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
//o.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddCookie()
.AddCookie(ExternalAuthenticationDefaults.AuthenticationScheme)
.AddGoogle(o =>
{ // ClienteId and Secret to authenticate the user from Google
o.SignInScheme =
ExternalAuthenticationDefaults.AuthenticationScheme;
o.ClientId = Configuration["Authentication:Google:ClientId"];
o.ClientSecret =
Configuration["Authentication:Google:ClientSecret"];
});

Blazor server-side custom login using Identity lose logged state after restart

I implemented own solution to make login without page refresh. However can't figure out why I am losing logged-in state on application restart (new debug run).
Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DatabaseContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")), ServiceLifetime.Transient);
services.AddIdentity<User, Role>(options =>
{ // options...
}).AddEntityFrameworkStores<DatabaseContext>().AddDefaultTokenProviders();
services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo("/keys")).SetApplicationName("App").SetDefaultKeyLifetime(TimeSpan.FromDays(90));
services.AddRazorPages();
services.AddServerSideBlazor().AddCircuitOptions(options =>
{
options.DetailedErrors = true;
});
services.AddSession();
services.AddSignalR();
services.AddBlazoredLocalStorage();
services.AddHttpClient();
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
services.AddAuthorization(options =>
{ // options...
});
// Add application services.
services.AddScoped<AuthenticationStateProvider, IdentityAuthenticationStateProvider>();
// .. services
services.AddMvc(options =>
{
options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
}).AddSessionStateTempDataProvider();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseSession();
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
AuthorizeController
[HttpPost("Login")]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginViewModel model)
{
if (ModelState.IsValid)
{
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
Microsoft.AspNetCore.Identity.SignInResult result = await signInMgr.PasswordSignInAsync(model.LEmail, model.LPassword, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
return Ok();
return BadRequest(string.Join(", ", ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage)));
}
return BadRequest();
}
[HttpGet("UserInfo")]
public UserInfo UserInfo()
{
return new UserInfo
{
IsAuthenticated = User.Identity.IsAuthenticated,
UserName = User.Identity.Name,
ExposedClaims = User.Claims.ToDictionary(c => c.Type, c => c.Value)
};
}
I believe problem is in my AuthenticationStateProvider implementation
public class IdentityAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
readonly AuthorizeService authorizeSvc;
readonly IServiceScopeFactory scopeFactory;
readonly IdentityOptions options;
UserInfo userInfoCache;
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
public IdentityAuthenticationStateProvider(AuthorizeService authorizeService, ILoggerFactory loggerFactory, IServiceScopeFactory serviceScopeFactory, IOptions<IdentityOptions> optionsAccessor) : base(loggerFactory)
{
authorizeSvc = authorizeService;
scopeFactory = serviceScopeFactory;
options = optionsAccessor.Value;
}
public async Task LoginAsync(LoginViewModel model)
{
await authorizeSvc.LoginAsync(model);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task RegisterAsync(RegisterViewModel register)
{
await authorizeSvc.RegisterAsync(register);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task LogoutAsync()
{
await authorizeSvc.LogoutAsync();
userInfoCache = null;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
async Task<UserInfo> GetUserInfoAsync()
{
if (userInfoCache != null && userInfoCache.IsAuthenticated)
return userInfoCache;
userInfoCache = await authorizeSvc.GetUserInfo();
return userInfoCache;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
ClaimsIdentity identity = new ClaimsIdentity();
try
{
UserInfo userInfo = await GetUserInfoAsync();
if (userInfo.IsAuthenticated)
{
IEnumerable<Claim> claims = new[] { new Claim(ClaimTypes.Name, userInfoCache.UserName) }.Concat(userInfoCache.ExposedClaims.Select(c => new Claim(c.Key, c.Value)));
identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
}
}
catch (HttpRequestException ex)
{
Console.WriteLine("Request failed:" + ex.ToString());
}
return new AuthenticationState(new ClaimsPrincipal(identity));
}
protected override async Task<bool> ValidateAuthenticationStateAsync(AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user manager from a new scope to ensure it fetches fresh data
IServiceScope scope = scopeFactory.CreateScope();
try
{
UserManager<User> userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
}
finally
{
if (scope is IAsyncDisposable asyncDisposable)
await asyncDisposable.DisposeAsync();
else
scope.Dispose();
}
}
async Task<bool> ValidateSecurityStampAsync(UserManager<User> userManager, ClaimsPrincipal principal)
{
User user = await userManager.GetUserAsync(principal);
if (user is null)
return false;
else if (!userManager.SupportsUserSecurityStamp)
return true;
string principalStamp = principal.FindFirstValue(options.ClaimsIdentity.SecurityStampClaimType);
string userStamp = await userManager.GetSecurityStampAsync(user);
return principalStamp == userStamp;
}
}
AuthorizeService just calls httprequests like
public async Task<UserInfo> GetUserInfo()
{
HttpContext context = contextAccessor.HttpContext;
HttpClient client = clientFactory.CreateClient();
client.BaseAddress = new Uri($"{context.Request.Scheme}://{context.Request.Host}");
string json = await client.GetStringAsync("api/Authorize/UserInfo");
return Newtonsoft.Json.JsonConvert.DeserializeObject<UserInfo>(json);
}
I noticed in Chrome developer tools that cookies is unchanged after login. This is probably main issue. Any idea how to fix it?
Thanks
AuthorizeService has to pass cookies. For server-side on prerendering pass cookies from HttpContext, at runtime pass cookies from javascript via IJSRuntime injection.
Implemntation looks like for custom AuthenticationStateProvider
async Task<string> GetCookiesAsync()
{
try
{
return $".AspNetCore.Identity.Application={await jsRuntime.InvokeAsync<string>("getLoginCookies")}";
}
catch
{
return $".AspNetCore.Identity.Application={httpContextAccessor.HttpContext.Request.Cookies[".AspNetCore.Identity.Application"]}";
}
}
public async Task<UserInfo> GetUserInfoAsync()
{
if (userInfoCache != null && userInfoCache.IsAuthenticated)
return userInfoCache;
userInfoCache = await authorizeSvc.GetUserInfo(await GetCookiesAsync());
return userInfoCache;
}
AuthorizeService
public async Task<UserInfo> GetUserInfo(string cookie)
{
string json = await CreateClient(cookie).GetStringAsync("api/Authorize/UserInfo");
return Newtonsoft.Json.JsonConvert.DeserializeObject<UserInfo>(json);
}
HttpClient CreateClient(string cookie = null)
{
HttpContext context = contextAccessor.HttpContext;
HttpClient client = clientFactory.CreateClient();
client.BaseAddress = new Uri($"{context.Request.Scheme}://{context.Request.Host}");
if(!string.IsNullOrEmpty(cookie))
client.DefaultRequestHeaders.Add("Cookie", cookie);
return client;
}
There is also need to mention need to do following steps for following actions
Login/Register
SignIn
GetUserInfo (including cookies)
Write cookie via javascript
Logout
Remove cookie via javascript
SignOut