Skip to content

Commit dee248c

Browse files
committed
add oidc->userclaim testing view in profile
1 parent aa336ee commit dee248c

6 files changed

Lines changed: 133 additions & 20 deletions

File tree

src/bornhack/oauth_validators.py

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,22 @@ class BornhackOAuth2Validator(OAuth2Validator):
88

99
# supported user claims and the scopes they require
1010
# https://django-oauth-toolkit.readthedocs.io/en/latest/oidc.html#using-oidc-scopes-to-determine-which-claims-are-returned
11-
oidc_claim_scope = OAuth2Validator.oidc_claim_scope
12-
oidc_claim_scope.update(
13-
{
14-
# the OIDC standard user claims we support, and the OIDC standard scopes they require
15-
"address": "address",
16-
"email": "email",
17-
"email_verified": "email",
18-
"phone_number": "phone",
19-
"phone_number_verified": "phone",
20-
"preferred_username": "profile",
21-
"updated_at": "profile",
22-
# the custom user claims we support, and the (mostly custom) scopes they require
23-
"bornhack:v2:description": "profile",
24-
"bornhack:v2:groups": "groups:read",
25-
"bornhack:v2:location": "location:read",
26-
"bornhack:v2:permissions": "permissions:read",
27-
"bornhack:v2:public_credit_name": "profile",
28-
"bornhack:v2:teams": "teams:read",
29-
},
30-
)
11+
oidc_claim_scope = {
12+
# the OIDC standard user claims we support, and the OIDC standard scopes they require
13+
"email": "email",
14+
"email_verified": "email",
15+
"phone_number": "phone",
16+
"preferred_username": "profile",
17+
"updated_at": "profile",
18+
# the custom user claims available under standard OIDC scopes
19+
"bornhack:v2:description": "profile",
20+
"bornhack:v2:public_credit_name": "profile",
21+
# the custom user claims we support under custom OIDC scopes
22+
"bornhack:v2:groups": "groups:read",
23+
"bornhack:v2:location": "location:read",
24+
"bornhack:v2:permissions": "permissions:read",
25+
"bornhack:v2:teams": "teams:read",
26+
}
3127

3228
def get_claim_dict(self, request) -> dict[str, str]:
3329
"""Return username (usually a uuid) instead of user pk in the 'sub' claim."""

src/profiles/forms.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from django import forms
2+
from bornhack.oauth_validators import BornhackOAuth2Validator
3+
4+
def get_scopes() -> list[str]:
5+
validator = BornhackOAuth2Validator()
6+
return ((claim, claim) for claim in sorted(set(validator.oidc_claim_scope.values())))
7+
8+
class OIDCForm(forms.Form):
9+
scopes = forms.MultipleChoiceField(
10+
choices=get_scopes,
11+
help_text="Select the scopes to simulate",
12+
)

src/profiles/templates/oidc.html

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{% extends 'profile_base.html' %}
2+
{% load django_bootstrap5 %}
3+
4+
{% block title %}
5+
OIDC Claims | {{ block.super }}
6+
{% endblock %}
7+
8+
{% block profile_content %}
9+
<div class="card">
10+
<div class="card-header">
11+
<h4>OIDC Claims</h4>
12+
</div>
13+
<div class="card-body">
14+
<p class="lead">When using BornHack as an IDP (logging into other sites using your BornHack account) you can control which user claims are returned by asking for one or more of the following claim scopes:</p>
15+
<p><ul>
16+
{% for scope in all_scopes %}
17+
<li><code>{{ scope }}</code></li>
18+
{% endfor %}
19+
</ul></p>
20+
<p>Note: In addition to this list the default <code>openid</code> scope is available (it is part of the standard) and must always be included when asking for a jwt.</p>
21+
<p class="lead">This form allows you to see which OIDC user claims are returned for your user with any combination of scopes.</p>
22+
<form method="GET">
23+
{% bootstrap_form form %}
24+
<button class="btn btn-primary" type="submit">Submit</button>
25+
</form>
26+
<hr>
27+
{% if not active_scopes %}
28+
<p class="lead">Select scopes in the form to see user claims</p>
29+
{% else %}
30+
<p class="lead">The following user claims will be returned in a jwt with these scopes:</p>
31+
<p>
32+
<ul>
33+
{% for scope in active_scopes %}
34+
<li><code>{{ scope }}</code></li>
35+
{% endfor %}
36+
</ul>
37+
</p>
38+
<table class="table table-striped">
39+
<tr>
40+
<th>Claim Name</th>
41+
<th>Required Scope</th>
42+
<th>Claim Value (JSON)</th>
43+
</tr>
44+
<tr>
45+
<td><code>sub</code></td>
46+
<td><code>openid</code></td>
47+
<td>{{ request.user.username }}</td>
48+
</tr>
49+
{% for claim, value in claims.items %}
50+
{% for claimname, scope in scopes.items %}
51+
{% if claimname == claim %}
52+
<tr>
53+
<td><code>{{ claim }}</code></td>
54+
<td><code>{{ scope }}</code></td>
55+
<td>{{ value }}</td>
56+
</tr>
57+
{% endif %}
58+
{% endfor %}
59+
{% endfor %}
60+
</table>
61+
{% endif %}
62+
</div>
63+
</div>
64+
{% endblock profile_content %}

src/profiles/templates/profile_base.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ <h2>Your BornHack Account</h2>
9191
</a>
9292
</li>
9393

94+
{% url 'profiles:oidc' as profile_oidc_url %}
95+
<li class="nav-item">
96+
<a class="nav-link{% if request.path == profile_oidc_url %} active{% endif %}" href="{{ profile_oidc_url }}">
97+
OIDC Scope<i class="fas fa-arrow-right"></i>Claim
98+
</a>
99+
</li>
100+
94101
<hr />
95102
<p class="text-break">You are logged in as username <b>{{ request.user.username }}</b> with email <b>{{ request.user.email }}</b></p>
96103

src/profiles/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
from .views import ProfileDetail
55
from .views import ProfileUpdate
66
from .views import ProfilePermissionList
7+
from .views import ProfileOIDCView
78

89
app_name = "profiles"
910
urlpatterns = [
1011
path("", ProfileDetail.as_view(), name="detail"),
1112
path("edit/", ProfileUpdate.as_view(), name="update"),
1213
path("api/", ProfileApiView.as_view(), name="api"),
1314
path("permissions/", ProfilePermissionList.as_view(), name="permissions_list"),
15+
path("oidc/", ProfileOIDCView.as_view(), name="oidc"),
1416
]

src/profiles/views.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
from django.urls import reverse_lazy
66
from django.views.generic import DetailView
77
from django.views.generic import ListView
8+
from django.views.generic import FormView
89
from django.views.generic import UpdateView
910
from jsonview.views import JsonView
1011
from oauth2_provider.views.generic import ScopedProtectedResourceView
1112
from leaflet.forms.widgets import LeafletWidget
1213

1314
from .models import Profile
15+
from .forms import OIDCForm
16+
from bornhack.oauth_validators import BornhackOAuth2Validator
1417

1518

1619
class ProfileDetail(LoginRequiredMixin, DetailView):
@@ -103,3 +106,32 @@ def get_queryset(self, *args, **kwargs):
103106
)
104107
)
105108
return perms
109+
110+
111+
class ProfileOIDCView(LoginRequiredMixin, FormView):
112+
template_name = "oidc.html"
113+
form_class = OIDCForm
114+
115+
def setup(self, *args, **kwargs):
116+
super().setup(*args, **kwargs)
117+
validator = BornhackOAuth2Validator()
118+
self.scopes = validator.oidc_claim_scope
119+
self.claims = validator.get_additional_claims(request=self.request)
120+
121+
def get_form(self, form_class=None):
122+
if form_class is None:
123+
form_class = self.get_form_class()
124+
self.initial['scopes'] = self.request.GET.getlist(key="scopes")
125+
return form_class(**self.get_form_kwargs())
126+
127+
def get_context_data(self, **kwargs):
128+
context = super().get_context_data(**kwargs)
129+
context["claims"] = {}
130+
for claim, value in self.claims.items():
131+
scope = self.scopes[claim]
132+
if scope in self.request.GET.getlist(key="scopes"):
133+
context["claims"][claim] = value
134+
context["scopes"] = self.scopes
135+
context["active_scopes"] = ["openid"] + sorted(list(set(self.request.GET.getlist(key="scopes"))))
136+
context["all_scopes"] = sorted(list(set(self.scopes.values())))
137+
return context

0 commit comments

Comments
 (0)