Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Auth methods #37

Merged
merged 12 commits into from
Feb 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/build_and_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ jobs:
--env SMTP_HOST='mail.profcomff.com' \
--env SMTP_PORT='465' \
--env APPLICATION_HOST='${{ secrets.HOST }}' \
--env GOOGLE_REDIRECT_URL='https://app.test.profcomff.com/auth/oauth-authorized/google' \
--env GOOGLE_CREDENTIALS='${{ secrets.GOOGLE_CREDENTIALS }}' \
--env PHYSICS_REDIRECT_URL='https://app.test.profcomff.com/auth/oauth-authorized/physics-msu' \
--env PHYSICS_CREDENTIALS='${{ secrets.PHYSICS_CREDENTIALS }}' \
--env LKMSU_REDIRECT_URL='https://app.test.profcomff.com/auth/oauth-authorized/lk-msu' \
--name ${{ env.CONTAITER_NAME }} \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:test

Expand Down Expand Up @@ -135,5 +140,10 @@ jobs:
--env SMTP_HOST='mail.profcomff.com' \
--env SMTP_PORT='465' \
--env APPLICATION_HOST='${{ secrets.HOST }}' \
--env GOOGLE_REDIRECT_URL='https://app.profcomff.com/auth/oauth-authorized/google' \
--env GOOGLE_CREDENTIALS='${{ secrets.GOOGLE_CREDENTIALS }}' \
--env PHYSICS_REDIRECT_URL='https://app.profcomff.com/auth/oauth-authorized/physics-msu' \
--env PHYSICS_CREDENTIALS='${{ secrets.PHYSICS_CREDENTIALS }}' \
--env LKMSU_REDIRECT_URL='https://app.profcomff.com/auth/oauth-authorized/lk-msu' \
--name ${{ env.CONTAITER_NAME }} \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
54 changes: 41 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,39 +21,67 @@ foo@bar:~$ pip install -r requirements.txt
foo@bar:~$ python -m auth_backend
```

---

## ENV-file description

DB_DSN=postgresql://admin:admin@localhost:5432/?
EMAIL_PASS=
EMAIL=
HOST=

### Google
`GOOGLE_REDIRECT_URL: str` – URL адрес страницы для получения данных авторизации на нашем фронтэнде
`GOOGLE_SCOPES: list[str]` – Запрашиваемые у гугла права на управление аккаунтом, по умолчанию запрашивает данные пользователя
`GOOGLE_CREDENTIALS: Json` – Данные приложения Google, получить можно в Google Cloud Console

### Physics
`PHYSICS_REDIRECT_URL: str` – см. секцию *Google*
`PHYSICS_SCOPES: list[str]` – см. секцию *Google*
`PHYSICS_CREDENTIALS: Json` – см. секцию *Google*

### LK MSU
`LKMSU_REDIRECT_URL` – URL адрес страницы для получения данных авторизации на нашем фронтэнде

---

## Сценарий использования
### Что надо сделать чтобы зарегистрировать пользователя через email

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

### Что надо сделать чтобы залогиниться через email

1. Дернуть ручку `POST /email/login`. там всего один вариант логина, никуда не денетесь
### Email: вход в аккаунт
1. Дернуть ручку `POST /email/login`. там всего один вариант логина, никуда не денетесь
2. Вам придет токен, сохраняйте его кууда нибудь, срок действия ограничен.

### Забыли пароль

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

### Смена пароля

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

### Что надо сделать, чтобы поменять почту

### Email: Изменение адреса электронной почты
1. Дернуть ручку `POST /email/reset/email/request`. Всего один вариант, передаете новое мыло в теле `{email: ""}` и токен атворизации в заголовках
2. На почту придет письмо с подтверждением почты, там будет токен подтверждения в query параметрах. Ссылка ведет на ручку GET пока что, но надо переделать, чтобы тоже вела на фронт.


### Google/Physics: вход пользователя с аккаунтом Google
*Все примеры написаны для Google аккаунта, для аккаунта physics.msu.ru средует делать запросы к `/physics-msu` вместо `/google`*

1. Получаем адрес для запроса на сервер Google: `GET /google/auth_url`
2. Редиректим пользователя на этот url, пользователь входит в аккаунт и возвращается на страницу, которую можно узнать запросом `GET /google/redirect_url`
3. Если Google не передал в ответе GET параметр `error`, передаем GET параметры страницы на сервер авториации в теле POST запроса в формате JSON: `POST /google/login`. Иначе возвращаем ошибку авторизации
4. При успешном входе получаем `token` сессии. Если сервер авторизации ответил ошибкой 401:
1. запоминаем значение id_token из ответа.
2. Предлагаем пользователю завести новый аккаунт нашего приложения, связанный с гуглом
5. Если пользователь соглашается, делаем запрос с `{"id_token": "<id-token>"}` в теле на адрес `POST /google/register`. При успешном входе получаем `token` сессии, иначе показываем экран ошибки авторизации

### Google/Physics: добавление аккаунта Google как второго метода входа
*Все примеры написаны для Google аккаунта, для аккаунта physics.msu.ru средует делать запросы к `/physics-msu` вместо `/google`*

1. Получаем адрес для запроса на сервер Google: `GET /google/auth_url`
2. Редиректим пользователя на этот url, пользователь входит в аккаунт и возвращается на страницу, которую можно узнать запросом `GET /google/redirect_url`
3. Если Google не передал в ответе GET параметр `error`, передаем данные на сервер авториации: `POST /google/register`, указываем заголовок `Authorization: <auth-token>`. Иначе возвращаем ошибку авторизации
4. При успешном входе получаем `token` сессии, иначе показываем экран ошибки авторизации
3 changes: 3 additions & 0 deletions auth_backend/auth_plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from .auth_method import AuthMethodMeta, AUTH_METHODS
from .email import Email
from .google import GoogleAuth
from .physics import PhysicsAuth
from .lkmsu import LkmsuAuth

__all__ = ["AUTH_METHODS", "AuthMethodMeta", "Email"]
86 changes: 85 additions & 1 deletion auth_backend/auth_plugins/auth_method.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
from __future__ import annotations

import logging
import random
import re
from abc import abstractmethod, ABCMeta
import string
from abc import ABCMeta, abstractmethod
from datetime import datetime

from fastapi import APIRouter
from pydantic import constr
from sqlalchemy.orm import Session

from auth_backend.base import Base
from auth_backend.models.db import User, UserSession
from auth_backend.settings import get_settings


logger = logging.getLogger(__name__)
settings = get_settings()


def random_string(length: int = 32) -> str:
return "".join([random.choice(string.ascii_letters) for _ in range(length)])


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

def __init_subclass__(cls, **kwargs):
if cls.__name__.endswith('Meta'):
return
logger.info(f'Init authmethod {cls.__name__}')
AUTH_METHODS[cls.__name__] = cls

@staticmethod
Expand All @@ -47,3 +64,70 @@ async def _register(*args, **kwargs) -> object:
@abstractmethod
async def _login(*args, **kwargs) -> Session:
raise NotImplementedError()

@staticmethod
async def _create_session(user: User, *, db_session: Session) -> Session:
"""Создает сессию пользователя"""
user_session = UserSession(user_id=user.id, token=random_string(length=settings.TOKEN_LENGTH))
db_session.add(user_session)
db_session.flush()
return Session(
user_id=user_session.user_id,
token=user_session.token,
id=user_session.id,
expires=user_session.expires,
)

@staticmethod
async def _create_user(*, db_session: Session) -> User:
"""Создает пользователя"""
user = User()
db_session.add(user)
db_session.flush()
return user

async def _get_user(
*,
db_session: Session,
user_session: UserSession = None,
session_token: str = None,
user_id: int = None,
with_deleted: bool = False,
with_expired: bool = False,
):
"""Отдает пользователя по сессии, токену или user_id"""
if user_id:
return User.get(user_id, with_deleted=with_deleted, session=db_session)
if session_token:
user_session: UserSession = (
UserSession.query(with_deleted=with_deleted, session=db_session)
.filter(UserSession.token == session_token)
.one_or_none()
)
if user_session and (not user_session.expired or with_expired):
return user_session.user
return


class OauthMeta(AuthMethodMeta):
"""Абстрактная авторизация и аутентификация через OAuth"""

class UrlSchema(Base):
url: str

def __init__(self):
super().__init__()
self.router.add_api_route("/redirect_url", self._redirect_url, methods=["GET"], response_model=self.UrlSchema)
self.router.add_api_route("/auth_url", self._auth_url, methods=["GET"], response_model=self.UrlSchema)

@staticmethod
@abstractmethod
async def _redirect_url(*args, **kwargs) -> UrlSchema:
"""URL на который происходит редирект после завершения входа на стороне провайдера"""
raise NotImplementedError()

@staticmethod
@abstractmethod
async def _auth_url(*args, **kwargs) -> UrlSchema:
"""URL на который происходит редирект из приложения для авторизации на стороне провайдера"""
raise NotImplementedError()
Loading