use the cookie from the frontend w/ burnettk

This commit is contained in:
jasquat 2023-01-11 17:27:12 -05:00
parent 8ab5ad7074
commit 2630dbfb45
12 changed files with 91 additions and 70 deletions

View File

@ -23,7 +23,7 @@ from spiffworkflow_backend.routes.admin_blueprint.admin_blueprint import admin_b
from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import ( from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import (
openid_blueprint, openid_blueprint,
) )
from spiffworkflow_backend.routes.user import verify_token from spiffworkflow_backend.routes.user import set_new_access_token_in_cookie, verify_token
from spiffworkflow_backend.routes.user_blueprint import user_blueprint from spiffworkflow_backend.routes.user_blueprint import user_blueprint
from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.background_processing_service import ( from spiffworkflow_backend.services.background_processing_service import (
@ -131,6 +131,7 @@ def create_app() -> flask.app.Flask:
app.before_request(verify_token) app.before_request(verify_token)
app.before_request(AuthorizationService.check_for_permission) app.before_request(AuthorizationService.check_for_permission)
app.after_request(set_new_access_token_in_cookie)
return app # type: ignore return app # type: ignore

View File

@ -29,8 +29,8 @@ CONNECTOR_PROXY_URL = environ.get(
# Open ID server # Open ID server
OPEN_ID_SERVER_URL = environ.get( OPEN_ID_SERVER_URL = environ.get(
# "OPEN_ID_SERVER_URL", default="http://localhost:7002/realms/spiffworkflow" "OPEN_ID_SERVER_URL", default="http://localhost:7002/realms/spiffworkflow"
"OPEN_ID_SERVER_URL", default="http://localhost:7000/openid" # "OPEN_ID_SERVER_URL", default="http://localhost:7000/openid"
) )
# Replace above line with this to use the built-in Open ID Server. # Replace above line with this to use the built-in Open ID Server.

View File

@ -34,8 +34,6 @@ def well_known() -> dict:
These urls can be very different from one openid impl to the next, this is just a small subset. These urls can be very different from one openid impl to the next, this is just a small subset.
""" """
host_url = request.host_url.strip("/") host_url = request.host_url.strip("/")
print(f"host_url: {host_url}")
print(f"request.path: {request.url}")
return { return {
"issuer": f"{host_url}/openid", "issuer": f"{host_url}/openid",
"authorization_endpoint": f"{host_url}{url_for('openid.auth')}", "authorization_endpoint": f"{host_url}{url_for('openid.auth')}",

View File

@ -14,9 +14,11 @@ from flask import redirect
from flask import request from flask import request
from flask_bpmn.api.api_error import ApiError from flask_bpmn.api.api_error import ApiError
from werkzeug.wrappers import Response from werkzeug.wrappers import Response
# from flask.wrappers import Response
import flask
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.authentication_service import AuthenticationService from spiffworkflow_backend.services.authentication_service import TokenExpiredError, AuthenticationService
from spiffworkflow_backend.services.authentication_service import ( from spiffworkflow_backend.services.authentication_service import (
MissingAccessTokenError, MissingAccessTokenError,
) )
@ -57,6 +59,11 @@ def verify_token(
if not token and "Authorization" in request.headers: if not token and "Authorization" in request.headers:
token = request.headers["Authorization"].removeprefix("Bearer ") token = request.headers["Authorization"].removeprefix("Bearer ")
# This should never be set here but just in case
tld = current_app.config['THREAD_LOCAL_DATA']
if hasattr(tld, "new_access_token"):
tld.new_access_token = None
if token: if token:
user_model = None user_model = None
decoded_token = get_decoded_token(token) decoded_token = get_decoded_token(token)
@ -73,13 +80,11 @@ def verify_token(
f" internal token. {e}" f" internal token. {e}"
) )
elif "iss" in decoded_token.keys(): elif "iss" in decoded_token.keys():
user_info = None
try: try:
if AuthenticationService.validate_id_token(token): if AuthenticationService.validate_id_or_access_token(token):
user_info = decoded_token user_info = decoded_token
except ( except (TokenExpiredError) as token_expired_error:
ApiError
) as ae: # API Error is only thrown in the token is outdated.
print("HEY WE IN ERROR")
# Try to refresh the token # Try to refresh the token
user = UserService.get_user_by_service_and_service_id( user = UserService.get_user_by_service_and_service_id(
decoded_token["iss"], decoded_token["sub"] decoded_token["iss"], decoded_token["sub"]
@ -92,26 +97,28 @@ def verify_token(
refresh_token refresh_token
) )
) )
# set_access_cookies()
print(f"auth_token: {auth_token}")
if auth_token and "error" not in auth_token: if auth_token and "error" not in auth_token:
print("SETTING NEW TOKEN")
print(f"auth_token: {auth_token}")
tld.new_access_token = auth_token['access_token']
# We have the user, but this code is a bit convoluted, and will later demand # We have the user, but this code is a bit convoluted, and will later demand
# a user_info object so it can look up the user. Sorry to leave this crap here. # a user_info object so it can look up the user. Sorry to leave this crap here.
user_info = {"sub": user.service_id, "iss": user.service} user_info = {"sub": user.service_id, "iss": user.service}
else:
raise ae if user_info is None:
else: raise ApiError(
raise ae error_code="invalid_token",
else: message="Your token is expired. Please Login",
raise ae status_code=401,
) from token_expired_error
except Exception as e: except Exception as e:
current_app.logger.error(f"Exception raised in get_token: {e}")
raise ApiError( raise ApiError(
error_code="fail_get_user_info", error_code="fail_get_user_info",
message="Cannot get user info from token", message="Cannot get user info from token",
status_code=401, status_code=401,
) from e ) from e
print(f"USER_INFO: {user_info}")
if ( if (
user_info is not None user_info is not None
and "error" not in user_info and "error" not in user_info
@ -165,6 +172,14 @@ def verify_token(
) )
def set_new_access_token_in_cookie(response: flask.wrappers.Response) -> flask.wrappers.Response:
print(f"response: {response.__class__}")
tld = current_app.config['THREAD_LOCAL_DATA']
if hasattr(tld, "new_access_token") and tld.new_access_token:
response.set_cookie('access_token', tld.new_access_token)
return response
def validate_scope(token: Any) -> bool: def validate_scope(token: Any) -> bool:
"""Validate_scope.""" """Validate_scope."""
print("validate_scope") print("validate_scope")
@ -220,7 +235,6 @@ def parse_id_token(token: str) -> Any:
decoded = base64.b64decode(padded) decoded = base64.b64decode(padded)
return json.loads(decoded) return json.loads(decoded)
def login_return(code: str, state: str, session_state: str) -> Optional[Response]: def login_return(code: str, state: str, session_state: str) -> Optional[Response]:
"""Login_return.""" """Login_return."""
state_dict = ast.literal_eval(base64.b64decode(state).decode("utf-8")) state_dict = ast.literal_eval(base64.b64decode(state).decode("utf-8"))
@ -231,7 +245,7 @@ def login_return(code: str, state: str, session_state: str) -> Optional[Response
user_info = parse_id_token(id_token) user_info = parse_id_token(id_token)
if AuthenticationService.validate_id_token(id_token): if AuthenticationService.validate_id_or_access_token(id_token):
if user_info and "error" not in user_info: if user_info and "error" not in user_info:
user_model = AuthorizationService.create_user_from_sign_in(user_info) user_model = AuthorizationService.create_user_from_sign_in(user_info)
g.user = user_model.id g.user = user_model.id
@ -244,6 +258,8 @@ def login_return(code: str, state: str, session_state: str) -> Optional[Response
+ f"access_token={auth_token_object['access_token']}&" + f"access_token={auth_token_object['access_token']}&"
+ f"id_token={id_token}" + f"id_token={id_token}"
) )
tld = current_app.config['THREAD_LOCAL_DATA']
tld.new_access_token = auth_token_object['access_token']
return redirect(redirect_url) return redirect(redirect_url)
raise ApiError( raise ApiError(

View File

@ -20,6 +20,15 @@ class MissingAccessTokenError(Exception):
"""MissingAccessTokenError.""" """MissingAccessTokenError."""
# These could be either 'id' OR 'access' tokens and we can't always know which
class TokenExpiredError(Exception):
pass
class TokenInvalidError(Exception):
pass
class AuthenticationProviderTypes(enum.Enum): class AuthenticationProviderTypes(enum.Enum):
"""AuthenticationServiceProviders.""" """AuthenticationServiceProviders."""
@ -128,21 +137,15 @@ class AuthenticationService:
return auth_token_object return auth_token_object
@classmethod @classmethod
def validate_id_token(cls, id_token: str) -> bool: def validate_id_or_access_token(cls, token: str) -> bool:
"""Https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation.""" """Https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation."""
valid = True valid = True
now = time.time() now = time.time()
try: try:
decoded_token = jwt.decode(id_token, options={"verify_signature": False}) decoded_token = jwt.decode(token, options={"verify_signature": False})
except Exception as e: except Exception as e:
raise ApiError( raise TokenInvalidError('Cannot decode token') from e
error_code="bad_id_token",
message="Cannot decode id_token",
status_code=401,
) from e
print(f"decoded_token: {decoded_token}")
print(f"cls.service_url(): {cls.server_url()}")
# import pdb; pdb.set_trace()
if decoded_token["iss"] != cls.server_url(): if decoded_token["iss"] != cls.server_url():
valid = False valid = False
elif ( elif (
@ -159,15 +162,10 @@ class AuthenticationService:
valid = False valid = False
if not valid: if not valid:
current_app.logger.error(f"Invalid token in validate_id_token: {id_token}")
return False return False
if now > decoded_token["exp"]: if now > decoded_token["exp"]:
raise ApiError( raise TokenExpiredError("Your token is expired. Please Login")
error_code="invalid_token",
message="Your token is expired. Please Login",
status_code=401,
)
return True return True

View File

@ -39,6 +39,7 @@
"bpmn-js": "^9.3.2", "bpmn-js": "^9.3.2",
"bpmn-js-properties-panel": "^1.10.0", "bpmn-js-properties-panel": "^1.10.0",
"bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#main", "bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#main",
"cookie": "^0.5.0",
"craco": "^0.0.3", "craco": "^0.0.3",
"date-fns": "^2.28.0", "date-fns": "^2.28.0",
"diagram-js": "^8.5.0", "diagram-js": "^8.5.0",
@ -66,6 +67,7 @@
}, },
"devDependencies": { "devDependencies": {
"@cypress/grep": "^3.1.0", "@cypress/grep": "^3.1.0",
"@types/cookie": "^0.5.1",
"@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.6", "@typescript-eslint/parser": "^5.30.6",
"cypress": "^12", "cypress": "^12",
@ -5654,6 +5656,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/cookie": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz",
"integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==",
"dev": true
},
"node_modules/@types/debug": { "node_modules/@types/debug": {
"version": "4.1.7", "version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
@ -35330,6 +35338,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/cookie": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz",
"integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==",
"dev": true
},
"@types/debug": { "@types/debug": {
"version": "4.1.7", "version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",

View File

@ -34,6 +34,7 @@
"bpmn-js": "^9.3.2", "bpmn-js": "^9.3.2",
"bpmn-js-properties-panel": "^1.10.0", "bpmn-js-properties-panel": "^1.10.0",
"bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#main", "bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#main",
"cookie": "^0.5.0",
"craco": "^0.0.3", "craco": "^0.0.3",
"date-fns": "^2.28.0", "date-fns": "^2.28.0",
"diagram-js": "^8.5.0", "diagram-js": "^8.5.0",
@ -102,6 +103,7 @@
}, },
"devDependencies": { "devDependencies": {
"@cypress/grep": "^3.1.0", "@cypress/grep": "^3.1.0",
"@types/cookie": "^0.5.1",
"@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.6", "@typescript-eslint/parser": "^5.30.6",
"cypress": "^12", "cypress": "^12",

View File

@ -20,7 +20,7 @@ const doRender = () => {
); );
}; };
UserService.getAuthTokenFromParams(); UserService.loginIfNeeded();
doRender(); doRender();
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function

View File

@ -42,7 +42,7 @@ export default function AuthenticationList() {
row.id row.id
}?redirect_url=${redirectUrl}/${ }?redirect_url=${redirectUrl}/${
row.id row.id
}?token=${UserService.getAuthToken()}`} }?token=${UserService.getAccessToken()}`}
> >
{row.id} {row.id}
</a> </a>

View File

@ -72,7 +72,6 @@ export default function ProcessGroupList() {
console.log('document.cookie', document.cookie); console.log('document.cookie', document.cookie);
return ( return (
<> <>
{document.cookie}
<ProcessBreadcrumb hotCrumbs={[['Process Groups']]} /> <ProcessBreadcrumb hotCrumbs={[['Process Groups']]} />
<Can I="POST" a={targetUris.processGroupListPath} ability={ability}> <Can I="POST" a={targetUris.processGroupListPath} ability={ability}>
<Button kind="secondary" href="/admin/process-groups/new"> <Button kind="secondary" href="/admin/process-groups/new">

View File

@ -11,7 +11,7 @@ const HttpMethods = {
const getBasicHeaders = (): object => { const getBasicHeaders = (): object => {
if (UserService.isLoggedIn()) { if (UserService.isLoggedIn()) {
return { return {
Authorization: `Bearer ${UserService.getAuthToken()}`, Authorization: `Bearer ${UserService.getAccessToken()}`,
}; };
} }
return {}; return {};

View File

@ -1,4 +1,5 @@
import jwt from 'jwt-decode'; import jwt from 'jwt-decode';
import cookie from 'cookie';
import { BACKEND_BASE_URL } from '../config'; import { BACKEND_BASE_URL } from '../config';
// NOTE: this currently stores the jwt token in local storage // NOTE: this currently stores the jwt token in local storage
@ -10,6 +11,14 @@ import { BACKEND_BASE_URL } from '../config';
// Some explanation: // Some explanation:
// https://dev.to/nilanth/how-to-secure-jwt-in-a-single-page-application-cko // https://dev.to/nilanth/how-to-secure-jwt-in-a-single-page-application-cko
const getCookie = (key: string) => {
const parsedCookies = cookie.parse(document.cookie);
if (key in parsedCookies) {
return parsedCookies[key];
}
return null;
};
// const getCurrentLocation = (queryParams: string = window.location.search) => { // const getCurrentLocation = (queryParams: string = window.location.search) => {
const getCurrentLocation = () => { const getCurrentLocation = () => {
const queryParamString = ''; const queryParamString = '';
@ -24,24 +33,25 @@ const doLogin = () => {
console.log('URL', url); console.log('URL', url);
window.location.href = url; window.location.href = url;
}; };
// Use access_token for now since it seems to work but if we need the
// id token then set that in a cookie in backend as well
const getIdToken = () => { const getIdToken = () => {
return localStorage.getItem('jwtIdToken'); return getCookie('access_token');
}; };
const doLogout = () => { const doLogout = () => {
const idToken = getIdToken(); const idToken = getIdToken();
localStorage.removeItem('jwtAccessToken');
localStorage.removeItem('jwtIdToken');
const redirectUrl = `${window.location.origin}`; const redirectUrl = `${window.location.origin}`;
const url = `${BACKEND_BASE_URL}/logout?redirect_url=${redirectUrl}&id_token=${idToken}`; const url = `${BACKEND_BASE_URL}/logout?redirect_url=${redirectUrl}&id_token=${idToken}`;
window.location.href = url; window.location.href = url;
}; };
const getAuthToken = () => { const getAccessToken = () => {
return localStorage.getItem('jwtAccessToken'); return getCookie('access_token');
}; };
const isLoggedIn = () => { const isLoggedIn = () => {
return !!getAuthToken(); return !!getAccessToken();
}; };
const getUserEmail = () => { const getUserEmail = () => {
@ -62,25 +72,8 @@ const getPreferredUsername = () => {
return null; return null;
}; };
// FIXME: we could probably change this search to a hook const loginIfNeeded = () => {
// and then could use useSearchParams here instead if (!isLoggedIn()) {
const getAuthTokenFromParams = () => {
const queryParams = new URLSearchParams(window.location.search);
const accessToken = queryParams.get('access_token');
const idToken = queryParams.get('id_token');
queryParams.delete('access_token');
queryParams.delete('id_token');
if (accessToken) {
localStorage.setItem('jwtAccessToken', accessToken);
if (idToken) {
localStorage.setItem('jwtIdToken', idToken);
}
// window.location.href = `${getCurrentLocation(queryParams.toString())}`;
console.log('THE PALCE: ', `${getCurrentLocation()}`);
window.location.href = `${getCurrentLocation()}`;
} else if (!isLoggedIn()) {
doLogin(); doLogin();
} }
}; };
@ -93,8 +86,8 @@ const UserService = {
doLogin, doLogin,
doLogout, doLogout,
isLoggedIn, isLoggedIn,
getAuthToken, getAccessToken,
getAuthTokenFromParams, loginIfNeeded,
getPreferredUsername, getPreferredUsername,
getUserEmail, getUserEmail,
hasRole, hasRole,