A little cleanup of the ui

Don't check authorization on static assets
Do not require unique username on user table (uniqueness check is on the service and service id composite.)
This commit is contained in:
Dan 2022-12-01 11:42:36 -05:00
parent ca339ee933
commit e8cbe1df84
10 changed files with 177 additions and 141 deletions

View File

@ -1,8 +1,8 @@
"""empty message """empty message
Revision ID: ff1c1628337c Revision ID: 3f049fa4d8ac
Revises: Revises:
Create Date: 2022-11-28 15:08:52.014254 Create Date: 2022-11-30 16:49:54.805372
""" """
from alembic import op from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = 'ff1c1628337c' revision = '3f049fa4d8ac'
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -79,8 +79,7 @@ def upgrade():
sa.Column('email', sa.String(length=255), nullable=True), sa.Column('email', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('service', 'service_id', name='service_key'), sa.UniqueConstraint('service', 'service_id', name='service_key'),
sa.UniqueConstraint('uid'), sa.UniqueConstraint('uid')
sa.UniqueConstraint('username')
) )
op.create_table('message_correlation_property', op.create_table('message_correlation_property',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),

View File

@ -4,11 +4,7 @@ users:
admin: admin:
email: admin@spiffworkflow.org email: admin@spiffworkflow.org
password: admin password: admin
dan: preferred_username: Admin
email: dan@spiffworkflow.org
password: password
groups: groups:
admin: admin:

View File

@ -30,7 +30,7 @@ class UserModel(SpiffworkflowBaseDBModel):
__table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),) __table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),)
id = db.Column(db.Integer, primary_key=True) 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) uid = db.Column(db.String(50), unique=True)
service = db.Column(db.String(50), nullable=False, unique=False) service = db.Column(db.String(50), nullable=False, unique=False)
service_id = db.Column(db.String(255), nullable=False, unique=False) service_id = db.Column(db.String(255), nullable=False, unique=False)

View File

@ -1,23 +1,22 @@
""" """
Provides the bare minimum endpoints required by SpiffWorkflow to Provides the bare minimum endpoints required by SpiffWorkflow to
handle openid authentication -- definitely not a production system. handle openid authentication -- definitely not a production ready system.
This is just here to make local development, testing, and This is just here to make local development, testing, and demonstration easier.
demonstration easier.
""" """
import base64 import base64
import time import time
import urllib
from urllib.parse import urlencode from urllib.parse import urlencode
import jwt import jwt
import yaml 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_blueprint = Blueprint(
"openid", __name__, template_folder="templates", static_folder="static" "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"]) @openid_blueprint.route("/.well-known/openid-configuration", methods=["GET"])
def well_known(): def well_known():
@ -26,8 +25,9 @@ def well_known():
host_url = request.host_url.strip('/') host_url = request.host_url.strip('/')
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')}",
"token_endpoint": f"{host_url}{url_for('openid.token')}", "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'), client_id=request.args.get('client_id'),
scope=request.args.get('scope'), scope=request.args.get('scope'),
redirect_uri=request.args.get('redirect_uri'), 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"]) @openid_blueprint.route("/form_submit", methods=["POST"])
def form_submit(): def form_submit():
users = get_users() users = get_users()
if request.values['Uname'] in users and request.values['Pass'] == users[request.values['Uname']]["password"]: 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 # Redirect back to the end user with some detailed information
state = request.values.get('state') state = request.values.get('state')
data = { data = {
"state": base64.b64encode(bytes(state, 'UTF-8')), "state": state,
"code": request.values['Uname'] + MY_SECRET_CODE, "code": request.values['Uname'] + MY_SECRET_CODE,
"session_state": "" "session_state": ""
} }
url = request.values.get('redirect_uri') + "?" + urlencode(data) url = request.values.get('redirect_uri') + "?" + urlencode(data)
return redirect(url, code=200) return redirect(url)
else: else:
return render_template('login.html', return render_template('login.html',
state=request.values.get('state'), state=request.values.get('state'),
@ -64,18 +63,19 @@ def form_submit():
client_id=request.values.get('client_id'), client_id=request.values.get('client_id'),
scope=request.values.get('scope'), scope=request.values.get('scope'),
redirect_uri=request.values.get('redirect_uri'), 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"]) @openid_blueprint.route("/token", methods=["POST"])
def token(): def token():
"""Url that will return a valid token, given the super secret sauce""" """Url that will return a valid token, given the super secret sauce"""
grant_type=request.values.get('grant_type') grant_type = request.values.get('grant_type')
code=request.values.get('code') code = request.values.get('code')
redirect_uri=request.values.get('redirect_uri') redirect_uri = request.values.get('redirect_uri')
"""We just stuffed the user name on the front of the code, so grab it.""" """We just stuffed the user name on the front of the code, so grab it."""
user_name, secret_hash = code.split(":") user_name, secret_hash = code.split(":")
user_details = get_users()[user_name]
"""Get authentication from headers.""" """Get authentication from headers."""
authorization = request.headers.get('Authorization') authorization = request.headers.get('Authorization')
@ -83,35 +83,50 @@ def token():
authorization = base64.b64decode(authorization).decode('utf-8') authorization = base64.b64decode(authorization).decode('utf-8')
client_id, client_secret = authorization.split(":") client_id, client_secret = authorization.split(":")
base_url = url_for(openid_blueprint) base_url = request.host_url + "openid"
access_token = "..." access_token = user_name + ":" + "always_good_demo_access_token"
refresh_token = "..." refresh_token = user_name + ":" + "always_good_demo_refresh_token"
id_token = jwt.encode({ id_token = jwt.encode({
"iss": base_url, "iss": base_url,
"aud": [client_id, "account"], "aud": [client_id, "account"],
"iat": time.time(), "iat": time.time(),
"exp": time.time() + 86400 # Exprire after a day. "exp": time.time() + 86400, # Expire after a day.
}) "sub": user_name,
"preferred_username": user_details.get('preferred_username', user_name)
{'exp': 1669757386, 'iat': 1669755586, 'auth_time': 1669753049, 'jti': '0ec2cc09-3498-4921-a021-c3b98427df70', },
'iss': 'http://localhost:7002/realms/spiffworkflow', 'aud': 'spiffworkflow-backend', client_secret,
'sub': '99e7e4ea-d4ae-4944-bd31-873dac7b004c', 'typ': 'ID', 'azp': 'spiffworkflow-backend', algorithm="HS256",
'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'}
response = { response = {
"access_token": id_token, "access_token": id_token,
"id_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"]) @openid_blueprint.route("/refresh", methods=["POST"])
def refresh(): def refresh():
pass pass
permission_cache = None
def get_users(): def get_users():
with open(current_app.config["PERMISSIONS_FILE_FULLPATH"]) as file: global permission_cache
permission_configs = yaml.safe_load(file) if not permission_cache:
if "users" in permission_configs: with open(current_app.config["PERMISSIONS_FILE_FULLPATH"]) as file:
return permission_configs["users"] permission_cache = yaml.safe_load(file)
if "users" in permission_cache:
return permission_cache["users"]
else: else:
return {} return {}

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -2,94 +2,27 @@
<html> <html>
<head> <head>
<title>Login Form</title> <title>Login Form</title>
<link rel="stylesheet" type="text/css" href="css/style.css"> <link rel="stylesheet" type="text/css" href="{{ url_for('openid.static', filename='login.css') }}">
<style>
body{
margin: 0;
padding: 0;
background-color:white;
font-family: 'Arial';
}
.error {
margin: 20px auto;
color: red;
font-weight: bold;
text-align: center;
}
.login{
width: 382px;
overflow: hidden;
margin: 20px auto;
padding: 80px;
background: #000;
border-radius: 15px ;
}
h2{
text-align: center;
color: #277582;
padding: 20px;
}
label{
color: #fff;
font-size: 17px;
}
#Uname{
width: 300px;
height: 30px;
border: none;
border-radius: 3px;
padding-left: 8px;
}
#Pass{
width: 300px;
height: 30px;
border: none;
border-radius: 3px;
padding-left: 8px;
}
#log{
width: 300px;
height: 30px;
border: none;
border-radius: 17px;
padding-left: 7px;
color: blue;
}
span{
color: white;
font-size: 17px;
}
a{
float: right;
background-color: grey;
}
</style>
</head> </head>
<body> <body>
<h2>Login to SpiffWorkflow</h2><br> <header>
<img class="logo_small" src="{{ url_for('openid.static', filename='logo_small.png') }}"/>
</header>
<h2>Login</h2>
<div class="error">{{error_message}}</div> <div class="error">{{error_message}}</div>
<div class="login"> <div class="login">
<form id="login" method="post" action="{{ url_for('openid.form_submit') }}"> <form id="login" method="post" action="{{ url_for('openid.form_submit') }}">
<label><b>User Name <input type="text" class="cds--text-input" name="Uname" id="Uname" placeholder="Username">
</b>
</label>
<input type="text" name="Uname" id="Uname" placeholder="Username">
<br><br> <br><br>
<label><b>Password <input type="Password" class="cds--text-input" name="Pass" id="Pass" placeholder="Password">
</b>
</label>
<input type="Password" name="Pass" id="Pass" placeholder="Password">
<br><br> <br><br>
<input type="hidden" name="state" value="{{state}}"/> <input type="hidden" name="state" value="{{state}}"/>
<input type="hidden" name="response_type" value="{{response_type}}"/> <input type="hidden" name="response_type" value="{{response_type}}"/>
<input type="hidden" name="client_id" value="{{client_id}}"/> <input type="hidden" name="client_id" value="{{client_id}}"/>
<input type="hidden" name="scope" value="{{scope}}"/> <input type="hidden" name="scope" value="{{scope}}"/>
<input type="hidden" name="redirect_uri" value="{{redirect_uri}}"/> <input type="hidden" name="redirect_uri" value="{{redirect_uri}}"/>
<input type="submit" name="log" id="log" value="Log In"> <input type="submit" name="log" class="cds--btn cds--btn--primary" value="Log In">
<br><br> <br><br>
<!-- should maybe add this stuff in eventually, but this is just for testing. <!-- should maybe add this stuff in eventually, but this is just for testing.
<input type="checkbox" id="check"> <input type="checkbox" id="check">

View File

@ -6,7 +6,7 @@ from typing import Union
import jwt import jwt
import yaml import yaml
from flask import current_app from flask import current_app, scaffold
from flask import g from flask import g
from flask import request from flask import request
from flask_bpmn.api.api_error import ApiError from flask_bpmn.api.api_error import ApiError
@ -253,6 +253,7 @@ class AuthorizationService:
or api_view_function.__name__ in authentication_exclusion_list or api_view_function.__name__ in authentication_exclusion_list
or api_view_function.__name__ in swagger_functions or api_view_function.__name__ in swagger_functions
or module == openid_blueprint or module == openid_blueprint
or module == scaffold # don't check permissions for static assets
): ):
return True return True

View File

@ -21,7 +21,7 @@ class TestFaskOpenId(BaseTest):
app: Flask, app: Flask,
client: FlaskClient, client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,) -> None: with_db_and_bpmn_file_cleanup: None,) -> None:
response = client.get("/openid/well-known/openid-configuration") response = client.get("/openid/.well-known/openid-configuration")
discovered_urls = response.json discovered_urls = response.json
assert "http://localhost/openid" == discovered_urls["issuer"] assert "http://localhost/openid" == discovered_urls["issuer"]
assert "http://localhost/openid/auth" == discovered_urls["authorization_endpoint"] assert "http://localhost/openid/auth" == discovered_urls["authorization_endpoint"]
@ -57,23 +57,3 @@ class TestFaskOpenId(BaseTest):
response = client.post("/openid/token", data=data, headers=headers) response = client.post("/openid/token", data=data, headers=headers)
assert response assert response
def test_refresh_token_endpoint(self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,) -> None:
pass
# Handle a refresh with the following
# data provided
# "grant_type": "refresh_token",
# "refresh_token": refresh_token,
# "client_id": open_id_client_id,
# "client_secret": open_id_client_secret_key,
# Return an json response with:
# id - (this users' id)
def test_logout(self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,) -> None:
pass
# It should be possible to logout and be redirected back.