summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPiotr Dobrowolski <admin@tastycode.pl>2020-05-30 22:53:06 +0200
committerPiotr Dobrowolski <admin@tastycode.pl>2020-05-30 22:53:06 +0200
commit64770ea1dab778b56e1acc4252462969471f97e1 (patch)
tree1016d19cf50fc8fdef11f3ffa941478174f39f71
parent3daace694ef73e947d148e4c079cd5e0cd5df368 (diff)
downloadsso-v2-64770ea1dab778b56e1acc4252462969471f97e1.tar.gz
sso-v2-64770ea1dab778b56e1acc4252462969471f97e1.tar.bz2
sso-v2-64770ea1dab778b56e1acc4252462969471f97e1.tar.xz
sso-v2-64770ea1dab778b56e1acc4252462969471f97e1.zip
final fixups
l---------.dockerignore1
-rw-r--r--.gitignore6
-rw-r--r--docker-compose.yml2
-rw-r--r--requirements.txt1
-rw-r--r--sso/__init__.py7
-rw-r--r--sso/extensions.py2
-rw-r--r--sso/forms.py2
-rw-r--r--sso/oauth2.py66
-rw-r--r--sso/settings.py6
-rw-r--r--sso/views.py17
-rw-r--r--templates/_helpers.html4
-rw-r--r--templates/oauthorize.html15
-rw-r--r--templates/profile.html3
13 files changed, 104 insertions, 28 deletions
diff --git a/.dockerignore b/.dockerignore
new file mode 120000
index 0000000..3e4e48b
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+.gitignore \ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 5d620ed..bd3bfe1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,3 @@
-.ropeproject
-*.py[co]
-.env
+**/.ropeproject/*
+**/*.py[co]
+**/.env
diff --git a/docker-compose.yml b/docker-compose.yml
index ef2c1bc..a249133 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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:
diff --git a/requirements.txt b/requirements.txt
index f69d07a..e4cf8cd 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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
diff --git a/sso/__init__.py b/sso/__init__.py
index 8e2cef0..78ba4fb 100644
--- a/sso/__init__.py
+++ b/sso/__init__.py
@@ -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
diff --git a/sso/extensions.py b/sso/extensions.py
index a3be2f3..3524110 100644
--- a/sso/extensions.py
+++ b/sso/extensions.py
@@ -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()
diff --git a/sso/forms.py b/sso/forms.py
index 555b0e3..42f71a8 100644
--- a/sso/forms.py
+++ b/sso/forms.py
@@ -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()],
)
diff --git a/sso/oauth2.py b/sso/oauth2.py
index 8323213..cfad0c8 100644
--- a/sso/oauth2.py
+++ b/sso/oauth2.py
@@ -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)
diff --git a/sso/settings.py b/sso/settings.py
index 71bcb8b..6f7294f 100644
--- a/sso/settings.py
+++ b/sso/settings.py
@@ -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)
diff --git a/sso/views.py b/sso/views.py
index e4c1b59..a67129c 100644
--- a/sso/views.py
+++ b/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)
diff --git a/templates/_helpers.html b/templates/_helpers.html
index 2c4a648..02a94c4 100644
--- a/templates/_helpers.html
+++ b/templates/_helpers.html
@@ -103,3 +103,7 @@ None
{% endif %}
</ul>
{% endmacro %}
+
+{% macro csrf_field() %}
+ <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
+{% endmacro %}
diff --git a/templates/oauthorize.html b/templates/oauthorize.html
index 4bf8c79..5798737 100644
--- a/templates/oauthorize.html
+++ b/templates/oauthorize.html
@@ -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
diff --git a/templates/profile.html b/templates/profile.html
index c9d787a..ad1d890 100644
--- a/templates/profile.html
+++ b/templates/profile.html
@@ -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>