Skip to content

Commit

Permalink
Merge pull request #10 from JohnGrubba/dev
Browse files Browse the repository at this point in the history
Critical Sparse Index Fix
  • Loading branch information
JohnGrubba authored Jul 11, 2024
2 parents 7d01270 + f4ec28f commit bf361f7
Show file tree
Hide file tree
Showing 22 changed files with 380 additions and 90 deletions.
3 changes: 2 additions & 1 deletion config/configtemplate.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"2fa": {
"enable": false,
"issuer_name": "EZAuth",
"issuer_image_url": ""
"issuer_image_url": "",
"qr_endpoint": true
}
}
}
13 changes: 12 additions & 1 deletion docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ services:
- "3250:80"
environment:
- DATABASE_URL=mongodb://admin:admin@db/ezauth?authSource=admin&retryWrites=true&w=majority
- REDIS_PASSWORD=admin
- REDIS_HOST=redis
restart: always
volumes:
- ./config:/src/app/config
- ./src:/src/app
command: |
bash -c 'uvicorn api.main:app --reload --host 0.0.0.0 --port 80 --log-level info'
bash -c 'uvicorn api.main:app --reload --host 0.0.0.0 --port 80'
depends_on:
- db
- redis
db:
image: mongo
ports:
Expand All @@ -23,6 +28,12 @@ services:
- MONGO_INITDB_DATABASE=ezauth
volumes:
- ezauth-data:/data/db
redis:
image: redis:alpine
restart: always
ports:
- '6379:6379'
command: redis-server --requirepass admin

volumes:
ezauth-data:
11 changes: 11 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ services:
- "3250:80"
environment:
- DATABASE_URL=mongodb://admin:admin@db/ezauth?authSource=admin&retryWrites=true&w=majority
- REDIS_PASSWORD=admin
- REDIS_HOST=redis
restart: always
volumes:
- ./config:/src/app/config
depends_on:
- db
- redis
db:
image: mongo
expose:
Expand All @@ -20,6 +25,12 @@ services:
- MONGO_INITDB_DATABASE=ezauth
volumes:
- ezauth-data:/data/db
redis:
image: redis:alpine
restart: always
expose:
- 6379
command: redis-server --requirepass admin

volumes:
ezauth-data:
10 changes: 9 additions & 1 deletion docs/advanced/further_custom.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,12 @@ The following environment variables can be set in the `docker-compose.yml` file
| `MONGO_INITDB_ROOT_PASSWORD` | **Datatype:** String <br> **Default:** `"admin"` <br> The Root Password for the Database. |
| `MONGO_INITDB_DATABASE` | **Datatype:** String <br> **Default:** `"ezauth"` <br> The Database Name. |

You can also change the location of the database data by changing the `volumes` section of the `db` section in the `docker-compose.yml` file.
You can also change the location of the database data by changing the `volumes` section of the `db` section in the `docker-compose.yml` file.

#### Redis Configuration
The Prefix for all Parameters here is `services.redis`.
When changing the Redis configuration, make sure to also change the `api` section in the `docker-compose.yml` file to reflect the new Redis connection.

| Parameter | Description |
|-----------|-------------|
| `command` | **Datatype:** String <br> **Default:** `redis-server --requirepass admin` <br> You can change the password for the redis server here. It has to be set in the environment of the API section too. |
21 changes: 18 additions & 3 deletions docs/advanced/oauth.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,25 @@ The following Rules are applied when using OAuth:<br>
3. Go to the [APIs & Services -> Credentials](https://console.cloud.google.com/apis/credentials) section.
4. Click on `Create credentials` and select `OAuth client ID`.
5. Select `Web application` as the application type.
6. Add the following URIs to the `Authorized redirect URIs`:
- `http://localhost:3250/oauth/google/callback`
6. Add the following URIs to the `Authorized redirect URIs` (Where `{BASE_URL}` is the Hostname of the server eg. `http://test.com`):
- `{BASE_URL}/oauth/google/callback`
7. Add the following scopes
<img src="../assets/scopes_google.png" style='margin-top: 10px;' />

8. Click on `Create` and download the credentials as JSON and place them in the `config` folder.
Make sure the name of the file is `client_secret.env.json`.
Make sure the name of the file is `google_client_secret.env.json`.

## GitHub OAuth
### Setup GitHub OAuth
1. Go to the [GitHub Developer Settings](https://github.com/settings/developers)
2. Click on `New OAuth App`.
3. Add the following URIs to the `Authorization callback URL` (Where `{BASE_URL}` is the Hostname of the server eg. `http://localhost:3250`):
- `{BASE_URL}/oauth/github/callback`
4. Create the Application and copy the `Client ID` and `Client Secret` and create the following file in the `config` folder.

```json title="github_client_secret.env.json"
{
"client_id": "YOUR_CLIENT",
"client_secret": "YOUR_SECRET"
}
```
3 changes: 2 additions & 1 deletion docs/configuration/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@ Make sure that all parameters are set correctly before starting the service.
| `account_features.reset_pswd_conf_mail` | **Datatype:** Boolean <br> **Default:** `true` <br> Enable or disable the password change confirmation E-Mail. |
| `account_features.2fa.enable` | **Datatype:** Boolean <br> **Default:** `false` <br> Enable or disable two factor for Login. 2FA was tested with [Google Authenticator](https://support.google.com/accounts/answer/1066447) and [2FAS Auth](https://2fas.com/) |
| `account_features.2fa.issuer_name` | **Datatype:** String <br> **Default:** `"EZAuth"` <br> How the two factor code will be titled in the users 2FA App. (Mostly the App Name) |
| `account_features.2fa.issuer_image_url` | **Datatype:** String <br> **Default:** `""` <br> URL for an optional Image which will be displayed in the 2FA App. |
| `account_features.2fa.issuer_image_url` | **Datatype:** String <br> **Default:** `""` <br> URL for an optional Image which will be displayed in the 2FA App. |
| `account_features.2fa.qr_endpoint` | **Datatype:** Boolean <br> **Default:** `true` <br> Enable or disable QR Code Generation Endpoint for 2FA Login. This can be useful if you don't want to use any libraries on the client Side. |
2 changes: 1 addition & 1 deletion docs/information.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Information about EZAuth

EZAuth is a simple and easy-to-use authentication service for your applications. It is built on top of [FastAPI](https://fastapi.tiangolo.com/)
and [MongoDB](https://www.mongodb.com/).
and [MongoDB](https://www.mongodb.com/). It also uses [Redis](https://redis.io/) for temporary storage.

## Security
To guarantee the security of your user data, EZAuth uses the following security measures:
Expand Down
10 changes: 6 additions & 4 deletions src/api/login.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fastapi import APIRouter, HTTPException, Response, Cookie
from fastapi import APIRouter, HTTPException, Response, Cookie, Request
from api.model import LoginRequest, LoginResponse
from crud.user import get_user_email_or_username
from crud.sessions import create_login_session, delete_session
Expand All @@ -21,7 +21,7 @@
},
response_model=LoginResponse,
)
async def login(login_form: LoginRequest, response: Response):
async def login(login_form: LoginRequest, response: Response, request: Request):
"""
# Log In (Create Session)
Expand Down Expand Up @@ -50,14 +50,16 @@ async def login(login_form: LoginRequest, response: Response):
# Validate 2FA
if not pyotp.TOTP(user["2fa_secret"]).verify(login_form.two_factor_code):
raise HTTPException(detail="Invalid 2FA Code", status_code=401)
session_token = create_login_session(user["_id"])
session_token = create_login_session(user["_id"], request)
if SessionConfig.auto_cookie:
response.set_cookie(
SessionConfig.auto_cookie_name,
session_token,
expires=SessionConfig.session_expiry_seconds,
)
return LoginResponse(session_token=session_token)
return LoginResponse(
session_token=session_token, expires=SessionConfig.session_expiry_seconds
)


@router.get("/logout", status_code=204)
Expand Down
9 changes: 7 additions & 2 deletions src/api/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fastapi import FastAPI, APIRouter
from fastapi import FastAPI, APIRouter, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse
from api.signup import router as signupRouter
Expand All @@ -9,7 +9,7 @@
from api.oauth_providers import router as oauthRouter
import logging

logging.basicConfig(format="%(asctime)s - %(message)s", level=logging.INFO)
logging.basicConfig(format="%(message)s", level=logging.INFO, force=True)

app = FastAPI(
title="EZAuth API",
Expand All @@ -36,6 +36,11 @@ async def root():
return RedirectResponse("/docs", status_code=301)


@router.get("/up")
async def up():
return Response(status_code=200)


app.include_router(router)
app.include_router(signupRouter)
app.include_router(loginRouter)
Expand Down
1 change: 1 addition & 0 deletions src/api/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class LoginRequest(BaseModel):

class LoginResponse(BaseModel):
session_token: str
expires: int


class ConfirmEmailCodeRequest(BaseModel):
Expand Down
4 changes: 4 additions & 0 deletions src/api/oauth_providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@
from .google import router as ggl

router.include_router(ggl)
if "github" in SignupConfig.oauth_providers:
from .github import router as gh

router.include_router(gh)
126 changes: 126 additions & 0 deletions src/api/oauth_providers/github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from fastapi import APIRouter, Request, BackgroundTasks, Response, HTTPException
from fastapi.responses import RedirectResponse
from tools.conf import SignupConfig, SessionConfig
import json, requests, re
from crud.user import (
create_user,
get_user_by_github_uid,
get_user_email_or_username,
link_github_account,
)
from crud.sessions import create_login_session
from api.model import LoginResponse

github_cnf = json.load(open("/src/app/config/github_client_secret.env.json"))
REDIRECT_URI = SignupConfig.oauth_base_url + "/oauth/github/callback"
CLIENT_ID = github_cnf["client_id"]
CLIENT_SECRET = github_cnf["client_secret"]

router = APIRouter(
prefix="/github",
dependencies=[],
)


@router.get("/login")
async def oauth_login():
"""
# OAuth Login
## Description
This endpoint is used to initiate the OAuth login flow.
"""
return RedirectResponse(
f"https://github.com/login/oauth/authorize?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope=user:email"
)


def login_usr(response: Response, usr: dict, request: Request) -> LoginResponse:
# User already exists
session_token = create_login_session(usr["_id"], request)
if SessionConfig.auto_cookie:
response.set_cookie(
SessionConfig.auto_cookie_name,
session_token,
expires=SessionConfig.session_expiry_seconds,
)
return LoginResponse(
session_token=session_token, expires=SessionConfig.session_expiry_seconds
)


@router.get("/callback")
async def oauth_callback(
request: Request, background_tasks: BackgroundTasks, response: Response
):
code = request.query_params.get("code")
if not code:
raise HTTPException(status_code=400, detail="No code provided")
print("Code: ", code)
rsp = requests.post(
"https://github.com/login/oauth/access_token",
data={
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"code": code,
"redirect_uri": REDIRECT_URI,
},
headers={"Accept": "application/json"},
)
js_resp = rsp.json()
try:
access_token = js_resp["access_token"]
except KeyError:
raise HTTPException(status_code=400, detail="Invalid code")
# Request User Information
rsp = requests.get(
"https://api.github.com/user",
headers={
"Authorization": f"Bearer {access_token}",
},
).json()
# Check if SignIn Possible
usr = get_user_by_github_uid(rsp["id"])
if usr:
return login_usr(response, usr, request)
# Because you can somehow hide emails on github, we have to query them separately
email_query = requests.get(
"https://api.github.com/user/emails",
headers={
"Authorization": f"Bearer {access_token}",
},
)
primary_email = None
for email in email_query.json():
if email["primary"]:
primary_email = email["email"]
break
username = rsp["login"]
# Validate Username
if len(username) < 4 or re.search("[^a-zA-Z0-9]", username) is not None:
username = primary_email.split("@")[0]

# If users email already exists, link the google account
usr = get_user_email_or_username(primary_email)
if usr:
link_github_account(usr["_id"], rsp["id"])
return login_usr(response, usr, request)

# Custom SignUp Form (Password Field missing etc.)
signup_form = {
"email": primary_email,
"username": username,
"password": "",
"github_uid": rsp["id"],
}
# Persist user in DB
session_token = create_user(signup_form, background_tasks, request, signup_form)
if SessionConfig.auto_cookie:
response.set_cookie(
SessionConfig.auto_cookie_name,
session_token,
expires=SessionConfig.session_expiry_seconds,
)
return LoginResponse(
session_token=session_token, expires=SessionConfig.session_expiry_seconds
)
Loading

0 comments on commit bf361f7

Please sign in to comment.