Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
33fce78
Add OpenID connect hybrid grant type
wiliamsouza Oct 29, 2017
f48a950
Add OpenID connect algorithm type to Application model
wiliamsouza Oct 29, 2017
583adab
Add OpenID connect id token model
wiliamsouza Oct 29, 2017
e381fa2
Add nonce Authorization as required by OpenID connect Implicit Flow
wiliamsouza Oct 29, 2017
c0ffa13
Add body to create_authorization_response to pass nonce and future Op…
wiliamsouza Oct 29, 2017
571ced5
Add OpenID connect ID token creation and validation methods and scopes
wiliamsouza Oct 29, 2017
521710f
Add OpenID connect response types
wiliamsouza Oct 29, 2017
ff10cf4
Add OpenID connect authorization code flow test
wiliamsouza Oct 29, 2017
e28b8e3
Add OpenID connect implicit flow tests
wiliamsouza Oct 29, 2017
9680cc3
Add validate_user_match method to OAuth2Validator
wiliamsouza Oct 29, 2017
f35889f
Add RSA_PRIVATE_KEY setting with blank value
wiliamsouza Oct 29, 2017
c773602
Update tox
wiliamsouza Oct 29, 2017
a9fbcee
Add get_jwt_bearer_token to OAuth2Validator
wiliamsouza Dec 19, 2017
a482133
Add validate_jwt_bearer_token to OAuth2Validator
wiliamsouza Dec 19, 2017
ee1e864
Change OAuth2Validator.validate_id_token default return value to Fals…
wiliamsouza Dec 19, 2017
c41b412
Change to use .encode to avoid py2.7 tox test error
wiliamsouza Jan 15, 2018
e16c1d2
Add OpenID connect hybrid flow tests
wiliamsouza Jan 15, 2018
effe816
Change to use .encode to avoid py2.7 tox test error
wiliamsouza Jan 16, 2018
94cef59
Add RSA_PRIVATE_KEY to the list of settings that cannot be empt
wiliamsouza Jan 18, 2018
eb04c75
Add support for oidc connect discovery
allisson Mar 14, 2018
d808723
Use double quotes for strings
allisson Mar 14, 2018
d9e4511
Rename migrations to avoid name and order conflict
wiliamsouza Jan 16, 2019
8167ed5
Remove commando to install OAuthLib from master and removed jwcrypto …
wiliamsouza Jan 16, 2019
80296db
Remove python 2 compatible code
wiliamsouza Jan 16, 2019
bb7b773
Change errors access_denied/unauthorized_client/consent_required/logi…
wiliamsouza Jan 16, 2019
10cb7ef
Change iss claim value to come from settings
wiliamsouza Jan 16, 2019
6359fc5
Change to use openid connect code server class
wiliamsouza Jan 16, 2019
3432d49
Change test to include missing state
wiliamsouza Jan 16, 2019
1abc97b
Add id_token relation to AbstractAccessToken
wiliamsouza Jan 29, 2019
5ebdf64
Add claims property to AbstractIDToken
wiliamsouza Jan 29, 2019
18c1fd3
Change OAuth2Validator._create_access_token to save id_token to acces…
wiliamsouza Jan 29, 2019
4246977
Add userinfo endpoint
wiliamsouza Jan 29, 2019
508e9fc
Update migrations and remove oauthlib duplication
wiliamsouza Apr 10, 2019
71505ad
Remove old generated migrations
wiliamsouza Apr 16, 2019
78df470
Add new migrations
wiliamsouza Apr 16, 2019
bd8f3f5
Fix tests
wiliamsouza Apr 16, 2019
b5d05f6
Add nonce to hybrid tests
wiliamsouza Jul 13, 2019
87d300a
Add missing new attributes to test migration
fvlima Mar 1, 2020
2f38690
Rebase fixing conflicts and tests
wiliamsouza Mar 2, 2020
bc065e9
Remove auto generate message
wiliamsouza Mar 2, 2020
dd47f20
Fix flake8 issues
fvlima Apr 26, 2020
00d477e
Fix test doc deps
fvlima Apr 26, 2020
fd6f2e5
Add project settings to be ignored in coverage
fvlima Apr 27, 2020
0be7230
Tweak migrations to support non-overidden models
shauns Aug 13, 2020
c7ceb0f
OIDC_USERINFO_ENDPOINT is not mandatory
shauns Aug 13, 2020
0ff5118
refresh_token grant should be support for OpenID hybrid
shauns Aug 13, 2020
db9c295
Fix the user info view, and remove hard dependency on DRF
shauns Aug 13, 2020
dacb9c1
Use proper URL generation for OIDC endpoints
shauns Aug 14, 2020
e80e4a3
Support rich ID tokens and userinfo claims
shauns Aug 14, 2020
cd53095
Bug fix for at_hash generation
shauns Aug 14, 2020
d4b308b
OIDC_ISS_ENDPOINT is an optional setting
shauns Aug 14, 2020
1f44d42
Support OIDC urls from issuer url if provided
shauns Aug 17, 2020
d8e3e93
Test for generated OIDC urls
shauns Aug 17, 2020
87191b8
Flake
shauns Aug 17, 2020
6b0bc35
Rebase on master and migrate url function to re_path
thinkwelltwd Aug 28, 2020
c93d677
Handle invalid token format exceptions as invalid tokens
thinkwelltwd Aug 28, 2020
1e8d66b
Merge migrations and sort imports isort for flake8 lint check
thinkwelltwd Aug 28, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ __pycache__
pip-log.txt

# Unit test / coverage reports
.cache
.pytest_cache
.coverage
.tox
.pytest_cache/
Expand Down
9 changes: 8 additions & 1 deletion oauth2_provider/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from .models import (
get_access_token_model, get_application_model,
get_grant_model, get_refresh_token_model
get_grant_model, get_id_token_model, get_refresh_token_model
)


Expand All @@ -26,6 +26,11 @@ class AccessTokenAdmin(admin.ModelAdmin):
raw_id_fields = ("user", "source_refresh_token")


class IDTokenAdmin(admin.ModelAdmin):
list_display = ("token", "user", "application", "expires")
raw_id_fields = ("user", )


class RefreshTokenAdmin(admin.ModelAdmin):
list_display = ("token", "user", "application")
raw_id_fields = ("user", "access_token")
Expand All @@ -34,9 +39,11 @@ class RefreshTokenAdmin(admin.ModelAdmin):
Application = get_application_model()
Grant = get_grant_model()
AccessToken = get_access_token_model()
IDToken = get_id_token_model()
RefreshToken = get_refresh_token_model()

admin.site.register(Application, ApplicationAdmin)
admin.site.register(Grant, GrantAdmin)
admin.site.register(AccessToken, AccessTokenAdmin)
admin.site.register(IDToken, IDTokenAdmin)
admin.site.register(RefreshToken, RefreshTokenAdmin)
1 change: 1 addition & 0 deletions oauth2_provider/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class AllowForm(forms.Form):
allow = forms.BooleanField(required=False)
redirect_uri = forms.CharField(widget=forms.HiddenInput())
scope = forms.CharField(widget=forms.HiddenInput())
nonce = forms.CharField(required=False, widget=forms.HiddenInput())
client_id = forms.CharField(widget=forms.HiddenInput())
state = forms.CharField(required=False, widget=forms.HiddenInput())
response_type = forms.CharField(widget=forms.HiddenInput())
Expand Down
2 changes: 0 additions & 2 deletions oauth2_provider/migrations/0002_auto_20190406_1805.py
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


Expand Down
48 changes: 48 additions & 0 deletions oauth2_provider/migrations/0003_auto_20200902_2022.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion

from oauth2_provider.settings import oauth2_settings


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('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),
),
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=oauth2_settings.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',
},
),
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=oauth2_settings.ID_TOKEN_MODEL),
),
]
114 changes: 114 additions & 0 deletions oauth2_provider/models.py
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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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)
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)
Expand All @@ -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)
Expand Down
26 changes: 21 additions & 5 deletions oauth2_provider/oauth2_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,16 @@ def validate_authorization_request(self, request):
except oauth2.OAuth2Error as error:
raise OAuthToolkitError(error=error)

def create_authorization_response(self, request, scopes, credentials, allow):
def create_authorization_response(self, uri, request, scopes, credentials, body, allow):
"""
A wrapper method that calls create_authorization_response on `server_class`
instance.

:param request: The current django.http.HttpRequest object
:param scopes: A list of provided scopes
:param credentials: Authorization credentials dictionary containing
`client_id`, `state`, `redirect_uri`, `response_type`
`client_id`, `state`, `redirect_uri` and `response_type`
:param body: Other body parameters not used in credentials dictionary
:param allow: True if the user authorize the client, otherwise False
"""
try:
Expand All @@ -124,10 +125,10 @@ def create_authorization_response(self, request, scopes, credentials, allow):
credentials["user"] = request.user

headers, body, status = self.server.create_authorization_response(
uri=credentials["redirect_uri"], scopes=scopes, credentials=credentials)
uri = headers.get("Location", None)
uri=uri, scopes=scopes, credentials=credentials, body=body)
redirect_uri = headers.get("Location", None)

return uri, headers, body, status
return redirect_uri, headers, body, status

except oauth2.FatalClientError as error:
raise FatalClientError(
Expand Down Expand Up @@ -166,6 +167,21 @@ def create_revocation_response(self, request):

return uri, headers, body, status

def create_userinfo_response(self, request):
"""
A wrapper method that calls create_userinfo_response on a
`server_class` instance.

:param request: The current django.http.HttpRequest object
"""
uri, http_method, body, headers = self._extract_params(request)
headers, body, status = self.server.create_userinfo_response(
uri, http_method, body, headers
)
uri = headers.get("Location", None)

return uri, headers, body, status

def verify_request(self, request, scopes):
"""
A wrapper method that calls verify_request on `server_class` instance.
Expand Down
Loading