-
Notifications
You must be signed in to change notification settings - Fork 814
Openid Connect Core support #545
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
Changes from all commits
b03f4b3
6af92ad
ec33a1a
60ca67e
75aeaca
41d3362
68729e8
0ae398b
5c3d245
0583bad
4ef5321
e043524
6ac4795
a77fb12
22596d4
7358dd8
1e91d84
86630e7
2319e73
0444220
73b8318
e3515f6
d6bf785
64761cc
2557e42
b0ba544
3bb351a
33e193a
31254d5
2d87515
69df9ee
961bb55
cafb0dc
8b79da7
48b7813
2211288
4eb46d1
249b21f
4e1f07b
df1a154
5b05ce0
368e03a
3b2b2c1
ef161c0
723d37a
e531284
5680ca4
6e71b66
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,3 @@ | ||
# Generated by Django 2.2 on 2019-04-06 18:05 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('oauth2_provider', '0002_auto_20190406_1805'), | ||
] | ||
|
||
operations = [ | ||
migrations.AddField( | ||
model_name='application', | ||
name='algorithm', | ||
field=models.CharField(choices=[('RS256', 'RSA with SHA-2 256'), ('HS256', 'HMAC with SHA-2 256')], default='RS256', max_length=5), | ||
), | ||
migrations.AlterField( | ||
model_name='application', | ||
name='authorization_grant_type', | ||
field=models.CharField(choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials'), ('openid-hybrid', 'OpenID connect hybrid')], max_length=32), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from django.conf import settings | ||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
('oauth2_provider', '0003_auto_20190413_2007'), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='IDToken', | ||
fields=[ | ||
('id', models.BigAutoField(primary_key=True, serialize=False)), | ||
('token', models.TextField(unique=True)), | ||
('expires', models.DateTimeField()), | ||
('scope', models.TextField(blank=True)), | ||
('created', models.DateTimeField(auto_now_add=True)), | ||
('updated', models.DateTimeField(auto_now=True)), | ||
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)), | ||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='oauth2_provider_idtoken', to=settings.AUTH_USER_MODEL)), | ||
], | ||
options={ | ||
'abstract': False, | ||
'swappable': 'OAUTH2_PROVIDER_ID_TOKEN_MODEL', | ||
}, | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
from django.conf import settings | ||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('oauth2_provider', '0004_idtoken'), | ||
] | ||
|
||
operations = [ | ||
migrations.AddField( | ||
model_name='accesstoken', | ||
name='id_token', | ||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='access_token', to=settings.OAUTH2_PROVIDER_ID_TOKEN_MODEL), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
import json | ||
import logging | ||
from datetime import timedelta | ||
from urllib.parse import parse_qsl, urlparse | ||
|
@@ -9,6 +10,7 @@ | |
from django.urls import reverse | ||
from django.utils import timezone | ||
from django.utils.translation import gettext_lazy as _ | ||
from jwcrypto import jwk, jwt | ||
|
||
from .generators import generate_client_id, generate_client_secret | ||
from .scopes import get_scopes_backend | ||
|
@@ -50,11 +52,20 @@ class AbstractApplication(models.Model): | |
GRANT_IMPLICIT = "implicit" | ||
GRANT_PASSWORD = "password" | ||
GRANT_CLIENT_CREDENTIALS = "client-credentials" | ||
GRANT_OPENID_HYBRID = "openid-hybrid" | ||
GRANT_TYPES = ( | ||
(GRANT_AUTHORIZATION_CODE, _("Authorization code")), | ||
(GRANT_IMPLICIT, _("Implicit")), | ||
(GRANT_PASSWORD, _("Resource owner password-based")), | ||
(GRANT_CLIENT_CREDENTIALS, _("Client credentials")), | ||
(GRANT_OPENID_HYBRID, _("OpenID connect hybrid")), | ||
) | ||
|
||
RS256_ALGORITHM = "RS256" | ||
HS256_ALGORITHM = "HS256" | ||
ALGORITHM_TYPES = ( | ||
(RS256_ALGORITHM, _("RSA with SHA-2 256")), | ||
(HS256_ALGORITHM, _("HMAC with SHA-2 256")), | ||
) | ||
|
||
id = models.BigAutoField(primary_key=True) | ||
|
@@ -82,6 +93,7 @@ class AbstractApplication(models.Model): | |
|
||
created = models.DateTimeField(auto_now_add=True) | ||
updated = models.DateTimeField(auto_now=True) | ||
algorithm = models.CharField(max_length=5, choices=ALGORITHM_TYPES, default=RS256_ALGORITHM) | ||
|
||
class Meta: | ||
abstract = True | ||
|
@@ -282,6 +294,10 @@ class AbstractAccessToken(models.Model): | |
related_name="refreshed_access_token" | ||
) | ||
token = models.CharField(max_length=255, unique=True, ) | ||
id_token = models.OneToOneField( | ||
oauth2_settings.ID_TOKEN_MODEL, on_delete=models.CASCADE, blank=True, null=True, | ||
related_name="access_token" | ||
) | ||
application = models.ForeignKey( | ||
oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE, blank=True, null=True, | ||
) | ||
|
@@ -415,6 +431,99 @@ class Meta(AbstractRefreshToken.Meta): | |
swappable = "OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL" | ||
|
||
|
||
class AbstractIDToken(models.Model): | ||
""" | ||
An IDToken instance represents the actual token to | ||
access user's resources, as in :openid:`2`. | ||
|
||
Fields: | ||
|
||
* :attr:`user` The Django user representing resources' owner | ||
* :attr:`token` ID token | ||
* :attr:`application` Application instance | ||
* :attr:`expires` Date and time of token expiration, in DateTime format | ||
* :attr:`scope` Allowed scopes | ||
""" | ||
id = models.BigAutoField(primary_key=True) | ||
user = models.ForeignKey( | ||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True, | ||
related_name="%(app_label)s_%(class)s" | ||
) | ||
token = models.TextField(unique=True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Testing locally using MySQL the migration created by this errors with MySQL err 1170, "BLOB/TEXT column 'token' used in key specification without a key length". Reference: https://dev.mysql.com/doc/refman/8.0/en/server-error-reference.html#error_er_blob_key_without_length Given that this is not the primary key and (if my understanding is correct) typically a value generated by the application, perhaps we could not use a database index to ensure uniqueness? Happy to make a PR if so. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure plz There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see wiliamsouza#10 |
||
application = models.ForeignKey( | ||
oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE, blank=True, null=True, | ||
) | ||
expires = models.DateTimeField() | ||
scope = models.TextField(blank=True) | ||
|
||
created = models.DateTimeField(auto_now_add=True) | ||
updated = models.DateTimeField(auto_now=True) | ||
|
||
def is_valid(self, scopes=None): | ||
""" | ||
Checks if the access token is valid. | ||
|
||
:param scopes: An iterable containing the scopes to check or None | ||
""" | ||
return not self.is_expired() and self.allow_scopes(scopes) | ||
|
||
def is_expired(self): | ||
""" | ||
Check token expiration with timezone awareness | ||
""" | ||
if not self.expires: | ||
return True | ||
|
||
return timezone.now() >= self.expires | ||
|
||
def allow_scopes(self, scopes): | ||
""" | ||
Check if the token allows the provided scopes | ||
|
||
:param scopes: An iterable containing the scopes to check | ||
""" | ||
if not scopes: | ||
return True | ||
|
||
provided_scopes = set(self.scope.split()) | ||
resource_scopes = set(scopes) | ||
|
||
return resource_scopes.issubset(provided_scopes) | ||
|
||
def revoke(self): | ||
""" | ||
Convenience method to uniform tokens' interface, for now | ||
simply remove this token from the database in order to revoke it. | ||
""" | ||
self.delete() | ||
|
||
@property | ||
def scopes(self): | ||
""" | ||
Returns a dictionary of allowed scope names (as keys) with their descriptions (as values) | ||
""" | ||
all_scopes = get_scopes_backend().get_all_scopes() | ||
token_scopes = self.scope.split() | ||
return {name: desc for name, desc in all_scopes.items() if name in token_scopes} | ||
|
||
@property | ||
def claims(self): | ||
key = jwk.JWK.from_pem(oauth2_settings.OIDC_RSA_PRIVATE_KEY.encode("utf8")) | ||
jwt_token = jwt.JWT(key=key, jwt=self.token) | ||
return json.loads(jwt_token.claims) | ||
|
||
def __str__(self): | ||
return self.token | ||
|
||
class Meta: | ||
abstract = True | ||
|
||
|
||
class IDToken(AbstractIDToken): | ||
class Meta(AbstractIDToken.Meta): | ||
swappable = "OAUTH2_PROVIDER_ID_TOKEN_MODEL" | ||
|
||
|
||
def get_application_model(): | ||
""" Return the Application model that is active in this project. """ | ||
return apps.get_model(oauth2_settings.APPLICATION_MODEL) | ||
|
@@ -430,6 +539,11 @@ def get_access_token_model(): | |
return apps.get_model(oauth2_settings.ACCESS_TOKEN_MODEL) | ||
|
||
|
||
def get_id_token_model(): | ||
""" Return the AccessToken model that is active in this project. """ | ||
return apps.get_model(oauth2_settings.ID_TOKEN_MODEL) | ||
|
||
|
||
def get_refresh_token_model(): | ||
""" Return the RefreshToken model that is active in this project. """ | ||
return apps.get_model(oauth2_settings.REFRESH_TOKEN_MODEL) | ||
|
Uh oh!
There was an error while loading. Please reload this page.