Only authenticated users can access the application as expected. I need to be able to track users via signalr. For example, if I run a ChatHub type of service, I'd like people to be able to chat using their AzureAD username which should be set automatically and not let people set their own usernames.
The hub always shows Context.Identity.User.Name is null.
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.AddTransient<HubConnectionBuilder>();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub<App>(selector: "app");
endpoints.MapFallbackToPage("/_Host");
endpoints.MapHub<SomeHub>("/SomeHub");
});
Any idea if here is a way to preserve identity information and pass to SignalR?
Inspect your JWT token and check its claims. You can past it on http://jwt.ms/ to decode it. Then, look for the claims that are being returned that references the user name (in my case it is preferred_username).
Then you can change the default mapping of the Identity.Name using this code:
services.Configure<OpenIdConnectOptions>(AzureADDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.NameClaimType = "<claim_name_that_returns_username>";
});
My workaround at the moment will be to just pass the username when the connection is created to the hub.
In codebehind (SomePage.razor.cs)
public class SomePageBase : ComponentBase
{
[Inject]
private HubConnectionBuilder _hubConnectionBuilder { get; set; }
[Inject]
private AuthenticationStateProvider authProvider { get; set; }
protected async override Task OnInitializedAsync()
{
var user = (await authProvider.GetAuthenticationStateAsync()).User.Identity.Name;
// in Component Initialization code
var connection = _hubConnectionBuilder // the injected one from above.
.WithUrl("https://localhost:44331/SomeHub")
.Build(); // Build the HubConnection
await connection.StartAsync();
var stringResult =
await connection.InvokeAsync<string>("HubMethodName", user);
}
}
Related
In my scenario a user can be linked to different tenants. A user should login in the context of a tenant. That means i would like the access token to contain a tenant claim type to restrict access to data of that tenant.
When the client application tries to login i specify an acr value to indicate for which tenant to login.
OnRedirectToIdentityProvider = redirectContext => {
if (redirectContext.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication) {
redirectContext.ProtocolMessage.AcrValues = "tenant:" + tenantId; // the acr value tenant:{value} is treated special by id4 and is made available in IIdentityServerInteractionService
}
return Task.CompletedTask;
}
The value is received by my identity provider solution and is as well available in the IIdentityServerInteractionService.
The question is now, where can i add a claim to the access token for the requested tenant?
IProfileService
In a IProfileService implementation the only point where acr values would be available is in the IsActiveAsync method when context.Caller == AuthorizeEndpoint in the HttpContext via IHttpContextAccessor.
String acr_values = _context.HttpContext.Request.Query["acr_values"].ToString();
But in IsActiveAsync i can not issue claims.
In the GetProfileDataAsync calls the acr values are not available in the ProfileDataRequestContext nor in the HttpContext. Here i wanted to access acr values when
context.Caller = IdentityServerConstants.ProfileDataCallers.ClaimsProviderAccessToken. If i would have access i could issue the tenant claim.
Further i analyzed CustomTokenRequestValidator, IClaimsService and ITokenService without success. It seems like the root problem is, that the token endpoint does not receive/process acr values. (event though here acr is mentioned)
I have a hard time figure this one out. Any help appreciated. Is it maybe completely wrong what i am trying? After figuring this one out i will have as well to understand how this affects access token refresh.
Since you want the user to login for each tenant (bypassing sso) makes this solution possible.
When logging in, you can add a claim to the local user (IdentityServer) where you store the tenant name:
public async Task<IActionResult> Login(LoginViewModel model, string button)
{
// take returnUrl from the query
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
if (context?.ClientId != null)
{
// acr value Tenant
if (context.Tenant == null)
await HttpContext.SignInAsync(user.Id, user.UserName);
else
await HttpContext.SignInAsync(user.Id, user.UserName, new Claim("tenant", context.Tenant));
When the ProfileService is called you can use the claim and pass it to the access token:
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
// Only add the claim to the access token
if (context.Caller == "ClaimsProviderAccessToken")
{
var tenant = context.Subject.FindFirstValue("tenant");
if (tenant != null)
claims.Add(new Claim("tenant", tenant));
}
The claim is now available in the client.
Problem is, that with single sign-on the local user is assigned to the last used tenant. So you need to make sure the user has to login again, ignoring and overwriting the cookie on IdentityServer.
This is the responsibility from the client, so you can set prompt=login to force a login. But originating from the client you may want to make this the responsibility of the server. In that case you may need to override the interaction response generator.
However, it would make sense to do something like this when you want to add tenant specific claims. But it seems you are only interested in making a distinction between tenants.
In that case I wouldn't use above implementation but move from perspective. I think there's an easier solution where you can keep the ability of SSO.
What if the tenant identifies itself at the resource? IdentityServer is a token provider, so why not create a custom token that contains the information of the tenant. Use extension grants to create an access token that combines tenant and user and restricts access to that combination only.
To provide some code for others who want to use the extension grant validator as one suggested option by the accepted answer.
Take care, the code is quick and dirty and must be properly reviewed.
Here is a similar stackoverflow answer with extension grant validator.
IExtensionGrantValidator
using IdentityServer4.Models;
using IdentityServer4.Validation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace IdentityService.Logic {
public class TenantExtensionGrantValidator : IExtensionGrantValidator {
public string GrantType => "Tenant";
private readonly ITokenValidator _validator;
private readonly MyUserManager _userManager;
public TenantExtensionGrantValidator(ITokenValidator validator, MyUserManager userManager) {
_validator = validator;
_userManager = userManager;
}
public async Task ValidateAsync(ExtensionGrantValidationContext context) {
String userToken = context.Request.Raw.Get("AccessToken");
String tenantIdRequested = context.Request.Raw.Get("TenantIdRequested");
if (String.IsNullOrEmpty(userToken)) {
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
var result = await _validator.ValidateAccessTokenAsync(userToken).ConfigureAwait(false);
if (result.IsError) {
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
if (Guid.TryParse(tenantIdRequested, out Guid tenantId)) {
var sub = result.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
var claims = result.Claims.ToList();
claims.RemoveAll(x => x.Type == "tenantid");
IEnumerable<Guid> tenantIdsAvailable = await _userManager.GetTenantIds(Guid.Parse(sub)).ConfigureAwait(false);
if (tenantIdsAvailable.Contains(tenantId)) {
claims.Add(new Claim("tenantid", tenantId.ToString()));
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
context.Result = new GrantValidationResult(principal);
return;
}
}
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
}
}
}
Client config
new Client {
ClientId = "tenant.client",
ClientSecrets = { new Secret("xxx".Sha256()) },
AllowedGrantTypes = new [] { "Tenant" },
RequireConsent = false,
RequirePkce = true,
AccessTokenType = AccessTokenType.Jwt,
AllowOfflineAccess = true,
AllowedScopes = new List<String> {
IdentityServerConstants.StandardScopes.OpenId,
},
},
Token exchange in client
I made a razor page which receives as url parameter the requested tenant id, because my test app is a blazor server side app and i had problems to do a sign in with the new token (via _userStore.StoreTokenAsync). Note that i am using IdentityModel.AspNetCore to manage token refresh. Thats why i am using the IUserTokenStore. Otherwise you would have to do httpcontext.signinasync as Here.
public class TenantSpecificAccessTokenModel : PageModel {
private readonly IUserTokenStore _userTokenStore;
public TenantSpecificAccessTokenModel(IUserTokenStore userTokenStore) {
_userTokenStore = userTokenStore;
}
public async Task OnGetAsync() {
Guid tenantId = Guid.Parse(HttpContext.Request.Query["tenantid"]);
await DoSignInForTenant(tenantId);
}
public async Task DoSignInForTenant(Guid tenantId) {
HttpClient client = new HttpClient();
Dictionary<String, String> parameters = new Dictionary<string, string>();
parameters.Add("AccessToken", await HttpContext.GetUserAccessTokenAsync());
parameters.Add("TenantIdRequested", tenantId.ToString());
TokenRequest tokenRequest = new TokenRequest() {
Address = IdentityProviderConfiguration.Authority + "connect/token",
ClientId = "tenant.client",
ClientSecret = "xxx",
GrantType = "Tenant",
Parameters = parameters
};
TokenResponse tokenResponse = await client.RequestTokenAsync(tokenRequest).ConfigureAwait(false);
if (!tokenResponse.IsError) {
await _userTokenStore.StoreTokenAsync(HttpContext.User, tokenResponse.AccessToken, tokenResponse.ExpiresIn, tokenResponse.RefreshToken);
Response.Redirect(Url.Content("~/").ToString());
}
}
}
I followed a link to achieve google SSO github.com/aspnet/Security/issues/1370. But even after successful login it is taking me to redirect uri mentioned in authentication property. It is not taking to the callback url. Could someone help on this? Our application is a .net core 3.1 with IdentityServer4.
Am expecting signinoauth2 API to be hit after google login, but thats not happening.
I could see a network call from browser with below format and getting correlation error.
https://localhost:44368/signinoauth2?state=&code=&scope=***&prompt=none
Exception: Correlation failed.
Show raw exception details
Exception: An error was encountered while handling the remote login.
Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler.HandleRequestAsync()
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
Soulbook.Api.Startup+<>c+<b__5_1>d.MoveNext() in Startup.cs
await next.Invoke();
Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
PFB my code for reference,
[HttpGet]
[Authorize(AuthenticationSchemes = GoogleDefaults.AuthenticationScheme)]
[Route("/Feed")]
public ActionResult Feed()
{
return Ok();
}
[HttpGet]
[Route("/signin")]
public ActionResult SignIn()
{
var authProperties = new AuthenticationProperties
{
RedirectUri = "/"
};
return new ChallengeResult(GoogleDefaults.AuthenticationScheme, authProperties);
}
[HttpPost]
[Route("/signinoauth2")]
public ActionResult<LoginResponse> signinoauth2Async([FromForm]object data)
{
return Ok();
}
Startup.cs
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddCookie(o => {
o.LoginPath = "/signin";
o.LogoutPath = "/signout";
o.ExpireTimeSpan = TimeSpan.FromDays(7);
})
.AddGoogle(o => {
o.ClientId = "***";
o.ClientSecret = "**";
o.SaveTokens = true;
o.CallbackPath = "/signinoauth2";
});
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(GoogleDefaults.AuthenticationScheme)
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
}).AddNewtonsoftJson();
EDIT: Having signinoauth2 in any one of the below formats also doesnt help.
[HttpGet]
[Route("/signinoauth2")]
public ActionResult<LoginResponse> signinoauth2Async(string state, string code, string scope, string prompt)
{
return Ok();
}
[HttpPost]
[Route("/signinoauth2")]
public ActionResult<LoginResponse> signinoauth2Async(string state, string code, string scope, string prompt)
{
return Ok();
}
I assume that you want to get Google user information in your enpoint?
Then what you have to do is configure the external authentication properties. And thanks to this you are going to be able to get the user on your redirect endpoint.
[HttpGet("login/google/")]
[AllowAnonymous]
public async Task<IActionResult> LoginGoogle()
{
var properties = _signInManager.ConfigureExternalAuthenticationProperties(GoogleDefaults.AuthenticationScheme, "/api/identity/google-redirect");
return Challenge(properties, GoogleDefaults.AuthenticationScheme);
}
What you configured on startup is a callback route which gets handled by Middleware and never hits the endpoint in your controller. What you want to achive is get user on redirect route like this
[HttpGet("google-redirect")]
[AllowAnonymous]
public async Task<IActionResult> CallbackGoogle()
{
var info = await _signInManager.GetExternalLoginInfoAsync();
return Ok();
}
It sounds like you aren't actually being properly authenticated, if you were the app would redirect to the landing page whose controller I assume has an [Authorize] attribute. Could you have possibly forgotten to add yourself as a user in the db that your identity server is referencing?
I have a web application where front-end is reactjs and backend is asp.net core, I want to allow access the page based on the user roles.
I have implemented the authorization in react but for security purpose need to perform authorization on server side.
I have tried the [Authorize(Roles = "Administrator")] but that will not work since I am reusing the controller(common for all user) for different role and also I am not returning the view since I am working with react, so I need help in approach to implement the authorization part on asp.net core (role based)
If you really want to use one action per use case for all roles, you may consider using ControllerBase.HttpContext:
[HttpGet("[action]")]
[Authorize(Roles = "Administrator,User")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetSomething()
{
var userRoles = this.HttpContext.User.Claims.Where(claim => claim.Type == System.Security.Claims.ClaimTypes.Role).Select(claim => claim.Value).ToList();
if (!userRoles.Any())
return Unauthorized();
return await GetSomethingDependingOnGivenRoles(userRoles);
}
You may use also extension for that:
public static class ControllerBaseExtensions
{
public static List<string> GetRoles(this ControllerBase controller)
{
return controller.HttpContext.User.Claims.Where(claim => claim.Type == System.Security.Claims.ClaimTypes.Role).Select(claim => claim.Value).ToList();
}
}
so you can then use it like this:
[HttpGet("[action]")]
[Authorize(Roles = "Administrator,User")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetSomething()
{
var userRoles = this.GetRoles();
if (!userRoles.Any())
return Unauthorized();
return await GetSomethingDependingOnGivenRoles(userRoles);
}
Once approach is to have different actions for each role
[Authorize(Roles = "Administrator")]
public IActionResult Action() {}
We are currently working on a identityserver4 implementation which will also have a few api calls.
Those api calls should only be available if a user is authorized(with the bearer token).
In the Startup.cs we have the services.AddIdentityServer() since this is the identityServer, and also added the AddAuthentication() call to make sure the authorized endpoints are only available for authorized connections.
Startup.cs => ConfigureServices():
services.AddIdentityServer();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:9000";
options.RequireHttpsMetadata = false;
options.ApiName = "identityserver4";
});
Startup.cs => Configure():
app.UseAuthentication();
app.UseIdentityServer();
//app.UseAuthentication();
using the UseAuthentication() before or after UseIdentityServer() does not change anything.
My api call within the identityserver is still avaialble to all.
Currently using postman to test the calls.
Do i need to add something? Is there something i missed?
Kind regards,
Walter
edit 1: added controller and full startup.cs
UserController.cs:
namespace Identity.Controllers
{
[Authorize]
[Route("[controller]")]
public class UserController : ControllerBase
{
private readonly ILogger _logger;
private readonly IUserBusinessLogic _userBusinessLogic;
public UserController(ILogger<UserController> logger, IUserBusinessLogic userBusinessLogic)
: base()
{
_logger = logger;
_userBusinessLogic = userBusinessLogic;
}
[Route("")]
[HttpGet]
public async Task<ActionResult<IList<UserDto>>> GetAllUsers()
{
var users = await _userBusinessLogic.GetAll();
return users.ToList();
}
}
}
Startup.cs:
namespace Identity
{
public class Startup
{
private readonly IConfiguration _configuration;
private readonly ILogger _logger;
public Startup(IConfiguration configuration, ILogger<Startup> logger)
: base()
{
_configuration = configuration;
_logger = logger;
}
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore()
.AddJsonFormatters()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddRazorViewEngine();
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = context => new ValidationProblemDetailsResult();
});
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:9000";
options.RequireHttpsMetadata = false;
options.ApiName = "identityserver4";
});
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddPersistedGrantStore<PersistedGrantStoreBusinessLogic>()
.AddResourceStore<ResourceBusinessLogic>()
.AddClientStore<ClientBusinessLogic>()
.AddProfileService<ProfileBusinessLogic>()
.AddCorsPolicyService<CorsPolicyBusinessLogic>();
services.AddCors(options =>
{
options.AddPolicy("default",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader().Build());
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseCors("default");
app.UseIdentityServer();
app.UseStaticFiles();
app.ConfigureExceptionHandler(_logger);
app.UseMvcWithDefaultRoute();
}
}
}
I just wrote some code for the exact same purpose, and I struggled with the same issues as you do.
According to the Identity Server Doc, do not forget to specify the authentication scheme in [Authorize] attribute.
Startup.cs:
services.AddAuthentication()
.AddIdentityServerAuthentication("Bearer", options =>
{
options.Authority = "http://localhost:9000";
options.RequireHttpsMetadata = false;
options.ApiName = "identityserver4";
});
Note that "Bearer" is given to AddIdentityServerAuthentication and not to AddAuthentication.
Controller.cs:
[Authorize(AuthenticationSchemes = "Bearer")]
public IActionResult YourControllerAction()
{
}
Hope it will works for you!
Found my problem!
in my startup i used services.AddMvcCore() when i should have used services.AddMvc() OR just add the services.AddAuthorization which is not added using services.AddMvcCore().
I came upon this solution after doing some research for something else. In my research i came upon this page: https://offering.solutions/blog/articles/2017/02/07/difference-between-addmvc-addmvcore/
It explains the differences between AddMvc() and AddMvcCore().
So after adding services.AddAuthorization() my problem was solved and the api within my identityserver where protected.
Thank you to all who tried to help me!
this answer may comes late but comes late better than never , using IdentityServer to secure other APIs and do not secure the main token or access provider may seems silly somehow ,so in this case if you want to secure the Api That implement IdentityServer it self you can add the predefined IdentityServer scope IdentityServerApi in the allowed scopes and also for the client scopes , and then you have to configure the services to use the local authentication (provided by identityserver) by adding services.AddLocalApiAuthentication();
and the final part is to add the authorize attribute to the controller or the action method as you wish as follow [Authorize(Policy = LocalApi.PolicyName)]
and in the end you can add claims policy authorization side by side with the local authentication
I have an API that uses IdentityServer4 for token validation.
I want to unit test this API with an in-memory TestServer. I'd like to host the IdentityServer in the in-memory TestServer.
I have managed to create a token from the IdentityServer.
This is how far I've come, but I get an error "Unable to obtain configuration from http://localhost:54100/.well-known/openid-configuration"
The Api uses [Authorize]-attribute with different policies. This is what I want to test.
Can this be done, and what am I doing wrong?
I have tried to look at the source code for IdentityServer4, but have not come across a similar integration test scenario.
protected IntegrationTestBase()
{
var startupAssembly = typeof(Startup).GetTypeInfo().Assembly;
_contentRoot = SolutionPathUtility.GetProjectPath(#"<my project path>", startupAssembly);
Configure(_contentRoot);
var orderApiServerBuilder = new WebHostBuilder()
.UseContentRoot(_contentRoot)
.ConfigureServices(InitializeServices)
.UseStartup<Startup>();
orderApiServerBuilder.Configure(ConfigureApp);
OrderApiTestServer = new TestServer(orderApiServerBuilder);
HttpClient = OrderApiTestServer.CreateClient();
}
private void InitializeServices(IServiceCollection services)
{
var cert = new X509Certificate2(Path.Combine(_contentRoot, "idsvr3test.pfx"), "idsrv3test");
services.AddIdentityServer(options =>
{
options.IssuerUri = "http://localhost:54100";
})
.AddInMemoryClients(Clients.Get())
.AddInMemoryScopes(Scopes.Get())
.AddInMemoryUsers(Users.Get())
.SetSigningCredential(cert);
services.AddAuthorization(options =>
{
options.AddPolicy(OrderApiConstants.StoreIdPolicyName, policy => policy.Requirements.Add(new StoreIdRequirement("storeId")));
});
services.AddSingleton<IPersistedGrantStore, InMemoryPersistedGrantStore>();
services.AddSingleton(_orderManagerMock.Object);
services.AddMvc();
}
private void ConfigureApp(IApplicationBuilder app)
{
app.UseIdentityServer();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
var options = new IdentityServerAuthenticationOptions
{
Authority = _appsettings.IdentityServerAddress,
RequireHttpsMetadata = false,
ScopeName = _appsettings.IdentityServerScopeName,
AutomaticAuthenticate = false
};
app.UseIdentityServerAuthentication(options);
app.UseMvc();
}
And in my unit-test:
private HttpMessageHandler _handler;
const string TokenEndpoint = "http://localhost/connect/token";
public Test()
{
_handler = OrderApiTestServer.CreateHandler();
}
[Fact]
public async Task LeTest()
{
var accessToken = await GetToken();
HttpClient.SetBearerToken(accessToken);
var httpResponseMessage = await HttpClient.GetAsync("stores/11/orders/asdf"); // Fails on this line
}
private async Task<string> GetToken()
{
var client = new TokenClient(TokenEndpoint, "client", "secret", innerHttpMessageHandler: _handler);
var response = await client.RequestClientCredentialsAsync("TheMOON.OrderApi");
return response.AccessToken;
}
You were on the right track with the code posted in your initial question.
The IdentityServerAuthenticationOptions object has properties to override the default HttpMessageHandlers it uses for back channel communication.
Once you combine this with the CreateHandler() method on your TestServer object you get:
//build identity server here
var idBuilder = new WebBuilderHost();
idBuilder.UseStartup<Startup>();
//...
TestServer identityTestServer = new TestServer(idBuilder);
var identityServerClient = identityTestServer.CreateClient();
var token = //use identityServerClient to get Token from IdentityServer
//build Api TestServer
var options = new IdentityServerAuthenticationOptions()
{
Authority = "http://localhost:5001",
// IMPORTANT PART HERE
JwtBackChannelHandler = identityTestServer.CreateHandler(),
IntrospectionDiscoveryHandler = identityTestServer.CreateHandler(),
IntrospectionBackChannelHandler = identityTestServer.CreateHandler()
};
var apiBuilder = new WebHostBuilder();
apiBuilder.ConfigureServices(c => c.AddSingleton(options));
//build api server here
var apiClient = new TestServer(apiBuilder).CreateClient();
apiClient.SetBearerToken(token);
//proceed with auth testing
This allows the AccessTokenValidation middleware in your Api project to communicate directly with your In-Memory IdentityServer without the need to jump through hoops.
As a side note, for an Api project, I find it useful to add IdentityServerAuthenticationOptions to the services collection in Startup.cs using TryAddSingleton instead of creating it inline:
public void ConfigureServices(IServiceCollection services)
{
services.TryAddSingleton(new IdentityServerAuthenticationOptions
{
Authority = Configuration.IdentityServerAuthority(),
ScopeName = "api1",
ScopeSecret = "secret",
//...,
});
}
public void Configure(IApplicationBuilder app)
{
var options = app.ApplicationServices.GetService<IdentityServerAuthenticationOptions>()
app.UseIdentityServerAuthentication(options);
//...
}
This allows you to register the IdentityServerAuthenticationOptions object in your tests without having to alter the code in the Api project.
I understand there is a need for a more complete answer than what #james-fera posted. I have learned from his answer and made a github project consisting of a test project and API project. The code should be self-explanatory and not hard to understand.
https://github.com/emedbo/identityserver-test-template
The IdentityServerSetup.cs class https://github.com/emedbo/identityserver-test-template/blob/master/tests/API.Tests/Config/IdentityServerSetup.cs can be abstracted away e.g. NuGetted away, leaving the base class IntegrationTestBase.cs
The essences is that can make the test IdentityServer work just like a normal IdentityServer, with users, clients, scopes, passwords etc. I have made the DELETE method [Authorize(Role="admin)] to prove this.
Instead of posting code here, I recommend read #james-fera's post to get the basics then pull my project and run tests.
IdentityServer is such a great tool, and with the ability to use the TestServer framework it gets even better.
I think you probably need to make a test double fake for your authorization middleware depending on how much functionality you want. So basically you want a middleware that does everything that the Authorization middleware does minus the back channel call to the discovery doc.
IdentityServer4.AccessTokenValidation is a wrapper around two middlewares. The JwtBearerAuthentication middleware, and the OAuth2IntrospectionAuthentication middleware. Both of these grab the discovery document over http to use for token validation. Which is a problem if you want to do an in-memory self-contained test.
If you want to go through the trouble you will probably need to make a fake version of app.UseIdentityServerAuthentication that doesnt do the external call that fetches the discovery document. It only populates the HttpContext principal so that your [Authorize] policies can be tested.
Check out how the meat of IdentityServer4.AccessTokenValidation looks here. And follow up with a look at how JwtBearer Middleware looks here
We stepped away from trying to host a mock IdentityServer and used dummy/mock authorizers as suggested by others here.
Here's how we did that in case it's useful:
Created a function which takes a type, creates a test Authentication Middleware and adds it to the DI engine using ConfigureTestServices (so that it's called after the call to Startup.)
internal HttpClient GetImpersonatedClient<T>() where T : AuthenticationHandler<AuthenticationSchemeOptions>
{
var _apiFactory = new WebApplicationFactory<Startup>();
var client = _apiFactory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("Test")
.AddScheme<AuthenticationSchemeOptions, T>("Test", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test");
return client;
}
Then we create what we called 'Impersonators' (AuthenticationHandlers) with the desired roles to mimic users with roles (We actually used this as a base class, and create derived classes based on this to mock different users):
public abstract class FreeUserImpersonator : AuthenticationHandler<AuthenticationSchemeOptions>
{
public Impersonator(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
base.claims.Add(new Claim(ClaimTypes.Role, "FreeUser"));
}
protected List<Claim> claims = new List<Claim>();
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "Test");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
Finally, we can perform our integration tests as follows:
// Arrange
HttpClient client = GetImpersonatedClient<FreeUserImpersonator>();
// Act
var response = await client.GetAsync("api/things");
// Assert
Assert.That.IsSuccessful(response);
Test API startup:
public class Startup
{
public static HttpMessageHandler BackChannelHandler { get; set; }
public void Configuration(IAppBuilder app)
{
//accept access tokens from identityserver and require a scope of 'Test'
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost",
BackchannelHttpHandler = BackChannelHandler,
...
});
...
}
}
Assigning the AuthServer.Handler to TestApi BackChannelHandler in my unit test project:
protected TestServer AuthServer { get; set; }
protected TestServer MockApiServer { get; set; }
protected TestServer TestApiServer { get; set; }
[OneTimeSetUp]
public void Setup()
{
...
AuthServer = TestServer.Create<AuthenticationServer.Startup>();
TestApi.Startup.BackChannelHandler = AuthServer.CreateHandler();
TestApiServer = TestServer.Create<TestApi.Startup>();
}
The trick is to create a handler using the TestServer that is configured to use IdentityServer4. Samples can be found here.
I created a nuget-package available to install and test using the Microsoft.AspNetCore.Mvc.Testing library and the latest version of IdentityServer4 for this purpose.
It encapsulates all the infrastructure code necessary to build an appropriate WebHostBuilder which is then used to create a TestServer by generating the HttpMessageHandler for the HttpClient used internally.
None of the other answers worked for me because they rely on 1) a static field to hold your HttpHandler and 2) the Startup class to have knowledge that it may be given a test handler. I've found the following to work, which I think is a lot cleaner.
First create an object that you can instantiate before your TestHost is created. This is because you won't have the HttpHandler until after the TestHost is created, so you need to use a wrapper.
public class TestHttpMessageHandler : DelegatingHandler
{
private ILogger _logger;
public TestHttpMessageHandler(ILogger logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_logger.Information($"Sending HTTP message using TestHttpMessageHandler. Uri: '{request.RequestUri.ToString()}'");
if (WrappedMessageHandler == null) throw new Exception("You must set WrappedMessageHandler before TestHttpMessageHandler can be used.");
var method = typeof(HttpMessageHandler).GetMethod("SendAsync", BindingFlags.Instance | BindingFlags.NonPublic);
var result = method.Invoke(this.WrappedMessageHandler, new object[] { request, cancellationToken });
return await (Task<HttpResponseMessage>)result;
}
public HttpMessageHandler WrappedMessageHandler { get; set; }
}
Then
var testMessageHandler = new TestHttpMessageHandler(logger);
var webHostBuilder = new WebHostBuilder()
...
services.PostConfigureAll<JwtBearerOptions>(options =>
{
options.Audience = "http://localhost";
options.Authority = "http://localhost";
options.BackchannelHttpHandler = testMessageHandler;
});
...
var server = new TestServer(webHostBuilder);
var innerHttpMessageHandler = server.CreateHandler();
testMessageHandler.WrappedMessageHandler = innerHttpMessageHandler;