Добавьте дополнительного поставщика аутентификации, но сохраните текущие данные сеанса

#asp.net-core #authentication #asp.net-core-mvc #.net-5 #challenge-response

Вопрос:

У меня есть проект в .NET5 MVC, в котором реализована аутентификация Twitch с использованием AspNet.Безопасность.OAuth.Twitch. Я все настроил, и все работает нормально, но я хочу добавить возможность связать дополнительную учетную запись с другими поставщиками, такими как Twitter. Я попытался добавить аутентификацию в Twitter с помощью Microsoft.AspNetCore.Идентификация.Twitter. Также все настроил.

Но когда я вхожу в систему с помощью Twitter, моя текущая сессия теряется, и все претензии Twitch были удалены и заменены претензиями Twitter. Я полагаю, что это ожидаемое поведение, но я не знаю, могу ли я сохранить эти утверждения или восстановить только утверждения Twitter без сохранения в удостоверении пользователя (например, хранить в базе данных). Моя главная цель-использовать аутентификацию Twitch в качестве единственного способа входа в приложение, но при этом иметь возможность связывать учетные записи от других поставщиков.

У меня в Startup.cs обоих моих провайдерах добавлены (и в конечном итоге, возможно, когда-нибудь в будущем добавятся другие)

 public void ConfigureServices(IServiceCollection services)
{
    // more stuff ...
    
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    })
    .AddTwitch(options =>
    {
        options.ClientId = Configuration["Twitch-ClientId"];
        options.ClientSecret = Configuration["Twitch-ClientSecret"];
    })
    .AddTwitter(options =>
    {
        options.ConsumerKey = Configuration["Twitter-ConsumerKey"];
        options.ConsumerSecret = Configuration["Twitter-ConsumerSecret"];
    });
}
 

По-моему AuthController.cs , у меня есть соответствующие методы с Вызовом.

 // Default Login using Twitch
[HttpGet("~/signin")]
public IActionResult Login() => RedirectToAction("Login", "Auth", new { provider = "Twitch" });

[HttpPost("~/signin")]
public IActionResult Login([FromForm] string provider)
{
     string redirect_uri = Url.Action("Index", "Home");

     return Challenge(new AuthenticationProperties() { RedirectUri = redirect_uri }, provider);
}
 

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

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

Комментарии:

1. «когда я вхожу в систему с помощью Twitter, моя текущая сессия теряется, и все претензии Twitch были удалены и заменены претензиями Twitter». Исчезает ли файл cookie Twitch __TwitchState ?

2. @abdusco, то же самое происходит и с файлами cookie…

3. Я написал сообщение в блоге об этой проблеме: abdus.dev/сообщения/aspnetcore-несколько сеансов

Ответ №1:

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

Но когда я вхожу в систему с помощью Twitter, моя текущая сессия теряется, и все претензии Twitch были удалены и заменены претензиями Twitter.

Это происходит потому, что вы пытаетесь использовать Cookie схему для хранения файлов cookie сеанса как для Twitter, так и для Twitch. Когда вы входите в систему с одним из них, он перезаписывает другой.

Чтобы решить эту проблему, вам необходимо добавить отдельные файлы cookie для каждого отдельного варианта входа в систему.

 services.AddAuthentication()
    .AddCookie("GoogleSession")
    .AddCookie("GithubSession")
    .AddGoogle(
        options => {
            // set the app credentials
            Configuration.GetSection("Google").Bind(options);
            // save session to this cookie
            options.SignInScheme = "GoogleSession";
        })
    .AddGitHub(
        options => {
            // set the app credentials
            Configuration.GetSection("Github").Bind(options);
            // save session to this cookie
            options.SignInScheme = "GithubSession";
        });
 

Затем бросьте вызов, чтобы заставить пользователя войти в систему:

 [AllowAnonymous]
[HttpGet("login-google")]
public ActionResult LoginGoogle()
{
    return Challenge(
        new AuthenticationProperties
        {
            RedirectUri = Url.Action("WhoAmI"),
        }, GoogleDefaults.AuthenticationScheme
    );
}

[AllowAnonymous]
[HttpGet("login-github")]
public ActionResult LoginGithub()
{
    return Challenge(
        new AuthenticationProperties
        {
            RedirectUri = Url.Action("WhoAmI"),
        }, GitHubAuthenticationDefaults.AuthenticationScheme
    );
}
 

Затем в любое время вы можете аутентифицировать пользователя, чтобы прочитать и проанализировать файлы cookie для доступа к утверждениям:

 [AllowAnonymous]
[HttpGet("me")]
public async Task<ActionResult> WhoAmI()
{
    var googleResult = await HttpContext.AuthenticateAsync(GoogleDefaults.AuthenticationScheme);
    if (googleResult.Succeeded)
    {
        var googlePrincipal = googleResult.Principal;
        // ... use google claims
        User.AddIdentity((ClaimsIdentity)googlePrincipal.Identity);
    }

    var githubResult = await HttpContext.AuthenticateAsync(GitHubAuthenticationDefaults.AuthenticationScheme);
    if (githubResult.Succeeded)
    {
        var githubPrincipal = githubResult.Principal;
        // ... use google claims
        User.AddIdentity((ClaimsIdentity)githubPrincipal.Identity);
    }

    return Ok(
        User.Identities.Select(
                id => new
                {
                    id.AuthenticationType, 
                    Claims = id.Claims.Select(c => new { c.Type, c.Value })
                }
            )
            .ToList()
    );
 

Теперь , когда я посещаю /me , я получаю список всех претензий со всей сессии:

 [
  {
    "authenticationType": null,
    "claims": []
  },
  {
    "authenticationType": "Google",
    "claims": [
      {
        "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
        "value": "123131231231312123123123"
      },
      {
        "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
        "value": "My Fullname"
      },
      {
        "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
        "value": "MyName"
      },
      {
        "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
        "value": "MyLastname"
      },
      {
        "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
        "value": "my@gmail.com"
      }
    ]
  },
  {
    "authenticationType": "GitHub",
    "claims": [
      {
        "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
        "value": "1313123123"
      },
      {
        "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
        "value": "abdusco"
      },
      {
        "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
        "value": "my@email.com"
      },
      {
        "type": "urn:github:name",
        "value": "my name"
      },
      {
        "type": "urn:github:url",
        "value": "https://api.github.com/users/abdusco"
      }
    ]
  }
]
 

Немного утомительно вручную аутентифицировать пользователя с помощью нескольких схем аутентификации. Мы можем позволить ASP.NET Ядро делает это за нас.

Определите политику авторизации, которая принимает несколько схем проверки подлинности.

 services.AddAuthorization(
    options => options.DefaultPolicy = new AuthorizationPolicyBuilder(
            GoogleDefaults.AuthenticationScheme,
            GitHubAuthenticationDefaults.AuthenticationScheme
        ).RequireAuthenticatedUser()
        .Build()
);
 

Теперь, когда вы оформляете действие [Authorize] (и указываете имя политики, если это необходимо), HttpContext.User оно будет содержать как удостоверения, так и утверждения из всех сеансов.

 [Authorize]
[HttpGet("me")]
public async Task<ActionResult> WhoAmI()
{
    return Ok(
        // user has claims from all sessions
        User.Identities.Select(
                id => new
                {
                    id.AuthenticationType,
                    Claims = id.Claims.Select(c => new { c.Type, c.Value })
                }
            )
            .ToList()
    );
}
 

Это имеет тот же вывод, что и раньше, но без шаблона.

Комментарии:

1. Спасибо @abdusco. Я прочитал ваш пост в блоге и теперь понимаю, как это работает. Я создал два файла cookie, один для основного сеанса, а другой для всех дополнительных провайдеров, потому что я хочу, чтобы претензии сохранялись в базе данных только один раз, а затем сохранялись в неизменном виде.