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

Add keycloak IDP hint and logout endpoint #108

Merged
merged 3 commits into from
May 28, 2024
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
1 change: 1 addition & 0 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ Utils

.. automodule:: mozilla_django_oidc_db.utils
:members:
:exclude-members: obfuscate_claim_value, extract_content_type
8 changes: 8 additions & 0 deletions mozilla_django_oidc_db/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class OpenIDConnectConfigAdmin(SingletonModelAdmin):
"oidc_op_token_endpoint",
"oidc_token_use_basic_auth",
"oidc_op_user_endpoint",
"oidc_op_logout_endpoint",
)
},
),
Expand All @@ -55,6 +56,13 @@ class OpenIDConnectConfigAdmin(SingletonModelAdmin):
)
},
),
(
_("Keycloak specific settings"),
{
"fields": ("oidc_keycloak_idp_hint",),
"classes": ["collapse in"],
},
),
(
_("Advanced settings"),
{
Expand Down
1 change: 1 addition & 0 deletions mozilla_django_oidc_db/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"oidc_op_token_endpoint": "token_endpoint",
"oidc_op_user_endpoint": "userinfo_endpoint",
"oidc_op_jwks_endpoint": "jwks_uri",
"oidc_op_logout_endpoint": "end_session_endpoint",
}

OPEN_ID_CONFIG_PATH = ".well-known/openid-configuration"
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 5.0.4 on 2024-05-25 19:36

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("mozilla_django_oidc_db", "0002_migrate_to_claim_field"),
]

operations = [
migrations.AddField(
model_name="openidconnectconfig",
name="oidc_keycloak_idp_hint",
field=models.CharField(
blank=True,
help_text="Specific for Keycloak: parameter that indicates which identity provider should be used (therefore skipping the Keycloak login screen).",
max_length=1000,
verbose_name="Keycloak Identity Provider hint",
),
),
migrations.AddField(
model_name="openidconnectconfig",
name="oidc_op_logout_endpoint",
field=models.URLField(
blank=True,
help_text="URL of your OpenID Connect provider logout endpoint",
max_length=1000,
verbose_name="Logout endpoint",
),
),
]
17 changes: 17 additions & 0 deletions mozilla_django_oidc_db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ class OpenIDConnectConfigBase(SingletonModel):
),
blank=True,
)
oidc_op_logout_endpoint = models.URLField(
_("Logout endpoint"),
max_length=1000,
help_text=_("URL of your OpenID Connect provider logout endpoint"),
blank=True,
)

# Advanced settings
oidc_use_nonce = models.BooleanField(
Expand Down Expand Up @@ -174,6 +180,17 @@ class OpenIDConnectConfigBase(SingletonModel):
),
)

# Keycloak specific config
oidc_keycloak_idp_hint = models.CharField(
_("Keycloak Identity Provider hint"),
max_length=1000,
help_text=_(
"Specific for Keycloak: parameter that indicates which identity provider "
"should be used (therefore skipping the Keycloak login screen)."
),
blank=True,
)

userinfo_claims_source = models.CharField(
verbose_name=_("user information claims extracted from"),
choices=UserInformationClaimsSources.choices,
Expand Down
36 changes: 35 additions & 1 deletion mozilla_django_oidc_db/utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import logging
from collections.abc import Collection
from copy import deepcopy

import requests
from glom import Path, PathAccessError, assign, glom
from requests.utils import _parse_content_type_header # type: ignore

from .models import OpenIDConnectConfigBase
from .typing import ClaimPath, JSONObject, JSONValue

logger = logging.getLogger(__name__)


def obfuscate_claim_value(value: JSONValue) -> JSONValue:
"""
Expand All @@ -27,7 +32,7 @@ def obfuscate_claims(
claims: JSONObject, claims_to_obfuscate: Collection[ClaimPath]
) -> JSONObject:
"""
Obfuscates the specified claims in the specified claims dict
Obfuscates the specified claims in the provided claims object.
"""
copied_claims = deepcopy(claims)
for claim_bits in claims_to_obfuscate:
Expand All @@ -51,3 +56,32 @@ def extract_content_type(ct_header: str) -> str:
content_type, _ = _parse_content_type_header(ct_header)
# discard the params, we only want the content type itself
return content_type


def do_op_logout(config: OpenIDConnectConfigBase, id_token: str) -> None:
sergei-maertens marked this conversation as resolved.
Show resolved Hide resolved
"""
Perform the logout with the OpenID Provider.

Standard: https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout

.. warning:: Preferably, you should send the user to the configured logout endpoint
so they can confirm the logout and any session cookies are cleared. If that is not
possible, you can call this helper for server-to-server logout, but there are no
guarantees this works for every possible OpenID Provider implementation. It has
been tested with Keycloak, but the standard says nothing about server-to-server
calls to log out a user.
"""
logout_endpoint = config.oidc_op_logout_endpoint
if not logout_endpoint:
return

response = requests.post(logout_endpoint, data={"id_token_hint": id_token})
if not response.ok:
logger.warning(
"Failed to log out the user at the OpenID Provider. Status code: %s",
response.status_code,
extra={
"response": response,
"status_code": response.status_code,
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 5.0.4 on 2024-05-25 19:37

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("testapp", "0001_initial"),
]

operations = [
migrations.AddField(
model_name="emptyconfig",
name="oidc_keycloak_idp_hint",
field=models.CharField(
blank=True,
help_text="Specific for Keycloak: parameter that indicates which identity provider should be used (therefore skipping the Keycloak login screen).",
max_length=1000,
verbose_name="Keycloak Identity Provider hint",
),
),
migrations.AddField(
model_name="emptyconfig",
name="oidc_op_logout_endpoint",
field=models.URLField(
blank=True,
help_text="URL of your OpenID Connect provider logout endpoint",
max_length=1000,
verbose_name="Logout endpoint",
),
),
]
Loading