RicoSuter / NSwag

The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript.
http://NSwag.org
MIT License
6.8k stars 1.3k forks source link

How to generate AuthorizeAttribute from OpenAPI3? #2478

Open MartinKuhne opened 5 years ago

MartinKuhne commented 5 years ago

I have an openAPI3 document as per https://swagger.io/docs/specification/authentication/

openapi: 3.0.0
# Added by API Auto Mocking Plugin
info:
  version: "1.0.0"
  title: ...
security:
 - OAuth2: [read]
...

  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://example.com/oauth/authorize
          tokenUrl: https://example.com/oauth/token
          scopes:
            read: Grants read access
            write: Grants write access
            admin: Grants access to admin operations

.csproj:

  <Target Name="GenerateServiceClientFromOpenApiDocument" BeforeTargets="BeforeBuild">
    <!-- Docs @ https://github.com/RicoSuter/NSwag/wiki/CommandLine#client-generators -->
    <Exec Command="$(NSwagExe_Core22) openapi2cscontroller /input:$(MSBuildProjectDirectory)\OpenApi.yaml /classname:MYController /namespace:web /usebaseurl:false /output:Controllers\Controller.cs /AspNetNamespace:Microsoft.AspNetCore.Mvc /ControllerBaseClass:Microsoft.AspNetCore.Mvc.Controller" />
    <Exec Command="$(NSwagExe_Core22) swagger2csclient /input:$(MSBuildProjectDirectory)\OpenApi.yaml /classname:client /namespace:web.client /usebaseurl:false /output:Client.cs" />
  </Target>

Question: How can I get the [Authorize] attribute generated on the generated controller?

RicoSuter commented 4 years ago

For now you probably need to override the controller template and add this manually.

martin-hirsch commented 2 years ago

Or use "Controller Base Class Name" to extend it. Thankfully it is partial. <3

davidkeaveny commented 1 year ago

Is that going to work when the [Authorize] attribute needs to be applied to the action and not just the controller, since the action methods generated by NSwag currently are not partial. The controller is partial, so you can create a partial class file for the controller and decorate that with whatever authentication process you want; but it won't work if, for instance, you have a read scope on your GET endpoints and a write scope on your POST endpoints.

davidkeaveny commented 1 year ago

For now you probably need to override the controller template and add this manually.

So I should look at, for instance, creating a custom Controller.Methods.Annotations.liquid template, since that already seems to be wired up in Controller.liquid? How do I then access the security sections that have been defined for the endpoint in the OpenAPI specification?

davidkeaveny commented 1 year ago

I've got as far as this for my template:

{% if operation.RequiresAuthentication -%}
{% for requirement in operation.Security -%}
[Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = "{{ requirement.Key}}", Roles = "{{ requirement.Values | join: "," }}")]
{% endfor -%}
{% endif -%}

which gets rendered as:

[Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = "", Roles = "")]
[Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = "", Roles = "")]

So it seems Liquid doesn't know how to handle rendering dictionaries (or more correctly, doesn't know how to handle KeyValuePair<string, string>). Does this mean that I need to modify CSharpOperationModel to expose something that Liquid can handle?

davidkeaveny commented 1 year ago

I eventually worked out the solution to my problem. The working template for Controller.Method.Annotations.liquid in my custom templates folder is now as follows:

{% if operation.RequiresAuthentication -%}
{% for security in operation.Security -%}
{% for requirement in security -%}
[Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = "{{ requirement[0] }}", Roles = "{{ requirement[1] | join: "," }}")]
{% endfor -%}
{% endfor -%}
{% endif -%}

which in my case, will result in an output like this:

[Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = "Bearer", Roles = "Employee:Write")]
[Microsoft.AspNetCore.Mvc.HttpPost, Microsoft.AspNetCore.Mvc.Route("employees/{employeeId}/payroll-details", Name = "Employees_PostPayroll")]
public System.Threading.Tasks.Task PostPayroll([Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired] System.Guid employeeId, [Microsoft.AspNetCore.Mvc.FromBody] [Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired] CreateEmployeePayrollDetailCommand body, System.Threading.CancellationToken cancellationToken)
{
  // etc
}

which is a thing of beauty in my eyes :-)

davidkeaveny commented 1 year ago

@MartinKuhne did you ever get this working to your satisfaction?