From 1662944867c998e72186bed0bfea38e06d2e27e2 Mon Sep 17 00:00:00 2001 From: Steve Shipway Date: Thu, 5 Dec 2019 13:21:50 +1300 Subject: [PATCH 1/3] Add Azure as an explicit OAuth provider --- app/models.py | 7 +++ app/oauth.py | 35 ++++++++++- .../admin_setting_authentication.html | 58 +++++++++++++++++++ app/views.py | 49 +++++++++++++++- 4 files changed, 147 insertions(+), 2 deletions(-) diff --git a/app/models.py b/app/models.py index 721231e..b5012ed 100644 --- a/app/models.py +++ b/app/models.py @@ -2078,6 +2078,13 @@ class Setting(db.Model): 'google_oauth_scope': 'openid email profile', 'google_authorize_url': 'https://accounts.google.com/o/oauth2/v2/auth', 'google_base_url': 'https://www.googleapis.com/oauth2/v3/', + 'azure_oauth_enabled': False, + 'azure_oauth_key': '', + 'azure_oauth_secret': '', + 'azure_oauth_scope': 'User.Read', + 'azure_oauth_api_url': 'https://graph.microsoft.com/v1.0/', + 'azure_oauth_token_url': 'https://login.microsoftonline.com/[tenancy]/oauth2/v2.0/token', + 'azure_oauth_authorize_url': 'https://login.microsoftonline.com/[tenancy]/oauth2/v2.0/authorize', 'oidc_oauth_enabled': False, 'oidc_oauth_key': '', 'oidc_oauth_secret': '', diff --git a/app/oauth.py b/app/oauth.py index e42bbfb..afc7858 100644 --- a/app/oauth.py +++ b/app/oauth.py @@ -74,6 +74,39 @@ def google_oauth(): return google +def azure_oauth(): + if not Setting().get('azure_oauth_enabled'): + return None + + def fetch_azure_token(): + return session.get('azure_token') + + azure = authlib_oauth_client.register( + 'azure', + client_id = Setting().get('azure_oauth_key'), + client_secret = Setting().get('azure_oauth_secret'), + api_base_url = Setting().get('azure_oauth_api_url'), + request_token_url = None, + access_token_url = Setting().get('azure_oauth_token_url'), + authorize_url = Setting().get('azure_oauth_authorize_url'), + client_kwargs={'scope': Setting().get('azure_oauth_scope')}, + fetch_token=fetch_azure_token, + ) + + @app.route('/azure/authorized') + def azure_authorized(): + session['azure_oauthredir'] = url_for('.azure_authorized', _external=True, _scheme='https') + token = azure.authorize_access_token() + if token is None: + return 'Access denied: reason=%s error=%s' % ( + request.args['error'], + request.args['error_description'] + ) + session['azure_token'] = (token) + return redirect(url_for('.login', _external=True, _scheme='https')) + + return azure + def oidc_oauth(): if not Setting().get('oidc_oauth_enabled'): return None @@ -105,4 +138,4 @@ def oidc_oauth(): session['oidc_token'] = (token) return redirect(url_for('.login')) - return oidc \ No newline at end of file + return oidc diff --git a/app/templates/admin_setting_authentication.html b/app/templates/admin_setting_authentication.html index 6e634b1..165f688 100644 --- a/app/templates/admin_setting_authentication.html +++ b/app/templates/admin_setting_authentication.html @@ -52,6 +52,7 @@
  • LDAP
  • Google OAuth
  • Github OAuth
  • +
  • Microsoft OAuth
  • OpenID Connect OAuth
  • @@ -359,6 +360,63 @@
    +
    +
    +
    +
    + + +
    + GENERAL +
    + + +
    +
    + + + +
    +
    + + + +
    +
    +
    + ADVANCE +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    +
    + +
    +
    +
    +
    + Help +

    Fill in all the fields in the left form.

    +
    +
    +
    diff --git a/app/views.py b/app/views.py index b94cbff..b869c4a 100755 --- a/app/views.py +++ b/app/views.py @@ -21,7 +21,7 @@ from werkzeug import secure_filename from .models import User, Account, AccountUser, Domain, Record, RecordEntry, Role, Server, History, Anonymous, Setting, DomainSetting, DomainTemplate, DomainTemplateRecord from app import app, login_manager, csrf from app.lib import utils -from app.oauth import github_oauth, google_oauth, oidc_oauth +from app.oauth import github_oauth, google_oauth, oidc_oauth, azure_oauth from app.decorators import admin_role_required, operator_role_required, can_access_domain, can_configure_dnssec, can_create_domain from yaml import Loader, load @@ -57,9 +57,11 @@ def register_modules(): global google global github global oidc + global azure google = google_oauth() github = github_oauth() oidc = oidc_oauth() + azure = azure_oauth() # START USER AUTHENTICATION HANDLER @@ -223,6 +225,15 @@ def github_login(): redirect_uri = url_for('github_authorized', _external=True) return github.authorize_redirect(redirect_uri) +@app.route('/azure/login') +def azure_login(): + if not Setting().get('azure_oauth_enabled') or azure is None: + logging.error('Microsoft OAuth is disabled or you have not yet reloaded the pda application after enabling.') + return abort(400) + else: + redirect_uri = url_for('azure_authorized', _external=True, _scheme='https') + return azure.authorize_redirect(redirect_uri) + @app.route('/oidc/login') def oidc_login(): if not Setting().get('oidc_oauth_enabled') or oidc is None: @@ -450,6 +461,42 @@ def login(): login_user(user, remember = False) return redirect(url_for('index')) + if 'azure_token' in session: + me = json.loads(azure.get('me').text) + + azure_username = me["userPrincipalName"] + azure_givenname = me["givenName"] + azure_familyname = me["surname"] + if "email" in me: + azure_email = me["email"] + else: + azure_email = "" + if not azure_email: + azure_email = me["userPrincipalName"] + # Handle foreign principals such as guest users + azure_email = re.sub( r"#.*$","",azure_email) + azure_username = re.sub( r"#.*$","",azure_username) + + user = User.query.filter_by(username=azure_username).first() + if not user: + user = User(username=azure_username, + plain_text_password=None, + firstname=azure_givenname, + lastname=azure_familyname, + email=azure_email) + + result = user.create_local_user() + if not result['status']: + logging.warning('Unable to create '+azure_username) + session.pop('azure_token', None) + # note: a redirect to login results in an endless loop, so render the login page instead + return render_template('login.html', saml_enabled=SAML_ENABLED, error=('User '+azure_username+' cannot be created.')) + + session['user_id'] = user.id + session['authentication_type'] = 'OAuth' + login_user(user, remember = False) + return redirect(url_for('index')) + if 'oidc_token' in session: me = json.loads(oidc.get('userinfo').text) oidc_username = me["preferred_username"] From 10ff312d950573ebe3931c56db9df064f5fa102a Mon Sep 17 00:00:00 2001 From: Steve Shipway Date: Thu, 5 Dec 2019 13:52:30 +1300 Subject: [PATCH 2/3] Azure OAuth documentation and html templates --- README.md | 23 ++++++++++++- app/models.py | 2 +- .../admin_setting_authentication.html | 34 +++++++++++++++++++ app/templates/login.html | 6 +++- 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9d99099..29709b9 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ A PowerDNS web interface with advanced features. - User access management based on domain - User activity logging - Support Local DB / SAML / LDAP / Active Directory user authentication -- Support Google / Github / OpenID OAuth +- Support Google / Github / Azure / OpenID OAuth - Support Two-factor authentication (TOTP) - Dashboard and pdns service statistics - DynDNS 2 protocol support @@ -179,3 +179,24 @@ source .env ``` eralchemy -i 'mysql://${PDA_DB_USER}:${PDA_DB_PASSWORD}@'$(docker inspect powerdns-admin-mysql|jq -jr '.[0].NetworkSettings.Networks.powerdnsadmin_default.IPAddress')':3306/powerdns_admin' -o /tmp/output.pdf ``` + +### OAuth Authentication + +#### Microsoft Azure + +To link to Azure for authentication, you need to register PowerDNS-Admin in Azure. This requires your PowerDNS-Admin web interface to use an HTTPS URL. + +* Under the Azure Active Directory, select App Registrations, and create a new one. Give it any name you want, and the Redirect URI shoule be type 'Web' and of the format https://powerdnsadmin/azure/authorized (replace the host name approriately). +* Select the newly-created registration +* On the Overview page, the Application ID is your new Client ID to use with PowerDNS-Admin +* On the Overview page, make a note of your Directory/Tenant ID - you need it for the API URLs later +* Ensure Access Tokens are enabled in the Authentication section +* Under Certificates and Secrets, create a new Client Secret. Note this secret as it is the new Client Secret to use with PowerDNS-Admin +* Under API Permissions, you need to add permissions. Add permissions for Graph API, Delegated. Add email, openid, profile, User.Read and possibly User.Read.All. You then need to grant admin approval for your organisation. + +Now you can enable the OAuth in PowerDNS-Admin. +* For the Scope, use 'User.Read openid mail profile' +* Replace the [tenantID] in the default URLs for authorize and token with your Tenant ID. +* Restart PowerDNS-Admin + +This should allow you to log in using OAuth. diff --git a/app/models.py b/app/models.py index b5012ed..611212c 100644 --- a/app/models.py +++ b/app/models.py @@ -2081,7 +2081,7 @@ class Setting(db.Model): 'azure_oauth_enabled': False, 'azure_oauth_key': '', 'azure_oauth_secret': '', - 'azure_oauth_scope': 'User.Read', + 'azure_oauth_scope': 'User.Read openid email profile', 'azure_oauth_api_url': 'https://graph.microsoft.com/v1.0/', 'azure_oauth_token_url': 'https://login.microsoftonline.com/[tenancy]/oauth2/v2.0/token', 'azure_oauth_authorize_url': 'https://login.microsoftonline.com/[tenancy]/oauth2/v2.0/authorize', diff --git a/app/templates/admin_setting_authentication.html b/app/templates/admin_setting_authentication.html index 165f688..3e3a5fa 100644 --- a/app/templates/admin_setting_authentication.html +++ b/app/templates/admin_setting_authentication.html @@ -676,6 +676,40 @@ {% endif %} // END: Github tab js + // START: Azure tab js + // update validation requirement when checkbox is togged + $('#azure_oauth_enabled').iCheck({ + checkboxClass : 'icheckbox_square-blue', + increaseArea : '20%' + }).on('ifChanged', function(e) { + var is_enabled = e.currentTarget.checked; + if (is_enabled){ + $('#azure_oauth_key').prop('required', true); + $('#azure_oauth_secret').prop('required', true); + $('#azure_oauth_scope').prop('required', true); + $('#azure_oauth_api_url').prop('required', true); + $('#azure_oauth_token_url').prop('required', true); + $('#azure_oauth_authorize_url').prop('required', true); + } else { + $('#azure_oauth_key').prop('required', false); + $('#azure_oauth_secret').prop('required', false); + $('#azure_oauth_scope').prop('required', false); + $('#azure_oauth_api_url').prop('required', false); + $('#azure_oauth_token_url').prop('required', false); + $('#azure_oauth_authorize_url').prop('required', false); + } + }); + // init validation requirement at first time page load + {% if SETTING.get('azure_oauth_enabled') %} + $('#azure_oauth_key').prop('required', true); + $('#azure_oauth_secret').prop('required', true); + $('#azure_oauth_scope').prop('required', true); + $('#azure_oauth_api_url').prop('required', true); + $('#azure_oauth_token_url').prop('required', true); + $('#azure_oauth_authorize_url').prop('required', true); + {% endif %} + // END: Azure tab js + // START: OIDC tab js $('#oidc_oauth_enabled').iCheck({ checkboxClass : 'icheckbox_square-blue', diff --git a/app/templates/login.html b/app/templates/login.html index 2208d50..fb2961a 100644 --- a/app/templates/login.html +++ b/app/templates/login.html @@ -84,13 +84,17 @@
    - {% if SETTING.get('google_oauth_enabled') or SETTING.get('github_oauth_enabled') or SETTING.get('oidc_oauth_enabled') %} + {% if SETTING.get('google_oauth_enabled') or SETTING.get('github_oauth_enabled') or SETTING.get('oidc_oauth_enabled') or SETTING.get('azure_oauth_enabled') %}