Skip to content

Commit

Permalink
PB-17k: Cookie based auth (#10)
Browse files Browse the repository at this point in the history
* PB-17k: adds cookie helper methods and JWT config

* PB-17k: adds cookie in header on login

* PB-14k: adds refresh of soon to expire cookies

* PB-17k: fixes lifetime of cookies

* PB-17k: switch to cookie auth in old frontend for testing

* PB-17k: removes print statement
  • Loading branch information
stianjsu authored Aug 16, 2023
1 parent 2e642f5 commit 21b1455
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 15 deletions.
6 changes: 4 additions & 2 deletions application/backend/app/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from app.repositories.user_repository import UserRepository
from app.models.user_schema import UserSchema
from app.auth import auth
from flask_jwt_extended import create_access_token, create_refresh_token, jwt_required, get_jwt_identity
from flask_jwt_extended import create_access_token, create_refresh_token, jwt_required, get_jwt_identity, set_access_cookies
from app.services.slack_organization_service import SlackOrganizationService
from app.services.injector import injector

Expand Down Expand Up @@ -114,5 +114,7 @@ def get(self):
}
access_token = create_access_token(identity=user, additional_claims=additional_claims)
refresh_token = create_refresh_token(identity=user, additional_claims=additional_claims)
return jsonify(access_token=access_token, refresh_token=refresh_token)
response = jsonify(access_token=access_token, refresh_token=refresh_token)
set_access_cookies(response, access_token)
return response
return abort(401, message = "User email not available or not verified by Slack.")
4 changes: 3 additions & 1 deletion application/backend/app/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from app.db import db, migrate
from app.api import api, ma
from app.auth import auth, jwt
from app.auth import auth, jwt, refresh_cookie
from app.services.broker import broker
from app.services.injector import injector
from app.services.invitation_service import InvitationService
Expand Down Expand Up @@ -104,8 +104,10 @@ def add_headers(response):
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
response.headers['Content-Type'] = 'application/json; charset=utf-8'
response = refresh_cookie(response)
return response


# Set up CORS
if app.config["ENV"] == "production":
origins = [FRONTEND_URI]
Expand Down
44 changes: 36 additions & 8 deletions application/backend/app/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
from oauthlib.oauth2 import WebApplicationClient
from flask_jwt_extended import JWTManager
from flask_jwt_extended import JWTManager, get_jwt, get_jwt_identity, set_access_cookies, create_access_token
from app.repositories.user_repository import UserRepository
from datetime import datetime, timedelta, timezone

from app.models.user_schema import UserSchema


class AuthClient():
client: WebApplicationClient
client: WebApplicationClient

def __init__(self, app=None, **kwargs):
if (app):
self.init_app(app, **kwargs)

def init_app(self, app, **kwargs):
self.client = WebApplicationClient(
app.config["SLACK_CLIENT_ID"], kwargs=kwargs)

def __init__(self, app = None, **kwargs):
if (app):
self.init_app(app, **kwargs)

def init_app(self, app, **kwargs):
self.client = WebApplicationClient(app.config["SLACK_CLIENT_ID"], kwargs=kwargs)

auth = AuthClient()
jwt = JWTManager()
Expand All @@ -30,3 +36,25 @@ def user_lookup_callback(_jwt_header, jwt_data):
identity = jwt_data["sub"]
user = UserRepository.get_by_id(identity)
return user


def refresh_cookie(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:
identity = get_jwt_identity()
user = UserRepository.get_by_id(identity)
json_user = UserSchema().dump(user)
additional_claims = {
# TODO handle roles
"user": {**json_user, "roles": []}
}
access_token = create_access_token(
identity=user, additional_claims=additional_claims)
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
6 changes: 4 additions & 2 deletions application/backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ class Base(object):
OPENAPI_SWAGGER_UI_PATH = "/swagger"
# The following is equivalent to OPENAPI_SWAGGER_UI_VERSION = '3.19.5'
OPENAPI_SWAGGER_UI_URL = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.19.5/"
# JWT
JWT_ACCESS_TOKEN_EXPIRES = timedelta(minutes=30)
# JWT TODO: JWT_SECRET_KEY ?
JWT_ACCESS_TOKEN_EXPIRES = timedelta(minutes=60)
JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30)
JWT_TOKEN_LOCATION = ["headers", "cookies"]
JWT_COOKIE_SECURE = True


class Test(Base):
Expand Down
11 changes: 9 additions & 2 deletions application/frontend/src/api/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@ import { RefreshJWT } from './AuthService';

const baseUrl = process.env.BACKEND_URI ? `${process.env.BACKEND_URI.replace(/\/+$/, '')}/api` : '/api';

function getCookie(name: string) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts && parts.length === 2) return parts.pop()?.split(';')?.shift();
}

export const httpClient = (token?: string): AxiosInstance => {
const headers: AxiosRequestHeaders = {};
const cookie = getCookie('csrf_access_token');

if (token) {
headers.Authorization = `Bearer ${token}`;
if (cookie) {
headers['X-CSRF-TOKEN'] = cookie;
}

return axios.create({
Expand Down

0 comments on commit 21b1455

Please sign in to comment.