一个项目中的IdentityServer4和Web Api无法通过身份验证 [英] IdentityServer4 and Web Api in one project fails to authenticate

查看:671
本文介绍了一个项目中的IdentityServer4和Web Api无法通过身份验证的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直在寻找我在这里遇到的问题.我试图从SO中的问题中查找,但可以找出问题所在.所以我非常绝望的自动取款机.

所以在我的解决方案中,我们有3个项目

API

  • 生产API资源

IdentityServer4

  • IdentityServer4
  • 用于访问IdentityServ4上的客户端,范围等的WebAPI管理API

客户端APP

  • MVC应用程序

一切都很好.客户端可以通过IS4登录并进行身份验证,并访问生产资源.现在,还需要创建api来从客户端应用程序管理IS4.但似乎我无法使用IS4发行的相同令牌进行身份验证.

IS4日志上的消息如下

info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1] Route matched with {action = "GetUserAccountsList", controller = "Accounts"}. Executing action Identity.API.API.AccountsController.GetUserAccountsList (Identity.API) dbug: IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler[9] AuthenticationScheme: Bearer was not authenticated. info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2] Authorization failed. info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3] Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'. info: Microsoft.AspNetCore.Mvc.ChallengeResult[1] Executing ChallengeResult with authentication schemes (Bearer). info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[12] AuthenticationScheme: BearerIdentityServerAuthenticationJwt was challenged. info: IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler[12] AuthenticationScheme: Bearer was challenged. info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2] Executed action Identity.API.API.AccountsController.GetUserAccountsList (Identity.API) in 0.212ms info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] Request finished in 0.6503ms 401

IS4 Web API上的API代码

[Authorize(AuthenticationSchemes = "Bearer")]
    [HttpGet]
    public async Task<IActionResult> GetUserAccountsList()
    {
        var userAccounts = await _accountService.GetIdentityAccountsAsync();
        return new JsonResult(userAccounts);
    }

在启动时 ConfigureServices

 public void ConfigureServices(IServiceCollection services)
    {
        var dbConnectionName = Constants.Environment.Development;
        if (_env.IsProduction())
        {
            dbConnectionName = Constants.Environment.Production;
        }

        services.AddDbContext<ApplicationDbContext>(options =>
       options.UseSqlServer(Configuration.GetConnectionString(dbConnectionName), sqlServerOptionsAction: sqlOptions =>
       {
           sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
       }));

        services.AddIdentity<ApplicationUser, IdentityRole>()
            //  use this if we want to implement default ASP.NET identity
            //services.AddDefaultIdentity<ApplicationUser>()
            .AddRoles<IdentityRole>()
            .AddRoleManager<RoleManager<IdentityRole>>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

        // Configure DI
        ConfigureDependencies(services);
        services.AddMvc();

        #region Registering ASP.NET Identity Server

        var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
        services.AddIdentityServer(options =>
        {
            options.IssuerUri = Constants.Address.GetIdentityServerAdress(_env.IsDevelopment());
            options.Authentication.CookieLifetime = TimeSpan.FromHours(2);
        })
         // change to certificate credentials on production
         // .AddSigningCredential(Certificate.Get())
         .AddDeveloperSigningCredential()
         .AddAspNetIdentity<ApplicationUser>()

        //// this adds the config data from DB (clients, resources) instead of memory
        //.AddInMemoryIdentityResources(Config.GetIdentityResources())
        //.AddInMemoryClients(Config.GetClients(_env.IsProduction()))
        //.AddInMemoryApiResources(Config.GetApiResources())

        .AddConfigurationStore(options =>
        {
            options.ConfigureDbContext = builder =>
                builder.UseSqlServer(Configuration.GetConnectionString(dbConnectionName),
                    sql => sql.MigrationsAssembly(migrationsAssembly));
        })

        //// this adds the operational data from DB (codes, tokens, consents)
        //.AddInMemoryPersistedGrants()
        .AddOperationalStore(options =>
        {
            options.ConfigureDbContext = builder =>
                builder.UseSqlServer(Configuration.GetConnectionString(dbConnectionName),
                    sql => sql.MigrationsAssembly(migrationsAssembly));

            // this enables automatic token cleanup. this is optional.
            options.EnableTokenCleanup = true;
            options.TokenCleanupInterval = 30;
        })
        .AddProfileService<IdentityProfileService>(); ;

        #endregion Registering ASP.NET Identity Server

        services.RegisterApplicationPolicy();

        #region External Auth

        services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
            .AddIdentityServerAuthentication(options =>
            {
                options.Authority = Constants.Address.GetIdentityServerAdress(_env.IsDevelopment());
                options.RequireHttpsMetadata = false;
                options.ApiName = Constants.Resource.Identity;
                // options.SupportedTokens = SupportedTokens.Both;
            }); ;

        #endregion External Auth
    }

Startup.cs 配置

public void Configure(IApplicationBuilder app, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager)
    {
        InitializeDatabase(app, _env, userManager, roleManager);

        if (_env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }
        app.UseIdentityServer();
        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseCookiePolicy();
        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }

将IS4上的客户端播种到数据库并按以下步骤进行设置

        public static class Resource
    {
        public static List<string> GetAllResourceList()
        {
            return new List<string>()
            {
                Clinic,
                Subscription,
                Module,
                Identity // this is IDS4 Server
            };
        }

        // this is used on db seed only.
        // resource name has to be updated on DB after db seed
        public const string Clinic = "Clinic";
        public const string Subscription = "Subscription";
        public const string Module = "Module";
        public const string Identity = "Identity";

        public const string ClinicAddress = "http://localhost:5100";
        public const string SubscriptionAddress = "http://localhost:5200";
        public const string ModuleAddress = "http://localhost:5300";
        public const string IdentityAddress = "http://localhost:5000";
    }

 public class Config
{
    public static IEnumerable<ApiResource> GetApiResources()
    {
        return Constants.Resource.GetAllResourceList().Select(s => new ApiResource(s));
    }

    // client want to access resources (aka scopes)
    public static IEnumerable<Client> GetClients(bool isDevelopment)
    {
        var client = new List<Client>();
        var mvcClient = new Client
        {
            ClientId = "mvc",
            ClientName = "MVC Client",
            AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
            RequireConsent = false,
            ClientSecrets =
            {
                new Secret("secret".Sha256())
            },
            RedirectUris = { $"{Constants.Address.GetClientServerAdress(isDevelopment)}/signin-oidc" },
            PostLogoutRedirectUris =
                {$"{Constants.Address.GetClientServerAdress(isDevelopment)}/signout-callback-oidc"},
            AlwaysIncludeUserClaimsInIdToken = true,
            AllowAccessTokensViaBrowser = true,
            AllowedScopes =
            {
                IdentityServerConstants.StandardScopes.OpenId,
                IdentityServerConstants.StandardScopes.Profile,
            },
            AllowOfflineAccess = true
        };
        foreach (var resource in Constants.Resource.GetAllResourceList())
        {
            mvcClient.AllowedScopes.Add(resource);
        }
        client.Add(mvcClient);
        return client;
    }

    //Add support for the standard openid (subject id) and profile scopes
    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
        };
    }
}

在客户端应用上 Startup.cs如下

        public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

        // Adding Authentication options+

        services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie("Cookies")
            .AddOpenIdConnect("oidc", options =>
            {
                options.SignInScheme = "Cookies";
                options.Authority = $"{Constants.Address.GetIdentityServerAdress(_env.IsDevelopment())}";
                options.ClientId = "mvc";
                options.ClientSecret = "secret";
                options.ResponseType = "code id_token";

                options.SaveTokens = true;
                options.GetClaimsFromUserInfoEndpoint = true;
                options.RequireHttpsMetadata = false;

                foreach (var resource in Constants.Resource.GetAllResourceList())
                {
                    options.Scope.Add(resource);
                }
                options.Scope.Add("offline_access");
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name",
                    RoleClaimType = "role",
                };
            });
        // Adding Authorisation
        services.RegisterApplicationPolicy();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }
        app.UseAuthentication();
        app.UseStaticFiles();
        app.UseMvcWithDefaultRoute();
    }

我们尝试使用以下代码访问IS4上的Web Api.看到下面的调用总是返回401 Unauthorize

  var accessToken = await HttpContext.GetTokenAsync("access_token");

        var client = new HttpClient();
        client.SetBearerToken(accessToken);

        var response = await client.GetAsync($"{Constants.Address.GetIdentityServerAdress(_env.IsDevelopment())}/api/Accounts/GetUserAccountsList");
        if (response.IsSuccessStatusCode)
        {
            var content = await response.Content.ReadAsStringAsync();
            ViewBag.Json = JArray.Parse(content).ToString();
        }
        else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            return Unauthorized();
        }
        return View("Json");

有关如何解决该问题的任何建议都将有所帮助.目前,我正在使用表单身份验证在IS4上进行客户端管理.

解决方案

在使用提琴手检查已发送的请求之后,我终于找出了请求的原因.范围和资源设置正确.原因是IdentityServer身份验证正在比较令牌发行者.

客户的两个部门和身份指向IDS http:localhost:5000而不是https.因此令牌发行者设置为http.所以我只需要将权限更改为https.我的一个愚蠢的错误=).

由于某些原因,WebApi在IDS和Resource APIS上的authorize属性在IDS检查颁发者和资源不起作用的情况下表现不同.必须在这个问题上做更多的研究.

im stuck on finding the issue i have here. i have tried to find from questions in SO but could figure out the problem. so im quite desperate atm.

so in my solutions we have 3 projects

API

  • Production API Resource

IdentityServer4

  • IdentityServer4
  • WebAPI Management API to access Client, Scopes, Etc on IdentityServ4

Client APP

  • MVC App

Everything went fine. Client can login and authenticate via IS4 and access production resources. there is now comes a need to also create api to manage IS4 from the client app as well. but it seems that i cant Authenticate using the same token issued by the IS4.

the message on IS4 Log is as follows

info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1] Route matched with {action = "GetUserAccountsList", controller = "Accounts"}. Executing action Identity.API.API.AccountsController.GetUserAccountsList (Identity.API) dbug: IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler[9] AuthenticationScheme: Bearer was not authenticated. info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2] Authorization failed. info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3] Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'. info: Microsoft.AspNetCore.Mvc.ChallengeResult[1] Executing ChallengeResult with authentication schemes (Bearer). info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[12] AuthenticationScheme: BearerIdentityServerAuthenticationJwt was challenged. info: IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler[12] AuthenticationScheme: Bearer was challenged. info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2] Executed action Identity.API.API.AccountsController.GetUserAccountsList (Identity.API) in 0.212ms info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] Request finished in 0.6503ms 401

API Code on IS4 Web API

[Authorize(AuthenticationSchemes = "Bearer")]
    [HttpGet]
    public async Task<IActionResult> GetUserAccountsList()
    {
        var userAccounts = await _accountService.GetIdentityAccountsAsync();
        return new JsonResult(userAccounts);
    }

And On StartUp ConfigureServices

 public void ConfigureServices(IServiceCollection services)
    {
        var dbConnectionName = Constants.Environment.Development;
        if (_env.IsProduction())
        {
            dbConnectionName = Constants.Environment.Production;
        }

        services.AddDbContext<ApplicationDbContext>(options =>
       options.UseSqlServer(Configuration.GetConnectionString(dbConnectionName), sqlServerOptionsAction: sqlOptions =>
       {
           sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
       }));

        services.AddIdentity<ApplicationUser, IdentityRole>()
            //  use this if we want to implement default ASP.NET identity
            //services.AddDefaultIdentity<ApplicationUser>()
            .AddRoles<IdentityRole>()
            .AddRoleManager<RoleManager<IdentityRole>>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

        // Configure DI
        ConfigureDependencies(services);
        services.AddMvc();

        #region Registering ASP.NET Identity Server

        var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
        services.AddIdentityServer(options =>
        {
            options.IssuerUri = Constants.Address.GetIdentityServerAdress(_env.IsDevelopment());
            options.Authentication.CookieLifetime = TimeSpan.FromHours(2);
        })
         // change to certificate credentials on production
         // .AddSigningCredential(Certificate.Get())
         .AddDeveloperSigningCredential()
         .AddAspNetIdentity<ApplicationUser>()

        //// this adds the config data from DB (clients, resources) instead of memory
        //.AddInMemoryIdentityResources(Config.GetIdentityResources())
        //.AddInMemoryClients(Config.GetClients(_env.IsProduction()))
        //.AddInMemoryApiResources(Config.GetApiResources())

        .AddConfigurationStore(options =>
        {
            options.ConfigureDbContext = builder =>
                builder.UseSqlServer(Configuration.GetConnectionString(dbConnectionName),
                    sql => sql.MigrationsAssembly(migrationsAssembly));
        })

        //// this adds the operational data from DB (codes, tokens, consents)
        //.AddInMemoryPersistedGrants()
        .AddOperationalStore(options =>
        {
            options.ConfigureDbContext = builder =>
                builder.UseSqlServer(Configuration.GetConnectionString(dbConnectionName),
                    sql => sql.MigrationsAssembly(migrationsAssembly));

            // this enables automatic token cleanup. this is optional.
            options.EnableTokenCleanup = true;
            options.TokenCleanupInterval = 30;
        })
        .AddProfileService<IdentityProfileService>(); ;

        #endregion Registering ASP.NET Identity Server

        services.RegisterApplicationPolicy();

        #region External Auth

        services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
            .AddIdentityServerAuthentication(options =>
            {
                options.Authority = Constants.Address.GetIdentityServerAdress(_env.IsDevelopment());
                options.RequireHttpsMetadata = false;
                options.ApiName = Constants.Resource.Identity;
                // options.SupportedTokens = SupportedTokens.Both;
            }); ;

        #endregion External Auth
    }

Startup.cs Configure

public void Configure(IApplicationBuilder app, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager)
    {
        InitializeDatabase(app, _env, userManager, roleManager);

        if (_env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }
        app.UseIdentityServer();
        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseCookiePolicy();
        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }

Client on IS4 is seeded to DB and set up as follows

        public static class Resource
    {
        public static List<string> GetAllResourceList()
        {
            return new List<string>()
            {
                Clinic,
                Subscription,
                Module,
                Identity // this is IDS4 Server
            };
        }

        // this is used on db seed only.
        // resource name has to be updated on DB after db seed
        public const string Clinic = "Clinic";
        public const string Subscription = "Subscription";
        public const string Module = "Module";
        public const string Identity = "Identity";

        public const string ClinicAddress = "http://localhost:5100";
        public const string SubscriptionAddress = "http://localhost:5200";
        public const string ModuleAddress = "http://localhost:5300";
        public const string IdentityAddress = "http://localhost:5000";
    }

 public class Config
{
    public static IEnumerable<ApiResource> GetApiResources()
    {
        return Constants.Resource.GetAllResourceList().Select(s => new ApiResource(s));
    }

    // client want to access resources (aka scopes)
    public static IEnumerable<Client> GetClients(bool isDevelopment)
    {
        var client = new List<Client>();
        var mvcClient = new Client
        {
            ClientId = "mvc",
            ClientName = "MVC Client",
            AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
            RequireConsent = false,
            ClientSecrets =
            {
                new Secret("secret".Sha256())
            },
            RedirectUris = { $"{Constants.Address.GetClientServerAdress(isDevelopment)}/signin-oidc" },
            PostLogoutRedirectUris =
                {$"{Constants.Address.GetClientServerAdress(isDevelopment)}/signout-callback-oidc"},
            AlwaysIncludeUserClaimsInIdToken = true,
            AllowAccessTokensViaBrowser = true,
            AllowedScopes =
            {
                IdentityServerConstants.StandardScopes.OpenId,
                IdentityServerConstants.StandardScopes.Profile,
            },
            AllowOfflineAccess = true
        };
        foreach (var resource in Constants.Resource.GetAllResourceList())
        {
            mvcClient.AllowedScopes.Add(resource);
        }
        client.Add(mvcClient);
        return client;
    }

    //Add support for the standard openid (subject id) and profile scopes
    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
        };
    }
}

On Client App Startup.cs is as follows

        public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

        // Adding Authentication options+

        services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie("Cookies")
            .AddOpenIdConnect("oidc", options =>
            {
                options.SignInScheme = "Cookies";
                options.Authority = $"{Constants.Address.GetIdentityServerAdress(_env.IsDevelopment())}";
                options.ClientId = "mvc";
                options.ClientSecret = "secret";
                options.ResponseType = "code id_token";

                options.SaveTokens = true;
                options.GetClaimsFromUserInfoEndpoint = true;
                options.RequireHttpsMetadata = false;

                foreach (var resource in Constants.Resource.GetAllResourceList())
                {
                    options.Scope.Add(resource);
                }
                options.Scope.Add("offline_access");
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name",
                    RoleClaimType = "role",
                };
            });
        // Adding Authorisation
        services.RegisterApplicationPolicy();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }
        app.UseAuthentication();
        app.UseStaticFiles();
        app.UseMvcWithDefaultRoute();
    }

where we are trying to access Web Api on IS4 with the following code. See that the following call always return 401 Unauthorize

  var accessToken = await HttpContext.GetTokenAsync("access_token");

        var client = new HttpClient();
        client.SetBearerToken(accessToken);

        var response = await client.GetAsync($"{Constants.Address.GetIdentityServerAdress(_env.IsDevelopment())}/api/Accounts/GetUserAccountsList");
        if (response.IsSuccessStatusCode)
        {
            var content = await response.Content.ReadAsStringAsync();
            ViewBag.Json = JArray.Parse(content).ToString();
        }
        else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            return Unauthorized();
        }
        return View("Json");

Any advice on how to fix the problem will be helpfull. at the moment im using form authentication to do client management on IS4.

解决方案

I finally figure out the cause of the request after checking the sent request using fiddler. the scope and resources setting was correct. The cause is identityServer authentication is comparing the token issuer.

both authority on client & identity is pointing to IDS http:localhost:5000 instead of https. therefore the token issuer is set as http. so i only need to change the authority to https. what a silly mistake of mine =).

for some reason WebApi's authorize attribute on IDS and Resource APIS behave differently where IDS checking issuer and resources doest. have to do more research on this issue.

这篇关于一个项目中的IdentityServer4和Web Api无法通过身份验证的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆