ASP.NET Core SignIn(Principal,Properties,AuthenticationScheme)正在抛出无法从此端点返回授权或令牌响应

时间:2017-09-28 17:53:18

标签: asp.net-core openiddict

我的应用程序的当前后续是我从前端获取访问令牌然后使用该令牌我的后端调用外部登录提供程序以符合用户身份并从中检索额外信息登录提供商。

在这种方法中,我必须为每个客户端(web,android)设置和配置登录提供程序。我尝试在我的后端使用Microsoft.AspNetCore.Authentication.Facebook来处理facebook登录,使用我从facebook获取的声明我想用它来生成Openidic JWT令牌。一切正常,但声明

return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);

正在抛出异常

An authorization or token response cannot be returned from this endpoint.

这里是同一错误的问题,但背景不同ASP.NET Core Openiddict throws "An OpenID Connect response cannot be returned from this endpoint"

这是另一个类似方法的问题,但How to Generate AccessToken for user who is logged in with External Providers

不同

这是我的startup.cs代码

 public void ConfigureServices(IServiceCollection services)
{
    var connectionString = Configuration["ConnectionStrings:ApplicationDbContext"];

    services.AddEntityFrameworkNpgsql();

    services.AddDbContext<ApplicationDbContext>(
        opts =>
        {
            opts.UseNpgsql(connectionString, b => b.MigrationsAssembly("Data.Repository"));
            opts.UseOpenIddict();
        }
    );

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

            services.AddAuthentication().AddFacebook(facebookOptions =>
            {
                facebookOptions.AppId = "APP_ID";
                facebookOptions.AppSecret = "APP_SECRET";
                facebookOptions.SaveTokens = true;

            });

    var validIssuer = Configuration["Token:Issuer"];
    services.AddAuthentication()
        .AddJwtBearer(cfg =>
        {
            cfg.TokenValidationParameters = new TokenValidationParameters
            {
                ValidIssuer = validIssuer,
                IssuerSigningKey = securityKey,

                ValidateIssuer = !String.IsNullOrEmpty(validIssuer),
                ValidateAudience = false,
                ValidateLifetime = true,
                ValidateActor = false,
                ValidateIssuerSigningKey = true
            };
        });

    services.AddOpenIddict(options =>
    {
        // Register the Entity Framework stores.
        options.AddEntityFrameworkCoreStores<ApplicationDbContext>();
        options.AddMvcBinders();
        options.EnableTokenEndpoint("/api/account/token");
        options.UseJsonWebTokens();
        options.AllowPasswordFlow();
        options.AllowCustomFlow("urn:ietf:params:oauth:grant-type:facebook_access_token");
        options.AllowCustomFlow("urn:ietf:params:oauth:grant-type:google_access_token");
        options.AllowCustomFlow("urn:ietf:params:oauth:grant-type:microsoft_access_token");
        options.DisableHttpsRequirement();
        options.AddSigningKey(securityKey);

    });


    services.Configure<IdentityOptions>(options =>
    {

    //OpenId

        options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
        options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
        options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;

        // Password settings
        options.Password.RequireDigit = true;
        options.Password.RequiredLength = 8;
        options.Password.RequireNonAlphanumeric = false;
        options.Password.RequireUppercase = true;
        options.Password.RequireLowercase = false;

        // Lockout settings
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
        options.Lockout.MaxFailedAccessAttempts = 10;

        // User settings
        options.User.RequireUniqueEmail = true;
    });


    services.AddCors(o => o.AddPolicy("CorsPolicy", builder =>
    {
        builder.AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader();
    }));

    // Add framework services.
    services.AddMvc(options =>
    {
        options.Filters.Add(new GlobalExceptionFilter());
    });   

    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public virtual void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, RoleManager<IdentityRole> roleManager)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.UseAuthentication();
    app.UseCors("CorsPolicy");

    app.UseMvc();
}

这是处理外部登录的控制器

   [HttpGet]
    [AllowAnonymous]
    public IActionResult ExternalLogin(string provider = "Facebook", string returnUrl = null)
    {
        // Request a redirect to the external login provider.
        var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Auth", new { returnUrl });
        var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
        return Challenge(properties, provider);
    }

    [HttpGet]
    [AllowAnonymous]
    public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
    {
        if (remoteError != null)
        {
            return BadRequest("Error from external provider");
        }
        var info = await _signInManager.GetExternalLoginInfoAsync();

        if (info == null)
        {
            return BadRequest();
        }
        try
        {
            var ticket = await _accountService.TokenExchangeAsync(info);
            return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);

        }
        catch (Exception ex)
        {
            return BadRequest(new OpenIdConnectResponse
            {
                Error = OpenIdConnectConstants.Errors.ServerError,
                ErrorDescription = ex.Message
            });
        }
    }

此处处理创建用户和其他相关内容的代码由ExternalLoginCallback

调用
    public async Task<AuthenticationTicket> TokenExchangeAsync (ExternalLoginInfo info)
        {
            var claims = info.Principal.Claims;
            var profile = new Profile
            {
                email = claims.FirstOrDefault(x => x.Type == ClaimTypes.Email).Value,
                id = claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier).Value,
                name = claims.FirstOrDefault(x => x.Type == ClaimTypes.Name).Value
            };

            // Find the user
            var user = await _userManager.FindByLoginAsync(info.LoginProvider, profile.id);
            if (user == null)
            {
                if (string.IsNullOrEmpty(profile.email))
                    throw new Exception("Email is not specified in the user profile for the provider.");

                await RegisterExtenalUserAsync(profile, info.LoginProvider);

                /// Try to find the user
                user = await _userManager.FindByLoginAsync(info.LoginProvider, profile.id);
                if (user == null)
                {
                    // Return bad request if the user doesn't exist
                    throw new Exception("Invalid profile or provider.");
                }
            }
            // Create the principal
            var principal = await _signInManager.CreateUserPrincipalAsync(user);

            // Claims will not be associated with specific destinations by default, so we must indicate whether they should
            // be included or not in access and identity tokens.
            foreach (var claim in principal.Claims)
            {
                // For this sample, just include all claims in all token types.
                // In reality, claims' destinations would probably differ by token type and depending on the scopes requested.
                claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken, OpenIdConnectConstants.Destinations.IdentityToken);
            }

            // Create a new authentication ticket for the user's principal
            var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(),
               OpenIdConnectServerDefaults.AuthenticationScheme);

  //  Include resources and scopes, as appropriate
          var scope = new[]
            {
                    OpenIdConnectConstants.Scopes.OpenId,
                    OpenIdConnectConstants.Scopes.Email,
                    OpenIdConnectConstants.Scopes.Profile,
                    OpenIddictConstants.Scopes.Roles
            };

            ticket.SetScopes(scope);

            // Sign in the user
            return ticket;
        }

这是日志文件

    Project> info: Microsoft.AspNetCore.Mvc.SignInResult[1]
Project>       Executing SignInResult with authentication scheme (ASOS) and the following principal: System.Security.Claims.ClaimsPrincipal.
Project> info: Microsoft.AspNetCore.Mvc.SignInResult[1]
Project>       Executing SignInResult with authentication scheme (ASOS) and the following principal: System.Security.Claims.ClaimsPrincipal.
Project> info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Project>       Executed action Project.Controllers.AuthController.ExternalLoginCallback (Project) in 9068.4071ms
Project> info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Project>       Executed action Project.Controllers.AuthController.ExternalLoginCallback (Project) in 9068.4071ms
Project> fail: Microsoft.AspNetCore.Server.Kestrel[13]
Project>       Connection id "0HL86DQMN0Q30", Request id "0HL86DQMN0Q30:00000003": An unhandled exception was thrown by the application.
Project> System.InvalidOperationException: An authorization or token response cannot be returned from this endpoint.
Project>    at AspNet.Security.OpenIdConnect.Server.OpenIdConnectServerHandler.<SignInAsync>d__6.MoveNext()
Project> --- End of stack trace from previous location where exception was thrown ---

任何帮助将不胜感激!

1 个答案:

答案 0 :(得分:0)

您的问题实际上与the second link you posted非常相似,您看到的异常具有相同的根本原因:您尝试从OpenIddict不被视为您的令牌端点的端点返回令牌响应(即ExternalLoginCallback操作的路径与调用options.EnableTokenEndpoint("/path")时设置的路径不匹配。

出于安全原因,OpenIddict拒绝处理此类响应并抛出异常。

要解决这个问题,你必须做两件事:

使用代码或隐式代替密码流

密码流不能与交互式身份验证流程一起使用,涉及往往外部提供商(如Facebook)的往返(您可以将此流视为一个简单的API调用,它将获取您的用户名/密码并返回令牌)。

相反,请考虑使用交互式流程,如代码(针对移动应用)或隐式(针对基于浏览器的应用)。您可以在此处找到这两个流的示例:https://github.com/openiddict/openiddict-samples/tree/dev/samples

例如,要启用隐式流支持,您可以执行以下操作:

// Register the OpenIddict services.
services.AddOpenIddict(options =>
{
    // Register the Entity Framework stores.
    options.AddEntityFrameworkCoreStores<ApplicationDbContext>();

    // Register the ASP.NET Core MVC binder used by OpenIddict.
    // Note: if you don't call this method, you won't be able to
    // bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
    options.AddMvcBinders();

    // Enable the authorization endpoint.
    options.EnableAuthorizationEndpoint("/connect/authorize");

    // Enable implicit flow support.
    options.AllowImplicitFlow();

    // During development, you can disable the HTTPS requirement.
    options.DisableHttpsRequirement();

    // Register a new ephemeral key, that is discarded when the application
    // shuts down. Tokens signed using this key are automatically invalidated.
    // This method should only be used during development.
    options.AddEphemeralSigningKey();
});

移动SignIn逻辑以避免从不受OpenIddict

管理的端点调用它

由于您无法从SignIn拨打ExternalLoginCallback,因此您必须将SignIn调用移至授权端点操作(在OpenIddict示例中,通常将其命名为{{ 1}}):

AuthorizationController.Authorize()

然后,还原var ticket = await _accountService.TokenExchangeAsync(info); return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme); 操作中所做的更改,以便在FB登录过程完成时将用户重定向到您的ExternalLoginCallback操作(请不要忘记此处{{1}参数表示授权端点的位置以及强制OIDC参数):

Authorize

如果事情正确实施,流程将是:

returnUrl