【问题标题】:IdentityServer4, local API, external REST clientIdentityServer4、本地 API、外部 REST 客户端
【发布时间】:2020-12-10 15:57:17
【问题描述】:

我正在使用 IdentityServer4 NuGet 包在我的 asp.net 核心服务器应用程序中设置 IdentityServer。 同一个应用程序托管一个受保护的 API,在通过服务器应用程序的 IdentityServer 部分进行身份验证后,应该可以通过 REST 客户端访问该 API。 我在服务器上的本地认证/授权有效。客户端也能够从服务器获取有效令牌。但是在服务器上调用 API 时出现“未授权”错误,并且 API 返回登录表单。 有没有人观察到同样的情况?出了什么问题?

运行 IdentityServer 和 API 的 ServerApp

        public void ConfigureServices(IServiceCollection services)
    {
        services.AddLogging(loggingBuilder =>
        {
            loggingBuilder.AddConsole().AddFilter(DbLoggerCategory.Database.Command.Name, LogLevel.Information);
            loggingBuilder.AddDebug();
        });

        // For Entity Framework  
        services.AddDbContextPool<ApplicationDbContext>(options =>
        {
            options.UseLazyLoadingProxies()
                .UseMySql(Configuration.GetConnectionString("ConnStr"), MySQLServerVersion.ServerVersion);
            options.EnableSensitiveDataLogging();
        });

        services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();
        //.AddDefaultUI();

        services.AddTransient<IEmailSender, EmailSender>();

        services.AddControllersWithViews();
        services.AddRazorPages().AddRazorRuntimeCompilation();

        var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
        // configure identity server with mysql stores, keys, clients and scopes
        var builder = services.AddIdentityServer(options =>
            {
                options.Events.RaiseErrorEvents = true;
                options.Events.RaiseInformationEvents = true;
                options.Events.RaiseFailureEvents = true;
                options.Events.RaiseSuccessEvents = true;

                // see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
                options.EmitStaticAudienceClaim = true;
            })
            .AddAspNetIdentity<ApplicationUser>()
            // this adds the config data from DB (clients, resources)
            .AddConfigurationStore(options =>
            {
                options.ConfigureDbContext = b =>
                    b.UseMySql(Configuration.GetConnectionString("ConnStr"), MySQLServerVersion.ServerVersion,
                        sql => sql.MigrationsAssembly(migrationsAssembly));
            })
            // this adds the operational data from DB (codes, tokens, consents)
            .AddOperationalStore(options =>
            {
                options.ConfigureDbContext = b =>
                    b.UseMySql(Configuration.GetConnectionString("ConnStr"), MySQLServerVersion.ServerVersion,
                        sql => sql.MigrationsAssembly(migrationsAssembly));

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

        services.AddLocalApiAuthentication();
        services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
            .AddIdentityServerAuthentication(options =>
            {
                // base-address of your identityserver
                options.Authority = "https://localhost:5001";

                // name of the API resource
                options.ApiName = "api1";
            });
        services.AddAuthorization(options =>
        {
            options.AddPolicy("ApiScope", policy =>
            {
                policy.RequireAuthenticatedUser();
                policy.RequireClaim("scope", "api1");
            });
        });

        services.AddTransient<IProfileService, ProfileService>();

        // not recommended for production - you need to store your key material somewhere secure
        //builder.AddDeveloperSigningCredential();
        var fileName = Path.Combine("cert.pfx");
        if (!File.Exists(fileName))
        {
            throw new FileNotFoundException("Signing Certificate is missing!");
        }

        var cert = new X509Certificate2(fileName, "secret");
        builder.AddSigningCredential(cert);

        // Register the Swagger services
        services.AddSwaggerDocument(options =>
        {
            options.Title = "TEST";
            options.AddSecurity("oauth2", new OpenApiSecurityScheme
            {
                Type = OpenApiSecuritySchemeType.OAuth2,
                Flows = new OpenApiOAuthFlows
                {
                    AuthorizationCode = new OpenApiOAuthFlow
                    {
                        AuthorizationUrl = "https://localhost:5001/connect/authorize",
                        TokenUrl = "https://localhost:5001/connect/token",
                        Scopes = new Dictionary<string, string> { { "api1", "Demo API - full access" } }
                    }
                }
            });
            options.OperationProcessors.Add(new OperationSecurityScopeProcessor("oauth2"));
        });
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // this will do the initial DB population
        app.InitializeDatabase();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            //app.UseBrowserLink();
            app.UseDatabaseErrorPage();
            app.UseMigrationsEndPoint();
        }
        else
        {
            app.UseExceptionHandler("/Home/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();

        app.UseRouting();

        // app.UseAuthentication(); // not needed, since UseIdentityServer adds the authentication middleware
        app.UseIdentityServer();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
            endpoints.MapRazorPages();
        });

        // Register the Swagger generator and the Swagger UI middlewares
        app.UseOpenApi();
        app.UseSwaggerUi3(options =>
        {
            options.DocumentTitle = "DEMO";
            options.OAuth2Client = new OAuth2ClientSettings
            {
                ClientId = "demo_api_swagger",
                AppName = "Demo API - Swagger",
                UsePkceWithAuthorizationCodeGrant = true
            };
        });
    }

Config.cs

public static class Config
{
    public static IEnumerable<IdentityResource> IdentityResources =>
        new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile()
        };


    public static IEnumerable<ApiScope> ApiScopes =>
        new List<ApiScope>
        {
            new ApiScope("api1", "My API")
        };

    public static IEnumerable<ApiResource> ApiResources =>
        new List<ApiResource>
        {
            new ApiResource("api1", "My API")
        };

    public static IEnumerable<Client> Clients =>
        new List<Client>
        {
            // machine to machine client
            new Client
            {
                ClientId = "client",
                ClientSecrets = {new Secret("secret".Sha256())},

                AllowedGrantTypes = GrantTypes.ClientCredentials,
                // scopes that client has access to
                AllowedScopes = {"api1"}
            },

            // external user login
            new Client
            {
                ClientId = "external",
                ClientSecrets = {new Secret("secret".Sha256())},

                AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                // scopes that client has access to
                AllowedScopes = {"api1"}
            },

            // interactive ASP.NET Core MVC client
            new Client
            {
                ClientId = "mvc",
                ClientSecrets = {new Secret("secret".Sha256())},

                AllowedGrantTypes = GrantTypes.Code,

                // where to redirect to after login
                RedirectUris =
                {
                    "https://localhost:5003/signin-oidc",
                },
                // where to redirect to after logout
                PostLogoutRedirectUris =
                {
                    "https://localhost:5003/signout-callback-oidc",
                },

                // scopes that client has access to
                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    "api1"
                }
            },

            //swagger
            new Client
            {
                ClientId = "demo_api_swagger",
                ClientName = "Swagger UI for demo_api",
                ClientSecrets = {new Secret("secret".Sha256())}, // change me!

                AllowedGrantTypes = GrantTypes.Code,
                RequirePkce = false,
                RequireClientSecret = false,

                // where is it allowed to redirect to after login
                RedirectUris =
                {
                    "https://localhost:5001/swagger/oauth2-redirect.html",
                    "https://localhost:5003/swagger/oauth2-redirect.html"
                },
                AllowedCorsOrigins =
                {
                    "https://localhost:5001",
                    "https://localhost:5003"
                },
                AllowedScopes = {"api1"}
            }
        };
}

服务器APP中被标记的Controller

[Authorize("ApiScope")]
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase

和客户端APP

        public void ConfigureServices(IServiceCollection services)
    {
        //Session data
        services.AddDistributedMemoryCache();
        services.AddSession(options =>
        {
            options.Cookie.Name = "MVCClient";
            options.IdleTimeout = TimeSpan.FromSeconds(10);
            options.Cookie.HttpOnly = true;
            options.Cookie.IsEssential = true;
        });

        JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
        services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie("Cookies", options =>
            {
                options.ExpireTimeSpan = new TimeSpan(12, 0, 0, 0);
            })
            .AddOpenIdConnect("oidc", options =>
            {
                options.Authority = "https://localhost:5001";

                options.ClientId = "mvc";
                options.ClientSecret = "secret";
                options.ResponseType = "code";

                options.Scope.Add("api1");

                options.GetClaimsFromUserInfoEndpoint = true;
                options.SaveTokens = true;

                options.ClaimActions.Add(new JsonKeyClaimAction(JwtClaimTypes.Role, null, "role"));
                options.ClaimActions.Add(new JsonKeyClaimAction("isactive", null, "isactive"));
                options.ClaimActions.Add(new JsonKeyClaimAction("title", null, "title"));

                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = JwtClaimTypes.Name,
                    RoleClaimType = JwtClaimTypes.Role
                };
            });

        // adds an authorization policy to make sure the token is for scope 'api1'
        services.AddAuthorization(options =>
        {
            options.AddPolicy("ApiScope", policy =>
            {
                policy.RequireAuthenticatedUser();
                policy.RequireClaim("scope", "api1");
            });
        });

        services.AddControllersWithViews();

        services.AddHttpContextAccessor();
        services.AddHttpClient("TheClient", c =>
        {
            c.BaseAddress = new Uri("https://localhost:5001");

            // access the DI container
            var serviceProvider = services.BuildServiceProvider();
            // Find the HttpContextAccessor service
            var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
            // Get the bearer token from the request context (header)
            var bearerToken = httpContextAccessor?.HttpContext?.GetTokenAsync("access_token").Result;
            c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
        });

        services.AddRazorPages().AddRazorRuntimeCompilation();
        services.RegisterDataTables();
        services.AddExpressiveAnnotations();
    }

这就是我在请求客户端应用程序(控制台应用程序)时获得的令牌的内容: TOKEN my ConsoleApp got

【问题讨论】:

  • 如果您发布 API 的启动代码和 IDS4 配置会有所帮助 - 我想这与 API 资源和范围有关,这就是为什么要求代码
  • @nahidf: 请看上面,我更新了我的帖子

标签: c# asp.net rest identityserver4


【解决方案1】:

更改 ApiResources 并在 IDS4 配置中添加范围。像这样:

public static IEnumerable<ApiResource> ApiResources =>
        new List<ApiResource>
        {
            new ApiResource("api1", "My API")
            {
               Scopes = { "api1" }
            }
        };

更改后,验证https://jwt.io/ 上的access_token,现在您应该将aud 作为api1 在令牌中

【讨论】:

  • 不幸的是,这并没有改变这种情况。我用 jwt.io 检查了我得到的令牌 - 范围声明在...
  • 你可以在这里发布你的令牌吗?还是在你原来的帖子上?
  • @nahdif:请看这里:i.stack.imgur.com/nYaUT.png
  • 您正在从数据库而不是内存中获取 ApiResource 和 Api Scope,您需要确保在我的答案中建议的 ApiResources 在数据库中。一旦有数据,您的令牌中就会有 aud=api1
  • 我在启动时播种(如果不存在)我的数据库包含这些条目...有趣的是:从 Swagger UI 调用方法有效。但不是生成的 Swagger Client,我请求一个令牌并使用提供的令牌设置 RequestHeader
【解决方案2】:
public static void InitializeDatabase(this IApplicationBuilder app)
    {
        using var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>()?.CreateScope();
        serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();

        var configurationDbContext = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
        configurationDbContext.Database.Migrate();

        if (!configurationDbContext.Clients.Any())
        {
            foreach (var client in Config.Clients)
            {
                configurationDbContext.Clients.Add(client.ToEntity());
            }
            configurationDbContext.SaveChanges();
        }

        if (!configurationDbContext.IdentityResources.Any())
        {
            foreach (var resource in Config.IdentityResources)
            {
                configurationDbContext.IdentityResources.Add(resource.ToEntity());
            }
            configurationDbContext.SaveChanges();
        }

        if (!configurationDbContext.ApiScopes.Any())
        {
            foreach (var resource in Config.ApiScopes)
            {
                configurationDbContext.ApiScopes.Add(resource.ToEntity());
            }
            configurationDbContext.SaveChanges();
        }

        if (!configurationDbContext.ApiResources.Any())
        {
            foreach (var resource in Config.ApiResources)
            {
                configurationDbContext.ApiResources.Add(resource.ToEntity());
            }
            configurationDbContext.SaveChanges();
        }

        var applicationDbContextDbContext = serviceScope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
        applicationDbContextDbContext.Database.Migrate();
        serviceScope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>().SeedRoles();
        serviceScope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>().SeedUsers();
    }

    public static void SeedRoles(this RoleManager<IdentityRole> roleManager)
    {
        if (!roleManager.RoleExistsAsync(UserRoles.Admin).Result)
        {
            var role = new IdentityRole { Name = UserRoles.Admin, NormalizedName = UserRoles.Admin.ToUpper() };
            var roleResult = roleManager.CreateAsync(role).Result;
            if (!roleResult.Succeeded)
            {
                throw new Exception(roleResult.Errors.First().Description);
            }
        }

        if (!roleManager.RoleExistsAsync(UserRoles.SuperUser).Result)
        {
            var role = new IdentityRole
            { Name = UserRoles.SuperUser, NormalizedName = UserRoles.SuperUser.ToUpper() };
            var roleResult = roleManager.CreateAsync(role).Result;
            if (!roleResult.Succeeded)
            {
                throw new Exception(roleResult.Errors.First().Description);
            }
        }

        if (!roleManager.RoleExistsAsync(UserRoles.User).Result)
        {
            var role = new IdentityRole { Name = UserRoles.User, NormalizedName = UserRoles.User.ToUpper() };
            var roleResult = roleManager.CreateAsync(role).Result;
            if (!roleResult.Succeeded)
            {
                throw new Exception(roleResult.Errors.First().Description);
            }
        }
    }

    public static void SeedUsers(this UserManager<ApplicationUser> userManager)
    {
        if (userManager.FindByNameAsync("admin").Result == null)
        {
            var user = new ApplicationUser
            { UserName = "admin", NormalizedUserName = "ADMIN", Email = "test@localhost.lan", FirstName = "X", LastName = "Y", IsActive = true };
            var result = userManager.CreateAsync(user, "secret").Result;
            if (!result.Succeeded)
            {
                throw new Exception(result.Errors.First().Description);
            }
            result = userManager.AddClaimsAsync(user, new[]
            {
                new Claim("isactive", user.IsActive.ToString()),
                new Claim(JwtClaimTypes.Name, user.UserName),
                new Claim(JwtClaimTypes.GivenName, user.FirstName),
                new Claim(JwtClaimTypes.FamilyName, user.LastName),
                new Claim(JwtClaimTypes.Email, user.Email),
                new Claim("scope", "api1")
            }).Result;
            if (!result.Succeeded)
            {
                throw new Exception(result.Errors.First().Description);
            }
            result = userManager.AddToRoleAsync(user, UserRoles.Admin).Result;
            if (!result.Succeeded)
            {
                throw new Exception(result.Errors.First().Description);
            }
        }
    }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-09-06
    • 2018-09-29
    • 1970-01-01
    • 1970-01-01
    • 2022-12-18
    • 2011-10-25
    • 2021-02-26
    • 2017-08-11
    相关资源
    最近更新 更多