diff --git a/migrations/versions/ff1c1628337c_.py b/migrations/versions/3f049fa4d8ac_.py similarity index 99% rename from migrations/versions/ff1c1628337c_.py rename to migrations/versions/3f049fa4d8ac_.py index d8da6d3c..dc8675a6 100644 --- a/migrations/versions/ff1c1628337c_.py +++ b/migrations/versions/3f049fa4d8ac_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: ff1c1628337c +Revision ID: 3f049fa4d8ac Revises: -Create Date: 2022-11-28 15:08:52.014254 +Create Date: 2022-11-30 16:49:54.805372 """ from alembic import op @@ -10,7 +10,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'ff1c1628337c' +revision = '3f049fa4d8ac' down_revision = None branch_labels = None depends_on = None @@ -79,8 +79,7 @@ def upgrade(): sa.Column('email', sa.String(length=255), nullable=True), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('service', 'service_id', name='service_key'), - sa.UniqueConstraint('uid'), - sa.UniqueConstraint('username') + sa.UniqueConstraint('uid') ) op.create_table('message_correlation_property', sa.Column('id', sa.Integer(), nullable=False), diff --git a/src/spiffworkflow_backend/config/permissions/development.yml b/src/spiffworkflow_backend/config/permissions/development.yml index 1acace14..d92daeaf 100644 --- a/src/spiffworkflow_backend/config/permissions/development.yml +++ b/src/spiffworkflow_backend/config/permissions/development.yml @@ -4,11 +4,7 @@ users: admin: email: admin@spiffworkflow.org password: admin - dan: - email: dan@spiffworkflow.org - password: password - - + preferred_username: Admin groups: admin: diff --git a/src/spiffworkflow_backend/models/user.py b/src/spiffworkflow_backend/models/user.py index eb88e5de..ed6d10e6 100644 --- a/src/spiffworkflow_backend/models/user.py +++ b/src/spiffworkflow_backend/models/user.py @@ -30,7 +30,7 @@ class UserModel(SpiffworkflowBaseDBModel): __table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),) id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(255), nullable=False, unique=True) + username = db.Column(db.String(255), nullable=False, unique=False) # server and service id must be unique, not username. uid = db.Column(db.String(50), unique=True) service = db.Column(db.String(50), nullable=False, unique=False) service_id = db.Column(db.String(255), nullable=False, unique=False) diff --git a/src/spiffworkflow_backend/routes/openid_blueprint/openid_blueprint.py b/src/spiffworkflow_backend/routes/openid_blueprint/openid_blueprint.py index b16ba46a..5c96d62b 100644 --- a/src/spiffworkflow_backend/routes/openid_blueprint/openid_blueprint.py +++ b/src/spiffworkflow_backend/routes/openid_blueprint/openid_blueprint.py @@ -1,23 +1,22 @@ """ Provides the bare minimum endpoints required by SpiffWorkflow to -handle openid authentication -- definitely not a production system. -This is just here to make local development, testing, and -demonstration easier. +handle openid authentication -- definitely not a production ready system. +This is just here to make local development, testing, and demonstration easier. """ import base64 import time -import urllib from urllib.parse import urlencode import jwt import yaml -from flask import Blueprint, render_template, request, current_app, redirect, url_for, g +from flask import Blueprint, render_template, request, current_app, redirect, url_for openid_blueprint = Blueprint( "openid", __name__, template_folder="templates", static_folder="static" ) -MY_SECRET_CODE = ":this_should_be_some_crazy_code_different_all_the_time" +MY_SECRET_CODE = ":this_is_not_secure_do_not_use_in_production" + @openid_blueprint.route("/.well-known/openid-configuration", methods=["GET"]) def well_known(): @@ -26,8 +25,9 @@ def well_known(): host_url = request.host_url.strip('/') return { "issuer": f"{host_url}/openid", - "authorization_endpoint": f"{host_url}{url_for('openid.auth')}", + "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')}", } @@ -40,23 +40,22 @@ def auth(): client_id=request.args.get('client_id'), scope=request.args.get('scope'), redirect_uri=request.args.get('redirect_uri'), - error_message=request.args.get('error_message')) + error_message=request.args.get('error_message', '')) @openid_blueprint.route("/form_submit", methods=["POST"]) def form_submit(): - users = get_users() if request.values['Uname'] in users and request.values['Pass'] == users[request.values['Uname']]["password"]: # Redirect back to the end user with some detailed information state = request.values.get('state') data = { - "state": base64.b64encode(bytes(state, 'UTF-8')), + "state": state, "code": request.values['Uname'] + MY_SECRET_CODE, "session_state": "" } url = request.values.get('redirect_uri') + "?" + urlencode(data) - return redirect(url, code=200) + return redirect(url) else: return render_template('login.html', state=request.values.get('state'), @@ -64,18 +63,19 @@ def form_submit(): client_id=request.values.get('client_id'), scope=request.values.get('scope'), redirect_uri=request.values.get('redirect_uri'), - error_message="Login failed. Please try agian.") + error_message="Login failed. Please try again.") @openid_blueprint.route("/token", methods=["POST"]) def token(): """Url that will return a valid token, given the super secret sauce""" - grant_type=request.values.get('grant_type') - code=request.values.get('code') - redirect_uri=request.values.get('redirect_uri') + grant_type = request.values.get('grant_type') + code = request.values.get('code') + redirect_uri = request.values.get('redirect_uri') """We just stuffed the user name on the front of the code, so grab it.""" user_name, secret_hash = code.split(":") + user_details = get_users()[user_name] """Get authentication from headers.""" authorization = request.headers.get('Authorization') @@ -83,35 +83,50 @@ def token(): authorization = base64.b64decode(authorization).decode('utf-8') client_id, client_secret = authorization.split(":") - base_url = url_for(openid_blueprint) - access_token = "..." - refresh_token = "..." + base_url = request.host_url + "openid" + access_token = user_name + ":" + "always_good_demo_access_token" + refresh_token = user_name + ":" + "always_good_demo_refresh_token" + id_token = jwt.encode({ "iss": base_url, "aud": [client_id, "account"], "iat": time.time(), - "exp": time.time() + 86400 # Exprire after a day. - }) - - {'exp': 1669757386, 'iat': 1669755586, 'auth_time': 1669753049, 'jti': '0ec2cc09-3498-4921-a021-c3b98427df70', - 'iss': 'http://localhost:7002/realms/spiffworkflow', 'aud': 'spiffworkflow-backend', - 'sub': '99e7e4ea-d4ae-4944-bd31-873dac7b004c', 'typ': 'ID', 'azp': 'spiffworkflow-backend', - 'session_state': '8751d5f6-2c60-4205-9be0-2b1005f5891e', 'at_hash': 'O5i-VLus6sryR0grMS2Y4w', 'acr': '0', - 'sid': '8751d5f6-2c60-4205-9be0-2b1005f5891e', 'email_verified': False, 'preferred_username': 'dan'} - + "exp": time.time() + 86400, # Expire after a day. + "sub": user_name, + "preferred_username": user_details.get('preferred_username', user_name) + }, + client_secret, + algorithm="HS256", + ) response = { "access_token": id_token, "id_token": id_token, + "refresh_token": id_token } + return response + + +@openid_blueprint.route("/end_session", methods=["GET"]) +def end_session(): + redirect_url = request.args.get('post_logout_redirect_uri') + id_token_hint = request.args.get('id_token_hint') + return redirect(redirect_url) + @openid_blueprint.route("/refresh", methods=["POST"]) def refresh(): pass + +permission_cache = None + + def get_users(): - with open(current_app.config["PERMISSIONS_FILE_FULLPATH"]) as file: - permission_configs = yaml.safe_load(file) - if "users" in permission_configs: - return permission_configs["users"] + global permission_cache + if not permission_cache: + with open(current_app.config["PERMISSIONS_FILE_FULLPATH"]) as file: + permission_cache = yaml.safe_load(file) + if "users" in permission_cache: + return permission_cache["users"] else: - return {} \ No newline at end of file + return {} diff --git a/src/spiffworkflow_backend/routes/openid_blueprint/static/login.css b/src/spiffworkflow_backend/routes/openid_blueprint/static/login.css new file mode 100644 index 00000000..15b093f6 --- /dev/null +++ b/src/spiffworkflow_backend/routes/openid_blueprint/static/login.css @@ -0,0 +1,112 @@ + body{ + margin: 0; + padding: 0; + background-color:white; + font-family: 'Arial'; + } + header { + width: 100%; + background-color: black; + } + .logo_small { + padding: 5px 20px; + } + .error { + margin: 20px auto; + color: red; + font-weight: bold; + text-align: center; + } + .login{ + width: 400px; + overflow: hidden; + margin: 20px auto; + padding: 50px; + background: #fff; + border-radius: 15px ; + } + h2{ + text-align: center; + color: #277582; + padding: 20px; + } + label{ + color: #fff; + width: 200px; + display: inline-block; + } + #log { + width: 100px; + height: 50px; + border: none; + padding-left: 7px; + background-color:#202020; + color: #DDD; + text-align: left; + } + .cds--btn--primary { + background-color: #0f62fe; + border: 1px solid #0000; + color: #fff; + } + .cds--btn { + align-items: center; + border: 0; + border-radius: 0; + box-sizing: border-box; + cursor: pointer; + display: inline-flex; + flex-shrink: 0; + font-family: inherit; + font-size: 100%; + font-size: .875rem; + font-weight: 400; + justify-content: space-between; + letter-spacing: .16px; + line-height: 1.28572; + margin: 0; + max-width: 20rem; + min-height: 3rem; + outline: none; + padding: calc(0.875rem - 3px) 63px calc(0.875rem - 3px) 15px; + position: relative; + text-align: left; + text-decoration: none; + transition: background 70ms cubic-bezier(0, 0, .38, .9), box-shadow 70ms cubic-bezier(0, 0, .38, .9), border-color 70ms cubic-bezier(0, 0, .38, .9), outline 70ms cubic-bezier(0, 0, .38, .9); + vertical-align: initial; + vertical-align: top; + width: max-content; + } + .cds--btn:hover { + background-color: #0145c5; + } + .cds--btn:focus { + background-color: #01369a; + } + + .cds--text-input { + background-color: #eee; + border: none; + border-bottom: 1px solid #8d8d8d; + color: #161616; + font-family: inherit; + font-size: .875rem; + font-weight: 400; + height: 2.5rem; + letter-spacing: .16px; + line-height: 1.28572; + outline: 2px solid #0000; + outline-offset: -2px; + padding: 0 1rem; + transition: background-color 70ms cubic-bezier(.2,0,.38,.9),outline 70ms cubic-bezier(.2,0,.38,.9); + width: 100%; + } + + span{ + color: white; + font-size: 17px; + } + a{ + float: right; + background-color: grey; + } diff --git a/src/spiffworkflow_backend/routes/openid_blueprint/static/logo.png b/src/spiffworkflow_backend/routes/openid_blueprint/static/logo.png new file mode 100644 index 00000000..4cffb07f Binary files /dev/null and b/src/spiffworkflow_backend/routes/openid_blueprint/static/logo.png differ diff --git a/src/spiffworkflow_backend/routes/openid_blueprint/static/logo_small.png b/src/spiffworkflow_backend/routes/openid_blueprint/static/logo_small.png new file mode 100644 index 00000000..d0ad4499 Binary files /dev/null and b/src/spiffworkflow_backend/routes/openid_blueprint/static/logo_small.png differ diff --git a/src/spiffworkflow_backend/routes/openid_blueprint/templates/login.html b/src/spiffworkflow_backend/routes/openid_blueprint/templates/login.html index 1da64914..6ef26457 100644 --- a/src/spiffworkflow_backend/routes/openid_blueprint/templates/login.html +++ b/src/spiffworkflow_backend/routes/openid_blueprint/templates/login.html @@ -2,94 +2,27 @@ Login Form - - + -

Login to SpiffWorkflow


+
+ +
+ +

Login

{{error_message}}
- - +

- - +

- +