Add support for Microsoft login

Microsoft login (or Azure AD) nearly supports the OpenID Connect
protocol.  With extensive configuration and some additional customization
of the values expected in the tokens, it will work with Zuul.

Add the necessary additional configuration and a HOWTO with the
procedure.

Change-Id: I445a0494796572762fbf78e580d7d6dd17be7239
This commit is contained in:
James E. Blair 2022-02-11 10:51:33 -08:00
parent ad1351c225
commit 5826a2a6c8
8 changed files with 151 additions and 9 deletions

View File

@ -151,4 +151,5 @@ authentication in Zuul and Zuul's Web UI.
howtos/openid-with-google
howtos/openid-with-keycloak
howtos/openid-with-microsoft
tutorials/keycloak

View File

@ -992,6 +992,28 @@ authentication on Zuul's web user interface.
The well-known configuration of the Identity Provider should provide this URL
under the key "jwks_uri", therefore this attribute is usually not necessary.
Some providers may not conform to the JWT specification and further
configuration may be necessary. In these cases, the following
additional values may be used:
.. attr:: authority
:default: issuer_id
If the authority in the token response is not the same as the
issuer_id in the request, it may be explicitly set here.
.. attr:: audience
:default: client_id
If the audience in the token response is not the same as the
issuer_id in the request, it may be explicitly set here.
.. attr:: load_user_info
:default: true
If the web UI should skip accessing the "UserInfo" endpoint and
instead rely only on the information returned in the token, set
this to ``false``.
Client
------

View File

@ -0,0 +1,84 @@
Configuring Microsoft Authentication
====================================
This document explains how to configure Zuul in order to enable
authentication with Microsoft Login.
Prerequisites
-------------
* The Zuul instance must be able to query Microsoft's OAUTH API servers. This
simply generally means that the Zuul instance must be able to send and
receive HTTPS data to and from the Internet.
* You must have an Active Directory instance in Azure and the ability
to create an App Registration.
By convention, we will assume Zuul's Web UI's base URL is
``https://zuul.example.com/``.
Creating the App Registration
-----------------------------
Navigate to the Active Directory instance in Azure and select `App
registrations` under ``Manage``. Select ``New registration``. This
will open a dialog to register an application.
Enter a name of your choosing (e.g., ``Zuul``), and select which
account types should have access. Under ``Redirect URI`` select
``Single-page application(SPA)`` and enter
``https://zuul.example.com/auth_callback`` as the redirect URI. Press
the ``Register`` button.
You should now be at the overview of the Zuul App registration. This
page displays several values which will be used later. Record the
``Application (client) ID`` and ``Directory (tenant) ID``. When we need
to construct values including these later, we will refer to them with
all caps (e.g., ``CLIENT_ID`` and ``TENANT_ID`` respectively).
Select ``Authentication`` under ``Manage``. You should see a
``Single-page application`` section with the redirect URI previously
configured during registration; if not, correct that now.
Under ``Implicit grant and hybrid flows`` select both ``Access
tokens`` and ``ID tokens``, then Save.
Back at the Zuul App Registration menu, select ``Expose an API``, then
press ``Set`` and then press ``Save`` to accept the default
Application ID URI (it should look like ``api://CLIENT_ID``).
Press ``Add a scope`` and enter ``zuul`` as the scope name. Enter
``Access zuul`` for both the ``Admin consent display name`` and
``Admin consent description``. Leave ``Who can consent`` set to
``Admins only``, then press ``Add scope``.
Optional: Include Groups Claim
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In order to include group information in the token sent to Zuul,
select ``Token configuration`` under ``Manage`` and then ``Add groups
claim``.
Setting up Zuul
---------------
Edit the ``/etc/zuul/zuul.conf`` to add the microsoft authenticator:
.. code-block:: ini
[auth microsoft]
default=true
driver=OpenIDConnect
realm=zuul.example.com
authority=https://login.microsoftonline.com/TENANT_ID/v2.0
issuer_id=https://sts.windows.net/TENANT_ID/
client_id=CLIENT_ID
scope=openid profile api://CLIENT_ID/zuul
audience=api://CLIENT_ID
load_user_info=false
Restart Zuul services (scheduler, web).
Head to your tenant's status page. If all went well, you should see a
`Sign in` button in the upper right corner of the
page. Congratulations!

View File

@ -0,0 +1,5 @@
---
features:
- |
The ability to configure Microsoft Login as an OpenID Connect
authentication provider has been added.

View File

@ -1312,6 +1312,7 @@ class TestWebCapabilitiesInfo(TestInfo):
'type': 'JWT',
'scope': 'openid profile',
'driver': 'OpenIDConnect',
'load_user_info': True,
},
'myOIDC2': {
'authority': 'http://oidc2',
@ -1319,6 +1320,7 @@ class TestWebCapabilitiesInfo(TestInfo):
'type': 'JWT',
'scope': 'openid profile email special-scope',
'driver': 'OpenIDConnect',
'load_user_info': True,
},
'zuul.example.com': {
'authority': 'zuul_operator',

View File

@ -36,6 +36,7 @@ function createAuthParamsFromJson(json) {
authority: '',
client_id: '',
scope: '',
loadUserInfo: true,
}
if (!auth_info) {
console.log('No auth config')
@ -47,6 +48,7 @@ function createAuthParamsFromJson(json) {
auth_params.client_id = client_config.client_id
auth_params.scope = client_config.scope
auth_params.authority = client_config.authority
auth_params.loadUserInfo = client_config.load_user_info
return auth_params
} else {
console.log('No OpenIDConnect provider found')

View File

@ -26,6 +26,7 @@ let auth_params = {
authority: '',
client_id: '',
scope: '',
loadUserInfo: true,
}
if (stored_params !== null) {
auth_params = JSON.parse(stored_params)

View File

@ -22,10 +22,34 @@ from urllib.parse import urljoin
from zuul import exceptions
from zuul.driver import AuthenticatorInterface
from zuul.lib.config import any_to_bool
logger = logging.getLogger("zuul.auth.jwt")
# A few notes on differences between the OpenID Connect specification
# and OIDC as implemented by Microsoft, which necessitates several
# extra configuration options:
#
# 1) The issuer (iss) returned in the JWT is not the authority, but
# rather a URL referring to the AD instance, therefore authority must
# be specified separately.
# 2) The audience (aud) returned in the JWT is not the client_id, but
# rather a URL constructed from the client_id, therefore must also be
# specified separately.
# 3) By default, the JWT is simply a forwarded copy of a token from
# the Microsoft Graph service. It is signed by the graph service and
# not the authority as requested, it therefore fails signature
# validation. In order to cause the authority to generate its own
# token signed by the expected keys, we must create a new scope
# (api://.../zuul) and request that scope with the token.
# 4) The userinfo service referred to by the JWT is the Microsoft
# Graph service, which means that once our token is signed by the
# Microsoft login keys (see item #3) the graph service is then unable
# to validate the token. Therefore we must configure the javascript
# oidc library not to request userinfo and rely only on what is
# supplied in the token.
class JWTAuthenticator(AuthenticatorInterface):
"""The base class for JWT-based authentication."""
@ -34,19 +58,17 @@ class JWTAuthenticator(AuthenticatorInterface):
# Common configuration for all authenticators
self.uid_claim = conf.get('uid_claim', 'sub')
self.issuer_id = conf.get('issuer_id')
self.audience = conf.get('client_id')
self.authority = conf.get('authority', self.issuer_id)
self.client_id = conf.get('client_id')
self.audience = conf.get('audience', self.client_id)
self.realm = conf.get('realm')
self.allow_authz_override = conf.get('allow_authz_override', False)
self.allow_authz_override = any_to_bool(
conf.get('allow_authz_override', False))
try:
self.skew = int(conf.get('skew', 0))
except Exception:
raise ValueError(
'skew must be an integer, got %s' % conf.get('skew'))
if isinstance(self.allow_authz_override, str):
if self.allow_authz_override.lower() == 'true':
self.allow_authz_override = True
else:
self.allow_authz_override = False
try:
self.max_validity_time = float(conf.get('max_validity_time',
math.inf))
@ -56,8 +78,8 @@ class JWTAuthenticator(AuthenticatorInterface):
def get_capabilities(self):
return {
self.realm: {
'authority': self.issuer_id,
'client_id': self.audience,
'authority': self.authority,
'client_id': self.client_id,
'type': 'JWT',
'driver': getattr(self, 'name', 'N/A'),
}
@ -190,6 +212,8 @@ class OpenIDConnectAuthenticator(JWTAuthenticator):
super(OpenIDConnectAuthenticator, self).__init__(**conf)
self.keys_url = conf.get('keys_url', None)
self.scope = conf.get('scope', 'openid profile')
self.load_user_info = any_to_bool(
conf.get('load_user_info', True))
def get_key(self, key_id):
keys_url = self.keys_url
@ -235,6 +259,7 @@ class OpenIDConnectAuthenticator(JWTAuthenticator):
def get_capabilities(self):
d = super(OpenIDConnectAuthenticator, self).get_capabilities()
d[self.realm]['scope'] = self.scope
d[self.realm]['load_user_info'] = self.load_user_info
return d
def _decode(self, rawToken):