Ticket T1150024
Visible to All Users

Microsoft.AspNetCore.Authorization.Authorize is not working?

created 2 years ago

Hi,

I am trying to implement XAF security to my RestAPI server. But I noticed, that Microsoft.AspNetCore.Authorization.Authorize attribute with Roles is not working and returns 403. Also ControllerBase.User.IsInRole("Viewer") is not working. And SecuritySystem.CurrentUser is null.

Security is implemented steb-by-step based on your template projects.
Can you tell me what I'm doing wrong?

Thank you for your time.
Tomáš

C#
[HttpGet("Test")] [Microsoft.AspNetCore.Authorization.Authorize(Roles = "Viewer")] public IActionResult Test() { var isInViewer = User.IsInRole("Viewer"); var currentUser = DevExpress.ExpressApp.SecuritySystem.CurrentUser; return Ok(User?.Identity?.Name); }
C#
public class Startup { IConfiguration Configuration; public Startup() { Configuration = ServiceProvider.Configuration; } /// <summary> /// This method gets called by the runtime. Use this method to add services to the container. /// </summary> public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IXpoDataStoreProvider>((serviceProvider) => { string connectionString = null; if (Configuration.GetConnectionString("ConnectionString") != null) { connectionString = Configuration.GetConnectionString("ConnectionString"); } return XPObjectSpaceProvider.GetDataStoreProvider(connectionString, null, true); }) .AddScoped<IAuthenticationTokenProvider, JwtTokenProviderService>() .AddScoped<IObjectSpaceProviderFactory, ObjectSpaceProviderFactory>(); services.AddXafAspNetCoreSecurity(Configuration, options => { options.RoleType = typeof(Module.BusinessObjects.UserModels.UserRole); // ApplicationUser descends from PermissionPolicyUser and supports the OAuth authentication. For more information, refer to the following topic: https://docs.devexpress.com/eXpressAppFramework/402197 // If your application uses PermissionPolicyUser or a custom user type, set the UserType property as follows: options.UserType = typeof(Module.BusinessObjects.UserModels.UserAccount); // ApplicationUserLoginInfo is only necessary for applications that use the ApplicationUser user type. // If you use PermissionPolicyUser or a custom user type, comment out the following line: //options.UserLoginInfoType = typeof(BusinessObjects.ApplicationUserLoginInfo); options.Events.OnSecurityStrategyCreated = securityStrategy => ((SecurityStrategy)securityStrategy).RegisterXPOAdapterProviders(); options.SupportNavigationPermissionsForTypes = false; }).AddAuthenticationStandard(options => { options.IsSupportChangePassword = true; }); services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuerSigningKey = true, //ValidIssuer = Configuration["Authentication:Jwt:Issuer"], //ValidAudience = Configuration["Authentication:Jwt:Audience"], ValidateIssuer = false, ValidateAudience = false, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Authentication:Jwt:IssuerSigningKey"])) }; }); services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder( JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser() .RequireXafAuthentication() .Build(); }); services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); var mvcBuilder = services .AddControllers(); services.Configure<KestrelServerOptions>(options => { options.Limits.MaxRequestBodySize = int.MaxValue; }); services.AddCors(options => { options.AddPolicy("AnyOrigin", builder1 => { builder1 .AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); services.AddSwaggerGen(options => { options.EnableAnnotations(); options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo { Title = "API", Version = "v1" }); options.OperationFilter<IITHeader>(); var xmlPath = Path.Combine(AppContext.BaseDirectory, "comments.xml"); options.IncludeXmlComments("comments.xml"); options.AddSecurityDefinition("JWT", new OpenApiSecurityScheme() { Type = SecuritySchemeType.Http, Name = "Bearer", Scheme = "bearer", BearerFormat = "JWT", In = ParameterLocation.Header }); options.AddSecurityRequirement(new OpenApiSecurityRequirement() { { new OpenApiSecurityScheme() { Reference = new OpenApiReference() { Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, Id = "JWT" } }, new string[0] }, }); }); } /// <summary> /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. /// </summary> /// <param name="app"></param> public void Configure(IApplicationBuilder app) { var configuration = ApiServerConfiguration.Get(); var serverAddress = configuration.Url; if (serverAddress.Last() == '/') serverAddress = serverAddress.Remove(serverAddress.Length - 1, 1); app.UseCors("AnyOrigin"); app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint($"{serverAddress}/swagger/v1/swagger.json", "API V1"); }); app.UseRequestLocalization(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } }

Answers approved by DevExpress Support

created 2 years ago

Hello Tomáš,

We have never considered such a case before. Although, XAF Security requires that the ClaimsPrincipal object has an authenticated state and XAF uses it to obtain such claims like ClaimTypes.NameIdentifier, but Microsoft.AspNetCore.Authorization and XAF authorization are not really related to each other. We use Microsoft ASP.NET Core security to create a cookie, and MS security processes this cookie for us, so we have to deal only with ClaimsPrincipal objects. OAuth2 authentication for an XAF Blazor application is a good example of that (see Active Directory and OAuth2 Authentication Providers in ASP.NET Core Blazor Applications).

I think that your case can be handled by adding custom claims manually. I used our MainDemo application for tests. When XAF standard authentication is used, we create a ClaimsPrincipal object with required claims and process this ClaimsPrincipal object later.

  1. To add additional claims to a user cookie for an XAF Blazor application, from XAF v22.2.4 the following events can be used:
    OnCustomizeLoginToken
    OnCustomizeClaims

The OnCustomizeLoginToken event is more interesting in this case. It can be used to add additional claims during XAF standard authentication:

C#
builder.Security .UseIntegratedMode(//...) .AddPasswordAuthentication(options => { //... options.Events.OnCustomizeLoginToken = context => { using IObjectSpace os = context.ServiceProvider.GetRequiredService<INonSecuredObjectSpaceFactory>().CreateNonSecuredObjectSpace<ApplicationUser>(); var user = os.FirstOrDefault<ApplicationUser>(u => u.Oid == new Guid(context.UserId)); foreach(var role in user.Roles) { context.Claims.Add(new Claim(ClaimTypes.Role, role.Name)); } }; });

Even when we need to create an IObjectSpace and load a user object, this is a fairly rare operation in the context of an application's lifetime. It only happens when the user logs in via the login form.

  1. For an XAF Web API Service, we can add required claims in the MainDemo application when a bearer token is created. In the AuthenticationController controller in the MainDemo application, it looks like this:
C#
using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using DevExpress.ExpressApp; using DevExpress.ExpressApp.Core; using DevExpress.ExpressApp.Security; using DevExpress.ExpressApp.Security.Authentication; using MainDemo.Module.BusinessObjects; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; using Swashbuckle.AspNetCore.Annotations; namespace MainDemo.WebApi.Jwt; [ApiController] [Route("api/[controller]")] // This is a JWT authentication service sample. public class AuthenticationController : ControllerBase { readonly IStandardAuthenticationService securityAuthenticationService; readonly IConfiguration configuration; readonly INonSecuredObjectSpaceFactory nonSecuredObjectSpaceFactory; public AuthenticationController(IStandardAuthenticationService securityAuthenticationService, IConfiguration configuration, INonSecuredObjectSpaceFactory nonSecuredObjectSpaceFactory) { this.securityAuthenticationService = securityAuthenticationService; this.configuration = configuration; this.nonSecuredObjectSpaceFactory = nonSecuredObjectSpaceFactory; } [HttpPost("Authenticate")] [SwaggerOperation("Checks if the user with the specified logon parameters exists in the database. If it does, authenticates this user.", "Refer to the following help topic for more information on authentication methods in the XAF Security System: <a href='https://docs.devexpress.com/eXpressAppFramework/119064/data-security-and-safety/security-system/authentication'>Authentication</a>.")] public IActionResult Authenticate( [FromBody] [SwaggerRequestBody(@"For example: <br /> { ""userName"": ""Sam"", ""password"": """" }")] AuthenticationStandardLogonParameters logonParameters ) { ClaimsPrincipal user = securityAuthenticationService.Authenticate(logonParameters); using IObjectSpace os = nonSecuredObjectSpaceFactory.CreateNonSecuredObjectSpace<ApplicationUser>(); Guid userOid = new Guid(user.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value); var xafUser = os.FirstOrDefault<ApplicationUser>(u => u.Oid == userOid); var claims = new List<Claim>(user.Claims); foreach(var role in xafUser.Roles) { claims.Add(new Claim(ClaimTypes.Role, role.Name)); } if(user != null) { var issuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Authentication:Jwt:IssuerSigningKey"])); var token = new JwtSecurityToken( issuer: configuration["Authentication:Jwt:ValidIssuer"], audience: configuration["Authentication:Jwt:ValidAudience"], claims: claims, expires: DateTime.Now.AddHours(2), signingCredentials: new SigningCredentials(issuerSigningKey, SecurityAlgorithms.HmacSha256) ); return Ok(new JwtSecurityTokenHandler().WriteToken(token)); } return Unauthorized("User name or password is incorrect."); } }

I used the following custom DataController type for tests:

C#
[Authorize(AuthenticationSchemes = "Bearer", Roles = "Administrators")] public class CustomDataController<TEntity, TDelta> : DataControllerBase<TEntity, TDelta> where TEntity : class where TDelta : class { public CustomDataController(IDataService dataService) : base(dataService) { } }

In such a case, when we want to specify allowed roles, we need to specify an authentication scheme, too.

To register custom data controlled by XAF Web API Service, you can use the WebApiOptions.DataControllerType property. Take note that this property is hidden form IntelliSense in the current XAF version, but it works as expected.

C#
services.AddXafWebApi(Configuration, options => { options.BusinessObject<ApplicationUser>(); //... options.DataControllerType = typeof(CustomDataController<,>); }).AddXpoServices();
    Comments (2)
    TB TB
    Tomáš Bureš 2 years ago

      Hi,

      thank you for your answer. For JWT it works. Can you provide me example for Windows (AD) authentication? I didn't manage to find suitable event for that.

      Tomáš

      Dmitry M (DevExpress) 2 years ago

        Hello,

        There are two possible cases where Windows (AD) authentication is used:

        • When Windows (AD) authentication is used as default authentication, the client automatically sends user information to the server on each request because the server doesn't allow anonymous authentication. Thus, XAF code doesn't create a WindowsPrincial object. I did not check whether it is possible to affect the creation of the WindowsPrincial object, e.g. by adding Claims. This is not related to XAF.
        • When Windows (AD) authentication is not default, and it can be selected on the login page, XAF code forces the authentication process. Thus, the WindowsPrincial object is obtained from the client. Since the client doesn't send user information on each request, it is required to store user data. We create ClaimsPrincipal and a cookie to make OnCustomizeClaims available.

        Please take into account the following:

        • When Active Directory authentication is used as the default authentication method, the client passes user identity information to the server on each request. In this casde, an authentication cookie is not used, so the OnCustomizeClaims delegate method is never called.
        • To keep the cookie small, only the Claims of the following types are copied from the principal object (the default setting): ClaimTypes.Name ClaimTypes.NameIdentifier

        When OnCustomizeLoginToken or
        OnCustomizeClaims is raised for password authentication, XAF security logic has already validated user input in the login form (name, password) - a user record is found and the password is validated. However, when OnCustomizeClaims for Windows (AD) authentication fires, XAF security is not yet called, so it is not known whether a user is in the database. In general, it's possible that the user is not in the database or the user doesn't have a related UserLoginInfo record, which enables Windows (AD) authentication. It also means that the 'CreateUserAutomatically' feature was not called yet, and even if the user will be created later, this user record is not available yet.

        The custom logic that loads a related XAF user record can look as follows:

        C#
        services.AddXafAspNetCoreSecurity(Configuration, options => { //... }) .AddAuthenticationStandard(options => { options.IsSupportChangePassword = true; }).AddAuthenticationActiveDirectory(options => { options.CreateUserAutomatically = true; options.SetNewUserInitializer(new NewUserInitializer()); //options.SignOutRedirect = "/FailedSignIn"; options.Events.OnCustomizeClaims = context => { string userName = context.Principal.Identity.Name; using IObjectSpace os = context.ServiceProvider.GetRequiredService<INonSecuredObjectSpaceFactory>().CreateNonSecuredObjectSpace<ApplicationUser>(); ApplicationUser user = (ApplicationUser)(ISecurityUserWithLoginInfo)FindUserLoginInfo(SecurityDefaults.WindowsAuthentication, userName, os)?.User; if(user == null) { user = (ApplicationUser)(ISecurityUserWithLoginInfo)FindUserLoginInfo("Negotiate", userName, os)?.User; } if(user != null) { foreach(var role in user.Roles) { context.Claims.Add(new Claim(ClaimTypes.Role, role.Name)); } } ISecurityUserLoginInfo FindUserLoginInfo(string loginProviderName, string providerUserKey, IObjectSpace objectSpace) { CriteriaOperator criteria = CriteriaOperator.And(new BinaryOperator(nameof(ISecurityUserLoginInfo.LoginProviderName), loginProviderName), new BinaryOperator(nameof(ISecurityUserLoginInfo.ProviderUserKey), providerUserKey)); return objectSpace.FindObject<ApplicationUserLoginInfo>(criteria); } }; }) .AddAuthenticationProvider<CustomAuthenticationProvider>();
        C#
        authentication .AddCookie(options => { options.LoginPath = "/LoginPage"; }) .AddGoogle(options => { Configuration.Bind("Authentication:Google", options); options.AuthorizationEndpoint += "?prompt=consent"; options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.ClaimActions.MapJsonKey(XafClaimTypes.UserImageUrl, "picture"); options.Events.OnCreatingTicket = context => { var claimsPrincipal = context.Principal; using IObjectSpace os = context.HttpContext.RequestServices.GetRequiredService<INonSecuredObjectSpaceFactory>().CreateNonSecuredObjectSpace<ApplicationUser>(); var userIdClaim = claimsPrincipal.FindFirst("sub") ?? claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier) ?? throw new InvalidOperationException("Unknown user id"); ApplicationUser user = (ApplicationUser)(ISecurityUserWithLoginInfo)FindUserLoginInfo(claimsPrincipal.Identity.AuthenticationType, userIdClaim.Value, os)?.User; if(user != null) { var customClaims = new List<Claim>(); foreach(var role in user.Roles) { customClaims.Add(new Claim(ClaimTypes.Role, role.Name)); } customClaims.Add(new Claim("MyClaim", "MyCustomClaimValue")); claimsPrincipal.AddIdentity(new ClaimsIdentity(customClaims, "MyCustomAuthType")); } ISecurityUserLoginInfo FindUserLoginInfo(string loginProviderName, string providerUserKey, IObjectSpace objectSpace) { CriteriaOperator criteria = CriteriaOperator.And(new BinaryOperator(nameof(ISecurityUserLoginInfo.LoginProviderName), loginProviderName), new BinaryOperator(nameof(ISecurityUserLoginInfo.ProviderUserKey), providerUserKey)); return objectSpace.FindObject<ApplicationUserLoginInfo>(criteria); } return System.Threading.Tasks.Task.CompletedTask; }; })

        In the following image, you can see the resulting ClaimsPrincipal object with which XAF is working:

        I assume that the same solution may be applied to Windows (AD) authentication when it is used as the default authentication method.

        Disclaimer: The information provided on DevExpress.com and affiliated web properties (including the DevExpress Support Center) is provided "as is" without warranty of any kind. Developer Express Inc disclaims all warranties, either express or implied, including the warranties of merchantability and fitness for a particular purpose. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.

        Confidential Information: Developer Express Inc does not wish to receive, will not act to procure, nor will it solicit, confidential or proprietary materials and information from you through the DevExpress Support Center or its web properties. Any and all materials or information divulged during chats, email communications, online discussions, Support Center tickets, or made available to Developer Express Inc in any manner will be deemed NOT to be confidential by Developer Express Inc. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.