Arhn - архитектура программирования

Безопасный однократный доступ к разделу приложения ASP.NET MVC 5

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

Это приложение размещено в Интернете, поэтому доступ сотрудников должен быть защищен с помощью входа в систему.

Мой вопрос: каков наилучший подход к предоставлению учетной записи для входа пользователю, который будет использовать систему только один раз?

Как я понимаю, у меня есть два варианта:

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

  2. Создайте учетную запись для входа в систему для каждого сотрудника по мере необходимости, а затем удалите учетную запись после ее использования. Для этого имени пользователя я бы объединил обычное слово (например, название компании) с уникальным идентификатором (возможно, идентификатором их задачи)

Вариант 2 кажется наиболее разумным с точки зрения безопасности. Есть ли какие-то подводные камни в этом подходе или есть альтернативные решения?


Ответы:


1

Лично я бы рассмотрел третий вариант: создать параллельную таблицу управления доступом для этой страницы. Другими словами, у вас будет что-то вроде:

public class PageAccess
{
    public string Email { get; set; }
    public string Token { get; set; }
    public DateTime Expiration { get; set; }
}

Когда администратор хочет предоставить доступ к странице, он сообщает адрес электронной почты пользователя, у которого должен быть доступ (Email). Затем будет сгенерирован случайный токен (сохраненный в хешированном виде как Token). Затем пользователю будет отправлено электронное письмо на его адрес электронной почты с URL-адресом страницы, который будет включать параметр, состоящий из адреса электронной почты и токена, а затем закодированный по базе 64.

При нажатии на ссылку пользователь попадет на страницу, где сначала будет проверен параметр: декодирование base 64, разделение электронной почты и токена, поиск записи доступа по электронной почте, хэш-токен и сравнение с сохраненным токеном и (необязательно) сравните срок действия с текущим (чтобы люди не пытались получить доступ к URL-адресу из электронного письма, отправленного несколько месяцев или лет назад).

Если все кошерно, пользователю показывается страница. Когда они завершат любое действие, которое им нужно сделать, вы удалите запись доступа.

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

ОБНОВЛЕНИЕ

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

using System;
using System.Security.Cryptography;
using System.Text;

public static class CryptoUtil
{
    // The following constants may be changed without breaking existing hashes.
    public const int SaltBytes = 32;
    public const int HashBytes = 32;
    public const int Pbkdf2Iterations = /* Some int here. Larger is better, but also slower. Something in the range of 1000-2000 works well. Don't expose this value. */;

    public const int IterationIndex = 0;
    public const int SaltIndex = 1;
    public const int Pbkdf2Index = 2;

    /// <summary>
    /// Creates a salted PBKDF2 hash of the password.
    /// </summary>
    /// <param name="password">The password to hash.</param>
    /// <returns>The hash of the password.</returns>
    public static string CreateHash(string password)
    {
        // TODO: Raise exception is password is null
        // Generate a random salt
        RNGCryptoServiceProvider csprng = new RNGCryptoServiceProvider();
        byte[] salt = new byte[SaltBytes];
        csprng.GetBytes(salt);

        // Hash the password and encode the parameters
        byte[] hash = PBKDF2(password, salt, Pbkdf2Iterations, HashBytes);
        return Pbkdf2Iterations.ToString("X") + ":" +
            Convert.ToBase64String(salt) + ":" +
            Convert.ToBase64String(hash);
    }

    /// <summary>
    /// Validates a password given a hash of the correct one.
    /// </summary>
    /// <param name="password">The password to check.</param>
    /// <param name="goodHash">A hash of the correct password.</param>
    /// <returns>True if the password is correct. False otherwise.</returns>
    public static bool ValidateHash(string password, string goodHash)
    {
        // Extract the parameters from the hash
        char[] delimiter = { ':' };
        string[] split = goodHash.Split(delimiter);
        int iterations = Int32.Parse(split[IterationIndex], System.Globalization.NumberStyles.HexNumber);
        byte[] salt = Convert.FromBase64String(split[SaltIndex]);
        byte[] hash = Convert.FromBase64String(split[Pbkdf2Index]);

        byte[] testHash = PBKDF2(password, salt, iterations, hash.Length);
        return SlowEquals(hash, testHash);
    }

    /// <summary>
    /// Compares two byte arrays in length-constant time. This comparison
    /// method is used so that password hashes cannot be extracted from
    /// on-line systems using a timing attack and then attacked off-line.
    /// </summary>
    /// <param name="a">The first byte array.</param>
    /// <param name="b">The second byte array.</param>
    /// <returns>True if both byte arrays are equal. False otherwise.</returns>
    private static bool SlowEquals(byte[] a, byte[] b)
    {
        uint diff = (uint)a.Length ^ (uint)b.Length;
        for (int i = 0; i < a.Length && i < b.Length; i++)
            diff |= (uint)(a[i] ^ b[i]);
        return diff == 0;
    }

    /// <summary>
    /// Computes the PBKDF2-SHA1 hash of a password.
    /// </summary>
    /// <param name="password">The password to hash.</param>
    /// <param name="salt">The salt.</param>
    /// <param name="iterations">The PBKDF2 iteration count.</param>
    /// <param name="outputBytes">The length of the hash to generate, in bytes.</param>
    /// <returns>A hash of the password.</returns>
    private static byte[] PBKDF2(string password, byte[] salt, int iterations, int outputBytes)
    {
        Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, salt);
        pbkdf2.IterationCount = iterations;
        return pbkdf2.GetBytes(outputBytes);
    }

    public static string GetUniqueKey(int length)
    {
        char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
        byte[] bytes = new byte[length];
        using (var rng = new RNGCryptoServiceProvider())
        {
            rng.GetNonZeroBytes(bytes);
        }
        var result = new StringBuilder(length);
        foreach (byte b in bytes)
        {
            result.Append(chars[b % (chars.Length - 1)]);
        }
        return result.ToString();
    }

    public static string Base64Encode(string str)
    {
        return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(str));
    }

    public static string Base64Decode(string str)
    {
        return System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(str));
    }

    public static string Base64EncodeGuid(Guid guid)
    {
        return Convert.ToBase64String(guid.ToByteArray());
    }

    public static Guid Base64DecodeGuid(string str)
    {
        return new Guid(Convert.FromBase64String(str));
    }
}

Затем я делаю что-то вроде следующего для создания сброса пароля:

var token = CryptoUtil.GetUniqueKey(16);
var hashedToken = CryptoUtil.CreateHash(token);
var emailToken = CryptoUtil.Base64Encode(string.Format("{0}:{1}", email, token));

Переменная hashedToken сохраняется в вашей базе данных, а emailToken — это то, что помещается в URL-адрес, отправляемый вашему пользователю. В действии, которое обрабатывает URL-адрес:

var parts = CryptoUtil.Base64Decode(emailToken).Split(':');
var email = parts[0];
var token = parts[1];

Найдите запись, используя email. Затем сравните, используя:

CryptoUtil.ValidateHash(token, hashedTokenFromDatabase)
02.02.2015
  • Этот подход кажется идеальным, но я не знаю, с чего начать. Как сгенерировать токен для одной вещи? Глядя на действие «забытый пароль» в контроллере учетных записей, оно использует функцию в UserManager, которую нельзя использовать для того, что я хочу сделать здесь. 05.02.2015
  • Спасибо, что приложил усилия к этому прекрасному объяснению, Крис. Теперь у меня все работает. 05.02.2015
  • Новые материалы

    Коллекции публикаций по глубокому обучению
    Последние пару месяцев я создавал коллекции последних академических публикаций по различным подполям глубокого обучения в моем блоге https://amundtveit.com - эта публикация дает обзор 25..

    Представляем: Pepita
    Фреймворк JavaScript с открытым исходным кодом Я знаю, что недостатка в фреймворках JavaScript нет. Но я просто не мог остановиться. Я хотел написать что-то сам, со своими собственными..

    Советы по коду Laravel #2
    1-) Найти // You can specify the columns you need // in when you use the find method on a model User::find(‘id’, [‘email’,’name’]); // You can increment or decrement // a field in..

    Работа с временными рядами спутниковых изображений, часть 3 (аналитика данных)
    Анализ временных рядов спутниковых изображений для данных наблюдений за большой Землей (arXiv) Автор: Рольф Симоэс , Жильберто Камара , Жильберто Кейрос , Фелипе Соуза , Педро Р. Андраде ,..

    3 способа решить квадратное уравнение (3-й мой любимый) -
    1. Методом факторизации — 2. Используя квадратичную формулу — 3. Заполнив квадрат — Давайте поймем это, решив это простое уравнение: Мы пытаемся сделать LHS,..

    Создание VR-миров с A-Frame
    Виртуальная реальность (и дополненная реальность) стали главными модными терминами в образовательных технологиях. С недорогими VR-гарнитурами, такими как Google Cardboard , и использованием..

    Демистификация рекурсии
    КОДЕКС Демистификация рекурсии Упрощенная концепция ошеломляющей О чем весь этот шум? Рекурсия, кажется, единственная тема, от которой у каждого начинающего студента-информатика..