Using Open ID Connect with Server Side Blazor - asp.net-core

I'd like to use Open ID Connect with Identity Server 4 for authorization in my server side Blazor application. I've got the same setup working in a MVC application.
With the newest .NET Core version, 3.0 Preview 6, it is possible to add the attribute ´#attribute [Authorize]´ to a site. But if I'm not authorized, I don't get redirected to the Identity Server to log in, as I am used from my MVC applications. Instead the site only shows the message "Not authorized".
In Startup.cs I've got the following setup:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "myClient";
options.SaveTokens = true;
});
and
app.UseAuthentication();
How do I tell the application, that I want to be redirected to the Identity Server if I'm not logged in?
EDIT: Codevisions answer works as a workaround. I found pending github issues here and here, planned for .NET Core 3.0 Preview 7 that will possibly cover this issue officially.

Add to ConfigureServices code below.
services.AddMvcCore(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});

Related

How to use ASP.NET Core Identity without IdentityServer4?

We are starting a new web application that will be hosted in our customers infrastructure. Since this is a solution that will be here for a while and serve as a base for a lot of future products, we wanted to have a future proof security, that would be future poff (SSO / MFA) but this is something for like in 3 years. It's important for our customer that we rely on some standards, so I thought about using OpenId.
The solution will be based on ASP.NET Core + Angular. So I found out there was ASP.NET Core Identity, already compatible with OpenID Connect, but then I saw here that Microsoft recommends Duende Identity Server (IdentityServer4).
The problem is that we are a small team, building a small application, but in a big enterprise, so will have to go for the licensed version. The other problem is that since its our customers that deploy themself the application, we do not control how many servers will be deployed, therefore we would have to opt for an "enterprise" subscription, which is totally out of our budget.
Despite this, we were hoping that we could still use ASP.NET Core Identity to connect to different sources of users, manage permissions for our app, use the attributes on our controllers.
So, how to use ASP.NET Core Identity, without using IdentityServer?
According to the MSFT docs
ASP.NET Core Identity adds user interface (UI) login functionality to
ASP.NET Core web apps.To secure web APIs and SPAs, use one of the
following:
Azure Active Directory Azure
Active Directory B2C (Azure AD B2C)
IdentityServer4
So they first offer their cloud solutions.Identityserver4 free version is still supported though till the .Net Core 3.1 EOL.
As a free-free option without any predefined EOL, you can try this OpenIddict sample as a start point for your solution, however it has a bit more gaps to be filled in yourself.
And here is an explanation why MSFT don't offer it in their docs (spoiler: see above)
You can use pure ASP.NET Core without IdentityServer.
It's quite easy if you're using the same backend for authentication and API.
Example (copied from source):
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey
(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = false,
ValidateIssuerSigningKey = true
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseHttpsRedirection();
app.MapGet("/security/getMessage", () => "Hello World!").RequireAuthorization();
app.MapPost("/security/createToken",
[AllowAnonymous] (User user) =>
{
if (user.UserName == "joydip" && user.Password == "joydip123")
{
var issuer = builder.Configuration["Jwt:Issuer"];
var audience = builder.Configuration["Jwt:Audience"];
var key = Encoding.ASCII.GetBytes
(builder.Configuration["Jwt:Key"]);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim("Id", Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(JwtRegisteredClaimNames.Email, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti,
Guid.NewGuid().ToString())
}),
Expires = DateTime.UtcNow.AddMinutes(5),
Issuer = issuer,
Audience = audience,
SigningCredentials = new SigningCredentials
(new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha512Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
var jwtToken = tokenHandler.WriteToken(token);
var stringToken = tokenHandler.WriteToken(token);
return Results.Ok(stringToken);
}
return Results.Unauthorized();
});
app.UseAuthentication();
app.UseAuthorization();
app.Run();
See also:
Similar thread

How to combine multiple authentication schemes for different types of clients (user/pass and client/secret) in a Blazor WASM project?

I have a Blazor WASM project with a Blazor Client and ASP.NET core server. I can authenticate with user/password using the following code:
services
.AddDefaultIdentity<ApplicationUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services
.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services
.AddAuthentication()
.AddIdentityServerJwt();
services.AddTransient<IProfileService, ProfileService>();
services.AddAuthorization(options =>
{
options.AddPolicy("ApiScope", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "api1");
});
});
When I add the following code, I can successfully authenticate with clientcredentials from a console client. But then the Blazor client user/password authentication stops working.
...
services
.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>()
+.AddInMemoryApiScopes(Config.ApiScopes)
+.AddClientStore<ClientStore>()
+.AddDeveloperSigningCredential();
services
.AddAuthentication()
.AddIdentityServerJwt();
+services
+ .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+ .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
+ {
+ options.Authority = "https://localhost:44311";
+ options.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateAudience = false,
+ };
+ });
...
In the browser while trying to authenticate in the Blazor client, the console prints:
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
I have tried a lot, but I'm not able to make both work together. It seems that somehow this configuration requires authentication for everything, even the pages/controllers that are marked AllowAnonymous. So, when I try to authenticate, it gives me an error telling me the user must be authenticated: DenyAnonymousAuthorizationRequirement. The policy, "ApiScope" is only intended for the clientcredentials client, not for the Blazor client. If removed, the RequireAuthenticatedUser call doesn't make a difference, same error.
Any help is appreciated.

Share authentication cookie across subdomains in ASP.NET Core - cannot login

I am trying to implement sharing authentication cookie across two subdomains in ASP.NET Core web apps. I have changed Startup.cs and added following code in ConfigureServices:
services.AddDataProtection()
.PersistKeysToAzureBlobStorage(new Uri("{{my_url_to_blob_storage}}"))
.SetApplicationName("myapp");
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = "myapp";
options.Cookie.Domain = ".azurewebsites.net";
});
I have published the web app to Azure, but when trying HttpContext.SignInAsync(), nothing happens - HttpContext.User is not set.
Thanks for any help
Solved! When using custom domain, everything runs as expected

identity callback path in production Azure website?

I have a asp.net core 2 mvc app with openid connect authentication. It works locally in Visual Studio Debug to IIS Express.
After I deploy to Azure, I see this error: invalid_request: The provided value for the input parameter 'redirect_uri' is not valid
This is my appsettings callback path: "/Identity/AuthResult/"
do i need to put in a full URI ,like https://mysite.azurewebsites.net/Identity/AuthResult/ ?
this is my startup code: ` services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/"; // Microsoft identity platform
options.TokenValidationParameters.ValidateIssuer = false; // accept several tenants (here simplified)
});`
I am following this wiki Quickstart: Add sign-in with Microsoft to an ASP.NET Core web app

Dynamic ports with Azure Ad Authentication

I am developing an ASP.Net Core 2.0 application in Azure Service Fabric. This application uses Azure Ad Authentication. This authentication requires a registered reply URL in the Azure portal. Service fabric however assigns a port to my application based on the available ports. How can I implement this way of authentication with dynamically assigned ports without registering over one hundred different reply URLs? The ports are currently resolved through the use of the Service Fabric reverse proxy.
In my startup I have entered my Azure Ad config in ConfigureServices with:"
services.AddAzureAd(options =>
{
Configuration.Bind("AzureAd", options);
})
My appsettings contains:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "mydomain.com",
"TenantId": "AD tenant id (GUID)",
"ClientId": "registered app id (GUID)",
"CallbackPath": "/signin-oidc"
},
...
}
For what it's worth, here is my suggestion.
Use SF reverse proxy url as ReplyUrl
You don't need to register every possible private address along with the port in ReplyUrls section for a given app registration. As you communicate with the service via SF Reverse Proxy, put its address as a reply url.
Adjust the code to build a correct redirect_uri
There is a change you'll need to apply to make sure that redirect_uri would be pointing at your reverse proxy rather than local ip. The exact code depends on many factors, include asp.net core build installed on your machine, but here is the prototype that I've got working on .net core 2.1.4 -
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.ClientId = "xxx-xxxxx-xxxx";
options.Authority = "xxx-xxxxx-xxxx";
options.CallbackPath = "/signin-oidc";
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = ctx =>
{
ctx.ProtocolMessage.RedirectUri = $"https://reverse_proxy_url/app_name/service_name{options.CallbackPath}";
return Task.CompletedTask;
}
};
});