Реализуем в приложении Golang двухфакторную аутентификацию 2FA с временным одноразовым паролем TOTP.
Одних паролей для защиты от несанкционированного доступа уже недостаточно, безопасность обеспечивается аутентификацией — причем двухфакторной, далее обозначаемой как 2FA. Двумя формами идентификации — паролем и временным кодом — риск несанкционированного доступа при 2FA снижается значительно.
Для реализации в веб-приложении на Golang двухфакторной аутентификации 2FA потребуются:
- Базовые знания языка программирования Golang.
- Настроенная в компьютере среда разработки.
- Знание концепций веб-разработки, таких как HTTP-запросы и HTML-шаблоны.
- Текстовый редактор или интегрированная среда разработки для написания и редактирования кода Go.
Начнем с основ двухфакторной аутентификации и принципов ее работы.
Двухфакторная аутентификация
Принцип работы 2FA
При 2FA к привычной авторизации по имени пользователя и пароля добавляется дополнительный уровень безопасности. Прежде чем войти в учетную запись, пользователь проходит две формы идентификации.
Продемонстрируем это реальным сценарием системы «клиент-банк»:
- При однофакторной аутентификации в аккаунт заходим только по логину и паролю. Какой-то уровень безопасности этим обеспечивается, но такой вход уязвим для взлома или кражи пароля.
- При двухфакторной аутентификации добавляется дополнительный этап: введя логин и пароль, в текстовом сообщении или специальном приложении аутентификации на смартфоне получаем одноразовый код.
- Что нужно знать: логин и пароль.
- Что нужно иметь: одноразовый код, отправленный на смартфон.
Для доступа к аккаунту нужен не только пароль, но и смартфон с отправленным на него одноразовым кодом. Даже зная пароль, без этого смартфона войти не получится.
Что такое TOTP?
Это метод реализации 2FA с помощью временных одноразовых паролей. Им генерируется шестизначный код, который каждые 30 секунд меняется, чем обеспечивается дополнительный уровень безопасности.
Принцип работы TOTP:
- Используется уникальный секретный ключ, доступный для устройства пользователя и сервера аутентификации.
- Одноразовый пароль генерируется устройством и сервером по текущему времени синхронизированно.
- Одноразовый пароль генерируется в TOTP криптографическим алгоритмом, обычно это HMAC-SHA1.
- Срок действия каждого одноразового пароля непродолжителен, обычно 30 секунд.
Например, при настройке TOTP для пользователя:
- Сервером генерируется секретный ключ, который передается на устройство пользователя.
- С этим ключом и по текущему времени устройством пользователя генерируется шестизначный код.
- Этот код вместе с паролем указывается пользователем при входе.
- Код проверяется сервером по секретному ключу и сроку действия внутри временного интервала.
Теперь переходим к реализации 2FA в веб-приложении на Golang с TOTP.
Настройка среды Golang
Выполним поэтапную настройку среды Golang для реализации 2FA:
- Устанавливаем Golang.
- Настраиваем рабочую область, создаем каталог для проектов 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 при повышении безопасности.
Запуск приложения и тестирование
- Для запуска приложения выполняем файл
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:
- Загружаем и устанавливаем приложение из App Store для iOS или Google Play Store для Android.
- Выбрав в приложении Scan a QR code («Сканировать QR-код») или Manual entry («Ручной ввод»), добавляем учетную запись.
- Камерой устройства сканируем QR-код на странице входа в приложение.
- После сканирования проверяем настройку: вводим в приложение шестизначный код TOTP, обновляемый каждые 30 секунд.
Рекомендации для 2FA
Для эффективности и применимости двухфакторной аутентификации важно следовать рекомендациям по реализации 2FA в веб-приложении Golang:
1. Информирование пользователей о резервных кодах
Информируйте пользователей о важности резервных кодов, предоставляйте механизмы их генерирования и безопасного хранения. Резервные коды — это запасной вариант в случае недоступности основных методов аутентификации.
2. Ограничение попыток аутентификации
Чтобы предотвратить атаки полного перебора и попытки несанкционированного доступа, внедрите механизмы ограничения скорости. Чтобы снизить риск атак с подстановкой украденных учетных данных, ограничьте количество попыток входа за период времени.
3. Приоритет пользовательского взаимодействия
Приоритизируйте пользовательское взаимодействие в процессе настройки 2FA и аутентификации. Чтобы применение пользователями 2FA обходилось без разочарований, разрабатывайте интуитивно понятные интерфейсы с четкими инструкциями и минимизируйте шероховатости.
4. Надежное хранение секретов
Обеспечьте безопасное хранение пользовательских секретов и конфиденциальной информации, связанной с 2FA. Чтобы защитить данные пользователей от несанкционированного доступа или разглашения, внедрите надежные методы шифрования и хеширования.
Заключение
Мы изучили концепцию 2FA и ее значение для повышения безопасности, поэтапно настроили среду Golang, интегрировали в проект библиотеку и рассмотрели различные аспекты реализации 2FA, включая генерирование и хранение секретов, обработку аутентификации пользователя и проверку кодов TOTP.
Остается просто добавить 2FA в веб-приложение Golang, вот полный код проекта.
Читайте также:
- 7 типичных ошибок в Go-интерфейсах
- AEGIS — система аутентификации платформы Ankorstore
- Лучшие практики для эффективного кода на Golang. Часть 2
Читайте нас в Telegram, VK и Дзен
Перевод статьи Ege Aytin: How to Implement Two-Factor Authentication (2FA) with TOTP in Golang