[Solved] Api controller backoffice user is null when firing requests though swagger.
j
Hello this is my first post. I am trying v14 of Umbraco, and have been following the tutorial on how to add a custom swagger document. Article: https://docs.umbraco.com/umbraco-cms/tutorials/creating-a-backoffice-api/adding-a-custom-swagger-document I created my first simple api controller "logout" and so far so good it shows up on the swagger dashboard. I authorize myself and try requesting from swagger, at first i get a 401, which i don't understand, as i am logged in as an admin.
Copy code
csharp
namespace Test.Web.Controllers.Api
{
    [ApiController]
    [ApiVersion("1.0")]
    [MapToApi("test-v1")]
    [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
    [JsonOptionsName(Constants.JsonOptionsNames.BackOffice)]
    [Route("api/v{version:apiVersion}/test")]
    public class LogoutController : Controller
    {
        private readonly INotificationService _notificationService;
        private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
        private readonly IHttpContextAccessor _httpContextAccessor;

        public LogoutController(IBackOfficeSecurityAccessor backOfficeSecurityAccessor, INotificationService notificationService, IHttpContextAccessor httpContextAccessor)
        {
            _backOfficeSecurityAccessor = backOfficeSecurityAccessor;
            _notificationService = notificationService;
            _httpContextAccessor = httpContextAccessor;
        }

        [HttpPost("logout")]
        [MapToApiVersion("1.0")]
        [ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
        public IActionResult Logout()
        {
            IUser? user = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser;
            if (user == null)
            {
                return Unauthorized();
            }

            return Ok();
        }
    }
}
The policy [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] Does not seem to be related/assigned to my role, what am i missing ?
futhermore if i remove the authorize attribute, i can hit the post method in my controller but then i see
Copy code
csharp
IUser? user = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser;
is always null, which i cant figure why is, as i have been following the guidelines described here: https://docs.umbraco.com/umbraco-cms/reference/custom-swagger-api
d
Hi there! A user in asp net 8 is only constructed when it is requested through an authorization attribute. So you need the authorize attribute in order to have the user property populated in your controller. So it's expected behaviour that you don't have a user because you removed the attribute. Can you tell me how you are logged in? More specifically, did you go to the backoffice on
/umbraco
and logged in there or did you use the authorize button in swagger?
j
I am using an external login provider. I have an authentication middelware that use OpenIdConnect to connect to our company Azure AD. This works fine as i am able to access the backoffice. I should in theory be assigned a user after OnTokenValidated has happened. Yes i login against ``/umbraco`` in my setup i have enabled the feature to directly go to the external login provider protal for sign in. After login i go to ``/umbraco/swagger`` where i also use the authorize button in swagger and set the client_id to
umbraco-swagger
and client_secret is left blank.
I have also tried to add app.UseAuthentication(); app.UseAuthorization(); without any noticeable effect. I feel like i am overlooking something...
d
This is all looking cool, as far as I can tell. You shouldn't have need to add
UseAuthentication
and
UseAuthorization
, I would highly advise against adding those, because Umbraco does it for you and adding it yourself may actually break your application.
If I remember correctly, the U14 endpoints use authentication with a jwt token in an authorize header. You obtain that token by using the authorize button in the swagger UI, so it's essential to log in through that button. Does it say in the documentation to use
umbraco-swagger
as the client id? I don't see it in a quick glance, it just says to press the authorize button. You could try leaving it empty instead. It should lead you to the backoffice login and through there, you should obtain a token.
j
I might have misread what was stated in this article https://docs.umbraco.com/umbraco-cms/reference/management-api under the section "Authorization". I just tried removing the client id (it appears it always prefilled to be ``umbraco-swagger`` when the authorize popup shows), but i receive the error that the mandatory client_id is missing 😅 https://cdn.discordapp.com/attachments/1267426623490097193/1267785528615505961/image.png?ex=66aa0cc8&is=66a8bb48&hm=7c0243861c981291e84b2c759f77fa0e005c227dc0fd76de2a111fc7b78754a3& https://cdn.discordapp.com/attachments/1267426623490097193/1267785528905043998/image.png?ex=66aa0cc8&is=66a8bb48&hm=918176042d4e0d506e90bddc4825b2640ecdd8838ddafb5141b5e04433b56c2a&
d
I see, yeah if it's prefilled, then I guess you just need to leave it there. I don't know then :/
j
I just found out that if i change my inheirted base type from "Controller" to "ManagementApiControllerBase", essentially moveing my endpoint into the api management swagger doc, i observe that the problem is not there 🥲 the user is actually assigned. So recreating the problem only takes changing to the out commented attributes instead of the ones i use in the file... that seem like a bug somehow or might require me to do additional stuff in order to roll on the user ? https://cdn.discordapp.com/attachments/1267426623490097193/1267808207078948874/LogoutController.cs?ex=66aa21e7&is=66a8d067&hm=7551214594ba61ff695e81743ca85411c9c636b1dea15ea94bc828238e34036b&
It seems to become a problem again if i map it to my custom swagger doc using the attribute ``[MapToApi("test-v1")]``, so maybe the problem is only linked to custom swagger docs ?
d
Perhaps you missed a step to connect the page in the swagger ui to the authorization of Umbraco
j
The doc only mentions i have to register the custom doc as the following
Copy code
csharp
    public class MyBackOfficeSecurityRequirementsOperationFilter : BackOfficeSecurityRequirementsOperationFilterBase
    {
        protected override string ApiName => "test";
    }

    public class MyConfigureSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
    {
        public void Configure(SwaggerGenOptions options)
        {
            options.SwaggerDoc("test-v1", new OpenApiInfo { Title = "Test v1", Version = "1.0" });
            options.OperationFilter<MyBackOfficeSecurityRequirementsOperationFilter>();
        }
    }

    public class MyComposer : IComposer
    {
        public void Compose(IUmbracoBuilder builder) => builder.Services.ConfigureOptions<MyConfigureSwaggerGenOptions>();
    }
d
I see right below that snippet that the controller in the docs also uses
ManagementApiControllerBase
as base class. So perhaps it is required to use that base class
j
Thats where i get confused. The documentation on how to add a custom swagger doc is described here https://docs.umbraco.com/umbraco-cms/reference/custom-swagger-api which use "Controller" as the base. But in a later tutorial seen here https://docs.umbraco.com/umbraco-cms/tutorials/creating-a-backoffice-api/adding-a-custom-swagger-document they use "ManagementApiControllerBase" 😅
Essentially the end result seems to be the same... not working, as it throws a 401. (when i also use the attribute [MapToApi("myCustomSwaggerDoc")]) But as soon as i create my endpoint and let it be assign to the api management doc it works as described, my user is assigned correctly and the 401 does not occur. Custom swagger docs seem to be related to the problem. Maybe there is some steps that need to be setup which is not described in details ?
d
hold on... I see in your
MyBackOfficeSecurityRequirementsOperationFilter
that you use "test" as the api name, while you use "test-v1" everywhere else
Maybe you should use "test-v1" in your operation filter, like you do everywhere else
j
Yes ! well spotted. Now the user is rolled on and authenticated correctly. Both appoarches work now.
Copy code
csharp
    [MapToApi("test-v1")]
    [VersionedApiBackOfficeRoute("api/test")]
    [ApiExplorerSettings(GroupName = "test API")]
    public class LogoutController : ManagementApiControllerBase
The first produce an endpoint that looks like this ``/umbraco/management/api/v1/api/test/logout`` (i should properly remove the api part of the route for a nicer looking endpoint).
Copy code
csharp
    [ApiController]
    [ApiVersion("1.0")]
    [MapToApi("test-v1")]
    [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
    [JsonOptionsName(Constants.JsonOptionsNames.BackOffice)]
    [Route("api/v{version:apiVersion}/test")]
    public class LogoutController : Controller
The second produce an endpoint that looks like this ``/api/v1/test/logout``
The solution really was aligning my naming in the operation filter, i totally missed this 😅 Many thanks @D_Inventor for your help. So changing this
Copy code
csharp
protected override string ApiName => "test";
To this
Copy code
csharp
protected override string ApiName => "test-v1";
Did the trick.
d
Awesome!!
j
I also see now by looking into the apply method in ``BackOfficeSecurityRequirementsOperationFilterBase`` that when the attribute [MapToApi("my-item-api")] does not match my apiName i have defined it just returns out, never applying the logic in the filter to my request 😄 but i still can't see how this ends up in a 401 when i review the code... Also how it ties into IBackOfficeSecurityAccessor service is also a mystery for me at this point 😅
d
Well, what I think happens here, is that the backoffice security requirement thing tells swagger how your apis are authenticated. By matching the names in the api, you group your apis together, so by using MapToApi, you're saying that your controller belongs to that specific api and by implementing the operation filter, you're saying that all endpoints in that api use the backoffice security requirement. When you make a call through the swagger ui to your endpoint, swagger sends the necessary login tokens along with your request. Then because you added the Authorize attribute to your api endpoint, a user object is created in your httpcontext through authentication middleware. The backoffice security accessor probably uses the httpcontext to get the current user, so that's why you only get a result when you have all of these components
j
I see, i am also missing the point you made earlier in how the [Authorize] attribute is tied to assigning the logged in user object in .net. That might explain why i observe IBackOfficeSecurityAccessor can't give me a user when i removed the attribute in my earlier tests 🙂 I know that the order of which you use attributes are important (or can be in cases), and i see your point, because of how the filter works it might never actually set the necessary security claims that swagger ui use to send the required tokens in my request, which seems to be needed to trigger the Authorize attribute correctly (and fetch my current user), which could result in the 401 i experienced before, that is as far as my understanding goes 😄
Thanks again for the insight 🥳 , i finally have an explanation to where i went wrong and more importantly now know why.
143 Views