ASP.Net Core Identity with JwtBearer AuthenticationScheme map claims to context User object - authentication

I have a React Front end using the msal lib to authenticate the user client side with our Azure AD. This works great and authentication has no issues. I also have an ASP.Net Core WebApi to provide data to the client. I am using the JwtTokens to pass the Bearer token in the request. The WebApi is able to validate the token and all is well... I thought, however, when the WebApi method is invoked the only way I can get the User's email or name is to query the User.Claims with Linq.
this.User.Claims.Where(c=> c.Type == "preferred_username").FirstOrDefault().Value
I was about to go down the road of mapping these linq queries to an object which could be injected into the WebApi's controller, but that seems wrong.
I am obviously missing something in my Startup.cs for the WebApi, Any help or suggestions would be great!:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
//add authentication JwtBearer Scheme
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Audience = Configuration["JwtSettings:Audience"];
options.Authority = Configuration["JwtSettings:Authority"];
options.Events = new JwtBearerEvents
{
OnTokenValidated = ctx =>
{
//log
return Task.CompletedTask;
},
OnAuthenticationFailed = ctx =>
{
//log
return Task.CompletedTask;
}
};
options.SaveToken = true;
});
services.AddAuthorization();
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}

Related

The mandatory 'code_challenge' parameter is missing with OpenIddict

I have an identity server running asp.net core 5, with openiddict 3.1.1
I'm having trouble where I'm getting the error from openiddict:
error:invalid_request
error_description:The mandatory 'code_challenge' parameter is missing.
error_uri:https://documentation.openiddict.com/errors/ID2029
in some scenarios, but not all. My identity server has a startup.cs of:
services.AddDbContext<IdentityContext>(options =>
{
options.UseSqlServer(dbConnectionString, x => x.UseNetTopologySuite());
// Register the entity sets needed by OpenIddict.
// Note: use the generic overload if you need
// to replace the default OpenIddict entities.
options.UseOpenIddict<Guid>();
});
services.AddTransient<IPasswordHasher<ApplicationUser>, CustomPasswordHasher>();
services.AddTransient<IOptions<IdentityOptions>, CustomOptions>();
services.AddScoped<SignInManager<ApplicationUser>, CustomSignInManager>();
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<IdentityContext>()
//.AddDefaultUI()
.AddDefaultTokenProviders();
services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
// Note: call ReplaceDefaultEntities() to replace the default entities.
options.UseEntityFrameworkCore()
.UseDbContext<IdentityContext>()
.ReplaceDefaultEntities<Guid>();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
// Enable the authorization, device, logout, token, userinfo and verification endpoints.
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetLogoutEndpointUris("/connect/signout")
.SetTokenEndpointUris("/connect/token");
// Enable the client credentials flow.
options
.AllowAuthorizationCodeFlow().RequireProofKeyForCodeExchange()
.AllowRefreshTokenFlow();
// Encryption and signing of tokens
options
.AddEphemeralEncryptionKey()
.AddEphemeralSigningKey()
.DisableAccessTokenEncryption(); //TODO: not a huge deal as long as we're not hiding anything bad here.
// Expose all the supported claims in the discovery document.
options.RegisterClaims(Configuration.GetSection("OpenIddict:Claims").Get<string[]>());
// Expose all the supported scopes in the discovery document.
options.RegisterScopes(Configuration.GetSection("OpenIddict:Scopes").Get<string[]>());
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
options.UseAspNetCore()
.EnableStatusCodePagesIntegration()
.EnableAuthorizationEndpointPassthrough()
.EnableAuthorizationRequestCaching()
.EnableLogoutEndpointPassthrough()
.EnableVerificationEndpointPassthrough()
.EnableTokenEndpointPassthrough();
})
// Register the OpenIddict validation components.
.AddValidation(options =>
{
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
// Enable authorization entry validation, which is required to be able
// to reject access tokens retrieved from a revoked authorization code.
options.EnableAuthorizationEntryValidation();
});
with an OpenIDWorker of:
public async Task StartAsync(CancellationToken cancellationToken)
{
using IServiceScope scope = _serviceProvider.CreateScope();
IdentityContext context = scope.ServiceProvider.GetRequiredService<IdentityContext>();
await RegisterApplicationsAsync(scope.ServiceProvider, _configuration);
static async Task RegisterApplicationsAsync(IServiceProvider provider, IConfiguration configuration)
{
IOpenIddictApplicationManager manager = provider.GetRequiredService<IOpenIddictApplicationManager>();
string clientID = configuration.GetSection("OpenIddict:ClientId").Get<string>();
string clientSecretString = "blahblahblah";
if (await manager.FindByClientIdAsync(clientID) is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = clientID,
ClientSecret = clientSecretString,
ConsentType = ConsentTypes.Explicit,
DisplayName = configuration.GetSection("OpenIddict:DisplayName").Get<string>(),
PostLogoutRedirectUris =
{
new Uri("https://localhost:44330/signout-callback-oidc")
},
RedirectUris =
{
new Uri("https://localhost:44330/signin-oidc")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Logout,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});
}
}
}
when i try to connect to the server with a C# razor app with the following startup.cs, it works fine and w/out any issues:
string clientSecretString = "blahblahblah";
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.LoginPath = "/login";
options.ExpireTimeSpan = TimeSpan.FromMinutes(50);
options.SlidingExpiration = false;
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
// Note: these settings must match the application details
// inserted in the database at the server level.
options.ClientId = Configuration.GetSection("ClientId").Get<string>();
options.ClientSecret = clientSecretString;
options.RequireHttpsMetadata = false;
options.GetClaimsFromUserInfoEndpoint = true;
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 = "https://localhost:44330/";
options.Scope.Add("email");
options.Scope.Add("roles");
options.Scope.Add("offline_access");
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";
options.AccessDeniedPath = "/";
});
But when i try to connect to it with https://oidcdebugger.com/, or if I try to connect to it using Azure B2C user flows, I get the error message above about missing a code_challenge (see image )
What am i missing here? My plan was to use B2C, but i'm not seeing what is blocking me.
See https://github.com/openiddict/openiddict-core/issues/1361 for reference. Issue ended up being a problem with B2C itself, and will require a fix for them

Fetch data return Untheorized 401 access to asp.net core API protected in Azure AD

Im new to `webassembly blazor, Im spend too much time trying to figure out what's wrong here but I couldnt manage.
I have the following scenario:
Asp.net API registered and protected in Azure AD
Expose API with Scope AcessApi with status enabled
A Client application is added to authorized client applications
Token configuration both are checked Access Token and ID Token
And a client app that will call the API, developed in webassembly blazor
client app is registered in Azure AD
Client API permissions has delegated permission to use my client API
with correct scope AccessApi.
I tested the API using swagger interface, it forces user to authenticate first before accessing the API.
I tested using curl and grabbed the token from swagger interface and works perfectly fine.
curl -X GET "http://localhost:9400/api/getdata" -H "accept: text/plain" -H "Authorization: Bearer XX"
However, when my client application trying to access the API, a sign-in page pop-up for credentials, I could see the Token ID at browser bar being retrieved and while calling the API the app logs error not authorized
program class of the client application:
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
//builder.Logging.SetMinimumLevel(LogLevel.Debug);
////builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<CustomAuthorizationMessageHandler>();
builder.Services.AddHttpClient("AccessApi",
client => client.BaseAddress = new Uri("http://localhost:9400"))
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
.CreateClient("AccessApi"));
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
});
await builder.Build().RunAsync();
}
in CustomAuthorizationMessageHandler class I have defined:
private static string scope = #"api://xxx-35fc2470889f/AccessApi";
public CustomAuthorizationMessageHandler(IAccessTokenProvider provider,
NavigationManager navigationManager)
: base(provider, navigationManager)
{
ConfigureHandler(
authorizedUrls: new[] { "http://localhost:9400" },
}
In appsettings.json a defined the client id of the API and tenant id without scopes since they are been defined in the CustomAuthorizationMessageHandlerclass:
{
"AzureAd": {
"Authority": "https://login.microsoftonline.com/<tenant_id>",
"ClientId": "<clientid>",
"CallbackPath": "/signin-oidc",
"ValidateAuthority": "true"
}
}
After a successful login via Azure AD, I call to fetch data from the API here
protected override async Task OnInitializedAsync()
{
...
try
{
responseBody = await Http.GetStringAsync("/api/getdata"); # use base URL of the API
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
}
}
the console logs
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[1]
Authorization was successful.
info: System.Net.Http.HttpClient.AccessApi.ClientHandler[100]
Sending HTTP request GET http://localhost:9400/api/getdata
:9400/customer-manager/api/getdata:1 Failed to load resource: the server responded with a status of 401 (Unauthorized)
What could be wrong here?
Is there a way how to print the return token?
Update
I tested the API using Postman where auth Grant type is Implicit, after successful login, I store token on variable and passed in the header as Bearer the API return 401 Unauthroized. I decoded the token it contains the right scope AccessApi , with the correct clientId. what could be wrong here ?
If you want to call Microsoft graph and your custom API in one blazor webassembly project, we can implement it by creating different HTTP client to call different API
For example
Register a server API app
Register an AAD app for the Server API app
Expose an API
Register a client app
Register a client app
Enable Implicit grant flow
Add API permissions. ( API app permissions)
Configure API app
Please add the following code in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddCors(options =>
{
options.AddDefaultPolicy(
builder => builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod());
});
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
options.Authority += "/v2.0";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuers = new[] {
$"https://sts.windows.net/{Configuration["AzureAD:TenantId"]}/",
$"https://login.microsoftonline.com/{Configuration["AzureAD:TenantId"]}/v2.0"
},
RoleClaimType = "roles",
// The web API accepts as audiences both the Client ID (options.Audience) and api://{ClientID}.
ValidAudiences = new[]
{
options.Audience,
$"api://{options.Audience}"
}
};
});
....
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.OAuthClientId(Configuration["Swagger:ClientId"]);
c.OAuthScopeSeparator(" ");
c.OAuthAppName("Protected Api");
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Configure Client APP
Create custom AuthorizationMessageHandler for Graph API and custom API
// custom API
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
public CustomAuthorizationMessageHandler(IAccessTokenProvider provider,
NavigationManager navigationManager)
: base(provider, navigationManager)
{
ConfigureHandler(
authorizedUrls: new[] { "<your web API url>" },
scopes: new[] { "the API app scope" });
}
}
Add the following code to the program.cs
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddScoped<CustomAuthorizationMessageHandler>();
// register HTTP client to call our own api
builder.Services.AddHttpClient("MyAPI", client => client.BaseAddress = new Uri("<your web API url>"))
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("<the API app scope>");
});
await builder.Build().RunAsync();
}
}
Call the api
#inject IHttpClientFactory _clientFactory
var httpClient = _clientFactory.CreateClient("<the client name you register>");
await apiClient.GetStringAsync("path");
Finally I found the issue was on the server side ASP.net core where I was validating the token in ConfigureServices at startup class:
// For token parameters validation
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o =>
{
o.Audience = "<xx>"; // Application id
o.Authority = "https://login.microsoftonline.com/<xx>"; // Tenant ID
//Token validation
o.TokenValidationParameters = new TokenValidationParameters {ValidateIssuerSigningKey = false, ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true};
});
I had to disable Issuer since the token is coming from a different application.

Failed to connect to SignalR in Blazor webassembly

I'm trying to connect to a SignalR service from my blazor webassembly client but this fails I think on CORS. This is the code in my razor file.
m_connection = new HubConnectionBuilder()
.WithUrl(myMircoServiceUrl, options =>
{
options.AccessTokenProvider = () => Task.FromResult(userService.Token);
})
.WithAutomaticReconnect()
.Build();
await m_connection.StartAsync();
Then in the webassembly logging I see the following error:
Access to fetch at 'xxxx/negotiate?negotiateVersion=1' from origin 'http://localhost:5010' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
I added the following CORS policy in my Blazor server configuration and something similar in the microservice config:
app.UseResponseCompression();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBlazorDebugging();
}
else
{
app.UseExceptionHandler(#"/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseCors(policy => policy
.WithOrigins("http://localhost:5010")
.AllowAnyHeader()
.AllowAnyMethod());
app.UseClientSideBlazorFiles<Client.Program>();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapFallbackToClientSideBlazor<Client.Program>(#"index.html");
});
Anybody got any idea what might be wrong?
Update 1
I now see the following error in the Chrome console:
dotnet.js:1 WebSocket connection to 'ws://localhost:5000/hubs/posts?id=9Jxs0DhP924zgw_eIeE9Lg' failed: HTTP Authentication failed; no valid credentials available
Update 2
I removed the [Authorize] attribute from the SignalR hub and now it connects. And I can send messages to the hub. Problem is there is a reason for this attribute, because I don't want that people can subscribe to messages that are not for them
Update 3
Still no progress. Looking at pulling out the authentication to a seperate microservice using IdentityServer4. Last status is I have the following startup routines:
Microservice: gist.github.com/njannink/15595b77ffe1c0593be1a555fa37f83f
Blazor server: gist.github.com/njannink/7302a888110e24d199ea45b66da4f26b
Blazor client: gist.github.com/njannink/add2568cbf48c8b3c070ccd4f28fd127
I've got the same errors with CORS and afterwards Websocket.
In my case the fallback longPolling was used as why the connection worked but the console logged the error HTTP Authentication failed; no valid credentials available.
If you use Identity Server JWT the following code solved the error for my case.
(The Code is from the Microsoft SignalR Documentation - Authentication and authorization in ASP.NET Core SignalR - Identity Server JWT authentication)
services.AddAuthentication()
.AddIdentityServerJwt();
// insert:
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>,
ConfigureJwtBearerOptions>());
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
public void PostConfigure(string name, JwtBearerOptions options)
{
var originalOnMessageReceived = options.Events.OnMessageReceived;
options.Events.OnMessageReceived = async context =>
{
await originalOnMessageReceived(context);
if (string.IsNullOrEmpty(context.Token))
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
path.StartsWithSegments("/hubs"))
{
context.Token = accessToken;
}
}
};
}
}
Important: Your Route has to start with hubs for the Options to trigger!
(see Line path.StartsWithSegments("/hubs")))
app.UseEndpoints(e =>
{
...
e.MapHub<ChatHub>("hubs/chat");
});
In my case, ASP.NET Core 2.2 I have an API from which I want to be able to use SignalR from the API to connect to my client application.
I have Projects for
Web API
IdentityServer4
MVC Client
With ASP.NET Core Identity as the for user management
In order for your user to be authenticated you need to implement a IUserIdProvider like this
public class IdBasedUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext connection)
{
//TODO: Implement USERID Mapper Here
//throw new NotImplementedException();
//return whatever you want to map/identify the user by here. Either ID/Email
return connection.User.FindFirst("sub").Value;
}
}
With this I make sure I am pushing along the ID/Email to a method I am calling either from the Server or Client. Although I can always use the .User on the HubContext and it works fine.
In my Web API Startup.cs file I came up with
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(cfg =>
{
cfg.AddDefaultPolicy(policy =>
{
policy.WithOrigins(Configuration.GetSection("AuthServer:DomainBaseUrl").Get<string[]>())
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
.SetIsOriginAllowed((_) => true)
.SetIsOriginAllowedToAllowWildcardSubdomains();
});
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, UserManager<AppUser> userManager,
RoleManager<IdentityRole> roleManager){
app.UseCors();
}
NOTE
Configuration.GetSection("AuthServer:DomainBaseUrl").Get() retrieves the list of domains to allow CORS for from a config file.
And I did this configuration in My Client App COnfigureService Method
services.AddCors(cfg =>
{
cfg.AddDefaultPolicy(policy => {
policy.AllowAnyHeader();
policy.AllowAnyMethod();
policy.SetIsOriginAllowed((host) => true);
policy.AllowAnyOrigin();
});
});
I hope this helps your situation.
The best solution is indeed as Ismail Umer described using a seperate authentication service using something like IdentityServer4. And use this service in all other services. This is something I will do in a next iteration.
As short term solution I temporary moved the blazor server part into my api service and use a dual authentication method (JWT header or cookie).
var key = Encoding.UTF8.GetBytes(m_configuration[#"SecurityKey"]);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = #"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true
};
})
.AddCookie();
// TODO: For time being support dual authorization. At later stage split in various micro-services and use IdentityServer4 for Auth
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
CookieAuthenticationDefaults.AuthenticationScheme,
JwtBearerDefaults.AuthenticationScheme);
defaultAuthorizationPolicyBuilder =
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
This is problem with Microsoft.AspNetCore.SignalR.Client 3.1.3.
You can read about it here in comments.
You can wait for update or temporarly fix this issue:
Disable negotiation
Set WebSocket transport explicitly
Modify query url
Add OnMessageReceived handler
Client side:
var token = await GetAccessToken();
var hubConnection = new HubConnectionBuilder()
.WithUrl($"/notification?access_token={token}", options =>
{
options.SkipNegotiation = true;
options.Transports = HttpTransportType.WebSockets;
options.AccessTokenProvider = GetAccessToken;
})
.Build();
Server side:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
// ...
})
.AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/notification", System.StringComparison.InvariantCulture)))
{
context.Token = accessToken;
}
return Task.CompletedTask;
},
};
});
}

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- Challenge ALL requests to API not just [Authorize]

I have an ASP.Net Core 2 API using IdentityServer4. I would like challenge ALL requests to the server and invoke the login redirect if the user is not authenticated, calling back to a specific URL after authentication.
The default is to invoke the login redirect only when an unauthenticated user requests a resource protected by the [Authorize] attribute. This will not work in my use case.
Basically, I want the functional equivalent of an [Authorize] attribute for the whole application not just specific controllers.
What is the easiest way to do this? Is there a setting I can use when configuring the services in Startup.cs (services.AddAuthentication)? Or through custom middleware right after app.UseAuthentication()?
I tried the following custom middleware but it says a handler is not configured.
ConfigureServices
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://localhost:4000";
options.ApiName = "myapi";
});
Configure
app.UseAuthentication();
app.Use(async (context, next) =>
{
if (!context.User.Identity.IsAuthenticated)
{
await context.ChallengeAsync(IdentityServerAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties {RedirectUri= "https://localhost:5000/" });
}
else { await next.Invoke(); }
});
app.UseMvc();
For configuring [Authorize] for the whole controllers, you could try AuthorizeFilter like below
services.AddMvc(config => {
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
For redirecting, you could try configure UserInteraction.LoginUrl
services.AddIdentityServer(opt => {
opt.UserInteraction.LoginUrl = "/Identity/Account/LogIn";
})
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddAspNetIdentity<IdentityUser>();