.NET Core 3.1 IdentityServer4:使用资源所有者密码凭据授予时获取无效访问令牌

Posted

技术标签:

【中文标题】.NET Core 3.1 IdentityServer4:使用资源所有者密码凭据授予时获取无效访问令牌【英文标题】:.NET Core 3.1 IdentityServer4: getting invalid access token when using Resource Owner Password Credentials grant 【发布时间】:2020-05-26 12:26:59 【问题描述】:

我正在尝试使用资源所有者密码凭据授予类型从身份提供者获取访问令牌。相同的配置适用于 .NET Core 2.2,但它不再适用于 .NET Core 3.1。下面是身份提供者的配置:

public class Startup

    public IConfiguration Configuration  get; 
    private readonly string _MyAllowSpecificOrigins = "fooorigin";
    private readonly IWebHostEnvironment _env;

    public Startup(IConfiguration configuration, IWebHostEnvironment env)
    
        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
        IdentityModelEventSource.ShowPII = true;
        _env = env;
        Configuration = configuration;
    

    public void ConfigureServices(IServiceCollection services)
    
        services.Configure<CookiePolicyOptions>(options =>
        
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        );

        services.AddPersistence(Configuration); //Custom extension
        services.AddAutoMapper(Assembly.GetAssembly(typeof(BaseMappingProfile)));

        #region Options
        services.Configure<IdentityServerOptions>(Configuration.GetSection("IdentityServerOptions"));
        services.Configure<Settings>(Configuration.GetSection("Settings"));
        #endregion

        #region Configurations
        services.AddTransient<IdentityServerOptions>();
        #endregion

        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        services.AddScoped<ITokenManagerHelper, TokenManagerHelper>();

        services.AddScoped<IUserService, UserService>();

        services.AddCors(options =>
        
            options.AddPolicy(_MyAllowSpecificOrigins,
            builder =>
            
                builder.AllowAnyOrigin()
                       .AllowAnyHeader()
                       .AllowAnyMethod();
            );
        );

        services.AddMvc().AddFluentValidation(fv =>
        
            fv.RegisterValidatorsFromAssemblyContaining<CommonValidator>();
            fv.ImplicitlyValidateChildProperties = true;
        )
        .SetCompatibilityVersion(CompatibilityVersion.Version_3_0);

        var identityServerDataDBConnectionString = Configuration.GetConnectionString("IdentityServerConfigDatabase");
        var migrationsAssembly = typeof(UsersDbContext).GetTypeInfo().Assembly.GetName().Name;
        var identityAuthority = Configuration.GetValue<string>("IdentityServerOptions:Authority");

        // Add Authentication
        services.AddAuthentication(options =>
           
               options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
               options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
               options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
               options.DefaultChallengeScheme = "oidc";
           )
          .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
          .AddOpenIdConnect("oidc", options =>
          
              options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
              options.Authority = Configuration.GetValue<string>("IdentityServerOptions:Authority");
              options.ClientId = Configuration.GetValue<string>("IdentityServerOptions:ClientName");
              options.ClientSecret = Configuration.GetValue<string>("IdentityServerOptions:ClientSecret");
              options.ResponseType = Configuration.GetValue<string>("IdentityServerOptions:ResponseType");

              options.Scope.Add("openid");
              options.Scope.Add("profile");
              options.Scope.Add("roles");
              options.Scope.Add("fooapi");
              options.Scope.Add("fooidentityapi");
              options.Scope.Add("offline_access");
              options.SaveTokens = true;

              options.GetClaimsFromUserInfoEndpoint = true;
              options.ClaimActions.Remove("amr");
              options.ClaimActions.DeleteClaim("sid");

              options.TokenValidationParameters = new TokenValidationParameters
              
                  NameClaimType = JwtClaimTypes.GivenName,
                  RoleClaimType = JwtClaimTypes.Role,
              ;
          );

        services.AddTransient<IPersistedGrantStore, PersistedGrantStore>();
        services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();

        // Add Identity Server
        // Add Signing Certificate
        // Add Users Store
        // Add Configurations Store
        // Add Operational Stores
        if (_env.IsDevelopment() || _env.IsStaging())
        
            services.AddIdentityServer()
            .AddDeveloperSigningCredential()
            .AddUserStore()
            .AddConfigurationStore(options =>
            
                options.ConfigureDbContext = builder =>
                
                    builder.UseSqlServer(identityServerDataDBConnectionString,
                        sql => sql.MigrationsAssembly(migrationsAssembly));
                ;
            )
            .AddOperationalStore(options =>
            
                options.ConfigureDbContext = builder =>
                
                    builder.UseSqlServer(identityServerDataDBConnectionString,
                        sql => sql.MigrationsAssembly(migrationsAssembly));
                ;
            )
            .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>();
        
        else
        
            //Todo: add certificate
        
    

    public void Configure(
        IApplicationBuilder app, 
        IWebHostEnvironment env, 
        IOptions<Settings> settingOptions)
    
        if (env.IsDevelopment())
        
            app.UseDeveloperExceptionPage();
        
        else
        
            app.UseHsts();
        

        app.UseCors(_MyAllowSpecificOrigins);

        app.UseCookiePolicy();
        var forwardOptions = new ForwardedHeadersOptions
        
            ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
            RequireHeaderSymmetry = false
        ;

        forwardOptions.KnownNetworks.Clear();
        forwardOptions.KnownProxies.Clear();

        app.UseForwardedHeaders(forwardOptions);

        app.UseAuthentication();
        app.UseRouting();
        app.UseIdentityServer();
    

这里是API的配置:

public class Startup

    #region Private Fields
    private readonly string _allowedOrigins = "fooorigin";
    #endregion

    public Startup(IConfiguration configuration)
    
        Configuration = configuration;
    

    public IConfiguration Configuration  get; 

    public void ConfigureServices(IServiceCollection services)
    
        services.AddControllers();
        services.AddAuthorization();
        services.AddAuthentication("Bearer")
            .AddIdentityServerAuthentication(options =>
            
                options.Authority = Configuration.GetValue<string>("IdentityServerOptions:Authority");
                options.RequireHttpsMetadata = false;
                options.ApiName = Configuration.GetValue<string>("IdentityServerOptions:ApiName");
                options.SupportedTokens = IdentityServer4.AccessTokenValidation.SupportedTokens.Jwt;
            );
        services.Configure<CookiePolicyOptions>(options =>
        
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        );

        #region Options
        services.AddOptions();
        #endregion

        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

        services.AddCors(options =>
        
            options.AddPolicy(_allowedOrigins, builder =>
            
                builder.AllowAnyOrigin()
                .AllowAnyHeader()
                .AllowAnyMethod();
            );
        );

        services.AddAntiforgery(options =>
        
            options.HeaderName = "X-XSRF-TOKEN";
        );
        services.AddMvc(o =>
        
            o.EnableEndpointRouting = false;
            o.Conventions.Add(new ApiExplorerGroupPerVersionConvention());
            o.Filters.Add(new ModelStateFilter());
        ).AddFluentValidation(fv =>
        
            fv.RegisterValidatorsFromAssemblyContaining<CommonValidator>();
            fv.ImplicitlyValidateChildProperties = true;
        )

        .SetCompatibilityVersion(CompatibilityVersion.Version_3_0);

        #region Customise default API behavour
        services.Configure<ApiBehaviorOptions>(options =>
        
            options.SuppressModelStateInvalidFilter = true;
        );
        #endregion

        #region Versioning
        services.AddApiVersioning(o =>
        
            o.ApiVersionReader = new HeaderApiVersionReader("api-version");
            o.DefaultApiVersion = new ApiVersion(1, 0);
            o.AssumeDefaultVersionWhenUnspecified = true;
            o.ReportApiVersions = true;
        );
        #endregion

        #region Register the Swagger generator, defining 1 or more Swagger documents
        services.AddSwaggerGen(c =>
        
            c.SwaggerDoc("v1.0", new OpenApiInfo
            
                Title = Configuration.GetValue<string>("SwaggerDocOptions:Title"),
                Version = Configuration.GetValue<string>("SwaggerDocOptions:Version"),
                Description = Configuration.GetValue<string>("SwaggerDocOptions:Description")
            );

            c.OperationFilter<RemoveApiVersionFromParamsOperationFilter>();

            var basePath = PlatformServices.Default.Application.ApplicationBasePath;
            var xmlPath = Path.Combine(basePath, "foo.xml");
            c.IncludeXmlComments(xmlPath);

            var scopes = Configuration.GetValue<string>("IdentityServerOptions:RequiredScopes").Split(',').ToList();
            var scopesDictionary = new Dictionary<string, string>();
            foreach (var scope in scopes)
            
                scopesDictionary.Add(scope, scope);
            

            c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
            
                Name = "Authorization",
                Type = SecuritySchemeType.OAuth2,
                Scheme = "Bearer",
                Flows = new OpenApiOAuthFlows
                
                    Password = new OpenApiOAuthFlow
                    
                        TokenUrl = new Uri(Configuration.GetValue<string>("IdentityServerOptions:TokenEndpoint")),
                        Scopes = scopesDictionary
                    
                ,
                In = ParameterLocation.Header
            );

            c.AddSecurityRequirement(new OpenApiSecurityRequirement
            
                
                    new OpenApiSecurityScheme
                    
                        Reference = new OpenApiReference
                        
                            Type = ReferenceType.SecurityScheme,
                            Id = "Bearer"
                        ,
                        Type = SecuritySchemeType.Http,
                        Scheme = "Bearer",
                        Name = "Bearer",
                        In = ParameterLocation.Header
                    ,
                    new List<string>()
                
            );
        );
        #endregion
    

    /// <summary>
    /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    /// </summary>
    /// <param name="app">Application builder</param>
    /// <param name="env">Web host environment</param>
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    
        if (env.IsDevelopment())
        
            app.UseDeveloperExceptionPage();
        
        else
        
            app.UseHsts();
        


        app.UseCors(_allowedOrigins);
        app.UseAuthentication();
        app.UseSerilogRequestLogging();
        app.UseMvc(routes =>
        
            routes.MapRoute(
                name: "default",
                template: "controller=Home/action=Index/id?");
        );

        app.UseSwagger(
            o =>
            
                o.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
                
                    var paths = new OpenApiPaths();
                    foreach (var x in swaggerDoc.Paths)
                    
                        var key = x.Key.Contains("version") ? x.Key.Replace("version", swaggerDoc.Info.Version) : x.Key;
                        paths.Add(key, x.Value);
                    
                    swaggerDoc.Paths = paths;
                    swaggerDoc.Extensions.Add(
                        new KeyValuePair<string,
                        IOpenApiExtension>("x-identity-authority",
                        new OpenApiString(Configuration.GetValue<string>("IdentityServerOptions:Authority"))));
                );
                o.RouteTemplate = "docs/documentName/swagger.json";
            );
        app.UseSwaggerUI(
            c =>
            
                c.SwaggerEndpoint("/docs/v1.0/swagger.json", "Foo API");
                c.OAuthClientId(Configuration.GetValue<string>("IdentityServerOptions:ClientName"));
                c.OAuthClientSecret(Configuration.GetValue<string>("IdentityServerOptions:ClientSecret"));
            
        );
    

现在让我们看一下获取访问令牌的过程:

当我按下“授权”时,它正在验证并获得一个令牌:

但是当我尝试访问需要授权的 API 资源时,它返回 401 错误:

我尝试在 Postman 中检查相同的内容,当我尝试访问令牌端点时,它会返回这样的访问令牌:

我已经工作了几个小时,尝试了很多东西,但都没有奏效。我试图提供可能导致此问题的所有内容,任何帮助将不胜感激,在此先感谢。

【问题讨论】:

IdentityServerOptions:ApiName 是什么并检查访问令牌的受众,使用 jwt.io 解码令牌。 我更改了客户端配置 int Identity Server 以便现在它返回 JWT 令牌,但现在我遇到了另一个问题:www-authenticate: Bearer error="invalid_token", error_description="观众'clientapi' 无效”。 我不知道为什么这是无效的,你对此有什么想法吗? clientapi 应该是您访问令牌中的受众之一,这意味着您应该在获取令牌时将clientapi 添加到范围。 好的,'clientapi' 只是一个通用名称,这实际上是上图中显示的范围之一,具体来说是 innovatorsnotificationsender。我在我需要的范围内有这个,它是在执行此代码时从 appsettings 中检索的:var scopes = Configuration.GetValue&lt;string&gt;("IdentityServerOptions:RequiredScopes").Split(',').ToList(); 然后它被添加到范围中,所以我不知道为什么它无效。跨度> 访问令牌的受众是什么?是否包括innovatorsnotificationsender 【参考方案1】:

经过研究发现,ApiName 必须与受众的名字相同,而且我们应该为客户端配置 JWT 令牌,而不是引用令牌。

【讨论】:

以上是关于.NET Core 3.1 IdentityServer4:使用资源所有者密码凭据授予时获取无效访问令牌的主要内容,如果未能解决你的问题,请参考以下文章