ASP.NET Core: OpenIdConnect: message.State is null or empty - asp.net-core

I get this error:
OpenIdConnectAuthenticationHandler: message.State is null or empty.
with the URL https://localhost:7208/home/index, but the authentication works with the url https://localhost:7208/.
Can anyone help me understand this?
enter image description here
enter image description here
This is my code:
Program.cs:
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using System.Security.Claims;
using System.Web.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddMvc().AddSessionStateTempDataProvider();
builder.Services.Configure<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
});
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
}).AddCookie(options =>
{
})
.AddOpenIdConnect(options =>
{
options.Authority = builder.Configuration["OpenIdConfigurations:Authority"];
options.MetadataAddress = builder.Configuration["OpenIdConfigurations:MetadataAddress"];
options.ResponseType = builder.Configuration["OpenIdConfigurations:ResponseType"];
options.GetClaimsFromUserInfoEndpoint = Convert.ToBoolean(builder.Configuration["OpenIdConfigurations:GetClaimsFromUserInfoEndpoint"]);
options.RequireHttpsMetadata = Convert.ToBoolean(builder.Configuration["OpenIdConfigurations:RequireHttpsMetadata"]);
options.ClientId = builder.Configuration["OpenIdConfigurations:ClientId"];
options.ClientSecret = builder.Configuration["OpenIdConfigurations:ClientSecret"];
options.CallbackPath = builder.Configuration["OpenIdConfigurations:CallbackPath"];
});
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}"
);
app.Run();
Controller:
namespace OIDCMVC.Controllers
{
[Authorize]
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
public IActionResult Index()
{
return View();
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
Note: We use a private provide and expects the call back path.
Callbackpath = "/home/index"

In my case I am using blazor (.net 6) and trying to protect hangfire with Microsoft oauth. To get the auth screen of microsoft login when locating to /hangfire. The solution to this error was as simple as removing CallbackPath from my settings:
// settings found under Azure AD -> App Registration -> Your registration
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "domain", // found under Branding and properties -> publisher domain like : ...outlook.onmicrosoft.com
"TenantId": "tenantid", // found under Overview Directory (tenant) ID GUID
"ClientId": "client_id" // found under Overview Application (client) ID GUID
//"CallbackPath": "/hangfire" REMOVED To get rid off message.State is null error
}
The setup from Program.cs:
services.AddAuthentication().AddMicrosoftIdentityWebApp(configuration.GetSection("AzureAd"));
services.AddAuthorization(options =>
{
options.AddPolicy("Hangfire", builder =>
{
builder
.AddAuthenticationSchemes(OpenIdConnectDefaults.AuthenticationScheme)
.RequireAuthenticatedUser();
});
});
services.AddHangfire(x =>
{
x.UseSqlServerStorage(connectionString);
x.UseConsole();
});
services.AddHangfireServer();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHangfireDashboard("/hangfire", new DashboardOptions()
{
Authorization = new List<IDashboardAuthorizationFilter> { new HangfireAuthorizeFilter() },
}).RequireAuthorization("Hangfire");
});
app.UseHangfireDashboard();
Authorization filter:
public class HangfireAuthorizeFilter:
IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var userIdentity = context.GetHttpContext().User.Identity;
return userIdentity is { IsAuthenticated: true };
}
}
And the app registration from Azure AD:
Under your app registration click: Authentication -> Mark "ID tokens" and enter your redirect urls, like: https://localhost:52908/hangfire

I've been integrating multiple custom policies (different flows) in a single app. Removing CallbackPath didn't work as request were coming there from multiple sources, not just the default sign in policy. We were receiving "message.State is null or empty" page, but after navigating to the base path of the app, the user was authenticated and properly logged in.
What ultimately helped was setting SkipUnrecognizedRequests property to true:
.AddOpenIdConnect(options =>
{
...
options.SkipUnrecognizedRequests = true;
});
or using appsettings:
{
...
"AzureAdB2C": {
"Instance": "",
"Domain": "",
"ClientId": "",
...
"SkipUnrecognizedRequests": true
},
...
}
According to the OpenIdConnectOptions.SkipUnrecognizedRequests documentation:
Indicates if requests to the CallbackPath may also be for other components. If enabled the handler will pass requests through that do not contain OpenIdConnect authentication responses. Disabling this and setting the CallbackPath to a dedicated endpoint may provide better error handling. This is disabled by default.
Also, maybe related issue here: OIDC handler running on '/' crashes even with SkipUnrecognizedRequests set to true #10028

Related

Visual studio's ASP.NET core with angular template always returns 401

I'm using Microsoft Visual Studio Community 2019 Version 16.10.2. I created an ASP.net core 5 project from their template "ASP.net core with Angular" with Authentication checked. But Every time I request an API Method marked with the attribute [Authorize] I get 401.
Their template was supposed to work with no problem but I got some trouble logging in. I fixed them but, the only problem I can't figure out how to fix is the 401 code returned ASP.
I read the doc several times but I could not find any useful information.
The thing is: I can create accounts and login with no problem. When I login, the server returns the token. From the Angular app it shows the name of the logged-in user. But when to access an [Authorize] controller it returns 404.
Here is the link of the project I pushed to github for better debugging.
Here is the startup code:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(op =>
{
// I added these lines because I was getting the error "Error: Client Angular_identity_test is not allowed access to scope Angular."
op.Clients[0].AllowedScopes = new List<string> { "Angular", "identity", "testAPI" };
op.ApiScopes = new Microsoft.AspNetCore.ApiAuthorization.IdentityServer.ApiScopeCollection(new List<ApiScope> { new ApiScope("Angular"), new ApiScope("identity"), new ApiScope("testAPI") });
op.ApiResources.AddApiResource("Angular", conf => conf.WithScopes(new string[] { "Angular", "identity", "testAPI" }));
op.ApiResources.AddApiResource("identity", conf => conf.WithScopes(new string[] { "Angular", "identity", "testAPI" }));
op.ApiResources.AddApiResource("testAPI", conf => conf.WithScopes(new string[] { "Angular", "identity", "testAPI" }));
});
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddControllersWithViews();
services.AddRazorPages();
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
}
// In public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
app.UseHttpsRedirection();
app.UseStaticFiles();
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
app.UseRouting();
// I added this line because some people say that fixed their problems.
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
In the appsettings.json there is:
"IdentityServer": {
"Clients": {
"Angular_identity_test": {
"Profile": "IdentityServerSPA"
}
}
}
And the controller that keeps return 401:
[Authorize()]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
So you say, But when to access a [Authorize] controller it returns 404. use this below code, instead of [Authorize].
[Authorize(AuthenticationSchemes = "Bearer")]
It will resolve your issue.
UPDATE
Use this below code and i assume that your Token is place of appsettings.development.json file. like "TokenKey":"super secret key",
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters=new TokenValidationParameters
{
ValidateIssuerSigningKey=true,
IssuerSigningKey=new SymmetricSecurityKey( Encoding.UTF8.GetBytes(config["TokenKey"]) ),
ValidateIssuer=false,
ValidateAudience=false
};
});
Hope it will resolve your issue.
This will be a proxy issue. In your CLIENTAPP go to the proxy.conf.js and add your controllers route into the PROXY_CONFIG->context array (like "/weatherforecast" is). Then restart your application. You don't need to change .NET Core middleware or services.
UPDATE
I added this DI BASE_URL into my service constructor and it works fine now.
constructor(private http: HttpClient, #Inject('BASE_URL') private bUrl: string) {
this.baseUrl = bUrl + "api/v1/items"; // my endpoint
}

IdentityServer4 endless redirect after login

I am trying to setup IdentityServer4 for the first time, and am following the steps in the docs for adding a JS client. I must have something configured incorrectly, but I can't figure out what it is. The flow is as follows:
User hits "login" on Client A
Client A sends login request to IS4 (upon debugging, the "returnUrl" parameter seems correct)
using the IS4 extension method I am signing the user in (HttpContext.SignInAsync)
user is redirected to "connect/authorize/callback" which redirects them to the login method again and the circular reference continues until the browser stops it and throws error.
Relevant code:
startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<UsersContext>(o =>
o.UseSqlServer(Configuration.GetConnectionString("UsersRuntime")));
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddIdentityServer(o =>
{
})
.AddTestUsers(Identity.Users.Get())
.AddConfigurationStore(o =>
{
o.ConfigureDbContext = b => b.UseSqlServer(Configuration.GetConnectionString("UsersRuntime"),
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(Configuration.GetConnectionString("UsersRuntime"),
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddDeveloperSigningCredential();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
InitializeDatabase(app);
app.UseRouting();
app.UseDefaultFiles();
app.UseStaticFiles();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseHttpStatusCodeExceptionMiddleware();
}
else
{
app.UseHttpStatusCodeExceptionMiddleware();
app.UseHsts();
}
app.UseCors(MyAllowSpecificOrigins);
if (env.IsProduction())
{
app.UseHttpsRedirection();
}
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHealthChecks("/health");
});
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Users API");
c.RoutePrefix = string.Empty;
});
}
private void InitializeDatabase(IApplicationBuilder app)
{
using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
serviceScope.ServiceProvider.GetRequiredService<UsersContext>().Database.Migrate();
var persistedGrantDbContext =
serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>();
persistedGrantDbContext.Database.Migrate();
var configDb = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
configDb.Database.Migrate();
var testClient = new Client
{
ClientId = "TestClient",
RequireClientSecret = false,
AllowOfflineAccess = true,
AlwaysIncludeUserClaimsInIdToken = true,
AllowedIdentityTokenSigningAlgorithms = new List<string>{SecurityAlgorithms.RsaSha256},
UpdateAccessTokenClaimsOnRefresh = true,
RefreshTokenExpiration = (int)TokenExpiration.Sliding,
AllowedGrantTypes = new List<string>
{
IdentityServerConstants.PersistedGrantTypes.AuthorizationCode
},
AllowedScopes = new List<string>
{
"Read",
"Write"
},
AllowedCorsOrigins = new List<string>
{
"https://localhost:5003"
},
RedirectUris = new List<string>{"https://localhost:5003/callback.html"}
};
configDb.Clients.Add(testClient.ToEntity());
configDb.SaveChanges();
var resource = new ApiResource
{
Name = "TestApi",
ShowInDiscoveryDocument = true,
AllowedAccessTokenSigningAlgorithms = new List<string>{SecurityAlgorithms.RsaSha256},
Scopes = new List<string>
{
"Read",
"Write"
}
};
configDb.ApiResources.Add(resource.ToEntity());
var readScope = new ApiScope("Read");
var writeScope = new ApiScope("Write");
configDb.ApiScopes.AddRange(new []{readScope.ToEntity(), writeScope.ToEntity()});
configDb.SaveChanges();
}
}
login controller
[Route("account/login")]
[Produces("application/json")]
[ApiController]
public class LoginControllerOidc: ControllerBase
{
[HttpGet]
public async Task<IActionResult> Get(string returnUrl)
{
await HttpContext.SignInAsync(new IdentityServerUser("Test")
{
DisplayName = "Test Display Name",
AdditionalClaims = new List<Claim>
{
new Claim("additionalClaim", "claimValue")
}
});
return Redirect(returnUrl);
}
}
config for oidc-client.js
var config = {
authority: "https://localhost:5001",
client_id: "TestClient",
redirect_uri: "https://localhost:5003/callback.html",
response_type: "code",
scope:"Read Write",
post_logout_redirect_uri : "https://localhost:5003/index.html"
};
Redirect Issue Screenshot
I'm at a loss for what it left to do. Following the docs I think I have everything setup correctly. Guides I am following can be found Here (adding javascript client) and Here (sign in)
The cookie is being set correctly (I think) as seen here
The problem that I was having was due to the subjectId in the controller not matching a subjectId in the TestUsers.

ASP.NET Core returns InternalServerError while using Identity server

I trying to add identity server for my web API as its identity server4 documentation. when I was trying to call API from my console application it's every time returns InternalServerError.
Here is My Identity server Config.cs
public static class Config
{
// register api
public static IEnumerable<ApiScope> ApiScopes => new List<ApiScope>
{
// in here add your api name
new ApiScope("api1", "My API")
};
// register client which is going to access api. eg: front-end application, mobile apps etc. can add multiple client.
public static IEnumerable<Client> Clients => new List<Client>
{
new Client
{
// which is going to access
ClientId = "client",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.ClientCredentials,
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
// scopes that client has access to
AllowedScopes = { "api1" }
}
};
}
and here the identity server startup file configuration service and configure functions
public void ConfigureServices(IServiceCollection services)
{
// uncomment, if you want to add an MVC-based UI
services.AddControllersWithViews();
var builder = services.AddIdentityServer()
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients);
builder.AddDeveloperSigningCredential();
builder.AddDeveloperSigningCredential();
}
public void Configure(IApplicationBuilder app)
{
if (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// uncomment if you want to add MVC
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
// uncomment, if you want to add MVC
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
and here is my API startup file's congurationService and configure functions
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:14030/";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
}
);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
here is my API controller
[Route("identity")]
public class IdentityController : ControllerBase
{
[HttpGet]
[Authorize]
public IActionResult Get() => Ok(new JsonResult(from c in User.Claims select new { c.Type, c.Value }));
}
and here is my console application client request a api
static async System.Threading.Tasks.Task Main(string[] args)
{
// discover endpoints from metadata
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:14030");
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
// request token
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "client",
ClientSecret = "secret",
Scope = "api1"
});
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}
Console.WriteLine(tokenResponse.Json);
Console.WriteLine("\n\n");
// call api
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var response = await apiClient.GetAsync("https://localhost:5001/identity");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
}
else
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);
}
}
what are the mistakes should I have to fix. Im really appreciates your valuable answers and efforts.
Thank
I got the code working, I would do the following:
use HTTPS here, not HTTP:
var disco = await
client.GetDiscoveryDocumentAsync("http://localhost:14030");
Remove the duplicate lines of in IdentityServer startup class:
builder.AddDeveloperSigningCredential();
I would add in your API startup.cs
services.AddAuthorization();
Remove the trailing / at the end of the URL here:
options.Authority = "https://localhost:14030/";
To get more debugging output from your API, you can add the following two trace lines to your appsettings.Development.json file:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore.Authentication": "Trace",
"Microsoft.AspNetCore.Authorization": "Trace"
}
}
}
If you want to validate the Audience (and using IdentityServer4 v4.00) you can add:
services.AddControllers();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:14030";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidAudiences = new[] {"https://localhost:14030/resources"},
ValidateAudience = true
};
}
);

Secure API with JWT access token

I am playing around with the openiddict Authorization code flow sample and all is working well.
https://github.com/openiddict/openiddict-samples/tree/dev/samples/CodeFlow
However, I want to make certain changes and I am struggling to do this. I would like to configure to use JWT tokens instead of the default opaque tokens, and also separate into an authorization server and a resource server. I also have an MCV web app that will communicate with the resource server via a httpClient.
Auth Server.Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
options.UseOpenIddict();
});
// Register the Identity services.
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
});
services.AddOpenIddict()
.AddCore(options =>
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
})
// Register the OpenIddict server handler.
.AddServer(options =>
{
options.UseMvc();
options.EnableAuthorizationEndpoint("/connect/authorize")
.EnableLogoutEndpoint("/connect/logout")
.EnableTokenEndpoint("/connect/token")
.EnableUserinfoEndpoint("/api/userinfo");
options.RegisterScopes(OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIddictConstants.Scopes.Roles);
options.AllowAuthorizationCodeFlow();
options.EnableRequestCaching();
options.DisableHttpsRequirement();
options.UseJsonWebTokens();
options.AddEphemeralSigningKey();
});
}
As this is no longer a resource server I have removed the validation parts as I don't think this is required. And as I want to use JWT I have un-commented the following lines:
options.UseJsonWebTokens();
options.AddEphemeralSigningKey();
The authorization endpoint returns a SignIn result exactly like the sample, which redirects to the MVC app which then issues an authentication cookie. I can now access protected resources on my MVC APP.
MVC APP startup
public void ConfigureServices(IServiceCollection services)
{
services.Configure<PortalDetails>(options => Configuration.GetSection("PortalDetails").Bind(options));
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(opts =>
{
opts.LoginPath = "/login";
opts.LogoutPath = "/logout";
})
.AddJwtBearer(options =>
{
//Authority must be a url. It does not have a default value.
options.Authority = "http://localhost:54540/";
options.Audience = "mvc"; //This must be included in ticket creation
options.RequireHttpsMetadata = false;
options.IncludeErrorDetails = true; //
options.TokenValidationParameters = new TokenValidationParameters()
{
NameClaimType = "sub",
RoleClaimType = "role"
};
})
.AddOpenIdConnect(options =>
{
// Note: these settings must match the application details
// inserted in the database at the server level.
options.ClientId = "mvc";
options.ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654";
options.RequireHttpsMetadata = false;
options.GetClaimsFromUserInfoEndpoint = false; // TODO: If this if true then it doesnt work??
options.SaveTokens = true;
// Use the authorization code flow.
options.ResponseType = OpenIdConnectResponseType.Code;
options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
// Note: setting the Authority allows the OIDC client middleware to automatically
// retrieve the identity provider's configuration and spare you from setting
// the different endpoints URIs or the token validation parameters explicitly.
options.Authority = "http://localhost:54540/";
options.Scope.Add("email");
options.Scope.Add("roles");
options.SecurityTokenValidator = new JwtSecurityTokenHandler
{
// Disable the built-in JWT claims mapping feature.,
InboundClaimTypeMap = new Dictionary<string, string>()
};
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "role";
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddHttpClient<IApiGatewayClient, ApiGatewayClient>();
services.AddSingleton<ITokenProvider, TokenProvider>();
}
When calling the resource server I use:
string accessToken = await HttpContext.GetTokenAsync("access_token");
and I can see an access token, I attach that to my http request:
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
but the result is forbidden.
Finally, I have a protected resource server:
Resource.Startup
public void ConfigureServices(IServiceCollection services)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();
//Add authentication and set default authentication scheme
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) //same as "Bearer"
.AddJwtBearer(options =>
{
//Authority must be a url. It does not have a default value.
options.Authority = "http://localhost:54540";
options.Audience = "mvc"; //This must be included in ticket creation
options.RequireHttpsMetadata = false;
options.IncludeErrorDetails = true; //
options.TokenValidationParameters = new TokenValidationParameters()
{
NameClaimType = OpenIdConnectConstants.Claims.Subject,
RoleClaimType = OpenIdConnectConstants.Claims.Role,
};
});
services.AddMvc();
}
I would like to know if this is the correct setup for my scenario, as I am getting a forbidden result from my resource server.
Thanks
Here is a package which
Makes integrating JWT Bearer Token Security in your Asp Net Core 2.0+ app a breeze!
Azure Active Directory auth integration.
Facebook auth integration.
Twitter auth integration.
Google auth integration.
Also, Swagger UI integration!
It is called AspNetCore.Security.Jwt
GitHub:
https://github.com/VeritasSoftware/AspNetCore.Security.Jwt
The package integrates JWT bearer token into your app as below:
1. Implement IAuthentication interface in your app
using AspNetCore.Security.Jwt;
using System.Threading.Tasks;
namespace XXX.API
{
public class Authenticator : IAuthentication
{
public async Task<bool> IsValidUser(string id, string password)
{
//Put your id authenication here.
return true;
}
}
}
2. In your Startup.cs
using AspNetCore.Security.Jwt;
using Swashbuckle.AspNetCore.Swagger;
.
.
public void ConfigureServices(IServiceCollection services)
{
.
.
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "XXX API", Version = "v1" });
});
services.AddSecurity<Authenticator>(this.Configuration, true);
services.AddMvc().AddSecurity();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
.
.
.
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "XXX API V1");
});
app.UseSecurity(true);
app.UseMvc();
}
3. In your appsettings.json
Note:- You can put these settings in Secret Manager by using Manage User Secrets menu (right-click your Project).
{
"SecuritySettings": {
"Secret": "a secret that needs to be at least 16 characters long",
"Issuer": "your app",
"Audience": "the client of your app",
"IdType": "Name",
"TokenExpiryInHours" : 2
},
.
.
.
}
Then you will get endpoints automatically:
/token
/facebook
When you call these endpoints and are successfully authenticated, you will get back a JWT Bearer Token.
In your Controller that you want to secure
You must mark the Controller or Action that you want to secure with Authorize attribute like:
using Microsoft.AspNetCore.Mvc;
.
.
.
namespace XXX.API.Controllers
{
using Microsoft.AspNetCore.Authorization;
[Authorize]
[Route("api/[controller]")]
public class XXXController : Controller
{
.
.
.
}
}
In Swagger UI, you will automatically see these endpoints.

IdentityServer4 on .Net Core2 with bearer - API not validating auth / custom policy handler

I try to setup an identity server 4 + API + web Scenario but can't get users authenticated in the api. Each component uses a separate Project within my VS solution. All Projects are on dotnetcore 2.0.
Startup.cs Identity Server
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddCors();
services.AddIdentityServer(options =>
{
options.Events.RaiseSuccessEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseErrorEvents = true;
})
.AddInMemoryClients(Clients.Get())
.AddInMemoryIdentityResources(Resources.GetIdentityResources())
.AddInMemoryApiResources(Resources.GetApiResources())
.AddDeveloperSigningCredential()
.AddExtensionGrantValidator<Extensions.ExtensionGrantValidator>()
.AddExtensionGrantValidator<Extensions.NoSubjectExtensionGrantValidator>()
.AddTestUsers(TestUsers.Users);
TestUsers.Users
public class TestUsers
{
public static List<IdentityServer4.Test.TestUser> Users = new List<IdentityServer4.Test.TestUser>
{
new IdentityServer4.Test.TestUser{SubjectId = "818727", Username = "alice", Password = "alice",
Claims =
{
new Claim(JwtClaimTypes.Role, "UserEditor"),
new Claim(JwtClaimTypes.Name, "Alice Smith"),
new Claim(JwtClaimTypes.Email, "AliceSmith#email.com"),
}
},
new IdentityServer4.Test.TestUser{SubjectId = "88421113", Username = "bob", Password = "bob",
Claims =
{
new Claim(JwtClaimTypes.Role, "Root"),
new Claim(JwtClaimTypes.Role, "Admin"),
new Claim(JwtClaimTypes.Role, "UserEditor"),
new Claim(JwtClaimTypes.Name, "Bob Smith"),
new Claim(JwtClaimTypes.Email, "BobSmith#email.com")
}
}
};
}
Getting a indentityserver jwt bearer token works via http://localhost:2266/connect/token and it contains the relevant Information:
{
...
...
"role": [
"Root",
"Admin",
"UserEditor"
],
"scope": [
...
],
...
}
However - on the API side authentication is not checked properly.
API Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext...
...
...
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:2266";
options.RequireHttpsMetadata = false;
options.ApiSecret = "secret";
options.ApiName = "MyApi";
});
services.AddCors(options =>
{
// this defines a CORS policy called "default"
options.AddPolicy("default", policy =>
{
policy.WithOrigins("http://localhost:44352")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
// custom policy attributes
services.AddAuthorization(options =>
{
options.AddPolicy("Root", policy => policy.Requirements.Add(new Models.Policies.MyPolicyRequirement("Root")));
options.AddPolicy("Admin", policy => policy.Requirements.Add(new Models.Policies.MyPolicyRequirement("Admin")));
options.AddPolicy("UserEdit", policy => policy.Requirements.Add(new Models.Policies.MyPolicyRequirement("UserEdit")));
});
services.AddSingleton<IAuthorizationHandler, Models.Policies.MyPolicyHandler>();
services.AddMvc();
// add swagger
...
}
The policy Validation
Controller/Actions are marked with the Authorize Attribute, e.g
[Authorize(Policy = "Root")]
The Code of the policy handler is hit while Debugging.
public class MyPolicyHandler : AuthorizationHandler<MyPolicyRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MyPolicyRequirement requirement)
{
if ( !context.User.Identity.IsAuthenticated )
return Task.CompletedTask;
if (context.User.Identities.FirstOrDefault().HasClaim(System.Security.Claims.ClaimTypes.Role, requirement.Policy))
{
context.Succeed(requirement);
return Task.CompletedTask;
}
return Task.CompletedTask;
}
}
The validation fails because the context.User.Identity.IsAuthenticated is false, the objects identity has no Claims as well.
Looks like something is missing in the Pipeline to convert my bearer authentication to an user identity.
Any suggestions?
Finally found the problem:
I had a reference to Microsoft.IdentityModel.Tokens (5.2.0-preview1) because of
some code to load the signing certificate.
If that package is enabled the Validation Fails.
I once started with validating the signing certificate of the token by myself so i needed that component but i guess all that will be handled automatically in the AddIdentityServerAuthentication part?
So the policy check was not the Problem. Thanks for asking me to start from base.