Skip to content

Commit 6ca2aa8

Browse files
authored
Merge pull request from feat/add-oidc-google-auth
feat: add Google OIDC authentication support
2 parents 8639a1c + 456f792 commit 6ca2aa8

12 files changed

Lines changed: 233 additions & 14 deletions

File tree

.env.dist

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
APP_TITLE="Mercury API Docs"
22
APP_DESCRIPTION="This is the Swagger documentation of the Mercury API"
33
APP_VERSION=1.0
4+
API_URL="http://localhost:8000"
45
API_VERSION="v1"
56
APP_ENV=local
7+
HTTP_REQUEST_TIMEOUT=60
68
## Admin Configuration
79
ADMIN_USERNAME="admin"
810
ADMIN_EMAIL="admin"
@@ -25,3 +27,9 @@ REDIS_PORT=6379
2527
JWT_SECRET_KEY="mysecretkey"
2628
JWT_ALGORITHM="HS256"
2729
ACCESS_TOKEN_EXPIRE_MINUTES=30
30+
## OIDC Configuration
31+
OIDC_GOOGLE_CLIENT_ID="changeme"
32+
OIDC_GOOGLE_CLIENT_SECRET="changeme"
33+
GOOGLE_AUTH_URL="https://accounts.google.com/o/oauth2/auth"
34+
GOOGLE_TOKEN_URL="https://accounts.google.com/o/oauth2/token"
35+
GOOGLE_USER_INFO_URL="https://www.googleapis.com/oauth2/v1/userinfo"

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
Mercury is a simple and reliable boilerplate that anyone can use from beginners to experts (no deep bullsh*t).
2626

2727
This project uses:
28-
- 🛡️ Basic [OAuth2](https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/?h=jwt) authentication provided by FastApi security nested package.
29-
- 🔋[PostgreSQL](https://hub.docker.com/_/postgres) as its main database, [Redis](https://hub.docker.com/_/redis) for caching, and [flyway](https://hub.docker.com/r/flyway/flyway) for database migration.
28+
- 🛡️ Basic [OAuth2](https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/?h=jwt) authentication, utilizing the FastAPI security module. It also supports user authentication via Google integration.
29+
- 🛢[PostgreSQL](https://hub.docker.com/_/postgres) as its main database, [Redis](https://hub.docker.com/_/redis) for caching, and [flyway](https://hub.docker.com/r/flyway/flyway) for database migration.
3030
- 🧪 Unit and integration tests.
3131
- 🔒️ Security scanner (Bandit).
3232

@@ -75,6 +75,7 @@ This will create a `.env` file in your project locally.
7575
APP_TITLE="Mercury API Docs"
7676
APP_DESCRIPTION="This is the Swagger documentation of the Mercury API"
7777
APP_VERSION=1.0
78+
API_URL="http://localhost:8000"
7879
API_VERSION="v1"
7980
APP_ENV=local
8081
## Admin Configuration
@@ -99,6 +100,12 @@ REDIS_PORT=6379
99100
JWT_SECRET_KEY="mysecretkey"
100101
JWT_ALGORITHM="HS256"
101102
ACCESS_TOKEN_EXPIRE_MINUTES=30
103+
## OIDC Configuration
104+
OIDC_GOOGLE_CLIENT_ID="changeme"
105+
OIDC_GOOGLE_CLIENT_SECRET="changeme"
106+
GOOGLE_AUTH_URL="https://accounts.google.com/o/oauth2/auth"
107+
GOOGLE_TOKEN_URL="https://accounts.google.com/o/oauth2/token"
108+
GOOGLE_USER_INFO_URL="https://www.googleapis.com/oauth2/v1/userinfo"
102109
```
103110

104111
### Run the containers

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version: '3.3'
1+
version: '3.8'
22
services:
33

44
mercury_api:

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ passlib[bcrypt]
1111
python-multipart==0.0.18
1212
pytest
1313
ruff
14-
httpx
14+
httpx
15+
google-auth

src/constants/environment_variables.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
APP_VERSION = os.environ['APP_VERSION']
66
APP_TITLE = os.environ['APP_TITLE']
77
APP_DESCRIPTION = os.environ['APP_DESCRIPTION']
8+
v = os.environ['API_VERSION']
89
APP_ENV = os.getenv('APP_ENV')
10+
API_URL = os.getenv('API_URL')
911
POSTGRES_DB = os.getenv('POSTGRES_DB')
1012
POSTGRES_USER = os.getenv('POSTGRES_USER')
1113
POSTGRES_PASSWORD = os.getenv('POSTGRES_PASSWORD')
@@ -19,4 +21,10 @@
1921
JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY')
2022
JWT_ALGORITHM = os.getenv('JWT_ALGORITHM')
2123
ACCESS_TOKEN_EXPIRE_MINUTES = os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES')
22-
v = os.environ['API_VERSION']
24+
HTTP_REQUEST_TIMEOUT = os.getenv('HTTP_REQUEST_TIMEOUT')
25+
GOOGLE_AUTH_URL = os.getenv('GOOGLE_AUTH_URL')
26+
GOOGLE_TOKEN_URL = os.getenv('GOOGLE_TOKEN_URL')
27+
GOOGLE_USER_INFO_URL = os.getenv('GOOGLE_USER_INFO_URL')
28+
OIDC_GOOGLE_CLIENT_ID = os.getenv('OIDC_GOOGLE_CLIENT_ID')
29+
OIDC_GOOGLE_CLIENT_SECRET = os.getenv('OIDC_GOOGLE_CLIENT_SECRET')
30+
OIDC_GOOGLE_REDIRECT_URI = f"{API_URL}/{v}/auth/google"

src/controllers/oidc/google.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import requests
2+
from google.auth.transport import requests as google_requests
3+
from google.oauth2 import id_token
4+
from models.User import User
5+
from utils.oidc import update_oidc_info
6+
from constants.environment_variables import (
7+
OIDC_GOOGLE_CLIENT_ID,
8+
OIDC_GOOGLE_CLIENT_SECRET,
9+
OIDC_GOOGLE_REDIRECT_URI,
10+
GOOGLE_TOKEN_URL,
11+
GOOGLE_USER_INFO_URL,
12+
HTTP_REQUEST_TIMEOUT
13+
)
14+
15+
def get_user_infos_from_google_token_url(code):
16+
token_data = {
17+
"code": code,
18+
"client_id": OIDC_GOOGLE_CLIENT_ID,
19+
"client_secret": OIDC_GOOGLE_CLIENT_SECRET,
20+
"redirect_uri": OIDC_GOOGLE_REDIRECT_URI,
21+
"grant_type": "authorization_code",
22+
}
23+
24+
token_response = requests.post(GOOGLE_TOKEN_URL, data=token_data, timeout=HTTP_REQUEST_TIMEOUT)
25+
access_token = token_response.json().get("access_token")
26+
27+
user_infos_response = requests.get(GOOGLE_USER_INFO_URL, headers={"Authorization": f"Bearer {access_token}"}, timeout=HTTP_REQUEST_TIMEOUT)
28+
user_infos = user_infos_response.json()
29+
30+
if not user_infos:
31+
return {
32+
"status": False,
33+
"user_infos": user_infos
34+
}
35+
36+
return {
37+
"status": True,
38+
"user_infos": user_infos
39+
}
40+
41+
def get_user_infos_from_google_token(id_token_str):
42+
id_info = id_token.verify_oauth2_token(id_token_str, google_requests.Request(), OIDC_GOOGLE_CLIENT_ID)
43+
user_id = id_info['sub']
44+
email = id_info.get('email')
45+
46+
user_infos = {
47+
'id': user_id,
48+
'email': email,
49+
}
50+
51+
if not user_infos:
52+
return {
53+
"status": False,
54+
"user_infos": user_infos
55+
}
56+
57+
return {
58+
"status": True,
59+
"user_infos": user_infos
60+
}
61+
62+
63+
def create_user(user_infos, db):
64+
user_exists = db.query(User).filter(User.email == user_infos['email']).first()
65+
66+
if user_exists:
67+
got_updated = update_oidc_info(
68+
user_exists.id,
69+
'google',
70+
user_infos['id'],
71+
user_infos['email'],
72+
db
73+
)
74+
if got_updated is False:
75+
return {
76+
"status": False,
77+
"message": "Failed to update user"
78+
}
79+
80+
return {
81+
"status": True,
82+
"user": user_exists
83+
}
84+
85+
new_user = User(
86+
email=user_infos['email'],
87+
oidc_configs=[{
88+
"provider": "google",
89+
"id": user_infos['id'],
90+
"email": user_infos['email']
91+
}]
92+
)
93+
new_user.save(db)
94+
return {
95+
"status": True,
96+
"user": new_user
97+
}

src/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from database.postgres_db import dbEngine, Base
55
import database.redis_db as redis
66
from restful_ressources import import_resources
7-
from utils.security import create_admin_user
7+
# from utils.security import create_admin_user
88

99
Base.metadata.create_all(bind=dbEngine)
1010
redis.init()
@@ -25,6 +25,6 @@
2525
allow_headers=["*"],
2626
)
2727

28-
create_admin_user()
28+
# create_admin_user()
2929

3030
import_resources(app)

src/migrations/V1/V1.2__create_user_table.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ CREATE TABLE IF NOT EXISTS public.user (
44
email VARCHAR(200),
55
password VARCHAR(200),
66
is_admin BOOLEAN,
7-
disabled BOOLEAN
7+
disabled BOOLEAN,
8+
oidc_configs JSONB DEFAULT '[]'::jsonb;
89
);

src/models/User.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import uuid
21
from database.postgres_db import Base
3-
from sqlalchemy import Column, Integer, String, Boolean, Numeric
4-
from sqlalchemy.sql import func
2+
from sqlalchemy import Column, String, Boolean
3+
from sqlalchemy.dialects.postgresql import JSONB
54
from fastapi_utils.guid_type import GUID, GUID_SERVER_DEFAULT_POSTGRESQL
65

76
class User(Base):
@@ -11,4 +10,5 @@ class User(Base):
1110
email = Column(String, nullable=False, unique=True)
1211
password = Column(String, nullable=False)
1312
is_admin = Column(Boolean, nullable=True, default=False)
14-
disabled = Column(Boolean, nullable=True, default=False)
13+
disabled = Column(Boolean, nullable=True, default=False)
14+
oidc_configs = Column(JSONB, default=lambda: [], nullable=False)

src/restful_ressources.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
2-
import os
31
from routes import health
42
import middleware.auth_guard as auth_guard
53
from routes.user import user
64
from routes.admin import user as admin_user
5+
from routes.oidc import google
76
from constants.environment_variables import v
87

98
def import_resources(app):
109
app.include_router(health.router, tags=['Information'], prefix=f'/{v}')
1110
app.include_router(auth_guard.router, tags=['Access Token'], prefix=f'/{v}')
11+
app.include_router(google.router, tags=['OIDC'], prefix=f'/{v}/oidc')
1212
app.include_router(admin_user.router, tags=['Admin'], prefix=f'/{v}/admin/user')
1313
app.include_router(user.router, tags=['User'], prefix=f'/{v}/user')

0 commit comments

Comments
 (0)