Реализуем в приложении Golang двухфакторную аутентификацию 2FA с временным одноразовым паролем TOTP.

Одних паролей для защиты от несанкционированного доступа уже недостаточно, безопасность обеспечивается аутентификацией  —  причем двухфакторной, далее обозначаемой как 2FA. Двумя формами идентификации  —  паролем и временным кодом  —  риск несанкционированного доступа при 2FA снижается значительно.

Для реализации в веб-приложении на Golang двухфакторной аутентификации 2FA потребуются:

  • Базовые знания языка программирования Golang.
  • Настроенная в компьютере среда разработки.
  • Знание концепций веб-разработки, таких как HTTP-запросы и HTML-шаблоны.
  • Текстовый редактор или интегрированная среда разработки для написания и редактирования кода Go.

Начнем с основ двухфакторной аутентификации и принципов ее работы.

Двухфакторная аутентификация

Принцип работы 2FA

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

Продемонстрируем это реальным сценарием системы «клиент-банк»:

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

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

Что такое TOTP?

Это метод реализации 2FA с помощью временных одноразовых паролей. Им генерируется шестизначный код, который каждые 30 секунд меняется, чем обеспечивается дополнительный уровень безопасности.

Принцип работы TOTP:

  • Используется уникальный секретный ключ, доступный для устройства пользователя и сервера аутентификации.
  • Одноразовый пароль генерируется устройством и сервером по текущему времени синхронизированно.
  • Одноразовый пароль генерируется в TOTP криптографическим алгоритмом, обычно это HMAC-SHA1.
  • Срок действия каждого одноразового пароля непродолжителен, обычно 30 секунд.

Например, при настройке TOTP для пользователя:

  • Сервером генерируется секретный ключ, который передается на устройство пользователя.
  • С этим ключом и по текущему времени устройством пользователя генерируется шестизначный код.
  • Этот код вместе с паролем указывается пользователем при входе.
  • Код проверяется сервером по секретному ключу и сроку действия внутри временного интервала.

Теперь переходим к реализации 2FA в веб-приложении на Golang с TOTP.

Настройка среды Golang

Выполним поэтапную настройку среды Golang для реализации 2FA:

  1. Устанавливаем Golang.
  2. Настраиваем рабочую область, создаем каталог для проектов Golang:
mkdir go-2fa-demo
cd go-2fa-demo

3. Клонируем представленный здесь образец проекта или создаем такую его структуру на Golang:

go-2fa-demo/
├── main.go
├── templates/
│ ├── dashboard.html
│ ├── index.html
│ ├── login.html
│ ├── qrcode.html
│ └── validate.html

4. Устанавливаем зависимости, здесь это сторонняя библиотека для генерирования TOTP:

go get github.com/pquerna/otp/totp

5. Проверяем установку и корректность настройки среды Golang, запуская в терминале образец проекта:

go run main.go

6. Появится сообщение о том, что сервер запускается на порту 8080.

Теперь среда Golang готова к реализации двухфакторной аутентификации в веб-приложении.

Реализация 2FA на Golang

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

Этап 1. Выбор метода 2FA

Среди доступных методов 2FA выделяется TOTP с временными одноразовыми паролями и приложением-аутентификатором Google Authenticator.

Почему TOTP?

TOTP, как метод 2FA, активно применяется многими онлайн-сервисами.

Вот его преимущества:

  • Безопасность: в TOTP генерируются временные коды непродолжительного действия, поэтому они менее подвержены атакам повторного воспроизведения.
  • Автономная работа: в TOTP для генерирования кода интернет-подключение не требуется, пользователи аутентифицируются даже в офлайне.
  • Стандартизация: TOTP стандартизирован согласно RFC 6238, чем обеспечивается совместимость с различными приложениями и библиотеками аутентификации.
  • Удобство: коды TOTP легко генерируются и вводятся, что хорошо для пользовательского взаимодействия.

Рекомендации

Прежде чем реализовывать TOTP в приложении, обратите внимание на:

  • Адаптацию пользователей: убедитесь, что они знакомы с TOTP и им удобно пользоваться приложениями аутентификации, такими как Google Authenticator.
  • Механизм резервного копирования: предоставьте пользователям резервные коды на случай, если они потеряют доступ к устройству аутентификации.
  • Сочетание безопасности и удобства: найдите баланс между ними, реализуя дополнительные меры безопасности, такие как ограничение скорости, без ущерба для пользовательского взаимодействия.

Выбрав метод 2FA, легко интегрируем его в веб-приложение с помощью библиотеки.

Этап 2. Интеграция библиотеки 2FA

Теперь интегрируем стороннюю библиотеку github.com/pquerna/otp/totp, в ней имеется функционал для генерирования и проверки кодов TOTP.

Вот ее преимущества:

  • Многофункциональность: библиотекой предоставляется всесторонняя поддержка генерирования, проверки и настройки TOTP.
  • Хорошее сопровождение: в библиотеке, разработанной и сопровождаемой авторитетным автором, регулярно получаются обновления и устраняются ошибки.
  • Поддержка сообщества: эта библиотека с обширной документацией активно применяется в экосистеме Golang, пользуется поддержкой сообщества.

Установка

Интегрируем github.com/pquerna/otp/totp в проект Golang:

go get github.com/pquerna/otp/totp

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

Переходим к реализации 2FA в приложении.

Этап 3. Настройка маршрутов

Теперь с помощью пакета net/http настроим в веб-приложении маршруты для обработки входящих HTTP-запросов и направления пользователей к соответствующим обработчикам.

Импорт необходимых пакетов

Прежде чем определять маршруты, импортируем пакеты:

import (
"net/http"
)

Определение маршрутов

В файле main.go проекта с помощью функции http.HandleFunc() определяем маршруты, каждый из них соответствует конкретному пути URL-адреса и связан с функцией-обработчиком, в которой обрабатываются запросы по этому пути:

func main() {
// Определяются маршруты
http.HandleFunc("/", homeHandler)
http.HandleFunc("/login", loginHandler)
http.HandleFunc("/dashboard", dashboardHandler)
http.HandleFunc("/generate-otp", generateOTPHandler)
http.HandleFunc("/validate-otp", validateOTPHandler)

// Запускается сервер
http.ListenAndServe(":8080", nil)
}

В этом коде:

  • Функцией http.HandleFunc() регистрируется функция-обработчик для заданного шаблона  —  пути URL-адреса  —  и принимается два аргумента: шаблон URL-адреса и функция-обработчик, выполняемая при соответствии запроса шаблону.
  • Маршрутами:
  • / обрабатываются запросы к корневому URL-адресу, пользователи направляются на домашнюю страницу;
  • /login обрабатываются запросы авторизации и аутентификация пользователя;
  • /dashboard обрабатываются запросы доступа к дашборду после пройденной аутентификации;
  • /generate-otp обрабатываются запросы генерирования одноразового пароля для 2FA;
  • /validate-otp обрабатываются запросы проверки одноразового пароля, введенного пользователем в процессе настройки 2FA или авторизации.

Этап 4. Создание домашней страницы

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

Файл шаблона

Сначала в каталоге templates проекта создаем файл HTML- шаблона index.html, которым определятся структура и содержимое домашней страницы:

<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Go 2FA Demo</title>
<link rel="stylesheet" href="<https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css>">
</head>
<body>
<div class="container mt-5">
<h1 class="mb-3">Welcome to the Go 2FA Demo</h1>
<a href="/login" class="btn btn-primary">Login</a>
</div>
</body>
</html>

Функция-обработчик

Затем в файле main.go определяем функцию-обработчик homeHandler, которой при обращении пользователей к корневому URL-адресу отображается домашняя страница:

func homeHandler(w http.ResponseWriter, r *http.Request) {
// Выполняется шаблон «index.html»
err := templates.ExecuteTemplate(w, "index.html", nil)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
}

В этом коде:

  • С помощью HTML-разметки файлом шаблона index.html определяется структура домашней страницы, в том числе приветственное сообщение и кнопка для перехода на страницу авторизации.
  • Функцией-обработчиком homeHandler обрабатываются запросы к корневому URL-адресу /, выполняется шаблон index.html и в качестве ответа отправляется отрисованное HTML-содержимое.

Этап 5. Создание страницы авторизации

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

Файл шаблона

В каталоге templates создаем файл HTML- шаблона login.html, которым определятся структура и содержимое страницы авторизации:

<!-- templates/login.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
<link rel="stylesheet" href="<https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css>">
</head>
<body>
<div class="container mt-5">
<h1 class="mb-3">Login</h1>
<form action="/login" method="post" class="needs-validation">
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" class="form-control" required>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" class="form-control" required>
</div>
<button type="submit" class="btn btn-success">Login</button>
</form>
</div>
</body>
</html>

Функция-обработчик

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

func loginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
// Отрисовывается шаблон «login.html» для GET-запросов
err := templates.ExecuteTemplate(w, "login.html", nil)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
return
}

// Обрабатываются POST-запросы для аутентификации пользователя
// Код для обработки отправки форм и аутентификации пользователя
}

В этом коде:

  • С помощью HTML-разметки файлом шаблона login.html определяется структура страницы авторизации, в том числе поля формы для ввода имени пользователя и пароля, а также кнопка отправки для инициирования процесса авторизации.
  • Функцией-обработчиком loginHandler обрабатываются запросы к пути URL-адреса /login: для GET-запросов отрисовывается шаблон login.html отображения страницы входа, для POST-запросов обрабатываются отправка форм и аутентификация пользователя, реализуемые позже.

Этап 6. Обработка аутентификации пользователя

Благодаря аутентификации доступ к защищенным ресурсам получают только авторизованные пользователи.

Функция-обработчик

В функции loginHandler файла main.go веб-приложения Golang реализуем логику аутентификации пользователей на основе указываемых учетных данных:

func loginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
// Отрисовывается шаблон «login.html» для GET-запросов
err := templates.ExecuteTemplate(w, "login.html", nil)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
return
}

// Для POST-запросов парсятся данные форм
if err := r.ParseForm(); err != nil {
http.Error(w, "Error parsing form", http.StatusBadRequest)
return
}

// Из данных формы извлекаются имя пользователя и пароль
username := r.Form.Get("username")
password := r.Form.Get("password")

// Выполняется аутентификация пользователя
user, ok := users[username]
if !ok || user.Password != password {
// Если аутентификация не пройдена, пользователь перенаправляется на страницу входа
http.Redirect(w, r, "/login", http.StatusFound)
return
}

// Если аутентификация пройдена, он перенаправляется в дашборд
http.Redirect(w, r, "/dashboard", http.StatusFound)
}

В этом коде:

  • Функцией-обработчиком loginHandler обрабатываются GET- и POST-запросы к пути URL-адреса /login: для GET-запросов страница авторизации отрисовывается с помощью шаблона login.html, для POST-запросов, чтобы извлекать введенные пользователем логин и пароль, парсятся данные форм.
  • Внутри блока обработки POST-запроса пользователь аутентифицируется функцией на основе указываемых учетных данных. Ею проверяется, существует ли логин в карте users и совпадает ли пароль с паролем, сохраненным для этого пользователя. Если аутентификация не пройдена, пользователь возвращается на страницу входа. А если пройдена, перенаправляется в дашборд.

Этап 7. Генерирование секрета TOTP

Настройку 2FA веб-приложения начинаем с реализации функциональности для генерирования секрета TOTP каждого пользователя.

Функция-обработчик

Чтобы обрабатывать генерирование секретов TOTP, в файле main.go создаем функцию-обработчик generateOTPHandler:

func generateOTPHandler(w http.ResponseWriter, r *http.Request) {
// Из параметров запроса извлекается имя пользователя
username := r.URL.Query().Get("username")

// Из «базы данных» в памяти извлекаются данные пользователя
user, ok := users[username]
if !ok {
http.Redirect(w, r, "/", http.StatusFound)
return
}

// Генерируется секрет TOTP, если еще не сгенерирован
if user.Secret == "" {
secret, err := totp.Generate(totp.GenerateOpts{
Issuer: "Go2FADemo",
AccountName: username,
})
if err != nil {
http.Error(w, "Failed to generate TOTP secret.", http.StatusInternalServerError)
return
}
user.Secret = secret.Secret()
}

// Создается URL-адрес одноразового пароля для генерирования QR-кода
otpURL := fmt.Sprintf("otpauth://totp/Go2FADemo:%s?secret=%s&issuer=Go2FADemo", username, user.Secret)

// Готовятся данные для отправки в шаблон
data := struct {
OTPURL string
Username string
}{
OTPURL: otpURL,
Username: username,
}

// Отрисовывается шаблон «qrcode.html» с данными URL-адреса одноразового пароля
err := templates.ExecuteTemplate(w, "qrcode.html", data)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
}

В этом коде:

  • Функцией-обработчиком generateOTPHandler обрабатываются запросы генерирования секретов TOTP для пользователей: из параметров запроса извлекается имя пользователя, а затем проверяется, существует ли пользователь в «базе данных» в памяти. Если существует, то функцией totp.Generate пакета otp/totp генерируется секрет TOTP, сохраняемый в структуре данных пользователя. Если секрет сгенерирован, функцией создается URL-адрес одноразового пароля для генерирования QR-кода. В итоге отрисовывается шаблон qrcode.html с данными URL-адреса одноразового пароля.

Этап 8. Отображение QR-кода

С приложениями-аутентификаторами 2FA удобно настраивать отображением QR-кода, для чего мы сделаем в приложении отдельный HTML-файл.

Файл шаблона

В каталоге templates создаем файл HTML- шаблона qrcode.html, которым определятся структура и содержимое для отображения QR-кода:

<!-- templates/qrcode.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>QR Code</title>
<link rel="stylesheet" href="<https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css>">
</head>
<body>
<div class="container mt-5 text-center">
<h1 class="mb-3">Scan QR Code with Authenticator App</h1>
<img src="<https://chart.googleapis.com/chart?cht=qr&chl={{.OTPURL}>}&chs=180x180&choe=UTF-8&chld=L|2" class="img-fluid mb-3" alt="QR Code">
<form action="/validate-otp" method="get">
<input type="hidden" name="username" value="{{.Username}}">
<button type="submit" class="btn btn-primary">I've Scanned the QR Code</button>
</form>
</div>
</body>
</html>

В этом коде:

  • Файлом шаблона qrcode.html определяется структура страницы для отображения QR-кода, в том числе тег <img> для показа сгенерированного в Google Chart API изображения QR-кода, а также кнопка для указания пользователями, что QR-код их приложением-аутентификатором отсканирован.

Этап 9. Проверка кода TOTP

Чтобы обезопасить процесс двухфакторной аутентификации, реализуем функциональность для проверки вводимого пользователями кода TOTP.

Функция-обработчик

Чтобы обрабатывать проверку кодов TOTP, создаем в файле main.go функцию-обработчик validateOTPHandler:

func validateOTPHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
// Из параметров запроса извлекается имя пользователя
username := r.URL.Query().Get("username")

// Отрисовывается шаблон «validate.html», в него передается имя пользователя
err := templates.ExecuteTemplate(w, "validate.html", struct{ Username string }{Username: username})
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}

case "POST":
// Парсятся данные форм
if err := r.ParseForm();

err != nil {
http.Error(w, "Error parsing form", http.StatusBadRequest)
return
}

// Из данных формы извлекаются имя пользователя и код TOTP
username := r.FormValue("username")
otpCode := r.FormValue("otpCode")

// Из «базы данных» в памяти извлекаются данные пользователя
user, exists := users[username]
if !exists {
http.Error(w, "User does not exist", http.StatusBadRequest)
return
}

// Код TOTP проверяется этой библиотекой TOTP
isValid := totp.Validate(otpCode, user.Secret)
if !isValid {
// Если проверка не пройдена, пользователь возвращается на страницу проверки
http.Redirect(w, r, fmt.Sprintf("/validate-otp?username=%s", username), http.StatusTemporaryRedirect)
return
}

// Если проверка пройдена, задаются куки сеанса и пользователь перенаправляется в дашборд
http.SetCookie(w, &http.Cookie{
Name: "authenticatedUser",
Value: "true",
Path: "/",
MaxAge: 3600, // Например, один час
})
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)

default:
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
}
  • Функцией-обработчиком validateOTPHandler обрабатываются GET- и POST-запросы:
  • когда получается GET-запрос, из его параметров извлекается имя пользователя и отображается шаблон validate.html, в который оно передается;
  • когда получается POST-запрос, для извлечения отправленных пользователем логина и кода TOTP парсятся данные форм. Затем код TOTP проверяется библиотекой TOTP: если код действителен, задаются куки сеанса для указания на то, что аутентификация пройдена, и пользователь перенаправляется в дашборд; если код недействителен, пользователь возвращается на страницу проверки и пробует аутентифицироваться заново.

Этап 10. Обработчик дашборда

После успешной аутентификации страница дашборда отображается его обработчиком.

Функция-обработчик

Чтобы обрабатывать запросы дашборда, создаем в файле main.go функцию-обработчик dashboardHandler:

func dashboardHandler(w http.ResponseWriter, r *http.Request) {
// Из куков сеанса извлекается имя пользователя, прошедшего аутентификацию
username, err := r.Cookie("authenticatedUser")
if err != nil || username.Value == "" {
// Если пользователь не аутентифицировался, он перенаправляется на домашнюю страницу
http.Redirect(w, r, "/", http.StatusFound)
return
}

// Отрисовывается шаблон «dashboard.html»
err = templates.ExecuteTemplate(w, "dashboard.html", nil)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}

В этом коде:

  • Функцией обработчика dashboardHandler из куков сеанса извлекается имя пользователя, прошедшего аутентификацию: неаутентифицированный пользователь  —  нет куков сеанса или они просрочены  —  перенаправляется на домашнюю страницу; для аутентифицированного отрисовывается шаблон dashboard.html отображения страницы дашборда.

Тестирование реализации 2FA

Тестированием обеспечиваются надежность и эффективность реализации 2FA при повышении безопасности.

Запуск приложения и тестирование

  1. Для запуска приложения выполняем файл main на Go и запускаем веб-сервер:
cd go-2fa-demo
go run main.go

2. Для доступа к приложению переходим в браузере на http://localhost:8080/:

3. Нажимая кнопку Login («Войти»), инициируем процесс аутентификации и вводим имя пользователя и пароль, например john и password:

4. После входа для настройки TOTP генерируется QR-код:

5. Чтобы отсканировать QR-код и настроить 2FA для учетной записи пользователя, воспользуемся совместимым с TOTP приложением-аутентификатором вроде Google Authenticator.

Для проверки аутентификации вводим код TOTP, сгенерированный приложением-аутентификатором:

Google Authenticator

Google Authenticator  —  это приложение-аутентификатор, которым генерируются коды TOTP для аутентификации 2FA. Безопасность учетных записей пользователей повышается им благодаря надежному хранению секретов и генерирированию временных кодов.

Настроим Google Authenticator:

  1. Загружаем и устанавливаем приложение из App Store для iOS или Google Play Store для Android.
  2. Выбрав в приложении Scan a QR code («Сканировать QR-код») или Manual entry («Ручной ввод»), добавляем учетную запись.
  3. Камерой устройства сканируем QR-код на странице входа в приложение.
  4. После сканирования проверяем настройку: вводим в приложение шестизначный код TOTP, обновляемый каждые 30 секунд.

Рекомендации для 2FA

Для эффективности и применимости двухфакторной аутентификации важно следовать рекомендациям по реализации 2FA в веб-приложении Golang:

1. Информирование пользователей о резервных кодах

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

2. Ограничение попыток аутентификации

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

3. Приоритет пользовательского взаимодействия

Приоритизируйте пользовательское взаимодействие в процессе настройки 2FA и аутентификации. Чтобы применение пользователями 2FA обходилось без разочарований, разрабатывайте интуитивно понятные интерфейсы с четкими инструкциями и минимизируйте шероховатости.

4. Надежное хранение секретов

Обеспечьте безопасное хранение пользовательских секретов и конфиденциальной информации, связанной с 2FA. Чтобы защитить данные пользователей от несанкционированного доступа или разглашения, внедрите надежные методы шифрования и хеширования.

Заключение

Мы изучили концепцию 2FA и ее значение для повышения безопасности, поэтапно настроили среду Golang, интегрировали в проект библиотеку и рассмотрели различные аспекты реализации 2FA, включая генерирование и хранение секретов, обработку аутентификации пользователя и проверку кодов TOTP.

Остается просто добавить 2FA в веб-приложение Golang, вот полный код проекта.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Ege Aytin: How to Implement Two-Factor Authentication (2FA) with TOTP in Golang

Предыдущая статья5 реальных способов достичь сбалансированности трудовой жизни
Следующая статьяFrontend Masters: принципы SOLID в React/React Native