signInManager.GetExternalLoginInfoAsync() returns Null In Blazor - asp.net-core

Im trying to add external login functions to my Sever-side blazor.
so far i could login with a google account and it seems to work great so far.
This is how i setup the authentication for google.
services.AddAuthentication(options => { /* Authentication options */ })
.AddGoogle(options =>
{
// Provide the Google Client ID
options.ClientId = "{MyClientID}";
// Provide the Google Client Secret
options.ClientSecret = "{ClientSecret}";
options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
options.SaveTokens = true;
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
ctx.Properties.StoreTokens(tokens);
return Task.CompletedTask;
};
})
My Problem is i dont know if the claims and additional info's are registered (cause there is no trace of them in my database and i can't retrieve them).
I know that i have to get the external user info via SignInManager.
So in my Blazor component i inject the SignManager like this:
#inject SignInManager<ApplicationUser> signInManager
then i call the ExternalInfo Like this:
var result= await signInManager.GetExternalLoginInfoAsync();
But the result is always null. What do i do wrong? Why is it always null?
A Quick update:
I tested a Razor Page. this works fine on razor pages. so signInManager.GetExternalLoginInfoAsync();
returns null when im calling it from a blazor component.

According to your description, I suggest you could use IHttpContextAccessor instead of using SignInManager to get the user information.
More details, you could refer to below codes:
Add HttpContextAccessor service in ConfigureServices method:
services.AddHttpContextAccessor();
Then use it in component:
#page "/"
#using Microsoft.AspNetCore.Http
#inject IHttpContextAccessor accessor
<h1>Hello, world!</h1>
Welcome to your new app.
<h1>
#username
</h1>
#code{
public string username;
protected override async Task OnInitializedAsync()
{
username = accessor.HttpContext.User.Identity.Name;
}
}
Result:

Related

Blazor WASM Authentication and Authorization on Components and Controllers

I am developing a Blazor WASM with authentication and authorization. The idea is that the user need to login in order to be able to view the Components of the Client Project but also to be able to consume data of Controllers from Server Project which are behind the /api.
Currently I have implemented the restriction on Client components:
<AuthorizeView>
<NotAuthorized>
<div class="row">
<div class="col-md-4">
<p>Please sign in to use the Platform...</p>
</div>
</div>
</NotAuthorized>
<Authorized>
#Body
</Authorized>
</AuthorizeView>
I have also a Login and a Logout Page which are storing a Cookie for later use and perform a custom AuthenticationStateProvider
await LocalStorage.SetItemAsync<int>($"{Parameters.application}_{Parameters.enviroment}_userid", authentication.user_id);
await LocalStorage.SetItemAsync<string>($"{Parameters.application}_{Parameters.enviroment}_username", authentication.user_name);
await AuthStateProvider.GetAuthenticationStateAsync();
The AuthenticationStateProvider code is the following:
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var state = new AuthenticationState(new ClaimsPrincipal());
string authcookie_name = $"{Parameters.application}_{Parameters.enviroment}_username";
string authcookie_value = await _localStorage.GetItemAsStringAsync(authcookie_name);
if (!string.IsNullOrEmpty(authcookie_value))
{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Authentication, authcookie_value)
}, "Login");
state = new AuthenticationState(new ClaimsPrincipal(identity));
}
NotifyAuthenticationStateChanged(Task.FromResult(state));
return state;
}
The authentication controller is the following:
[HttpPost, Route("/api/auth/login")]
public IActionResult AuthLogin(Authentication authentication)
{
try
{
int auth = _IAuth.AuthLogin(authentication);
if (auth != -1)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Authentication, authentication.user_name)
};
var claimsIdentity = new ClaimsIdentity(claims, "Login");
var properties = new AuthenticationProperties()
{
IsPersistent = true
};
HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), properties);
}
return Ok(auth);
}
catch { throw; }
}
Everything is working as excepted and the user need to login in order to see the content of the pages, but he is able to see the data of each page if he perform an http call http://domain.ext/api/model/view
In order to resolve this problem I added the Authorize attribute on each controller of Server project like this:
[Authorize]
[Route("/api/model")]
[ApiController]
public class Controller_Model : ControllerBase
{
}
And also added this code on the Program.cs of Server project in order to be able to make controller to work
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.SlidingExpiration = true;
options.LoginPath = new PathString("/auth/login");
options.LogoutPath = new PathString("/auth/logout");
options.Cookie = new CookieBuilder();
options.Cookie.MaxAge = options.ExpireTimeSpan;
options.AccessDeniedPath = "/";
options.EventsType = typeof(CustomCookieAuthenticationEvents);
});
Now the user is not able to see the content of a page even he is making a request to the /api.
The problem is that after some time, even I see the User is still logged in the Authorize attribute of controllers is consider the user not authorized and it returns an error because controller is not returning the supposed object list.
I have no clue why and when this is happening. Then if user Logout and Login again it works for a while again.
===============UPDATE 1===============
After lot of investigation, seems that the client side is authenticated and then every time it sees the localstorage item it continues to be in authenticated state. On the other side the server state is based on a cookie which expires after 30mins.
So the Client and the Server states are operated differently and that's why the Client seems authenticated while Server is not while denying access on controllers.
I think the solution is to change the CustomAuthenticationStateProvider in order to check if the cookie exists and if it's valid. So the event order be as follow:
User SingIn via Client Page -> Server Controller creates the cookie -> Client Page is authenticated via Authentication State Provider which reads the cookie.
Any ideas?
Seems that is possible to read and write cookies from Client Project only via Javascript. What needs to be done is the following:
A custom javascript file "cookie.js", under wwwroot/js:
export function get() {
return document.cookie;
}
export function set(key, value) {
document.cookie = `${key}=${value}`;
}
A C# class file "CookieStorageAccessor.cs", under /Classes:
public class CookieStorageAccessor
{
private Lazy<IJSObjectReference> _accessorJsRef = new();
private readonly IJSRuntime _jsRuntime;
public CookieStorageAccessor(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
private async Task WaitForReference()
{
if (_accessorJsRef.IsValueCreated is false)
{
_accessorJsRef = new(await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "/js/cookie.js"));
}
}
public async ValueTask DisposeAsync()
{
if (_accessorJsRef.IsValueCreated)
{
await _accessorJsRef.Value.DisposeAsync();
}
}
public async Task<T> GetValueAsync<T>(string key)
{
await WaitForReference();
var result = await _accessorJsRef.Value.InvokeAsync<T>("get", key);
return result;
}
public async Task SetValueAsync<T>(string key, T value)
{
await WaitForReference();
await _accessorJsRef.Value.InvokeVoidAsync("set", key, value);
}
}
The C# class can be used injecting javascript and reading the cookie on
CustomAuthStateProvider:
//CREATE INSTANCE OF COOKIE ACCESSOR
CookieStorageAccessor cookieStorageAccessor = new CookieStorageAccessor(_jSRuntime);
//CHECK IF COOKIE IS EXISTS FROM COOKIE ACCESSOR
string auth_cookie = await cookieStorageAccessor.GetValueAsync<string>("authentication");
if (!string.IsNullOrEmpty(auth_cookie))
{ }
else
{ }

Blazor - B2C authentication - What is the proper way to persist user data on login?

I'm building a Blazor app to see how I can persist user data after a B2C AD login.
I want to persist claim data to sql database (ef 6 core) when the user logs in to the app.
I'm trying to capture a Tenant for the user for use in filtering on the app.
I is custom middleware a good way to go with this?
This is a Blazor Server Side app
I have something like this for testing.
public class PersistUserChangesMiddleware
{
private readonly RequestDelegate _next;
public PersistUserChangesMiddleware(RequestDelegate next)
{
_next = next;
}
[Authorize]
public Task Invoke(HttpContext httpContext, MyContext context)
{
try
{
var user = httpContext.User;
var claims = user.Claims;
var tenant = claims?.FirstOrDefault(c => c.Type.Equals("extension_CompanyId", StringComparison.OrdinalIgnoreCase));
if(tenant != null)
{
context.Tenants.Add(new Models.Tenant()
{
TenantName = tenant.Value
});
context.SaveChanges();
}
}
catch (Exception)
{
throw;
}
return _next(httpContext);
}
}
}
I'm not getting the user back from this call in the middleware. Do I need to do it a different way for Blazor? I set [Authorize] but still no user.
I can't see which AuthenticationStateProvider is configured, but it's likely to be ServerAuthenticationStateProvider.
Create a custom AuthenticationStateProvider which is essentially a pass through provider that just grabs the ClaimsPrincipal user and does whatever you want with it. (Let me know if you're using a different provider).
public class MyAuthenticationStateProvider : ServerAuthenticationStateProvider
{
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var authstate = await base.GetAuthenticationStateAsync();
if (authstate.User is not null)
{
ClaimsPrincipal user = authstate.User;
// do stuff with the ClaimsPrincipal
}
return authstate;
}
}
And then register it in Program:
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
// sequence is crucial - Must be after AddServerSideBlazor
builder.Services.AddScoped<AuthenticationStateProvider, MyAuthenticationStateProvider>();
builder.Services.AddSingleton<WeatherForecastService>();
Test it with a break point on the first line of GetAuthenticationStateAsync.

Can I access my ID Token from an AuthenticationStateProvider in my Blazor client?

I'm trying to access the ID Token provided by Azure AD. I really need to get access to the signature and all of the claims.
I can easily access the AuthenticationStateProvider and then get the User from that. The User property has all of the claims in it, but that seems to be all. I cannot figure out how to get access to the rest of the ID Token.
Here is the code I'm using in the code behind on my blazor page:
[Inject]
AuthenticationStateProvider authenticationState { get; set; }
public async Task<string> GetIDTokenAsync()
{
var authState = await authenticationState.GetAuthenticationStateAsync();
return string.Join(", ", authState.User.Claims.ToList());
}
I'm just returning the list as a string to see what data I'm getting.
Additionally, my bootstrap looks like this:
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
});
await builder.Build().RunAsync();
}
}
After I open the website I log in using the RemoteAuthenticatorView component. I need to access the ID Token after logging in. Also I just want to clarify that I am not looking for the access token. I do not ask for any scopes, and therefore there is no access token. My goal here is to get the ID Token and pass it to my API where the API can verify the ID Token and then issue it's own access token.

A single login page for multiple authentication types (including Azure AD) in .NET Core MVC

I have a .NET Core 2.2 MVC web-application. And I've added two authentication types/providers there:
Login/password with local users database (custom thing, without .NET Core Identity)
Azure AD
My goal is to have a login page at /account/login where users can choose between these two authentications and log-in with either of those. So every time an unauthenticated user would open any page (from a controller with [Authorize] attrubite), he would get redirected to /account/login page, which has a login/password web-form with its own submit button, and additionally a Office 365 login link/button.
Just to make it clear - I don't want a custom Microsoft sign-in / Azure AD page. I only want unauthenticated users to get my login page first, from where they can either log-in using my web-form or click on Office 365 login and get to Microsoft sign-in page.
Now, the authentication part is done and seems to work fine, I can log-in with either of authentications, but my plan with redirecting unauthenticated user to /account/login failed. What happens instead is that user is being redirected to Microsoft sign-in page right away. So it looks like Azure AD authentication has a higher priority somehow.
Here's my implementation.
Startup.cs:
// ...
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
});
// the presence of CookieAuthenticationDefaults.AuthenticationScheme doesn't seem to influence anything
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
// makes no difference either
//services.AddAuthentication(
// options =>
// {
// options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// }
//)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, // and it also can be omitted here
options =>
{
options.LoginPath = "/Account/Login";
options.ExpireTimeSpan = TimeSpan.FromDays(45);
})
.AddAzureAD(options => _configuration.Bind("AzureAD", options));
services.AddAuthorization(options =>
{
// as the default policy, it applies to all [Authorize] controllers
options.DefaultPolicy = new AuthorizationPolicyBuilder(
CookieAuthenticationDefaults.AuthenticationScheme,
AzureADDefaults.AuthenticationScheme
)
.RequireAuthenticatedUser() // a simple policy that only requires a user to be authenticated
.Build();
});
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/";
options.TokenValidationParameters.ValidateIssuer = false;
});
services.AddMvc(options =>
{
// it is my understanding that there is no need create a policy here
// and perform "options.Filters.Add(new AuthorizeFilter(policy))",
// because the default policy is already added and controllers have explicit [Authorize] attribute
// [...] well, actually I tried that too, but it didn't change anything
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory
)
{
// ...
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}"
);
});
}
AccountController.cs:
[Authorize]
[Route("account")]
public class AccountController : Controller
{
// ...
// that is where "Office 365 login" link leads
[HttpGet("login-ad")]
[AllowAnonymous]
public IActionResult LoginAD(string returnUrl = null)
{
if (User.Identity.IsAuthenticated)
{
return RedirectToAction("Index", "Account");
}
else
{
if (string.IsNullOrEmpty(returnUrl)) { returnUrl = "/"; }
return Challenge(
new AuthenticationProperties { RedirectUri = returnUrl },
AzureADDefaults.AuthenticationScheme
);
}
}
[HttpGet("login")]
[AllowAnonymous]
public IActionResult Login(string returnUrl = null)
{
if (User.Identity.IsAuthenticated)
{
return RedirectToAction("Index", "Account");
}
ViewData["ReturnUrl"] = returnUrl;
return View();
}
// that is where login/password web-form submits to
[HttpPost("login")]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
if (ModelState.IsValid)
{
await _usersManager.SignIn(
model.Login,
model.Password
);
// ...
return LocalRedirect(returnUrl);
}
ViewData["ReturnUrl"] = returnUrl;
return View(model);
}
// ...
}
HomeController.cs:
[Authorize]
public class HomeController : Controller
{
// ...
public IActionResult Index()
{
return View();
}
// ...
}
So, opening any page by an unauthenticated user results in immediate redirect to Microsoft sign-in page. And in order to get to /account/login (to have a chance to log-in using another authentication) users have to open that URL explicitly.
If I remove AzureADDefaults.AuthenticationScheme from default policy, then all unauthenticated requests will now get redirected to /account/login - exactly what I want - but naturally Azure AD authentication doesn't work anymore:
These redirects tell me that after successful authentication at Microsoft sign-in page it returns user back to /account/login, but user is still not authenticated on my website.
I can of course add [AllowAnonymous] to Index action of HomeController and return redirect to /account/login for unauthenticated users, but that obviously would only work for / route.
I have a feeling that I don't understand some things about AddAuthentication(), schemes and policies, thus apparently I did something wrong in Startup.cs. Can you please help me to understand what's wrong there? Or maybe there is some other way to achieve what I want?
Updated answer
I decided to clone the example project mentioned here in the quickstart-v2-aspnet-core-webapp documentation and see if I could reproduce your error.
After cloning the project I added two NuGet packages.
Microsoft.AspNetCore.Identity 2.2.0
Microsoft.AspNetCore.Identity.EntityFrameworkCore 2.2.0
Then added the database context that extends IdentityContext.
ApplicationDbContext.cs
In Startup.cs
Registered Identity
Registered the database context and provided connection string
In AppSettings.json
Configured TenantID and ClientID
Ran the application.
At this point, the app launches and redirects me to Account/Login, where I choose Sign in via Microsoft account.
Now, I can obviously see there is something wrong. It wouldn't authenticate the user.
Turns out:
The extension method .AddAzureAd() actually cannot be used in combination with other authentication methods. See this issue on github.
But luckily the workaround is fairly simple. Just switch out .AddAzureAd() for .AddOpenIdConnect() and change your AppSettings' AzureAd section to:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Authority": "https://login.microsoftonline.com/{tenantID}/v2.0/",
"TenantId": "{tenantID}",
"ClientId": "{clientID}",
"CallbackPath": "/signin-oidc"
},
Now I can log in perfectly fine with AzureAD and local user accounts as well.
For your convenience, I uploaded the complete example project to my GitHub page.

How to redirect after Azure AD authentication to different controller action in ASP Net Core MVC

I have setup my ASP Net Core 2.0 project to authenticate with Azure AD (using the standard Azure AD Identity Authentication template in VS2017 which uses OIDC). Everything is working fine and the app returns to the base url (/) and runs the HomeController.Index action after authentication is successful.
However I now want to redirect to a different controller action (AccountController.CheckSignIn) after authentication so that I can check if the user already exists in my local database table and if not (ie it's a new user) create a local user record and then redirect to HomeController.Index action.
I could put this check in the HomeController.Index action itself but I want to avoid this check from running every time the user clicks on Home button.
Here are some code snippets which may help give clarity...
AAD settings in appsettings.json
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "<my-domain>.onmicrosoft.com",
"TenantId": "<my-tennant-id>",
"ClientId": "<my-client-id>",
"CallbackPath": "/signin-oidc" // I don't know where this goes but it doesn't exist anywhere in my app and authentication fails if i change it
}
I added a new action to my AccountController.CheckSignIn to handle this requirement but I cannot find a way to call it after authentication.
public class AccountController : Controller
{
// I want to call this action after authentication is successful
// GET: /Account/CheckSignIn
[HttpGet]
public IActionResult CheckSignIn()
{
var provider = OpenIdConnectDefaults.AuthenticationScheme;
var key = User.FindFirstValue(ClaimTypes.NameIdentifier);
var info = new ExternalLoginInfo(User, provider, key, User.Identity.Name);
if (info == null)
{
return BadRequest("Something went wrong");
}
var user = new ApplicationUser { UserName = User.Identity.Name };
var result = await _userManager.CreateAsync(user);
if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);
if (!result.Succeeded)
{
return BadRequest("Something else went wrong");
}
}
return RedirectToAction(nameof(HomeController.Index), "Home");
}
// This action only gets called when user clicks on Sign In link but not when user first navigates to site
// GET: /Account/SignIn
[HttpGet]
public IActionResult SignIn()
{
return Challenge(
new AuthenticationProperties { RedirectUri = "/Account/CheckSignIn" }, OpenIdConnectDefaults.AuthenticationScheme);
}
}
I have found a way to make it work by using a redirect as follows...
Inside Startup
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Account}/{action=SignIn}/{id?}");
});
Inside AccountController
// GET: /Account/CheckSignIn
[HttpGet]
[Authorize]
public IActionResult CheckSignIn()
{
//add code here to check if AzureAD identity exists in user table in local database
//if not then insert new user record into local user table
return RedirectToAction(nameof(HomeController.Index), "Home");
}
//
// GET: /Account/SignIn
[HttpGet]
public IActionResult SignIn()
{
return Challenge(
new AuthenticationProperties { RedirectUri = "/Account/CheckSignIn" }, OpenIdConnectDefaults.AuthenticationScheme);
}
Inside AzureAdServiceCollectionExtensions (.net core 2.0)
private static Task RedirectToIdentityProvider(RedirectContext context)
{
if (context.Request.Path != new PathString("/"))
{
context.Properties.RedirectUri = new PathString("/Account/CheckSignIn");
}
return Task.FromResult(0);
}
The default behavior is: user will be redirected to the original page. For example, user is not authenticated and access Index page, after authenticated, he will be redirected to Index page; user is not authenticated and access Contact page, after authenticated, he will be redirected to Contact page.
As a workaround, you can modify the default website route to redirect user to specific controller/action:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Account}/{action=CheckSignIn}/{id?}"
);
});
After your custom logic, you could redirect user to your truly default page(Home/Index).
I want to check if the user exists in my local database, not only when Sign in is selected, but also when any other link to my website is clicked which requires authentication.
After a lot of trial and error I found a solution. Not sure if it is the best solution, but it works.
Basically I use the Authorize attribute with a policy [Authorize(Policy = "HasUserId")] as described in Claims-based authorization in ASP.NET Core.
Now when the policy is not met, you can reroute to the register action.
A – very simplified – version of the AccountController would look like this (I use a LogOn action instead of SignIn to prevent conflicts with the AzureADB2C AccountController):
public class AccountController : Controller
{
public IActionResult AccessDenied([FromQuery] string returnUrl)
{
if (User.Identity.IsAuthenticated)
return RedirectToAction(nameof(Register), new { returnUrl });
return new ActionResult<string>($"Access denied: {returnUrl}").Result;
}
public IActionResult LogOn()
{
// TODO: set redirectUrl to the view you want to show when a registerd user is logged on.
var redirectUrl = Url.Action("Test");
return Challenge(
new AuthenticationProperties { RedirectUri = redirectUrl },
AzureADB2CDefaults.AuthenticationScheme);
}
// User must be authorized to register, but does not have to meet the policy:
[Authorize]
public string Register([FromQuery] string returnUrl)
{
// TODO Register user in local database and after successful registration redirect to returnUrl.
return $"This is the Account:Register action method. returnUrl={returnUrl}";
}
// Example of how to use the Authorize attribute with a policy.
// This action will only be executed whe the user is logged on AND registered.
[Authorize(Policy = "HasUserId")]
public string Test()
{
return "This is the Account:Test action method...";
}
}
In Startup.cs, in the ConfigureServices method, set the AccessDeniedPath:
services.Configure<CookieAuthenticationOptions>(AzureADB2CDefaults.CookieScheme,
options => options.AccessDeniedPath = new PathString("/Account/AccessDenied/"));
A quick-and-dirty way to implement the HasUserId policy is to add the UserId from your local database as a claim in the OnSigningIn event of the CookieAuthenticationOptions and then use RequireClaim to check for the UserId claim. But because I need my data context (with a scoped lifetime) I used an AuthorizationRequirement with an AuthorizationHandler (see Authorization Requirements):
The AuthorizationRequirement is in this case just an empty marker class:
using Microsoft.AspNetCore.Authorization;
namespace YourAppName.Authorization
{
public class HasUserIdAuthorizationRequirement : IAuthorizationRequirement
{
}
}
Implementation of the AuthorizationHandler:
public class HasUserIdAuthorizationHandler : AuthorizationHandler<HasUserIdAuthorizationRequirement>
{
// Warning: To make sure the Azure objectidentifier is present,
// make sure to select in your Sign-up or sign-in policy (user flow)
// in the Return claims section: User's Object ID.
private const string ClaimTypeAzureObjectId = "http://schemas.microsoft.com/identity/claims/objectidentifier";
private readonly IUserService _userService;
public HasUserIdAuthorizationHandler(IUserService userService)
{
_userService = userService;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, HasUserIdAuthorizationRequirement requirement)
{
// Load User Id from database:
var azureObjectId = context.User?.FindFirst(ClaimTypeAzureObjectId)?.Value;
var userId = await _userService.GetUserIdForAzureUser(azureObjectId);
if (userId == 0)
return;
context.Succeed(requirement);
}
}
_userService.GetUserIdForAzureUser searches for an existing UserId in the database, connected to the azureObjectId and returns 0 when not found or when azureObjectId is null.
In Startup.cs, in the ConfigureServices method, add the Authorization policy and the AuthorizationHandler:
services.AddAuthorization(options => options.AddPolicy("HasUserId",
policy => policy.Requirements.Add(new HasUserIdAuthorizationRequirement())));
// AddScoped used for the HasUserIdAuthorizationHandler, because it uses the
// data context with a scoped lifetime.
services.AddScoped<IAuthorizationHandler, HasUserIdAuthorizationHandler>();
// My custom service to access user data from the database:
services.AddScoped<IUserService, UserService>();
And finally, in _LoginPartial.cshtml change the SignIn action from:
<a class="nav-link text-dark" asp-area="AzureADB2C" asp-controller="Account" asp-action="SignIn">Sign in</a>
To:
<a class="nav-link text-dark" asp-controller="Account" asp-action="LogOn">Sign in</a>
Now, when the user is not logged on and clicks Sign in, or any link to an action or controller decorated with [Authorize(Policy="HasUserId")], he will first be rerouted to the AD B2C logon page. Then, after logon, when the user is already registered, he will be rerouted to the selected link. When not registered, he will be rerouted to the Account/Register action.
Remark: If using policies does not fit well for your solution, take a look at https://stackoverflow.com/a/41348219.