final fixups
parent
3daace694e
commit
64770ea1da
|
@ -0,0 +1 @@
|
|||
.gitignore
|
|
@ -1,3 +1,3 @@
|
|||
.ropeproject
|
||||
*.py[co]
|
||||
.env
|
||||
**/.ropeproject/*
|
||||
**/*.py[co]
|
||||
**/.env
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()],
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
17
sso/views.py
17
sso/views.py
|
@ -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)
|
||||
|
|
|
@ -103,3 +103,7 @@ None
|
|||
{% endif %}
|
||||
</ul>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro csrf_field() %}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
{% endmacro %}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue