add oauth2 token managment, partial password change, bugfixes

This commit is contained in:
TuxCoder 2020-05-26 22:55:37 +02:00
parent 6c388c8129
commit 6334c993a9
19 changed files with 236 additions and 137 deletions

View file

@ -77,6 +77,27 @@ window.fido2 = {
} }
} }
window.password_change= {
init: function(){
var form = $('form');
SimpleFormSubmit.submitForm(form.action, form)
.then(response =>{
});
}
}
window.oauth2_token = {
revoke: function(href, id){
var dialog = new ConfirmDialog(`Are you sure to revoke all tokens from client "${id}"?`);
dialog.show().then(()=>{
fetch(href, {
method: 'DELETE'
});
});
return false;
}
}
window.client_cert = { window.client_cert = {

View file

@ -1,5 +1,5 @@
from flask import current_app from flask import current_app
from .form.login import PasswordForm, TotpForm, Fido2Form from .form.auth import PasswordForm, TotpForm, Fido2Form
from ldap3 import Server, Connection from ldap3 import Server, Connection
from ldap3.core.exceptions import LDAPException from ldap3.core.exceptions import LDAPException

View file

@ -3,7 +3,7 @@ from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextField, \ from wtforms import StringField, SubmitField, TextField, \
TextAreaField, PasswordField, IntegerField, FloatField, \ TextAreaField, PasswordField, IntegerField, FloatField, \
DateTimeField, DateField, FormField, BooleanField, \ DateTimeField, DateField, FormField, BooleanField, \
SelectField, Form as NoCsrfForm SelectField, Form as NoCsrfForm, SelectMultipleField
from wtforms.widgets.html5 import NumberInput, DateInput from wtforms.widgets.html5 import NumberInput, DateInput
from wtforms.validators import DataRequired, NumberRange, Optional, NoneOf, Length from wtforms.validators import DataRequired, NumberRange, Optional, NoneOf, Length
from datetime import datetime from datetime import datetime
@ -27,3 +27,11 @@ class TotpForm(FlaskForm):
class Fido2Form(FlaskForm): class Fido2Form(FlaskForm):
fido2 = TextField(gettext('Fido2'), default="Javascript Required") fido2 = TextField(gettext('Fido2'), default="Javascript Required")
submit = SubmitField(gettext('Authorize')) submit = SubmitField(gettext('Authorize'))
class ConsentForm(FlaskForm):
# scopes = SelectMultipleField(gettext('scopes'))
# audiences = SelectMultipleField(gettext('audiences'))
remember = BooleanField(gettext('remember me'))
submit = SubmitField()

View file

@ -6,7 +6,7 @@ from wtforms import StringField, SubmitField, TextField, \
SelectField, Form as NoCsrfForm, HiddenField SelectField, Form as NoCsrfForm, HiddenField
from wtforms.widgets.html5 import NumberInput, DateInput from wtforms.widgets.html5 import NumberInput, DateInput
from wtforms.validators import DataRequired, NumberRange, \ from wtforms.validators import DataRequired, NumberRange, \
Optional, NoneOf, Length Optional, NoneOf, Length, EqualTo
class ClientCertForm(FlaskForm): class ClientCertForm(FlaskForm):
@ -34,5 +34,11 @@ class TOTPDeleteForm(FlaskForm):
submit = SubmitField(gettext('Delete')) submit = SubmitField(gettext('Delete'))
class PasswordChangeForm(FlaskForm):
old_password = PasswordField(gettext('Old Password'), validators=[DataRequired()])
password = PasswordField(gettext('New Password'), validators=[DataRequired()])
password_repeat = PasswordField(gettext('Repeat Password'), validators=[DataRequired(),EqualTo('password')])
submit = SubmitField(gettext('Change Password'))
class OidcAuthenticationConfirm(FlaskForm): class OidcAuthenticationConfirm(FlaskForm):
submit = SubmitField(gettext('Continue')) submit = SubmitField(gettext('Continue'))

View file

@ -6,24 +6,12 @@ from flask.helpers import make_response
from flask.templating import render_template from flask.templating import render_template
from oic.oic.message import TokenErrorResponse, UserInfoErrorResponse, EndSessionRequest from oic.oic.message import TokenErrorResponse, UserInfoErrorResponse, EndSessionRequest
from pyop.access_token import AccessToken, BearerTokenError
from pyop.exceptions import InvalidAuthenticationRequest, InvalidAccessToken, InvalidClientAuthentication, OAuthError, \
InvalidSubjectIdentifier, InvalidClientRegistrationRequest
from pyop.util import should_fragment_encode
from flask import Blueprint, render_template, request, url_for from flask import Blueprint, render_template, request, url_for
from flask_login import login_required, login_user, logout_user
from werkzeug.utils import redirect
import logging import logging
from urllib.parse import urlparse
from base64 import b64decode, b64encode
import ory_hydra_client as hydra
from requests_oauthlib.oauth2_session import OAuth2Session
import requests import requests
from ..model import User, SecurityUser from ..model import User
from ..model_db import User as DbUser from ..model_db import User as DbUser
from ..form.login import LoginForm
from ..auth_providers import LdapAuthProvider from ..auth_providers import LdapAuthProvider
@ -37,8 +25,9 @@ def userinfo():
user_db = DbUser.query.get(token_info.sub) user_db = DbUser.query.get(token_info.sub)
user = User.query().by_username(user_db.username) user = User.query().by_username(user_db.username)
public_url = current_app.config.get('HYDRA_PUBLIC_URL')
r = requests.get( r = requests.get(
"http://127.0.0.1:4444/userinfo", f"{public_url}/userinfo",
headers={ headers={
'authorization': request.headers['authorization']}) 'authorization': request.headers['authorization']})
userinfo = r.json() userinfo = r.json()

View file

@ -4,19 +4,11 @@ from urllib.parse import urlencode, parse_qs
import flask import flask
from flask import Blueprint, redirect from flask import Blueprint, redirect
from flask import current_app, session from flask import current_app, session
from flask import jsonify
from flask.helpers import make_response
from flask.templating import render_template from flask.templating import render_template
from oic.oic.message import TokenErrorResponse, UserInfoErrorResponse, EndSessionRequest from flask_babel import gettext
from pyop.access_token import AccessToken, BearerTokenError from flask import request, url_for
from pyop.exceptions import InvalidAuthenticationRequest, InvalidAccessToken, InvalidClientAuthentication, OAuthError, \
InvalidSubjectIdentifier, InvalidClientRegistrationRequest
from pyop.util import should_fragment_encode
from flask import Blueprint, render_template, request, url_for
from flask_login import login_required, login_user, logout_user from flask_login import login_required, login_user, logout_user
from werkzeug.utils import redirect
import logging import logging
from urllib.parse import urlparse from urllib.parse import urlparse
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
@ -24,35 +16,48 @@ import http
from ..model import User, SecurityUser from ..model import User, SecurityUser
from ..model_db import db, User as DbUser from ..model_db import db, User as DbUser
from ..form.login import LoginForm from ..form.auth import ConsentForm, LoginForm
from ..auth_providers import AUTH_PROVIDER_LIST from ..auth_providers import AUTH_PROVIDER_LIST
auth_views = Blueprint('auth', __name__, url_prefix='/auth') auth_views = Blueprint('auth', __name__, url_prefix='/auth')
@auth_views.route('/consent', methods=['GET', 'POST']) @auth_views.route('/consent', methods=['GET', 'POST'])
def consent(): def consent():
"""Always grant consent.""" """Always grant consent."""
# DUMMPY ONLY # DUMMPY ONLY
remember_me = True form = ConsentForm()
remember_for = 60*60*24*7 # remember for 7 days remember_for = 60*60*24*7 # remember for 7 days
consent_request = current_app.hydra_api.get_consent_request(request.args['consent_challenge']) consent_request = current_app.hydra_api.get_consent_request(
request.args['consent_challenge'])
requested_scope = consent_request.requested_scope requested_scope = consent_request.requested_scope
resp = current_app.hydra_api.accept_consent_request(consent_request.challenge, body={ requested_audiences = consent_request.requested_access_token_audience
'grant_scope': requested_scope,
'remember': remember_me, if form.validate_on_submit() or consent_request.skip:
'remember_for': remember_for, resp = current_app.hydra_api.accept_consent_request(
}) consent_request.challenge, body={
return redirect(resp.redirect_to) 'grant_scope': requested_scope,
'grant_access_token_audience': requested_audiences,
'remember': form.data['remember'],
# 'remember_for': remember_for,
})
return redirect(resp.redirect_to)
return render_template(
'auth/consent.html.j2',
form=form,
client=consent_request.client,
requested_scope=requested_scope,
requested_audiences=requested_audiences)
@auth_views.route('/login', methods=['GET', 'POST']) @auth_views.route('/login', methods=['GET', 'POST'])
def login(): def login():
login_challenge = request.args.get('login_challenge') login_challenge = request.args.get('login_challenge')
login_request = current_app.hydra_api.get_login_request(login_challenge) login_request = current_app.hydra_api.get_login_request(login_challenge)
if login_request.skip: if login_request.skip:
resp = current_app.hydra_api.accept_login_request( resp = current_app.hydra_api.accept_login_request(
login_challenge, login_challenge,
@ -66,8 +71,9 @@ def login():
else: else:
session['user'] = None session['user'] = None
session['auth_providers'] = [] session['auth_providers'] = []
return redirect(url_for('auth.login_auth', login_challenge=login_challenge)) return redirect(
return render_template('frontend/login.html.j2', form=form) url_for('auth.login_auth', login_challenge=login_challenge))
return render_template('auth/login.html.j2', form=form)
@auth_views.route('/login/auth', methods=['GET', 'POST']) @auth_views.route('/login/auth', methods=['GET', 'POST'])
@ -114,7 +120,7 @@ def login_auth():
except TypeError: except TypeError:
_next = None _next = None
return redirect(_next or url_for('frontend.index')) return redirect(_next or url_for('frontend.index'))
return render_template('frontend/login_auth.html.j2', forms=auth_forms) return render_template('auth/login_auth.html.j2', forms=auth_forms)
@auth_views.route("/logout") @auth_views.route("/logout")
@ -122,6 +128,7 @@ def logout():
logout_challenge = request.args.get('logout_challenge') logout_challenge = request.args.get('logout_challenge')
logout_request = current_app.hydra_api.get_logout_request(logout_challenge) logout_request = current_app.hydra_api.get_logout_request(logout_challenge)
resp = current_app.hydra_api.accept_logout_request(logout_challenge) resp = current_app.hydra_api.accept_logout_request(logout_challenge)
logout_user()
return redirect(resp.redirect_to) return redirect(resp.redirect_to)

View file

@ -27,8 +27,7 @@ from flask_dance.consumer import OAuth2ConsumerBlueprint
from ..model import User, SecurityUser, Totp from ..model import User, SecurityUser, Totp
from ..model_db import OAuth, db, User as DbUser from ..model_db import OAuth, db, User as DbUser
from ..form.login import LoginForm from ..form.frontend import ClientCertForm, TOTPForm, TOTPDeleteForm, PasswordChangeForm
from ..form.frontend import ClientCertForm, TOTPForm, TOTPDeleteForm
from ..auth_providers import AUTH_PROVIDER_LIST from ..auth_providers import AUTH_PROVIDER_LIST
@ -218,3 +217,42 @@ def totp_delete(totp_name):
return jsonify({ return jsonify({
'status': 'ok'}) 'status': 'ok'})
@frontend_views.route('/password_change')
@login_required
def password_change():
form = PasswordChangeForm()
return render_template('frontend/password_change.html.j2', form=form)
@frontend_views.route('/password_change', methods=['POST'])
@login_required
def password_change_post():
form = PasswordChangeForm()
if form.validate():
return jsonify({})
return jsonify({'errors': form.errors})
@frontend_views.route('/oauth2_token')
@login_required
def oauth2_tokens():
subject = current_app.oauth.session.get('/userinfo').json()['sub']
consent_sessions = current_app.hydra_api.list_subject_consent_sessions(
subject)
return render_template('frontend/oauth2_tokens.html.j2', consent_sessions=consent_sessions)
@frontend_views.route('/oauth2_token/<client_id>', methods=['DELETE'])
@login_required
def oauth2_token_revoke(client_id: str):
subject = current_app.oauth.session.get('/userinfo').json()['sub']
current_app.hydra_api.revoke_consent_sessions(
subject,
client=client_id)
return jsonify({})

View file

@ -1,35 +1,10 @@
import flask from flask import current_app, Blueprint
from flask import Blueprint, redirect, request
from flask import current_app, session
from flask import jsonify
from flask.helpers import make_response
from flask.templating import render_template
from oic.oic.message import TokenErrorResponse, UserInfoErrorResponse, EndSessionRequest
from pyop.access_token import AccessToken, BearerTokenError
from pyop.exceptions import InvalidAuthenticationRequest, InvalidAccessToken, InvalidClientAuthentication, OAuthError, \
InvalidSubjectIdentifier, InvalidClientRegistrationRequest
from pyop.util import should_fragment_encode
from flask import Blueprint, render_template, request, url_for
from flask_login import login_required, login_user, logout_user
from werkzeug.utils import redirect
import logging
from urllib.parse import urlparse
from base64 import b64decode, b64encode
import ory_hydra_client as hydra
from requests_oauthlib.oauth2_session import OAuth2Session
import requests
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from ..model import User, SecurityUser
from ..model_db import User as DbUser
from ..form.login import LoginForm
from ..auth_providers import LdapAuthProvider
pki_views = Blueprint('pki', __name__, url_prefix='/') pki_views = Blueprint('pki', __name__, url_prefix='/')
@pki_views.route('/<service_name>.crl') @pki_views.route('/<service_name>.crl')
def crl(service_name: str): def crl(service_name: str):
service = current_app.lenticular_services[service_name] service = current_app.lenticular_services[service_name]

View file

@ -4,6 +4,7 @@ flask_babel
flask_wtf flask_wtf
flask_login flask_login
flask_sqlalchemy flask_sqlalchemy
flask_dancer
ldap3 ldap3
ldap3_orm ldap3_orm

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,45 @@
{% extends 'base.html.j2' %}
{% block body %}
<nav class="navbar navbar-dark fixed-top bg-dark flex-md-nowrap p-0 shadow">
<div class="col-xs-1"><button id="sidebarCollapse" class="btn btn-primary d-xs-block d-md-none" ><i class="fa fa-bars fa-2x"></i></button></div>
<div class="col-xs-11"><a class="navbar-brand col-xs-11 col-sm-3 col-md-2 mr-0" href="/">Lenticular Cloud</a></div>
</nav>
<div style="height: 40px"></div>
<div class="container-fluid">
<div class="row">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">&times;</button>
{{ message }}
</div>
{% endfor %}
</div>
<div class="row">
{% if current_user.is_authenticated %}
<nav class="col-md-2 d-none d-md-block bg-light sidebar fixed-top">
<div class="sidebar-sticky active">
{#<a href="/"><img alt="logo" class="container-fluid" src="/static/images/dog_main_small.png"></a>#}
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.index') }}">{{ gettext('Account') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.client_cert') }}">{{ gettext('Client Cert') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.totp') }}">{{ gettext('2FA - TOTP') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.logout') }}">{{ gettext('Logout') }}</a></li>
</div>
</nav>
{% endif %}
<main class="col-md-9 ml-sm-auto col-lg-10 px-4" role="main">
<h1>{% block title %}{% endblock %}</h1>
<div class="card">
<div class="card-body mt-5 mb-5">
<div class="tab-content">
{% block content %}{% endblock %}
</div>
</div>
</div>
</main>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends 'auth/base.html.j2' %}
{% block title %}{{ gettext('Consent') }}{% endblock %}
{% block content %}
<p>
The application "{{ client.client_id }}" requested the following scopes: {{ requested_scope }}
</p>
<p> Allow this app to access that data?</p>
{{ render_form(form) }}
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'frontend/base.html.j2' %} {% extends 'auth/base.html.j2' %}
{% block title %}{{ gettext('Login') }}{% endblock %} {% block title %}{{ gettext('Login') }}{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'frontend/base.html.j2' %} {% extends 'auth/base.html.j2' %}
{% block title %}{{ gettext('Login') }}{% endblock %} {% block title %}{{ gettext('Login') }}{% endblock %}

View file

@ -1,62 +1,8 @@
{% extends 'base.html.j2' %} {% extends 'base.html.j2' %}
{% macro show_categories(categories) -%}
<li>
{% for category in categories %}
<ul>
<a href="{{ url_for('.sorts', category_id=category.id) }}">{{ category.name_de }}</a>
{% if category.children %}
{{ show_categories(category.children) }}
{% endif %}
</ul>
{% endfor %}
</li>
{%- endmacro %}
{% block body %} {% block body %}
<div class='messages-box'>
</div>
<template id='confirm-dialog-template'>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"></h5>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger close" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary btn-ok process">Process</button>
</div>
</div>
</div>
</template>
<div class="modal fade" id="confirm-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"></h5>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<a class="btn btn-danger btn-ok">Process</a>
</div>
</div>
</div>
</div>
<nav class="navbar navbar-dark fixed-top bg-dark flex-md-nowrap p-0 shadow"> <nav class="navbar navbar-dark fixed-top bg-dark flex-md-nowrap p-0 shadow">
<div class="col-xs-1"><button id="sidebarCollapse" class="btn btn-primary d-xs-block d-md-none" ><i class="fa fa-bars fa-2x"></i></button></div> <div class="col-xs-1"><button id="sidebarCollapse" class="btn btn-primary d-xs-block d-md-none" ><i class="fa fa-bars fa-2x"></i></button></div>
<div class="col-xs-11"><a class="navbar-brand col-xs-11 col-sm-3 col-md-2 mr-0" href="/">Lenticular Cloud</a></div> <div class="col-xs-11"><a class="navbar-brand col-xs-11 col-sm-3 col-md-2 mr-0" href="/">Lenticular Cloud</a></div>
@ -74,17 +20,17 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="row"> <div class="row">
{% if current_user.is_authenticated %}
<nav class="col-md-2 d-none d-md-block bg-light sidebar fixed-top"> <nav class="col-md-2 d-none d-md-block bg-light sidebar fixed-top">
<div class="sidebar-sticky active"> <div class="sidebar-sticky active">
{#<a href="/"><img alt="logo" class="container-fluid" src="/static/images/dog_main_small.png"></a>#} {#<a href="/"><img alt="logo" class="container-fluid" src="/static/images/dog_main_small.png"></a>#}
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.index') }}">{{ gettext('Account') }}</a></li> <li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.index') }}">{{ gettext('Account') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.client_cert') }}">{{ gettext('Client Cert') }}</a></li> <li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.client_cert') }}">{{ gettext('Client Cert') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.totp') }}">{{ gettext('2FA - TOTP') }}</a></li> <li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.totp') }}">{{ gettext('2FA - TOTP') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.oauth2_tokens') }}">{{ gettext('Oauth2 Tokens') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.password_change') }}">{{ gettext('Password Change') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.logout') }}">{{ gettext('Logout') }}</a></li> <li class="nav-item"><a class="nav-link" href="{{ url_for('frontend.logout') }}">{{ gettext('Logout') }}</a></li>
</div> </div>
</nav> </nav>
{% endif %}
<main class="col-md-9 ml-sm-auto col-lg-10 px-4" role="main"> <main class="col-md-9 ml-sm-auto col-lg-10 px-4" role="main">
<h1>{% block title %}{% endblock %}</h1> <h1>{% block title %}{% endblock %}</h1>
@ -98,13 +44,6 @@
</main> </main>
</div> </div>
</div> </div>
<div class='container'>
<div class="mt-5 row justify-content-center">
<footer>
<span class="text-muted">Render Time: {{ g.request_time() }}</span> | <span class="text-muted">{{ gettext('All right reserved. &copy;') + '2020' }}</span>
</footer>
</div>
</div>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,26 @@
{% extends 'frontend/base.html.j2' %}
{% block title %}{{ gettext('Oauth2 tokens') }}{% endblock %}
{% block content %}
<table class="table">
<thead>
<th>{{ gettext('Client ID') }}</th>
<th>{{ gettext('Remember me') }}</th>
<th>{{ gettext('Created at') }}
<th>{{ gettext('Action') }}
</thead>
{% for consent_session in consent_sessions %}
<tr>
<td>{{ consent_session.consent_request.client.client_id }}</td>
<td>{{ consent_session.remember }}</td>
<td>{{ consent_session.handled_at }}</td>
<td>
<a title="{{ gettext('Revoke')}}" href="{{ url_for('.oauth2_token_revoke', client_id=consent_session.consent_request.client.client_id) }}" onclick="oauth2_token.revoke(this.href, '{{ consent_session.consent_request.client.client_id }}'); return false;"><i class="fas fa-ban"></i></a>
</td>
</tr>
{% endfor %}
{% endblock %}

View file

@ -0,0 +1,9 @@
{% extends 'frontend/base.html.j2' %}
{% block title %}{{ gettext('Password Change') }}{% endblock %}
{% block content %}
{{ render_form(form)}}
{% endblock %}

View file

@ -8,6 +8,27 @@
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
<div class='messages-box'>
</div>
<template id='confirm-dialog-template'>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"></h5>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger close" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary btn-ok process">Process</button>
</div>
</div>
</div>
</template>
{% block body %}{% endblock %} {% block body %}{% endblock %}
<script type="application/javascript" src="/static/main.js?v={{ GIT_HASH }}" ></script> <script type="application/javascript" src="/static/main.js?v={{ GIT_HASH }}" ></script>
<script type="application/javascript" > <script type="application/javascript" >