Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authorization example #1300

Closed
verdie-g opened this issue Aug 20, 2023 · 10 comments · Fixed by #1303
Closed

Authorization example #1300

verdie-g opened this issue Aug 20, 2023 · 10 comments · Fixed by #1303
Labels

Comments

@verdie-g
Copy link
Sponsor Contributor

Is your feature request related to a problem? Please describe.

I would like to add authorization to my JSON:API, especially scope-based authorization. For example the user needs to have the user.read scope to read user information.

Describe the solution you'd like

It's probably already doable but I'm looking for the idiomatic way to do that and since it's a very common use-case, I think it would be great to add that to the documentation.

@bkoelman
Copy link
Member

If you want to add [Authorize] at the class level, use a partial class. If you want it at the indiviual action methods, define your own controllers that inherit from our base class and turn off auto-generated controllers. It's all described at https://www.jsonapi.net/usage/extensibility/controllers.html. There are so many different ways to do authorization, but those are ASP.NET features that JsonApiDotNetCore is agnostic about. We don't intend to teach users how ASP.NET works. And this is a question, not a feature request.

@verdie-g
Copy link
Sponsor Contributor Author

It's not clear from the documentation what it the best way to achieve that. Should I copy the BaseJsonApiController for each one of my resource to add the Authorize attribute on each endpoint? I'm thinking that it would extremely useful to control the Authorize attribute on the generated controllers.

@bkoelman
Copy link
Member

Our documentation assumes basic knowledge of C#, ASP.NET and EF Core. We favor covering just the basics to keep it concise and maintainable. And we assume that users are willing to experiment, look at the samples and integration tests, search the history of GitHub issues and the Gitter chat, google for common solutions etc. How to inherit from a base class is not something you should learn here. See also the issue template for asking a question. To get a satisfying answer, you'll need to put in some more effort upfront.

How do you propose to implement adding [Authorize] in the framework? Note that generated controllers are merely a convenience to quickly get something running. The feature was added recently and there are many cases where adding your own controllers is the better solution for full control.

@verdie-g
Copy link
Sponsor Contributor Author

Note that generated controllers are merely a convenience to quickly get something running

That's probably the piece I was missing. I thought the generated controllers were the idiomatic way to go.

@verdie-g
Copy link
Sponsor Contributor Author

For future readers you can follow the section earlier versions and create such controller:

public class UsersController : JsonApiController<User, long>
{
    public UsersController(
        IJsonApiOptions options,
        IResourceGraph resourceGraph,
        ILoggerFactory loggerFactory,
        IResourceService<User, long> resourceService)
        : base(options, resourceGraph, loggerFactory, resourceService)
    {
    }

    [HttpGet("{id}")]
    [HttpHead("{id}")]
    [Authorize(AuthenticationSchemes = "XXX")]
    public override Task<IActionResult> GetAsync(long id, CancellationToken cancellationToken)
    {
        return base.GetAsync(id, cancellationToken);
    }
}

This is only a partial answer because

  1. You can bypass the authorization by using a potential "secondary get" endpoint or an include
  2. The 401 and 403 responses won't return a JSON:API object

@bkoelman
Copy link
Member

To overcome these, you can inject IEnumerable<IQueryConstraintProvider> to inspect the query string parameters and throw JsonApiException, based on HttpContext.User. In addition to includes, you may want to check filters such as /blogs?filter=has(posts) and /blogs?filter[posts]=contains(title,'ASP.NET'). QueryExpressionRewriter may come in handy to walk the tree.

@verdie-g
Copy link
Sponsor Contributor Author

I was able to implement a basic scope-based authorization middleware using IJsonApiRequest

internal class JsonApiAuthorizationMiddleware
{
    private readonly RequestDelegate _next;

    public JsonApiAuthorizationMiddleware(RequestDelegate next)
    {
        _next = next ?? throw new ArgumentNullException(nameof(next));
    }

    public Task Invoke(HttpContext context, IJsonApiRequest jsonApiRequest)
    {
        var resourceType = jsonApiRequest.SecondaryResourceType ?? jsonApiRequest.PrimaryResourceType;
        if (resourceType != null)
        {
            string expectedScope = (jsonApiRequest.IsReadOnly ? "read" : "write") + ':' + resourceType;

            string? scopeClaim = context.User.GetClaim("scope");
            if (scopeClaim == null || !Array.Exists(scopeClaim.Split(','), s => s == expectedScope))
            {
                throw new JsonApiException(new ErrorObject(HttpStatusCode.Forbidden)
                {
                    // ...
                });
            }
        }

        return _next(context);
    }
}

Several limitations:

  1. It doesn't support includes. I'm not sure how to use IEnumerable<IQueryConstraintProvider>
  2. JsonApiException thrown here don't seem to be caught by the JsonApi middleware so it should write the object in the HTTP response instead
  3. As mentioned in the last comment you can probably leak information of forbidden resources using filters

@bkoelman
Copy link
Member

There's a sample usage of constraint providers at https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/Examples/NoEntityFrameworkExample/Services/InMemoryResourceService.cs#L83. But it gets populated from an MVC action filter, which executes after any middleware. It would be more appropriate to run these checks at a deeper level. If it runs from a resource service, it works for atomic:operations as well. Alternatively, an optimization would be to verify all scopes upfront in an atomic:operations request, similar to how ModelState validation runs. See https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs.

@bkoelman
Copy link
Member

bkoelman commented Sep 3, 2023

@verdie-g I've added an example in #1303. Hope this helps.

@verdie-g
Copy link
Sponsor Contributor Author

verdie-g commented Sep 3, 2023

Very nice, thanks! I think you can close the issue once that's merged, it gives a good idea of how authorization could be implemented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Development

Successfully merging a pull request may close this issue.

2 participants