带有IdentityServer4 Cookie和基于API JWT的身份验证的.Net Core 2.1用于同一应用

时间:2019-01-07 21:25:41

标签: .net reactjs authentication cookies identityserver4

我有一个IdentityServer设置,带有一个API和一个用React编写的SPA。 SPA使用javascript oidc客户端库向IdentityServer进行身份验证,然后从API获取数据。 SPA和API在同一个项目中,SPA使用services.AddSpaStaticFiles和app.UseSpa提供服务,所以我认为我应该可以互换使用两种身份验证方案。

问题是我有图像存储在API端,我希望SPA客户端能够在标记中获取并放置图像,并可以选择单击并在新的图像中打开完整尺寸的图像窗口。图像必须要求用户经过身份验证才能访问它们。

我尝试将基于Cookie的身份验证添加到API的ConfigureServices中,希望让用户在SPA上进行身份验证,然后访问API上的图片URL即可。

services.AddAuthentication(options => 
{ 
    options.DefaultScheme = "Cookies";
    options.DefaultChallengeScheme = "oidc";
})
.AddIdentityServerAuthentication("apiAuth", options =>
{
    options.Authority = "http://localhost:5000";
    options.RequireHttpsMetadata = false;
    options.ApiName = "api";
})
.AddCookie("cookieAuth")
.AddOpenIdConnect("oidc", options =>
{
    options.Authority = "http://localhost:5000";
    options.RequireHttpsMetadata = false;
    options.ClientId = "afx_api";
    options.SaveTokens = true;
 });

然后在将返回图像的控制器上添加[Authorize(AuthenticationSchemes =“ cookieAuth”)),并在所有其他API控制器上添加[Authorize(AuthenticationSchemes =“ apiAuth”)]。

但是,当我尝试访问图像时,比如说http://localhost:6000/api/file/1,即使我已经通过身份验证并且正常的API调用也可以重定向到http://localhost:6001/Account/Login?ReturnUrl=%2Fapi%2Fdocuments%2Ffile%2F90404

我将如何去做?谢谢

编辑:我设置中的更多代码

IdentityServer / Config.cs客户端配置

new Client
{
    ClientId = "client",
    ClientName = "React Client",
    AllowedGrantTypes = GrantTypes.Implicit,
    AllowAccessTokensViaBrowser = true,
    RequireConsent = false,
    AccessTokenLifetime = 3600,

    RedirectUris = {
        "http://localhost:6001/callback",
        "http://localhost:6001/silent_renew.html",
    },
    PostLogoutRedirectUris =
    {
        "http://localhost:6001/",
    },
    AllowedCorsOrigins =
    {
        "http://localhost:6001",
    },

    AllowedScopes = new List<string>
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
        "api"
    },
    AlwaysIncludeUserClaimsInIdToken = true
}

ClientApp / src / userManager.js

import { createUserManager } from 'redux-oidc';

const settings = {
    client_id: 'client',
    redirect_uri: `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}/callback`,
    response_type: 'id_token token',
    scope:"openid profile api",
    authority: 'http://localhost:5000',
    silent_redirect_uri: `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}/silent_renew.html`,
    automaticSilentRenew: true,
    loadUserInfo: true,
    monitorSession: true
};

const userManager = createUserManager(settings);

export default userManager;

基于Elrashid的答案的新创业公司:

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
    {
        options.DefaultScheme = "cookies";
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie("cookies",
                    options => options.ForwardDefaultSelector = ctx => ctx.Request.Path.StartsWithSegments("/api") ? "jwt" : "cookies")
    .AddJwtBearer("jwt", options =>
    {
        options.Authority = "http://localhost:5000";
        options.Audience = "api";
        options.RequireHttpsMetadata = false;
    })
    .AddOpenIdConnect("oidc", options =>
    {
        options.SignInScheme = "cookies";
        options.Authority = "http://localhost:5000";
        options.RequireHttpsMetadata = false;
        options.ClientId = "client";
        options.SaveTokens = true;

        options.ResponseType = "id_token token";
        options.GetClaimsFromUserInfoEndpoint = true;
        options.Scope.Add("api");
        options.Scope.Add("offline_access");
        options.ForwardDefaultSelector = ctx => ctx.Request.Path.StartsWithSegments("/api") ? "jwt" : "oidc";
    });

仍然得到相同的结果,当我尝试从控制器获取图像时,这是重定向到/ Account / Login?ReturnUrl =%2Fimages%2Ffile%2F90404。该API仍然有效。控制器方法返回一个PNG,并且无需授权即可使用。

[Authorize(AuthenticationSchemes = "cookies")]
[Route("[controller]")]
public class ImagesController : Controller
{
    ...
}

2 个答案:

答案 0 :(得分:1)

使用ForwardDefaultSelector

  • 所有路线都将使用cookie

  • 但以 / api 开头的路由将使用jwt

    ctx.Request.Path.StartsWithSegments("/api") ? "jwt" : "cookies")
    
    ctx.Request.Path.StartsWithSegments("/api") ? "jwt" : "oidc")
    

如果您有2条路线

- localhost/Secure/Index

- localhost/api/secure/Get

SecureController

public class SecureController : Controller
{
    [Authorize]
    public IActionResult Index()
    {
        return View();
    }
}

SecureApi

[Route("api/secure")]
[Authorize]
public class SecureApi : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
    }
}

清除Jwt安全令牌处理程序

JwtSecurityTokenHandler
   .DefaultInboundClaimTypeMap.Clear();

使用AddAuthentication将cookie设置为DefaultScheme

services.AddAuthentication(options =>
        {
            // Notice the schema name is case sensitive [ cookies != Cookies ]
            options.DefaultScheme = "cookies";
            options.DefaultChallengeScheme = "oidc";
        })

添加Cookie选项

    .AddCookie("cookies", options => 
     options.ForwardDefaultSelector = ctx => 
      ctx.Request.Path.StartsWithSegments("/api") ? "jwt" : "cookies")

添加Jwt Bearer

   .AddJwtBearer("jwt", options =>
    {
        options.Authority = "http://localhost:5010";
        options.Audience = "app2api";
        options.RequireHttpsMetadata = false;
    })

AddOpenIdConnect

        .AddOpenIdConnect("oidc", options =>
    {
        options.SignInScheme = "cookies";
        options.Authority = "http://localhost:5010";
        options.RequireHttpsMetadata = false;
        options.ClientId = "mvc";
        options.SaveTokens = true;

        options.ClientSecret = "secret";
        options.ResponseType = "code id_token";
        options.GetClaimsFromUserInfoEndpoint = true;
        options.Scope.Add("app2api");
        options.Scope.Add("offline_access");
        //https://github.com/leastprivilege/AspNetCoreSecuritySamples/blob/aspnetcore21/OidcAndApi/src/AspNetCoreSecurity/Startup.cs
        options.ForwardDefaultSelector = ctx => ctx.Request.Path.StartsWithSegments("/api") ? "jwt" : "oidc";
    });

完整的Startup.cs

 public class Startup
{


    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<CookiePolicyOptions>(options =>
        {
            // This lambda determines whether user consent for non-essential cookies is needed for a given request.
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });

        ////////////////////////////////
        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
        services.AddAuthentication(options =>
        {
            // Notice the schema name is case sensitive [ cookies != Cookies ]
            options.DefaultScheme = "cookies";
            options.DefaultChallengeScheme = "oidc";
        })

        .AddCookie("cookies", options => options.ForwardDefaultSelector = ctx => ctx.Request.Path.StartsWithSegments("/api") ? "jwt" : "cookies")
        .AddJwtBearer("jwt", options =>
        {
            options.Authority = "http://localhost:5010";
            options.Audience = "app2api";
            options.RequireHttpsMetadata = false;
        })
        .AddOpenIdConnect("oidc", options =>
        {
            options.SignInScheme = "cookies";
            options.Authority = "http://localhost:5010";
            options.RequireHttpsMetadata = false;
            options.ClientId = "mvc";
            options.SaveTokens = true;

            options.ClientSecret = "secret";
            options.ResponseType = "code id_token";
            options.GetClaimsFromUserInfoEndpoint = true;
            options.Scope.Add("app2api");
            options.Scope.Add("offline_access");

            options.ForwardDefaultSelector = ctx => ctx.Request.Path.StartsWithSegments("/api") ? "jwt" : "oidc";
        });
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    }

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

        app.UseAuthentication();
        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseCookiePolicy();

        app.UseMvcWithDefaultRoute();
    }
}

在身份服务器中使用HybridAndClientCredentials

    new Client
    {
        ClientId = "mvc",
        ClientName = "MVC Client",
        AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
        ClientSecrets =
        {
            new Secret("secret".Sha256())
        },
        // where to redirect to after login
        RedirectUris = { "http://localhost:5011/signin-oidc" },
        // where to redirect to after logout
        PostLogoutRedirectUris = { "http://localhost:5011/signout-callback-oidc" },
        AllowedScopes = new List<string>
        {
            IdentityServerConstants.StandardScopes.OpenId,
            IdentityServerConstants.StandardScopes.Profile,
            "app2api"
        },
        AllowOfflineAccess = true
    }

摘要:我的Github上的示例应用程序

答案 1 :(得分:0)

@MichaelD, 我可以在您的帖子稍加修改后在我的项目上修复此问题。我通过这篇文章https://docs.microsoft.com/en-us/aspnet/core/security/authentication/scaffold-identity?view=aspnetcore-3.1&tabs=visual-studio#scaffold-identity-into-an-mvc-project-with-authorization架设了Identity。您需要选择Login.cshtml文件进行覆盖。完成此操作后,您应该在项目中获得一个Areas / Identity / Pages / Account / Manage / Login.cshtml.cs文件。打开它并转到方法OnPostAsync。

    public async Task<IActionResult> OnPostAsync(string returnUrl = null)
    {
        returnUrl = returnUrl ?? Url.Content("~/");

        if (ModelState.IsValid)
        {
            // This doesn't count login failures towards account lockout
            // To enable password failures to trigger account lockout, set lockoutOnFailure: true
            var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
            if (result.Succeeded)
            {
                await AddAuthorizationCookie();
                _logger.LogInformation("User logged in.");
                return LocalRedirect(returnUrl);
            }
            if (result.RequiresTwoFactor)
            {
                return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
            }
            if (result.IsLockedOut)
            {
                _logger.LogWarning("User account locked out.");
                return RedirectToPage("./Lockout");
            }
            else
            {
                ModelState.AddModelError(string.Empty, "Invalid login attempt.");
                return Page();
            }
        }

        // If we got this far, something failed, redisplay form
        return Page();
    }

    private async Task AddAuthorizationCookie()
    {
        var user = await _userManager.FindByEmailAsync(Input.Email);
        if (user == null)
        {
            return;
        }
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, user.Email),
            new Claim("FullName", user.FullName)
        };

        var claimsIdentity = new ClaimsIdentity(
            claims, "cookies");

        var authProperties = new AuthenticationProperties
        {
            //AllowRefresh = <bool>,
            // Refreshing the authentication session should be allowed.

            //ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10),
            // The time at which the authentication ticket expires. A 
            // value set here overrides the ExpireTimeSpan option of 
            // CookieAuthenticationOptions set with AddCookie.

            //IsPersistent = true,
            // Whether the authentication session is persisted across 
            // multiple requests. When used with cookies, controls
            // whether the cookie's lifetime is absolute (matching the
            // lifetime of the authentication ticket) or session-based.

            //IssuedUtc = <DateTimeOffset>,
            // The time at which the authentication ticket was issued.

            //RedirectUri = <string>
            // The full path or absolute URI to be used as an http 
            // redirect response value.
        };

        await HttpContext.SignInAsync(
            "cookies",
            new ClaimsPrincipal(claimsIdentity),
            authProperties);
    }

OnPostAsync方法来自Identity,但是我添加了“ await AddAuthorizationCookie()”行,该行允许我们向客户端添加所需的cookie。现在,当我访问图像控制器时,将显示正确的cookie。您还应该在注销时以及可能在更新令牌时删除cookie。我还没有更新令牌。