Commit 77837526 authored by Sjoerd Simons's avatar Sjoerd Simons Committed by Igor Ponomarev
Browse files

Add support for Open ID Connect (OIDC) identity provider

Open ID Connect is an authentication protocol build on top
of OAuth2. It is used for example in Azure AD and Keycloack

django-allauth unfortunately does not support the generic OIDC.

However, Mozzila developed a OIDC library for django called
`mozilla-django-oidc`

The integration is entirely optional and nothing is imported
from `mozilla-django-oidc` unless it is activated with `AUTH_OIDC`
key in YAML configuration files.
parent a351ec86
......@@ -131,3 +131,35 @@ need to be performed (following example covers `GitLab OAuth2 authentication`_):
.. note:: If SMTP is not set up in LAVA, you can get a 500 Internal server
error. Login will still work despite the error.
Using Open ID Connect (OIDC) authentication providers
-----------------------------------------------------
LAVA server can be configured to authenticate using OIDC providers
such as Keycloack or Azure AD. The OIDC library used is
`mozilla-django-oidc <https://github.com/mozilla/mozilla-django-oidc>`_.
The library does not come pre-installed and must be installed through
external means. (for example, with ``pip``)
To enable OIDC authorization set ``AUTH_OIDC`` dictionary in one of the
configuration files.
Example::
---
AUTH_OIDC:
OIDC_RP_CLIENT_ID: "1"
OIDC_RP_CLIENT_SECRET: "bd01adf93cfb"
OIDC_OP_AUTHORIZATION_ENDPOINT: "http://testprovider:8080/openid/authorize"
OIDC_OP_TOKEN_ENDPOINT: "http://testprovider:8080/openid/token"
OIDC_OP_USER_ENDPOINT: "http://testprovider:8080/openid/userinfo"
See `mozilla-django-oidc settings <https://mozilla-django-oidc.readthedocs.io/en/stable/settings.html>`_
for the list of configuration keys.
One extra setting that LAVA provides is ``LAVA_OIDC_ACCOUNT_NAME``
which sets the login message for OIDC login prompt. For example,
it can be set to ``Azure AD account``. By default it is set to
``Open ID Connect account``.
......@@ -48,6 +48,13 @@ def ldap_available(request):
return {"ldap_available": ldap_enabled, "login_message_ldap": login_message_ldap}
def oidc_context(request):
return {
"oidc_enabled": settings.OIDC_ENABLED,
"oidc_account_name": settings.LAVA_OIDC_ACCOUNT_NAME,
}
def socialaccount(request):
return {
"socialaccount_enabled": settings.AUTH_SOCIALACCOUNT is not None,
......
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
import logging
class OIDCAuthenticationBackendUsernameFromEmail(OIDCAuthenticationBackend):
def create_user(self, claims):
# TODO try multiple options and fallbacks
logger = logging.getLogger("mozilla_django_oidc")
email = claims.get("email")
logger.info(f"Creating new user for e-mail: {email}")
# On Azure AD the username is not part of the claims
username = email.split("@")[0]
return self.UserModel.objects.create_user(username, email=email)
......@@ -28,6 +28,11 @@ class LavaRequireLoginMiddleware:
HOME_PATH: ClassVar[PurePosixPath] = PurePosixPath("/") / settings.MOUNT_POINT
LOGIN_PATH: ClassVar[PurePosixPath] = PurePosixPath(settings.LOGIN_URL)
SCHEDULER_INTERNALS_PATH: ClassVar[PurePosixPath] = (
HOME_PATH / "scheduler/internal/v1"
)
OIDC_PATH: ClassVar[PurePosixPath] = HOME_PATH / "oidc"
def __init__(self, get_response):
self.get_response = get_response
self.require_login = login_required(get_response)
......@@ -40,6 +45,21 @@ class LavaRequireLoginMiddleware:
if path == cls.LOGIN_PATH:
return True
try:
path.relative_to(cls.SCHEDULER_INTERNALS_PATH)
except ValueError:
...
else:
return True
if settings.OIDC_ENABLED:
try:
path.relative_to(cls.OIDC_PATH)
except ValueError:
...
else:
return True
return False
def __call__(self, request):
......
......@@ -114,6 +114,7 @@ TEMPLATES = [
# LAVA context processors
"lava_server.context_processors.lava",
"lava_server.context_processors.ldap_available",
"lava_server.context_processors.oidc_context",
"lava_server.context_processors.socialaccount",
]
},
......@@ -231,6 +232,11 @@ AUTH_LDAP_GROUP_TYPE = None
# Social accounts support
AUTH_SOCIALACCOUNT = None
# OIDC support
AUTH_OIDC = None
OIDC_ENABLED = False
LAVA_OIDC_ACCOUNT_NAME = "Open ID Connect account"
# Gitlab support
AUTH_GITLAB_URL = None
AUTH_GITLAB_SCOPE = ["read_user"]
......@@ -337,6 +343,7 @@ def update(values):
AUTH_LDAP_USER_SEARCH = values.get("AUTH_LDAP_USER_SEARCH")
AUTH_DEBIAN_SSO = values.get("AUTH_DEBIAN_SSO")
AUTH_SOCIALACCOUNT = values.get("AUTH_SOCIALACCOUNT")
AUTH_OIDC = values.get("AUTH_OIDC")
AUTH_GITLAB_URL = values.get("AUTH_GITLAB_URL")
AUTH_GITLAB_SCOPE = values.get("AUTH_GITLAB_SCOPE")
AUTHENTICATION_BACKENDS = values.get("AUTHENTICATION_BACKENDS")
......@@ -412,6 +419,60 @@ def update(values):
)
SOCIALACCOUNT_PROVIDERS = auth_socialaccount
# OIDC authentication
if AUTH_OIDC is not None:
try:
LAVA_OIDC_ACCOUNT_NAME = AUTH_OIDC.pop("LAVA_OIDC_ACCOUNT_NAME")
except KeyError:
...
oidc_keys = {
"OIDC_OP_AUTHORIZATION_ENDPOINT",
"OIDC_OP_TOKEN_ENDPOINT",
"OIDC_OP_USER_ENDPOINT",
"OIDC_RP_CLIENT_ID",
"OIDC_RP_CLIENT_SECRET",
"OIDC_VERIFY_JWT",
"OIDC_VERIFY_KID",
"OIDC_USE_NONCE",
"OIDC_VERIFY_SSL",
"OIDC_TIMEOUT",
"OIDC_PROXY",
"OIDC_EXEMPT_URLS",
"OIDC_CREATE_USER",
"OIDC_STATE_SIZE",
"OIDC_NONCE_SIZE",
"OIDC_MAX_STATES",
"OIDC_REDIRECT_FIELD_NAME",
"OIDC_CALLBACK_CLASS",
"OIDC_AUTHENTICATE_CLASS",
"OIDC_RP_SCOPES",
"OIDC_STORE_ACCESS_TOKEN",
"OIDC_STORE_ID_TOKEN",
"OIDC_AUTH_REQUEST_EXTRA_PARAMS",
"OIDC_RP_SIGN_ALGO",
"OIDC_RP_IDP_SIGN_KEY",
"OIDC_OP_LOGOUT_URL_METHOD",
"OIDC_AUTHENTICATION_CALLBACK_URL",
"OIDC_ALLOW_UNSECURED_JWT",
"OIDC_TOKEN_USE_BASIC_AUTH",
}
if not oidc_keys.issuperset(AUTH_OIDC.keys()):
raise ImproperlyConfigured(
"Unknown OIDC configuration keys: ",
set(AUTH_OIDC.keys()).difference(oidc_keys),
)
INSTALLED_APPS.append("mozilla_django_oidc")
AUTHENTICATION_BACKENDS.append(
"lava_server.oidc_sso.OIDCAuthenticationBackendUsernameFromEmail"
)
MIDDLEWARE.append("mozilla_django_oidc.middleware.SessionRefresh")
OIDC_ENABLED = True
locals().update(AUTH_OIDC)
# LDAP authentication config
if AUTH_LDAP_SERVER_URI:
INSTALLED_APPS.append("ldap")
......@@ -542,6 +603,11 @@ def update(values):
"level": "DEBUG",
"propagate": False,
},
"mozilla_django_oidc": {
"handlers": ["logfile"],
"level": "DEBUG",
"propagate": True,
},
},
}
......
......@@ -52,10 +52,14 @@
</form>
</div>
{% endif %}
{% if oidc_enabled %}
<div class="col-md-4">
<h4>Local account</h4>
<hr/>
<h4 class="modal-header">{{ oidc_account_name }}</h4>
<a href="{% url 'oidc_authentication_init' %}">Login</a>
</div>
{% endif %}
<div class="col-md-4">
<h4 class="modal-header">Local account</h4>
<form class="form-horizontal" method="post" action="{% url 'login' %}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.GET.next }}" />
......
......@@ -166,6 +166,14 @@ urlpatterns = [
),
]
if settings.OIDC_ENABLED:
urlpatterns.append(
url(
r"^{mount_point}oidc/".format(mount_point=settings.MOUNT_POINT),
include("mozilla_django_oidc.urls"),
)
)
if settings.USE_DEBUG_TOOLBAR:
import debug_toolbar
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment