Using file extension content negotiation with spring security plugin? - spring

I'm trying to use the latest spring security plugin for grails, but I've hit a little bump.
I have a controller with this method:
#Secured(['ROLE_USER'])
def query = {
}
When I hit http://localhost:8080/myApp/myController/query, I get prompted for authorization as appropriate. However, I need to do content type negotiation via the filename extension. Using
grails.mime.file.extensions=true
I can use the same UrlMappings and get to my controller method via .../myApp/myController/query.js?params=blah. However, I am not prompted for authentication, and either the request goes through automatically or fails, depending on how I've set grails.plugins.springsecurity.rejectIfNoRule
How can I use file type negotiation with the spring security plugin?

Turn off grails.mime.file.extensions and add this filter:
class FileExtensionContentNegotiationFilters {
final static String DEFAULT_FORMAT = "js"
def filters = {
all(controller: '*', action: '*') {
before = {
addFormatToRequestByFileExtension(request)
}
after = {
}
afterView = {
}
}
}
protected addFormatToRequestByFileExtension(def request) {
String suffix = getSuffixFromPath(request.forwardURI)
String extension = FilenameUtils.getExtension(suffix)
if (extension.isEmpty()) {
request[GrailsApplicationAttributes.CONTENT_FORMAT] = DEFAULT_FORMAT
}
else {
request[GrailsApplicationAttributes.CONTENT_FORMAT] = extension
}
}
protected String getSuffixFromPath(String pathWithoutParams) {
int lastSlash = pathWithoutParams.lastIndexOf("/")
if (lastSlash < 0) {
return ""
}
return pathWithoutParams.substring(lastSlash + 1)
}
}

The solution above does not work for me as expected. A 404 response is generated when I request an URL with extension.
I come with another solution that does not need to turn off grails.mime.file.extensions and does not need an extra Filter. Instead, it's a modification of the plugin's class org.codehaus.groovy.grails.plugins.springsecurity.AnnotationFilterInvocationDefinition. What you need to do is to edit the method determineUrl as shown below (look at the comments to identify the changes):
#Override
protected String determineUrl(final FilterInvocation filterInvocation) {
HttpServletRequest request = filterInvocation.getHttpRequest();
HttpServletResponse response = filterInvocation.getHttpResponse();
GrailsWebRequest existingRequest = WebUtils.retrieveGrailsWebRequest();
String requestUrl = request.getRequestURI().substring(request.getContextPath().length());
/** The following 2 lines were added */
int indexOfPeriod = requestUrl.indexOf('.');
String requestUrlForMatching = (indexOfPeriod != -1) ? requestUrl.substring(0, indexOfPeriod) : requestUrl;
String url = null;
try {
GrailsWebRequest grailsRequest = new GrailsWebRequest(request, response,
ServletContextHolder.getServletContext());
WebUtils.storeGrailsWebRequest(grailsRequest);
Map<String, Object> savedParams = copyParams(grailsRequest);
/* Use requestUrlForMatching instead of requestUrl */
for (UrlMappingInfo mapping : _urlMappingsHolder.matchAll(requestUrlForMatching)) {
configureMapping(mapping, grailsRequest, savedParams);
url = findGrailsUrl(mapping);
if (url != null) {
break;
}
}
}
finally {
if (existingRequest == null) {
WebUtils.clearGrailsWebRequest();
}
else {
WebUtils.storeGrailsWebRequest(existingRequest);
}
}
if (!StringUtils.hasLength(url)) {
// probably css/js/image
url = requestUrl;
}
return lowercaseAndStripQuerystring(url);
}
The problem is that URLs with extensions do not match any URL in UrlMappings using UrlMappingsHolder.matchAll method. So, the solution is to ommit the extension prior to look for matches. With this changes everything works as expected.
I also create a pull request with the fix available at https://github.com/grails-plugins/grails-spring-security-core/pull/24
You can see the changes at https://github.com/arcesino/grails-spring-security-core/commit/19f87168ec4422b4fe06cc6914adeb1bae4b8752
Tested with version 1.2.7.3

Related

AllowAnonymous attribute is not working in .net core api 2.2. Please consider (.NetCore) and version(2.2) before suggesting duplicate or answer

I am working in .net core api 2.2 where I am using authorization filter. I am just checking bearer token in authorization tag in header and if bearer token is already there then user action can be called. But some action I wanna exclude from authorization part. I am using AllAnonymous attribute on specific action but calling on same anonymous method the authorization filter is being called. The code of filter is given below :
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
public class ApiAuthorizeFilter : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
string token = context.HttpContext.GetToken();
if (string.IsNullOrEmpty(token))
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
else
{
string realmId = context.HttpContext.GetRealm();
if (string.IsNullOrEmpty(realmId))
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
}
}
}
As per I checked the some solution per this is not getting resolved. Please share any solution regarding .net core API version 2.2 .
You can check the AllowAnonymous attribute inside OnAuthorization method :
// Allow Anonymous skips all authorization
if (context.Filters.Any(item => item is IAllowAnonymousFilter))
{
return;
}
Base on your codes :
public class ApiAuthorizeFilter : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
// Allow Anonymous skips all authorization
if (context.Filters.Any(item => item is IAllowAnonymousFilter))
{
return;
}
string token = context.HttpContext.GetToken();
if (string.IsNullOrEmpty(token))
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
else
{
string realmId = context.HttpContext.GetRealm();
if (string.IsNullOrEmpty(realmId))
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
}
}
}
When doing endpoint routing, MVC does not add AllowAnonymousFilters for AllowAnonymousAttributes that
were discovered on controllers and actions. To maintain compat with 2.x,
we'll check for the presence of IAllowAnonymous in endpoint metadata.
var endpoint = context.HttpContext.GetEndpoint();
if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
{
return true;
}

Sending new parameters in MvxViewModelRequest from a IMvxNavigationFacade when deeplinking

I am using deeplinking in my app and Im looking to preset some parameters when navigating to the viewmodel using a IMvxNavigationFacade. The deep link url is like this:
myappname://deeplink/toviewwithdata/?navigatetoview=viewtype1&id=78910
So the deep linking is working and im getting to the navigation facade using the assembly attribute
[assembly: MvxNavigation(typeof(RoutingFacade), #"myappname://deeplink/toviewwithdata/\?navigatetoview=(?<viewtype>viewtype1)&id=(?<id>\d{5})")]
I tried to add other parameters to the MvxViewModelRequest using a MvxBundle but dont think im doing it right. here is my navigation facade:
public class RoutingFacade : IMvxNavigationFacade
{
public Task<MvxViewModelRequest> BuildViewModelRequest(string url, IDictionary<string, string> currentParameters)
{
var viewModelType = typeof(FirstViewModel);
var parameters = new MvxBundle();
try
{
// TODO: Update this to handle different view types and add error handling
if (currentParameters != null)
{
Debug.WriteLine($"RoutingFacade - {currentParameters["viewtype"]}, {currentParameters["id"]}");
switch (currentParameters["viewtype"])
{
case "viewtype1":
viewModelType = typeof(FirstViewModel);
parameters.Data.Add("test", "somevalue");
break;
default:
case "viewtype2":
viewModelType = typeof(FirstViewModel);
break;
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"RoutingFacade - Exception: {ex.Message}");
//TODO viewModelType = typeof(ErrorViewModel);
}
return Task.FromResult(new MvxViewModelRequest(viewModelType, parameters, null));
}
then my viewmodel Init method
public void Init(string id, string viewtype, string test)
{
// Do stuff with parameters
}
but the test parameter is null? How do you pass parameters into a MvxViewModelRequest?
Update:
Don’t know if its possible from looking at the source here https://github.com/MvvmCross/MvvmCross/blob/f4b2a7241054ac288a391c4c7b7a7342852e1e19/MvvmCross/Core/Core/Navigation/MvxNavigationService.cs#L122 as the request parameters get set from the regex of the deeplink url and the return from BuildViewModelRequest, facadeRequest.parameterValues get ignored.
Added this functionality in this pull request

Custom Async Action Filter for Web API 2

I have a web api to consume the data coming from android mobile. This web api will consume the multi part file from along with the form data the web api request. I followed this article to archive.
[CustAuthAsync]
public async Task<HttpResponseMessage> SaveEHSInspectionData()
{
try
{
string root = HttpContext.Current.Server.MapPath("~/App_Data");
MultipartFormDataStreamProvider provider = new MultipartFormDataStreamProvider(root);
//do stuff
var res = await Request.Content.ReadAsMultipartAsync(provider);
// DO SOME STUFF
}
catch (Exception exp)
{
}
return Request.CreateResponse(HttpStatusCode.OK, result);
}
I wanted to do the custom access validation for this web api, so implemented a filter to validate the request.
I have the filter like below
public class CustAuthAsyncAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
InternalOnExecutingAsync(actionContext);
}
}
The internal method like this
protected void InternalOnExecutingAsync(HttpActionContext actionContext)
{
var authValue = actionContext.Request.Headers;
if (authValue.Contains("CustomAccessToken"))
{
string token = authValue.GetValues("CustomAccessToken").First();
var result = // doing some decription
if (result != null)
{
bool validationResult = // validation with database
if (!validationResult)
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized)
{ ReasonPhrase = "Invalid token" };
}
}
else
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized)
{ ReasonPhrase = "Invalid token" };
}
}
else
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized)
{ ReasonPhrase = "Unauthorized Request" };
}
These implementations are working fine in API Client Tools (Example: Postman) if the validation passes, allows the request to the method.
Postman Response screen shot
This is not working in mobile app, Saying the response message as Unauthorized Access. and not allowing the request to the method even the custom access validations are passed.
FYI : This method is working fine in mobile without filter
Help me to get this works in mobile app also.
Thanks in advance.
Your using the wrong type of filter to manage access. You should use an authorization filter. Besides you can't have an async method to authorize. You have to make the calling client wait for clearance. This may cause the side effects you're experiencing.
I'm not sure this has any to do with fact that it's a mobile application, however the authorization phase ir prior to the processing of the request. Verify that your are not using any other form of authorization in your project.
You should implement an authorization filter by inheriting AuthorizeAttribute and overriding IsAuthorized(HttpActionContext actionContext) method:
public class CustAuthAsync : AuthorizeAttribute
{
public CustAuthAsync()
{
///Some initialization if required. Otherwise, not necessary to declare the constructor..
}
protected override bool IsAuthorized(HttpActionContext actionContext)
{
var authValue = actionContext.Request.Headers;
if (authValue.Contains("CustomAccessToken"))
{
string token = authValue.GetValues("CustomAccessToken").First();
var result = // doing some decription
if (result != null)
{
return //database validation
}
else
{
return false;
//No need to create special unauthorized response. You should not hint the reason at this point. You can do this in the HandleUnauthorizedRequest method.
}
}
else
{
return false;//No need to create special unauthorized response.
}
}
}
You can use this attribute to decorate your controllers. You can even pass parameter in the constructor for more granular control on access management, like a required role to access de controller.

OData V4 modify $filter on server side

I would like to be able to modify the filter inside the controller and then return the data based on the altered filter.
So for I have an ODataQueryOptions parameter on the server side that I can use to look at the FilterQueryOption.
Let's assume the filter is something like this "$filter=ID eq -1" but on the server side if I see "-1" for an ID this tells me that the user wants to select all records.
I tried to change the "$filter=ID eq -1" to "$filter=ID ne -1" which would give me all by setting the Filter.RawValue but this is read only.
I tried to create a new FilterQueryOption but this requires a ODataQueryContext and a ODataQueryOptionParser which I can't figure out how to create.
I then tried to set the Filter = Null and then us the ApplyTo which seems to work when I set a break point in the controller and check this on the immediate window but once it leaves the GET method on the controller then it "reverts" back to what was passed in the URL.
This article talks about doing something very similar "The best way to modify a WebAPI OData QueryOptions.Filter" but once it leaves the controller GET method then it reverts back to the URL query filter.
UPDATE WITH SAMPLE CODE
[EnableQuery]
[HttpGet]
public IQueryable<Product> GetProducts(ODataQueryOptions<Product> queryOptions)
{
if (queryOptions.Filter != null)
{
var url = queryOptions.Request.RequestUri.AbsoluteUri;
string filter = queryOptions.Filter.RawValue;
url = url.Replace("$filter=ID%20eq%201", "$filter=ID%20eq%202");
var req = new HttpRequestMessage(HttpMethod.Get, url);
queryOptions = new ODataQueryOptions<Product>(queryOptions.Context, req);
}
IQueryable query = queryOptions.ApplyTo(db.Products.AsQueryable());
return query as IQueryable<Product>;
}
Running this code will not return any product this is because the original query in the URL wanted product 1 and I swapped the ID filter of product 1 with product 2.
Now if I run SQL Profiler, I can see that it added something like "Select * from Product WHERE ID = 1 AND ID = 2".
BUT if I try the same thing by replacing the $top then it works fine.
[EnableQuery]
[HttpGet]
public IQueryable<Product> GetProducts(ODataQueryOptions<Product> queryOptions)
{
if (queryOptions.Top != null)
{
var url = queryOptions.Request.RequestUri.AbsoluteUri;
string filter = queryOptions.Top.RawValue;
url = url.Replace("$top=2", "$top=1");
var req = new HttpRequestMessage(HttpMethod.Get, url);
queryOptions = new ODataQueryOptions<Product>(queryOptions.Context, req);
}
IQueryable query = queryOptions.ApplyTo(db.Products.AsQueryable());
return query as IQueryable<Product>;
}
END RESULT
With Microsoft's help. Here is the final output that supports filter, count, and paging.
using System.Net.Http;
using System.Web.OData;
using System.Web.OData.Extensions;
using System.Web.OData.Query;
/// <summary>
/// Used to create custom filters, selects, groupings, ordering, etc...
/// </summary>
public class CustomEnableQueryAttribute : EnableQueryAttribute
{
public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
{
IQueryable result = default(IQueryable);
// get the original request before the alterations
HttpRequestMessage originalRequest = queryOptions.Request;
// get the original URL before the alterations
string url = originalRequest.RequestUri.AbsoluteUri;
// rebuild the URL if it contains a specific filter for "ID = 0" to select all records
if (queryOptions.Filter != null && url.Contains("$filter=ID%20eq%200"))
{
// apply the new filter
url = url.Replace("$filter=ID%20eq%200", "$filter=ID%20ne%200");
// build a new request for the filter
HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Get, url);
// reset the query options with the new request
queryOptions = new ODataQueryOptions(queryOptions.Context, req);
}
// set a top filter if one was not supplied
if (queryOptions.Top == null)
{
// apply the query options with the new top filter
result = queryOptions.ApplyTo(queryable, new ODataQuerySettings { PageSize = 100 });
}
else
{
// apply any pending information that was not previously applied
result = queryOptions.ApplyTo(queryable);
}
// add the NextLink if one exists
if (queryOptions.Request.ODataProperties().NextLink != null)
{
originalRequest.ODataProperties().NextLink = queryOptions.Request.ODataProperties().NextLink;
}
// add the TotalCount if one exists
if (queryOptions.Request.ODataProperties().TotalCount != null)
{
originalRequest.ODataProperties().TotalCount = queryOptions.Request.ODataProperties().TotalCount;
}
// return all results
return result;
}
}
Remove [EnableQuery] attribute, your scenario should work, because after using this attribute, OData/WebApi will apply your original query option after you return data in controller, if you already apply in your controller method, then you shouldn't use that attribute.
But if your query option contains $select, those code are not working because the result's type is not Product, we use a wrapper to represent the result of $select, so I suggest you use try this:
Make a customized EnableQueryAttribute
public class MyEnableQueryAttribute : EnableQueryAttribute
{
public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
{
if (queryOptions.Filter != null)
{
queryOptions.ApplyTo(queryable);
var url = queryOptions.Request.RequestUri.AbsoluteUri;
url = url.Replace("$filter=Id%20eq%201", "$filter=Id%20eq%202");
var req = new HttpRequestMessage(HttpMethod.Get, url);
queryOptions = new ODataQueryOptions(queryOptions.Context, req);
}
return queryOptions.ApplyTo(queryable);
}
}
Use this attribute in your controller method
[MyEnableQueryAttribute]
public IHttpActionResult Get()
{
return Ok(_products);
}
Hope this can solve your problem, thanks!
Fan.
In response of #Chris Schaller I post my own solution as below:
public class CustomEnableQueryAttribute : EnableQueryAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var url = actionContext.Request.RequestUri.OriginalString;
//change something in original url,
//for example change all A charaters to B charaters,
//consider decoding url using WebUtility.UrlDecode() if necessary
var newUrl = ModifyUrl(url);
actionContext.Request.RequestUri = new Uri(newUrl);
base.OnActionExecuting(actionContext);
}
}

ASP.NET Web API Help Pages and Versioning

I would like to create a separate help page for each version of my API. For example, the user could go to /help?v=1 to see version 1.0 routes and /help?v=2 to see version 2.0 routes.
Using SDammann.WebApi.Versioning, I added a Version property to VersionedApiExplorer that will return only the routes for the defined version and added the version as an argument to the constructor. Then I tried this:
config.Services.Add(typeof(IApiExplorer), new VersionedApiExplorer(config, "1"));
config.Services.Add(typeof(IApiExplorer), new VersionedApiExplorer(config, "2"));
But this gives me the following error:
The service type IApiExplorer is not supported.
Parameter name: serviceType
I added just one instance of the service - config.Services.Replace(typeof(IApiExplorer), new VersionedApiExplorer(GlobalConfiguration.Configuration, "1")); - to get the configuration to work, so I could test my help controller. Then tried this:
foreach (var service in Configuration.Services.GetServices(typeof(IApiExplorer))) {
if (service.GetType() != typeof(VersionedApiExplorer)) continue;
var explorer = service as VersionedApiExplorer;
if (explorer.Version == v) {
apiExplorer = explorer;
}
}
This gives the same error I received above. I know I would normally use this.Configuration.Services.GetApiExplorer() but I don't know how I could use that to get the appropriate instance of VersionedApiExplorer. I know I could instantiate the appropriate ApiExplorer directly in the controller, but I would prefer to keep that in my configuration file if possible.
So I have two questions:
How could I add two services of type VersionedApiExplorer to my config object?
How would I retrieve the appropriate service in my help controller?
Or is there a completely different approach I could take to accomplish the same goal?
Thank you!
I ultimately ended up going with the solution I hinted at in my question. I feel like there's a better solution to this problem, but this gets the job done.
First, I added a Version property to VersionedApiExplorer:
public string Version { get; private set; }
Then I modified InitializeApiDescriptions to look like this:
private Collection<ApiDescription> InitializeApiDescriptions()
{
Collection<ApiDescription> apiDescriptions = new Collection<ApiDescription>();
var controllerSelector = configuration.Services.GetHttpControllerSelector();
IDictionary<string, HttpControllerDescriptor> allControllerMappings = controllerSelector.GetControllerMapping();
IDictionary<string, HttpControllerDescriptor> controllerMappings = new Dictionary<string, HttpControllerDescriptor>();
// get only mappings for defined version
if (allControllerMappings != null && Version != null) {
foreach (var key in allControllerMappings.Keys) {
if (key.Substring(0, key.IndexOf('.')) == VersionedControllerSelector.VersionPrefix + Version) {
controllerMappings.Add(key, allControllerMappings[key]);
}
}
}
else if (Version == null) {
controllerMappings = allControllerMappings;
}
if (controllerMappings != null)
{
foreach (var route in configuration.Routes)
ExploreRouteControllers(controllerMappings, route, apiDescriptions);
}
return apiDescriptions;
}
I also added a method I could use to set the Version:
public void SetVersion(string version) {
this.Version = version;
this.apiDescription = new Lazy<Collection<ApiDescription>>(InitializeApiDescriptions);
}
Finally, I modified my HelpController to looks like this:
public ActionResult Index(string v) {
return this.View(GetApiExplorer(v).ApiDescriptions);
}
private IApiExplorer GetApiExplorer(string version) {
if (version == null) {
version = "1";
}
var apiExplorer = this.Configuration.Services.GetApiExplorer() as VersionedApiExplorer;
if (apiExplorer != null) {
apiExplorer.SetVersion(version);
}
return apiExplorer;
}

Resources