I am developing a Web API 2 with "OAuth Client Credentials" flow where, in the first call for the authorization token, I need an additional parameter to be accessed after in the controller without the need to traffics it all the time in requests.
What is the best way to pass an additional parameter for the authentication process and uses it later in controllers without the need for traffics it in future requests and considering security issues ?
I found developers passing these parameters in the URL, and adding it to the OWinContext:
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId;
string clientSecret;
if (context.TryGetBasicCredentials(out clientId, out clientSecret) ||
context.TryGetFormCredentials(out clientId, out clientSecret))
{
try
{
if (context.Parameters.Any(x => x.Key == "Participant"))
{
// Get Participant(Additional Parameters)
string participant = context.Parameters.First(x => x.Key == "participant").Value[0];
//Validating Client
Microsoft.AspNet.Identity.PasswordHasher passwordHaser = new Microsoft.AspNet.Identity.PasswordHasher();
Client client = _clientAppService.GetById(Guid.Parse(clientId));
if (client != null && passwordHaser.VerifyHashedPassword(client.SecretHash, clientSecret) == Microsoft.AspNet.Identity.PasswordVerificationResult.Success)
{
context.OwinContext.Set<Client>("oauth:client", client);
//Store Participant(Additional Parameters)
context.OwinContext.Set<Participant>("urn:participant", new Participant { Document = participant });
context.Validated();
}
else
{
context.SetError("invalid_client", "Client credentials are invalid.");
context.Rejected();
}
}
else
{
context.SetError("invalid_request", "Participant are invalid.");
context.Rejected();
}
}
catch (Exception)
{
context.SetError("server_error");
context.Rejected();
}
}
else
{
context.SetError("invalid_client", "Client credentials could not be retrieved through the Authorization header.");
context.Rejected();
}
return Task.FromResult(0);
}
And I am trying to get it via an ModelBinder, unfortunately the context always returns null:
public class ParticipantModelBinder : IModelBinder
{
public bool BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, ModelBindingContext bindingContext)
{
//Returning null
Participant participant = HttpContext.Current.GetOwinContext().Get<Participant>("urn:participant");
bindingContext.Model = participant;
return true;
}
}
Controller:
[Authorize]
public class ParticipantController : ApiController
{
public decimal GetBalance([ModelBinder(BinderType=typeof(ParticipantModelBinder))] Participant participant)
{
return 0;
}
}
And another solution I found on the web is to add a new Claim on the method GrantClientCredentials, but i dont know a way to get the value on the controllers:
public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
{
//Client validated, generate token
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
Client client = context.OwinContext.Get<Client>("oauth:client");
if (client.AllowedGrant == OAuthGrant.Client)
{
ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(context.ClientId, OAuthDefaults.AuthenticationType));
identity.AddClaim(new Claim(ClaimTypes.Role, "User"));
// Add an custum Claim with the additional parameter
identity.AddClaim(new Claim("Participant", context.OwinContext.Get<Participant>("urn:participant").Document));
context.Validated(identity);
}
else
{
context.SetError(
"unauthorized_client",
"The authenticated client is not authorized to use this authorization grant type");
}
return Task.FromResult(0);
}
Related
Good morning. I am having an issue with Authorize in controllers and using Roles and / or Policy set up. It is always returning a 403 forbidden, with the following in the logs:
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
The request address matched a server endpoint: Token.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
The token request was successfully extracted: {
"grant_type": "password",
"username": "Administrator#MRM2Inc.com",
"password": "[redacted]"
}.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
The token request was successfully validated.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
The response was successfully returned as a JSON document: {
"access_token": "[redacted]",
"token_type": "Bearer",
"expires_in": 3600
}.
info: OpenIddict.Validation.OpenIddictValidationDispatcher[0]
The response was successfully returned as a challenge response: {
"error": "insufficient_access",
"error_description": "The user represented by the token is not allowed to perform the requested action.",
"error_uri": "https://documentation.openiddict.com/errors/ID2095"
}.
info: OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandler[13]
AuthenticationScheme: OpenIddict.Validation.AspNetCore was forbidden.
If I remove the Roles = or Policy = from the Authorize Tag it works. I have the project setup as follows:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<IdentDbContext>(options =>
{
options.UseSqlServer(
Configuration.GetConnectionString("IdentityDB"));
options.UseOpenIddict();
});
// Add the Identity Services we are going to be using the Application Users and the Application Roles
services.AddIdentity<ApplicationUsers, ApplicationRoles>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
config.SignIn.RequireConfirmedAccount = true;
config.User.RequireUniqueEmail = true;
config.Lockout.MaxFailedAccessAttempts = 3;
}).AddEntityFrameworkStores<IdentDbContext>()
.AddUserStore<ApplicationUserStore>()
.AddRoleStore<ApplicationRoleStore>()
.AddRoleManager<ApplicationRoleManager>()
.AddUserManager<ApplicationUserManager>()
.AddErrorDescriber<ApplicationIdentityErrorDescriber>()
.AddDefaultTokenProviders()
.AddDefaultUI();
services.AddDataLibrary();
// Configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
});
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
});
// Add in the email
var emailConfig = Configuration.GetSection("EmailConfiguration").Get<EmailConfiguration>();
services.AddSingleton(emailConfig);
services.AddEmailLibrary();
services.AddAuthorization(option =>
{
option.AddPolicy("SiteAdmin", policy => policy.RequireClaim("Site Administrator"));
});
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<IdentDbContext>()
/*.ReplaceDefaultEntities<Guid>()*/;
})
// Register the OpenIddict server components.
.AddServer(options =>
{
// Enable the token endpoint. What other endpoints?
options.SetLogoutEndpointUris("/api/LogoutPost")
.SetTokenEndpointUris("/Token");
// Enable the client credentials flow. Which flow do I need?
options.AllowPasswordFlow();
options.AcceptAnonymousClients();
options.DisableAccessTokenEncryption();
// Register the signing and encryption credentials.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the ASP.NET Core host and configure the ASP.NET Core options.
options.UseAspNetCore()
.EnableLogoutEndpointPassthrough()
.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();
});
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(swagger =>
{
swagger.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer'[space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\""
});
swagger.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] {}
}
});
swagger.OperationFilter<SwaggerDefaultValues>();
swagger.OperationFilter<AuthenticationRequirementOperationFilter>();
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
swagger.IncludeXmlComments(xmlPath);
});
services.AddApiVersioning();
services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVVV";
options.DefaultApiVersion = ApiVersion.Parse("0.6.alpha");
options.AssumeDefaultVersionWhenUnspecified = true;
});
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.DisplayOperationId();
var versionDescription = provider.ApiVersionDescriptions;
foreach (var description in provider.ApiVersionDescriptions.OrderByDescending(_ => _.ApiVersion))
{
c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", $"MRM2 Identity API {description.GroupName}");
}
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
AuthorizationController.cs
/// <summary>
/// Controls the Authorization aspects of the API
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
[ApiVersion("0.8.alpha")]
[Produces(MediaTypeNames.Application.Json)]
public class AuthorizationController : ControllerBase
{
private readonly IConfiguration _configuration;
private readonly IdentDbContext _context;
private readonly ApplicationUserManager _userManager;
private readonly ApplicationRoleManager _roleManager;
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictAuthorizationManager _authorizationManager;
private readonly IOpenIddictScopeManager _scopeManager;
private readonly SignInManager<ApplicationUsers> _signInManager;
private HttpClient _client;
public AuthorizationController(IConfiguration configuration, IdentDbContext context, ApplicationUserManager userManager,
ApplicationRoleManager roleManager, IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager,
IOpenIddictScopeManager scopeManager, SignInManager<ApplicationUsers> signInManager)
{
_configuration = configuration;
_context = context;
_userManager = userManager;
_roleManager = roleManager;
_applicationManager = applicationManager;
_authorizationManager = authorizationManager;
_scopeManager = scopeManager;
_signInManager = signInManager;
}
[HttpPost("/token"), Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
ClaimsPrincipal claimsPrincipal;
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
var roleList = await _userManager.GetRolesListAsync(user);
var databaseList = await _userManager.GetDatabasesAsync(user);
string symKey = _configuration["Jwt:Symmetrical:Key"];
string jwtSub = _configuration["Jwt:Subject"];
string issuer = _configuration["Jwt:Issuer"];
string audience = _configuration["Jwt:Audience"];
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, jwtSub, issuer),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), issuer),
new Claim(ClaimTypes.Name, user.UserName, issuer)
};
foreach (var role in roleList)
{
claims.Add(new Claim(ClaimTypes.Role, role.Name));
}
foreach (var database in databaseList)
{
claims.Add(new Claim(type: "DatabaseName", database));
}
var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
identity.AddClaim(OpenIddictConstants.Claims.Name, user.UserName, OpenIddictConstants.Destinations.AccessToken);
identity.AddClaim(OpenIddictConstants.Claims.Subject, jwtSub, OpenIddictConstants.Destinations.AccessToken);
identity.AddClaim(OpenIddictConstants.Claims.Audience, audience, OpenIddictConstants.Destinations.AccessToken);
foreach (var cl in claims)
{
identity.AddClaim(cl.Type, cl.Value);
}
claimsPrincipal = new ClaimsPrincipal(identity);
// Set the list of scopes granted to the client application.
claimsPrincipal.SetScopes(new[]
{
Scopes.OpenId,
Scopes.Email,
Scopes.Profile,
Scopes.Roles
}.Intersect(request.GetScopes()));
foreach (var claim in claimsPrincipal.Claims)
{
claim.SetDestinations(GetDestinations(claim, claimsPrincipal));
}
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the authorization code/device code/refresh token.
var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
// Retrieve the user profile corresponding to the authorization code/refresh token.
// Note: if you want to automatically invalidate the authorization code/refresh token
// when the user password/roles change, use the following line instead:
// var user = _signInManager.ValidateSecurityStampAsync(info.Principal);
var user = await _userManager.GetUserAsync(principal);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
}));
}
// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
}));
}
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
{
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
switch (claim.Type)
{
case Claims.Name:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
case Claims.Email:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Email))
yield return Destinations.IdentityToken;
yield break;
case Claims.Role:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Roles))
yield return Destinations.IdentityToken;
yield break;
// Never include the security stamp in the access and identity tokens, as it's a secret value.
case "AspNet.Identity.SecurityStamp": yield break;
default:
yield return Destinations.AccessToken;
yield break;
}
}
}
RolesController.cs
/// <summary>
/// Controls the actions for roles within the API
/// </summary>
/// <response code="401">If the user did not login correctly or
/// does not have the correct permissions</response>
[Route("api/[controller]")]
[ApiController]
[ApiVersion("0.8.alpha")]
[Produces(MediaTypeNames.Application.Json)]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme, Roles = "Site Administrator")] //If I change this to Policy = "SiteAdmin" still does not work. If I remove the Roles completely it works.
public class RolesController : ControllerBase
{
private readonly ApplicationRoleManager _roleManager;
private readonly ILogger<RolesController> _logger;
private readonly IApplicationDatabaseData _databaseData;
public RolesController(ApplicationRoleManager roleManager, ILogger<RolesController> logger, IApplicationDatabaseData databaseData)
{
_roleManager = roleManager;
_logger = logger;
_databaseData = databaseData;
}
/// <summary>
/// Gets a List of all the Roles
/// </summary>
/// <returns>A list of ApplicationRoles</returns>
/// <response code="200">Returns the list</response>
/// <response code="404">If the list is empty</response>
[HttpGet("ListRoles", Name = nameof(ListRolesAsync))]
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme/*, Roles = "Site Administrator"*/)] //Currently commented out until Roles work.
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<IList<ApplicationRoles>>> ListRolesAsync()
{
var roles = await _roleManager.GetAllRoles();
if (!roles.Any())
{
return NotFound();
}
else
{
var output = roles;
return Ok(output);
}
}
}
What I noticed that when stepping through this in debugging except for the Claims.Name when going through GetDestinations all claims hit the default switch case. So I am not sure where in the startup or authorizationcontoller I went wrong. But I am pretty sure my issue is within there.
What have I missed to get my Roles and / or Policy to work correctly within the Controllers?
Update to the AuthorizationController allowed this to work. New section of the AuthorizationContoller for the Exchange method is as follows (still a work in progress, but now a working work in progress):
[HttpPost("/token"), Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
ClaimsPrincipal claimsPrincipal;
if (request.IsClientCredentialsGrantType())
{
var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
identity.AddClaim(OpenIddictConstants.Claims.Subject, request.ClientId ?? throw new InvalidOperationException());
identity.AddClaim("some-claim", "some-value", OpenIddictConstants.Destinations.AccessToken);
claimsPrincipal = new ClaimsPrincipal(identity);
claimsPrincipal.SetScopes(request.GetScopes());
}
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
var roleList = await _userManager.GetRolesListAsync(user);
var databaseList = await _userManager.GetDatabasesAsync(user);
string symKey = _configuration["Jwt:Symmetrical:Key"];
string jwtSub = _configuration["Jwt:Subject"] + " " + user.Id;
string issuer = _configuration["Jwt:Issuer"];
string audience = _configuration["Jwt:Audience"];
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, jwtSub, issuer),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), issuer),
new Claim(ClaimTypes.Name, user.UserName, issuer)
};
foreach (var role in roleList)
{
claims.Add(new Claim(ClaimTypes.Role, role.Name, ClaimValueTypes.String, issuer));
}
foreach (var database in databaseList)
{
claims.Add(new Claim(type: "DatabaseName", database, ClaimValueTypes.String, issuer));
}
var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
identity.AddClaim(OpenIddictConstants.Claims.Name, user.UserName, OpenIddictConstants.Destinations.AccessToken);
identity.AddClaim(OpenIddictConstants.Claims.Subject, jwtSub, OpenIddictConstants.Destinations.AccessToken);
identity.AddClaim(OpenIddictConstants.Claims.Audience, audience, OpenIddictConstants.Destinations.AccessToken);
foreach (var cl in claims)
{
if (cl.Type == ClaimTypes.Role)
{
identity.AddClaim(OpenIddictConstants.Claims.Role, cl.Value, OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken);
}
identity.AddClaim(cl.Type, cl.Value, OpenIddictConstants.Destinations.IdentityToken);
}
claimsPrincipal = new ClaimsPrincipal(identity);
// Set the list of scopes granted to the client application.
claimsPrincipal.SetScopes(new[]
{
Scopes.OpenId,
Scopes.Email,
Scopes.Profile,
Scopes.Roles
}.Intersect(request.GetScopes()));
foreach (var claim in claimsPrincipal.Claims)
{
claim.SetDestinations(GetDestinations(claim, claimsPrincipal));
}
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the authorization code/device code/refresh token.
var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
// Retrieve the user profile corresponding to the authorization code/refresh token.
// Note: if you want to automatically invalidate the authorization code/refresh token
// when the user password/roles change, use the following line instead:
// var user = _signInManager.ValidateSecurityStampAsync(info.Principal);
var user = await _userManager.GetUserAsync(principal);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
}));
}
// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
}));
}
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
I'm using ASP.NET Core 3.1, and I have multiple external login providers configured:
services.AddAuthentication()
.AddDeviantArt(d => {
d.Scope.Add("feed");
d.ClientId = Configuration["Authentication:DeviantArt:ClientId"];
d.ClientSecret = Configuration["Authentication:DeviantArt:ClientSecret"];
d.SaveTokens = true;
})
.AddTwitter(t => {
t.ConsumerKey = Configuration["Authentication:Twitter:ConsumerKey"];
t.ConsumerSecret = Configuration["Authentication:Twitter:ConsumerSecret"];
t.SaveTokens = true;
});
I'd like to create a custom AuthenticationProvider that, instead of redirecting to another website, asks for that website's "API key" and treats that as the access token. (The website in question does not support any version of OAuth.)
Before actually hooking it up, I want to test that I can get a custom AuthenticationProvider working at all, so I found one that implements HTTP Basic authentication:
public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> {
public CustomAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
if (!Request.Headers.ContainsKey("Authorization")) {
return AuthenticateResult.NoResult();
}
if (!AuthenticationHeaderValue.TryParse(Request.Headers["Authorization"], out AuthenticationHeaderValue headerValue)) {
return AuthenticateResult.NoResult();
}
if (!"Basic".Equals(headerValue.Scheme, StringComparison.OrdinalIgnoreCase)) {
return AuthenticateResult.NoResult();
}
byte[] headerValueBytes = Convert.FromBase64String(headerValue.Parameter);
string userAndPassword = Encoding.UTF8.GetString(headerValueBytes);
string[] parts = userAndPassword.Split(':');
if (parts.Length != 2) {
return AuthenticateResult.Fail("Invalid Basic authentication header");
}
string user = parts[0];
string password = parts[1];
bool isValidUser = true;
if (!isValidUser) {
return AuthenticateResult.Fail("Invalid username or password");
}
var claims = new[] { new Claim(ClaimTypes.Name, user) };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties) {
Response.Headers["WWW-Authenticate"] = $"Basic realm=\"Custom realm name here\", charset=\"UTF-8\"";
await base.HandleChallengeAsync(properties);
}
}
I added this to Startup.cs:
.AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>("Test", "Testing", o => { })
The problem is that the HandleAuthenticateAsync method is never called. The other solutions I find to this problem generally say you need to make it the "default" authentication scheme, but I don't want this to interfere with how the other external login providers are set up.
Is there a way I can get this working without changing the default authentication scheme or adding additional attributes to my controllers or actions?
You need add a default Scheme when you add AddAuthentication
like services.AddAuthentication("Test")
I need to implement multi-tenant REST API on asp.net core and use the Jwt Web token for authentication.
Asp.net core docs suggest using the following code in Startup.cs ConfigureServices method:
services.AddAuthentication().AddJwtBearer("Bearer", options =>
{
options.Audience = "MyAudience";
options.Authority = "https://myauhorityserver.com";
}
The issue is that my REST API application is multi-tenant. The tenant is discovered from the URL, e.g.
https://apple.myapi.com,
https://samsung.myapi.com,
https://google.myapi.com
So each of such URLs will eventually point to the same IP, but based on the first word in the URL the app discovers tenant on using the appropriate DB connection.
Each such tenant has its own Authority URL. We use Keycloak as an identity management server, so each tenant on it has its own REALM.
So the Authority URL per tenant is something like that:
https://mykeycloack.com/auth/realms/11111111,
https://mykeycloack.com/auth/realms/22222222,
https://mykeycloack.com/auth/realms/33333333
The API application should be able to add and remove tenants dynamically, without restarting the application so, setting all the tenants in the application startup is not a good idea.
I was trying to add more schemas with more calls to AddJwtBearer, however, all the calls go to the schema "Bearer", according to options.Events.OnAuthenticationFailed event. It's not clear how to make other schemas to handle calls with Bearer token in HTTP header. Even though if it's somehow possible with a help of custom middle-ware, as I mention before providing tenant-specific configuration for Bearer token authentication in the app startup is not a solution as new tenants need to be added dynamically.
Additional info:
According to the fiddler, the Authority URL is finally combined with
/.well-known/openid-configuration
and called when the first request comes to the API endpoint marked with
[Authorize]
If the to configuration fails, the API call fails too. If the call to configuration is successful, the application is not calling it again on the next API requests.
I have found a solution and made it a Nuget Package for reusability you can take a look.
Pretty much I replace the JwtBearerTokenHandler by a DynamicBearerTokenHandler, it gives the flexibility, to resolve OpenIdConnectOptions at runtime.
I've also made another package that cares care of that for you, the only thing you need is to resolve the authority for the ongoing request, you receive the HttpContext.
Here is the package:
https://github.com/PoweredSoft/DynamicJwtBearer
How to use it.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddMemoryCache();
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddDynamicJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.ValidateAudience = false;
})
.AddDynamicAuthorityJwtBearerResolver<ResolveAuthorityService>();
services.AddControllers();
}
}
The service
internal class ResolveAuthorityService : IDynamicJwtBearerAuthorityResolver
{
private readonly IConfiguration configuration;
public ResolveAuthorityService(IConfiguration configuration)
{
this.configuration = configuration;
}
public TimeSpan ExpirationOfConfiguration => TimeSpan.FromHours(1);
public Task<string> ResolveAuthority(HttpContext httpContext)
{
var realm = httpContext.Request.Headers["X-Tenant"].FirstOrDefault() ?? configuration["KeyCloak:MasterRealm"];
var authority = $"{configuration["KeyCloak:Endpoint"]}/realms/{realm}";
return Task.FromResult(authority);
}
}
Whats different from the original one
// before
if (_configuration == null && Options.ConfigurationManager != null)
{
_configuration = await
Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}
// after
var currentConfiguration = await this.dynamicJwtBearerHanderConfigurationResolver.ResolveCurrentOpenIdConfiguration(Context);
How its replaced
public static AuthenticationBuilder AddDynamicJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, Action<JwtBearerOptions> action = null)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
if (action != null)
return builder.AddScheme<JwtBearerOptions, DynamicJwtBearerHandler>(authenticationScheme, null, action);
return builder.AddScheme<JwtBearerOptions, DynamicJwtBearerHandler>(authenticationScheme, null, _ => { });
}
Source of the Handler
public class DynamicJwtBearerHandler : JwtBearerHandler
{
private readonly IDynamicJwtBearerHanderConfigurationResolver dynamicJwtBearerHanderConfigurationResolver;
public DynamicJwtBearerHandler(IOptionsMonitor<JwtBearerOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IDynamicJwtBearerHanderConfigurationResolver dynamicJwtBearerHanderConfigurationResolver) : base(options, logger, encoder, clock)
{
this.dynamicJwtBearerHanderConfigurationResolver = dynamicJwtBearerHanderConfigurationResolver;
}
/// <summary>
/// Searches the 'Authorization' header for a 'Bearer' token. If the 'Bearer' token is found, it is validated using <see cref="TokenValidationParameters"/> set in the options.
/// </summary>
/// <returns></returns>
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
string token = null;
try
{
// Give application opportunity to find from a different location, adjust, or reject token
var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options);
// event can set the token
await Events.MessageReceived(messageReceivedContext);
if (messageReceivedContext.Result != null)
{
return messageReceivedContext.Result;
}
// If application retrieved token from somewhere else, use that.
token = messageReceivedContext.Token;
if (string.IsNullOrEmpty(token))
{
string authorization = Request.Headers[HeaderNames.Authorization];
// If no authorization header found, nothing to process further
if (string.IsNullOrEmpty(authorization))
{
return AuthenticateResult.NoResult();
}
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
token = authorization.Substring("Bearer ".Length).Trim();
}
// If no token found, no further work possible
if (string.IsNullOrEmpty(token))
{
return AuthenticateResult.NoResult();
}
}
var currentConfiguration = await this.dynamicJwtBearerHanderConfigurationResolver.ResolveCurrentOpenIdConfiguration(Context);
var validationParameters = Options.TokenValidationParameters.Clone();
if (currentConfiguration != null)
{
var issuers = new[] { currentConfiguration.Issuer };
validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers;
validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(currentConfiguration.SigningKeys)
?? currentConfiguration.SigningKeys;
}
List<Exception> validationFailures = null;
SecurityToken validatedToken;
foreach (var validator in Options.SecurityTokenValidators)
{
if (validator.CanReadToken(token))
{
ClaimsPrincipal principal;
try
{
principal = validator.ValidateToken(token, validationParameters, out validatedToken);
}
catch (Exception ex)
{
Logger.TokenValidationFailed(ex);
// Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
&& ex is SecurityTokenSignatureKeyNotFoundException)
{
Options.ConfigurationManager.RequestRefresh();
}
if (validationFailures == null)
{
validationFailures = new List<Exception>(1);
}
validationFailures.Add(ex);
continue;
}
Logger.TokenValidationSucceeded();
var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
{
Principal = principal,
SecurityToken = validatedToken
};
await Events.TokenValidated(tokenValidatedContext);
if (tokenValidatedContext.Result != null)
{
return tokenValidatedContext.Result;
}
if (Options.SaveToken)
{
tokenValidatedContext.Properties.StoreTokens(new[]
{
new AuthenticationToken { Name = "access_token", Value = token }
});
}
tokenValidatedContext.Success();
return tokenValidatedContext.Result;
}
}
if (validationFailures != null)
{
var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
{
Exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures)
};
await Events.AuthenticationFailed(authenticationFailedContext);
if (authenticationFailedContext.Result != null)
{
return authenticationFailedContext.Result;
}
return AuthenticateResult.Fail(authenticationFailedContext.Exception);
}
return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]");
}
catch (Exception ex)
{
Logger.ErrorProcessingMessage(ex);
var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
{
Exception = ex
};
await Events.AuthenticationFailed(authenticationFailedContext);
if (authenticationFailedContext.Result != null)
{
return authenticationFailedContext.Result;
}
throw;
}
}
}
If you're still trying to figure this out you can try Finbuckle.
https://www.finbuckle.com/MultiTenant/Docs/Authentication
I'm usin ROPC flow with IdentityServer4 with ASP.NET Core Identity where I need to send username and password to /connect/token endpoint to get access_token.
How to configure IdentityServer4 to accept either Username or Email, and password in /connect/token requests?
PS: asp.net core: 2.0.2
IdentityServer4: 2.0.2
Found this solution:
1) Copy file IdentityServer4.AspNetIdentity/src/IdentityServer4.AspNetIdentity/ResourceOwnerPasswordValidator.cs to your project from https://github.com/IdentityServer/IdentityServer4.AspNetIdentity/blob/dev/src/IdentityServer4.AspNetIdentity/ResourceOwnerPasswordValidator.cs
2) Fix ValidateAsync method to find user with email
var user = await _userManager.FindByNameAsync(context.UserName);
if (user == null) {
user = await _userManager.FindByEmailAsync(context.UserName);
}
3) Add Validator to IS4:
.AddAspNetIdentity<AppUser>()
.AddResourceOwnerValidator<Services.ResourceOwnerPasswordValidator<AppUser>>();
4) Profit!
You need to override the ValidateAsync by creating the Custom PasswordValidator class which implement IResourceOwnerPasswordValidator interface like below.
public class OwnerPasswordValidatorService : IResourceOwnerPasswordValidator
{
public UserManager<ApplicationUser> _userManager { get; }
public OwnerPasswordValidatorService(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
var user = await _userManager.FindByNameAsync(context.UserName);
if (user == null)
{
user = await _userManager.FindByEmailAsync(context.UserName);
if (user == null)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Username or password is incorrect");
return;
}
}
var passwordValid = await _userManager.CheckPasswordAsync(user, context.Password);
if (!passwordValid)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Username or password is incorrect");
return;
}
context.Result = new GrantValidationResult(subject: context.UserName, authenticationMethod: "custom");
}
}
Then configure your dependency for your custom class like below.
services.AddIdentityServer()
.AddDeveloperSigningCredential()
...
.AddAspNetIdentity<ApplicationUser>()
.AddResourceOwnerValidator<OwnerPasswordValidatorService>() //here
.AddProfileService<ProfileService>();
If any one still cannot make it work let me know. I will be happy to help.
I just came across a great reference example of using authenticated WebAPI with AngularJS:
http://www.codeproject.com/Articles/742532/Using-Web-API-Individual-User-Account-plus-CORS-En?msg=4841205#xx4841205xx
An ideal solution for me would be to have such WebAPI service self hosted instead of running it as a Web application.
I just do not know where to place all of the authentication / authorization logic within a self hosted (OWIN / Topshelf) solution.
For example, in the Web app, we have these two files: Startup.Auth, and ApplicationOAuthProvider:
Startup.Auth:
public partial class Startup
{
static Startup()
{
PublicClientId = "self";
UserManagerFactory = () => new UserManager<IdentityUser>(new UserStore<IdentityUser>());
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId, UserManagerFactory),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
AllowInsecureHttp = true
};
}
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
public static Func<UserManager<IdentityUser>> UserManagerFactory { get; set; }
public static string PublicClientId { get; private set; }
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
}
}
ApplicationOAuthProvider:
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
private readonly string _publicClientId;
private readonly Func<UserManager<IdentityUser>> _userManagerFactory;
public ApplicationOAuthProvider(string publicClientId, Func<UserManager<IdentityUser>> userManagerFactory)
{
if (publicClientId == null)
{
throw new ArgumentNullException("publicClientId");
}
if (userManagerFactory == null)
{
throw new ArgumentNullException("userManagerFactory");
}
_publicClientId = publicClientId;
_userManagerFactory = userManagerFactory;
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
// Add Access-Control-Allow-Origin header as Enabling the Web API CORS will not enable it for this provider request.
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
using (UserManager<IdentityUser> userManager = _userManagerFactory())
{
IdentityUser user = await userManager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
ClaimsIdentity oAuthIdentity = await userManager.CreateIdentityAsync(user,
context.Options.AuthenticationType);
ClaimsIdentity cookiesIdentity = await userManager.CreateIdentityAsync(user,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
}
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Resource owner password credentials does not provide a client ID.
if (context.ClientId == null)
{
context.Validated();
}
return Task.FromResult<object>(null);
}
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == _publicClientId)
{
Uri expectedRootUri = new Uri(context.Request.Uri, "/");
if (expectedRootUri.AbsoluteUri == context.RedirectUri)
{
context.Validated();
}
}
return Task.FromResult<object>(null);
}
public static AuthenticationProperties CreateProperties(string userName)
{
IDictionary<string, string> data = new Dictionary<string, string>
{
{ "userName", userName }
};
return new AuthenticationProperties(data);
}
}
I'm looking for a way to integrate these into my OWIN self hosted app, and have these authentication features. start upon application startup, and function as they do in the Web app version.