Implemented OIDC using authlib
This commit is contained in:
Chris Pritchard 2018-10-21 23:38:12 +01:00 committed by GitHub
parent 4540d9a293
commit 396ce14b9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 189 additions and 9 deletions

View file

@ -4,6 +4,7 @@ from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy as SA from flask_sqlalchemy import SQLAlchemy as SA
from flask_migrate import Migrate from flask_migrate import Migrate
from flask_oauthlib.client import OAuth from flask_oauthlib.client import OAuth
from authlib.flask.client import OAuth as AuthlibOAuth
from sqlalchemy.exc import OperationalError from sqlalchemy.exc import OperationalError
# subclass SQLAlchemy to enable pool_pre_ping # subclass SQLAlchemy to enable pool_pre_ping
@ -30,6 +31,7 @@ login_manager.init_app(app)
db = SQLAlchemy(app) # database db = SQLAlchemy(app) # database
migrate = Migrate(app, db) # flask-migrate migrate = Migrate(app, db) # flask-migrate
oauth_client = OAuth(app) # oauth oauth_client = OAuth(app) # oauth
authlib_oauth_client = AuthlibOAuth(app) # authlib oauth
if app.config.get('SAML_ENABLED') and app.config.get('SAML_ENCRYPT'): if app.config.get('SAML_ENABLED') and app.config.get('SAML_ENCRYPT'):
from app.lib import certutil from app.lib import certutil

View file

@ -1840,6 +1840,13 @@ class Setting(db.Model):
'google_token_params': {'scope': 'email profile'}, 'google_token_params': {'scope': 'email profile'},
'google_authorize_url':'https://accounts.google.com/o/oauth2/auth', 'google_authorize_url':'https://accounts.google.com/o/oauth2/auth',
'google_base_url':'https://www.googleapis.com/oauth2/v1/', 'google_base_url':'https://www.googleapis.com/oauth2/v1/',
'oidc_oauth_enabled': False,
'oidc_oauth_key': '',
'oidc_oauth_secret': '',
'oidc_oauth_scope': 'email',
'oidc_oauth_api_url': '',
'oidc_oauth_token_url': '',
'oidc_oauth_authorize_url': '',
'forward_records_allow_edit': {'A': True, 'AAAA': True, 'AFSDB': False, 'ALIAS': False, 'CAA': True, 'CERT': False, 'CDNSKEY': False, 'CDS': False, 'CNAME': True, 'DNSKEY': False, 'DNAME': False, 'DS': False, 'HINFO': False, 'KEY': False, 'LOC': True, 'MX': True, 'NAPTR': False, 'NS': True, 'NSEC': False, 'NSEC3': False, 'NSEC3PARAM': False, 'OPENPGPKEY': False, 'PTR': True, 'RP': False, 'RRSIG': False, 'SOA': False, 'SPF': True, 'SSHFP': False, 'SRV': True, 'TKEY': False, 'TSIG': False, 'TLSA': False, 'SMIMEA': False, 'TXT': True, 'URI': False}, 'forward_records_allow_edit': {'A': True, 'AAAA': True, 'AFSDB': False, 'ALIAS': False, 'CAA': True, 'CERT': False, 'CDNSKEY': False, 'CDS': False, 'CNAME': True, 'DNSKEY': False, 'DNAME': False, 'DS': False, 'HINFO': False, 'KEY': False, 'LOC': True, 'MX': True, 'NAPTR': False, 'NS': True, 'NSEC': False, 'NSEC3': False, 'NSEC3PARAM': False, 'OPENPGPKEY': False, 'PTR': True, 'RP': False, 'RRSIG': False, 'SOA': False, 'SPF': True, 'SSHFP': False, 'SRV': True, 'TKEY': False, 'TSIG': False, 'TLSA': False, 'SMIMEA': False, 'TXT': True, 'URI': False},
'reverse_records_allow_edit': {'A': False, 'AAAA': False, 'AFSDB': False, 'ALIAS': False, 'CAA': False, 'CERT': False, 'CDNSKEY': False, 'CDS': False, 'CNAME': False, 'DNSKEY': False, 'DNAME': False, 'DS': False, 'HINFO': False, 'KEY': False, 'LOC': True, 'MX': False, 'NAPTR': False, 'NS': True, 'NSEC': False, 'NSEC3': False, 'NSEC3PARAM': False, 'OPENPGPKEY': False, 'PTR': True, 'RP': False, 'RRSIG': False, 'SOA': False, 'SPF': False, 'SSHFP': False, 'SRV': False, 'TKEY': False, 'TSIG': False, 'TLSA': False, 'SMIMEA': False, 'TXT': True, 'URI': False}, 'reverse_records_allow_edit': {'A': False, 'AAAA': False, 'AFSDB': False, 'ALIAS': False, 'CAA': False, 'CERT': False, 'CDNSKEY': False, 'CDS': False, 'CNAME': False, 'DNSKEY': False, 'DNAME': False, 'DS': False, 'HINFO': False, 'KEY': False, 'LOC': True, 'MX': False, 'NAPTR': False, 'NS': True, 'NSEC': False, 'NSEC3': False, 'NSEC3PARAM': False, 'OPENPGPKEY': False, 'PTR': True, 'RP': False, 'RRSIG': False, 'SOA': False, 'SPF': False, 'SSHFP': False, 'SRV': False, 'TKEY': False, 'TSIG': False, 'TLSA': False, 'SMIMEA': False, 'TXT': True, 'URI': False},
} }

View file

@ -1,7 +1,7 @@
from ast import literal_eval from ast import literal_eval
from flask import request, session, redirect, url_for from flask import request, session, redirect, url_for
from app import app, oauth_client from app import app, oauth_client, authlib_oauth_client
from app.models import Setting from app.models import Setting
# TODO: # TODO:
@ -75,3 +75,36 @@ def google_oauth():
return session.get('google_token') return session.get('google_token')
return google return google
def oidc_oauth():
if not Setting().get('oidc_oauth_enabled'):
return None
def fetch_oidc_token():
return session.get('oidc_token')
oidc = authlib_oauth_client.register(
'oidc',
client_id = Setting().get('oidc_oauth_key'),
client_secret = Setting().get('oidc_oauth_secret'),
api_base_url = Setting().get('oidc_oauth_api_url'),
request_token_url = None,
access_token_url = Setting().get('oidc_oauth_token_url'),
authorize_url = Setting().get('oidc_oauth_authorize_url'),
client_kwargs={'scope': Setting().get('oidc_oauth_scope')},
fetch_token=fetch_oidc_token,
)
@app.route('/oidc/authorized')
def oidc_authorized():
session['oidc_oauthredir'] = url_for('.oidc_authorized', _external=True)
token = oidc.authorize_access_token()
if token is None:
return 'Access denied: reason=%s error=%s' % (
request.args['error'],
request.args['error_description']
)
session['oidc_token'] = (token)
return redirect(url_for('.login'))
return oidc

View file

@ -37,6 +37,7 @@
<li class="active"><a href="#tabs-ldap" data-toggle="tab">LDAP</a></li> <li class="active"><a href="#tabs-ldap" data-toggle="tab">LDAP</a></li>
<li><a href="#tabs-google" data-toggle="tab">Google OAuth</a></li> <li><a href="#tabs-google" data-toggle="tab">Google OAuth</a></li>
<li><a href="#tabs-github" data-toggle="tab">Github OAuth</a></li> <li><a href="#tabs-github" data-toggle="tab">Github OAuth</a></li>
<li><a href="#tabs-oidc" data-toggle="tab">OpenID Connect OAuth</a></li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane active" id="tabs-general"> <div class="tab-pane active" id="tabs-general">
@ -325,8 +326,63 @@
<legend>Help</legend> <legend>Help</legend>
<p>Fill in all the fields in the left form.</p> <p>Fill in all the fields in the left form.</p>
</div> </div>
</div>
</div>
<div class="tab-pane" id="tabs-oidc">
<div class="row">
<div class="col-md-4">
<form role="form" method="post" data-toggle="validator">
<input type="hidden" value="oidc" name="config_tab" />
<fieldset>
<legend>GENERAL</legend>
<div class="form-group">
<input type="checkbox" id="oidc_oauth_enabled" name="oidc_oauth_enabled" class="checkbox" {% if SETTING.get('oidc_oauth_enabled') %}checked{% endif %}>
<label for="oidc_oauth_enabled">Enable OpenID Connect OAuth</label>
</div>
<div class="form-group">
<label for="oidc_oauth_key">Client key</label>
<input type="text" class="form-control" name="oidc_oauth_key" id="oidc_oauth_key" placeholder="OIDC OAuth client ID" data-error="Please input Client key" value="{{ SETTING.get('oidc_oauth_key') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="oidc_oauth_secret">Client secret</label>
<input type="text" class="form-control" name="oidc_oauth_secret" id="oidc_oauth_secret" placeholder="OIDC OAuth client secret" data-error="Please input Client secret" value="{{ SETTING.get('oidc_oauth_secret') }}">
<span class="help-block with-errors"></span>
</div>
</fieldset>
<fieldset>
<legend>ADVANCE</legend>
<div class="form-group">
<label for="oidc_oauth_scope">Scope</label>
<input type="text" class="form-control" name="oidc_oauth_scope" id="oidc_oauth_scope" placeholder="e.g. email" data-error="Please input scope" value="{{ SETTING.get('oidc_oauth_scope') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="oidc_oauth_api_url">API URL</label>
<input type="text" class="form-control" name="oidc_oauth_api_url" id="oidc_oauth_api_url" placeholder="e.g. https://api.oidc.com/user" data-error="Please input API URL" value="{{ SETTING.get('oidc_oauth_api_url') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="oidc_oauth_token_url">Token URL</label>
<input type="text" class="form-control" name="oidc_oauth_token_url" id="oidc_oauth_token_url" placeholder="e.g. https://oidc.com/login/oauth/access_token" data-error="Please input Token URL" value="{{ SETTING.get('oidc_oauth_token_url') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="oidc_oauth_authorize_url">Authorize URL</label>
<input type="text" class="form-control" name="oidc_oauth_authorize_url" id="oidc_oauth_authorize_url" placeholder="e.g. https://oidc.com/login/oauth/authorize" data-error="Plesae input Authorize URL" value="{{ SETTING.get('oidc_oauth_authorize_url') }}">
<span class="help-block with-errors"></span>
</div>
</fieldset>
<div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Save</button>
</div>
</form>
</div>
<div class="col-md-8">
<legend>Help</legend>
<p>Fill in all the fields in the left form.</p>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@ -487,9 +543,8 @@
$('#github_oauth_authorize_url').prop('required', false); $('#github_oauth_authorize_url').prop('required', false);
} }
}); });
// init validation requirement at first time page load // init validation requirement at first time page load
{% if SETTING.get('google_oauth_enabled') %} {% if SETTING.get('github_oauth_enabled') %}
$('#github_oauth_key').prop('required', true); $('#github_oauth_key').prop('required', true);
$('#github_oauth_secret').prop('required', true); $('#github_oauth_secret').prop('required', true);
$('#github_oauth_scope').prop('required', true); $('#github_oauth_scope').prop('required', true);
@ -499,5 +554,38 @@
{% endif %} {% endif %}
// END: Github tab js // END: Github tab js
// START: OIDC tab js
$('#oidc_oauth_enabled').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
}).on('ifChanged', function(e) {
var is_enabled = e.currentTarget.checked;
if (is_enabled){
$('#oidc_oauth_key').prop('required', true);
$('#oidc_oauth_secret').prop('required', true);
$('#oidc_oauth_scope').prop('required', true);
$('#oidc_oauth_api_url').prop('required', true);
$('#oidc_oauth_token_url').prop('required', true);
$('#oidc_oauth_authorize_url').prop('required', true);
} else {
$('#oidc_oauth_key').prop('required', false);
$('#oidc_oauth_secret').prop('required', false);
$('#oidc_oauth_scope').prop('required', false);
$('#oidc_oauth_api_url').prop('required', false);
$('#oidc_oauth_token_url').prop('required', false);
$('#oidc_oauth_authorize_url').prop('required', false);
}
});
// init validation requirement at first time page load
{% if SETTING.get('oidc_oauth_enabled') %}
$('#oidc_oauth_key').prop('required', true);
$('#oidc_oauth_secret').prop('required', true);
$('#oidc_oauth_scope').prop('required', true);
$('#oidc_oauth_api_url').prop('required', true);
$('#oidc_oauth_token_url').prop('required', true);
$('#oidc_oauth_authorize_url').prop('required', true);
{% endif %}
//END: OIDC Tab JS
</script> </script>
{% endblock %} {% endblock %}

View file

@ -83,14 +83,17 @@
<!-- /.col --> <!-- /.col -->
</div> </div>
</form> </form>
{% if SETTING.get('google_oauth_enabled') or SETTING.get('github_oauth_enabled') %} {% if SETTING.get('google_oauth_enabled') or SETTING.get('github_oauth_enabled') or SETTING.get('oidc_oauth_enabled') %}
<div class="social-auth-links text-center"> <div class="social-auth-links text-center">
<p>- OR -</p> <p>- OR -</p>
{% if SETTING.get('oidc_oauth_enabled') %}
<a href="{{ url_for('oidc_login') }}" class="btn btn-block btn-social btn-openid btn-flat"><i class="fa fa-openid"></i> Sign in using
OpenID Connect</a>
{% endif %}
{% if SETTING.get('github_oauth_enabled') %} {% if SETTING.get('github_oauth_enabled') %}
<a href="{{ url_for('github_login') }}" class="btn btn-block btn-social btn-github btn-flat"><i class="fa fa-github"></i> Sign in using <a href="{{ url_for('github_login') }}" class="btn btn-block btn-social btn-github btn-flat"><i class="fa fa-github"></i> Sign in using
Github</a> Github</a>
{% endif %} {% endif %}
{% if SETTING.get('google_oauth_enabled') %} {% if SETTING.get('google_oauth_enabled') %}
<a href="{{ url_for('google_login') }}" class="btn btn-block btn-social btn-google btn-flat"><i class="fa fa-google-plus"></i> Sign in using <a href="{{ url_for('google_login') }}" class="btn btn-block btn-social btn-google btn-flat"><i class="fa fa-google-plus"></i> Sign in using
Google</a> Google</a>

View file

@ -4,6 +4,7 @@ import os
import traceback import traceback
import re import re
import datetime import datetime
import json
from distutils.util import strtobool from distutils.util import strtobool
from distutils.version import StrictVersion from distutils.version import StrictVersion
from functools import wraps from functools import wraps
@ -19,7 +20,7 @@ from werkzeug import secure_filename
from .models import User, Account, Domain, Record, Role, Server, History, Anonymous, Setting, DomainSetting, DomainTemplate, DomainTemplateRecord from .models import User, Account, Domain, Record, Role, Server, History, Anonymous, Setting, DomainSetting, DomainTemplate, DomainTemplateRecord
from app import app, login_manager from app import app, login_manager
from app.lib import utils from app.lib import utils
from app.oauth import github_oauth, google_oauth from app.oauth import github_oauth, google_oauth, oidc_oauth
from app.decorators import admin_role_required, operator_role_required, can_access_domain, can_configure_dnssec, can_create_domain from app.decorators import admin_role_required, operator_role_required, can_access_domain, can_configure_dnssec, can_create_domain
if app.config['SAML_ENABLED']: if app.config['SAML_ENABLED']:
@ -53,8 +54,10 @@ def inject_setting():
def register_modules(): def register_modules():
global google global google
global github global github
global oidc
google = google_oauth() google = google_oauth()
github = github_oauth() github = github_oauth()
oidc = oidc_oauth()
# START USER AUTHENTICATION HANDLER # START USER AUTHENTICATION HANDLER
@ -171,6 +174,15 @@ def github_login():
else: else:
return github.authorize(callback=url_for('github_authorized', _external=True)) return github.authorize(callback=url_for('github_authorized', _external=True))
@app.route('/oidc/login')
def oidc_login():
if not Setting().get('oidc_oauth_enabled') or oidc is None:
logging.error('OIDC OAuth is disabled or you have not yet reloaded the pda application after enabling.')
return abort(400)
else:
redirect_uri = url_for('oidc_authorized', _external=True)
return oidc.authorize_redirect(redirect_uri)
@app.route('/saml/login') @app.route('/saml/login')
def saml_login(): def saml_login():
@ -341,6 +353,31 @@ def login():
login_user(user, remember = False) login_user(user, remember = False)
return redirect(url_for('index')) return redirect(url_for('index'))
if 'oidc_token' in session:
me = json.loads(oidc.get('userinfo').text)
oidc_username = me["preferred_username"]
oidc_givenname = me["name"]
oidc_familyname = ""
oidc_email = me["email"]
user = User.query.filter_by(username=oidc_username).first()
if not user:
user = User(username=oidc_username,
plain_text_password=None,
firstname=oidc_givenname,
lastname=oidc_familyname,
email=oidc_email)
result = user.create_local_user()
if not result['status']:
session.pop('oidc_token', None)
return redirect(url_for('login'))
session['user_id'] = user.id
session['authentication_type'] = 'OAuth'
login_user(user, remember = False)
return redirect(url_for('index'))
if request.method == 'GET': if request.method == 'GET':
return render_template('login.html', saml_enabled=SAML_ENABLED) return render_template('login.html', saml_enabled=SAML_ENABLED)
@ -1508,6 +1545,15 @@ def admin_setting_authentication():
Setting().set('github_oauth_token_url', request.form.get('github_oauth_token_url')) Setting().set('github_oauth_token_url', request.form.get('github_oauth_token_url'))
Setting().set('github_oauth_authorize_url', request.form.get('github_oauth_authorize_url')) Setting().set('github_oauth_authorize_url', request.form.get('github_oauth_authorize_url'))
result = {'status': True, 'msg': 'Saved successfully. Please reload PDA to take effect.'} result = {'status': True, 'msg': 'Saved successfully. Please reload PDA to take effect.'}
elif conf_type == 'oidc':
Setting().set('oidc_oauth_enabled', True if request.form.get('oidc_oauth_enabled') else False)
Setting().set('oidc_oauth_key', request.form.get('oidc_oauth_key'))
Setting().set('oidc_oauth_secret', request.form.get('oidc_oauth_secret'))
Setting().set('oidc_oauth_scope', request.form.get('oidc_oauth_scope'))
Setting().set('oidc_oauth_api_url', request.form.get('oidc_oauth_api_url'))
Setting().set('oidc_oauth_token_url', request.form.get('oidc_oauth_token_url'))
Setting().set('oidc_oauth_authorize_url', request.form.get('oidc_oauth_authorize_url'))
result = {'status': True, 'msg': 'Saved successfully. Please reload PDA to take effect.'}
else: else:
return abort(400) return abort(400)

View file

@ -9,7 +9,7 @@ mysqlclient==1.3.12
configobj==5.0.6 configobj==5.0.6
bcrypt==3.1.4 bcrypt==3.1.4
requests==2.18.4 requests==2.18.4
python-ldap==3.0.0 python-ldap==3.1.0
pyotp==2.2.6 pyotp==2.2.6
qrcode==6.0 qrcode==6.0
dnspython==1.15.0 dnspython==1.15.0
@ -19,3 +19,4 @@ pyOpenSSL>=0.15
pytz>=2017.3 pytz>=2017.3
cssmin==0.2.0 cssmin==0.2.0
jsmin==2.2.2 jsmin==2.2.2
Authlib==0.10

2
run.py
View file

@ -4,4 +4,4 @@ from config import PORT
from config import BIND_ADDRESS from config import BIND_ADDRESS
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug = True, host=BIND_ADDRESS, port=PORT) app.run(debug = True, host=BIND_ADDRESS, port=PORT, use_reloader=False)