Skip to content

Commit 1ce51bd

Browse files
authored
New Auth methods (#37)
* No unnecessary user_id and email * Auth with google, physics * Docs * Some refactorings
1 parent 7525754 commit 1ce51bd

File tree

22 files changed

+732
-133
lines changed

22 files changed

+732
-133
lines changed

.github/workflows/build_and_publish.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ jobs:
8989
--env SMTP_HOST='mail.profcomff.com' \
9090
--env SMTP_PORT='465' \
9191
--env APPLICATION_HOST='${{ secrets.HOST }}' \
92+
--env GOOGLE_REDIRECT_URL='https://app.test.profcomff.com/auth/oauth-authorized/google' \
93+
--env GOOGLE_CREDENTIALS='${{ secrets.GOOGLE_CREDENTIALS }}' \
94+
--env PHYSICS_REDIRECT_URL='https://app.test.profcomff.com/auth/oauth-authorized/physics-msu' \
95+
--env PHYSICS_CREDENTIALS='${{ secrets.PHYSICS_CREDENTIALS }}' \
96+
--env LKMSU_REDIRECT_URL='https://app.test.profcomff.com/auth/oauth-authorized/lk-msu' \
9297
--name ${{ env.CONTAITER_NAME }} \
9398
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:test
9499
@@ -135,5 +140,10 @@ jobs:
135140
--env SMTP_HOST='mail.profcomff.com' \
136141
--env SMTP_PORT='465' \
137142
--env APPLICATION_HOST='${{ secrets.HOST }}' \
143+
--env GOOGLE_REDIRECT_URL='https://app.profcomff.com/auth/oauth-authorized/google' \
144+
--env GOOGLE_CREDENTIALS='${{ secrets.GOOGLE_CREDENTIALS }}' \
145+
--env PHYSICS_REDIRECT_URL='https://app.profcomff.com/auth/oauth-authorized/physics-msu' \
146+
--env PHYSICS_CREDENTIALS='${{ secrets.PHYSICS_CREDENTIALS }}' \
147+
--env LKMSU_REDIRECT_URL='https://app.profcomff.com/auth/oauth-authorized/lk-msu' \
138148
--name ${{ env.CONTAITER_NAME }} \
139149
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

README.md

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,39 +21,67 @@ foo@bar:~$ pip install -r requirements.txt
2121
foo@bar:~$ python -m auth_backend
2222
```
2323

24+
---
25+
2426
## ENV-file description
2527

2628
DB_DSN=postgresql://admin:admin@localhost:5432/?
2729
EMAIL_PASS=
2830
EMAIL=
2931
HOST=
3032

33+
### Google
34+
`GOOGLE_REDIRECT_URL: str` – URL адрес страницы для получения данных авторизации на нашем фронтэнде
35+
`GOOGLE_SCOPES: list[str]` – Запрашиваемые у гугла права на управление аккаунтом, по умолчанию запрашивает данные пользователя
36+
`GOOGLE_CREDENTIALS: Json` – Данные приложения Google, получить можно в Google Cloud Console
37+
38+
### Physics
39+
`PHYSICS_REDIRECT_URL: str` – см. секцию *Google*
40+
`PHYSICS_SCOPES: list[str]` – см. секцию *Google*
41+
`PHYSICS_CREDENTIALS: Json` – см. секцию *Google*
42+
43+
### LK MSU
44+
`LKMSU_REDIRECT_URL` – URL адрес страницы для получения данных авторизации на нашем фронтэнде
45+
3146
---
3247

3348
## Сценарий использования
34-
### Что надо сделать чтобы зарегистрировать пользователя через email
35-
49+
### Email: регистрация нового аккаунта
3650
1. Дернуть ручку `POST /email/registrate` . Вы передаете `{email: "", password: ""}`
3751
2. На почту приходит письмо с линком на `GET /email/approve?token='...'`, если по ней перейти то почта будет подтверждена и регистрацию можно считать завершенной.
3852

39-
### Что надо сделать чтобы залогиниться через email
40-
41-
1. Дернуть ручку `POST /email/login`. там всего один вариант логина, никуда не денетесь
53+
### Email: вход в аккаунт
54+
1. Дернуть ручку `POST /email/login`. там всего один вариант логина, никуда не денетесь
4255
2. Вам придет токен, сохраняйте его кууда нибудь, срок действия ограничен.
4356

44-
### Забыли пароль
45-
46-
1. Дернуть ручку `POST /email/reset/password/request`. Вы передаете `{email: ""}`в нагрузке
57+
### Email: Восстановление забытого пароля
58+
1. Дернуть ручку `POST /email/reset/password/request`. Вы передаете `{email: ""}`в нагрузке
4759
2. Вам придет письмо, где будет ссылка НА ФРОНТ(надо сделать это), в ссылке будет reset_token
4860
3. Токен надо передать в ручку `POST /email/reset/password` в заголовках, вместе с `{email: "", new_password: ""}` и пароль будет изменен. email не понадобится после решения #36
49-
50-
### Смена пароля
5161

62+
### Email: Изменение пароля
5263
1. Если пароль не забыт, а просто надо его поменять. Тогда в `POST /email/reset/password/request` передается токен авторизации, в теле вы передаете `{email: "", password: "", new_password: ""}`
5364
2. Отправляете запрос и всё, пароль изменен, вам придет письмо с уведомлением о смене пароляю
5465

55-
### Что надо сделать, чтобы поменять почту
56-
66+
### Email: Изменение адреса электронной почты
5767
1. Дернуть ручку `POST /email/reset/email/request`. Всего один вариант, передаете новое мыло в теле `{email: ""}` и токен атворизации в заголовках
5868
2. На почту придет письмо с подтверждением почты, там будет токен подтверждения в query параметрах. Ссылка ведет на ручку GET пока что, но надо переделать, чтобы тоже вела на фронт.
59-
69+
70+
### Google/Physics: вход пользователя с аккаунтом Google
71+
*Все примеры написаны для Google аккаунта, для аккаунта physics.msu.ru средует делать запросы к `/physics-msu` вместо `/google`*
72+
73+
1. Получаем адрес для запроса на сервер Google: `GET /google/auth_url`
74+
2. Редиректим пользователя на этот url, пользователь входит в аккаунт и возвращается на страницу, которую можно узнать запросом `GET /google/redirect_url`
75+
3. Если Google не передал в ответе GET параметр `error`, передаем GET параметры страницы на сервер авториации в теле POST запроса в формате JSON: `POST /google/login`. Иначе возвращаем ошибку авторизации
76+
4. При успешном входе получаем `token` сессии. Если сервер авторизации ответил ошибкой 401:
77+
1. запоминаем значение id_token из ответа.
78+
2. Предлагаем пользователю завести новый аккаунт нашего приложения, связанный с гуглом
79+
5. Если пользователь соглашается, делаем запрос с `{"id_token": "<id-token>"}` в теле на адрес `POST /google/register`. При успешном входе получаем `token` сессии, иначе показываем экран ошибки авторизации
80+
81+
### Google/Physics: добавление аккаунта Google как второго метода входа
82+
*Все примеры написаны для Google аккаунта, для аккаунта physics.msu.ru средует делать запросы к `/physics-msu` вместо `/google`*
83+
84+
1. Получаем адрес для запроса на сервер Google: `GET /google/auth_url`
85+
2. Редиректим пользователя на этот url, пользователь входит в аккаунт и возвращается на страницу, которую можно узнать запросом `GET /google/redirect_url`
86+
3. Если Google не передал в ответе GET параметр `error`, передаем данные на сервер авториации: `POST /google/register`, указываем заголовок `Authorization: <auth-token>`. Иначе возвращаем ошибку авторизации
87+
4. При успешном входе получаем `token` сессии, иначе показываем экран ошибки авторизации
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
from .auth_method import AuthMethodMeta, AUTH_METHODS
22
from .email import Email
3+
from .google import GoogleAuth
4+
from .physics import PhysicsAuth
5+
from .lkmsu import LkmsuAuth
36

47
__all__ = ["AUTH_METHODS", "AuthMethodMeta", "Email"]

auth_backend/auth_plugins/auth_method.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
from __future__ import annotations
22

3+
import logging
4+
import random
35
import re
4-
from abc import abstractmethod, ABCMeta
6+
import string
7+
from abc import ABCMeta, abstractmethod
58
from datetime import datetime
69

710
from fastapi import APIRouter
811
from pydantic import constr
12+
from sqlalchemy.orm import Session
913

1014
from auth_backend.base import Base
15+
from auth_backend.models.db import User, UserSession
16+
from auth_backend.settings import get_settings
17+
18+
19+
logger = logging.getLogger(__name__)
20+
settings = get_settings()
21+
22+
23+
def random_string(length: int = 32) -> str:
24+
return "".join([random.choice(string.ascii_letters) for _ in range(length)])
1125

1226

1327
class Session(Base):
@@ -36,6 +50,9 @@ def __init__(self):
3650
self.router.add_api_route("/login", self._login, methods=["POST"], response_model=Session)
3751

3852
def __init_subclass__(cls, **kwargs):
53+
if cls.__name__.endswith('Meta'):
54+
return
55+
logger.info(f'Init authmethod {cls.__name__}')
3956
AUTH_METHODS[cls.__name__] = cls
4057

4158
@staticmethod
@@ -47,3 +64,70 @@ async def _register(*args, **kwargs) -> object:
4764
@abstractmethod
4865
async def _login(*args, **kwargs) -> Session:
4966
raise NotImplementedError()
67+
68+
@staticmethod
69+
async def _create_session(user: User, *, db_session: Session) -> Session:
70+
"""Создает сессию пользователя"""
71+
user_session = UserSession(user_id=user.id, token=random_string(length=settings.TOKEN_LENGTH))
72+
db_session.add(user_session)
73+
db_session.flush()
74+
return Session(
75+
user_id=user_session.user_id,
76+
token=user_session.token,
77+
id=user_session.id,
78+
expires=user_session.expires,
79+
)
80+
81+
@staticmethod
82+
async def _create_user(*, db_session: Session) -> User:
83+
"""Создает пользователя"""
84+
user = User()
85+
db_session.add(user)
86+
db_session.flush()
87+
return user
88+
89+
async def _get_user(
90+
*,
91+
db_session: Session,
92+
user_session: UserSession = None,
93+
session_token: str = None,
94+
user_id: int = None,
95+
with_deleted: bool = False,
96+
with_expired: bool = False,
97+
):
98+
"""Отдает пользователя по сессии, токену или user_id"""
99+
if user_id:
100+
return User.get(user_id, with_deleted=with_deleted, session=db_session)
101+
if session_token:
102+
user_session: UserSession = (
103+
UserSession.query(with_deleted=with_deleted, session=db_session)
104+
.filter(UserSession.token == session_token)
105+
.one_or_none()
106+
)
107+
if user_session and (not user_session.expired or with_expired):
108+
return user_session.user
109+
return
110+
111+
112+
class OauthMeta(AuthMethodMeta):
113+
"""Абстрактная авторизация и аутентификация через OAuth"""
114+
115+
class UrlSchema(Base):
116+
url: str
117+
118+
def __init__(self):
119+
super().__init__()
120+
self.router.add_api_route("/redirect_url", self._redirect_url, methods=["GET"], response_model=self.UrlSchema)
121+
self.router.add_api_route("/auth_url", self._auth_url, methods=["GET"], response_model=self.UrlSchema)
122+
123+
@staticmethod
124+
@abstractmethod
125+
async def _redirect_url(*args, **kwargs) -> UrlSchema:
126+
"""URL на который происходит редирект после завершения входа на стороне провайдера"""
127+
raise NotImplementedError()
128+
129+
@staticmethod
130+
@abstractmethod
131+
async def _auth_url(*args, **kwargs) -> UrlSchema:
132+
"""URL на который происходит редирект из приложения для авторизации на стороне провайдера"""
133+
raise NotImplementedError()

0 commit comments

Comments
 (0)