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 **/.ropeproject/*
*.py[co] **/*.py[co]
.env **/.env

View File

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

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ class ClientForm(FlaskForm):
token_endpoint_auth_method = SelectField( token_endpoint_auth_method = SelectField(
"Token endpoint authentication method", "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()], validators=[DataRequired()],
) )

View File

@ -4,6 +4,7 @@ from authlib.integrations.sqla_oauth2 import (
create_save_token_func, create_save_token_func,
create_bearer_token_validator, create_bearer_token_validator,
) )
from authlib.oauth2.rfc6749.errors import InvalidClientError
from authlib.oauth2.rfc6749.grants import ( from authlib.oauth2.rfc6749.grants import (
AuthorizationCodeGrant as _AuthorizationCodeGrant, AuthorizationCodeGrant as _AuthorizationCodeGrant,
) )
@ -13,11 +14,15 @@ from authlib.oidc.core.grants import (
OpenIDHybridGrant as _OpenIDHybridGrant, OpenIDHybridGrant as _OpenIDHybridGrant,
) )
from authlib.oidc.core import UserInfo from authlib.oidc.core import UserInfo
from authlib.common.urls import urlparse, url_decode
from werkzeug.security import gen_salt from werkzeug.security import gen_salt
from .extensions import db from .extensions import db
from .models import Client, AuthorizationCode, Token from .models import Client, AuthorizationCode, Token
from .directory import LDAPUserProxy from .directory import LDAPUserProxy
from flask import current_app as app from flask import current_app as app
import logging
log = logging.getLogger(__name__)
DUMMY_JWT_CONFIG = { DUMMY_JWT_CONFIG = {
@ -110,8 +115,34 @@ class HybridGrant(_OpenIDHybridGrant):
return generate_user_info(user, scope) return generate_user_info(user, scope)
authorization = AuthorizationServer() def _validate_client(query_client, client_id, state=None, status_code=400):
require_oauth = ResourceProtector() 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): def save_token(token, request):
@ -134,13 +165,42 @@ def save_token(token, request):
db.session.commit() 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): def config_oauth(app):
query_client = create_query_client_func(db.session, Client) query_client = create_query_client_func(db.session, Client)
authorization.init_app(app, query_client=query_client, save_token=save_token) 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 # support all openid grants
authorization.register_grant( authorization.register_grant(
AuthorizationCodeGrant, [OpenIDCode(require_nonce=False)] CustomAuthorizationCodeGrant, [OpenIDCode(require_nonce=False)]
) )
authorization.register_grant(ImplicitGrant) authorization.register_grant(ImplicitGrant)
authorization.register_grant(HybridGrant) authorization.register_grant(HybridGrant)

View File

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

View File

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

View File

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

View File

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

View File

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