Я часто путаю понятия авторизации и аутентификации между собой, поэтому решил создать материал, который закрепил бы эти понятия через какой-то практический опыт.
Термины аутентификации и авторизации для меня достаточно расплывчаты, если представить их в контексте практической бэкенд разработки. Я не совсем понимал логику работы аутентификации в фреймворках. В добавок эти концепции можно легко перепутать между собой. При общении с коллегами это было достаточно часто.
В данном случае под бэкенд разработкой я подразумеваю создание разного рода API с использованием протокола HTTP. Мне, как разработчику, часто приходится писать очередной REST API для мобильных приложений или веб страниц. Например, берем Flask и быстро пишем микросервис с API для конкретной части приложения заказчика. Или берем django и django-rest-framework и пишем REST API для стартапа. Во всех таких проектах когда-нибудь, да приходится иметь дело с аутентификацей. Аутентификация через JWT, Oauth. И всегда реализация этой части сводилась к гуглению статей, копированию кода и подходов.
Я же хочу создать статью, которая поэтапно раскроет базовые концепции аутентификации и авторизации на практике, после чего вы сможете применять эти знания, чтобы не путать понятия и строить свои системы. Надеюсь, вам будет легче решать связанные с этим задачи. Я не собираюсь просто в лоб написать, что означают эти термины. Статья написана в таком стиле, будто бы вы изначально находитесь у истоков создания аутентификации и придумываете свой велосипед.
Сервис для создания заметок
Давайте представим, что вы делаете веб-сервис API для заметок. Новый стартап кремниевой долины. В качестве фреймворка вы выбрали Flask, БД – sqlite, для доступа к БД через ORM – SQLAlchemy.
Сервис должен уметь сохранять заметки с заголовком и текстом. Начальная модель данных достаточно простая – заголовок(varchar 256) и содержание(text).
# app.py
from flask import Flask
app = Flask(__name__)
# db.py
from flask_sqlalchemy import SQLAlchemy
from app import app
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///./notes.db'
db = SQLAlchemy(app)
def init_db():
from models import User, Note # noqa
db.create_all()
# models.py
class Note(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(256))
body = db.Column(db.Text)
def __repr__(self):
return '<Note %r>' % self.title
Вы написали эндпоинт, который принимает данные для заметки, вызывает сервисную функцию create_note и возвращает HTTP ответ с кодом 201 CREATED и json, дублирующий информацию о заметке. Почему такой статус? Потому что это стандартный ответ для методов создания сущностей в REST API.
Этот эндпоинт может быть использован любой персоной, у кого есть доступ к сервису. Например, наш сервис тестирует ваш друг Вован. Вован садится за компьютер, включает свой любимый HTTP клиент и шлет запросы. Заметка Вована сохраняется на сервере.
# services.py
from flask import Request
from db import db
from models import Note
def create_note(request: Request) -> Note:
note = Note(
title=request.json['title'],
body=request.json['body'],
)
db.session.add(note)
db.session.commit()
return note
# main.py
from flask import request, jsonify
import services as notes_service
from app import app
@app.route("/notes", methods=['POST'])
def create_note():
note = notes_service.create_note(request=request)
response = make_response(
jsonify(
{
'title': note.title,
'body': note.body,
}
), 201
)
response.headers["Content-Type"] = "application/json"
return response
Вован решил позвать еще одного друга – Димона. Димон садится за компьютер, включает свой любимый HTTP клиент и тоже шлет запросы. Затем вы вместе любуетесь своими заметками в базе данных.
После тестирования сервиса вы с Вованом и Диманом обсуждаете, что можно
добавить в ваш новый стартап. Вован говорит, что сейчас заметки кладутся в
общую кучу и невозможно понять, кто какую заметку написал. Новые юзеры точно
так же будут класть заметки в общую кучу. Разобраться, кто ее написал, будет
очень сложно.
Идентификация
Вы обсуждаете, что делать дальше. Как понять, кто написал заметку? Нужно узнать человека по какому-то признаку. Например, по имени, но имя должно быть уникальным. Можно использовать псевдоним. Пусть Димон будет сообщать, что он dimon.
Чтобы понять, кто есть кто в вашей системе, люди должны пометить себя какой-то уникальной меткой. Эти люди должны быть нам известны, то есть известны нашей базе денных. Для этого мы создадим отдельную таблицу/модель пользователя. Новая модель пользователя будет иметь псевдоним, который является набором символом, причем уникальным.
Таким образом, username позволит нам уникально обозначить пользователя.
from db import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return '<User %r>' % self.username
class Note(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(256))
body = db.Column(db.Text)
def __repr__(self):
return '<Note %r>' % self.title
Теперь вы создаете пару записей в базе данных и заносите туда 2 пользователя – dimon_01 и vovan. Наша база данных теперь знает об этих пользователях.
Теперь вам нужно дать возможно связывать заметки и пользователей. Для этого вы добавляет новое поле в модель заметки, которое указывает на автора заметки. Такая связь называется один ко многим - один пользователь может иметь много заметок.
# models.py
from db import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return '<User %r>' % self.username
class Note(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(256))
body = db.Column(db.Text)
user_id = db.Column(
db.Integer, db.ForeignKey('user.id'),
nullable=False
)
user = db.relationship(
'User',
backref=db.backref('notes', lazy=True)
)
def __repr__(self):
return '<Note %r>' % self.title
После того, как мы создали записи пользователей в БД, мы хотим как-то использовать свои учетные данные(username) для создания заметки. Для этого в json помимо добавления данных о заметке(ее название и тело) мы будем передавать еще и учетные данные в поле credentials. В коде эндпоинта мы можем добавить вызов функции, которая считывает учетные данные и ищет пользователя в БД по его псевдониму. Если пользователь найден – возвращаем экземпляр класса модели юзера и передаем в сервисную функцию create_note, в которой создаем заметку с указанием автора.
Функция, считывающая данные пользователя и находящая его в БД, это есть идентификация пользователя.
# main.py
from typing import Optional
from flask import request, jsonify
import services as notes_service
from app import app
from models import User
def identify_user(credentials: Optional[dict]) -> Optional[User]:
if credentials is None:
return None
username = credentials.get('username')
user = User.query.filter_by(username=username).first()
return user
@app.route("/notes", methods=['POST'])
def create_note_api():
credentials = request.json().get('credentials')
user = identify_user(credentials=credentials)
note = notes_service.create_note(request=request, user=user)
response = make_response(
jsonify(
{
'title': note.title,
'body': note.body,
}
), 201
)
response.headers["Content-Type"] = "application/json"
return response
# services.py
from flask import Request
from db import db
from models import Note, User
def create_note(request: Request, user: User) -> Note:
note = Note(
title=request.json['title'],
body=request.json['body'],
user=user,
)
db.session.add(note)
db.session.commit()
return note
Более формальное определение идентификации можно получить из википедии и викисловаря
Викисловарь:
комп. процесс определения какой-либо сущности (устройства, объекта или данных) путём присвоения уникального идентификатора либо сравнения идентификатора с перечнем присвоенных идентификаторов
Википедия:
Идентифика́ция в информационных системах — процедура, в результате выполнения которой для субъекта идентификации выявляется его идентификатор, однозначно идентифицирующий этого субъекта в информационной системе. Для выполнения процедуры идентификации в информационной системе субъекту предварительно должен быть назначен соответствующий идентификатор (то есть проведена регистрация субъекта в информационной системе).
В нашем случае мы присвоили уникальные идентификаторы пользователям в виде их псевдонимов(username), а затем использовали их, чтобы найти пользователей в БД. Поиск в БД это и есть сравнение идентификатора пользователя с перечнем присвоенных идентификаторов(определение из викисловаря), то есть идентификация. В Других случаях мы можем не проводить поиск в БД, а просто проверить наличие уникального идентификатора в сообщении и принять это за идентификацию, но сейчас нам нужно именно найти пользователя и сравнить пароли.
Аутентификация
В нашем решении есть одна проблема. Что будет, если кто-то другой представит себя пользователем, которым он не является? Например, какой-нибудь злоумышленник в credentials отошлет тот же самый username, который использует Вован.
Ответ прост – злоумышленник будет действовать от имени Вована. Это дыра в безопасности. Чтобы защититься от этого пользователю нужно доказать, что он является Вованом(prove user’s identity).
Вы собираете команду и размышляете, как Вован может доказать, что он и есть Вован.
Сказать, что Вован это Вован, уже недостаточно. Он должен предоставить только то, что есть только у него. Что может охарактеризовать и выделить Вована от остальных людей и что знает только он? Набор атомов? Уникальный ДНК? Вована отличают от остальных его темные волосы. Пусть вместе со своим псевдонимом сообщит и то, что у него темные волосы. А еще у него есть собака по имени Тузик. Еще его любимый цвет – красный. А ведь по сути это все факты о нем. Его характеристика Пусть Вован сам выбирает, какой факт о нем можно сообщить, чтобы система поверила ему. Нам хватит и одного факта Это, наверное, будет секретная фраза. Один фактор для аутентификации.
Вы пришли к выводу, что к модели юзера нужно добавить поле, которое будет отражать какой-то характеристику Вована. И это что-то будет одно. Вы вспоминаете, что в фильмах люди используют такие фразы и называют паролями. Вам нужен будет один фактор для аутентификации - пароль. В добавок вы узнали, что пароли нужно хэшироваать, но вдаваться в детали тут не будем.
from db import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(120), unique=True, nullable=False)
password = db.Column(db.String(256))
def __repr__(self):
return '<User %r>' % self.username
class Note(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(256))
body = db.Column(db.Text)
user_id = db.Column(
db.Integer, db.ForeignKey('user.id'),
nullable=False
)
user = db.relationship(
'User',
backref=db.backref('notes', lazy=True)
)
def __repr__(self):
return '<Note %r>' % self.title
Таким образом, мы дополняем БД паролями, которые знают только админ. Админ затем передает лично пароли Вовану и Димон. Те, в свою очередь, используют их при взаимодействии с эндпоинтом для создания заметок.
Внутри эндпоинта при успешном сценарии происходит следующее:
Мы идентифицируем пользователя по псевдониму(username);
Сравниваете пароли;
Сервисная функция выполняется и создается новая заметка, привязанная к пользователю.
После идентификации пользователя вы сравниваете пароли. Если они совпадают, то пользователь подтверждает свою личность(prove user’s identity. 1 и 2 шаги в совокупности называются аутентификацией.
# main.py
from typing import Optional
from flask import request, jsonify, make_response
import services as notes_service
from app import app
from models import User
from utils import hash_password
def authenticate_user(user: Optional[User], password: str) -> bool:
if user is None:
return False
return user.password == hash_password(password)
def identify_user(credentials: Optional[dict]) -> Optional[User]:
if credentials is None:
return None
username = credentials.get('username')
user = User.query.filter_by(username=username).first()
return user
@app.route("/notes", methods=['POST'])
def create_note_api():
credentials = request.json().get('credentials')
user = identify_user(credentials=credentials)
password = credentials.get('password')
if not authenticate_user(user=user, password=password):
# Can't authenticate user, permission denied
response = make_response(
jsonify(
{
'msg': "Not authenticated"
}
), 403
)
response.headers["Content-Type"] = "application/json"
return response
note = notes_service.create_note(request=request, user=user)
response = make_response(
jsonify(
{
'title': note.title,
'body': note.body,
}
), 201
)
response.headers["Content-Type"] = "application/json"
return response
Более формальное определение аутентификации можно получить из википедии:
Аутентифика́ция (англ. authentication < греч. αὐθεντικός [authentikos] «реальный, подлинный» < αὐτός [autos] «сам; он самый») — процедура проверки подлинности, например:
- проверка подлинности пользователя путём сравнения введённого им пароля (для указанного логина) с паролем, сохранённым в базе данных пользовательских логинов;
- подтверждение подлинности электронного письма путём проверки цифровой подписи письма по открытому ключу отправителя;
- проверка контрольной суммы файла на соответствие сумме, заявленной автором этого файла.
Резюмируя:
Аутентификация это процесс доказательства личности пользователя;
Мы аутентифицируем пользователя с помощью одного факта/характеристики пользователя – его пароля. Однофакторная аутентификация;
Перед аутентификацией мы можем идентифицировать юзера;
Пример аутентификации – предоставление никнейма/имейла и сверка пароля(пароль доказывает, что этот человек тот, кем себя выдает).
Аутентификация может происходить при процедуре логина, а так же при каждом запросе к ресурсу.
Авторизация
Что будет, если пользователь предоставил учетные данные, которых нет в базе данных? Или может пароль от пользователя не совпадает с тем, что есть в БД?
Вы подумали, что заметки могут оставлять только пользователи, которых мы можем идентифицировать и которые предоставили правильный пароль. Если процедуры идентификации и сравнения не пройдены, то пользователь не имеет доступа к функционалу, то есть он не авторизован выполнять такие действия.
Какой HTTP код мы можем вернуть пользователю в таком случае? Порыскав в интернете список HTTP кодов, вы обнаружили говорящий за себя ответ 401 Unauthroized. То есть говорим пользователю, что он не авторизован.
# main.py
...
@app.route("/notes", methods=['POST'])
def create_note():
data = request.get_json()
credentials = data.get('credentials')
user = identify_user(credentials=credentials)
if not is_authorized(user=user):
response = make_response(
jsonify(
{
'msg': 'Credentials not valid',
}
), 401 # <----------- Unauthorized status
)
response.headers["Content-Type"] = "application/json"
return response
...
Вы всей командой вновь тестируете сервис. Каждый из вас обнаруживает, что неудобно как-то писать каждый раз свои учетные данные в json. Вован вспоминает, что в протоколе HTTP сообщения могут содержать заголовки, куда можно будет спрятать данные для аутентификации и последующей авторизации.
Какой заголовок нам нужен? Если данные для аутентификации неправильные, то мы не авторизуем юзера и возвращаем код ответа 401 Unathorized. Пусть пользователь введет правильные данные и повторит запрос с заголовком Authorization
Мы не можем его авторизовать, потому что у него неправильные данные для аутентификации. Если они станут валидными, то мы его авторизуем. Пользователь посылает заголовок Authorization
и говорит как бы контекст "я с повторной авторизцией”. При этом данные для аутентиикации пусть будут представлены в виде одной строки, но разделены двоеточием username:passsword
Таким образом, мы пришли к следующему алгоритму:
Пользователь посылает данные для создания заметки, но без каких-либо данных для аутентификации, либо с неверными данными аутентификации.
Наш эндпоинт видит, что данных для аутентификации нет, либо они неправильные и говорит “ты не авторизован”.
HTTP клиент пользователя просит пользователя ввести правильные данные для аутентификации.
Пользователь посылает данные для создания заметки с заголовком Authorization, содержащим данные для аутентификации перед авторизацией.
Пользователь получает успешный статус о создании заметки.
from typing import Optional
from flask import request, jsonify, make_response
import services as notes_service
from app import app
from models import User
from utils import hash_password
def authenticate_user(user: Optional[User], password: str) -> bool:
if user is None:
return False
return user.password == hash_password(password)
def identify_user() -> Optional[User]:
credentials = request.headers.get('Authorization') # <--- new header
if not credentials:
return None
username, _ = credentials.split(':')
user = User.query.filter_by(username=username).first()
return user
def is_authorized(user: Optional[User]) -> bool:
credentials = request.headers.get('Authorization')
username, password = credentials.split(':')
if not authenticate_user(user=user, password=password):
# Can't authenticate user, permission denied
return False
return True
@app.route("/notes", methods=['POST'])
def create_note():
user = identify_user()
if not is_authorized(user=user):
response = make_response(
jsonify(
{
'msg': 'Credentials not valid',
}
), 401
)
response.headers["Content-Type"] = "application/json"
return response
note = notes_service.create_note(request=request, user=user)
response = make_response(
jsonify(
{
'title': note.title,
'body': note.body,
}
), 201
)
response.headers["Content-Type"] = "application/json"
return response
Масштабирование алгоритма аутентификации
Димон всегда думал наперед и предложил подумать о том, как поступать, если мы придумаем дополнительный способ аутентификации. Тот, который мы сейчас придумали, давайте назовем базовый(basic). Когда пользователь предоставит данные для другого способа аутентификации(например, будет использовать какой-то одноразовый код), мы сообщим ему, что в данном контексте нужно использовать базовую аутентификацию, а не одноразовый код. Будем возвращать в ответе заголовок WWW-Authenticate
, который укажет схему аутентификации. Например, WWW-Authenticate: Basic
Еще я не хочу, чтобы учетные данные передавались в виде китайских или кириллических символов. Придется возиться с кодировками, это боль. Пусть лучше будут закодированы с помощью base64. Эта кодировка переводит все в подмножество ASCII символов без возни с другими символами. То, что нужно. Будет выглядеть так: вован:мой_пароль
превратится в 0LLQvtCy0LDQvTrQvNC+0Llf0L/QsNGA0L7Qu9GM
, а vovan:1234test
превратится в dm92YW46MTIzNHRlc3Q=
Вы еще забыли, что если мы добавим в сервис заметок сервис секретных файлов, для аутентификации в котором нужен будет другой хост, то их стоило бы разграничить, чтобы не перепутали окружения. Давайте назовем это realm. Будет выглядеть так: WWW-Authenticate: Basic realm="dev_api"
В итоге вы пришли к следующей схеме, учитывающей масштабирование способов аутентификации:
Пользователь посылает данные для создания заметки, но без каких-либо данных для аутентификации, либо с неверными данными аутентификации
Наш эндпоинт видит, что данных для аутентификации нет, либо они неправильные и говорит “ты не авторизован, предоставь данные для базовой схемы аутентификации”. В добавок мы возвращаем заголовок
WWW-Authenticate
со значением нужной схемы аутентификации и указанием окружения(realm) API заметок.HTTP клиент пользователя смотрит, что нужно взять учетные данные для базовой аутентификации и просит пользователя ввести псевдоним и пароль. Смотрит, что нужно использовать тот же самый сервис
notes_api
.Пользователь посылает закодированные в base64 данные для создания заметки с заголовком
Authorization
, содержащим данные для базовой аутентификации перед авторизациейПользователь получает успешный статус
# main.py
from typing import Optional
from flask import request, jsonify, make_response
import base64
import services as notes_service
from app import app
from models import User
from utils import hash_password
def authenticate_user(user: Optional[User], password: str) -> bool:
if user is None:
return False
return user.password == hash_password(password)
def identify_user() -> Optional[User]:
scheme, credentials = request.headers.get('Authorization').split()
if not credentials:
return None
decoded_credentials = base64.b64decode(credentials).decode()
username, _ = decoded_credentials.split(':')
user = User.query.filter_by(username=username).first()
return user
def is_authorized(user: Optional[User]) -> bool:
scheme, credentials = request.headers.get('Authorization').split()
decoded_credentials = base64.b64decode(credentials).decode()
username, password = decoded_credentials.split(':')
if not authenticate_user(user=user, password=password):
# Can't authenticate user, permission denied
return False
return True
@app.route("/notes", methods=['POST'])
def create_note():
user = identify_user()
if not is_authorized(user=user):
response = make_response(
jsonify(
{
'msg': 'Credentials not valid',
}
), 401
)
response.headers["Content-Type"] = "application/json"
# Add authentication scheme in header
response.headers["WWW-Authenticate"] = "Basic realm=notes_api"
return response
note = notes_service.create_note(request=request, user=user)
response = make_response(
jsonify(
{
'title': note.title,
'body': note.body,
}
), 201
)
response.headers["Content-Type"] = "application/json"
return response
Middleware
Чтобы не проводить аутентификацию прямо в коде эндпоинта, мы можем переместить этот код в место, которое аутентифицирует юзера до попадания запроса в эндпоинт. Обычно для этого в различных фреймворках используют Middleware. Этот термин обозначает ряд функций или классов, которые по цепочке вызываются друг за другом перед попаданием запроса в эндпоинт. Они могут менять какие-то данные, что-то проверять и т.д Чтобы избежать дублирования кода, мы можем написать свой кастомный authentication middleware и использовать его повсюду в приложении, где нужна аутентификация. Мы можем использовать просто декораторы. Декораторы оборачивают какую-то функцию и выполняют какой-то код до обработки самим эндпоинтом. Это и будет своего рода middleware в нашем случае
# main.py
import base64
from functools import wraps
from typing import Optional
from flask import request, jsonify, make_response
import services as notes_service
from app import app
from models import User
from utils import hash_password
def authenticate_user(user: Optional[User], password: str) -> bool:
if user is None:
return False
return user.password == hash_password(password)
def identify_user() -> Optional[User]:
scheme, credentials = request.headers.get('Authorization').split()
if not credentials:
return None
decoded_credentials = base64.b64decode(credentials).decode()
username, _ = decoded_credentials.split(':')
user = User.query.filter_by(username=username).first()
return user
def authenticated_only(f):
@wraps(f)
def wrapper(*args, **kwargs):
user = identify_user()
scheme, credentials = request.headers.get('Authorization').split()
decoded_credentials = base64.b64decode(credentials).decode()
username, password = decoded_credentials.split(':')
is_authenticated = authenticate_user(user=user, password=password)
# Not authenticated users not authorized to do this action
if not is_authenticated:
response = make_response(
jsonify(
{
'msg': 'Credentials not valid',
}
), 401
)
response.headers["Content-Type"] = "application/json"
response.headers["WWW-Authenticate"] = "Basic realm=notes_api"
return response
request.user = user
return f(*args, **kwargs)
return wrapper
@app.route("/notes", methods=['POST'])
@authenticated_only
def create_note():
note = notes_service.create_note(request=request, user=request.user)
response = make_response(
jsonify(
{
'title': note.title,
'body': note.body,
}
), 201
)
response.headers["Content-Type"] = "application/json"
return response
Выводы и зачем это все
Что мы узнали из этой статьи:
идентификация это процесс выявления какой-то сущности с идентификатором путем сравнения с уже присвоенными идентификаторами. Мы хотим узнать, кто есть кто в системе, поэтому ввели новую сущность юзера, которая имеет уникальный идентификатор username. Он и позволяет нам идентифицировать юзера. Например, под идентификацией можно понимать поиск в БД пользователя с указанным username/email.
аутентификация позволяет доказать, что идентифицированный пользователь тот, кем себя выдает. Например, таким процессом служит идентификация пользователя и сверка пароля этого пользователя. Схема аутентификации с паролем это однофакторная аутентификация, так-как мы используем один фактор проверки - пароль.
авторизация это проверка прав доступа. Обычно перед авторизацией в HTTP проверяется аутентификация. Например, если доступ к ресурсу ограничен только аутентифицированными юзерами.
Почему эта статья может помочь? Система аутентификации в таких фреймворках, как django, django-rest-framework, flask, fastAPI использует похожие понятия и процессы, хотя детали реализации разные - где-то нужно наследовать специальный класс, где-то реализовать вызываемый объект и т.д.
Кусок внутреннего кода django-rest-framework:
class BasicAuthentication(BaseAuthentication):
"""
HTTP Basic authentication against username/password.
"""
www_authenticate_realm = 'api'
def authenticate(self, request):
"""
Returns a `User` if a correct username and password have been supplied
using HTTP Basic authentication. Otherwise returns `None`.
"""
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != b'basic':
return None
if len(auth) == 1:
msg = _('Invalid basic header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid basic header. Credentials string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
try:
try:
auth_decoded = base64.b64decode(auth[1]).decode('utf-8')
except UnicodeDecodeError:
auth_decoded = base64.b64decode(auth[1]).decode('latin-1')
auth_parts = auth_decoded.partition(':')
except (TypeError, UnicodeDecodeError, binascii.Error):
msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
raise exceptions.AuthenticationFailed(msg)
userid, password = auth_parts[0], auth_parts[2]
return self.authenticate_credentials(userid, password, request)
def authenticate_credentials(self, userid, password, request=None):
"""
Authenticate the userid and password against username and password
with optional request for context.
"""
credentials = {
get_user_model().USERNAME_FIELD: userid,
'password': password
}
user = authenticate(request=request, **credentials)
if user is None:
raise exceptions.AuthenticationFailed(_('Invalid username/password.'))
if not user.is_active:
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
return (user, None)
def authenticate_header(self, request):
return 'Basic realm="%s"' % self.www_authenticate_realm
кусок кода FastAPI
class HTTPBasic(HTTPBase):
def __init__(
self,
*,
scheme_name: Optional[str] = None,
realm: Optional[str] = None,
description: Optional[str] = None,
auto_error: bool = True,
):
self.model = HTTPBaseModel(scheme="basic", description=description)
self.scheme_name = scheme_name or self.__class__.__name__
self.realm = realm
self.auto_error = auto_error
async def __call__( # type: ignore
self, request: Request
) -> Optional[HTTPBasicCredentials]:
authorization: str = request.headers.get("Authorization")
scheme, param = get_authorization_scheme_param(authorization)
if self.realm:
unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
else:
unauthorized_headers = {"WWW-Authenticate": "Basic"}
invalid_user_credentials_exc = HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers=unauthorized_headers,
)
if not authorization or scheme.lower() != "basic":
if self.auto_error:
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers=unauthorized_headers,
)
else:
return None
try:
data = b64decode(param).decode("ascii")
except (ValueError, UnicodeDecodeError, binascii.Error):
raise invalid_user_credentials_exc
username, separator, password = data.partition(":")
if not separator:
raise invalid_user_credentials_exc
return HTTPBasicCredentials(username=username, password=password)
кусок кода flask_httpauth
class HTTPBasicAuth(HTTPAuth):
def __init__(self, scheme=None, realm=None):
super(HTTPBasicAuth, self).__init__(scheme or 'Basic', realm)
self.hash_password_callback = None
self.verify_password_callback = None
def hash_password(self, f):
self.hash_password_callback = f
return f
def verify_password(self, f):
self.verify_password_callback = f
return f
def get_auth(self):
# this version of the Authorization header parser is more flexible
# than Werkzeug's, as it also accepts other schemes besides "Basic"
header = self.header or 'Authorization'
if header not in request.headers:
return None
value = request.headers[header].encode('utf-8')
try:
scheme, credentials = value.split(b' ', 1)
username, password = b64decode(credentials).split(b':', 1)
except (ValueError, TypeError):
return None
try:
username = username.decode('utf-8')
password = password.decode('utf-8')
except UnicodeDecodeError:
username = None
password = None
return Authorization(
scheme, {'username': username, 'password': password})
def authenticate(self, auth, stored_password):
if auth:
username = auth.username
client_password = auth.password
else:
username = ""
client_password = ""
if self.verify_password_callback:
return self.ensure_sync(self.verify_password_callback)(
username, client_password)
if not auth:
return
if self.hash_password_callback:
try:
client_password = self.ensure_sync(
self.hash_password_callback)(client_password)
except TypeError:
client_password = self.ensure_sync(
self.hash_password_callback)(username, client_password)
return auth.username if client_password is not None and \
stored_password is not None and \
hmac.compare_digest(client_password, stored_password) else None
В кусках кода сверху можно заметить, что в деталях реализации они отличаются, но есть и одинаковые элементы. Например, краем глаза можно зацепиться за методы и переменные authenticate
, заголовокAuthorization
, заголовок WWW-Authenticate
, scheme
, credentials
, HTTP_401_UNAUTHORIZED
Хотя идентификацию юзера тут явно не выделяют, но этот процесс присутствует.
Итого - вы можете примерно понять о чем речь. Поменять эту аутентификацию и реализовать свою. Например, для JWT аутентификации нужно будет точно так же читать заголовок Authorization
, парсить scheme
(Bearer) и credentials
(сам токен), идентифицировать юзера(по айди в токене) и аутентифицировать(проверять валидность токена и существование юзера). И фреймворки как раз прячут это все где-то в части, связанной с middleware.
В следующей статье мы реализуем что-то более практичное. Например, кастомную JWT аутентификацию в django-rest-framework
.
Ссылки для более глубокого изучения
RFC HTTP Authentication https://datatracker.ietf.org/doc/html/rfc7235;
401 Unauthorized status code https://developer.mozilla.org/ru/docs/Web/HTTP/Status/401;
WWW-Authenticate header https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate;
Authorization header https://developer.mozilla.org/ru/docs/Web/HTTP/Headers/Authorization;
RFC Basic authentication https://datatracker.ietf.org/doc/html/rfc7617;
Django ModelBackend https://github.com/django/django/blob/main/django/contrib/auth/backends.py#L40;
DRF Basic authentication https://github.com/encode/django-rest-framework/blob/master/rest_framework/authentication.py#L53;