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

Keycloak OIDC groups sync Issue: 7096 #9787

Closed
wants to merge 5 commits into from
Closed
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
48 changes: 20 additions & 28 deletions docs/content/en/integrations/social-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ Follow along below.
button on the login page.

## Keycloak
There is also an option to use Keycloak as OAuth2 provider in order to authenticate users to Defect Dojo, also by using
There is also an option to use Keycloak as OAuth2/OIDC provider in order to authenticate users to Defect Dojo, also by using
the social-auth plugin.

Here are suggestion on how to configure Keycloak and DefectDojo:
Expand All @@ -279,55 +279,46 @@ Here are suggestion on how to configure Keycloak and DefectDojo:
* Set `access type` to `confidential`
* Under `valid Redirect URIs`, add the URI to your defect dojo installation, e.g. 'https://<YOUR_DD_HOST>/*'
* Under `web origins`, add the same (or '+')
* Under `Fine grained openID connect configuration` -> `user info signed response algorithm`: set to `RS256`
* Under `Fine grained openID connect configuration` -> `request object signature algorithm`: set to `RS256`
* -> save these settings in keycloak (hit save button)
3. Under `Scope` -> `Full Scope Allowed` set to `off`
4. Under `mappers` -> add a custom mapper here:
* Name: `aud`
* Mapper type: `audience`
* Included audience: select your client/client-id here
* Add ID to token: `off`
* Add access to token: `on`
5. Under `credentials`: copy the secret (and use as DD_SOCIAL_AUTH_KEYCLOAK_SECRET below)
6. In your realm settings -> keys: copy the "Public key" (signing key) (use for DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY below)
7. In your realm settings -> general -> endpoints: look into openId endpoint configuration
and look up your authorization and token endpoint (use them below)
3. In your realm settings -> general -> endpoints: look into openId endpoint configuration
and use the url below for the `DD_SOCIAL_AUTH_OIDC_OIDC_ENDPOINT` property (you can remove the `/.well-known/openid-configuration` part as its standard and the python library adds it)

### Configure Defect Dojo
Edit the settings (see [Configuration]({{< ref "/getting_started/configuration" >}})) with the following
information:

{{< highlight python >}}
DD_SESSION_COOKIE_SECURE=True,
DD_CSRF_COOKIE_SECURE=True,
DD_SECURE_SSL_REDIRECT=True,
DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_ENABLED=True,
DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY=(str, '<your realm public key>'),
DD_SOCIAL_AUTH_OIDC_OIDC_ENDPOINT="<https://yourkeycloakinstance.com/realms/your-realm>"
DD_SOCIAL_AUTH_KEYCLOAK_KEY=(str, '<your client id>'),
DD_SOCIAL_AUTH_KEYCLOAK_SECRET=(str, '<your keycloak client credentials secret>'),
DD_SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL=(str, '<your authorization endpoint>'),
DD_SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL=(str, '<your token endpoint>')
DD_SOCIAL_AUTH_KEYCLOAK_SECRET=(str, '<your keycloak client credentials secret>'),
{{< /highlight >}}

or, alternatively, for helm configuration, add this to the `extraConfig` section:

```
DD_SESSION_COOKIE_SECURE: 'True'
DD_CSRF_COOKIE_SECURE: 'True'
DD_SECURE_SSL_REDIRECT: 'True'
DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_ENABLED: 'True'
DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY: '<your realm public key>'
DD_SOCIAL_AUTH_OIDC_OIDC_ENDPOINT="<https://yourkeycloakinstance.com/realms/your-realm>"
DD_SOCIAL_AUTH_KEYCLOAK_KEY: '<your client id>'
DD_SOCIAL_AUTH_KEYCLOAK_SECRET: '<your keycloak client credentials secret>'
DD_SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL: '<your authorization endpoint>'
DD_SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL: '<your token endpoint>'
```

Optionally, you *can* set `DD_SOCIAL_AUTH_KEYCLOAK_LOGIN_BUTTON_TEXT` in order to customize the login button's text caption.

### Syncing groups from Keycloak to Defectdojo
It is also possible to sync groups from Keycloak into Defectdojo, for this you will first need to configure a `client scope` which enables the groups of your users to be included
into the authentication tokens of your users. Keycloak provides a `Groups` `Token mapper` specifically for this purpose.

After enabling the `Groups` mapper you can configure Defectdojo to sync these groups with the following properties:

```
DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_GET_GROUPS: "True"
DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_CLEANUP_GROUPS: "True"
DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_GROUPS_FILTER: "<your regex here>"
```

## GitHub Enterprise
1. Navigate to your GitHub Enterprise Server and follow instructions to create a new OAuth App [https://docs.github.com/en/enterprise-server/developers/apps/building-oauth-apps/creating-an-oauth-app](https://docs.github.com/en/enterprise-server/developers/apps/building-oauth-apps/creating-an-oauth-app)
1. Navigate to your GitHub Enterprise Server and follow instructions to create a new OAuth App [https://docs.github.com/en/enterprise-server/developers/apps/building-oauth-apps/creating-an-oauth-app](https://docs.github.com/en/enterprise-server/developers/apps/building-oauth-apps/creating-an-oauth-app)
2. Choose a name for your application
3. For the Redirect URI, enter the DefectDojo URL with the following
format
Expand Down Expand Up @@ -448,6 +439,7 @@ Some Identity Providers are able to send list of groups to which should user bel

- [Azure](#automatic-import-of-user-groups): Check `DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_GET_GROUPS` and `DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_CLEANUP_GROUPS`
- [RemoteUser](#remoteuser): Check `DD_AUTH_REMOTEUSER_GROUPS_HEADER` and `DD_AUTH_REMOTEUSER_GROUPS_CLEANUP`
- [Keycloak](#Syncing-groups-from-Keycloak-to-Defectdojo) Check `DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_GET_GROUPS` and `DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_CLEANUP_GROUPS`

## Login speed-up

Expand Down
3 changes: 3 additions & 0 deletions dojo/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ def globalize_vars(request):
"AZUREAD_TENANT_OAUTH2_GET_GROUPS": settings.AZUREAD_TENANT_OAUTH2_GET_GROUPS,
"AZUREAD_TENANT_OAUTH2_GROUPS_FILTER": settings.AZUREAD_TENANT_OAUTH2_GROUPS_FILTER,
"AZUREAD_TENANT_OAUTH2_CLEANUP_GROUPS": settings.AZUREAD_TENANT_OAUTH2_CLEANUP_GROUPS,
"KEYCLOAK_TENANT_OAUTH2_GET_GROUPS": settings.KEYCLOAK_TENANT_OAUTH2_GET_GROUPS,
"KEYCLOAK_TENANT_OAUTH2_GROUPS_FILTER": settings.KEYCLOAK_TENANT_OAUTH2_GROUPS_FILTER,
"KEYCLOAK_TENANT_OAUTH2_CLEANUP_GROUPS": settings.KEYCLOAK_TENANT_OAUTH2_CLEANUP_GROUPS,
"KEYCLOAK_ENABLED": settings.KEYCLOAK_OAUTH2_ENABLED,
"SOCIAL_AUTH_KEYCLOAK_LOGIN_BUTTON_TEXT": settings.SOCIAL_AUTH_KEYCLOAK_LOGIN_BUTTON_TEXT,
"GITHUB_ENTERPRISE_ENABLED": settings.GITHUB_ENTERPRISE_OAUTH2_ENABLED,
Expand Down
18 changes: 18 additions & 0 deletions dojo/db_migrations/0213_alter_dojo_group_social_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.1.13 on 2024-03-18 16:35

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('dojo', '0212_sla_configuration_enforce_critical_and_more'),
]

operations = [
migrations.AlterField(
model_name='dojo_group',
name='social_provider',
field=models.CharField(blank=True, choices=[('AzureAD', 'AzureAD'), ('Remote', 'Remote'), ('Keycloak', 'Keycloak')], help_text='Group imported from a social provider.', max_length=10, null=True, verbose_name='Social Authentication Provider'),
),
]
2 changes: 1 addition & 1 deletion dojo/group/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def group_post_save_handler(sender, **kwargs):
group.auth_group = auth_group
group.save()
user = get_current_user()
if user and not settings.AZUREAD_TENANT_OAUTH2_GET_GROUPS:
if user and not settings.AZUREAD_TENANT_OAUTH2_GET_GROUPS and not settings.KEYCLOAK_TENANT_OAUTH2_GET_GROUPS:
# Add the current user as the owner of the group
member = Dojo_Group_Member()
member.user = user
Expand Down
2 changes: 2 additions & 0 deletions dojo/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,11 @@ class UserContactInfo(models.Model):
class Dojo_Group(models.Model):
AZURE = 'AzureAD'
REMOTE = 'Remote'
KEYCLOAK = 'Keycloak'
SOCIAL_CHOICES = (
(AZURE, _('AzureAD')),
(REMOTE, _('Remote')),
(KEYCLOAK, _('Keycloak')),
)
name = models.CharField(max_length=255, unique=True)
description = models.CharField(max_length=4000, null=True, blank=True)
Expand Down
20 changes: 20 additions & 0 deletions dojo/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.conf import settings
from social_core.backends.azuread_tenant import AzureADTenantOAuth2
from social_core.backends.google import GoogleOAuth2
from social_core.backends.open_id_connect import OpenIdConnectAuth

from dojo.authorization.roles_permissions import Permissions, Roles
from dojo.models import Dojo_Group, Dojo_Group_Member, Product, Product_Member, Product_Type, Role
Expand Down Expand Up @@ -66,6 +67,25 @@ def modify_permissions(backend, uid, user=None, social=None, *args, **kwargs):
pass


def update_keycloak_groups(backend, uid, user=None, social=None, *args, **kwargs):
if settings.KEYCLOAK_OAUTH2_ENABLED and settings.KEYCLOAK_TENANT_OAUTH2_GET_GROUPS and isinstance(backend, OpenIdConnectAuth):
group_names = []
if 'groups' not in kwargs['response'] or kwargs['response']['groups'] == "":
logger.warning("No groups in response. Stopping to update groups of user based on azureAD")
return
group_ids = kwargs['response']['groups']
for group_from_response in group_ids:
if settings.KEYCLOAK_TENANT_OAUTH2_GROUPS_FILTER == "" or re.search(settings.KEYCLOAK_TENANT_OAUTH2_GROUPS_FILTER, group_from_response):
group_names.append(group_from_response)
else:
logger.debug("Skipping group " + group_from_response + " due to KEYCLOAK_TENANT_OAUTH2_GROUPS_FILTER " + settings.KEYCLOAK_TENANT_OAUTH2_GROUPS_FILTER)

if len(group_names) > 0:
assign_user_to_groups(user, group_names, 'Keycloak')
if settings.KEYCLOAK_TENANT_OAUTH2_CLEANUP_GROUPS:
cleanup_old_groups_for_user(user, group_names)


def update_azure_groups(backend, uid, user=None, social=None, *args, **kwargs):
if settings.AZUREAD_TENANT_OAUTH2_ENABLED and settings.AZUREAD_TENANT_OAUTH2_GET_GROUPS and isinstance(backend, AzureADTenantOAuth2):
# In some wild cases, there could be two social auth users
Expand Down
2 changes: 1 addition & 1 deletion dojo/settings/.settings.dist.py.sha256sum
Original file line number Diff line number Diff line change
@@ -1 +1 @@
f23b780905f138f168436a6579b45a1ccaa0deb35e6b11b1e9169f13266e4bb0
530c28be2efca480e9b8ec7dbf9481a5f35987f1fe5c8e9b5e21daff15cbccb1
22 changes: 13 additions & 9 deletions dojo/settings/settings.dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@
DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_GET_GROUPS=(bool, False),
DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_GROUPS_FILTER=(str, ''),
DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_CLEANUP_GROUPS=(bool, True),
DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_GET_GROUPS=(bool, False),
DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_CLEANUP_GROUPS=(bool, True),
DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_GROUPS_FILTER=(str, ''),
DD_SOCIAL_AUTH_GITLAB_OAUTH2_ENABLED=(bool, False),
DD_SOCIAL_AUTH_GITLAB_PROJECT_AUTO_IMPORT=(bool, False),
DD_SOCIAL_AUTH_GITLAB_PROJECT_IMPORT_TAGS=(bool, False),
Expand All @@ -141,11 +144,9 @@
DD_SOCIAL_AUTH_GITLAB_API_URL=(str, 'https://gitlab.com'),
DD_SOCIAL_AUTH_GITLAB_SCOPE=(list, ['read_user', 'openid']),
DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_ENABLED=(bool, False),
DD_SOCIAL_AUTH_OIDC_OIDC_ENDPOINT=(str, ''),
DD_SOCIAL_AUTH_KEYCLOAK_KEY=(str, ''),
DD_SOCIAL_AUTH_KEYCLOAK_SECRET=(str, ''),
DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY=(str, ''),
DD_SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL=(str, ''),
DD_SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL=(str, ''),
DD_SOCIAL_AUTH_KEYCLOAK_LOGIN_BUTTON_TEXT=(str, 'Login with Keycloak'),
DD_SOCIAL_AUTH_GITHUB_ENTERPRISE_OAUTH2_ENABLED=(bool, False),
DD_SOCIAL_AUTH_GITHUB_ENTERPRISE_URL=(str, ''),
Expand Down Expand Up @@ -489,7 +490,7 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param
'dojo.okta.OktaOAuth2',
'social_core.backends.azuread_tenant.AzureADTenantOAuth2',
'social_core.backends.gitlab.GitLabOAuth2',
'social_core.backends.keycloak.KeycloakOAuth2',
'social_core.backends.open_id_connect.OpenIdConnectAuth',
'social_core.backends.github_enterprise.GithubEnterpriseOAuth2',
'dojo.remote_user.RemoteUserBackend',
'django.contrib.auth.backends.RemoteUserBackend',
Expand Down Expand Up @@ -522,6 +523,7 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
'dojo.pipeline.update_azure_groups',
'dojo.pipeline.update_keycloak_groups',
'dojo.pipeline.update_product_access',
)

Expand Down Expand Up @@ -583,13 +585,15 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param
SOCIAL_AUTH_TRAILING_SLASH = env('DD_SOCIAL_AUTH_TRAILING_SLASH')

KEYCLOAK_OAUTH2_ENABLED = env('DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_ENABLED')
SOCIAL_AUTH_KEYCLOAK_KEY = env('DD_SOCIAL_AUTH_KEYCLOAK_KEY')
SOCIAL_AUTH_KEYCLOAK_SECRET = env('DD_SOCIAL_AUTH_KEYCLOAK_SECRET')
SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY = env('DD_SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY')
SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL = env('DD_SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL')
SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL = env('DD_SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL')
SOCIAL_AUTH_OIDC_OIDC_ENDPOINT = env('DD_SOCIAL_AUTH_OIDC_OIDC_ENDPOINT')
SOCIAL_AUTH_OIDC_KEY = env('DD_SOCIAL_AUTH_KEYCLOAK_KEY')
SOCIAL_AUTH_OIDC_SECRET = env('DD_SOCIAL_AUTH_KEYCLOAK_SECRET')
SOCIAL_AUTH_KEYCLOAK_LOGIN_BUTTON_TEXT = env('DD_SOCIAL_AUTH_KEYCLOAK_LOGIN_BUTTON_TEXT')

KEYCLOAK_TENANT_OAUTH2_GET_GROUPS = env('DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_GET_GROUPS')
KEYCLOAK_TENANT_OAUTH2_CLEANUP_GROUPS = env('DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_CLEANUP_GROUPS')
KEYCLOAK_TENANT_OAUTH2_GROUPS_FILTER = env('DD_SOCIAL_AUTH_KEYCLOAK_OAUTH2_GROUPS_FILTER')

GITHUB_ENTERPRISE_OAUTH2_ENABLED = env('DD_SOCIAL_AUTH_GITHUB_ENTERPRISE_OAUTH2_ENABLED')
SOCIAL_AUTH_GITHUB_ENTERPRISE_URL = env('DD_SOCIAL_AUTH_GITHUB_ENTERPRISE_URL')
SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL = env('DD_SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL')
Expand Down
2 changes: 1 addition & 1 deletion dojo/templates/dojo/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ <h3>{% trans "Login" %}</h3>
{% if KEYCLOAK_ENABLED is True %}
<div class="col-sm-offset-1 col-sm-2">
<button class="btn btn-success" type="button">
<a href="{% url 'social:begin' 'keycloak' %}?next={{ request.GET.next }}" style="color: rgb(255,255,255)">{{ SOCIAL_AUTH_KEYCLOAK_LOGIN_BUTTON_TEXT }}</a>
<a href="{% url 'social:begin' 'oidc' %}?next={{ request.GET.next }}" style="color: rgb(255,255,255)">{{ SOCIAL_AUTH_KEYCLOAK_LOGIN_BUTTON_TEXT }}</a>
</button>
</div>
{% endif %}
Expand Down
2 changes: 1 addition & 1 deletion dojo/user/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def login_view(request):
elif settings.GITLAB_OAUTH2_ENABLED:
social_auth = 'gitlab'
elif settings.KEYCLOAK_OAUTH2_ENABLED:
social_auth = 'keycloak'
social_auth = 'oidc'
elif settings.AUTH0_OAUTH2_ENABLED:
social_auth = 'auth0'
elif settings.GITHUB_ENTERPRISE_OAUTH2_ENABLED:
Expand Down
Loading