Базовая аутентификация промежуточного ПО с API-интерфейсом OWIN и ASP.NET

Я создал проект ASP.NET WEB API 2.2. Я использовал шаблон, основанный на Windows Identity Foundation, для отдельных учетных записей, доступных в visual studio см. Здесь.

Веб-клиент (написанный в angularJS) использует реализацию OAUTH с куки файлами веб-браузера для хранения токена и токена обновления. Мы используем полезные классы UserManager и RoleManager для управления пользователями и их ролями. Все отлично работает с OAUTH и клиентом веб-браузера.

Однако, для некоторых проблем ретро-совместимости с клиентами на базе настольных компьютеров мне также необходимо поддерживать обычную проверку подлинности. В идеале я хотел бы, чтобы атрибуты [Authorize], [Authorize (Role = "administrators" )] и т.д. Работали как с OAUTH, так и с базовой схемой аутентификации.

Таким образом, следуя коду LeastPrivilege, я создал OWIN BasicAuthenticationMiddleware, который наследуется от AuthenticationMiddleware.  Я пришел к следующей реализации. Для BasicAuthenticationMiddleWare только обработчик изменился по сравнению с кодом Leastprivilege. На самом деле мы используем ClaimsIdentity, а не ряд требований.

 class BasicAuthenticationHandler: AuthenticationHandler<BasicAuthenticationOptions>
{
    private readonly string _challenge;

    public BasicAuthenticationHandler(BasicAuthenticationOptions options)
    {
        _challenge = "Basic realm=" + options.Realm;
    }

    protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
    {
        var authzValue = Request.Headers.Get("Authorization");
        if (string.IsNullOrEmpty(authzValue) || !authzValue.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
        {
            return null;
        }

        var token = authzValue.Substring("Basic ".Length).Trim();
        var claimsIdentity = await TryGetPrincipalFromBasicCredentials(token, Options.CredentialValidationFunction);

        if (claimsIdentity == null)
        {
            return null;
        }
        else
        {
            Request.User = new ClaimsPrincipal(claimsIdentity);
            return new AuthenticationTicket(claimsIdentity, new AuthenticationProperties());
        }
    }

    protected override Task ApplyResponseChallengeAsync()
    {
        if (Response.StatusCode == 401)
        {
            var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
            if (challenge != null)
            {
                Response.Headers.AppendValues("WWW-Authenticate", _challenge);
            }
        }

        return Task.FromResult<object>(null);
    }

    async Task<ClaimsIdentity> TryGetPrincipalFromBasicCredentials(string credentials,
        BasicAuthenticationMiddleware.CredentialValidationFunction validate)
    {
        string pair;
        try
        {
            pair = Encoding.UTF8.GetString(
                Convert.FromBase64String(credentials));
        }
        catch (FormatException)
        {
            return null;
        }
        catch (ArgumentException)
        {
            return null;
        }

        var ix = pair.IndexOf(':');
        if (ix == -1)
        {
            return null;
        }

        var username = pair.Substring(0, ix);
        var pw = pair.Substring(ix + 1);

        return await validate(username, pw);
    }

Затем в Startup.Auth я объявляю следующий делегат для проверки подлинности (просто проверяет, существует ли пользователь и правильно ли пароль, и генерирует связанное с ним свойство ClaimsIdentity)

 public void ConfigureAuth(IAppBuilder app)
    {
        app.CreatePerOwinContext(DbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

        Func<string, string, Task<ClaimsIdentity>> validationCallback = (string userName, string password) =>
        {
            using (DbContext dbContext = new DbContext())
            using(UserStore<ApplicationUser> userStore = new UserStore<ApplicationUser>(dbContext))
            using(ApplicationUserManager userManager = new ApplicationUserManager(userStore))
            {
                var user = userManager.FindByName(userName);
                if (user == null)
                {
                    return null;
                }
                bool ok = userManager.CheckPassword(user, password);
                if (!ok)
                {
                    return null;
                }
                ClaimsIdentity claimsIdentity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
                return Task.FromResult(claimsIdentity);
            }
        };

        var basicAuthOptions = new BasicAuthenticationOptions("KMailWebManager", new BasicAuthenticationMiddleware.CredentialValidationFunction(validationCallback));
        app.UseBasicAuthentication(basicAuthOptions);
        // Configure the application for OAuth based flow
        PublicClientId = "self";
        OAuthOptions = new OAuthAuthorizationServerOptions
        {
            TokenEndpointPath = new PathString("/Token"),
            Provider = new ApplicationOAuthProvider(PublicClientId),
            //If the AccessTokenExpireTimeSpan is changed, also change the ExpiresUtc in the RefreshTokenProvider.cs.
            AccessTokenExpireTimeSpan = TimeSpan.FromHours(2),
            AllowInsecureHttp = true,
            RefreshTokenProvider = new RefreshTokenProvider()
        };

        // Enable the application to use bearer tokens to authenticate users
        app.UseOAuthBearerTokens(OAuthOptions);
    }

Однако даже с настройками Request.User в методе AuthenticationAsyncCore обработчика атрибут [Авторизовать] работает не так, как ожидалось: при ошибке с ошибкой 401 каждый раз, когда я пытаюсь использовать схему базовой проверки подлинности, неавторизируется. Любая идея о том, что происходит не так?

Ответ 1

Я обнаружил, что виновник, в файле WebApiConfig.cs, шаблон "индивидуального пользователя" вставил следующие строки.

//// Web API configuration and services
//// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

Таким образом, мы также должны зарегистрировать нашу BasicAuthenticationMiddleware

config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
config.Filters.Add(new HostAuthenticationFilter(BasicAuthenticationOptions.BasicAuthenticationType));

где BasicAuthenticationType - это константная строка "Basic", которая передается базовому конструктору BasicAuthenticationOptions

public class BasicAuthenticationOptions : AuthenticationOptions
{
    public const string BasicAuthenticationType = "Basic";

    public BasicAuthenticationMiddleware.CredentialValidationFunction CredentialValidationFunction { get; private set; }

    public BasicAuthenticationOptions( BasicAuthenticationMiddleware.CredentialValidationFunction validationFunction)
        : base(BasicAuthenticationType)
    {
        CredentialValidationFunction = validationFunction;
    }
}