Feature/typeahead allow guest user (#897)
* decode tokens with jwt instead of with base64 w/ burnettk * try to verify jwt token with keycloak when we decode it w/ burnettk * make the token algorithm a constant w/ burnettk * WIP: create more valid looking jwt from spiff w/ burnettk * tests are passsing now w/ burnettk * some pyl stuff w/ burnettk * fixed mypy issues w/ burnettk * fixed issues from mypy fixes w/ burnettk * do not load openid blueprint if not using those configs w/ burnettk * used the process instance to determine if guest user can use connector api w/ burnettk * only check the db for process instance if the api call is for typeahead * removed unused test code * pyl --------- Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
parent
2d9fb4430e
commit
80bc2f2e42
17
bin/run_pyl
17
bin/run_pyl
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
function error_handler() {
|
||||
>&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
|
||||
echo >&2 "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
|
||||
exit "$2"
|
||||
}
|
||||
trap 'error_handler ${LINENO} $?' ERR
|
||||
|
@ -31,23 +31,16 @@ function get_python_dirs() {
|
|||
}
|
||||
|
||||
function run_autofixers() {
|
||||
# checking command -v ruff is not good enough, since the asdf shim may be installed, which will make command -v succeed,
|
||||
# but ruff may not have been pip installed inside the correct version of python.
|
||||
if ! ruff --help >/dev/null 2>&1; then
|
||||
pip install ruff
|
||||
asdf reshim python
|
||||
fi
|
||||
|
||||
python_dirs="$(get_python_dirs) bin"
|
||||
# shellcheck disable=2086
|
||||
ruff --fix $python_dirs || echo ''
|
||||
poetry run ruff --fix $python_dirs || echo ''
|
||||
}
|
||||
|
||||
function run_pre_commmit() {
|
||||
poetry run pre-commit run --verbose --all-files
|
||||
}
|
||||
|
||||
for react_project in "${react_projects[@]}" ; do
|
||||
for react_project in "${react_projects[@]}"; do
|
||||
# if pre, only do stuff when there are changes
|
||||
if [[ -n "$(git status --porcelain "$react_project")" ]]; then
|
||||
pushd "$react_project"
|
||||
|
@ -56,7 +49,7 @@ for react_project in "${react_projects[@]}" ; do
|
|||
fi
|
||||
done
|
||||
|
||||
for python_project in "${python_projects[@]}" ; do
|
||||
for python_project in "${python_projects[@]}"; do
|
||||
# if pre, only do stuff when there are changes
|
||||
if [[ "$subcommand" != "pre" ]] || [[ -n "$(git status --porcelain "$python_project")" ]]; then
|
||||
pushd "$python_project"
|
||||
|
@ -73,7 +66,7 @@ fi
|
|||
function clear_log_file() {
|
||||
unit_testing_log_file="./log/unit_testing.log"
|
||||
if [[ -f "$unit_testing_log_file" ]]; then
|
||||
> "$unit_testing_log_file"
|
||||
>"$unit_testing_log_file"
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
function error_handler() {
|
||||
>&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
|
||||
echo >&2 "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
|
||||
exit "$2"
|
||||
}
|
||||
trap 'error_handler ${LINENO} $?' ERR
|
||||
|
@ -19,7 +19,6 @@ if [[ -d "$process_model_dir" ]]; then
|
|||
shift
|
||||
fi
|
||||
|
||||
|
||||
if [[ -z "${SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR:-}" ]]; then
|
||||
if [[ -n "${process_model_dir}" ]] && [[ -d "${process_model_dir}" ]]; then
|
||||
SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR="$process_model_dir"
|
||||
|
@ -33,8 +32,8 @@ if [[ "$process_model_dir" == "acceptance" ]]; then
|
|||
export SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA=true
|
||||
export SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME=acceptance_tests.yml
|
||||
elif [[ "$process_model_dir" == "localopenid" ]]; then
|
||||
export SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS__0__identifier="openid"
|
||||
export SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS__0__label="openid"
|
||||
export SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS__0__identifier="default"
|
||||
export SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS__0__label="inernal openid"
|
||||
export SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS__0__uri="http://localhost:$port/openid"
|
||||
export SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS__0__client_id="spiffworkflow-backend"
|
||||
export SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS__0__client_secret="JXeQExm0JhQPLumgHtIIqf52bDalHz0q"
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
function error_handler() {
|
||||
>&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
|
||||
echo >&2 "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
|
||||
exit "$2"
|
||||
}
|
||||
trap 'error_handler ${LINENO} $?' ERR
|
||||
set -o errtrace -o errexit -o nounset -o pipefail
|
||||
|
||||
script_dir="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
|
||||
script_dir="$(
|
||||
cd -- "$(dirname "$0")" >/dev/null 2>&1
|
||||
pwd -P
|
||||
)"
|
||||
. "${script_dir}/local_development_environment_setup"
|
||||
|
||||
server_type="${1:-api}"
|
||||
|
@ -24,7 +27,7 @@ else
|
|||
SPIFFWORKFLOW_BACKEND_RUN_BACKGROUND_SCHEDULER_IN_CREATE_APP=false SPIFFWORKFLOW_BACKEND_FAIL_ON_INVALID_PROCESS_MODELS=false poetry run python bin/save_all_bpmn.py
|
||||
fi
|
||||
|
||||
# this line blocks
|
||||
poetry run flask run -p "$port" --host=0.0.0.0
|
||||
# this line blocks
|
||||
poetry run flask run -p "$port" --host=0.0.0.0
|
||||
fi
|
||||
fi
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
function error_handler() {
|
||||
>&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
|
||||
echo >&2 "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
|
||||
exit "$2"
|
||||
}
|
||||
trap 'error_handler ${LINENO} $?' ERR
|
||||
set -o errtrace -o errexit -o nounset -o pipefail
|
||||
|
||||
if [[ ! -f ./src/instance/db_unit_testing_gw0.sqlite3 ]] ; then
|
||||
>&2 echo -e "ERROR: please run the following command first in order to set up and migrate the sqlite unit_testing database:\n\n\tSPIFFWORKFLOW_BACKEND_DATABASE_TYPE=sqlite ./bin/recreate_db clean"
|
||||
if [[ ! -f ./src/instance/db_unit_testing_gw0.sqlite3 ]]; then
|
||||
echo >&2 -e "ERROR: please run the following command first in order to set up and migrate the sqlite unit_testing database:\n\n\tSPIFFWORKFLOW_BACKEND_DATABASE_TYPE=sqlite ./bin/recreate_db clean"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check if python package pytest-xdist is installed
|
||||
if ! python -c "import xdist" &>/dev/null; then
|
||||
>&2 echo -e "ERROR: please install the python package pytest-xdist by running poetry install"
|
||||
if ! poetry run python -c "import xdist" &>/dev/null; then
|
||||
echo >&2 -e "ERROR: please install the python package pytest-xdist by running poetry install"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
|
@ -23,12 +23,16 @@ if os.environ.get("RUN_TYPEGUARD") == "true":
|
|||
from spiffworkflow_backend import create_app # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app() -> Flask: # noqa
|
||||
def _set_unit_testing_env_variables() -> None:
|
||||
os.environ["SPIFFWORKFLOW_BACKEND_ENV"] = "unit_testing"
|
||||
os.environ["FLASK_SESSION_SECRET_KEY"] = (
|
||||
"e7711a3ba96c46c68e084a86952de16f" # noqa: S105, do not care about security when running unit tests
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app() -> Flask: # noqa
|
||||
_set_unit_testing_env_variables()
|
||||
app = create_app()
|
||||
|
||||
# to screw with this, poet add nplusone --group dev
|
||||
|
|
|
@ -19,10 +19,9 @@ from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX
|
|||
from spiffworkflow_backend.models.db import db
|
||||
from spiffworkflow_backend.models.db import migrate
|
||||
from spiffworkflow_backend.routes.authentication_controller import _set_new_access_token_in_cookie
|
||||
from spiffworkflow_backend.routes.authentication_controller import verify_token
|
||||
from spiffworkflow_backend.routes.authentication_controller import omni_auth
|
||||
from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import openid_blueprint
|
||||
from spiffworkflow_backend.routes.user_blueprint import user_blueprint
|
||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||
from spiffworkflow_backend.services.monitoring_service import configure_sentry
|
||||
from spiffworkflow_backend.services.monitoring_service import setup_prometheus_metrics
|
||||
|
||||
|
@ -72,7 +71,11 @@ def create_app() -> flask.app.Flask:
|
|||
|
||||
app.register_blueprint(user_blueprint)
|
||||
app.register_blueprint(api_error_blueprint)
|
||||
app.register_blueprint(openid_blueprint, url_prefix="/openid")
|
||||
|
||||
# only register the backend openid server if the backend is configured to use it
|
||||
backend_auths = app.config["SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS"]
|
||||
if len(backend_auths) == 1 and backend_auths[0]["uri"] == f"{app.config['SPIFFWORKFLOW_BACKEND_URL']}/openid":
|
||||
app.register_blueprint(openid_blueprint, url_prefix="/openid")
|
||||
|
||||
# preflight options requests will be allowed if they meet the requirements of the url regex.
|
||||
# we will add an Access-Control-Max-Age header to the response to tell the browser it doesn't
|
||||
|
@ -89,8 +92,7 @@ def create_app() -> flask.app.Flask:
|
|||
|
||||
configure_sentry(app)
|
||||
|
||||
app.before_request(verify_token)
|
||||
app.before_request(AuthorizationService.check_for_permission)
|
||||
app.before_request(omni_auth)
|
||||
app.after_request(_set_new_access_token_in_cookie)
|
||||
|
||||
# The default is true, but we want to preserve the order of keys in the json
|
||||
|
|
|
@ -714,8 +714,7 @@ paths:
|
|||
minItems: 1
|
||||
get:
|
||||
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_caller_list
|
||||
summary:
|
||||
Return a list of information about all processes that call the provided process id
|
||||
summary: Return a list of information about all processes that call the provided process id
|
||||
tags:
|
||||
- Process Models
|
||||
responses:
|
||||
|
@ -728,7 +727,6 @@ paths:
|
|||
items:
|
||||
$ref: "#/components/schemas/Process"
|
||||
|
||||
|
||||
/processes:
|
||||
get:
|
||||
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_list
|
||||
|
@ -2222,15 +2220,14 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Task"
|
||||
$ref: "#/components/schemas/Task"
|
||||
responses:
|
||||
"200":
|
||||
description: Returns the same data structure provided, but after some replacements.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Task"
|
||||
|
||||
$ref: "#/components/schemas/Task"
|
||||
|
||||
/tasks/{process_instance_id}/instruction:
|
||||
parameters:
|
||||
|
@ -2827,16 +2824,8 @@ paths:
|
|||
"200":
|
||||
description: A list of the data stored in the requested data store.
|
||||
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
jwt:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
x-bearerInfoFunc: spiffworkflow_backend.routes.authentication_controller.verify_token
|
||||
x-scopeValidateFunc: spiffworkflow_backend.routes.authentication_controller.validate_scope
|
||||
|
||||
oAuth2AuthCode:
|
||||
type: oauth2
|
||||
description: authenticate with openid server
|
||||
|
|
|
@ -181,6 +181,7 @@ def setup_config(app: Flask) -> None:
|
|||
|
||||
env_config_prefix = "spiffworkflow_backend.config."
|
||||
env_config_module = env_config_prefix + app.config["ENV_IDENTIFIER"]
|
||||
|
||||
load_config_file(app, env_config_module)
|
||||
|
||||
# This allows config/testing.py or instance/config.py to override the default config
|
||||
|
@ -226,7 +227,7 @@ def setup_config(app: Flask) -> None:
|
|||
app.config["SPIFFWORKFLOW_BACKEND_MAX_INSTANCE_LOCK_DURATION_IN_SECONDS"]
|
||||
)
|
||||
|
||||
if "SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS" not in app.config:
|
||||
if app.config.get("SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS") is None:
|
||||
app.config["SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS"] = [
|
||||
{
|
||||
"identifier": "default",
|
||||
|
|
|
@ -127,6 +127,7 @@ else:
|
|||
}
|
||||
]
|
||||
|
||||
|
||||
### logs
|
||||
# loggers to use is a comma separated list of logger prefixes that we will be converted to list of strings
|
||||
config_from_env("SPIFFWORKFLOW_BACKEND_LOGGERS_TO_USE")
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# These configs are specifically for the open id server run from backend.
|
||||
# This should only be used for development and demonstration. SHOULD NOT BE USED IN PROD.
|
||||
class OpenIdConfigsForDevOnly:
|
||||
private_key = """-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCB34SwG3IFKAzf
|
||||
BGm2VCWrqhlhjxXgQjx42Wsp4Nh8UqhSCnBE+/3sMwSxgwmuwDANiV9pkifWDbAy
|
||||
jLcc5i9YrrwqagyeMk1xjwVmCSB5RPgwJMP3NOyiciM8V5z8qQXE8+Xv9jOoDExC
|
||||
DL5M/vDsGg4Xe02kdJleUuxmQI2r9PlLklY9p7NUvcFbh1qVEuMQ7ToYqMyuiNop
|
||||
1cIhRdKzV7BcMhE8Bby+bmmhfRDNhq4zM2W4OHithsdREoeurzTebhUR/RwA9j4a
|
||||
w6Niy/awBOomgMuqZ+uQXK5R9B2P21jSt+ljMTxcUzoYhJ/yIv/NvgQAvqs0Umqx
|
||||
4gT7erRPAgMBAAECggEACGj4Hf7w/I3lSeEILW/rVPks/C9rv1V0UAkYRbwTMl3L
|
||||
gfokeG5VbG25vWFnoTYKoCqUqXDDydQIfgWBuBcQQTuZ4MJ0TB9xLcwREYeNLEn/
|
||||
xMvxfexV8VQ90HyfisPK2IziVEGXtidxt4pwULeAbdgA484uyKcD5Xx6/xYc3OIA
|
||||
T+bfwfydJB5e09e6hZ8glTD50LX7b6Hy5b/iYsYCVzfXh8+DM1UQQQEWA++ZsYo0
|
||||
qUBJMoB7L5AbMZaWWmTJilEi4MaBnjPw8JEoW9VH7ZjZiVCPlAV9DxSSeBuwfqUy
|
||||
KZpbYcJEZNELiTgVcyaqjR3mt1Q4iCYuIkMUJ9/ZdQKBgQC1PPBQYeOcgAzx9o5N
|
||||
gNYy/19xqL9lhoFle8Y9a28JbX2XpLtTzwj6WbvW4hb2AlBLLc/BAtr2TRn4b7qU
|
||||
0MhAclvvmh6Zccwfp1tCNn3t6JpdTqIbG8eYgHDnBGK27xvhsyX22rmBcBpJWfMK
|
||||
T7MpB9y7/r0iU84wAYvOcAS1awKBgQC3cloRPloKqXUQh3k8uWnZucm6/gGPHNir
|
||||
FyFfHT45rlewlfuLPcN13vezb0vmKRjzHVDqo+1xrUmjFj857uLpQZ4lz7X5Xl9k
|
||||
hSw0+1heqRmKumQIF12rg5elBfTSvys2N5CiH4EykO3P97jz0zTFij+orYdtbrA8
|
||||
LqfyuMARrQKBgQCAlvse3UVkTcphhwESZl4UEvMCLquV+igm+/n8rBQ9SS06AcxT
|
||||
u2pwTmijHwkRhTS5Eoj8Ne1roerSRVvJqJTcfQdT6jLZxk8BCnoKcaVJvqZ/m4IS
|
||||
39PvFPqGPqtXhjFvIu/FxQynlQVhk+uIHmJMs2JfFG/XQkTh9MbgMsR0fwKBgQCS
|
||||
67rG5LEoqN9hBZ9LyxPDlNDEOnr1K508KaJIkxsrBz6j5vs3YZgR5ylrRE/9Xhzl
|
||||
WS1dPz0ENk0rmL26oGCLgEow7lJIDhVIZIArTsJPzg7u1KkY8d3LZ/Ej8clKoGDz
|
||||
Yz0rGyBWZ0yPq08tuJIjQ74IUjKMqoHrMVLBSsZJYQJ/U6LpkmH6JrJfC81ql95y
|
||||
iOtxcJaAH/38HVvhXsCn4/uQUu/CyxH6BLleh7ImdnmO5BMInZwkFHd9tS1AdjiA
|
||||
M8V/du4022V11ivFv8mwyKbBjsODcwhfOarCm1phOi95ksFA8KJdQPYsEw/Gba0K
|
||||
CQKKq+mlG5zUY1rU1uXxjA==
|
||||
-----END PRIVATE KEY-----"""
|
||||
|
||||
public_key = """-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgd+EsBtyBSgM3wRptlQl
|
||||
q6oZYY8V4EI8eNlrKeDYfFKoUgpwRPv97DMEsYMJrsAwDYlfaZIn1g2wMoy3HOYv
|
||||
WK68KmoMnjJNcY8FZgkgeUT4MCTD9zTsonIjPFec/KkFxPPl7/YzqAxMQgy+TP7w
|
||||
7BoOF3tNpHSZXlLsZkCNq/T5S5JWPaezVL3BW4dalRLjEO06GKjMrojaKdXCIUXS
|
||||
s1ewXDIRPAW8vm5poX0QzYauMzNluDh4rYbHURKHrq803m4VEf0cAPY+GsOjYsv2
|
||||
sATqJoDLqmfrkFyuUfQdj9tY0rfpYzE8XFM6GISf8iL/zb4EAL6rNFJqseIE+3q0
|
||||
TwIDAQAB
|
||||
-----END PUBLIC KEY-----"""
|
|
@ -9,6 +9,11 @@ SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get(
|
|||
"SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME", default="unit_testing.yml"
|
||||
)
|
||||
|
||||
SPIFFWORKFLOW_BACKEND_OPEN_ID_SERVER_URL = "http://localhost:7000/openid"
|
||||
SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_ID = "spiffworkflow-backend"
|
||||
SPIFFWORKFLOW_BACKEND_OPEN_ID_CLIENT_SECRET_KEY = "JXeQExm0JhQPLumgHtIIqf52bDalHz0q" # noqa: S105
|
||||
SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS = None
|
||||
|
||||
SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get("SPIFFWORKFLOW_BACKEND_LOG_LEVEL", default="debug")
|
||||
SPIFFWORKFLOW_BACKEND_GIT_COMMIT_ON_SAVE = False
|
||||
|
||||
|
|
|
@ -323,5 +323,4 @@ def handle_exception(exception: Exception) -> flask.wrappers.Response:
|
|||
if api_exception.response_headers is not None:
|
||||
for header, value in api_exception.response_headers.items():
|
||||
error_response.headers[header] = value
|
||||
|
||||
return error_response
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
@ -13,6 +15,9 @@ from spiffworkflow_backend.models.group import GroupModel
|
|||
|
||||
SPIFF_NO_AUTH_USER = "spiff_no_auth_guest_user"
|
||||
SPIFF_GUEST_USER = "spiff_guest_user"
|
||||
SPIFF_GENERATED_JWT_KEY_ID = "spiff_backend"
|
||||
SPIFF_GENERATED_JWT_ALGORITHM = "HS256"
|
||||
SPIFF_GENERATED_JWT_AUDIENCE = "spiffworkflow-backend"
|
||||
|
||||
|
||||
class UserNotFoundError(Exception):
|
||||
|
@ -47,6 +52,10 @@ class UserModel(SpiffworkflowBaseDBModel):
|
|||
)
|
||||
principal = relationship("PrincipalModel", uselist=False, cascade="delete") # type: ignore
|
||||
|
||||
@classmethod
|
||||
def spiff_generated_jwt_issuer(cls) -> str:
|
||||
return str(current_app.config["SPIFFWORKFLOW_BACKEND_URL"])
|
||||
|
||||
def encode_auth_token(self, extra_payload: dict | None = None) -> str:
|
||||
"""Generate the Auth Token.
|
||||
|
||||
|
@ -62,16 +71,17 @@ class UserModel(SpiffworkflowBaseDBModel):
|
|||
"email": self.email,
|
||||
"preferred_username": self.username,
|
||||
"sub": f"service:{self.service}::service_id:{self.service_id}",
|
||||
"token_type": "internal",
|
||||
"iss": self.__class__.spiff_generated_jwt_issuer(),
|
||||
"iat": math.floor(time.time()),
|
||||
"exp": round(time.time()) + 3600,
|
||||
"aud": SPIFF_GENERATED_JWT_AUDIENCE,
|
||||
}
|
||||
|
||||
payload = base_payload
|
||||
if extra_payload is not None:
|
||||
payload = {**base_payload, **extra_payload}
|
||||
return jwt.encode(
|
||||
payload,
|
||||
secret_key,
|
||||
algorithm="HS256",
|
||||
payload, secret_key, algorithm=SPIFF_GENERATED_JWT_ALGORITHM, headers={"kid": SPIFF_GENERATED_JWT_KEY_ID}
|
||||
)
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import ast
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import flask
|
||||
import jwt
|
||||
from flask import current_app
|
||||
from flask import g
|
||||
from flask import jsonify
|
||||
|
@ -41,8 +38,14 @@ def authentication_options() -> Response:
|
|||
return make_response(jsonify(response), 200)
|
||||
|
||||
|
||||
# this does both authx and authn
|
||||
def omni_auth() -> None:
|
||||
decoded_token = verify_token()
|
||||
AuthorizationService.check_for_permission(decoded_token)
|
||||
|
||||
|
||||
# authorization_exclusion_list = ['status']
|
||||
def verify_token(token: str | None = None, force_run: bool | None = False) -> None:
|
||||
def verify_token(token: str | None = None, force_run: bool | None = False) -> dict | None:
|
||||
"""Verify the token for the user (if provided).
|
||||
|
||||
If in production environment and token is not provided, gets user from the SSO headers and returns their token.
|
||||
|
@ -67,8 +70,10 @@ def verify_token(token: str | None = None, force_run: bool | None = False) -> No
|
|||
_clear_auth_tokens_from_thread_local_data()
|
||||
|
||||
user_model = None
|
||||
decoded_token = None
|
||||
if token_info["token"] is not None:
|
||||
user_model = _get_user_model_from_token(token_info["token"])
|
||||
decoded_token = _get_decoded_token(token_info["token"])
|
||||
user_model = _get_user_model_from_token(decoded_token)
|
||||
elif token_info["api_key"] is not None:
|
||||
user_model = _get_user_model_from_api_key(token_info["api_key"])
|
||||
|
||||
|
@ -89,7 +94,7 @@ def verify_token(token: str | None = None, force_run: bool | None = False) -> No
|
|||
get_scope(token_info["token"])
|
||||
elif token_info["api_key"]:
|
||||
g.authenticated = True
|
||||
return None
|
||||
return decoded_token
|
||||
|
||||
raise ApiError(error_code="invalid_token", message="Cannot validate token.", status_code=401)
|
||||
|
||||
|
@ -120,7 +125,7 @@ def login(
|
|||
AuthenticationService.create_guest_token(
|
||||
username=SPIFF_GUEST_USER,
|
||||
group_identifier=SPIFF_GUEST_GROUP,
|
||||
auth_token_properties={"only_guest_task_completion": True},
|
||||
auth_token_properties={"only_guest_task_completion": True, "process_instance_id": process_instance_id},
|
||||
authentication_identifier=authentication_identifier,
|
||||
)
|
||||
return redirect(redirect_url)
|
||||
|
@ -139,11 +144,11 @@ def login_return(code: str, state: str, session_state: str = "") -> Response | N
|
|||
auth_token_object = AuthenticationService().get_auth_token_object(code, authentication_identifier=authentication_identifier)
|
||||
if "id_token" in auth_token_object:
|
||||
id_token = auth_token_object["id_token"]
|
||||
user_info = _parse_id_token(id_token)
|
||||
decoded_token = _get_decoded_token(id_token)
|
||||
|
||||
if AuthenticationService.validate_id_or_access_token(id_token, authentication_identifier=authentication_identifier):
|
||||
if user_info and "error" not in user_info:
|
||||
user_model = AuthorizationService.create_user_from_sign_in(user_info)
|
||||
if AuthenticationService.validate_decoded_token(decoded_token, authentication_identifier=authentication_identifier):
|
||||
if decoded_token and "error" not in decoded_token:
|
||||
user_model = AuthorizationService.create_user_from_sign_in(decoded_token)
|
||||
g.user = user_model.id
|
||||
g.token = auth_token_object["id_token"]
|
||||
if "refresh_token" in auth_token_object:
|
||||
|
@ -174,11 +179,11 @@ def login_return(code: str, state: str, session_state: str = "") -> Response | N
|
|||
|
||||
# FIXME: share more code with login_return and maybe attempt to get a refresh token
|
||||
def login_with_access_token(access_token: str, authentication_identifier: str) -> Response:
|
||||
user_info = _parse_id_token(access_token)
|
||||
decoded_token = _get_decoded_token(access_token)
|
||||
|
||||
if AuthenticationService.validate_id_or_access_token(access_token, authentication_identifier=authentication_identifier):
|
||||
if user_info and "error" not in user_info:
|
||||
AuthorizationService.create_user_from_sign_in(user_info)
|
||||
if AuthenticationService.validate_decoded_token(decoded_token, authentication_identifier=authentication_identifier):
|
||||
if decoded_token and "error" not in decoded_token:
|
||||
AuthorizationService.create_user_from_sign_in(decoded_token)
|
||||
else:
|
||||
raise ApiError(
|
||||
error_code="invalid_login",
|
||||
|
@ -224,7 +229,7 @@ def logout_return() -> Response:
|
|||
|
||||
def get_scope(token: str) -> str:
|
||||
scope = ""
|
||||
decoded_token = jwt.decode(token, options={"verify_signature": False})
|
||||
decoded_token = _get_decoded_token(token)
|
||||
if "scope" in decoded_token:
|
||||
scope = decoded_token["scope"]
|
||||
return scope
|
||||
|
@ -280,7 +285,7 @@ def _clear_auth_tokens_from_thread_local_data() -> None:
|
|||
delattr(tld, "user_has_logged_out")
|
||||
|
||||
|
||||
def _force_logout_user_if_necessary(user_model: UserModel | None = None) -> bool:
|
||||
def _force_logout_user_if_necessary(user_model: UserModel | None, decoded_token: dict) -> bool:
|
||||
"""Logs out a guest user if certain criteria gets met.
|
||||
|
||||
* if the user is a no auth guest and we have auth enabled
|
||||
|
@ -294,7 +299,7 @@ def _force_logout_user_if_necessary(user_model: UserModel | None = None) -> bool
|
|||
) or (
|
||||
user_model.username == SPIFF_GUEST_USER
|
||||
and user_model.service_id == "spiff_guest_service_id"
|
||||
and not AuthorizationService.request_allows_guest_access()
|
||||
and not AuthorizationService.request_allows_guest_access(decoded_token)
|
||||
):
|
||||
AuthenticationService.set_user_has_logged_out()
|
||||
return True
|
||||
|
@ -328,88 +333,87 @@ def _get_user_model_from_api_key(api_key: str) -> UserModel | None:
|
|||
return user_model
|
||||
|
||||
|
||||
def _get_user_model_from_token(token: str) -> UserModel | None:
|
||||
def _get_user_model_from_token(decoded_token: dict) -> UserModel | None:
|
||||
user_model = None
|
||||
decoded_token = _get_decoded_token(token)
|
||||
|
||||
if decoded_token is not None:
|
||||
if "token_type" in decoded_token:
|
||||
token_type = decoded_token["token_type"]
|
||||
if token_type == "internal": # noqa: S105
|
||||
if "iss" in decoded_token.keys():
|
||||
if decoded_token["iss"] == UserModel.spiff_generated_jwt_issuer():
|
||||
try:
|
||||
user_model = _get_user_from_decoded_internal_token(decoded_token)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Exception in verify_token getting user from decoded internal token. {e}")
|
||||
|
||||
# if the user is forced logged out then stop processing the token
|
||||
if _force_logout_user_if_necessary(user_model):
|
||||
return None
|
||||
# if the user is forced logged out then stop processing the token
|
||||
if _force_logout_user_if_necessary(user_model, decoded_token):
|
||||
return None
|
||||
else:
|
||||
user_info = None
|
||||
authentication_identifier = _get_authentication_identifier_from_request()
|
||||
try:
|
||||
if AuthenticationService.validate_decoded_token(
|
||||
decoded_token, authentication_identifier=authentication_identifier
|
||||
):
|
||||
user_info = decoded_token
|
||||
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"])
|
||||
if user:
|
||||
refresh_token = AuthenticationService.get_refresh_token(user.id)
|
||||
if refresh_token:
|
||||
auth_token: dict = AuthenticationService.get_auth_token_from_refresh_token(
|
||||
refresh_token, authentication_identifier=authentication_identifier
|
||||
)
|
||||
if auth_token and "error" not in auth_token and "id_token" in auth_token:
|
||||
tld = current_app.config["THREAD_LOCAL_DATA"]
|
||||
tld.new_access_token = auth_token["id_token"]
|
||||
tld.new_id_token = auth_token["id_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,
|
||||
}
|
||||
|
||||
elif "iss" in decoded_token.keys():
|
||||
user_info = None
|
||||
authentication_identifier = _get_authentication_identifier_from_request()
|
||||
try:
|
||||
if AuthenticationService.validate_id_or_access_token(token, authentication_identifier=authentication_identifier):
|
||||
user_info = decoded_token
|
||||
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"])
|
||||
if user:
|
||||
refresh_token = AuthenticationService.get_refresh_token(user.id)
|
||||
if refresh_token:
|
||||
auth_token: dict = AuthenticationService.get_auth_token_from_refresh_token(
|
||||
refresh_token, authentication_identifier=authentication_identifier
|
||||
)
|
||||
if auth_token and "error" not in auth_token and "id_token" in auth_token:
|
||||
tld = current_app.config["THREAD_LOCAL_DATA"]
|
||||
tld.new_access_token = auth_token["id_token"]
|
||||
tld.new_id_token = auth_token["id_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,
|
||||
}
|
||||
if user_info is None:
|
||||
AuthenticationService.set_user_has_logged_out()
|
||||
raise ApiError(
|
||||
error_code="invalid_token",
|
||||
message="Your token is expired. Please Login",
|
||||
status_code=401,
|
||||
) from token_expired_error
|
||||
|
||||
if user_info is None:
|
||||
except Exception as e:
|
||||
AuthenticationService.set_user_has_logged_out()
|
||||
raise ApiError(
|
||||
error_code="invalid_token",
|
||||
message="Your token is expired. Please Login",
|
||||
error_code="fail_get_user_info",
|
||||
message="Cannot get user info from token",
|
||||
status_code=401,
|
||||
) from token_expired_error
|
||||
|
||||
except Exception as e:
|
||||
AuthenticationService.set_user_has_logged_out()
|
||||
raise ApiError(
|
||||
error_code="fail_get_user_info",
|
||||
message="Cannot get user info from token",
|
||||
status_code=401,
|
||||
) from e
|
||||
if user_info is not None and "error" not in user_info and "iss" in user_info: # not sure what to test yet
|
||||
user_model = (
|
||||
UserModel.query.filter(UserModel.service == user_info["iss"])
|
||||
.filter(UserModel.service_id == user_info["sub"])
|
||||
.first()
|
||||
)
|
||||
if user_model is None:
|
||||
) from e
|
||||
if user_info is not None and "error" not in user_info and "iss" in user_info: # not sure what to test yet
|
||||
user_model = (
|
||||
UserModel.query.filter(UserModel.service == user_info["iss"])
|
||||
.filter(UserModel.service_id == user_info["sub"])
|
||||
.first()
|
||||
)
|
||||
if user_model is None:
|
||||
AuthenticationService.set_user_has_logged_out()
|
||||
raise ApiError(
|
||||
error_code="invalid_user",
|
||||
message="Invalid user. Please log in.",
|
||||
status_code=401,
|
||||
)
|
||||
# no user_info
|
||||
else:
|
||||
AuthenticationService.set_user_has_logged_out()
|
||||
raise ApiError(
|
||||
error_code="invalid_user",
|
||||
message="Invalid user. Please log in.",
|
||||
error_code="no_user_info",
|
||||
message="Cannot retrieve user info",
|
||||
status_code=401,
|
||||
)
|
||||
# no user_info
|
||||
else:
|
||||
AuthenticationService.set_user_has_logged_out()
|
||||
raise ApiError(
|
||||
error_code="no_user_info",
|
||||
message="Cannot retrieve user info",
|
||||
status_code=401,
|
||||
)
|
||||
|
||||
else:
|
||||
current_app.logger.debug("token_type not in decode_token in verify_token")
|
||||
current_app.logger.debug("iss not in decode_token in verify_token")
|
||||
AuthenticationService.set_user_has_logged_out()
|
||||
raise ApiError(
|
||||
error_code="invalid_token",
|
||||
|
@ -432,13 +436,13 @@ def _get_user_from_decoded_internal_token(decoded_token: dict) -> UserModel | No
|
|||
return user
|
||||
|
||||
|
||||
def _get_decoded_token(token: str) -> dict | None:
|
||||
def _get_decoded_token(token: str) -> dict:
|
||||
try:
|
||||
decoded_token: dict = jwt.decode(token, options={"verify_signature": False})
|
||||
decoded_token: dict = AuthenticationService.parse_jwt_token(_get_authentication_identifier_from_request(), token)
|
||||
except Exception as e:
|
||||
raise ApiError(error_code="invalid_token", message="Cannot decode token.") from e
|
||||
else:
|
||||
if "token_type" in decoded_token or "iss" in decoded_token:
|
||||
if "iss" in decoded_token:
|
||||
return decoded_token
|
||||
else:
|
||||
current_app.logger.error(f"Unknown token type in get_decoded_token: token: {token}")
|
||||
|
@ -448,20 +452,6 @@ def _get_decoded_token(token: str) -> dict | None:
|
|||
)
|
||||
|
||||
|
||||
def _parse_id_token(token: str) -> Any:
|
||||
"""Parse the id token."""
|
||||
parts = token.split(".")
|
||||
if len(parts) != 3:
|
||||
raise Exception("Incorrect id token format")
|
||||
|
||||
payload = parts[1]
|
||||
padded = payload + "=" * (4 - len(payload) % 4)
|
||||
|
||||
# https://lists.jboss.org/pipermail/keycloak-user/2016-April/005758.html
|
||||
decoded = base64.urlsafe_b64decode(padded)
|
||||
return json.loads(decoded)
|
||||
|
||||
|
||||
def _get_authentication_identifier_from_request() -> str:
|
||||
if "authentication_identifier" in request.cookies:
|
||||
return request.cookies["authentication_identifier"]
|
||||
|
|
|
@ -7,6 +7,7 @@ This is just here to make local development, testing, and demonstration easier.
|
|||
"""
|
||||
import base64
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
|
@ -15,15 +16,21 @@ import jwt
|
|||
import yaml
|
||||
from flask import Blueprint
|
||||
from flask import current_app
|
||||
from flask import jsonify
|
||||
from flask import make_response
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import request
|
||||
from flask import url_for
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
from spiffworkflow_backend.config.openid.rsa_keys import OpenIdConfigsForDevOnly
|
||||
|
||||
openid_blueprint = Blueprint("openid", __name__, template_folder="templates", static_folder="static")
|
||||
|
||||
OPEN_ID_CODE = ":this_is_not_secure_do_not_use_in_production"
|
||||
SPIFF_OPEN_ID_KEY_ID = "spiffworkflow_backend_open_id"
|
||||
SPIFF_OPEN_ID_ALGORITHM = "RS256"
|
||||
|
||||
|
||||
@openid_blueprint.route("/.well-known/openid-configuration", methods=["GET"])
|
||||
|
@ -38,6 +45,7 @@ def well_known() -> dict:
|
|||
"authorization_endpoint": f"{host_url}{url_for('openid.auth')}",
|
||||
"token_endpoint": f"{host_url}{url_for('openid.token')}",
|
||||
"end_session_endpoint": f"{host_url}{url_for('openid.end_session')}",
|
||||
"jwks_uri": f"{host_url}{url_for('openid.jwks')}",
|
||||
}
|
||||
|
||||
|
||||
|
@ -97,22 +105,24 @@ def token() -> Response | dict:
|
|||
authorization = request.headers.get("Authorization", "Basic ")
|
||||
authorization = authorization[6:] # Remove "Basic"
|
||||
authorization = base64.b64decode(authorization).decode("utf-8")
|
||||
client_id, client_secret = authorization.split(":")
|
||||
client_id = authorization.split(":")
|
||||
|
||||
base_url = request.host_url + "openid"
|
||||
private_key = OpenIdConfigsForDevOnly.private_key
|
||||
|
||||
id_token = jwt.encode(
|
||||
{
|
||||
"iss": base_url,
|
||||
"aud": [client_id, "account"],
|
||||
"iat": time.time(),
|
||||
"exp": time.time() + 86400, # Expire after a day.
|
||||
"aud": client_id,
|
||||
"iat": math.floor(time.time()),
|
||||
"exp": round(time.time()) + 3600,
|
||||
"sub": user_name,
|
||||
"email": user_details["email"],
|
||||
"preferred_username": user_details.get("preferred_username", user_name),
|
||||
},
|
||||
client_secret,
|
||||
algorithm="HS256",
|
||||
private_key,
|
||||
algorithm=SPIFF_OPEN_ID_ALGORITHM,
|
||||
headers={"kid": SPIFF_OPEN_ID_KEY_ID},
|
||||
)
|
||||
response = {
|
||||
"access_token": id_token,
|
||||
|
@ -129,6 +139,21 @@ def end_session() -> Response:
|
|||
return redirect(redirect_url)
|
||||
|
||||
|
||||
@openid_blueprint.route("/jwks", methods=["GET"])
|
||||
def jwks() -> Response:
|
||||
public_key = base64.b64encode(OpenIdConfigsForDevOnly.public_key.encode()).decode("utf-8")
|
||||
jwks_configs = {
|
||||
"keys": [
|
||||
{
|
||||
"kid": SPIFF_OPEN_ID_KEY_ID,
|
||||
"kty": "RSA",
|
||||
"x5c": [public_key],
|
||||
}
|
||||
]
|
||||
}
|
||||
return make_response(jsonify(jwks_configs), 200)
|
||||
|
||||
|
||||
@openid_blueprint.route("/refresh", methods=["POST"])
|
||||
def refresh() -> str:
|
||||
return ""
|
||||
|
|
|
@ -6,6 +6,16 @@ import time
|
|||
from hashlib import sha256
|
||||
from hmac import HMAC
|
||||
from hmac import compare_digest
|
||||
from typing import Any
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.x509 import load_der_x509_certificate
|
||||
|
||||
from spiffworkflow_backend.models.user import SPIFF_GENERATED_JWT_ALGORITHM
|
||||
from spiffworkflow_backend.models.user import SPIFF_GENERATED_JWT_AUDIENCE
|
||||
from spiffworkflow_backend.models.user import SPIFF_GENERATED_JWT_KEY_ID
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
from spiffworkflow_backend.routes.openid_blueprint.openid_blueprint import SPIFF_OPEN_ID_KEY_ID
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from typing_extensions import NotRequired
|
||||
|
@ -57,6 +67,7 @@ class AuthenticationOptionNotFoundError(Exception):
|
|||
|
||||
class AuthenticationService:
|
||||
ENDPOINT_CACHE: dict[str, dict[str, str]] = {} # We only need to find the openid endpoints once, then we can cache them.
|
||||
JSON_WEB_KEYSET_CACHE: dict[str, dict[str, str]] = {}
|
||||
|
||||
@classmethod
|
||||
def authentication_options_for_api(cls) -> list[AuthenticationOptionForApi]:
|
||||
|
@ -86,6 +97,10 @@ class AuthenticationService:
|
|||
config: str = cls.authentication_option_for_identifier(authentication_identifier)["client_id"]
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def valid_audiences(cls, authentication_identifier: str) -> list[str]:
|
||||
return [cls.client_id(authentication_identifier)]
|
||||
|
||||
@classmethod
|
||||
def server_url(cls, authentication_identifier: str) -> str:
|
||||
"""Returns the server url from the config."""
|
||||
|
@ -104,6 +119,8 @@ class AuthenticationService:
|
|||
openid_config_url = f"{cls.server_url(authentication_identifier)}/.well-known/openid-configuration"
|
||||
if authentication_identifier not in cls.ENDPOINT_CACHE:
|
||||
cls.ENDPOINT_CACHE[authentication_identifier] = {}
|
||||
if authentication_identifier not in cls.JSON_WEB_KEYSET_CACHE:
|
||||
cls.JSON_WEB_KEYSET_CACHE[authentication_identifier] = {}
|
||||
if name not in AuthenticationService.ENDPOINT_CACHE[authentication_identifier]:
|
||||
try:
|
||||
response = requests.get(openid_config_url, timeout=HTTP_REQUEST_TIMEOUT_SECONDS)
|
||||
|
@ -115,6 +132,63 @@ class AuthenticationService:
|
|||
config: str = AuthenticationService.ENDPOINT_CACHE[authentication_identifier].get(name, "")
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def get_jwks_config_from_uri(cls, jwks_uri: str) -> dict:
|
||||
if jwks_uri not in AuthenticationService.JSON_WEB_KEYSET_CACHE:
|
||||
try:
|
||||
jwt_ks_response = requests.get(jwks_uri, timeout=HTTP_REQUEST_TIMEOUT_SECONDS)
|
||||
AuthenticationService.JSON_WEB_KEYSET_CACHE[jwks_uri] = jwt_ks_response.json()
|
||||
except requests.exceptions.ConnectionError as ce:
|
||||
raise OpenIdConnectionError(f"Cannot connect to given jwks url: {jwks_uri}") from ce
|
||||
return AuthenticationService.JSON_WEB_KEYSET_CACHE[jwks_uri]
|
||||
|
||||
@classmethod
|
||||
def jwks_public_key_for_key_id(cls, authentication_identifier: str, key_id: str) -> dict:
|
||||
jwks_uri = cls.open_id_endpoint_for_name("jwks_uri", authentication_identifier)
|
||||
jwks_configs = cls.get_jwks_config_from_uri(jwks_uri)
|
||||
json_key_configs: dict = next(jk for jk in jwks_configs["keys"] if jk["kid"] == key_id)
|
||||
return json_key_configs
|
||||
|
||||
@classmethod
|
||||
def parse_jwt_token(cls, authentication_identifier: str, token: str) -> dict:
|
||||
header = jwt.get_unverified_header(token)
|
||||
key_id = str(header.get("kid"))
|
||||
|
||||
# if the token has our key id then we issued it and should verify to ensure it's valid
|
||||
if key_id == SPIFF_GENERATED_JWT_KEY_ID:
|
||||
return jwt.decode(
|
||||
token,
|
||||
str(current_app.secret_key),
|
||||
algorithms=[SPIFF_GENERATED_JWT_ALGORITHM],
|
||||
audience=SPIFF_GENERATED_JWT_AUDIENCE,
|
||||
options={"verify_exp": False},
|
||||
)
|
||||
else:
|
||||
json_key_configs = cls.jwks_public_key_for_key_id(authentication_identifier, key_id)
|
||||
x5c = json_key_configs["x5c"][0]
|
||||
algorithm = str(header.get("alg"))
|
||||
decoded_certificate = base64.b64decode(x5c)
|
||||
|
||||
# our backend-based openid provider implementation (which you should never use in prod)
|
||||
# uses a public/private key pair. we played around with adding an x509 cert so we could
|
||||
# follow the exact same mechanism for getting the public key that we use for keycloak,
|
||||
# but using an x509 cert for no reason seemed a little overboard for this toy-openid use case,
|
||||
# when we already have the public key that can work hardcoded in our config.
|
||||
public_key: Any = None
|
||||
if key_id == SPIFF_OPEN_ID_KEY_ID:
|
||||
public_key = decoded_certificate
|
||||
else:
|
||||
x509_cert = load_der_x509_certificate(decoded_certificate, default_backend())
|
||||
public_key = x509_cert.public_key()
|
||||
|
||||
return jwt.decode(
|
||||
token,
|
||||
public_key,
|
||||
algorithms=[algorithm],
|
||||
audience=cls.valid_audiences(authentication_identifier)[0],
|
||||
options={"verify_exp": False},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_backend_url() -> str:
|
||||
return str(current_app.config["SPIFFWORKFLOW_BACKEND_URL"])
|
||||
|
@ -177,7 +251,7 @@ class AuthenticationService:
|
|||
if azp is None:
|
||||
return True
|
||||
|
||||
valid_client_ids = [cls.client_id(authentication_identifier), "account"]
|
||||
valid_client_ids = [cls.client_id(authentication_identifier)]
|
||||
if (
|
||||
"additional_valid_client_ids" in cls.authentication_option_for_identifier(authentication_identifier)
|
||||
and cls.authentication_option_for_identifier(authentication_identifier)["additional_valid_client_ids"] is not None
|
||||
|
@ -190,30 +264,26 @@ class AuthenticationService:
|
|||
return azp in valid_client_ids
|
||||
|
||||
@classmethod
|
||||
def validate_id_or_access_token(cls, token: str, authentication_identifier: str) -> bool:
|
||||
def validate_decoded_token(cls, decoded_token: dict, authentication_identifier: str) -> bool:
|
||||
"""Https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation."""
|
||||
valid = True
|
||||
now = round(time.time())
|
||||
try:
|
||||
decoded_token = jwt.decode(token, options={"verify_signature": False})
|
||||
except Exception as e:
|
||||
raise TokenInvalidError("Cannot decode token") from e
|
||||
|
||||
# give a 5 second leeway to iat in case keycloak server time doesn't match backend server
|
||||
iat_clock_skew_leeway = 5
|
||||
|
||||
iss = decoded_token["iss"]
|
||||
aud = decoded_token["aud"]
|
||||
aud = decoded_token["aud"] if "aud" in decoded_token else None
|
||||
azp = decoded_token["azp"] if "azp" in decoded_token else None
|
||||
iat = decoded_token["iat"]
|
||||
|
||||
valid_audience_values = (cls.client_id(authentication_identifier), "account")
|
||||
valid_audience_values = cls.valid_audiences(authentication_identifier)
|
||||
audience_array_in_token = aud
|
||||
if isinstance(aud, str):
|
||||
audience_array_in_token = [aud]
|
||||
overlapping_aud_values = [x for x in audience_array_in_token if x in valid_audience_values]
|
||||
|
||||
if iss != cls.server_url(authentication_identifier):
|
||||
if iss not in [cls.server_url(authentication_identifier), UserModel.spiff_generated_jwt_issuer()]:
|
||||
current_app.logger.error(
|
||||
f"TOKEN INVALID because ISS '{iss}' does not match server url '{cls.server_url(authentication_identifier)}'"
|
||||
)
|
||||
|
|
|
@ -30,6 +30,7 @@ from spiffworkflow_backend.models.human_task import HumanTaskModel
|
|||
from spiffworkflow_backend.models.permission_assignment import PermissionAssignmentModel
|
||||
from spiffworkflow_backend.models.permission_target import PermissionTargetModel
|
||||
from spiffworkflow_backend.models.principal import PrincipalModel
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
|
||||
from spiffworkflow_backend.models.service_account import SPIFF_SERVICE_ACCOUNT_AUTH_SERVICE
|
||||
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
||||
from spiffworkflow_backend.models.user import SPIFF_GUEST_USER
|
||||
|
@ -281,7 +282,7 @@ class AuthorizationService:
|
|||
return None
|
||||
|
||||
@classmethod
|
||||
def check_for_permission(cls) -> None:
|
||||
def check_for_permission(cls, decoded_token: dict | None) -> None:
|
||||
if cls.should_disable_auth_for_request():
|
||||
return None
|
||||
|
||||
|
@ -293,7 +294,7 @@ class AuthorizationService:
|
|||
if cls.request_is_excluded_from_permission_check():
|
||||
return None
|
||||
|
||||
if cls.request_allows_guest_access():
|
||||
if cls.request_allows_guest_access(decoded_token):
|
||||
return None
|
||||
|
||||
permission_string = cls.get_permission_from_http_method(request.method)
|
||||
|
@ -319,7 +320,7 @@ class AuthorizationService:
|
|||
return False
|
||||
|
||||
@classmethod
|
||||
def request_allows_guest_access(cls) -> bool:
|
||||
def request_allows_guest_access(cls, decoded_token: dict | None) -> bool:
|
||||
if cls.request_is_excluded_from_permission_check():
|
||||
return True
|
||||
|
||||
|
@ -329,6 +330,18 @@ class AuthorizationService:
|
|||
task_guid = request.path.split("/")[4]
|
||||
if TaskModel.task_guid_allows_guest(task_guid, process_instance_id):
|
||||
return True
|
||||
|
||||
if (
|
||||
decoded_token is not None
|
||||
and "process_instance_id" in decoded_token
|
||||
and "only_guest_task_completion" in decoded_token
|
||||
and decoded_token["only_guest_task_completion"] is True
|
||||
and api_view_function.__name__ == "typeahead"
|
||||
and api_view_function.__module__ == "spiffworkflow_backend.routes.connector_proxy_controller"
|
||||
):
|
||||
process_instance = ProcessInstanceModel.query.filter_by(id=decoded_token["process_instance_id"]).first()
|
||||
if process_instance is not None and not process_instance.has_terminal_status():
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import ast
|
||||
import base64
|
||||
import re
|
||||
import time
|
||||
|
||||
from flask.app import Flask
|
||||
from flask.testing import FlaskClient
|
||||
|
@ -45,8 +44,6 @@ class TestAuthentication(BaseTest):
|
|||
"groups": ["group_one", "group_two"],
|
||||
"iss": app.config["SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS"][0]["uri"],
|
||||
"aud": "spiffworkflow-backend",
|
||||
"iat": round(time.time()),
|
||||
"exp": round(time.time()) + 1000,
|
||||
}
|
||||
)
|
||||
response = None
|
||||
|
@ -63,8 +60,6 @@ class TestAuthentication(BaseTest):
|
|||
"groups": ["group_one"],
|
||||
"iss": app.config["SPIFFWORKFLOW_BACKEND_AUTH_CONFIGS"][0]["uri"],
|
||||
"aud": "spiffworkflow-backend",
|
||||
"iat": round(time.time()),
|
||||
"exp": round(time.time()) + 1000,
|
||||
}
|
||||
)
|
||||
response = client.post(
|
||||
|
|
Loading…
Reference in New Issue