Добавление нового OAuth-провайдера¶
TESSA поддерживает вход в систему с помощью внешних провайдеров, которые реализуют протокол OAuth 2.0 или OpenID Connect (OIDC) - расширение протокола OAuth.
Чтобы добавить нового провайдера в приложение, достаточно настроить параметры этого провайдера в конфигурационном файле app-oauth.json
для веб-сервиса web
. Параметры описаны подробно в руководстве по настройке аутентификации через OAuth.
Далее мы рассмотрим добавление OIDC-провайдера на примере GitLab и добавление OAuth-провайдера на примере GitHub.
Провайдер GitLab¶
Так как GitLab полноценно поддерживает протокол OIDC, то достаточно просто сконфигурировать провайдер и настроить конфигурационный файл app-oauth.json
приложения TESSA.
Регистрация приложения у провайдера¶
Чтобы воспользоваться провайдером GitLab в качестве поставщика услуг для внешней аутентификации необходимо зарегистрировать и настроить приложение, которое будет взаимодействовать с внешним провайдером. В нашем случае таким приложением является TESSA.
Обратитесь к документации для создания приложения GitLab.
После создания и настройки приложения будет сформирован идентификатор (ClientId
) и секрет (ClientSecret
) клиента. Данные параметры необходимо запомнить, чтобы использовать далее при настройке конфигурации провайдера в приложении TESSA.
Important
При регистрации приложения в качестве пути обратного вызова необходимо использовать конечную точку /oauth/callback
.
Настройка провайдера в приложении¶
Для добавления провайдера в TESSA необходимо модифицировать конфигурационный файл app-oauth.json
:
{
"Name": "GitLab",
"Icon": "gitlab.png",
"Authority": "https://gitlab.com/",
"GetClaimsFromUserInfoEndpoint": true,
"ResponseType": "code id_token",
"UsePkce": true,
"UseOpenIdConnect": true,
"Claims": {
"username": "given_name",
"preferred_username": "nickname"
},
"ClientId": "****",
"ClientSecret": "***"
}
где
-
"Icon"
- параметр с иконкой провайдера, которая размещается на окне входа в систему. Сохраните иконку провайдераgitlab.png
в папку с веб-сервисомweb
в подпапку/wwwroot/images/oauth
. -
"Authority"
- общедоступная конечная точка, которая обслуживает документ конфигурации OIDC для GitLab. Если необходимо использовать протокол OAuth вместо OIDC, то выполните следующие шаги:-
удалите параметр
"UseOpenIdConnect"
или установите для него значениеfalse
, -
вместо параметра
"Authority"
укажите следующие параметры:"AuthorizationEndpoint": "https://gitlab.com/oauth/authorize", "TokenEndpoint": "https://gitlab.com/oauth/token", "UserInformationEndpoint": "https://gitlab.com/oauth/userinfo"
-
укажите необходимые области доступа, например:
"Scopes": [ "openid", "profile", "email" ]
-
-
"ClientId"
- идентификатор клиента, который был получен при регистрации приложения у провайдера. -
"ClientSecret"
- секрет клиента, который был получен при регистрации приложения у провайдера.
Узнать допустимые конечные точки (endpoints), области действия (scopes), утверждения (claims) и другую информацию о конфигурации приложения можно в документации GitLab.
Проверка внешней аутентификации¶
Запустите веб-сервис web
и перейдите на окно входа в систему. Убедитесь, что в окне входа отображается кнопка для входа с помощью GitLab.
Нажмите на кнопку для выполнения внешней аутентификации. Веб-клиент выполнит перенаправление на страницу поставщика услуг:
Если пользователь был ранее зарегистрирован в GitHub, то достаточно ввести логин и пароль пользователя. Иначе - необходимо выполнить регистрацию. После ввода корректного логина и пароля для учётной записи GitLab, будет отображено окно с подтверждением выдачи доступа приложению TESSA.
После подтверждения доступа веб-клиент выполнит перенаправление в TESSA и произведёт вход в систему.
Если же доступ был запрещён, то веб-клиент выполнит перенаправление в TESSA и отобразит сообщение об ошибке.
Note
Если пользователь отсутствовал в системе TESSA на момент аутентификации, то он будет создан, если в конфигурационном файле app-oauth.json
включён соответствующий параметр.
Провайдер GitHub¶
Многие поставщики услуг внешней аутентификации, в силу своей специфики, могут отклоняться от спецификации протоколов OAuth и OIDC, что приводит к изменению набора стандартных параметров в запросе на аутентификацию, а также к изменению способа обработки ответа от внешнего провайдера. Некоторые провайдеры поддерживают OIDC только частично, а некоторые не поддерживают его вовсе. Поэтому невозможно реализовать универсальный обработчик, учитывающий все особенности аутентификации с помощью протоколов OAuth и OIDC.
Для учёта нюансов и особенностей аутентификации с различными провайдерами, необходимо создавать специфические для каждого из них обработчики аутентификации и детально реализовывать в них протоколы OAuth и OIDC (если они поддерживаются). Например, GitHub не поддерживает OIDC, хотя в его конфигурации он упоминается. Этот сервис также отклоняется от спецификации OAuth, и для получения пользовательской информации, такой как адрес электронной почты пользователя, нужно использовать отдельную конечную точку.
Давайте рассмотрим создание схемы внешней аутентификации с использованием протокола OAuth на примере поставщика услуг GitHub.
Регистрация приложения у провайдера¶
Чтобы воспользоваться провайдером GitHub в качестве поставщика услуг для внешней аутентификации необходимо зарегистрировать и настроить приложение, которое будет взаимодействовать с внешним провайдером. В нашем случае таким приложением является TESSA.
Обратитесь к документации для создания приложения GitHub.
После создания и настройки приложения будет сформирован идентификатор (ClientId
) и секрет (ClientSecret
) клиента. Данные параметры необходимо запомнить, чтобы использовать далее при настройке конфигурации провайдера в приложении TESSA.
Important
При регистрации приложения в качестве пути обратного вызова необходимо использовать конечную точку /oauth/callback
. При необходимости конечную точку можно установить другую, тогда придётся перезаписать свойство RemoteAuthenticationOptions.CallbackPath
при добавлении новой схемы внешней аутентификации в методе расширения (см. далее).
Настройка провайдера в приложении¶
Для добавления провайдера в TESSA необходимо модифицировать конфигурационный файл app-oauth.json
:
{
"Name": "GitHub",
"Caption": {
"en": "Login with GitHub",
"ru": "Войти с GitHub"
},
"AuthorizationEndpoint": "https://github.com/login/oauth/authorize",
"TokenEndpoint": "https://github.com/login/oauth/access_token",
"UserInformationEndpoint": "https://api.github.com/user",
"UserEmailsEndpoint": "https://api.github.com/user/emails",
"EnterpriseDomain": null,
"AllowSignup": null,
"Scopes": [
"user",
"read:user",
"user:email"
],
"Claims": {
"login": "given_name",
"avatar_url": "picture"
},
"ClientId": "****",
"ClientSecret": "****"
}
где
-
"ClientId"
- идентификатор клиента, который был получен при регистрации приложения у провайдера; -
"ClientSecret"
- секрет клиента, который был получен при регистрации приложения у провайдера.
Узнать допустимые конечные точки (endpoints), области действия (scopes), утверждения (claims) и другую информацию о конфигурации приложения можно в документации GitHub.
Создание схемы внешней аутентификации¶
В корне проекта Tessa.Extensions.Server.Web
добавьте папку OAuth
с подпапкой GitHub
:
|- Tessa.Extensions.Server.Web
|- OAuth
|- GitHub
Внутри папки GitHub
создайте файл GitHubOptions.cs
.
Класс GitHubOptions
используется как промежуточное хранилище для сеанса внешней аутентификации. В нём можно разместить дополнительные свойства и методы, которые будут использоваться в обработчике внешней аутентификации.
GitHubOptions.cs
using Microsoft.AspNetCore.Authentication.OAuth;
namespace Tessa.Extensions.Server.Web.OAuth.GitHub
{
/// <summary>
/// Набор опций для конфигурирования GitHub-аутентификации
/// посредством OAuth, используемые в <see cref="GitHubHandler"/>.
/// </summary>
public sealed class GitHubOptions : OAuthOptions
{
/// <summary>
/// Имя домена, используемое при локальном развертывании GitHub Enterprise.
/// Значение может быть <see langword="null"/>, если версия Enterprise не используется.
/// </summary>
public string? EnterpriseDomain { get; set; }
/// <summary>
/// Конечная точка для получения адреса электронной почты аутентифицированного пользователя.
/// </summary>
public string? UserEmailsEndpoint { get; set; }
/// <summary>
/// Предоставляется ли пользователям возможность регистрации на GitHub во время процесса OAuth,
/// если они не прошли проверку подлинности. Используйте значение <see langword="false"/>,
/// когда политика запрещает регистрацию. По умолчанию регистрация разрешена.
/// </summary>
public bool? AllowSignup { get; set; }
}
}
Внутри папки GitHub
создайте файл GitHubPostOptions.cs
.
Класс GitHubPostOptions
используется для финальной настройки опций GitHubOptions
после их конфигурирования. Внутри объекта GitHubPostOptions
можно переопределить свойства опций и задать для них корректное значение.
GitHubPostOptions.cs
using System;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.Extensions.Options;
namespace Tessa.Extensions.Server.Web.OAuth.GitHub
{
/// <summary>
/// Объект, используемый для установки значений по умолчанию для всех <see cref="GitHubOptions"/>.
/// </summary>
public sealed class GitHubPostOptions : IPostConfigureOptions<GitHubOptions>
{
#region Constants
/// <summary>
/// Путь по умолчанию, используемый для GitHub Enterprise v3 REST API.
/// </summary>
private const string EnterpriseApiPath = "/api/v3";
/// <summary>
/// Путь по умолчанию, используемый для <see cref="OAuthOptions.AuthorizationEndpoint"/>.
/// </summary>
private const string AuthorizationEndpointPath = "/login/oauth/authorize";
/// <summary>
/// Путь по умолчанию, используемый для <see cref="OAuthOptions.TokenEndpoint"/>.
/// </summary>
private const string TokenEndpointPath = "/login/oauth/access_token";
/// <summary>
/// Путь по умолчанию, используемый для <see cref="OAuthOptions.UserInformationEndpoint"/>.
/// </summary>
private const string UserInformationEndpointPath = "/user";
/// <summary>
/// Путь по умолчанию, используемый для <see cref="GitHubOptions.UserEmailsEndpoint"/>.
/// </summary>
private const string UserEmailsEndpointPath = "/user/emails";
#endregion
#region Base Overrides
/// <inheritdoc/>
public void PostConfigure(string? name, GitHubOptions options)
{
ThrowIfNull(options);
if (!string.IsNullOrWhiteSpace(options.EnterpriseDomain))
{
options.AuthorizationEndpoint = CreateUrl(options.EnterpriseDomain, AuthorizationEndpointPath);
options.TokenEndpoint = CreateUrl(options.EnterpriseDomain, TokenEndpointPath);
options.UserInformationEndpoint = CreateUrl(options.EnterpriseDomain, EnterpriseApiPath + UserInformationEndpointPath);
options.UserEmailsEndpoint = CreateUrl(options.EnterpriseDomain, EnterpriseApiPath + UserEmailsEndpointPath);
}
}
#endregion
#region Private Methods
private static string CreateUrl(string domain, string path)
{
var builder = new UriBuilder(domain)
{
Path = path,
Port = -1,
Scheme = Uri.UriSchemeHttps,
};
return builder.Uri.ToString();
}
#endregion
}
}
Внутри папки GitHub
создайте файл GitHubHandler.cs
.
Класс GitHubHandler
используется для обработки запросов внешней аутентификации. В нём учитываются специфичные параметры провайдера:
-
при создании запроса внешней аутентификации задаётся дополнительный параметр
allow_signup
в строке запроса, чтобы сообщить поставщику услуг о необходимости отображения регистрационного окна, если пользователь ранее не был зарегистрирован; -
при получении информации о пользователе используется отдельная конечная точка для получения адреса электронной почты пользователя, к которой выполняется дополнительный запрос с токеном доступа.
GitHubHandler.cs
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Net.Mime;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Tessa.Web.Client.OAuth;
namespace Tessa.Extensions.Server.Web.OAuth.GitHub
{
/// <summary>
/// Обработчик GitHub-аутентификации посредством OAuth.
/// </summary>
public sealed class GitHubHandler : OAuthDefaultHandler<GitHubOptions>
{
#region Constructors
/// <summary>
/// Создаёт экземпляр класса <see cref="GitHubHandler"/>.
/// </summary>
/// <inheritdoc />
public GitHubHandler(
IOptionsMonitor<GitHubOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
IOAuthEventHandler eventHandler)
: base(options, logger, encoder, eventHandler)
{
}
#endregion
#region Base Overrides
/// <inheritdoc />
protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
{
// Дополнительную информацию см. https://docs.github.com/ru/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
// Сначала выполняется базовый метод, чтобы получить сформированный URL с основными параметрами
var baseUrl = base.BuildChallengeUrl(properties, redirectUri);
var parameters = QueryHelpers.ParseQuery(new Uri(baseUrl).Query);
// Затем в строку запроса добавляются дополнительные параметры
this.SetQueryParameter(parameters, properties, OAuthChallengeProperties.ScopeKey, this.FormatScope, this.Options.Scope);
this.SetQueryParameter(parameters, properties, "allow_signup", this.FormatBoolean, this.Options.AllowSignup);
// Некоторые свойства удаляются при настройке параметров запроса, поэтому состояние необходимо сбросить
parameters["state"] = this.Options.StateDataFormat.Protect(properties);
// Добавление параметров к конечной точке авторизации
return QueryHelpers.AddQueryString(this.Options.AuthorizationEndpoint, parameters);
}
/// <inheritdoc/>
protected override async Task InitializeEventsAsync()
{
await base.InitializeEventsAsync();
this.Events.OnCreatingTicket = async context =>
{
// Если адрес электронной почты не является публичным, то его необходимо
// получить из конечной точки emails, если указана соответствующая область видимости
if (context.Identity is { } identity
&& !string.IsNullOrWhiteSpace(this.Options.UserEmailsEndpoint)
&& !identity.HasClaim(claim => claim.Type is ClaimTypes.Email)
&& this.Options.Scope.Any(scope => scope is "user" or "user:email"))
{
var email = await this.GetEmailAsync(context.TokenResponse, this.Context.RequestAborted);
if (!string.IsNullOrWhiteSpace(email))
{
identity.AddClaim(new Claim(ClaimTypes.Email, email, ClaimValueTypes.String, this.Options.ClaimsIssuer));
}
}
};
}
#endregion
#region Private Methods
private async Task<string?> GetEmailAsync(OAuthTokenResponse tokens, CancellationToken cancellationToken)
{
// Запрос на получение адреса электронной почты пользователя. Подробнее см. https://developer.github.com/v3/users/emails/
using var request = new HttpRequestMessage(HttpMethod.Get, this.Options.UserEmailsEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
request.Version = this.Backchannel.DefaultRequestVersion;
using var response = await this.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
if (!response.IsSuccessStatusCode)
{
this.Logger.LogError(
"An error occurred while retrieving the email address associated with the logged in user: the remote server returned a {0} response with the following payload: {1} {2}.",
response.StatusCode,
response.Headers.ToString(),
await response.Content.ReadAsStringAsync(cancellationToken));
throw new HttpRequestException("An error occurred while retrieving the email address associated to the user profile.");
}
return (await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: cancellationToken))
.EnumerateArray()
.Where(address => address.GetProperty("primary").GetBoolean())
.Select(address => address.GetString("email"))
.FirstOrDefault();
}
#endregion
}
}
Внутри папки GitHub
создайте файл GitHubConfigurator.cs
.
В классе GitHubConfigurator
реализованы методы для добавления схемы аутентификации с помощью внешнего провайдера GitHub и настройки этой аутентификации.
GitHubExtensions.cs
using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Tessa.Platform.Storage;
using Tessa.Web.Client.OAuth;
namespace Tessa.Extensions.Server.Web.OAuth.GitHub
{
/// <summary>
/// Объект, посредством которого выполняется добавление и настройка
/// внешней аутентификации по протоколу OAuth для провайдера GitHub.
/// </summary>
public sealed class GitHubConfigurator :
OAuthDefaultConfigurator<GitHubOptions, GitHubHandler>
{
#region Constructors
/// <summary>
/// Создает экземпляр класса <see cref="GitHubConfigurator"/>.
/// </summary>
/// <inheritdoc/>
public GitHubConfigurator(Dictionary<string, object?> provider) : base(provider) { }
#endregion
#region Base Overrides
/// <inheritdoc/>
public override void AddScheme(AuthenticationBuilder builder)
{
builder.Services.TryAddSingleton<IPostConfigureOptions<GitHubOptions>, GitHubPostOptions>();
base.AddScheme(builder);
}
/// <inheritdoc/>
protected override void ConfigureOptions(GitHubOptions options)
{
base.ConfigureOptions(options);
options.UserEmailsEndpoint = this.Provider.TryGet<string?>(nameof(GitHubOptions.UserEmailsEndpoint));
options.EnterpriseDomain = this.Provider.TryGet<string?>(nameof(GitHubOptions.EnterpriseDomain));
options.AllowSignup = this.Provider.TryGet<bool?>(nameof(GitHubOptions.AllowSignup));
// options.CallbackPath = "/signin-github";
}
#endregion
}
}
Внутри папки OAuth
создайте файл WebRegistrator.cs
.
В классе WebRegistrator
выполняется регистрация схемы внешней аутентификации, которая использует специфичный обработчик для GitHub. Регистрация выполняется внутри DI-контейнера ASP NET.
WebRegistrator.cs
using Tessa.Extensions.Server.Web.OAuth.GitHub;
using Tessa.Web.Client.OAuth;
using Tessa.Web.Registrations;
namespace Tessa.Extensions.Server.Web.OAuth
{
/// <summary>
/// Регистратор, используемый для конфигурации аутентификации посредством OAuth.
/// </summary>
[WebRegistrator]
public sealed class WebRegistrator : WebRegistratorBase
{
/// <inheritdoc/>
public override void InitializeRegistration()
{
OAuthHandlersRegistry.Instance.TryRegister("GitHub", provider => new GitHubConfigurator(provider));
}
}
}
Проверка внешней аутентификации¶
Запустите веб-сервис web
и перейдите на окно входа в систему. Убедитесь, что в окне входа отображается кнопка для входа с помощью GitHub.
Нажмите на кнопку для выполнения внешней аутентификации. Веб-клиент выполнит перенаправление на страницу поставщика услуг:
Если пользователь был ранее зарегистрирован в GitHub, то достаточно ввести логин и пароль пользователя. Иначе - необходимо выполнить регистрацию. После ввода корректного логина и пароля для учётной записи GitHub, веб-клиент выполнит перенаправление в TESSA и произведёт вход в систему:
Note
Если пользователь отсутствовал в системе TESSA на момент аутентификации, то он будет создан, если в конфигурационном файле app-oauth.json
включён соответствующий параметр.