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