Skip to content

Commit cd7c6a3

Browse files
authored
Merge branch 'master' into css_do_over_2
2 parents f799548 + e1cd6af commit cd7c6a3

7 files changed

Lines changed: 139 additions & 22 deletions

File tree

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,16 @@ Enjoy!
166166
The BornHack website can act as an OIDC IDP. You are welcome to use it for your projects.
167167

168168

169-
### OIDC User Claims
169+
### OIDC Scopes and User Claims
170+
The website has a view to inspect which OIDC user claims are returned when using the various claim scopes. It can be accessed at https://bornhack.dk/profile/oidc/
171+
172+
173+
### OIDC User Claims Source Code
170174

171175
The supported standard and custom OIDC user claims can be seen in `bornhack/oauth_validators.py` https://github.com/bornhack/bornhack-website/blob/master/src/bornhack/oauth_validators.py
172176

173177

174-
### OIDC Scopes
178+
### OIDC Scopes Source Code
175179

176180
Supported oauth2 scopes are divided into standard OIDC claim scopes, custom OIDC claim scopes, and API scopes. The current list of supported scopes can be seen in the `OAUTH2_PROVIDER["SCOPES"]` dict in `bornhack/settings.py` https://github.com/bornhack/bornhack-website/blob/master/src/bornhack/settings.py
177181

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
@@ -5,6 +5,7 @@
55
from .views import ProfileUpdate
66
from .views import ProfilePermissionList
77
from .views import ProfileSessionThemeSwitchView
8+
from .views import ProfileOIDCView
89

910
app_name = "profiles"
1011
urlpatterns = [
@@ -13,4 +14,5 @@
1314
path("edit/", ProfileUpdate.as_view(), name="update"),
1415
path("api/", ProfileApiView.as_view(), name="api"),
1516
path("permissions/", ProfilePermissionList.as_view(), name="permissions_list"),
17+
path("oidc/", ProfileOIDCView.as_view(), name="oidc"),
1618
]

src/profiles/views.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
from django.shortcuts import redirect
88
from django.views.generic import DetailView
99
from django.views.generic import ListView
10+
from django.views.generic import FormView
1011
from django.views.generic import UpdateView
1112
from django.views import View
1213
from jsonview.views import JsonView
1314
from oauth2_provider.views.generic import ScopedProtectedResourceView
1415
from leaflet.forms.widgets import LeafletWidget
1516

1617
from .models import Profile
18+
from .forms import OIDCForm
19+
from bornhack.oauth_validators import BornhackOAuth2Validator
1720

1821

1922
class ProfileDetail(LoginRequiredMixin, DetailView):
@@ -126,3 +129,32 @@ def get(self, request, *args, **kwargs):
126129
return redirect(next_url)
127130
else:
128131
return HttpResponseForbidden()
132+
133+
134+
class ProfileOIDCView(LoginRequiredMixin, FormView):
135+
template_name = "oidc.html"
136+
form_class = OIDCForm
137+
138+
def setup(self, *args, **kwargs):
139+
super().setup(*args, **kwargs)
140+
validator = BornhackOAuth2Validator()
141+
self.scopes = validator.oidc_claim_scope
142+
self.claims = validator.get_additional_claims(request=self.request)
143+
144+
def get_form(self, form_class=None):
145+
if form_class is None:
146+
form_class = self.get_form_class()
147+
self.initial['scopes'] = self.request.GET.getlist(key="scopes")
148+
return form_class(**self.get_form_kwargs())
149+
150+
def get_context_data(self, **kwargs):
151+
context = super().get_context_data(**kwargs)
152+
context["claims"] = {}
153+
for claim, value in self.claims.items():
154+
scope = self.scopes[claim]
155+
if scope in self.request.GET.getlist(key="scopes"):
156+
context["claims"][claim] = value
157+
context["scopes"] = self.scopes
158+
context["active_scopes"] = ["openid"] + sorted(list(set(self.request.GET.getlist(key="scopes"))))
159+
context["all_scopes"] = sorted(list(set(self.scopes.values())))
160+
return context

0 commit comments

Comments
 (0)