feat(templates): add oath consent template#7454
Conversation
There was a problem hiding this comment.
LGTM — straightforward branded template for the OAuth2 consent view.
Extended reasoning...
Overview
This PR adds a single new template file at cl/assets/templates/oauth2_provider/authorize.html that overrides django-oauth-toolkit's default authorize view with a CourtListener-branded version. It extends base.html, renders the application name, requested scopes, redirect URI, and hidden form fields, and provides a fallback error rendering branch.
Security risks
Minimal. The template displays values that come from a trusted form (application.name, scopes_descriptions, form.redirect_uri.value) through Django's auto-escaped templating. CSRF is included. The user-visible <code> block wrapping redirect_uri is escaped by Django and renders as text only. noindex meta is correctly applied to prevent indexing of the consent page.
Level of scrutiny
Low — this is a pure presentation-layer change with no Python logic, no view changes, and no auth flow changes. The behavior is entirely driven by django-oauth-toolkit's existing AuthorizationView, which already supplies the same context variables to its stock template.
Other factors
The bug hunter flagged a single cosmetic nit: {{ form.errors }} already renders non-field errors (it's an ErrorDict), so the following {{ form.non_field_errors }} line duplicates them in the error alert. This is purely visual and only shows on the AllowForm validation-failure path, which is rare. It's already posted as an inline comment for the author to optionally fix; the rest of the template is correct, and screenshots in the PR description confirm the happy path renders as intended.
| {% if form.errors %} | ||
| <div class="alert alert-danger"> | ||
| {{ form.errors }} | ||
| {{ form.non_field_errors }} | ||
| </div> | ||
| {% endif %} |
There was a problem hiding this comment.
🟡 {{ form.errors }} renders Django's ErrorDict (via as_ul()), which already includes non-field errors under the __all__ key — so the immediately-following {{ form.non_field_errors }} prints those same messages a second time inside the alert box. Since django-oauth-toolkit's AllowForm validation failures (invalid redirect_uri, scope, etc.) are almost exclusively non-field errors, the duplication will be visible in essentially every error case. Drop one of the two lines (typically {{ form.non_field_errors }}).
Extended reasoning...
What goes wrong
Django's Form.errors is an ErrorDict. Its default rendering (ErrorDict.__str__ → as_ul()) emits a single <ul class="errorlist"> containing every error on the form, including non-field errors keyed under the special __all__ key (NON_FIELD_ERRORS). form.non_field_errors() is then just self.errors.get(NON_FIELD_ERRORS, self.error_class(...)) — it returns the same ErrorList that form.errors already rendered.
So this block:
{% if form.errors %}
<div class="alert alert-danger">
{{ form.errors }}
{{ form.non_field_errors }}
</div>
{% endif %}renders the non-field errors twice inside the same alert div.
Why this matters for AllowForm
The form used here is django-oauth-toolkit's AllowForm. Validation failures from clean() / add_error(None, ...) (invalid redirect_uri, invalid scope, etc.) are non-field errors, so practically every error message a user would see on this page will appear duplicated.
Step-by-step proof
- User submits the consent form with an invalid
scope. AllowForm.clean()raises aValidationError, which is stored under__all__.- Template hits
{% if form.errors %}→ True; enters the alert div. {{ form.errors }}→ErrorDict.as_ul()emits e.g.
<ul class="errorlist nonfield"><li>Invalid scope.</li></ul>.{{ form.non_field_errors }}→ returns the sameErrorListand renders
<ul class="errorlist nonfield"><li>Invalid scope.</li></ul>again.- The user sees the message twice in the same alert box.
Why this slipped past
Both Django snippets look complementary at a glance — "errors and non-field errors" reads naturally — but form.errors is a superset, not a sibling, of form.non_field_errors. The standard Django idiom is to render either {{ form.errors }} alone, or {{ form.non_field_errors }} plus per-field errors — never both.
Impact
Cosmetic only — no functional or security impact, and only visible on the error path. The visible OAuth error code path mostly flows through the {% else %} branch (which renders error.error / error.description), so this duplication shows up only when AllowForm itself fails validation.
Fix
Delete the {{ form.non_field_errors }} line; {{ form.errors }} already covers it:
{% if form.errors %}
<div class="alert alert-danger">
{{ form.errors }}
</div>
{% endif %}|
Updates the errors to mirror the login template. Using |
|
@elisa-a-v could you review? |
|
@nadahlberg be sure to add the item to the Web team project board and set it as Todo so that Elisa sees it there too. I think that's the latest approach unless I missed something. |
ERosendo
left a comment
There was a problem hiding this comment.
@mlissner @nadahlberg code looks good to me. One thing I'd like to double-check: do we feel strongly about using the blue background for the confirmation and error messages?
My impression is that a white background might be a bit easier to read and would also align better with some of our other pages (for example, the 404 page). I put together a couple of screenshots showing how it looks with a white background:
- Confirmation message:
- Error message:
And for reference, here's our 404 page, which also uses a white background:
Curious to hear what you both think!
|
Yeah, I agree! |
|
Thanks, @mlissner! Back to you, @nadahlberg. I think the only change needed is removing the |
elisa-a-v
left a comment
There was a problem hiding this comment.
Looks good, and agreed with Eduardo on the white background 🫡
Fixes
Fixes #7228
Summary
This PR adds a branded template to the oath2_provider apps authorize view.
Deployment
This PR should:
skip-deploy(skips everything below)skip-web-deployskip-celery-deployskip-cronjob-deployskip-daemon-deployScreenshots
Desktop
Mobile