Объединение аутентификации форм и базовой проверки подлинности

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

Решение, с которым я столкнулся, похоже, работает, но я ничего не теряю здесь?

Во-первых, весь сайт работает под HTTPS.

Сайт настроен на использование проверки подлинности форм в web.config

<authentication mode="Forms">
  <forms loginUrl="~/Login.aspx" timeout="2880"/>
</authentication>
<authorization>
  <deny users="?"/>
</authorization>

Затем я переопределяю AuthenticateRequest в Global.asax, чтобы запустить базовую проверку подлинности на страницах веб-службы:

void Application_AuthenticateRequest(object sender, EventArgs e)
{
    //check if requesting the web service - this is the only page
    //that should accept Basic Authentication
    HttpApplication app = (HttpApplication)sender;
    if (app.Context.Request.Path.StartsWith("/Service/MyService.asmx"))
    {

        if (HttpContext.Current.User != null)
        {
            Logger.Debug("Web service requested by user " + HttpContext.Current.User.Identity.Name);
        }
        else
        {
            Logger.Debug("Null user - use basic auth");

            HttpContext ctx = HttpContext.Current;

            bool authenticated = false;

            // look for authorization header
            string authHeader = ctx.Request.Headers["Authorization"];

            if (authHeader != null && authHeader.StartsWith("Basic"))
            {
                // extract credentials from header
                string[] credentials = extractCredentials(authHeader);

                // because i'm still using the Forms provider, this should
                // validate in the same way as a forms login
                if (Membership.ValidateUser(credentials[0], credentials[1]))
                {
                    // create principal - could also get roles for user
                    GenericIdentity id = new GenericIdentity(credentials[0], "CustomBasic");
                    GenericPrincipal p = new GenericPrincipal(id, null);
                    ctx.User = p;

                    authenticated = true;
                }
            }

            // emit the authenticate header to trigger client authentication
            if (authenticated == false)
            {
                ctx.Response.StatusCode = 401;
                ctx.Response.AddHeader(
                    "WWW-Authenticate",
                    "Basic realm=\"localhost\"");
                ctx.Response.Flush();
                ctx.Response.Close();

                return;
            }
        }
    }            
}

private string[] extractCredentials(string authHeader)
{
    // strip out the "basic"
    string encodedUserPass = authHeader.Substring(6).Trim();

    // that the right encoding
    Encoding encoding = Encoding.GetEncoding("iso-8859-1");
    string userPass = encoding.GetString(Convert.FromBase64String(encodedUserPass));
    int separator = userPass.IndexOf(':');

    string[] credentials = new string[2];
    credentials[0] = userPass.Substring(0, separator);
    credentials[1] = userPass.Substring(separator + 1);

    return credentials;
}

Ответ 1

.Net 4.5 имеет новое свойство Response: SuppressFormsAuthenticationRedirect. Если установлено значение true, это предотвращает перенаправление ответа 401 на страницу входа на веб-сайт. Вы можете использовать следующий фрагмент кода в файле global.asax.cs, чтобы включить базовую аутентификацию, например. папке /HealthCheck.

  /// <summary>
  /// Authenticates the application request.
  /// Basic authentication is used for requests that start with "/HealthCheck".
  /// IIS Authentication settings for the HealthCheck folder:
  /// - Windows Authentication: disabled.
  /// - Basic Authentication: enabled.
  /// </summary>
  /// <param name="sender">The source of the event.</param>
  /// <param name="e">A <see cref="System.EventArgs"/> that contains the event data.</param>
  protected void Application_AuthenticateRequest(object sender, EventArgs e)
  {
     var application = (HttpApplication)sender;
     if (application.Context.Request.Path.StartsWith("/HealthCheck", StringComparison.OrdinalIgnoreCase))
     {
        if (HttpContext.Current.User == null)
        {
           var context = HttpContext.Current;
           context.Response.SuppressFormsAuthenticationRedirect = true;
        }
     }
  }

Ответ 2

У меня есть решение для работы на основе идей OP и указателей от Samuel Meacham.

В global.asax.cs:

    protected void Application_AuthenticateRequest(object sender, EventArgs e)
    {
        if (DoesUrlNeedBasicAuth() && Request.IsSecureConnection) //force https before we try and use basic authentication
        {
            if (HttpContext.Current.User != null && HttpContext.Current.User.Identity.IsAuthenticated)
            {
                _log.Debug("Web service requested by user " + HttpContext.Current.User.Identity.Name);
            }
            else
            {
                _log.Debug("Null user - use basic auth");

                HttpContext ctx = HttpContext.Current;

                bool authenticated = false;

                // look for authorization header
                string authHeader = ctx.Request.Headers["Authorization"];

                if (authHeader != null && authHeader.StartsWith("Basic"))
                {
                    // extract credentials from header
                    string[] credentials = extractCredentials(authHeader);

                    //Lookup credentials (we'll do this in config for now)
                    //check local config first
                    var localAuthSection = ConfigurationManager.GetSection("apiUsers") as ApiUsersSection;
                    authenticated = CheckAuthSectionForCredentials(credentials[0], credentials[1], localAuthSection);

                    if (!authenticated)
                    {
                        //check sub config
                        var webAuth = System.Web.Configuration.WebConfigurationManager.GetSection("apiUsers") as ApiUsersSection;
                        authenticated = CheckAuthSectionForCredentials(credentials[0], credentials[1], webAuth);
                    }
                }

                // emit the authenticate header to trigger client authentication
                if (authenticated == false)
                {
                    ctx.Response.StatusCode = 401;
                    ctx.Response.AddHeader("WWW-Authenticate","Basic realm=\"localhost\"");
                    ctx.Response.Flush();
                    ctx.Response.Close();

                    return;
                }
            }
        }
        else
        {
            //do nothing
        }
    }

    /// <summary>
    /// Detect if current request requires basic authentication instead of Forms Authentication.
    /// This is determined in the web.config files for folders or pages where forms authentication is denied.
    /// </summary>
    public bool DoesUrlNeedBasicAuth()
    {
        HttpContext context = HttpContext.Current;
        string path = context.Request.AppRelativeCurrentExecutionFilePath;
        if (context.SkipAuthorization) return false;

        //if path is marked for basic auth, force it

        if (context.Request.Path.StartsWith(Request.ApplicationPath + "/integration", true, CultureInfo.CurrentCulture)) return true; //force basic

        //if no principal access was granted force basic auth
        //if (!UrlAuthorizationModule.CheckUrlAccessForPrincipal(path, context.User, context.Request.RequestType)) return true;

        return false;
    }

    private string[] extractCredentials(string authHeader)
    {
        // strip out the "basic"
        string encodedUserPass = authHeader.Substring(6).Trim();

        // that the right encoding
        Encoding encoding = Encoding.GetEncoding("iso-8859-1");
        string userPass = encoding.GetString(Convert.FromBase64String(encodedUserPass));
        int separator = userPass.IndexOf(':');

        string[] credentials = new string[2];
        credentials[0] = userPass.Substring(0, separator);
        credentials[1] = userPass.Substring(separator + 1);

        return credentials;
    }

    /// <summary>
    /// Checks whether the given basic authentication details can be granted access. Assigns a GenericPrincipal to the context if true.
    /// </summary>
    private bool CheckAuthSectionForCredentials(string username, string password, ApiUsersSection section)
    {
        if (section == null) return false;
        foreach (ApiUserElement user in section.Users)
        {
            if (user.UserName == username && user.Password == password)
            {
                Context.User = new GenericPrincipal(new GenericIdentity(user.Name, "Basic"), user.Roles.Split(','));
                return true;
            }
        }
        return false;
    }

Учетные данные, которым разрешен доступ, хранятся в пользовательском разделе в файле web.config, но вы можете сохранить их как пожелаете.

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

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

Здесь прокомментирована строка, включающая CheckUrlAccessForPrincipal, которая предоставит доступ к любой странице на сайте, используя базовый auth, если пользователь не войдет в систему через проверку подлинности с помощью форм.

Использование Application_AuthenticateRequest вместо Application_AuthorizeRequest оказалось важным, поскольку Application_AuthorizeRequest заставил бы базовый auth, но в любом случае перенаправить на страницу входа в Autodesk Forms. Мне не удалось выполнить эту работу, играя с разрешениями на основе местоположения в web.config и так и не узнав причину этого. Перестановка на Application_AuthenticateRequest сделала трюк, поэтому я оставил его на этом.

Результат этого оставил мне папку, к которой можно было получить доступ, используя базовый auth через HTTPS внутри приложения, которое обычно использует проверку подлинности на языке. Записанные пользователи могут в любом случае получить доступ к папке.

Надеюсь, что это поможет.

Ответ 3

Ты на правильном пути, я думаю. Однако я не уверен, что вы должны выполнять работу в проверке подлинности. Это когда пользователь идентифицируется, а не когда проверяется разрешение на ресурс (что позже в авторизации). Во-первых, в вашем web.config используйте <location> для удаления форм auth для ресурсов, где вы хотите использовать базовый auth.

Web.config

<configuration>
    <!-- don't require forms auth for /public -->
    <location path="public">
        <authorization>
            <allow users="*" />
        </authorization>
    </location>
</configuration>

Global.asax.cs или где угодно (IHttpModule и т.д.)

Затем вместо жесткого кодирования конкретных обработчиков или попытки разобрать URL-адрес, чтобы увидеть, находится ли вы в определенной папке, в Application_AuthorizeRequest, что-то вроде следующего, сделает все по умолчанию (формы auth 1st, basic auth если формы auth были удалены с помощью параметров <location> в web.config).

/// <summary>
/// Checks to see if the current request can skip authorization, either because context.SkipAuthorization is true,
/// or because UrlAuthorizationModule.CheckUrlAccessForPrincipal() returns true for the current request/user/url.
/// </summary>
/// <returns></returns>
public bool DoesUrlRequireAuth()
{
    HttpContext context = HttpContext.Current;
    string path = context.Request.AppRelativeCurrentExecutionFilePath;
    return context.SkipAuthorization ||
        UrlAuthorizationModule.CheckUrlAccessForPrincipal(
            path, context.User, context.Request.RequestType);
}

void Application_AuthorizeRequest(object sender, EventArgs e)
{
    if (DoesUrlRequireAuth())
    {
        // request protected by forms auth
    }
    else
    {
        // do your http basic auth code here
    }
}

Неподтвержденный (просто набрал inline здесь), но я много сделал с пользовательскими поставщиками членства, ваши требования полностью выполнимы.

Надеюсь, что это полезно =)