feat: init auth across the app

This commit is contained in:
swve 2022-09-24 13:15:40 +02:00
parent 9479a4b127
commit 3b85a73ec1
12 changed files with 204 additions and 29 deletions

16
app.py
View file

@ -1,6 +1,9 @@
from urllib.request import Request
from fastapi import FastAPI
from src.main import global_router
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi_jwt_auth.exceptions import AuthJWTException
########################
# Pre-Alpha Version 0.1.0
@ -8,7 +11,8 @@ from fastapi.middleware.cors import CORSMiddleware
# (c) LearnHouse 2022
########################
# Init
#
# Global Config
app = FastAPI(
title="LearnHouse",
description="LearnHouse is a new open-source platform tailored for learning experiences.",
@ -24,8 +28,16 @@ app.add_middleware(
allow_headers=["*"]
)
app.include_router(global_router)
#
# Exception Handler
@app.exception_handler(AuthJWTException)
def authjwt_exception_handler(request: Request, exc: AuthJWTException):
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.message}
)
app.include_router(global_router)
@app.get("/")
async def root():

View file

@ -1,12 +1,15 @@
import React from "react";
import styled from "styled-components";
import Link from "next/link";
import { AuthContext } from "../security/AuthProvider";
export const HeaderProfileBox = () => {
const auth: any = React.useContext(AuthContext);
return (
<ProfileArea>
{" "}
<span>HeaderProfileBox</span>{" "}
<span>HeaderProfileBox {String(auth.isAuthenticated)}</span>{" "}
<UnidentifiedArea>
<ul>
<li>

View file

@ -0,0 +1,47 @@
import React, { useEffect } from "react";
import { getRefreshToken, getUserInfo } from "../../services/auth/auth";
export const AuthContext: any = React.createContext({});
export interface Auth {
access_token: string;
isAuthenticated: boolean;
userInfo: {};
isLoading: boolean;
}
const AuthProvider = (props: any) => {
const [auth, setAuth] = React.useState<Auth>({ access_token: "", isAuthenticated: false, userInfo: {}, isLoading: true });
async function checkRefreshToken() {
let data = await getRefreshToken();
return data.access_token;
}
async function checkAuth() {
let access_token = await checkRefreshToken();
let isAuthenticated = false;
let userInfo = {};
let isLoading = false;
if (access_token) {
userInfo = await getUserInfo(access_token);
isAuthenticated = true;
setAuth({ access_token, isAuthenticated, userInfo, isLoading });
} else if (!access_token) {
isAuthenticated = false;
setAuth({ access_token, isAuthenticated, userInfo, isLoading });
}
}
// TODO(mvp) : fix performance issues > no need to check auth on every render
useEffect(() => {
if (!auth.isAuthenticated) {
checkAuth();
}
}, []);
return <AuthContext.Provider value={auth}>{props.children}</AuthContext.Provider>;
};
export default AuthProvider;

View file

@ -6,8 +6,10 @@ import Link from "next/link";
export const Menu = () => {
return (
<GlobalHeader>
<LogoArea>
<Logo>
<img style={{ width: "30px", opacity: "0.9", margin: "10px", paddingRight: "4px" }} src="./learnhouse_icon.png" alt="" />
<Link href={"/"}>
<a>
<img src="./learnhouse_logo.png" alt="" />
@ -50,7 +52,9 @@ const Logo = styled.div`
display: flex;
place-items: center;
padding-left: 20px;
a{
margin: 0;
}
img {
width: 100px;
}

View file

@ -2,23 +2,26 @@ import React from "react";
import Head from "next/head";
import { Header } from "./header";
import styled from "styled-components";
import AuthProvider from "../security/AuthProvider";
const Layout = (props: any) => {
return (
<div>
<Head>
<title>{props.title}</title>
<meta name="description" content={props.description} />
<link rel="icon" href="/favicon.ico" />
</Head>
<Header></Header>
<Main className="min-h-screen">{props.children}</Main>
<AuthProvider>
<Head>
<title>{props.title}</title>
<meta name="description" content={props.description} />
<link rel="icon" href="/favicon.ico" />
</Head>
<Header></Header>
<Main className="min-h-screen">{props.children}</Main>
<Footer>
<a href="" target="_blank" rel="noopener noreferrer">
<img src="/learnhouse_icon.png" alt="Learnhouse Logo" />
</a>
</Footer>
<Footer>
<a href="" target="_blank" rel="noopener noreferrer">
<img src="/learnhouse_icon.png" alt="Learnhouse Logo" />
</a>
</Footer>
</AuthProvider>
</div>
);
};
@ -33,9 +36,9 @@ const Footer = styled.footer`
margin: 20px;
font-size: 16px;
img{
width: 40px;
opacity: 0.40;
img {
width: 20px;
opacity: 0.4;
display: inline;
}
`;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Before After
Before After

View file

@ -5,7 +5,8 @@ interface LoginAndGetTokenResponse {
token_type: "string";
}
// TODO : everything in this file need to be refactored / mvp phase
// ⚠️ mvp phase code
// TODO : everything in this file need to be refactored including security issues fix
export async function loginAndGetToken(username: string, password: string): Promise<LoginAndGetTokenResponse> {
// Request Config
@ -21,21 +22,34 @@ export async function loginAndGetToken(username: string, password: string): Prom
};
// fetch using await and async
const response = await fetch(`${getAPIUrl()}auth/token`, requestOptions);
const response = await fetch(`${getAPIUrl()}auth/login`, requestOptions);
const data = await response.json();
return data;
}
export async function getUserInfo(token: string): Promise<any> {
const HeadersConfig = new Headers({ Authorization: `Bearer ${token}`, Origin: "http://localhost:3000" });
const requestOptions: any = {
method: "GET",
headers: HeadersConfig,
redirect: "follow",
credentials: "include"
credentials: "include",
};
return fetch(`${getAPIUrl()}auth/users/me`, requestOptions)
return fetch(`${getAPIUrl()}users/profile`, requestOptions)
.then((result) => result.json())
.catch((error) => console.log("error", error));
}
export async function getRefreshToken(): Promise<any> {
const requestOptions: any = {
method: "POST",
redirect: "follow",
credentials: "include",
};
return fetch(`${getAPIUrl()}auth/refresh`, requestOptions)
.then((result) => result.json())
.catch((error) => console.log("error", error));
}

View file

@ -1,10 +1,11 @@
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500&display=swap');
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-family: 'DM Sans' , -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {

View file

@ -5,3 +5,4 @@ pymongo==4.1.1
python-multipart
python-jose
passlib
fastapi-jwt-auth

View file

@ -1,3 +1,4 @@
from urllib.request import Request
from fastapi import Depends, APIRouter, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from src.services.auth import *
@ -5,9 +6,10 @@ from src.services.users import *
from datetime import timedelta
from fastapi.responses import JSONResponse
router = APIRouter()
# DEPRECATED
@router.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
"""
@ -29,3 +31,44 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(
response.set_cookie(key="user_token", value=access_token, httponly=True, expires="3600",secure=True)
return response
@router.post('/refresh')
def refresh(Authorize: AuthJWT = Depends()):
"""
The jwt_refresh_token_required() function insures a valid refresh
token is present in the request before running any code below that function.
we can use the get_jwt_subject() function to get the subject of the refresh
token, and use the create_access_token() function again to make a new access token
"""
Authorize.jwt_refresh_token_required()
current_user = Authorize.get_jwt_subject()
new_access_token = Authorize.create_access_token(subject=current_user)
return {"access_token": new_access_token}
@router.post('/login')
async def login(Authorize: AuthJWT = Depends(), form_data: OAuth2PasswordRequestForm = Depends()):
user = await authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect Email or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = Authorize.create_access_token(subject=form_data.username)
refresh_token = Authorize.create_refresh_token(subject=form_data.username)
Authorize.set_refresh_cookies(refresh_token)
return {"access_token": access_token , "refresh_token": refresh_token}
@router.delete('/logout')
def logout(Authorize: AuthJWT = Depends()):
"""
Because the JWT are stored in an httponly cookie now, we cannot
log the user out by simply deleting the cookies in the frontend.
We need the backend to send us a response to delete the cookies.
"""
Authorize.jwt_required()
Authorize.unset_jwt_cookies()
return {"msg":"Successfully logout"}

View file

@ -9,7 +9,7 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
router = APIRouter()
# DEPRECATED
@router.get("/me")
async def api_get_current_user(current_user: User = Depends(get_current_user)):
"""
@ -17,6 +17,13 @@ async def api_get_current_user(current_user: User = Depends(get_current_user)):
"""
return current_user.dict()
@router.get("/profile")
async def api_get_current_user(current_user: User = Depends(get_current_user_jwt)):
"""
Get current user
"""
return current_user.dict()
@router.get("/username/{username}")
async def api_get_user_by_username(username: str):

View file

@ -5,9 +5,28 @@ from passlib.context import CryptContext
from jose import JWTError, jwt
from datetime import datetime, timedelta
from src.services.users import *
from fastapi import Cookie, FastAPI
from src.services.security import *
from fastapi_jwt_auth import AuthJWT
from fastapi_jwt_auth.exceptions import AuthJWTException
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
#### JWT Auth ####################################################
class Settings(BaseModel):
authjwt_secret_key: str = "secret"
authjwt_token_location = {"cookies", "headers"}
authjwt_cookie_csrf_protect = False
@AuthJWT.load_config
def get_config():
return Settings()
#### JWT Auth ####################################################
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
#### Classes ####################################################
@ -42,7 +61,7 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None):
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# DEPRECATED
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@ -57,7 +76,28 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user = await security_get_user(username=token_data.username)
user = await security_get_user(email=token_data.username)
if user is None:
raise credentials_exception
return PublicUser(**user.dict())
async def get_current_user_jwt(Authorize: AuthJWT = Depends()):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
Authorize.jwt_required()
username = Authorize.get_jwt_subject()
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user = await security_get_user(email=token_data.username) # treated as an email
if user is None:
raise credentials_exception
return PublicUser(**user.dict())