final fixups

master
informatic 2020-05-30 22:53:06 +02:00
parent 3daace694e
commit 64770ea1da
13 changed files with 104 additions and 28 deletions

1
.dockerignore Symbolic link
View File

@ -0,0 +1 @@
.gitignore

6
.gitignore vendored
View File

@ -1,3 +1,3 @@
.ropeproject
*.py[co]
.env
**/.ropeproject/*
**/*.py[co]
**/.env

View File

@ -10,6 +10,7 @@ services:
backend:
build: .
image: registry.k0.hswaw.net/informatic/sso-v2
ports:
- 5000:5000
volumes:
@ -18,6 +19,7 @@ services:
- TEMPLATES_AUTO_RELOAD=true
- LDAP_BIND_DN
- LDAP_BIND_PASSWORD
- LOGGING_LEVEL=DEBUG
volumes:
pgdata:

View File

@ -28,6 +28,7 @@ python-ldap==3.2.0
requests==2.23.0
six==1.15.0
SQLAlchemy==1.3.17
sqlalchemy-cockroachdb==0.4.0
urllib3==1.25.9
Werkzeug==1.0.1
WTForms==2.3.1

View File

@ -1,6 +1,7 @@
import flask
from sso.extensions import db, migrate, login_manager
from sso.extensions import db, migrate, login_manager, csrf
from sso.oauth2 import config_oauth
import logging
def create_app():
@ -14,6 +15,7 @@ def create_app():
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
csrf.init_app(app)
config_oauth(app)
import sso.views
@ -31,4 +33,7 @@ def create_app():
app.config.get("PROXYFIX_NUM_PROXIES"),
)
if app.config.get('LOGGING_LEVEL'):
logging.basicConfig(level=app.config['LOGGING_LEVEL'])
return app

View File

@ -1,9 +1,11 @@
import flask_sqlalchemy
import flask_migrate
import flask_login
from flask_wtf.csrf import CSRFProtect
db = flask_sqlalchemy.SQLAlchemy()
migrate = flask_migrate.Migrate()
login_manager = flask_login.LoginManager()
login_manager.login_view = "/login"
csrf = CSRFProtect()

View File

@ -50,7 +50,7 @@ class ClientForm(FlaskForm):
token_endpoint_auth_method = SelectField(
"Token endpoint authentication method",
choices=[("client_secret_basic", "Basic"), ("client_secret_post", "POST")],
choices=[("client_secret_basic", "Basic"), ("client_secret_post", "POST"), ("client_secret_get", "Query args (DEPRECATED)")],
validators=[DataRequired()],
)

View File

@ -4,6 +4,7 @@ from authlib.integrations.sqla_oauth2 import (
create_save_token_func,
create_bearer_token_validator,
)
from authlib.oauth2.rfc6749.errors import InvalidClientError
from authlib.oauth2.rfc6749.grants import (
AuthorizationCodeGrant as _AuthorizationCodeGrant,
)
@ -13,11 +14,15 @@ from authlib.oidc.core.grants import (
OpenIDHybridGrant as _OpenIDHybridGrant,
)
from authlib.oidc.core import UserInfo
from authlib.common.urls import urlparse, url_decode
from werkzeug.security import gen_salt
from .extensions import db
from .models import Client, AuthorizationCode, Token
from .directory import LDAPUserProxy
from flask import current_app as app
import logging
log = logging.getLogger(__name__)
DUMMY_JWT_CONFIG = {
@ -110,8 +115,34 @@ class HybridGrant(_OpenIDHybridGrant):
return generate_user_info(user, scope)
authorization = AuthorizationServer()
require_oauth = ResourceProtector()
def _validate_client(query_client, client_id, state=None, status_code=400):
if client_id is None:
raise InvalidClientError(state=state, status_code=status_code)
client = query_client(client_id)
if not client:
raise InvalidClientError(state=state, status_code=status_code)
return client
def authenticate_client_secret_get(query_client, request):
"""Authenticates clients providing their secret via query args (either via GET or POST) request"""
data = request.args
client_id = data.get('client_id')
client_secret = data.get('client_secret')
if client_id and client_secret:
client = _validate_client(query_client, client_id, request.state)
if client.check_token_endpoint_auth_method('client_secret_get') \
and client.check_client_secret(client_secret):
log.debug(
'Authenticate %s via "client_secret_get" '
'success', client_id
)
return client
log.debug(
'Authenticate %s via "client_secret_get" '
'failed', client_id
)
def save_token(token, request):
@ -134,13 +165,42 @@ def save_token(token, request):
db.session.commit()
class CustomAuthorizationCodeGrant(AuthorizationCodeGrant):
# kill me (inventory)
TOKEN_ENDPOINT_HTTP_METHODS = ['GET', 'POST']
TOKEN_ENDPOINT_AUTH_METHODS = [
'client_secret_basic', 'client_secret_post', 'client_secret_get', 'none'
]
def validate_token_request(self):
# TODO apply this hack only on client_secret_get authentication method
self.request.form = self.request.data
return super(CustomAuthorizationCodeGrant, self).validate_token_request()
class CustomResourceProtector(ResourceProtector):
def validate_request(self, scope, request, scope_operator='AND'):
# damn you gerrit
args = dict(url_decode(urlparse.urlparse(request.uri).query))
if args.get('access_token'):
token_string = args.get('access_token')
return self._token_validators['bearer'](token_string, scope, request, scope_operator)
return super(CustomResourceProtector, self).validate_request(scope, request, scope_operator)
authorization = AuthorizationServer()
require_oauth = CustomResourceProtector()
def config_oauth(app):
query_client = create_query_client_func(db.session, Client)
authorization.init_app(app, query_client=query_client, save_token=save_token)
authorization.register_client_auth_method('client_secret_get', authenticate_client_secret_get)
# support all openid grants
authorization.register_grant(
AuthorizationCodeGrant, [OpenIDCode(require_nonce=False)]
CustomAuthorizationCodeGrant, [OpenIDCode(require_nonce=False)]
)
authorization.register_grant(ImplicitGrant)
authorization.register_grant(HybridGrant)

View File

@ -3,6 +3,9 @@ from environs import Env
env = Env()
env.read_env()
SQLALCHEMY_TRACK_MODIFICATIONS = False
WTF_CSRF_CHECK_DEFAULT = False
SECRET_KEY = env.str("SECRET_KEY", default="randomstring")
db_username = env.str("DATABASE_USERNAME", default="postgres")
@ -33,7 +36,6 @@ LDAP_BIND_DN = env.str(
"LDAP_BIND_DN", default="cn=auth,ou=Services,dc=hackerspace,dc=pl"
)
LDAP_BIND_PASSWORD = env.str("LDAP_BIND_PASSWORD", default="insert password here")
SQLALCHEMY_TRACK_MODIFICATIONS = False
PROXYFIX_ENABLE = env.bool('PROXYFIX_ENABLE', default=True)
PROXYFIX_NUM_PROXIES = env.int('PROXYFIX_NUM_PROXIES', default=1)
@ -44,3 +46,5 @@ JWT_CONFIG = {
"iss": "https://sso.hackerspace.pl",
"exp": 3600,
}
LOGGING_LEVEL = env.str('LOGGING_LEVEL', default=None)

View File

@ -11,6 +11,7 @@ from flask import (
)
import uuid
from flask_login import login_required, current_user, login_user, logout_user
from sso.extensions import csrf
from sso.directory import LDAPUserProxy, check_credentials
from sso.models import db, Token, Client
from sso.forms import LoginForm, ClientForm
@ -38,6 +39,8 @@ def profile():
@bp.route("/token/<int:id>/revoke", methods=["POST"])
@login_required
def token_revoke(id):
csrf.protect()
token = Token.query.filter(
Token.user_id == current_user.username, Token.id == id
).first()
@ -109,8 +112,6 @@ def client_edit(client_id):
# OAuth API
@bp.route("/oauth/authorize", methods=["GET", "POST"])
@login_required
def authorize():
@ -128,9 +129,12 @@ def authorize():
return authorization.create_authorization_response(grant_user=current_user)
return render_template(
"oauthorize.html", user=current_user, grant=grant, client=grant.client
"oauthorize.html", user=current_user, grant=grant, client=grant.client,
scopes=grant.request.scope.split()
)
csrf.protect()
if request.form["confirm"]:
grant_user = current_user
else:
@ -139,7 +143,7 @@ def authorize():
return authorization.create_authorization_response(grant_user=grant_user)
@bp.route("/oauth/token", methods=["POST"])
@bp.route("/oauth/token", methods=["GET", "POST"])
def issue_token():
return authorization.create_token_response()
@ -147,7 +151,7 @@ def issue_token():
# HSWAW specific endpoint
@bp.route("/api/profile")
@bp.route("/api/1/profile")
@require_oauth("profile:read")
@require_oauth("profile:read openid", "OR")
def api_profile():
user = current_token.user
return jsonify(
@ -161,8 +165,7 @@ def api_profile():
# OpenIDConnect userinfo
@bp.route("/api/1/userinfo")
# @require_oauth("profile:read")
@require_oauth("openid")
@require_oauth("profile:read openid", "OR")
def api_userinfo():
user = current_token.user
# user = LDAPUserProxy(flask.request.oauth.user)

View File

@ -103,3 +103,7 @@ None
{% endif %}
</ul>
{% endmacro %}
{% macro csrf_field() %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endmacro %}

View File

@ -1,14 +1,15 @@
{% extends "base.html" %}
{% from "_helpers.html" import csrf_field %}
{% block content %}
<div class="container" id="authorize-container">
<center><img src="/static/hswaw_wht.svg" style="width: 50%;"/></center>
<h2>{{ client.name }}
<h2>{{ client.client_name }}
{% if client.approved %}<small title="This application is approved."><sup><i class="glyphicon glyphicon-ok-circle text-success"></i></sup></small>{% endif %}
</h2>
<h4>This app would like to:</h4>
<ul class="list-group">
{% if 'profile:read' in scopes and 'profile:write' not in scopes %}
{% if ('profile:read' in scopes or 'openid' in scopes) and 'profile:write' not in scopes %}
<li class="list-group-item">
<span class="glyphicon glyphicon-user" aria-hidden="true"></span>
Read your profile data.
@ -33,17 +34,9 @@
</li>
{% endif %}
</ul>
{{ user }}
<h4 style="margin-bottom: 20px;">On your ({{user.username}}) behalf.</h4>
<form action="" method="post">
{#{{ form.csrf_token }}#}
<!--<input type="hidden" name="client_id" value="{{ client.client_id }}">
<input type="hidden" name="scope" value="{{ scopes|join(' ') }}">
<input type="hidden" name="response_type" value="{{ response_type }}">
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
{% if state %}
<input type="hidden" name="state" value="{{ state }}">
{% endif %}-->
{{ csrf_field() }}
<button class="btn btn-lg btn-default" name="confirm" value="yes">
<span class="glyphicon glyphicon-ok-circle" aria-hidden="true"></span>
Grant Access

View File

@ -1,4 +1,5 @@
{% extends "base.html" %}
{% from "_helpers.html" import csrf_field %}
{% block content %}
<div class="container">
@ -30,7 +31,7 @@
<td>{{ token.get_expires_at() }}</td>
<td>
<form class="text-right" method="post" action="{{ url_for('.token_revoke', id=token.id) }}">
{# FIXME <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>#}
{{ csrf_field() }}
<button class="btn btn-danger btn-xs">Revoke <i class="glyphicon glyphicon-remove"></i></button>
</form>
</td>