Authorization doesn't work on Razor pages outside of Areas/Identity, cannot figure out why:
Steps to reproduce:
Fresh app. ASP.NET Core Blazor WASM Hosted
Create page: Areas/Identity/Index2.cshtml
Create page: Areas/NewArea/NewPage.cshtml
Register and Log in.
Access Index2 and NewPage and break in OnGet method while debugging.
Index2: property User.Claims contains multiple claims.
NewPage: property User.Claims is empty
Why the authorization is not "propagated" into NewPage?
When I set #page "/Identity/NewPage" it works so it is somehow related to Identity Area (??).
EDITS:
(Placing page into Pages folder of the server project results the same (no auth))
Reason why do I care:
For quick loading speed I want my index page (Index.cshtml) to be razor page (not Blazor wasm). I need to display login status on index page.
More research:
I have found exactly the same (unresolved, but closed) issue in aspnetcore repo. https://github.com/dotnet/aspnetcore/issues/34080
This issue describe the same scenario I have: Authorise normal Razor Pages in a Blazor WebAssemby App? and there is also a solution:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
})
Another same issue on SO: Blazor WebAssembly with server side Razor page authorize problem
But I still don't understand why is this happening.
EDIT - Solution
The solution with changing DefaultAuthenticateScheme didn't work well after all - it break authorization for api controllers and wasm app.
This may be caused by my setup - I am not actually using server app for "hosting" the Blazor. Blazor app is just SPA using server app for auth and api.
Anyway: Instead of changing default schema for "everything" I change it only for particular .cshtml pages:
[AllowAnonymous]
[Authorize(AuthenticationSchemes = "Identity.Application")]
With that ↑ everything works. But you have to paste this into every .cshtml page... Or - you can configure it in Program.cs:
//add policy which uses ApplicationScheme
services.AddAuthorization(config =>
{
config.AddPolicy("RazorPagesAuthPolicy",
policyBuilder =>
{
policyBuilder.AddAuthenticationSchemes(IdentityConstants.ApplicationScheme);
policyBuilder.RequireAssertion(context => true);//has to be here
});
}
//...
services.AddAuthentication()//keep this
.AddIdentityServerJwt();
//...
services.AddRazorPages(cfg =>
{
cfg.Conventions.AddFolderApplicationModelConvention("/", mo =>
{//add authorize and allowAnonymous filters
mo.Filters.Add(new AuthorizeFilter("RazorPagesAuthPolicy"));
mo.Filters.Add(new AllowAnonymousFilter());
}
);
}
);
which I am not sure if it is way to go
As I've previously mentioned, you can either use cookies authentication or Jwt authentication in order to access the ClaimsPrincipal from the Server project. The solution proposed is fine, and it is used to configure the cookies middleware used by the identity system. Incidentally, it should be:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
}).AddIdentityServerJwt();
But I still don't understand why is this happening
Do you mean why the code above works ?
The code above works because it configures the cookies middleware to authenticate the user, tells the Identity System what scheme to use, and how to challenge the user. Understand this, the Server side of your app is not authenticated, unless you configure it to perform authentication, which differs from the authentication performed when you are re-directed from the WebAssembly Blazor App. I'll try to clarify this by giving you this example:
If you created a Blazor Server App with individual accounts, and then added client SignalR to enable a chat system in your app, and you want to secure your hub object with the Authorize attribute, an authenticated user to your Blazor Server App won't be able to access secured end points on the hub, unless he is authenticated on the hub as well. How to authenticate the user ? Simply by passing the application cookie to the hub. This is more or less simialr to the issue under discussion; that is, though the Identity folder and its content are authenticated (and authorized), the hosting Server App is not.
Note: Its hard to internalized this on first shot, but rest assured that the proposed code not only works but is also 100% kosher.
Related
I need to access some Google APIs (via Google.Apis.* NuGet Packages). Therefore I need to use the Google.Apis.Auth.AspNetCore package as described in the
official documentation:
services
.AddAuthentication(o =>
{
o.DefaultChallengeScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
o.DefaultForbidScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddGoogleOpenIdConnect(options =>
{
options.ClientId = googleClientId;
options.ClientSecret = googleClientSecret;
});
On the other hand, I use classical ASP.NET Core Identity, especially the Google external login setup
using the
Microsoft.AspNetCore.Authentication.Google NuGet package which is initialized like this:
services
.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = googleClientId;
options.ClientSecret = googleClientSecret;
});
Is there a way to share the OAuth configurations, logins, ...? 🤔 Both packages use their own OAuth intialization code... 💭 Will I face problems calling both AddGoogle() and AddGoogleOpenIdConnect()?
Background info
Authorization is OAuth2. You would use it for example to request access to a users google drive account. Once you have been granted access you would be given a refresh token and you would be able to access the users data when ever you need.
Open id connect its authentication or login. It tells you that a user is in fact logged in it doesn't give you access to more then the users profile information by default.
Google.Apis.Auth.AspNetCore
Strictly speaking the Google APIs library normally only gave you access to authorization. However Google.Apis.Auth.AspNetCore does add an open id connect component to to the authentication in the form of AddGoogleOpenIdConnect which will give you both authentication and authorization at the same time.
Microsoft.AspNetCore.Authentication.Google
Is as its states Authentication or login. The user is logging in and authenticating that they are currently accessing your application.
recap
Microsoft.AspNetCore.Authentication.Google only gives you authentication or login, while Google.Apis.Auth.AspNetCore will potentially give you login and authorization.
Adding both
TBH I question why you would add both. Technically if you did and they didn't bash heads with each other which they might. You may end up with a double login. As these two libraries will both be trying to Authenticate the user. You will need two access tokens one for each library. I'm not sure if they set different cookies or not if they set the same cookie then they will reset each other cookies causing the user to need to login or authorize the application again to reset the cookie for each library you are trying to use.
Not to mention what adding [Authorize] attribute to a method is going to do how will it know which one to pick if your adding to authorization forms
Using .net Core 3.1 I followed the instructions in the official docs (Scaffold Identity in ASP.NET Core projects) to scaffold Identity onto my empty Web project.
I'm using claims-based authorization. In my Startup.cs file I created the following policy:
services.AddAuthorization(options =>
{
options.AddPolicy("Admin", policy =>
{
policy.RequireClaim("CanListUsers", true.ToString());
policy.RequireClaim("CanEditUsers", true.ToString());
});
});
and immediately after that I am gating certain Area pages, as follows:
services.AddRazorPages(options =>
{
options.Conventions.AuthorizeAreaPage("Identity", "/Account/Register", "Admin");
options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage");
});
I want to shut down registration to "Admin" users only. And I want to allow only signed in users to manage accounts.
I can't gate the Registration page. I can still access it without a logged-in user. Why not?
On the other hand, strangely, if I remove the authorization convention /Account/Manage, the page will redirect anonymous users to Login. How is that happening?
I think I figured out both questions.
I can't gate the Registration page. I can still access it without a logged-in user. Why not?
The page was decorated with [AllowAnonymous]. 🤦 Clearly decorators have higher priority than Conventions set at configuration time.
On the other hand, strangely, if I remove the authorization convention /Account/Manage, the page will redirect anonymous users to Login. How is that happening?
I noticed that all the pages that should be accessible by not-logged-in users are decorated with [AllowAnonymous]. I'm thinking that the scaffolding, being an Identity template, assumes the implementer will eventually add
app.UseAuthentication();
app.UseAuthorization();
My thinking here is that these Middlewares automatically clamp down all pages, essentially strapping an [Authorize] attribute on all pages so that a page that can allow not-logged-in users needs to opt-out with a [AllowAnonymous].
These are not much more than educated guesses, so please feel free to correct me in the comments 👍
Short version: I am having trouble in merging together the correct Authentication config in my .NET Core MVC Website to allow my users to authenticate against Azure Active Directory, but to also allow a Daemon connection (from a Console App) in, too.
Long version:
I've got a .NET Core MVC website, which authenticates against Azure Active Directory perfectly fine when using the following in the ConfigureServices method in the Startup.cs:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddSignIn("AzureAd", Configuration, options => Configuration.Bind("AzureAd", options));
I am also trying to get my .NET Core Console App to call into the APIs (as a Daemon connection) into the above MVC website (all is configured in the App Registration section in my Microsoft Azure account). I can connect the Console App to the MVC website and it will successfully hit an Action Result in a controller but only if I am using the following in the ConfigureServices method in the Startup.cs of the website:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddProtectedWebApi("AzureAd", Configuration, options => Configuration.Bind("AzureAD", options));
BASICALLY, if I only use the OpenIdConnect option, my web users can access the website but my console app is denied. If I only use the JwtBearer option, then my Console App can connect, but my web users are denied.
I have Google-Bing'd all day and I'm struggling to get a mash-up of these two configurations to work without knocking the other out.
I have tried to use the .AddJwtBearer() method, but am completely confused by it:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddSignIn("AzureAd", Configuration, options => Configuration.Bind("AzureAd", options))
.AddJwtBearer(options => Configuration.Bind("AzureAD", options));
How do these work together, such that both can be in place and my web app works through a browser, and the Console App (Daemon) works too? Can I bind both to my appsettings.json file??
Incidentally, the appsettings.json file looks like this:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "zzzzzzzzzzzzzz.onmicrosoft.com",
"TenantId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
"ClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath ": "/signout-callback-oidc",
"ClientSecret": "myAzureClientSecret"
}
}
UPDATE 2020-06-15:
Having working on/off of this for AGES, I've found a suitable resolution that works, hence my awarding the bounty points to #michael-shterenberg. ALSO, I now know that I have a great deal to learn from #gary-archer and his impressive blog site. I just happened to get success from Michael's input.
Here's the mods to the Startup.cs file, within the ASP.NET Core MVC Web App in the diagram above:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddSignIn("AzureAd", Configuration, options =>
Configuration.Bind("AzureAd", options))
.AddJwtBearer(o =>
{
o.Authority = "https://login.microsoftonline.com/common";
o.TokenValidationParameters.ValidateAudience = false;
o.TokenValidationParameters.ValidateIssuer = false;
});
services.AddAuthorization(options =>
{
options.AddPolicy("UserAndApp", builder =>
{
builder.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
builder.AuthenticationSchemes.Add(OpenIdConnectDefaults.AuthenticationScheme);
builder.RequireAuthenticatedUser();
});
});
...coupled with the use of the following attribute on the Controller that I'm trying to call from the Daemon app.
[Authorize("UserAndApp")]
My users can still log into the website using the Azure Active Directory processes and now my automated processes can log in, too.
In case anyone is struggling to understand how the Azure App Registration side of all of this works, try this really explanatory blog post:
Secure a .NET Core API using Bearer Authentication
(I wish that I had seen that earlier, when I was trying to get my head around how the Azure App Registration process works!)
Here is the solution that worked for me (Tested on ASP .NET Core 2.1 and 3.1)
Don't set a default authentication scheme since you have 2 types (Cookies and JWT). i.e. your call to AddAuthentication should be without parameters:
services.AddAuthentication()
.AddAzureAD(options => Configuration.Bind("AzureAd", options))
.AddJwtBearer(o=> {
o.Authority = "https://login.microsoftonline.com/common";
o.TokenValidationParameters.ValidateAudience = false;
o.TokenValidationParameters.ValidateIssuer = false;
});
Note that I explicitly didn't bind your AD configuration because /common is needed to be applied to the authority (or the tenant id)
Also I set validation for audience and issuer to false so that any AAD token will work for testing. You should obviously set the correct audience/issuer
I used AddAzureAd and not AddSignIn (is that a custom external library you are using?)
Create a policy that accepts both authentication schemes:
services.AddAuthorization(options =>
{
options.AddPolicy("UserAndApp", builer =>
{
builer.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
builer.AuthenticationSchemes.Add(AzureADDefaults.AuthenticationScheme);
builer.RequireAuthenticatedUser();
});
});
Replace this with your existing authorization setup
Use the new policy name in your controller:
[Authorize("UserAndApp")]
public class HomeController : Controller
Some explanation on the mechanics:
You don't want to setup automatic authentication scheme since this will be the default schema run in the authorization middleware, while you have 2 different types
The policy will try run both authentication handlers, if one of them succeeds then authentication succeeded
Note: if you send a request with an invalid Bearer token, both authetnication handlers will fail, in this case the AzureADDefaults will "win" since it actually implement a challenge method and will redirect you (status code 302), so make sure to handle this in your app
It feels like the architecture is not quite right, and you need to separate the 2 roles performed by your Web Back End:
CURRENT WEB ONLY ARCHITECTURE
You have a Web UI front end that uses auth cookies
You have a Web Back End that requires cookies for view requests
You have a Web Back End that requires cookies for API requests
You have a Console Client that cannot use cookies so cannot call API entry points
MULTI CLIENT ARCHITECTURE
You'll need to update the web back end to include API entry points that are secured by OAuth 2.0 access tokens and not by cookies. The console app will then be able to call your web back end.
.NET CORE SUB PATHS
Introduce an additional /api subpath in your web back end. The UseWhen feature will allow you to do this without impacting other web back end behaviour:
/*
* Apply API behaviour to only subpaths, without impacting the rest of the app
*/
app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments(new PathString("/api")),
api => {
api.useAuthentication();
api.useJwtBearer();
});
EXAMPLE .NET CORE API
For an example that uses subpaths, see my Sample .Net Core API. The startup class is where ASP.Net middleware is wired with different authenticaiton handling for different subpaths.
FUTURE POSSIBILITIES
Once you have the above logical separation you could potentially evolve it further in future, eg to a completely cookieless model:
Develop APIs as completely independent components based only on tokens
Update the Web UI to an SPA that uses client side security libraries
My blog at https://authguidance.com follows this approach, and my sample APIs all support any type of client.
I'm working on a dotnet core/js application in which all of the frontend code including authentication page is produced with a JS framework where the bundled frontend is injected into a single Razor page. The frontend accesses all dotnet core logic from controller api endpoints. The authentication utilize bearer tokens and the [Authorize(role='whatever')] is used in several controllers.
For some reason, a single javascript plugin is injected into the app from the razor page. I basically need this plugin not to be visible before the user is authenticated. It's not a matter of "secret" content but the plugin just isn't useful before the user is authenticated. Is there a simple way for me to find out if the user is authorized within the CSHTML of the code as with the controller attributes? I've been searching around and found nothing very useful. Not an expert on c# or dotnet core (mostly been working on the frontend) and simplest possible solution would be appreciated. Don't know if I've missed any relevant information. If so, tell me!
Depending on the complexity of your condition, you have a few ways to authorize the user.
If you just want to verify that the user is authenticated, i.e. that they are successfully logged in, regardless of who the user is, then you can just check against User.Identity.IsAuthenticated:
#if (User.Identity.IsAuthenticated)
{
<p>User is signed in.</p>
}
If you want to verify that the user has a specific role, then you can also operate on the User principal object directly and just check for that specific role:
#if (User.IsInRole("role-name"))
{
<p>User is in role <em>role-name</em>.</p>
}
Otherwise, if you want to utilize the full capability of the authorization system in ASP.NET Core, you can also inject the IAuthorizationService and authorize the user against a configured authorization policy:
#inject IAuthorizationService _authorizationService
#if ((await _authorizationService.AuthorizeAsync(User, "policy-name")).Succeeded)
{
<p>User was authorized against policy <em>policy-name</em>.</p>
}
Is it possible to use SignInManager without having some HTTPContext ? I'm making a Blazor server-side app and I need to make end-users signed in using PasswordSignInAsync() method of SignInManager.
If there is other ways to do it with cookies or something else, I'll take it too, as long they are "clean" methods.
If I could get also an explanation on how to configure for Startup.cs the solution, it will be perfect.
I'd strongly suggest that you use the Identity authentication system in your app.
You should not try to create any authentication system instead. This is something free, and is set up within a couple of minutes. Don't waste your time to do something so complicated. Instead learn what are the Blazor Authorization components and how to use them in your application. They are great.
Note: The communication between Blazor Server and its client-side is done through SignaleR. HttpContext is not available most of the time. Do not try to use the HttpContext. Actually, you can't because whenever you try to access it, it is null.
The following describe how you can create a Blazor Server App with the Identity UI:
Start creating a Blazor App
In the window titled Create a new Blazor app do this:
Select Blazor Server App
On the right side of the window is a link with the text Change, under the Authentication title. Tap the link and select Individual User Account. Press OK...
Click the "Create" button
Visual Studio has created for you a Blazor Server App with Identity UI to authenticate your users. Note that the Identity UI is actually the Razor Pages Identity UI used with Razor Pages and MVC. You may scaffold one or more items from this system if you need to make some changes. Go to solution explorer and verify that Visual Studio has added two folders named Areas and Data. She also has configured your Startup class with the necessary services to mange the Identity UI. What you want now is to create the database where user names, roles, claims, etc. are stored. To create the database you should use migrations. Here is a link telling you how to run the commands that do the job for you. Now you can run your app, register to the web site, login, logout, etc.
Good luck.
To use SignInManager in server-side Blazor,
First, addAddIdentity service into Startup.cs
services.AddIdentity<IdentityUser, IdentityRole>(options => {
options.SignIn.RequireConfirmedAccount = false;
})
.AddEntityFrameworkStores<AppDBContext>()
.AddDefaultTokenProviders();
Then, Inject SignInManager into your razor page
#using Microsoft.AspNetCore.Identity
#inject SignInManager<IdentityUser> SignInManager
#inject NavigationManager navManager;
<!-- Put Login Form Here -->
#code{
protected async Task Submit()
{
var SignInResult= await SignInManager.PasswordSignInAsync("username", "password", true, lockoutOnFailure: false);
if (SignInResult.Succeeded)
{
navManager.NavigateTo("Home", false);
}
}
}
There's another way that nobody has mentioned, so thought I might as well: you could use middleware to login, since middleware carries the full html request and can modify it (for example, by logging in), before it follows the pipeline to Blazor rendering. I use this method for creating temporary guest users for my gaming pages-- which works because the guest doesn't need to type in and submit their own password
as plaintext.
The logistics of making the login secure would probably not be easier than following #enet's advice of using the built-in scaffolded Authentication system, or of logging in using UserManager and re-navigating to your page. You could, however, use a modal box to sign in, rather than having to navigate to the MVC-style login page.