Выдает вход в систему ASP.NET Core (участник, свойства, AuthenticationScheme) Авторизация или ответ токена не могут быть возвращены с этой конечной точки

Следующим шагом моего приложения является то, что я получаю токен доступа из внешнего интерфейса, а затем использую этот токен, мой бэкэнд вызывает поставщика внешнего входа, чтобы согласовать идентификатор пользователя и получить дополнительную информацию от поставщика входа.

Как и в этом методе, мне нужно установить и настроить поставщика входа в систему для каждого клиента (Интернет, 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 выдает Ответ OpenID Connect не может быть возвращен с этой конечной точки

вот еще один вопрос с аналогичным подходом, но с другим Как сгенерировать AccessToken для пользователя, который вошел в систему с помощью внешних поставщиков

Вот мой 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 ---

Любая помощь будет оценена по достоинству!


person Bipn Paul    schedule 28.09.2017    source источник
comment
@poke SignIn - это метод ControllerBase и одна из точек входа в стек проверки подлинности ASP.NET Core в MVC Core (другие - SignOut, Challenge и Forbid). Здесь OP реализует сервер OIDC с использованием OpenIddict, и реализация логики в контроллере авторизации действительно является рекомендуемым вариантом.   -  person Kévin Chalet    schedule 29.09.2017
comment
@Pinpoint А, ладно, я не понял, что это сервер, а не приложение. Спасибо!   -  person poke    schedule 29.09.2017


Ответы (1)


Ваш вопрос действительно похож на вторая ссылка, которую вы разместили, и исключение, которое вы видите, имеют ту же основную причину: вы пытаетесь вернуть ответ токена от конечной точки, которая не рассматривается OpenIddict как конечная точка вашего токена (т. е. путь к ваше ExternalLoginCallback действие не соответствует заданному при вызове options.EnableTokenEndpoint("/path").

По соображениям безопасности OpenIddict отказывается обрабатывать такой ответ и выдает исключение.

Чтобы исправить это, вам нужно сделать 2 вещи:

Используйте код или неявный вместо потока паролей

Поток паролей нельзя использовать с потоками интерактивной аутентификации, которые включают обратные пути к внешним поставщикам, таким как 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 он обычно называется AuthorizationController.Authorize()):

var ticket = await _accountService.TokenExchangeAsync(info);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);

Затем отмените изменения, внесенные в ваше действие ExternalLoginCallback, чтобы оно просто перенаправляло пользователя к вашему действию Authorize, когда процесс входа в FB завершен (не забывайте, что здесь параметр returnUrl представляет местоположение вашей конечной точки авторизации с обязательными параметрами OIDC ):

return LocalRedirect(returnUrl);

Если все реализовано правильно, поток будет следующим:

Client application -> authorization endpoint -> login page -> Facebook -> external login callback -> authorization endpoint -> client application.
person Kévin Chalet    schedule 29.09.2017