-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* backend-authentication * fix projects * remove extra requirements * fix test-auth_server * added: frontend redirect link * fix backend auth * fix error + code_challenge in frontend link * line too long + frontend linter errors * actual linter fix * removed code_verifier until milestone 3 * added ability to log out and added everything to init * redis added later * added login button to header * linter fix unused import * add display_name to user creation * test tests * linter fix * cookies test * flask test cookies? * added logout button * with client
- Loading branch information
Showing
16 changed files
with
328 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
"""Auth api endpoint""" | ||
from os import getenv | ||
|
||
from dotenv import load_dotenv | ||
import requests | ||
from flask import Blueprint, request, redirect, abort, make_response | ||
from flask_jwt_extended import create_access_token, set_access_cookies | ||
from flask_restful import Resource, Api | ||
from sqlalchemy.exc import SQLAlchemyError | ||
|
||
from project import db | ||
|
||
from project.models.user import User, Role | ||
|
||
auth_bp = Blueprint("auth", __name__) | ||
auth_api = Api(auth_bp) | ||
|
||
load_dotenv() | ||
API_URL = getenv("API_HOST") | ||
AUTH_METHOD = getenv("AUTH_METHOD") | ||
AUTHENTICATION_URL = getenv("AUTHENTICATION_URL") | ||
CLIENT_ID = getenv("CLIENT_ID") | ||
CLIENT_SECRET = getenv("CLIENT_SECRET") | ||
HOMEPAGE_URL = getenv("HOMEPAGE_URL") | ||
TENANT_ID = getenv("TENANT_ID") | ||
|
||
def microsoft_authentication(): | ||
""" | ||
This function will handle a microsoft based login, | ||
creating a new user profile in the process and | ||
return a valid access token as a cookie. | ||
Redirects to the homepage of the website | ||
""" | ||
code = request.args.get("code") | ||
if code is None: | ||
return {"message":"This endpoint is only used for authentication."}, 400 | ||
# got code from microsoft | ||
data = {"client_id":CLIENT_ID, | ||
"scope":".default", | ||
"code":code, | ||
"redirect_uri":f"{API_URL}/auth", | ||
"grant_type":"authorization_code", | ||
"client_secret":CLIENT_SECRET} | ||
try: | ||
res = requests.post(f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token", | ||
data=data, | ||
timeout=5) | ||
if res.status_code != 200: | ||
abort(make_response(( | ||
{"message": | ||
"An error occured while trying to authenticate your authorization code"}, | ||
500))) | ||
token = res.json()["access_token"] | ||
profile_res = requests.get("https://graph.microsoft.com/v1.0/me", | ||
headers={"Authorization":f"Bearer {token}"}, | ||
timeout=5) | ||
except TimeoutError: | ||
return {"message":"Request to Microsoft timed out"}, 500 | ||
if not profile_res or profile_res.status_code != 200: | ||
abort(make_response(({"message": | ||
"An error occured while trying to authenticate your access token"}, | ||
500))) | ||
auth_user_id = profile_res.json()["id"] | ||
try: | ||
user = db.session.get(User, auth_user_id) | ||
except SQLAlchemyError: | ||
db.session.rollback() | ||
abort(make_response(({"message": | ||
"An unexpected database error occured while fetching the user"}, | ||
500))) | ||
|
||
if not user: | ||
role = Role.STUDENT | ||
if profile_res.json()["jobTitle"] is not None: | ||
role = Role.TEACHER | ||
|
||
# add user if not yet in database | ||
try: | ||
new_user = User(uid=auth_user_id, | ||
role=role, | ||
display_name=profile_res.json()["displayName"]) | ||
db.session.add(new_user) | ||
db.session.commit() | ||
user = new_user | ||
except SQLAlchemyError: | ||
db.session.rollback() | ||
abort(make_response(({"message": | ||
"""An unexpected database error occured | ||
while creating the user during authentication"""}, 500))) | ||
resp = redirect(HOMEPAGE_URL, code=303) | ||
additional_claims = {"is_teacher":user.role == Role.TEACHER, | ||
"is_admin":user.role == Role.ADMIN} | ||
set_access_cookies(resp, | ||
create_access_token(identity=profile_res.json()["id"], | ||
additional_claims=additional_claims)) | ||
return resp | ||
|
||
|
||
def test_authentication(): | ||
""" | ||
This function will handle the logins done using our | ||
own authentication server for testing purposes | ||
""" | ||
code = request.args.get("code") | ||
if code is None: | ||
return {"message":"Not yet"}, 500 | ||
profile_res = requests.get(AUTHENTICATION_URL, headers={"Authorization":f"{code}"}, timeout=5) | ||
resp = redirect(HOMEPAGE_URL, code=303) | ||
set_access_cookies(resp, create_access_token(identity=profile_res.json()["id"])) | ||
return resp | ||
|
||
|
||
class Auth(Resource): | ||
"""Api endpoint for the /auth route""" | ||
|
||
def get(self): | ||
""" | ||
Will handle the request according to the method defined in the .env variables. | ||
Currently only Microsoft and our test authentication are supported | ||
""" | ||
if AUTH_METHOD == "Microsoft": | ||
return microsoft_authentication() | ||
return test_authentication() | ||
|
||
auth_api.add_resource(Auth, "/auth") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
"""Api endpoint to handle logout requests""" | ||
from os import getenv | ||
|
||
from dotenv import load_dotenv | ||
from flask import Blueprint, redirect | ||
from flask_jwt_extended import unset_jwt_cookies, jwt_required | ||
from flask_restful import Resource, Api | ||
|
||
logout_bp = Blueprint("logout", __name__) | ||
logout_api = Api(logout_bp) | ||
|
||
load_dotenv() | ||
HOMEPAGE_URL = getenv("HOMEPAGE_URL") | ||
|
||
class Logout(Resource): | ||
"""Api endpoint for the /auth route""" | ||
|
||
@jwt_required() | ||
def get(self): | ||
""" | ||
Will handle the request according to the method defined in the .env variables. | ||
Currently only Microsoft and our test authentication are supported | ||
""" | ||
resp = redirect(HOMEPAGE_URL, 303) | ||
unset_jwt_cookies(resp) | ||
return resp | ||
|
||
logout_api.add_resource(Logout, "/logout") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
"""User info api endpoint""" | ||
from os import getenv | ||
|
||
from dotenv import load_dotenv | ||
from flask import Blueprint | ||
from flask_jwt_extended import get_jwt_identity, jwt_required | ||
from flask_restful import Resource, Api | ||
|
||
from project.models.user import User | ||
from project.utils.query_agent import query_by_id_from_model | ||
|
||
load_dotenv() | ||
API_URL = getenv("API_HOST") | ||
|
||
me_bp = Blueprint("me", __name__) | ||
me_api = Api(me_bp) | ||
|
||
class Me(Resource): | ||
"""Api endpoint for the /user_info route""" | ||
|
||
@jwt_required() | ||
def get(self): | ||
""" | ||
Will return all user data associated with the access token in the request | ||
""" | ||
uid = get_jwt_identity | ||
|
||
return query_by_id_from_model(User, | ||
"uid", | ||
uid, | ||
"Could not find you in the database, please log in again") | ||
|
||
me_api.add_resource(Me, "/me") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
""" This file will change the JWT return messages to custom messages | ||
and make it so the access tokens implicitly refresh | ||
""" | ||
from datetime import timedelta, timezone, datetime | ||
|
||
from flask_jwt_extended import get_jwt, get_jwt_identity,\ | ||
create_access_token, set_access_cookies | ||
|
||
def auth_init(jwt, app): | ||
""" | ||
This function will overwrite the default return messages from | ||
the flask-jwt-extended package with custom messages | ||
and make it so the access tokens implicitly refresh | ||
""" | ||
@jwt.expired_token_loader | ||
def expired_token_callback(jwt_header, jwt_payload): | ||
return ( | ||
{"message":"Your access token cookie has expired, please log in again"}, | ||
401) | ||
|
||
@jwt.invalid_token_loader | ||
def invalid_token_callback(jwt_header, jwt_payload): | ||
return ( | ||
{"message":("The server cannot recognize this access token cookie, " | ||
"please log in again if you think this is an error")}, | ||
401) | ||
|
||
@jwt.revoked_token_loader | ||
def revoked_token_callback(jwt_header, jwt_payload): | ||
return ( | ||
{"message":("This access token cookie has been revoked, " | ||
"possibly from logging out. Log in again to receive a new access token")}, | ||
401) | ||
|
||
@jwt.unauthorized_loader | ||
def unauthorized_callback(jwt_header): | ||
return {"message":"You need an access token to get this data, please log in"}, 401 | ||
|
||
@app.after_request | ||
def refresh_expiring_jwts(response): | ||
try: | ||
exp_timestamp = get_jwt()["exp"] | ||
now = datetime.now(timezone.utc) | ||
target_timestamp = datetime.timestamp(now + timedelta(minutes=30)) | ||
if target_timestamp > exp_timestamp: | ||
access_token = create_access_token( | ||
identity=get_jwt_identity(), | ||
additional_claims= | ||
{"is_admin":get_jwt()["is_admin"], | ||
"is_teacher":get_jwt()["is_teacher"]} | ||
) | ||
set_access_cookies(response, access_token) | ||
return response | ||
except (RuntimeError, KeyError): | ||
# Case where there is not a valid JWT. Just return the original response | ||
return response |
Oops, something went wrong.