Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

customizable OIDC #1088

Merged
merged 23 commits into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 34 additions & 12 deletions docs/admin/additional_features/oidc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,49 @@ You will also need to know the following information about your OIDC provider:
============================== ===============================================
Value Usual value
============================== ===============================================
OIDC_RP_CLIENT_ID *displayed after OIDC client registration*
OIDC_RP_CLIENT_SECRET *displayed after OIDC client registration*
OIDC_RP_SIGN_ALGO RS256
OIDC_OP_AUTHORIZATION_ENDPOINT https://your-oidc-provider.com/auth
OIDC_OP_TOKEN_ENDPOINT https://your-oidc-provider.com/token
OIDC_OP_USER_ENDPOINT https://your-oidc-provider.com/me
OIDC_OP_JWKS_ENDPOINT https://your-oidc-provider.com/certs
client id *displayed after OIDC client registration*
client secret *displayed after OIDC client registration*
============================== ===============================================

These values as well as ``ENABLE_OIDC_CLIENT=True`` must be provided to the ephios configuration as environment variables.
After completing these steps, users will see a "Login" button that starts the OIDC authentication flow. If you want to
log in with a local user account, you can still do so by navigating to ``/accounts/login/?local=true``.
Configuration
-------------

To configure your ephios instance, head to Settings -> Identity Providers and add a new OIDC provider.
You are then asked to provide the base url of your identity provider. This is the url that is used to access the OIDC endpoints and depends on your provider.
For example, if you are using Keycloak, this would be ``https://your-keycloak-instance.com/realms/your-realm``.
If your provider supports auto-discovery, we will automatically fetch the required information from the OIDC provider.
Otherwise, you will need to provide the following information:

============================== ===============================================
Value Usual value
============================== ===============================================
AUTHORIZATION_ENDPOINT https://your-oidc-provider.com/auth
TOKEN_ENDPOINT https://your-oidc-provider.com/token
USERINFO_ENDPOINT https://your-oidc-provider.com/me
JWKS_URI https://your-oidc-provider.com/certs
============================== ===============================================

The following additional configuration options are available:

============================== =================================================== ========================
Value Usage Default value
============================== =================================================== ========================
OIDC_RP_SCOPES Scopes to request from the RP (for additional data) ``openid profile email``
LOGOUT_REDIRECT_URL redirect the user to the RP logout page None (no redirect)
scopes Scopes to request from the RP (for additional data) ``openid profile email``
end_session_endpoint redirect the user to the logout page of the IDP None (no redirect)
default groups groups to add all users logging in with this IDP to None (no groups)
============================== =================================================== ========================

If users are logged in exclusively using identity providers, you can also hide the local login form with the appropriate settings under "ephios instance".

.. warning::
ephios uses the email adress provided by the IDP to identify a user account. If the IDP allows the user to change their email adress,
users could enter the email adress of another user and log in as that user. To prevent this, you should configure your IDP to not allow users to change their email adress.

jeriox marked this conversation as resolved.
Show resolved Hide resolved
Usage
-----
After you configured at least one identity providers, the login page will display a button for each identity provider.
Clicking on this button will redirect you to the OIDC provider, where you can log in.
To log in with a local user account when the login form is hidden, you can still do so by navigating to ``/accounts/login/?local=true``.

.. toctree::
:maxdepth: 2
2 changes: 2 additions & 0 deletions ephios/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
WorkingHours,
)
from ephios.core.models.events import PlaceholderParticipation
from ephios.core.models.users import IdentityProvider

admin.site.register(UserProfile)
admin.site.register(Qualification)
Expand All @@ -29,3 +30,4 @@
admin.site.register(LocalParticipation)
admin.site.register(PlaceholderParticipation)
admin.site.register(Notification)
admin.site.register(IdentityProvider)
13 changes: 13 additions & 0 deletions ephios/core/dynamic_preferences_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
global_preferences_registry,
)
from dynamic_preferences.types import (
BooleanPreference,
DateTimePreference,
ModelMultipleChoicePreference,
MultipleChoicePreference,
Expand Down Expand Up @@ -101,6 +102,18 @@ def set_last_call(cls, value):
preferences[f"{cls.section.name}__{cls.name}"] = value


@global_preferences_registry.register
class HideLoginForm(BooleanPreference):
name = "hide_login_form"
verbose_name = _("Hide login form")
help_text = _(
"Hide the login form on the login page. This only takes effect if you configured at least one identity provider."
)
default = False
section = general_global_section
required = False


@user_preferences_registry.register
class NotificationPreference(JSONPreference):
name = "notifications"
Expand Down
12 changes: 12 additions & 0 deletions ephios/core/forms/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,3 +470,15 @@ class UserOwnDataForm(ModelForm):
class Meta:
model = UserProfile
fields = ["preferred_language"]


class OIDCDiscoveryForm(Form):
url = forms.URLField(
label=_("OIDC Provider URL"), help_text=_("The base URL of the OIDC provider.")
)

def clean_url(self):
url = self.cleaned_data["url"]
if not url.endswith("/"):
url += "/"
return url
132 changes: 132 additions & 0 deletions ephios/core/migrations/0022_identityprovider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Generated by Django 4.2.4 on 2023-10-19 23:06
from django.core.exceptions import ImproperlyConfigured
from django.db import migrations, models


def migrate_oidc_provider(apps, schema_editor):
from ephios import settings

try:
if settings.env.bool("ENABLE_OIDC_CLIENT"):
IdentityProvider = apps.get_model("core", "IdentityProvider")
db_alias = schema_editor.connection.alias
IdentityProvider.objects.using(db_alias).create(
label="OIDC",
client_id=settings.env.str("OIDC_RP_CLIENT_ID"),
client_secret=settings.env.str("OIDC_RP_CLIENT_SECRET"),
scopes=settings.env.str("OIDC_RP_SCOPES"),
authorization_endpoint=settings.env.str("OIDC_OP_AUTHORIZATION_ENDPOINT"),
token_endpoint=settings.env.str("OIDC_OP_TOKEN_ENDPOINT"),
userinfo_endpoint=settings.env.str("OIDC_OP_USER_ENDPOINT"),
end_session_endpoint=settings.env.str("LOGOUT_REDIRECT_URL", None),
jwks_uri=settings.env.str("OIDC_OP_JWKS_ENDPOINT", None),
)
except (AttributeError, KeyError, ImproperlyConfigured):
pass


def reverse_migrate_oidc_provider(apps, schema_editor):
IdentityProvider = apps.get_model("core", "IdentityProvider")
db_alias = schema_editor.connection.alias
IdentityProvider.objects.using(db_alias).filter(label="OIDC").delete()


class Migration(migrations.Migration):
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("core", "0021_userprofile_preferred_language_and_more"),
]

operations = [
migrations.CreateModel(
name="IdentityProvider",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"label",
models.CharField(
help_text="The label displayed to users attempting to log in with this provider.",
max_length=255,
verbose_name="label",
),
),
(
"client_id",
models.CharField(
help_text="Your client id provided by the OIDC provider.",
max_length=255,
verbose_name="client id",
),
),
(
"client_secret",
models.CharField(
help_text="Your client secret provided by the OIDC provider.",
max_length=255,
verbose_name="client secret",
),
),
(
"scopes",
models.CharField(
default="openid profile email",
help_text="The OIDC scopes to request from the provider. Separate multiple scopes with spaces. Use the default value if you are unsure.",
max_length=255,
verbose_name="scopes",
),
),
(
"authorization_endpoint",
models.URLField(
help_text="The OIDC authorization endpoint.",
verbose_name="authorization endpoint",
),
),
(
"token_endpoint",
models.URLField(
help_text="The OIDC token endpoint.", verbose_name="token endpoint"
),
),
(
"userinfo_endpoint",
models.URLField(
help_text="The OIDC user endpoint.", verbose_name="user endpoint"
),
),
(
"end_session_endpoint",
models.URLField(
blank=True,
help_text="The OIDC end session endpoint, if supported by your provider.",
null=True,
verbose_name="end session endpoint",
),
),
(
"jwks_uri",
models.URLField(
blank=True,
help_text="The OIDC JWKS endpoint. A less secure signing method will be used if this is not provided.",
null=True,
verbose_name="JWKS endpoint",
),
),
(
"default_groups",
models.ManyToManyField(
blank=True,
help_text="The groups that users logging in with this provider will be added to.",
to="auth.group",
verbose_name="default groups",
),
),
],
),
migrations.RunPython(migrate_oidc_provider, reverse_migrate_oidc_provider),
]
jeriox marked this conversation as resolved.
Show resolved Hide resolved
58 changes: 58 additions & 0 deletions ephios/core/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,3 +532,61 @@ def as_plaintext(self):

def get_actions(self):
return self.notification_type.get_actions(self)


class IdentityProvider(Model):
label = models.CharField(
max_length=255,
verbose_name=_("label"),
help_text=_("The label displayed to users attempting to log in with this provider."),
)
client_id = models.CharField(
max_length=255,
verbose_name=_("client id"),
help_text=_("Your client id provided by the OIDC provider."),
)
client_secret = models.CharField(
max_length=255,
verbose_name=_("client secret"),
help_text=_("Your client secret provided by the OIDC provider."),
)
scopes = models.CharField(
max_length=255,
default="openid profile email",
verbose_name=_("scopes"),
help_text=_(
"The OIDC scopes to request from the provider. Separate multiple scopes with spaces. Use the default value if you are unsure."
),
)
authorization_endpoint = models.URLField(
verbose_name=_("authorization endpoint"), help_text=_("The OIDC authorization endpoint.")
)
token_endpoint = models.URLField(
verbose_name=_("token endpoint"), help_text=_("The OIDC token endpoint.")
)
userinfo_endpoint = models.URLField(
verbose_name=_("user endpoint"), help_text=_("The OIDC user endpoint.")
)
end_session_endpoint = models.URLField(
blank=True,
null=True,
verbose_name=_("end session endpoint"),
help_text=_("The OIDC end session endpoint, if supported by your provider."),
)
jwks_uri = models.URLField(
blank=True,
null=True,
verbose_name=_("JWKS endpoint"),
help_text=_(
"The OIDC JWKS endpoint. A less secure signing method will be used if this is not provided."
),
)
default_groups = models.ManyToManyField(
Group,
blank=True,
verbose_name=_("default groups"),
help_text=_("The groups that users logging in with this provider will be added to."),
)

def __str__(self):
return _("Identity provider {label}").format(label=self.label)
16 changes: 16 additions & 0 deletions ephios/core/templates/core/identityprovider_confirm_delete.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{% extends "core/settings/settings_base.html" %}
{% load i18n %}

{% block settings_content %}
<h3>{% translate "Delete identity provider" %}</h3>
<form method="post">
{% csrf_token %}
<p>{% blocktrans trimmed with label=identityprovider.label %}
Are you sure you want to delete the identity provider "{{ label }}"?
Users will no longer be able to log in with this identity provider.
Please make sure that they have an alternative means of logging in.
{% endblocktrans %}</p>
<a role="button" class="btn btn-secondary" href="{% url "core:settings_idp_list" %}">{% translate "Back" %}</a>
<button type="submit" class="btn btn-danger">{% translate "Delete" %}</button>
</form>
{% endblock %}
18 changes: 18 additions & 0 deletions ephios/core/templates/core/identityprovider_discovery.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{% extends "core/settings/settings_base.html" %}
{% load i18n %}
{% load crispy_forms_filters %}

{% block settings_content %}
<h3>{% translate "Add identity provider" %}</h3>
<div class="alert alert-info" role="alert">
{% blocktranslate trimmed %}
We can try to auto-discover certain values for your identity provider. To start the discovery, please enter the base URL for the OpenID Connect protocol at your provider below.
{% endblocktranslate %}
</div>
<form method="POST">
{% csrf_token %}
{{ form|crispy }}
<input class="btn btn-primary" type="submit" value="{% translate "Start auto-discovery" %}">
<a class="btn btn-secondary" href="{% url "core:settings_idp_create" %}">{% translate "Continue without auto-discovery" %}</a>
</form>
{% endblock %}
18 changes: 18 additions & 0 deletions ephios/core/templates/core/identityprovider_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{% extends "core/settings/settings_base.html" %}
{% load i18n %}
{% load crispy_forms_filters %}

{% block settings_content %}
<h3>
{% if identityprovider %}
{% translate "Edit identity provider" %}
{% else %}
{% translate "Add identity provider" %}
{% endif %}
</h3>
<form method="POST">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn btn-primary">{% translate "Save" %}</button>
</form>
{% endblock %}
Loading