OpenIddict 角色/策略返回 403 Forbidden

问题描述

早上好。我在控制器中授权和使用角色和/或策略设置时遇到问题。它总是返回 403 forbidden,日志中包含以下内容

info: OpenIddict.Server.OpenIddictServerdispatcher[0]
  The request address matched a server endpoint: Token.
info: OpenIddict.Server.OpenIddictServerdispatcher[0]
  The token request was successfully extracted: {
    "grant_type": "password","username": "Administrator@MRM2Inc.com","password": "[redacted]"
  }.
info: OpenIddict.Server.OpenIddictServerdispatcher[0]
  The token request was successfully validated.
info: OpenIddict.Server.OpenIddictServerdispatcher[0]
  The response was successfully returned as a JSON document: {
    "access_token": "[redacted]","token_type": "Bearer","expires_in": 3600
  }.
info: OpenIddict.Validation.OpenIddictValidationdispatcher[0]
  The response was successfully returned as a challenge response: {
    "error": "insufficient_access","error_description": "The user represented by the token is not allowed to perform the requested action.","error_uri": "https://documentation.openiddict.com/errors/ID2095"
  }.
info: OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandler[13]
  AuthenticationScheme: OpenIddict.Validation.AspNetCore was forbidden.

如果我从授权标签删除 Roles = 或 Policy = 它会起作用。我的项目设置如下:

Startup.cs

 public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        services.AddDbContext<IdentDbContext>(options =>
        {
            options.UsesqlServer(
                Configuration.GetConnectionString("IdentityDB"));

            options.USEOpenIddict();
        });
        
        // Add the Identity Services we are going to be using the Application Users and the Application Roles
        services.AddIdentity<ApplicationUsers,ApplicationRoles>(config =>
        {
            config.SignIn.RequireConfirmedEmail = true;
            config.SignIn.RequireConfirmedAccount = true;
            config.User.RequireUniqueEmail = true;
            config.Lockout.MaxFailedAccessAttempts = 3;
        }).AddEntityFrameworkStores<IdentDbContext>()
        .AddUserStore<ApplicationUserStore>()
        .AddRoleStore<ApplicationRoleStore>()
        .AddRoleManager<ApplicationRoleManager>()
        .AddUserManager<ApplicationUserManager>()
        .AddErrorDescriber<ApplicationIdentityErrorDescriber>()
        .AddDefaultTokenProviders()
        .AddDefaultUI();

        services.AddDataLibrary();

        // Configure Identity to use the same JWT claims as OpenIddict instead
        // of the legacy WS-Federation claims it uses by default (ClaimTypes),// which saves you from doing the mapping in your authorization controller.
        services.Configure<IdentityOptions>(options =>
        {
            options.ClaimsIdentity.UserNameClaimType = Claims.Name;
            options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
            options.ClaimsIdentity.RoleClaimType = Claims.Role;
        });

        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
        });

        // Add in the email
        var emailConfig = Configuration.GetSection("EmailConfiguration").Get<EmailConfiguration>();
        services.AddSingleton(emailConfig);
        services.AddEmailLibrary();

        services.AddAuthorization(option =>
        {
            option.AddPolicy("SiteAdmin",policy => policy.RequireClaim("Site Administrator"));
        });

        services.AddOpenIddict()
            // Register the OpenIddict core components.
            .AddCore(options =>
            {
                // Configure OpenIddict to use the Entity Framework Core stores and models.
                // Note: call ReplaceDefaultEntities() to replace the default entities.
                options.UseEntityFrameworkCore()
                .UseDbContext<IdentDbContext>()
                /*.ReplaceDefaultEntities<Guid>()*/;
            })
            // Register the OpenIddict server components.
            .AddServer(options =>
            {
                // Enable the token endpoint.  What other endpoints?
                options.SetlogoutEndpointUris("/api/logoutPost")
                .SetTokenEndpointUris("/Token");

                // Enable the client credentials flow.  Which flow do I need?
                options.AllowPasswordFlow();

                options.AcceptAnonymousClients();

                options.disableAccesstokenEncryption();

                // Register the signing and encryption credentials.
                options.AddDevelopmentEncryptionCertificate()
                      .AddDevelopmentSigningCertificate();

                // Register the ASP.NET Core host and configure the ASP.NET Core options.
                options.UseAspNetCore()
                       .EnablelogoutEndpointPassthrough()
                       .EnabletokenEndpointPassthrough();
            })
            // Register the OpenIddict validation components.
            .AddValidation(options =>
            {
                // Import the configuration from the local OpenIddict server instance.
                options.UseLocalServer();

                // Register the ASP.NET Core host.
                options.UseAspNetCore();
            });            

        // Register the Swagger generator,defining 1 or more Swagger documents
        services.AddSwaggerGen(swagger => 
        {
            swagger.AddSecurityDeFinition("Bearer",new OpenApiSecurityScheme()
            {
                Name = "Authorization",Type = SecuritySchemeType.Http,Scheme = "Bearer",BearerFormat = "JWT",In = ParameterLocation.Header,Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer'[space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\""
            });
            swagger.AddSecurityRequirement(new OpenApiSecurityRequirement
            {
                {
                    new OpenApiSecurityScheme
                    {
                        Reference = new OpenApiReference
                        {
                            Type = ReferenceType.SecurityScheme,Id = "Bearer"
                        }
                    },new string[] {}
                }
            });
            swagger.OperationFilter<SwaggerDefaultValues>();
            swagger.OperationFilter<AuthenticationRequirementOperationFilter>();

            // Set the comments path for the Swagger JSON and UI.
            var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
            var xmlPath = Path.Combine(AppContext.BaseDirectory,xmlFile);
            swagger.IncludeXmlComments(xmlPath);
        });
        services.AddApiVersioning();
        services.AddVersionedApiExplorer(options =>
        {
            options.GroupNameFormat = "'v'VVVV";
            options.DefaultApiVersion = ApiVersion.Parse("0.6.alpha");
            options.AssumeDefaultVersionWhenUnspecified = true;
        });
        services.AddTransient<IConfigureOptions<SwaggerGenoptions>,ConfigureSwaggerOptions>();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app,IWebHostEnvironment env,IApiVersionDescriptionProvider provider)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios,see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        // Enable middleware to serve generated Swagger as a JSON endpoint.
        app.UseSwagger();

        // Enable middleware to serve swagger-ui (HTML,JS,CSS,etc.),// specifying the Swagger JSON endpoint.
        app.UseSwaggerUI(c =>
        {               
            c.displayOperationId();
            var versionDescription = provider.ApiVersionDescriptions;
            foreach (var description in provider.ApiVersionDescriptions.OrderByDescending(_ => _.ApiVersion))
            {
                c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json",$"MRM2 Identity API {description.GroupName}");
            }
        });

        app.UseRouting();

        app.UseAuthentication();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }

授权控制器.cs

/// <summary>
/// Controls the Authorization aspects of the API
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
[ApiVersion("0.8.alpha")]
[Produces(MediaTypeNames.Application.Json)]
public class AuthorizationController : ControllerBase
{
    private readonly IConfiguration _configuration;
    private readonly IdentDbContext _context;
    private readonly ApplicationUserManager _userManager;
    private readonly ApplicationRoleManager _roleManager;
    private readonly IOpenIddictApplicationManager _applicationManager;
    private readonly IOpenIddictAuthorizationManager _authorizationManager;
    private readonly IOpenIddictScopeManager _scopeManager;
    private readonly SignInManager<ApplicationUsers> _signInManager;
    private HttpClient _client;

    public AuthorizationController(IConfiguration configuration,IdentDbContext context,ApplicationUserManager userManager,ApplicationRoleManager roleManager,IOpenIddictApplicationManager applicationManager,IOpenIddictAuthorizationManager authorizationManager,IOpenIddictScopeManager scopeManager,SignInManager<ApplicationUsers> signInManager)
    {            
        _configuration = configuration;
        _context = context;
        _userManager = userManager;
        _roleManager = roleManager;
        _applicationManager = applicationManager;
        _authorizationManager = authorizationManager;
        _scopeManager = scopeManager;
        _signInManager = signInManager;
    }

    [HttpPost("/token"),Produces("application/json")]
    public async Task<IActionResult> Exchange()
    {
        var request = HttpContext.GetopenIddictServerRequest() ??
            throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

        ClaimsPrincipal claimsPrincipal;

        if (request.IsPasswordGrantType())
        {
            var user = await _userManager.FindByNameAsync(request.Username);
            var roleList = await _userManager.GetRolesListAsync(user);
            var databaseList = await _userManager.GetDatabasesAsync(user);
            string symKey = _configuration["Jwt:Symmetrical:Key"];
            string jwtSub = _configuration["Jwt:Subject"];
            string issuer = _configuration["Jwt:Issuer"];
            string audience = _configuration["Jwt:Audience"];

            var claims = new List<Claim>
            {
                new Claim(JwtRegisteredClaimNames.Sub,jwtSub,issuer),new Claim(ClaimTypes.NameIdentifier,user.Id.ToString(),new Claim(ClaimTypes.Name,user.UserName,issuer)
            };

            foreach (var role in roleList)
            {
                claims.Add(new Claim(ClaimTypes.Role,role.Name));
            }

            foreach (var database in databaseList)
            {
                claims.Add(new Claim(type: "DatabaseName",database));
            }
            var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            identity.AddClaim(OpenIddictConstants.Claims.Name,OpenIddictConstants.Destinations.Accesstoken);
            identity.AddClaim(OpenIddictConstants.Claims.Subject,OpenIddictConstants.Destinations.Accesstoken);
            identity.AddClaim(OpenIddictConstants.Claims.Audience,audience,OpenIddictConstants.Destinations.Accesstoken);
            foreach (var cl in claims)
            {
                identity.AddClaim(cl.Type,cl.Value);       
            }

            claimsPrincipal = new ClaimsPrincipal(identity);

            // Set the list of scopes granted to the client application.
            claimsPrincipal.SetScopes(new[]
            {
                Scopes.OpenId,Scopes.Email,Scopes.Profile,Scopes.Roles
            }.Intersect(request.GetScopes()));

            foreach (var claim in claimsPrincipal.Claims)
            {
                claim.SetDestinations(GetDestinations(claim,claimsPrincipal));
            }
            return SignIn(claimsPrincipal,OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
        {
            // Retrieve the claims principal stored in the authorization code/device code/refresh token.
            var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;

            // Retrieve the user profile corresponding to the authorization code/refresh token.
            // Note: if you want to automatically invalidate the authorization code/refresh token
            // when the user password/roles change,use the following line instead:
            // var user = _signInManager.ValidateSecurityStampAsync(info.Principal);
            var user = await _userManager.GetUserAsync(principal);
            if (user == null)
            {
                return Forbid(
                    authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,properties: new AuthenticationProperties(new Dictionary<string,string>
                    {
                        [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
                    }));
            }

            // Ensure the user is still allowed to sign in.
            if (!await _signInManager.CanSignInAsync(user))
            {
                return Forbid(
                    authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
                    }));
            }

            foreach (var claim in principal.Claims)
            {
                claim.SetDestinations(GetDestinations(claim,principal));
            }

            // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
            return SignIn(principal,OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        throw new InvalidOperationException("The specified grant type is not supported.");
    }

    private IEnumerable<string> GetDestinations(Claim claim,ClaimsPrincipal principal)
    {
        // Note: by default,claims are NOT automatically included in the access and identity tokens.
        // To allow OpenIddict to serialize them,you must attach them a destination,that specifies
        // whether they should be included in access tokens,in identity tokens or in both.

        switch (claim.Type)
        {
            case Claims.Name:
                yield return Destinations.Accesstoken;

                if (principal.HasScope(Scopes.Profile))
                    yield return Destinations.IdentityToken;

                yield break;

            case Claims.Email:
                yield return Destinations.Accesstoken;

                if (principal.HasScope(Scopes.Email))
                    yield return Destinations.IdentityToken;

                yield break;

            case Claims.Role:
                yield return Destinations.Accesstoken;

                if (principal.HasScope(Scopes.Roles))
                    yield return Destinations.IdentityToken;

                yield break;

            // Never include the security stamp in the access and identity tokens,as it's a secret value.
            case "AspNet.Identity.SecurityStamp": yield break;

            default:
                yield return Destinations.Accesstoken;
                yield break;
        }
    }
    
}

角色控制器.cs

/// <summary>
/// Controls the actions for roles within the API
/// </summary>
/// <response code="401">If the user did not login correctly or 
/// does not have the correct permissions</response>
[Route("api/[controller]")]
[ApiController]
[ApiVersion("0.8.alpha")]
[Produces(MediaTypeNames.Application.Json)]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme,Roles = "Site Administrator")]  //If I change this to Policy = "SiteAdmin" still does not work.  If I remove the Roles completely it works.
public class RolesController : ControllerBase
{
    private readonly ApplicationRoleManager _roleManager;
    private readonly ILogger<RolesController> _logger;
    private readonly IApplicationDatabaseData _databaseData;

    public RolesController(ApplicationRoleManager roleManager,ILogger<RolesController> logger,IApplicationDatabaseData databaseData)
    {
        _roleManager = roleManager;
        _logger = logger;
        _databaseData = databaseData;
    }

    /// <summary>
    /// Gets a List of all the Roles
    /// </summary>
    /// <returns>A list of ApplicationRoles</returns>
    /// <response code="200">Returns the list</response>
    /// <response code="404">If the list is empty</response>
    [HttpGet("ListRoles",Name = nameof(ListRolesAsync))]
    [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme/*,Roles = "Site Administrator"*/)]  //Currently commented out until Roles work.
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<IList<ApplicationRoles>>> ListRolesAsync()
    {
        var roles = await _roleManager.GetAllRoles();
        if (!roles.Any())
        {
            return NotFound();
        }
        else
        {
            var output = roles;
            return Ok(output);
        }            
    }        
}

我注意到,在调试中逐步执行此操作时,除了在通过 GetDestinations 时的 Claims.Name 之外,所有声明都达到了认的 switch case。所以我不确定我在启动或授权控制器中哪里出错了。但我很确定我的问题就在那里。 为了让我的角色和/或政策在控制器内正常工作,我错过了什么?

解决方法

对 AuthorizationController 的更新允许它工作。 Exchange 方法的 AuthorizationContoller 的新部分如下(仍在进行中,但现在正在进行中):

    [HttpPost("/token"),Produces("application/json")]
    public async Task<IActionResult> Exchange()
    {
        var request = HttpContext.GetOpenIddictServerRequest() ??
            throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

        ClaimsPrincipal claimsPrincipal;

        if (request.IsClientCredentialsGrantType())
        {
            var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

            identity.AddClaim(OpenIddictConstants.Claims.Subject,request.ClientId ?? throw new InvalidOperationException());

            identity.AddClaim("some-claim","some-value",OpenIddictConstants.Destinations.AccessToken);

            claimsPrincipal = new ClaimsPrincipal(identity);

            claimsPrincipal.SetScopes(request.GetScopes());
        }

        if (request.IsPasswordGrantType())
        {
            var user = await _userManager.FindByNameAsync(request.Username);
            var roleList = await _userManager.GetRolesListAsync(user);
            var databaseList = await _userManager.GetDatabasesAsync(user);
            string symKey = _configuration["Jwt:Symmetrical:Key"];
            string jwtSub = _configuration["Jwt:Subject"] + " " + user.Id;
            string issuer = _configuration["Jwt:Issuer"];
            string audience = _configuration["Jwt:Audience"];

            var claims = new List<Claim>
            {
                new Claim(JwtRegisteredClaimNames.Sub,jwtSub,issuer),new Claim(ClaimTypes.NameIdentifier,user.Id.ToString(),new Claim(ClaimTypes.Name,user.UserName,issuer)
            };

            foreach (var role in roleList)
            {
                claims.Add(new Claim(ClaimTypes.Role,role.Name,ClaimValueTypes.String,issuer));
            }

            foreach (var database in databaseList)
            {
                claims.Add(new Claim(type: "DatabaseName",database,issuer));
            }
            var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            identity.AddClaim(OpenIddictConstants.Claims.Name,OpenIddictConstants.Destinations.AccessToken);
            identity.AddClaim(OpenIddictConstants.Claims.Subject,OpenIddictConstants.Destinations.AccessToken);
            identity.AddClaim(OpenIddictConstants.Claims.Audience,audience,OpenIddictConstants.Destinations.AccessToken);
            foreach (var cl in claims)
            {
                if (cl.Type == ClaimTypes.Role)
                {
                    identity.AddClaim(OpenIddictConstants.Claims.Role,cl.Value,OpenIddictConstants.Destinations.AccessToken,OpenIddictConstants.Destinations.IdentityToken);
                }
                identity.AddClaim(cl.Type,OpenIddictConstants.Destinations.IdentityToken);       
            }

            claimsPrincipal = new ClaimsPrincipal(identity);

            // Set the list of scopes granted to the client application.
            claimsPrincipal.SetScopes(new[]
            {
                Scopes.OpenId,Scopes.Email,Scopes.Profile,Scopes.Roles
            }.Intersect(request.GetScopes()));

            foreach (var claim in claimsPrincipal.Claims)
            {
                claim.SetDestinations(GetDestinations(claim,claimsPrincipal));
            }
            return SignIn(claimsPrincipal,OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
        {
            // Retrieve the claims principal stored in the authorization code/device code/refresh token.
            var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;

            // Retrieve the user profile corresponding to the authorization code/refresh token.
            // Note: if you want to automatically invalidate the authorization code/refresh token
            // when the user password/roles change,use the following line instead:
            // var user = _signInManager.ValidateSecurityStampAsync(info.Principal);
            var user = await _userManager.GetUserAsync(principal);
            if (user == null)
            {
                return Forbid(
                    authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,properties: new AuthenticationProperties(new Dictionary<string,string>
                    {
                        [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
                    }));
            }

            // Ensure the user is still allowed to sign in.
            if (!await _signInManager.CanSignInAsync(user))
            {
                return Forbid(
                    authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
                    }));
            }

            foreach (var claim in principal.Claims)
            {
                claim.SetDestinations(GetDestinations(claim,principal));
            }

            // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
            return SignIn(principal,OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        throw new InvalidOperationException("The specified grant type is not supported.");
    }