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

#3718 - Implement SSO for rca.ac.uk #1061

Merged
merged 5 commits into from
Nov 11, 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
4 changes: 4 additions & 0 deletions docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ You an use the endpoint below to confirm the ID works.
```python
mailchimp.lists.list_interest_category_interests(settings.MAILCHIMP_LIST_ID, MAILCHIMP_PROGRAMMES_INTEREST_CATEGORY_ID)
```

## Azure AD SSO

To add SSO functionality, we used `social-auth-app-django`. All of the configurations and necessary keys can be seen in the `Azure AD (SSO)` and `Social Auth (SSO)` sections in the base settings. Logging in via SSO is optional. Users can still login via the standard Wagtail login form.
6 changes: 6 additions & 0 deletions docs/support-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ See also our [incident process](https://intranet.torchbox.com/propositions/desig
- `buckup-rca-staging`
- `buckup-rca-development`

## SSO

SSO has been added using `social-auth-app-django`. This handles logging in and creating of the necessary user -- see `SOCIAL_AUTH_PIPELINE` in `rca.settings.base`.

The Azure environment is handled by RCA. If we need to update keys or redirect URLs, we'll need to contact RCA.

## Enquire to study form throwing 500s:

### 1. Check the data
Expand Down
10 changes: 10 additions & 0 deletions docs/upgrading.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ As well as testing the critical paths, these areas of functionality should be ch
1. The site has custom code to show/hide fields and panels depending on if a StudentPage is viewed by an admin (superuser) or a Student (none superuser).
2. The site has custom code to show/hide sidebar elements depending on if a Student is viewing the editor vs an admin (superuser).

### Users who logged in via SSO automatically gets 'Editor' permissions.

1. When users log in via SSO, they are automatically made editors. This is done via `rca.utils.pipeline.make_sso_users_editors` and added to the `SOCIAL_AUTH_PIPELINE`. RCA will manually elevate permissiosn if necessary.

### Users who logged in via SSO is redirected to a logout confirmation when they logout.

1. The site overrides the `/admin/logout/` endpoint to redirect users who logged in to `/logout/`. This is a confirmation screen that users will still need to manually log out of their SSO accounts. This is done with `rca.account_management.views.CustomLogoutView` and `rca.account_management.views.SSOLogoutConfirmationView`.
2. Users who did not log in via SSO should be able to log out without seeing any confirmation screen.

patrickcuagan marked this conversation as resolved.
Show resolved Hide resolved
---

## Overridden core Wagtail templates
Expand All @@ -113,6 +122,7 @@ The following templates are overridden and should be checked for changes when up
Last checked against Wagtail version: 6.1

- `rca/account_management/templates/wagtailadmin/base.html`
- `rca/project_styleguide/templates/patterns/pages/auth/login.html` - This was overridden to add the "Sign in with single sign-on" button to the login template.
- ~~`rca/users/templates/wagtailusers/users/list.html`~~ This template was deleted in 2645c204425b8fa3409a110f46b2822a1953fe49 because as of Wagtail 6.1, [it's no longer used](https://github.com/wagtail/wagtail/commit/7b1644eb37b6b6cf7800276acf9abef5254fc096). Please note that the `user_listing_buttons` template tag was used in this template, and it has since been [deprecated](https://docs.wagtail.org/en/latest/releases/6.1.html#deprecation-of-user-listing-buttons-template-tag).

!!! warning "Technical Debt - to be addressed in Wagtail 6.2"
Expand Down
78 changes: 77 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ sentry-sdk = "^2.12.0"
tblib = "^3.0.0"
urllib3 = "<2"
whitenoise = "~6.6"
social-auth-app-django = "^5.4.2"

[tool.poetry.extras]
gunicorn = ["gunicorn"]
Expand Down
37 changes: 35 additions & 2 deletions rca/account_management/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
from django.contrib.auth.models import Group
from django.core.exceptions import PermissionDenied
from django.core.mail import send_mail
from django.shortcuts import redirect, render
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import FormView
from wagtail.admin import messages
from wagtail.admin.views.account import LoginView
from wagtail.admin.views.account import LoginView, LogoutView

from rca.people.models import StudentIndexPage, StudentPage
from rca.users.models import User
Expand Down Expand Up @@ -164,7 +165,7 @@ def form_valid(self, form):
class CustomLoginView(LoginView):
"""Custom login view to redirect students to their profile page"""

template_name = "wagtailadmin/login.html"
template_name = "patterns/pages/auth/login.html"

def get_success_url(self):
"""Override the success URL if the user meets the following:
patrickcuagan marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -190,3 +191,35 @@ def get_success_url(self):
"wagtailadmin_pages:edit", kwargs={"page_id": student_page.id}
)
return super().get_success_url()


class CustomLogoutView(LogoutView):
"""Check if user is signed in via SSO. If yes, redirect them to a confirmation logout page."""

def dispatch(self, request, *args, **kwargs):
if (
self.request.session.get("_auth_user_backend", "")
== "social_core.backends.azuread_tenant.AzureADTenantOAuth2"
):
# redirect to `sso_logout_confirmation`
return redirect("sso_logout_confirmation")

return super().dispatch(request, *args, **kwargs)


class SSOLogoutConfirmationView(LogoutView):
template_name = "patterns/pages/auth/logout_confirmation.html"

def dispatch(self, request, *args, **kwargs):
if (
self.request.session.get("_auth_user_backend", "")
== "social_core.backends.azuread_tenant.AzureADTenantOAuth2"
):
# If the request is a POST, log out the user
if request.method == "POST":
return super().dispatch(request, *args, **kwargs)

# If the request is GET, render the confirmation page
return render(request, self.template_name)

return super().dispatch(request, *args, **kwargs)
80 changes: 80 additions & 0 deletions rca/project_styleguide/templates/patterns/pages/auth/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{% extends "wagtailadmin/admin_base.html" %}
{% load i18n wagtailadmin_tags %}
{% block titletag %}{% trans "Sign in" %}{% endblock %}
{% block bodyclass %}login{% endblock %}

{% block furniture %}
<main class="content-wrapper" id="main">
<h1>{% block branding_login %}{% trans "Sign in to Wagtail" %}{% endblock %}</h1>

<div class="messages" role="status">
{# Always show messages div so it can be appended to by JS #}
{% if messages or form.errors %}
<ul>
{% if form.errors %}
{% for error in form.non_field_errors %}
<li class="error">{{ error }}</li>
{% endfor %}
{% endif %}
{% for message in messages %}
<li class="{{ message.tags }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
</div>

{% block above_login %}{% endblock %}

<form class="login-form" action="{% url 'wagtailadmin_login' %}" method="post" autocomplete="off" novalidate>
{% block login_form %}
{% csrf_token %}

{% url 'wagtailadmin_home' as home_url %}
<input type="hidden" name="next" value="{{ next|default:home_url }}" />

{% block fields %}
{% formattedfield form.username %}
{% formattedfield form.password %}

{% if show_password_reset %}
<a class="reset-password" href="{% url 'wagtailadmin_password_reset' %}">{% trans "Forgotten password?" %}</a>
{% endif %}

{% block extra_fields %}
{% for field_name, field in form.extra_fields %}
{% formattedfield field %}
{% endfor %}
{% endblock extra_fields %}

{% include "wagtailadmin/shared/forms/single_checkbox.html" with label_classname="remember-me" name="remember" text=_("Remember me") %}
{% endblock %}
{% endblock %}
<footer class="form-actions">
{% block submit_buttons %}
<button
type="submit"
class="button button-longrunning"
data-controller="w-progress"
data-action="w-progress#activate"
data-w-progress-active-value="{% trans 'Signing in…' %}"
>
{% icon name="spinner" %}
<em data-w-progress-target="label">{% trans 'Sign in' %}</em>
</button>
<br /><br />
<a class="button" href="{% url "social:begin" "azuread-tenant-oauth2" %}?next={{ request.path }}">
Sign in with single sign-on
</a>
{% endblock %}
</footer>
</form>

{% block below_login %}{% endblock %}

{% block branding_logo %}
<div class="login-logo">
{% include "wagtailadmin/logo.html" with wordmark="True" %}
</div>
{% endblock %}
</main>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{% extends "wagtailadmin/admin_base.html" %}
{% load i18n wagtailadmin_tags %}
{% block titletag %}{% trans "Log out" %}{% endblock %}
{% block bodyclass %}login{% endblock %}

{% block furniture %}
<main class="content-wrapper" id="main">
<h1>{% block branding_login %}{% trans "Logout from Wagtail" %}{% endblock %}</h1>

<form class="login-form" action="{% url 'sso_logout_confirmation' %}" method="post" novalidate>
{% block login_form %}
{% csrf_token %}

<div class="messages">
<p>
You have signed in using Single Sign-On (SSO). Logging out here will only end your session for this application. To completely sign out, make sure to log out from your SSO provider as well.
</p>
</div>

<button type="submit" class="button button-longrunning">
{% trans 'Log out' %}
</button>

<br /><br />

<button type="button" class="button" onclick="window.history.back();">
{% trans 'Cancel' %}
</button>
{% endblock %}
</form>

{% block branding_logo %}
<div class="login-logo">
{% include "wagtailadmin/logo.html" with wordmark="True" %}
</div>
{% endblock %}
</main>
{% endblock %}
36 changes: 36 additions & 0 deletions rca/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"wagtail_rangefilter",
"rangefilter",
"wagtail_modeladmin",
"social_django",
]

# Middleware classes
Expand Down Expand Up @@ -150,6 +151,9 @@
# This is a custom context processor that lets us add custom
# global variables to all the templates.
"rca.utils.context_processors.global_vars",
# Social auth context_processors
"social_django.context_processors.backends",
"social_django.context_processors.login_redirect",
],
"builtins": ["pattern_library.loader_tags"],
},
Expand Down Expand Up @@ -860,3 +864,35 @@
)
# Vepple
VEPPLE_API_URL = env.get("VEPPLE_API_URL", "https://editor.rca.rvhosted.com")

# Azure AD (SSO)
AUTHENTICATION_BACKENDS = [
"social_core.backends.azuread_tenant.AzureADTenantOAuth2",
"django.contrib.auth.backends.ModelBackend",
]

# Social Auth (SSO)
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY = env.get(
"SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY", None
)
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET = env.get(
"SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET", None
)
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID = env.get(
"SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID", None
)
SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ["username", "first_name", "last_name", "email"]
SOCIAL_AUTH_JSONFIELD_ENABLED = True
SOCIAL_AUTH_USER_MODEL = "users.User"
SOCIAL_AUTH_PIPELINE = (
"social_core.pipeline.social_auth.social_details",
"social_core.pipeline.social_auth.social_uid",
"social_core.pipeline.social_auth.auth_allowed",
"social_core.pipeline.social_auth.social_user",
"social_core.pipeline.social_auth.associate_by_email",
"social_core.pipeline.user.create_user",
"social_core.pipeline.social_auth.associate_user",
"social_core.pipeline.social_auth.load_extra_data",
"social_core.pipeline.user.user_details",
"rca.utils.pipeline.make_sso_users_editors",
)
Loading
Loading