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 (
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.services.authorization_service import AuthorizationService
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(AuthorizationService.check_for_permission)
app.after_request(set_new_access_token_in_cookie)
return app # type: ignore

View File

@ -29,8 +29,8 @@ CONNECTOR_PROXY_URL = environ.get(
# Open ID server
OPEN_ID_SERVER_URL = environ.get(
# "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:7002/realms/spiffworkflow"
# "OPEN_ID_SERVER_URL", default="http://localhost:7000/openid"
)
# 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.
"""
host_url = request.host_url.strip("/")
print(f"host_url: {host_url}")
print(f"request.path: {request.url}")
return {
"issuer": f"{host_url}/openid",
"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_bpmn.api.api_error import ApiError
from werkzeug.wrappers import Response
# from flask.wrappers import Response
import flask
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 (
MissingAccessTokenError,
)
@ -57,6 +59,11 @@ def verify_token(
if not token and "Authorization" in request.headers:
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:
user_model = None
decoded_token = get_decoded_token(token)
@ -73,13 +80,11 @@ def verify_token(
f" internal token. {e}"
)
elif "iss" in decoded_token.keys():
user_info = None
try:
if AuthenticationService.validate_id_token(token):
if AuthenticationService.validate_id_or_access_token(token):
user_info = decoded_token
except (
ApiError
) as ae: # API Error is only thrown in the token is outdated.
print("HEY WE IN ERROR")
except (TokenExpiredError) as token_expired_error:
# Try to refresh the token
user = UserService.get_user_by_service_and_service_id(
decoded_token["iss"], decoded_token["sub"]
@ -92,26 +97,28 @@ def verify_token(
refresh_token
)
)
# set_access_cookies()
print(f"auth_token: {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
# 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}
else:
raise ae
else:
raise ae
else:
raise ae
if user_info is None:
raise ApiError(
error_code="invalid_token",
message="Your token is expired. Please Login",
status_code=401,
) from token_expired_error
except Exception as e:
current_app.logger.error(f"Exception raised in get_token: {e}")
raise ApiError(
error_code="fail_get_user_info",
message="Cannot get user info from token",
status_code=401,
) from e
print(f"USER_INFO: {user_info}")
if (
user_info is not None
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:
"""Validate_scope."""
print("validate_scope")
@ -220,7 +235,6 @@ def parse_id_token(token: str) -> Any:
decoded = base64.b64decode(padded)
return json.loads(decoded)
def login_return(code: str, state: str, session_state: str) -> Optional[Response]:
"""Login_return."""
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)
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:
user_model = AuthorizationService.create_user_from_sign_in(user_info)
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"id_token={id_token}"
)
tld = current_app.config['THREAD_LOCAL_DATA']
tld.new_access_token = auth_token_object['access_token']
return redirect(redirect_url)
raise ApiError(

View File

@ -20,6 +20,15 @@ class MissingAccessTokenError(Exception):
"""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):
"""AuthenticationServiceProviders."""
@ -128,21 +137,15 @@ class AuthenticationService:
return auth_token_object
@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."""
valid = True
now = time.time()
try:
decoded_token = jwt.decode(id_token, options={"verify_signature": False})
decoded_token = jwt.decode(token, options={"verify_signature": False})
except Exception as e:
raise ApiError(
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()
raise TokenInvalidError('Cannot decode token') from e
if decoded_token["iss"] != cls.server_url():
valid = False
elif (
@ -159,15 +162,10 @@ class AuthenticationService:
valid = False
if not valid:
current_app.logger.error(f"Invalid token in validate_id_token: {id_token}")
return False
if now > decoded_token["exp"]:
raise ApiError(
error_code="invalid_token",
message="Your token is expired. Please Login",
status_code=401,
)
raise TokenExpiredError("Your token is expired. Please Login")
return True

View File

@ -39,6 +39,7 @@
"bpmn-js": "^9.3.2",
"bpmn-js-properties-panel": "^1.10.0",
"bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#main",
"cookie": "^0.5.0",
"craco": "^0.0.3",
"date-fns": "^2.28.0",
"diagram-js": "^8.5.0",
@ -66,6 +67,7 @@
},
"devDependencies": {
"@cypress/grep": "^3.1.0",
"@types/cookie": "^0.5.1",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.6",
"cypress": "^12",
@ -5654,6 +5656,12 @@
"@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": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
@ -35330,6 +35338,12 @@
"@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": {
"version": "4.1.7",
"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-properties-panel": "^1.10.0",
"bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#main",
"cookie": "^0.5.0",
"craco": "^0.0.3",
"date-fns": "^2.28.0",
"diagram-js": "^8.5.0",
@ -102,6 +103,7 @@
},
"devDependencies": {
"@cypress/grep": "^3.1.0",
"@types/cookie": "^0.5.1",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.6",
"cypress": "^12",

View File

@ -20,7 +20,7 @@ const doRender = () => {
);
};
UserService.getAuthTokenFromParams();
UserService.loginIfNeeded();
doRender();
// 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
}?redirect_url=${redirectUrl}/${
row.id
}?token=${UserService.getAuthToken()}`}
}?token=${UserService.getAccessToken()}`}
>
{row.id}
</a>

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import jwt from 'jwt-decode';
import cookie from 'cookie';
import { BACKEND_BASE_URL } from '../config';
// NOTE: this currently stores the jwt token in local storage
@ -10,6 +11,14 @@ import { BACKEND_BASE_URL } from '../config';
// Some explanation:
// 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 = () => {
const queryParamString = '';
@ -24,24 +33,25 @@ const doLogin = () => {
console.log('URL', 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 = () => {
return localStorage.getItem('jwtIdToken');
return getCookie('access_token');
};
const doLogout = () => {
const idToken = getIdToken();
localStorage.removeItem('jwtAccessToken');
localStorage.removeItem('jwtIdToken');
const redirectUrl = `${window.location.origin}`;
const url = `${BACKEND_BASE_URL}/logout?redirect_url=${redirectUrl}&id_token=${idToken}`;
window.location.href = url;
};
const getAuthToken = () => {
return localStorage.getItem('jwtAccessToken');
const getAccessToken = () => {
return getCookie('access_token');
};
const isLoggedIn = () => {
return !!getAuthToken();
return !!getAccessToken();
};
const getUserEmail = () => {
@ -62,25 +72,8 @@ const getPreferredUsername = () => {
return null;
};
// FIXME: we could probably change this search to a hook
// and then could use useSearchParams here instead
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()) {
const loginIfNeeded = () => {
if (!isLoggedIn()) {
doLogin();
}
};
@ -93,8 +86,8 @@ const UserService = {
doLogin,
doLogout,
isLoggedIn,
getAuthToken,
getAuthTokenFromParams,
getAccessToken,
loginIfNeeded,
getPreferredUsername,
getUserEmail,
hasRole,