Skip to content
93 changes: 88 additions & 5 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,109 @@ pipeline {
sh 'toxtest/bin/pip install tox==3.28.0 setuptools pathlib2'
}
}
stage('Test'){
stage('Test 3.11'){
parallel {
stage('Unit Test Django 3.2'){
steps {
sh 'toxtest/bin/tox -e py3.12-django{3.2}'
sh 'toxtest/bin/tox -e py311-django{32}'
}
}
stage('Unit Test Django 4.0'){
steps {
sh 'toxtest/bin/tox -e py3.12-django{4.0}'
sh 'toxtest/bin/tox -e py311-django{40}'
}
}
stage('Unit Test Django 4.1'){
steps {
sh 'toxtest/bin/tox -e py3.12-django{4.1}'
sh 'toxtest/bin/tox -e py311-django{41}'
}
}
stage('Unit Test Django 4.2'){
steps {
sh 'toxtest/bin/tox -e py3.12-django{4.2}'
sh 'toxtest/bin/tox -e py311-django{42}'
}
}
stage('Unit Test Django 5.0'){
steps {
sh 'toxtest/bin/tox -e py311-django{50}'
}
}
stage('Unit Test Django 5.1'){
steps {
sh 'toxtest/bin/tox -e py311-django{51}'
}
}
stage('Unit Test Django 5.2'){
steps {
sh 'toxtest/bin/tox -e py311-django{52}'
}
}
}
}
stage('Test 3.12'){
parallel {
stage('Unit Test Django 3.2'){
steps {
sh 'toxtest/bin/tox -e py312-django{32}'
}
}
stage('Unit Test Django 4.0'){
steps {
sh 'toxtest/bin/tox -e py312-django{40}'
}
}
stage('Unit Test Django 4.1'){
steps {
sh 'toxtest/bin/tox -e py312-django{41}'
}
}
stage('Unit Test Django 4.2'){
steps {
sh 'toxtest/bin/tox -e py312-django{42}'
}
}
stage('Unit Test Django 5.0'){
steps {
sh 'toxtest/bin/tox -e py312-django{50}'
}
}
stage('Unit Test Django 5.1'){
steps {
sh 'toxtest/bin/tox -e py312-django{51}'
}
}
stage('Unit Test Django 5.2'){
steps {
sh 'toxtest/bin/tox -e py312-django{52}'
}
}
}
}
stage('Test 3.13'){
parallel {
stage('Unit Test Django 4.1'){
steps {
sh 'toxtest/bin/tox -e py313-django{41}'
}
}
stage('Unit Test Django 4.2'){
steps {
sh 'toxtest/bin/tox -e py313-django{42}'
}
}
stage('Unit Test Django 5.0'){
steps {
sh 'toxtest/bin/tox -e py313-django{50}'
}
}
stage('Unit Test Django 5.1'){
steps {
sh 'toxtest/bin/tox -e py313-django{51}'
}
}
stage('Unit Test Django 5.2'){
steps {
sh 'toxtest/bin/tox -e py313-django{52}'
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion provider/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@
ENFORCE_CLIENT_SECURE = getattr(settings, 'OAUTH_ENFORCE_CLIENT_SECURE', True)

SESSION_KEY = getattr(settings, 'OAUTH_SESSION_KEY', 'oauth')

TOKEN_PREFIX_LENGTH = getattr(settings, 'OAUTH_TOKEN_PREFIX_LENGTH', 4)
assert 0 < TOKEN_PREFIX_LENGTH <= 10, "OAUTH_TOKEN_PREFIX_LENGTH must be 10 or less"
6 changes: 3 additions & 3 deletions provider/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ class OAuthForm(forms.Form):
"""
def __init__(self, *args, **kwargs):
self.client = kwargs.pop('client', None)
super(OAuthForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)

def _clean_fields(self):
"""
Overriding the default cleaning behaviour to exit early on errors
instead of validating each field.
"""
try:
super(OAuthForm, self)._clean_fields()
super()._clean_fields()
except OAuthValidationError as e:
self._errors.update(e.args[0])

Expand All @@ -59,6 +59,6 @@ def _clean_form(self):
Overriding the default cleaning behaviour for a shallow error dict.
"""
try:
super(OAuthForm, self)._clean_form()
super()._clean_form()
except OAuthValidationError as e:
self._errors.update(e.args[0])
35 changes: 32 additions & 3 deletions provider/oauth2/admin.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
from django.contrib import admin
from django.contrib import messages
from django.utils.safestring import mark_safe

from provider.oauth2 import models
from provider.oauth2.forms import ClientSecretAdminCreateForm


class AccessTokenAdmin(admin.ModelAdmin):
list_display = ('user', 'client', 'token', 'expires',)
list_display = ('user', 'client', 'token_prefix', 'expires',)
raw_id_fields = ('user',)


class RefreshTokenAdmin(admin.ModelAdmin):
list_display = ('__str__', 'access_token', 'user', 'client', 'expired')
raw_id_fields = ('user',)


Expand All @@ -13,11 +22,30 @@ class GrantAdmin(admin.ModelAdmin):


class ClientAdmin(admin.ModelAdmin):
list_display = ('url', 'user', 'redirect_uri', 'client_id',
list_display = ('name', 'user', 'redirect_uri', 'client_id',
'client_type', 'auto_authorize')
raw_id_fields = ('user',)


class ClientSecretAdmin(admin.ModelAdmin):
list_display = ('client_name', 'client_id', 'secret_prefix', 'description', 'expiration_date')

def get_form(self, request, obj=None, change=None, **kwargs):
kwargs["form"] = ClientSecretAdminCreateForm
return super().get_form(request, obj=obj, change=change, **kwargs)

def save_form(self, request, form, change):
if form.plain_client_secret:
messages.info(request, mark_safe(f"New client secret created for client {form.instance.client.client_id}: {form.plain_client_secret}<br/>This will not be shown again."))
return super().save_form(request, form, change)

def client_id(self, obj):
return obj.client.client_id

def client_name(self, obj):
return obj.client.name


class AuthorizedClientAdmin(admin.ModelAdmin):
list_display = ('user', 'client', 'authorized_at')
raw_id_fields = ('user',)
Expand All @@ -31,7 +59,8 @@ class AwsAccountAdmin(admin.ModelAdmin):
admin.site.register(models.AccessToken, AccessTokenAdmin)
admin.site.register(models.Grant, GrantAdmin)
admin.site.register(models.Client, ClientAdmin)
admin.site.register(models.ClientSecret, ClientSecretAdmin)
admin.site.register(models.AuthorizedClient, AuthorizedClientAdmin)
admin.site.register(models.AwsAccount, AwsAccountAdmin)
admin.site.register(models.RefreshToken)
admin.site.register(models.RefreshToken, RefreshTokenAdmin)
admin.site.register(models.Scope)
1 change: 1 addition & 0 deletions provider/oauth2/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class Oauth2(AppConfig):
name = 'provider.oauth2'
label = 'oauth2'
verbose_name = "Provider Oauth2"
default_auto_field = "django.db.models.AutoField"

def ready(self):
import provider.oauth2.signals
6 changes: 3 additions & 3 deletions provider/oauth2/fixtures/test_oauth2.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"is_superuser": true,
"last_login": "2012-01-23 05:52:32",
"last_name": "",
"password": "sha1$da29e$498b9faab2d002183bc1d874689634b0e15ad6d7",
"password": "pbkdf2_sha256$600000$X9VBl26TIACb4rC7RYDXVa$/MgXp49qusu4+nPJ6BAMf0ti8iR3dLt4DnAchmA5FUo=",
"user_permissions": [],
"username": "test-user-1"
},
Expand All @@ -98,7 +98,7 @@
"is_superuser": false,
"last_login": "2012-01-23 05:53:31",
"last_name": "",
"password": "sha1$0cf1b$d66589690edd96b410170fcae5cc2bdfb68821e7",
"password": "pbkdf2_sha256$600000$X9VBl26TIACb4rC7RYDXVa$/MgXp49qusu4+nPJ6BAMf0ti8iR3dLt4DnAchmA5FUo=",
"user_permissions": [],
"username": "test-user-2"
},
Expand All @@ -116,7 +116,7 @@
"is_superuser": false,
"last_login": "2012-01-23 05:53:31",
"last_name": "",
"password": "sha1$0cf1b$d66589690edd96b410170fcae5cc2bdfb68821e7",
"password": "pbkdf2_sha256$600000$X9VBl26TIACb4rC7RYDXVa$/MgXp49qusu4+nPJ6BAMf0ti8iR3dLt4DnAchmA5FUo=",
"user_permissions": [],
"username": "test-user-aws"
},
Expand Down
45 changes: 37 additions & 8 deletions provider/oauth2/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from django.utils import timezone
from provider.constants import RESPONSE_TYPE_CHOICES, PKCE, PUBLIC
from provider.forms import OAuthForm, OAuthValidationError
from provider.utils import now, ArnHelper
from provider.oauth2.models import Client, Grant, RefreshToken, Scope
from provider.utils import now, ArnHelper, make_client_secret
from provider.oauth2.models import Client, Grant, RefreshToken, Scope, ClientSecret

log = logging.getLogger('provider.oauth2')

Expand Down Expand Up @@ -42,13 +42,26 @@ class ClientAuthForm(forms.Form):
client_secret = forms.CharField()

def clean(self):

data = self.cleaned_data
client = None
try:
client = Client.objects.get(client_id=data.get('client_id'),
client_secret=data.get('client_secret'))
client = Client.objects.get(
client_id=data.get('client_id'),
client_secret=data.get('client_secret'),
)
except Client.DoesNotExist:
raise forms.ValidationError(_("Client could not be validated with "
"key pair."))
try:
client_secret = ClientSecret.objects.get_by_secret(
data.get('client_id'),
data.get('client_secret'),
)
client = client_secret.client
except ClientSecret.DoesNotExist:
pass

if not client:
raise forms.ValidationError(_("Client could not be validated with key pair."))

data['client'] = client
return data
Expand Down Expand Up @@ -247,8 +260,8 @@ def clean_refresh_token(self):
raise OAuthValidationError({'error': 'invalid_request'})

try:
token = RefreshToken.objects.get(token=token,
expired=False, client=self.client)
token = RefreshToken.objects.get_token(token=token,
expired=False, client=self.client)
except RefreshToken.DoesNotExist:
raise OAuthValidationError({'error': 'invalid_grant'})

Expand Down Expand Up @@ -462,3 +475,19 @@ def clean(self):
data['client'] = client
data['grant'] = grant
return data


class ClientSecretAdminCreateForm(forms.ModelForm):
class Meta:
model = ClientSecret
fields = ['description', 'client', 'expiration_date']

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.plain_client_secret = None
if not self.instance.secret_prefix or not self.instance.secret_hash:
new_secret, prefix, secret_hash = make_client_secret()
self.plain_client_secret = new_secret
self.instance.secret_prefix = prefix
self.instance.secret_hash = secret_hash
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from django.core.management import BaseCommand
from django.contrib.auth.hashers import make_password

from provider.oauth2.models import AccessToken, RefreshToken
from provider.constants import TOKEN_PREFIX_LENGTH


class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('--save', action='store_true', default=False,
help="Save changes (defaults to showing what would be migrated otherwise)")

def handle(self, *args, **options):
if not options.get('save'):
self.stderr.write("Token migration changes will be shown, but not saved. Run with --save to actually migrate.")
at_qs = AccessToken.objects.filter(token_prefix__isnull=True)
self.stdout.write(f"Found {at_qs.count()} AccessTokens to migrate.")
for access_token in at_qs:
prefix = access_token.token[:TOKEN_PREFIX_LENGTH]
hashed = make_password(access_token.token)
self.stdout.write(f" Converting AccessToken {prefix}: {hashed}\n")
access_token.token_prefix = prefix
access_token.token = hashed
if options.get('save'):
access_token.save()


rt_qs = RefreshToken.objects.filter(token_prefix__isnull=True, expired=False)
self.stdout.write(f"Found {rt_qs.count()} RefreshTokens to migrate.")
for refresh_token in rt_qs:
prefix = refresh_token.token[:TOKEN_PREFIX_LENGTH]
hashed = make_password(refresh_token.token)
self.stdout.write(f" Converting AccessToken {prefix}: {hashed}\n")
refresh_token.token_prefix = prefix
refresh_token.token = hashed
if options.get('save'):
refresh_token.save()
29 changes: 29 additions & 0 deletions provider/oauth2/migrations/0006_clientsecrets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 4.2 on 2025-08-17 03:13

from django.db import migrations, models
import django.db.models.deletion
import provider.utils


class Migration(migrations.Migration):

dependencies = [
('oauth2', '0005_pkce'),
]

operations = [
migrations.CreateModel(
name='ClientSecret',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.CharField(default=provider.utils.client_secret_description, max_length=255)),
('secret_prefix', models.CharField(db_index=True, max_length=6)),
('secret_hash', models.CharField(max_length=255)),
('expiration_date', models.DateField(blank=True, null=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oauth2.client')),
],
options={
'db_table': 'oauth2_clientsecret',
},
),
]
Loading