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 8993748934
commit 7e3daaab3d
10 changed files with 177 additions and 141 deletions

View File

@ -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),

View File

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

View File

@ -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)

View File

@ -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 {}
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>
<head>
<title>Login Form</title>
<link rel="stylesheet" type="text/css" href="css/style.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>
<link rel="stylesheet" type="text/css" href="{{ url_for('openid.static', filename='login.css') }}">
</head>
<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="login">
<form id="login" method="post" action="{{ url_for('openid.form_submit') }}">
<label><b>User Name
</b>
</label>
<input type="text" name="Uname" id="Uname" placeholder="Username">
<input type="text" class="cds--text-input" name="Uname" id="Uname" placeholder="Username">
<br><br>
<label><b>Password
</b>
</label>
<input type="Password" name="Pass" id="Pass" placeholder="Password">
<input type="Password" class="cds--text-input" name="Pass" id="Pass" placeholder="Password">
<br><br>
<input type="hidden" name="state" value="{{state}}"/>
<input type="hidden" name="response_type" value="{{response_type}}"/>
<input type="hidden" name="client_id" value="{{client_id}}"/>
<input type="hidden" name="scope" value="{{scope}}"/>
<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>
<!-- should maybe add this stuff in eventually, but this is just for testing.
<input type="checkbox" id="check">

View File

@ -6,7 +6,7 @@ from typing import Union
import jwt
import yaml
from flask import current_app
from flask import current_app, scaffold
from flask import g
from flask import request
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 swagger_functions
or module == openid_blueprint
or module == scaffold # don't check permissions for static assets
):
return True

View File

@ -21,7 +21,7 @@ class TestFaskOpenId(BaseTest):
app: Flask,
client: FlaskClient,
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
assert "http://localhost/openid" == discovered_urls["issuer"]
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)
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.