use the cookie from the frontend w/ burnettk
This commit is contained in:
parent
8ab5ad7074
commit
2630dbfb45
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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')}",
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -20,7 +20,7 @@ const doRender = () => {
|
|||
);
|
||||
};
|
||||
|
||||
UserService.getAuthTokenFromParams();
|
||||
UserService.loginIfNeeded();
|
||||
doRender();
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
|
|
@ -42,7 +42,7 @@ export default function AuthenticationList() {
|
|||
row.id
|
||||
}?redirect_url=${redirectUrl}/${
|
||||
row.id
|
||||
}?token=${UserService.getAuthToken()}`}
|
||||
}?token=${UserService.getAccessToken()}`}
|
||||
>
|
||||
{row.id}
|
||||
</a>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -11,7 +11,7 @@ const HttpMethods = {
|
|||
const getBasicHeaders = (): object => {
|
||||
if (UserService.isLoggedIn()) {
|
||||
return {
|
||||
Authorization: `Bearer ${UserService.getAuthToken()}`,
|
||||
Authorization: `Bearer ${UserService.getAccessToken()}`,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue