Skip to content
This repository has been archived by the owner on Oct 30, 2021. It is now read-only.

[BB-1718] Implement JWT token refreshing #24

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compose/local/django/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ ENV PYTHONUNBUFFERED 1

RUN apk update \
# psycopg2 dependencies
&& apk add --virtual build-deps gcc python3-dev musl-dev \
&& apk add --virtual build-deps gcc python3-dev musl-dev git \
&& apk add postgresql-dev \
# Pillow dependencies
&& apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev \
Expand Down
13 changes: 7 additions & 6 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"rest_auth.registration",
"drf_yasg",
"corsheaders",
'rest_framework_simplejwt.token_blacklist',
]
LOCAL_APPS = [
"sprints.users.apps.UsersConfig",
Expand Down Expand Up @@ -297,7 +298,7 @@
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
Expand All @@ -306,11 +307,11 @@
REST_AUTH_SERIALIZERS = {
'USER_DETAILS_SERIALIZER': 'sprints.users.serializers.UserDetailsSerializer',
}
JWT_AUTH = {
'JWT_PAYLOAD_HANDLER': 'sprints.users.jwt.jwt_payload_handler_custom',
'JWT_EXPIRATION_DELTA': datetime.timedelta(minutes=env.int("DJANGO_JWT_EXPIRATION_MINUTES", 10)),
'JWT_ALLOW_REFRESH': env.bool("DJANGO_JWT_ALLOW_REFRESH", True),
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=env.int("DJANGO_JWT_REFRESH_EXPIRATION_DAYS", 7)),
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': datetime.timedelta(hours=env.float("JWT_ACCESS_TOKEN_LIFETIME_HOURS", 3)),
'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=env.float("JWT_REFRESH_TOKEN_LIFETIME_DAYS", 30)),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
}


Expand Down
2 changes: 2 additions & 0 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions
from rest_framework_simplejwt.views import TokenRefreshView

from sprints.users.api import GoogleLogin

Expand Down Expand Up @@ -59,6 +60,7 @@

urlpatterns += [
path("accounts/", include("allauth.urls")),
url(r'^rest-auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
url(r'^rest-auth/', include('rest_auth.urls')),
url(r'^rest-auth/registration/', include('rest_auth.registration.urls')),
url(r'^rest-auth/google/$', GoogleLogin.as_view(), name='google_login'),
Expand Down
42 changes: 13 additions & 29 deletions frontend/src/actions/auth.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
import {PATH_GOOGLE, PATH_LOGIN, PATH_LOGOUT, PATH_REGISTER, PATH_USER, PATH_VERIFY_EMAIL} from "../constants";
import {callApi} from "../middleware/api";

export const loadUser = () => {
return (dispatch, getState) => {
dispatch({type: "USER_LOADING"});

const token = getState().auth.token;

let headers = {
"Content-Type": "application/json",
};

if (token) {
headers["Authorization"] = `JWT ${token}`;
}
return fetch(PATH_USER, {headers,})
return (dispatch) => {
return callApi(PATH_USER)
.then(res => {
if (res.status < 500) {
return res.json().then(data => {
Expand All @@ -37,11 +27,10 @@ export const loadUser = () => {
};

export const login = (email, password) => {
return (dispatch, getState) => {
let headers = {"Content-Type": "application/json"};
return (dispatch) => {
let body = JSON.stringify({email, password});

return fetch(PATH_LOGIN, {headers, body, method: "POST"})
return callApi(PATH_LOGIN, body)
.then(res => {
if (res.status < 500) {
return res.json().then(data => {
Expand All @@ -68,11 +57,10 @@ export const login = (email, password) => {
};

export const social_login = (access_token) => {
return (dispatch, getState) => {
let headers = {"Content-Type": "application/json"};
return (dispatch) => {
let body = JSON.stringify({access_token});

return fetch(PATH_GOOGLE, {headers, body, method: "POST"})
return callApi(PATH_GOOGLE, body)
.then(res => {
if (res.status < 500) {
return res.json().then(data => {
Expand All @@ -99,11 +87,10 @@ export const social_login = (access_token) => {
};

export const register = (email, password1, password2) => {
return (dispatch, getState) => {
let headers = {"Content-Type": "application/json"};
return (dispatch) => {
let body = JSON.stringify({email, password1, password2});

return fetch(PATH_REGISTER, {headers, body, method: "POST"})
return callApi(PATH_REGISTER, body)
.then(res => {
if (res.status < 500) {
return res.json().then(data => {
Expand All @@ -130,10 +117,8 @@ export const register = (email, password1, password2) => {
};

export const logout = () => {
return (dispatch, getState) => {
let headers = {"Content-Type": "application/json"};

return fetch(PATH_LOGOUT, {headers, body: "", method: "POST"})
return (dispatch) => {
return callApi(PATH_LOGOUT, "", "POST")
.then(res => {
if (res.status === 200) {
return {status: res.status, data: {}};
Expand All @@ -159,11 +144,10 @@ export const logout = () => {
};

export const verify_email = (key) => {
return (dispatch, getState) => {
let headers = {"Content-Type": "application/json"};
return (dispatch) => {
let body = JSON.stringify({key});

return fetch(PATH_VERIFY_EMAIL, {headers, body, method: "POST"})
return callApi(PATH_VERIFY_EMAIL, body)
.then(res => {
if (res.status < 500) {
return res.json().then(data => {
Expand Down
25 changes: 5 additions & 20 deletions frontend/src/actions/sprints.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {PARAM_BOARD_ID, PATH_CELLS, PATH_DASHBOARD} from "../constants";
import {callApi} from "../middleware/api";

const prepareCellIds = (cells) => {
const result = {};
Expand All @@ -8,18 +9,10 @@ const prepareCellIds = (cells) => {
};

export const loadCells = () => {
return (dispatch, getState) => {
return (dispatch) => {
dispatch({type: "CELLS_LOADING"});

let token = getState().auth.token;
let headers = {
"Content-Type": "application/json",
};
if (token) {
headers["Authorization"] = `JWT ${token}`;
}

return fetch(PATH_CELLS, {headers,})
return callApi(PATH_CELLS)
.then(response => {
if (response.status < 500) {
return response.json().then(data => {
Expand All @@ -46,18 +39,10 @@ export const loadCells = () => {
export const loadBoard = (board_id) => {
let board_url = `${PATH_DASHBOARD}?${PARAM_BOARD_ID}${board_id}`;

return (dispatch, getState) => {
return (dispatch) => {
dispatch({type: "BOARD_LOADING", board_id: board_id});

let token = getState().auth.token;
let headers = {
"Content-Type": "application/json",
};
if (token) {
headers["Authorization"] = `JWT ${token}`;
}

return fetch(board_url, {headers,})
return callApi(board_url)
.then(response => {
if (response.status < 500) {
return response.json().then(data => {
Expand Down
13 changes: 3 additions & 10 deletions frontend/src/actions/sustainability.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {PARAM_FROM, PARAM_TO, PARAM_YEAR, PATH_SUSTAINABILITY_DASHBOARD} from "../constants";
import {callApi} from "../middleware/api";

const aggregateAccounts = (data) => {
let result = {};
Expand Down Expand Up @@ -31,18 +32,10 @@ export const loadAccounts = (from, to) => {
sustainability_url = `${PATH_SUSTAINABILITY_DASHBOARD}?${PARAM_YEAR}${from}`;
}

return (dispatch, getState) => {
return (dispatch) => {
dispatch({type: "ACCOUNTS_LOADING"});

let token = getState().auth.token;
let headers = {
"Content-Type": "application/json",
};
if (token) {
headers["Authorization"] = `JWT ${token}`;
}

return fetch(sustainability_url, {headers,})
return callApi(sustainability_url)
.then(response => {
if (response.status < 500) {
return response.json().then(data => {
Expand Down
10 changes: 2 additions & 8 deletions frontend/src/components/sprint/SprintActionButtons.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {connect} from "react-redux";
import React, {Component} from 'react';
import {PARAM_BOARD_ID, PATH_COMPLETE_SPRINT, PATH_CREATE_NEXT_SPRINT} from "../../constants";
import {callApi} from "../../middleware/api";

class SprintActionButton extends Component {

Expand All @@ -10,16 +11,9 @@ class SprintActionButton extends Component {
}

this.btn.setAttribute("disabled", "disabled");
let token = this.props.auth.token;
let headers = {
"Content-Type": "application/json",
};
if (token) {
headers["Authorization"] = `JWT ${token}`;
}

let complete_url = `${this.props.url}?${PARAM_BOARD_ID}${this.props.board_id}`;
fetch(complete_url, {headers, body: "", method: "POST"})
callApi(complete_url, "", "POST")
.then(response => response.json())
};

Expand Down
1 change: 1 addition & 0 deletions frontend/src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const PATH_REGISTER = `${PATH_AUTH}/registration/`;
export const PATH_GOOGLE = `${PATH_AUTH}/google/`;
export const PATH_LOGOUT = `${PATH_AUTH}/logout/`;
export const PATH_VERIFY_EMAIL = `${PATH_AUTH}/registration/verify-email/`;
export const PATH_REFRESH_TOKEN = `${PATH_AUTH}/refresh/`;

export const PATH_SPRINT_DASHBOARD = `${PATH_BASE}/dashboard`;
export const PATH_CELLS = `${PATH_SPRINT_DASHBOARD}/cells/`;
Expand Down
45 changes: 45 additions & 0 deletions frontend/src/middleware/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {PATH_REFRESH_TOKEN} from "../constants";

const refreshToken = () => {
const refresh = localStorage.getItem("refresh_token");
const body = JSON.stringify({refresh: refresh});

return callApi(PATH_REFRESH_TOKEN, body, "POST", true).then(res => {
if (res.status === 200) {


}
return res;
});
};

export const callApi = (endpoint, body = null, method = null, refresh_token = false) => {
method = method ? method : body ? "POST" : "GET";
let access_token = localStorage.getItem("access_token");

let headers = {
"Content-Type": "application/json",
};

if (access_token) {
headers.Authorization = `Bearer ${access_token}`;
}

return fetch(endpoint, {headers, body, method: method}).then(res => {
if (res.status === 401 && !refresh_token) {
return refreshToken().then(ref => {
if (ref.status === 200) {
return ref.json().then(json => {
headers.Authorization = `Bearer ${json.access}`;
localStorage.setItem("access_token", json.access);
localStorage.setItem("refresh_token", json.refresh);

return fetch(endpoint, {headers, body, method: method});
});
}
return res;
});
}
return res;
});
};
15 changes: 11 additions & 4 deletions frontend/src/reducers/auth.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const initialState = {
token: localStorage.getItem("token"),
isAuthenticated: null,
isLoading: true,
user: null,
Expand All @@ -19,7 +18,8 @@ export default function auth(state=initialState, action) {

case 'LOGIN_SUCCESSFUL':
case 'REGISTRATION_SUCCESSFUL':
localStorage.setItem("token", action.data.token);
localStorage.setItem("access_token", action.data.access_token);
localStorage.setItem("refresh_token", action.data.refresh_token);
return {...state, ...action.data, isAuthenticated: true, isLoading: false, errors: null};

case 'EMAIL_VERIFICATION_SUCCESSFUL':
Expand All @@ -30,8 +30,15 @@ export default function auth(state=initialState, action) {
case 'REGISTRATION_FAILED':
case 'LOGOUT_SUCCESSFUL':
case 'EMAIL_VERIFICATION_FAILED':
localStorage.removeItem("token");
return {...state, errors: action.data, token: null, user: null,
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");

// Handle non-standard error Object from simpleJWT.
if (action.data && action.data.messages) {
action.data = action.data.messages.map(entry => entry.message);
}

return {...state, errors: action.data, user: null,
isAuthenticated: false, isLoading: false};

default:
Expand Down
3 changes: 2 additions & 1 deletion requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ django-cors-headers~=3.0.2 # https://github.com/ottoyiu/django-cors-headers
# ------------------------------------------------------------------------------
djangorestframework~=3.9.4 # https://github.com/encode/django-rest-framework
djangorestframework-jwt~=1.11.0 # https://github.com/GetBlimp/django-rest-framework-jwt
djangorestframework_simplejwt~=4.3.0 # https://github.com/davesque/django-rest-framework-simplejwt
coreapi~=2.3.3 # https://github.com/core-api/python-client
drf-yasg~=1.15.0 # https://github.com/axnsan12/drf-yasg

# REST auth
# ------------------------------------------------------------------------------
django-rest-auth~=0.9.5 # https://github.com/Tivix/django-rest-auth
git+https://github.com/open-craft/django-rest-auth[email protected] # fork of https://github.com/Tivix/django-rest-auth with simpleJWT support


# Jira API
Expand Down
9 changes: 0 additions & 9 deletions sprints/users/jwt.py

This file was deleted.