I am using client credential/app identity flow (OAuth 2.0) where the API is able to authenticate the web app by its app id. There are 2 things that I need to make sure the authentication is successful:
The access token passed from web app to access the API should be a valid bearer token (eg: not expired, valid format, etc)
The app id from the access token has to be the specified web app
When I put the [authorize] attribute in the controller class, it kept returning 401.
Here is startup.cs class
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddAzureAdBearer(options => Configuration.Bind("AzureAd", options));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseMvc();
}
AzureAdAuthenticationBuilderExtentsions class
public static class AzureAdAuthenticationBuilderExtentsions
{
public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder)
=> builder.AddAzureAdBearer(_ => { });
public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
{
builder.Services.Configure(configureOptions);
builder.Services.AddSingleton<IConfigureOptions<JwtBearerOptions>, ConfigureAzureOptions>();
builder.AddJwtBearer();
return builder;
}
private class ConfigureAzureOptions : IConfigureNamedOptions<JwtBearerOptions>
{
private readonly AzureAdOptions _azureOptions;
public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions)
{
_azureOptions = azureOptions.Value;
}
public void Configure(string name, JwtBearerOptions options)
{
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidAudiences = new string[] {
_azureOptions.ClientId,
_azureOptions.ClientIdUrl
},
ValidateAudience = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
RequireExpirationTime = true
};
options.Audience = _azureOptions.ClientId;
options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
}
public void Configure(JwtBearerOptions options)
{
Configure(Options.DefaultName, options);
}
}
}
Here's AzureAdOptions class
public class AzureAdOptions
{
internal static readonly object Settings;
public string ClientId { get; set; }
public string ClientIdUrl { get; set; }
public string ClientSecret { get; set; }
public string Instance { get; set; }
public string Domain { get; set; }
public string TenantId { get; set; }
}
And controller class
[Route("api")]
[ApiController]
public class FindController : ControllerBase
{
private IConfiguration _configuration;
HttpClient _client;
public ContentController( IConfiguration configuration)
{
_configuration = configuration;
}
private bool ValidateRequest()
{
var authHeader = Request.Headers["Authorization"];
if (StringValues.IsNullOrEmpty(authHeader) || authHeader.Count == 0)
{
throw new UnauthorizedAccessException(Messages.AuthHeaderIsRequired);
}
var tokenWithBearer = authHeader.Single();
var token = tokenWithBearer.Substring(7); //remove bearer in the token
var jwtHandler = new JwtSecurityTokenHandler();
if (!jwtHandler.CanReadToken(token))
{
throw new FormatException("Invalid JWT Token");
}
var tokenS = jwtHandler.ReadToken(token) as JwtSecurityToken;
var appId = tokenS.Audiences.First();
if (string.IsNullOrEmpty(appId))
{
throw new UnauthorizedAccessException(Messages.AppIdIsMissing);
}
var registeredAppId = _configuration.GetSection("AzureAd:AuthorizedApplicationIdList")?.Get<List<string>>();
return (registeredAppId.Contains(appId)) ? true : false;
}
[HttpPost("Find")]
[Produces("application/json")]
[Authorize]
public async Task<IActionResult> Find()
{
try
{
if (!ValidateRequest())
{
return Unauthorized();
}
return new ObjectResult("hello world!");
}
catch (InvalidOperationException)
{
return null;
}
}
}
Anyone knows why it keeps returning 401 error? One thing I would like to mention is between the time I start calling the API till it returns 401 error, the break points inside the controller class never got hit...
If the resource is App ID URI of the api application when acquiring access token for accessing api application . In api application , allowed audience should also include the App ID URI of the api application .
I would make 2 points for you to check:
1. Check for appID matching AuthorizedApplicationIdList in your code
I think the way you have described conditions to check in words is fine, but there is a problem with how you have implemented the second condition in code.
The app id from the access token has to be the specified web app
When implementing this condition, it seems you are setting appId as the value from aud i.e. audience claim in token. This is incorrect, because audience will always be your own API for which this token is intended.
What you want to check instead is the value for appid claim in token, which will be the application ID for client that acquired this token. This should be the front end web app's application ID which you want to check against your list of Authorized applications.
Take a look at Microsoft Docs reference for Access Tokens
Also, you can verify this easily by decoding the token using https://jwt.ms
Relevant code from your post where I see issue:
var appId = tokenS.Audiences.First();
if (string.IsNullOrEmpty(appId))
{
throw new UnauthorizedAccessException(Messages.AppIdIsMissing);
}
var registeredAppId = _configuration.GetSection("AzureAd:AuthorizedApplicationIdList")?.Get<List<string>>();
return (registeredAppId.Contains(appId)) ? true : false;
2. General Log/Debug
Also, on a side note, you can probably debug or put log/trace statements in your API code to find out exactly where it's failing in your code.. or if it unexpectedly fails somewhere even before your custom logic is called. Maybe while some of the initial validations are being performed.
Related
I am working on ASP.NET Core Web API. I am trying to create a custom Authorize attribute but I am stuck. I could not understand what I am missing. I have the following code for the Authorize attribute and filter:
public class AuthorizeAttribute : TypeFilterAttribute
{
public AuthorizeAttribute(params string[] claim) : base(typeof(AuthorizeFilter))
{
Arguments = new object[] { claim };
}
}
public class AuthorizeFilter : IAuthorizationFilter
{
readonly string[] _claim;
public AuthorizeFilter(params string[] claim)
{
_claim = claim;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var IsAuthenticated = context.HttpContext.User.Identity.IsAuthenticated;
var claimsIndentity = context.HttpContext.User.Identity as ClaimsIdentity;
if (IsAuthenticated)
{
bool flagClaim = false;
foreach (var item in _claim)
{
if (context.HttpContext.User.HasClaim("Role", item))
flagClaim = true;
}
if (!flagClaim)
{
//if (context.HttpContext.Request.IsAjaxRequest())
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; //Set HTTP 403
//else
// context.Result = new RedirectResult("~/Login/Index");
}
}
else
{
//if (context.HttpContext.Request.IsAjaxRequest())
//{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; //Set HTTP 401 -
//}
//else
//{
// context.Result = new RedirectResult("~/Login/Index");
//}
}
return;
}
}
I have copied this code from somewhere and commented unnecessary lines.
Here is my controller class where I am trying to put this:
[Route("api/[controller]/[action]")]
[ApiController]
[Authorize]
public class JobController : ControllerBase
{
// GET: api/<JobController>
[HttpGet]
[ActionName("GetAll")]
public List<Job> Get()
{
return JobDataLog.GetAllJobQueue();
}
// GET api/<JobController>/5
[HttpGet("{ID}")]
[ActionName("GetByID")]
public Job Get(Guid ID)
{
return JobDataLog.GetJob(ID);
}
// GET api/<JobController>/5
[HttpGet]
[ActionName("GetCount")]
public int GetCount()
{
return JobDataLog.GetJobTotal();
}
}
Also the Configure and ConfigureService methods of Startup.cs
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(60);
});
var tokenKey = Configuration.GetValue<string>("TokenKey");
var key = Encoding.ASCII.GetBytes(tokenKey);
services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; })
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddSingleton<IJWTAuthenticationManager>(new JWTAuthenticationManager(tokenKey));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseCookiePolicy();
app.UseSession();
app.Use(async (context, next) =>
{
var JWToken = context.Session.GetString("JWToken");
if (!string.IsNullOrEmpty(JWToken))
{
context.Request.Headers.Add("Authorization", "Bearer " + JWToken);
}
await next();
});
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
The problem is that even this controller has the Authorize attribute, all the actions are being called even the Authorize filter invalidates the authorization.
Also when I placed the following code in the OnAuthorization method:
context.Result = new StatusCodeResult(StatusCodes.Status401Unauthorized);
It blocked the access of all actions, including those which have an AllowAnnoynmous attribute.
Please help me, I have been stuck on this for last 3 hours.
If you really want to use a custom AuthorizeAttribute, here you go, this works. :)
You'll have a few squiggly lines, but VS will be able to automatically add the using statements.
The original code had multiple problems:
Setting Reponse.StatusCode doesn't actually lead to a response being returned.
HttpContext.User wouldn't be populated in the first place, because ASP.NET Core only attempts to authenticate the user and populate the user's claims/identity if an endpoint is secured with the built-in AuthorizeAttribute. The following code solves this by deriving from AuthorizeAttribute.
In this case the additional filter factory class wasn't needed, since you're not injecting dependencies. Though, if you had to inject, you'd be out of luck I think, because you couldn't derive both from TypeFilterAttribute and AuthorizeAttribute, and the claims list would be always empty.
Working code
public class MyAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
readonly string[] _requiredClaims;
public MyAuthorizeAttribute(params string[] claims)
{
_requiredClaims = claims;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var isAuthenticated = context.HttpContext.User.Identity.IsAuthenticated;
if (!isAuthenticated)
{
context.Result = new UnauthorizedResult();
return;
}
var hasAllRequredClaims = _requiredClaims.All(claim => context.HttpContext.User.HasClaim(x => x.Type == claim));
if (!hasAllRequredClaims)
{
context.Result = new ForbidResult();
return;
}
}
}
You should probably use policies instead
The reason why this works in such a crappy way is that the ASP.NET Core team doesn't want you to write custom Authorize Attributes. See this answer on the subject. The 'proper' way is to create policies, and assign your claim requirements to those policies. But I also think it's silly that authorization is so inflexible and lacking support for basic scenarios.
We are using .NET Core 3.1 to develop a web application. We want to use Google.Apis.Drive.v3 NuGet package to list all files saved in Google Drive. The account from which we want to retrieve files will always be the same, ex. company-data#company.com. We found official documentation on how to authenticate in web applications. However, this example doesn't seem to be working in .NET Core.
Can anyone provide a simple example on how to authenticate against Google Drive API and list all files. Official documentation doesn't cover .NET Core at all.
EDIT: There is a very similar question here. Unfortunately, there are no answers.
This is an example with Google analytics let me know if you need help altering it for drive.
startup.cs
public class Client
{
public class Web
{
public string client_id { get; set; }
public string client_secret { get; set; }
}
public Web web { get; set; }
}
public class ClientInfo
{
public Client Client { get; set; }
private readonly IConfiguration _configuration;
public ClientInfo(IConfiguration configuration)
{
_configuration = configuration;
Client = Load();
}
private Client Load()
{
var filePath = _configuration["TEST_WEB_CLIENT_SECRET_FILENAME"];
if (string.IsNullOrEmpty(filePath))
{
throw new InvalidOperationException(
$"Please set the TEST_WEB_CLIENT_SECRET_FILENAME environment variable before running tests.");
}
if (!File.Exists(filePath))
{
throw new InvalidOperationException(
$"Please set the TEST_WEB_CLIENT_SECRET_FILENAME environment variable before running tests.");
}
var x = File.ReadAllText(filePath);
return JsonConvert.DeserializeObject<Client>(File.ReadAllText(filePath));
}
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ClientInfo>();
services.AddControllers();
services.AddAuthentication(o =>
{
// This is for challenges to go directly to the Google OpenID Handler, so there's no
// need to add an AccountController that emits challenges for Login.
o.DefaultChallengeScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
// This is for forbids to go directly to the Google OpenID Handler, which checks if
// extra scopes are required and does automatic incremental auth.
o.DefaultForbidScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogleOpenIdConnect(options =>
{
var clientInfo = new ClientInfo(Configuration);
options.ClientId = clientInfo.Client.web.client_id;
options.ClientSecret = clientInfo.Client.web.client_secret;
});
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
Controller with Auth
[ApiController]
[Route("[controller]")]
public class GAAnalyticsController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
public GAAnalyticsController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
// Test showing use of incremental auth.
// This attribute states that the listed scope(s) must be authorized in the handler.
[GoogleScopedAuthorize(AnalyticsReportingService.ScopeConstants.AnalyticsReadonly)]
public async Task<GetReportsResponse> Get([FromServices] IGoogleAuthProvider auth, [FromServices] ClientInfo clientInfo)
{
var GoogleAnalyticsViewId = "78110423";
var cred = await auth.GetCredentialAsync();
var service = new AnalyticsReportingService(new BaseClientService.Initializer
{
HttpClientInitializer = cred
});
var dateRange = new DateRange
{
StartDate = "2015-06-15",
EndDate = "2015-06-30"
};
// Create the Metrics object.
var sessions = new Metric
{
Expression = "ga:sessions",
Alias = "Sessions"
};
//Create the Dimensions object.
var browser = new Dimension
{
Name = "ga:browser"
};
// Create the ReportRequest object.
var reportRequest = new ReportRequest
{
ViewId = GoogleAnalyticsViewId,
DateRanges = new List<DateRange> {dateRange},
Dimensions = new List<Dimension> {browser},
Metrics = new List<Metric> {sessions}
};
var requests = new List<ReportRequest> {reportRequest};
// Create the GetReportsRequest object.
var getReport = new GetReportsRequest {ReportRequests = requests};
// Make the request.
var response = service.Reports.BatchGet(getReport).Execute();
return response;
}
}
You can use Google.Apis.Auth.AspNetCore3 for authenticating in .NET Core 3.1. The Google.Apis.Auth.AspNetCore3.IntegrationTests is a good example (it's just an ASP.NET Core 3 Web Application) of how to use the library and shows all of its features. Feel free to create issues in https://github.com/googleapis/google-api-dotnet-client if you encounter any problems.
I'm trying to access HttpContext to get RemoteIpAddress and User-Agent, but within Startup.cs.
public class Startup
{
public Startup(IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
{
Configuration = configuration;
_httpContextAccessor = httpContextAccessor;
}
public IConfiguration Configuration { get; }
public IHttpContextAccessor _httpContextAccessor { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
IdentityModelEventSource.ShowPII = true;
var key = Encoding.ASCII.GetBytes(Configuration.GetValue<string>("claveEncriptacion"));
var ip = _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString();
var userAgent = _httpContextAccessor.HttpContext.Request.Headers["User-Agent"].ToString();
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x =>
{
//x.Audience = ip + "-" + userAgent;
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = true
};
});
With the previous code I have an error executing the project.
Unable to resolve service for type 'Microsoft.AspNetCore.Http.IHttpContextAccessor' while attempting to activate 'JobSiteMentorCore.Startup'.'
According to the ASP.NET Core documentation , only the following service types can be injected into the Startup constructor when using the Generic Host (IHostBuilder):
IWebHostEnvironment
IHostEnvironment
IConfiguration
So you cannot inject IHttpContextAccessor to Startup constructor.
However you can get DI resolved service in ConfigureServices method of the Startup class as follows:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IYourService, YourService>();
// Build an intermediate service provider
var serviceProvider = services.BuildServiceProvider();
// Resolve the services from the service provider
var yourService = serviceProvider.GetService<IYourService>();
}
But you can not get the HttpContext using IHttpContextAccessor similarly because HttpContext is null unless the code executed during any HttpRequest. So you have to do your desired operation from any custom middleware in Configure method of the Startup class as follows:
public class YourCustomMiddleMiddleware
{
private readonly RequestDelegate _requestDelegate;
public YourCustomMiddleMiddleware(RequestDelegate requestDelegate)
{
_requestDelegate = requestDelegate;
}
public async Task Invoke(HttpContext context)
{
// Your HttpContext related task is in here.
await _requestDelegate(context);
}
}
Then in the Configure method of the Startup class as follows:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMiddleware(typeof(YourCustomMiddleMiddleware));
}
I've finally found a possible solution, using middleware to validate the token.
I created a class ValidationHandler, and this class can use HttpContext.
public class ValidationRequirement : IAuthorizationRequirement
{
public string Issuer { get; }
public string Scope { get; }
public HasScopeRequirement(string scope, string issuer)
{
Scope = scope ?? throw new ArgumentNullException(nameof(scope));
Issuer = issuer ?? throw new ArgumentNullException(nameof(issuer));
}
}
public class ValidationHandler : AuthorizationHandler<ValidationRequirement>
{
IHttpContextAccessor _httpContextAccessor;
public HasScopeHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement)
{
var ip = _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString();
var userAgent = _httpContextAccessor.HttpContext.Request.Headers["User-Agent"].ToString();
//context.Succeed(requirement);
return Task.CompletedTask;
}
}
Finally in the Startup.cs class it is necessary to add the following.
services.AddAuthorization(options =>
{
options.AddPolicy("read:messages", policy => policy.Requirements.Add(new HasScopeRequirement("read:messages", "david")));
});
Thank you vidgarga. I would upvote but I have a new account. Your answer helped me on my project. I'm coming from f# land so I am including my implementation for any fsharpers that need this solution.
type HasScopeHandler() =
inherit AuthorizationHandler<HasScopeRequirement>()
override __.HandleRequirementAsync(context, requirement) =
let scopeClaimFromIssuer = Predicate<Claim>(fun (c: Claim) -> c.Type = "scope" && c.Issuer = requirement.Issuer)
let userDoesNotHaveScopeClaim = not (context.User.HasClaim(scopeClaimFromIssuer))
let isRequiredScope s = (s = requirement.Scope)
let claimOrNull = context.User.FindFirst(scopeClaimFromIssuer)
if (userDoesNotHaveScopeClaim) then
Task.CompletedTask
else
match claimOrNull with
| null -> Task.CompletedTask
| claim ->
let scopes = claim.Value.Split(' ')
let hasRequiredScope = scopes.Any(fun s -> isRequiredScope s)
if (hasRequiredScope) then
context.Succeed(requirement)
Task.CompletedTask
else
Task.CompletedTask
So i've been trying to set up a small test api with token authentication in the form of JWT distribution, the token distribution part worked as intended.
However as i wanted to make the methods for my JWT service more generic to allow for different kinds of signing the tokens(since i would prefer a private/public keypair), i tried to set up some more options within the appsettings files which would then dictate the way that tokens would be generated, i began loading in those settings with dependency injection which i've barely touched up until now.
So the problem came when i wanted to take those configuration classes that i set up as singletons(most of the guides i've read through up until now have done this, so i assume it's somewhat correct) and use them within the ConfigureServices method in which they added, so that i could use the parameters which, in my mind should have been set since i configured a few lines above by getting a section of the appsettings file.
However once i try and access them, i don't get anything back, and am instead left with empty values.
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//the token config takes values from the appsettings.json file
var tokenConf = Configuration.GetSection("TokenConfiguration");
services.Configure<TokenConfiguration>(tokenConf);
//the signing credentials are assigned in the JwtTokenService constructor
var signingConf = new SigningConfiguration();
services.AddSingleton<SigningConfiguration>(signingConf);
//my token service
services.AddSingleton<IJwtTokenService, JwtTokenService>();
//i try to get hold of the actual values to use later on
var provider = services.BuildServiceProvider();
TokenConfiguration tc = provider.GetService<TokenConfiguration>();
SigningConfiguration sc = provider.GetService<SigningConfiguration>();
//i wanna use the values in here when i set the parameters for my authentication
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x =>
{
x.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
return Task.CompletedTask;
}
};
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
//values used here since i specify issuer, audience and what kind of key to use in the settings
//the key & credentials differ based on a bool in the settings file and will either be a symmetric or asymmetric key
ValidIssuer = tc.Issuer,
ValidAudience = tc.Audience,
IssuerSigningKey = sc.Key
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc();
}
}
JwtTokenService.cs (IJwtTokenService only has the CreateToken method which is implemented here)
public class JwtTokenService : IJwtTokenService
{
private TokenConfiguration tokenConf;
public SigningConfiguration signingConf;
public JwtTokenService(IOptions<TokenConfiguration> tc) {
tokenConf = tc.Value;
signingConf = new SigningConfiguration();
//if the asymmetric bool is set to true, assign a new rsa keypair to the signing configuration
//otherwise, use a symmetric key with a hmac hash
if (tc.Value.AsymmetricKey)
{
using (var provider = new RSACryptoServiceProvider(2048))
{
signingConf.Key = new RsaSecurityKey(provider.ExportParameters(true));
}
signingConf.SigningCredentials =
new SigningCredentials(
signingConf.Key,
SecurityAlgorithms.RsaSha256);
}
else {
signingConf.Key =
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(tc.Value.HmacSecret));
signingConf.SigningCredentials =
new SigningCredentials(
signingConf.Key,
SecurityAlgorithms.HmacSha512);
}
}
/// <summary>
/// Creates a token based on the running configuration
/// </summary>
public string CreateToken(List<Claim> claims)
{
var token = new JwtSecurityToken(
issuer: tokenConf.Issuer,
audience: tokenConf.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(tokenConf.Minutes),
signingCredentials: signingConf.SigningCredentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
TokenConfiguration.cs
public class TokenConfiguration
{
public string Audience { get; set; }
public string Issuer { get; set; }
public int Minutes { get; set; }
public bool AsymmetricKey { get; set; }
public string HmacSecret { get; set; }
}
SigningConfiguration.cs
public class SigningConfiguration
{
public SecurityKey Key { get; set; }
public SigningCredentials SigningCredentials { get; set; }
}
appsettings.json
"TokenConfiguration": {
"Audience": "ExampleAudience",
"Issuer": "ExampleIssuer",
"Minutes": 30,
"AsymmetricKey": true,
"HmacSecret": "example-secret-top-secret-secret-is_secret"
}
(the project is running in asp.net core 2.1 in case that matters)
I am new to DI and can't find a lot of examples where the usecase is the same as mine, and most such cases involve actual services rather than adding a 'configuration' class through DI.
There's probably a way better method of doing this, and i'm probably just stupid for not noticing or not knowing what exact thing to google to get a proper answer for it, will most likely be watching/reading a bit about DI after this regardless.
Any input or thoughts would be highly appreciated, as i'm still new to asp.net core and the whole flow of things.
As a small side question regarding the private key generation, in my case, would it be best to store the generated keypair in the keystore, or to store it in memory, or would generating them through something like openSSL and reading from them at startup be the best option?
You are requesting TokenConfiguration instead of IOptions<TokenConfiguration> from ServiceProvider.
Change this lines
TokenConfiguration tc = provider.GetService<TokenConfiguration>();
SigningConfiguration sc = provider.GetService<SigningConfiguration>();
with
IOptions<TokenConfiguration> tc = provider.GetService<IOptions<TokenConfiguration>>();
IOptions<SigningConfiguration> sc = provider.GetService<IOptions<SigningConfiguration>>();
And then access options with tc.Value.
Building ServiceProvider in ConfigureServices is not a good idea i would take directly from config by Configuration["TokenConfiguration:Audience"] wherever i need in ConfigureServices.
I have a custom policy based authorization handler as defined below. Authentication is handled before the user hit this application so I only need authorization. I am getting the error:
No authenticationScheme was specified, and there was no DefaultForbidScheme.
If the authorization check succeeds then I do not get the error and all is well. This error only happens when the authorization check fails. I would expect that a 401 is returned on failure.
public class EasRequirement : IAuthorizationRequirement
{
public EasRequirement(string easBaseAddress, string applicationName, bool bypassAuthorization)
{
_client = GetConfiguredClient(easBaseAddress);
_applicationName = applicationName;
_bypassAuthorization = bypassAuthorization;
}
public async Task<bool> IsAuthorized(ActionContext actionContext)
{
...
}
}
public class EasHandler : AuthorizationHandler<EasRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, EasRequirement requirement)
{
var mvcContext = context.Resource as ActionContext;
bool isAuthorized;
try
{
isAuthorized = requirement.IsAuthorized(mvcContext).Result;
}
catch (Exception)
{
// TODO: log the error?
isAuthorized = false;
}
if (isAuthorized)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
context.Fail();
return Task.FromResult(0);
}
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
var easBaseAddress = Configuration.GetSection("EasBaseAddress").Value;
var applicationName = Configuration.GetSection("ApplicationName").Value;
var bypassAuthorization = bool.Parse(Configuration.GetSection("BypassEasAuthorization").Value);
var policy = new AuthorizationPolicyBuilder()
.AddRequirements(new EasRequirement(easBaseAddress, applicationName, bypassAuthorization))
.Build();
services.AddAuthorization(options =>
{
options.AddPolicy("EAS", policy);
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSingleton<IAuthorizationHandler, EasHandler>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseMvc();
}
}
Authorization and authentication are closely linked in ASP.NET Core. When authorization fails, this will be passed to an authentication handler to handle the authorization failure.
So even if you don’t need actual authentication to identify your users, you will still need to set up some authentication scheme that is able to handle forbid and challenge results (403 and 401).
To do that, you need to call AddAuthentication() and configure a default forbid/challenge scheme:
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "scheme name";
// you can also skip this to make the challenge scheme handle the forbid as well
options.DefaultForbidScheme = "scheme name";
// of course you also need to register that scheme, e.g. using
options.AddScheme<MySchemeHandler>("scheme name", "scheme display name");
});
MySchemeHandler needs to implement IAuthenticationHandler and in your case, you especially need to implement ChallengeAsync and ForbidAsync:
public class MySchemeHandler : IAuthenticationHandler
{
private HttpContext _context;
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
_context = context;
return Task.CompletedTask;
}
public Task<AuthenticateResult> AuthenticateAsync()
=> Task.FromResult(AuthenticateResult.NoResult());
public Task ChallengeAsync(AuthenticationProperties properties)
{
// do something
}
public Task ForbidAsync(AuthenticationProperties properties)
{
// do something
}
}
For IIS/IIS Express, can just add this line instead of all of the above in the accepted answer to get the appropriate 403 response you're expecting;
services.AddAuthentication(IISDefaults.AuthenticationScheme);