Skip to content

Commit 266ff15

Browse files
authored
Merge pull request #1 from allisson/openid-connect
Add support for oidc connect discovery
2 parents b2847e2 + 99948cf commit 266ff15

File tree

11 files changed

+153
-16
lines changed

11 files changed

+153
-16
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ __pycache__
2525
pip-log.txt
2626

2727
# Unit test / coverage reports
28-
.cache
28+
.pytest_cache
2929
.coverage
3030
.tox
3131
nosetests.xml

oauth2_provider/models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ class AbstractApplication(models.Model):
6262
RS256_ALGORITHM = "RS256"
6363
HS256_ALGORITHM = "HS256"
6464
ALGORITHM_TYPES = (
65-
("RS256", _("RSA with SHA-2 256")),
66-
("HS256", _("HMAC with SHA-2 256")),
65+
(RS256_ALGORITHM, _("RSA with SHA-2 256")),
66+
(HS256_ALGORITHM, _("HMAC with SHA-2 256")),
6767
)
6868

6969
id = models.BigAutoField(primary_key=True)
@@ -92,7 +92,7 @@ class AbstractApplication(models.Model):
9292

9393
created = models.DateTimeField(auto_now_add=True)
9494
updated = models.DateTimeField(auto_now=True)
95-
algorithm = models.CharField(max_length=5, choices=ALGORITHM_TYPES, default="RS256")
95+
algorithm = models.CharField(max_length=5, choices=ALGORITHM_TYPES, default=RS256_ALGORITHM)
9696

9797
class Meta:
9898
abstract = True

oauth2_provider/oauth2_validators.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ def get_jwt_bearer_token(self, token, token_handler, request):
604604

605605
def get_id_token(self, token, token_handler, request):
606606

607-
key = jwk.JWK.from_pem(oauth2_settings.RSA_PRIVATE_KEY.encode("utf8"))
607+
key = jwk.JWK.from_pem(oauth2_settings.OIDC_RSA_PRIVATE_KEY.encode("utf8"))
608608

609609
# TODO: http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken2
610610
# Save the id_token on database bound to code when the request come to
@@ -667,7 +667,7 @@ def validate_id_token(self, token, scopes, request):
667667
if not token:
668668
return False
669669

670-
key = jwk.JWK.from_pem(oauth2_settings.RSA_PRIVATE_KEY.encode("utf8"))
670+
key = jwk.JWK.from_pem(oauth2_settings.OIDC_RSA_PRIVATE_KEY.encode("utf8"))
671671

672672
try:
673673
jwt_token = jwt.JWT(key=key, jwt=token)

oauth2_provider/settings.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,21 @@
5555
"REFRESH_TOKEN_MODEL": REFRESH_TOKEN_MODEL,
5656
"REQUEST_APPROVAL_PROMPT": "force",
5757
"ALLOWED_REDIRECT_URI_SCHEMES": ["http", "https"],
58-
"RSA_PRIVATE_KEY": "",
58+
"OIDC_ISS_ENDPOINT": "",
59+
"OIDC_USERINFO_ENDPOINT": "",
60+
"OIDC_RSA_PRIVATE_KEY": "",
61+
"OIDC_RESPONSE_TYPES_SUPPORTED": [
62+
"code",
63+
"token",
64+
"id_token",
65+
"id_token token",
66+
"code token",
67+
"code id_token",
68+
"code id_token token",
69+
],
70+
"OIDC_SUBJECT_TYPES_SUPPORTED": ["public"],
71+
"OIDC_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED": ["RS256", "HS256"],
72+
"OIDC_TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED": ["client_secret_post", "client_secret_basic"],
5973

6074
# Special settings that will be evaluated at runtime
6175
"_SCOPES": [],
@@ -76,7 +90,13 @@
7690
"OAUTH2_BACKEND_CLASS",
7791
"SCOPES",
7892
"ALLOWED_REDIRECT_URI_SCHEMES",
79-
"RSA_PRIVATE_KEY",
93+
"OIDC_ISS_ENDPOINT",
94+
"OIDC_USERINFO_ENDPOINT",
95+
"OIDC_RSA_PRIVATE_KEY",
96+
"OIDC_RESPONSE_TYPES_SUPPORTED",
97+
"OIDC_SUBJECT_TYPES_SUPPORTED",
98+
"OIDC_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED",
99+
"OIDC_TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED"
80100
)
81101

82102
# List of settings that may be in string import notation.

oauth2_provider/urls.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,10 @@
2929
name="authorized-token-delete"),
3030
]
3131

32+
oidc_urlpatterns = [
33+
url(r"^\.well-known/openid-configuration/$", views.ConnectDiscoveryInfoView.as_view(), name="oidc-connect-discovery-info"),
34+
url(r"^jwks/$", views.JwksInfoView.as_view(), name="jwks-info")
35+
]
36+
3237

33-
urlpatterns = base_urlpatterns + management_urlpatterns
38+
urlpatterns = base_urlpatterns + management_urlpatterns + oidc_urlpatterns

oauth2_provider/views/__init__.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
# flake8: noqa
2-
from .base import AuthorizationView, TokenView, RevokeTokenView
3-
from .application import ApplicationRegistration, ApplicationDetail, ApplicationList, \
4-
ApplicationDelete, ApplicationUpdate
5-
from .generic import ProtectedResourceView, ScopedProtectedResourceView, ReadWriteScopedResourceView
6-
from .token import AuthorizedTokensListView, AuthorizedTokenDeleteView
2+
from .application import (
3+
ApplicationDelete, ApplicationDetail, ApplicationList,
4+
ApplicationRegistration, ApplicationUpdate
5+
)
6+
from .base import AuthorizationView, RevokeTokenView, TokenView
7+
from .generic import (
8+
ProtectedResourceView, ReadWriteScopedResourceView,
9+
ScopedProtectedResourceView
10+
)
711
from .introspect import IntrospectTokenView
12+
from .oidc import ConnectDiscoveryInfoView, JwksInfoView
13+
from .token import AuthorizedTokenDeleteView, AuthorizedTokensListView

oauth2_provider/views/oidc.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from __future__ import absolute_import, unicode_literals
2+
3+
import json
4+
5+
from django.http import JsonResponse
6+
from django.urls import reverse_lazy
7+
from django.views.generic import View
8+
from jwcrypto import jwk
9+
10+
from ..settings import oauth2_settings
11+
12+
13+
class ConnectDiscoveryInfoView(View):
14+
"""
15+
View used to show oidc provider configuration information
16+
"""
17+
def get(self, request, *args, **kwargs):
18+
issuer_url = oauth2_settings.OIDC_ISS_ENDPOINT
19+
data = {
20+
"issuer": issuer_url,
21+
"authorization_endpoint": "{}{}".format(issuer_url, reverse_lazy("oauth2_provider:authorize")),
22+
"token_endpoint": "{}{}".format(issuer_url, reverse_lazy("oauth2_provider:token")),
23+
"userinfo_endpoint": oauth2_settings.OIDC_USERINFO_ENDPOINT,
24+
"jwks_uri": "{}{}".format(issuer_url, reverse_lazy("oauth2_provider:jwks-info")),
25+
"response_types_supported": oauth2_settings.OIDC_RESPONSE_TYPES_SUPPORTED,
26+
"subject_types_supported": oauth2_settings.OIDC_SUBJECT_TYPES_SUPPORTED,
27+
"id_token_signing_alg_values_supported": oauth2_settings.OIDC_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED,
28+
"token_endpoint_auth_methods_supported": oauth2_settings.OIDC_TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED,
29+
}
30+
response = JsonResponse(data)
31+
response["Access-Control-Allow-Origin"] = "*"
32+
return response
33+
34+
35+
class JwksInfoView(View):
36+
"""
37+
View used to show oidc json web key set document
38+
"""
39+
def get(self, request, *args, **kwargs):
40+
key = jwk.JWK.from_pem(oauth2_settings.OIDC_RSA_PRIVATE_KEY.encode("utf8"))
41+
data = {
42+
"keys": [{
43+
"alg": "RS256",
44+
"use": "sig",
45+
"kid": key.thumbprint()
46+
}]
47+
}
48+
data["keys"][0].update(json.loads(key.export_public()))
49+
response = JsonResponse(data)
50+
response["Access-Control-Allow-Origin"] = "*"
51+
return response

tests/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,9 @@
124124
},
125125
}
126126
}
127+
128+
OAUTH2_PROVIDER = {
129+
"OIDC_ISS_ENDPOINT": "http://localhost",
130+
"OIDC_USERINFO_ENDPOINT": "http://localhost/userinfo/",
131+
"OIDC_RSA_PRIVATE_KEY": "-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQCbCYh5h2NmQuBqVO6G+/CO+cHm9VBzsb0MeA6bbQfDnbhstVOT\nj0hcnZJzDjYc6ajBZZf6gxVP9xrdm9Uh599VI3X5PFXLbMHrmzTAMzCGIyg+/fnP\n0gocYxmCX2+XKyj/Zvt1pUX8VAN2AhrJSfxNDKUHERTVEV9bRBJg4F0C3wIDAQAB\nAoGAP+i4nNw+Ec/8oWh8YSFm4xE6qKG0NdTtSMAOyWwy+KTB+vHuT1QPsLn1vj77\n+IQrX/moogg6F1oV9YdA3vat3U7rwt1sBGsRrLhA+Spp9WEQtglguNo4+QfVo2ju\nYBa2rG+h75qjiA3xnU//F3rvwnAsOWv0NUVdVeguyR+u6okCQQDBUmgWeH2WHmUn\n2nLNCz+9wj28rqhfOr9Ptem2gqk+ywJmuIr4Y5S1OdavOr2UZxOcEwncJ/MLVYQq\nMH+x4V5HAkEAzU2GMR5OdVLcxfVTjzuIC76paoHVWnLibd1cdANpPmE6SM+pf5el\nfVSwuH9Fmlizu8GiPCxbJUoXB/J1tGEKqQJBALhClEU+qOzpoZ6/voYi/6kdN3zc\nuEy0EN6n09AKb8gS9QH1STgAqh+ltjMkeMe3C2DKYK5/QU9/Pc58lWl1FkcCQG67\nZamQgxjcvJ85FvymS1aqW45KwNysIlzHjFo2jMlMf7dN6kobbPMQftDENLJvLWIT\nqoFyGycdsxZiPAIyZSECQQCZFn3Dl6hnJxWZH8Fsa9hj79kZ/WVkIXGmtdgt0fNr\ndTnvCVtA59ne4LEVie/PMH/odQWY0SxVm/76uBZv/1vY\n-----END RSA PRIVATE KEY-----"
132+
}

tests/test_implicit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def setUp(self):
4646
"write": "Writing scope",
4747
"openid": "OpenID connect"
4848
}
49-
self.key = jwk.JWK.from_pem(oauth2_settings.RSA_PRIVATE_KEY.encode("utf8"))
49+
self.key = jwk.JWK.from_pem(oauth2_settings.OIDC_RSA_PRIVATE_KEY.encode("utf8"))
5050

5151
def tearDown(self):
5252
self.application.delete()

tests/test_oidc_views.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from __future__ import unicode_literals
2+
3+
from django.test import TestCase
4+
from django.urls import reverse
5+
6+
7+
class TestConnectDiscoveryInfoView(TestCase):
8+
def test_get_connect_discovery_info(self):
9+
expected_response = {
10+
"issuer": "http://localhost",
11+
"authorization_endpoint": "http://localhost/o/authorize/",
12+
"token_endpoint": "http://localhost/o/token/",
13+
"userinfo_endpoint": "http://localhost/userinfo/",
14+
"jwks_uri": "http://localhost/o/jwks/",
15+
"response_types_supported": [
16+
"code",
17+
"token",
18+
"id_token",
19+
"id_token token",
20+
"code token",
21+
"code id_token",
22+
"code id_token token"
23+
],
24+
"subject_types_supported": ["public"],
25+
"id_token_signing_alg_values_supported": ["RS256", "HS256"],
26+
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"]
27+
}
28+
response = self.client.get(reverse("oauth2_provider:oidc-connect-discovery-info"))
29+
self.assertEqual(response.status_code, 200)
30+
assert response.json() == expected_response
31+
32+
33+
class TestJwksInfoView(TestCase):
34+
def test_get_jwks_info(self):
35+
expected_response = {
36+
"keys": [{
37+
"alg": "RS256",
38+
"use": "sig",
39+
"kid": "s4a1o8mFEd1tATAIH96caMlu4hOxzBUaI2QTqbYNBHs",
40+
"e": "AQAB",
41+
"kty": "RSA",
42+
"n": "mwmIeYdjZkLgalTuhvvwjvnB5vVQc7G9DHgOm20Hw524bLVTk49IXJ2Scw42HOmowWWX-oMVT_ca3ZvVIeffVSN1-TxVy2zB65s0wDMwhiMoPv35z9IKHGMZgl9vlyso_2b7daVF_FQDdgIayUn8TQylBxEU1RFfW0QSYOBdAt8"
43+
}]
44+
}
45+
response = self.client.get(reverse("oauth2_provider:jwks-info"))
46+
self.assertEqual(response.status_code, 200)
47+
assert response.json() == expected_response

0 commit comments

Comments
 (0)