Skip to content

Expire refresh token #347

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

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion oidc_provider/lib/endpoints/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,15 @@ def validate_params(self):
try:
self.token = Token.objects.get(refresh_token=self.params['refresh_token'],
client=self.client)

except Token.DoesNotExist:
logger.debug(
'[Token] Refresh token does not exist: %s', self.params['refresh_token'])
raise TokenError('invalid_grant')

if self.token.has_expired_refresh_token():
logger.debug(
'[Token] Refresh token expired: %s', self.params['refresh_token'])
raise TokenError('invalid_token')
elif self.params['grant_type'] == 'client_credentials':
if not self.client._scope:
logger.debug('[Token] Client using client credentials with empty scope')
Expand Down
9 changes: 4 additions & 5 deletions oidc_provider/lib/utils/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from oidc_provider.lib.errors import BearerTokenError
from oidc_provider.models import Token


logger = logging.getLogger(__name__)


Expand All @@ -21,7 +20,7 @@ def extract_access_token(request):
"""
auth_header = request.META.get('HTTP_AUTHORIZATION', '')

if re.compile('^[Bb]earer\s{1}.+$').match(auth_header):
if re.compile(r'^[Bb]earer\s{1}.+$').match(auth_header):
access_token = auth_header.split()[1]
else:
access_token = request.GET.get('access_token', '')
Expand All @@ -39,7 +38,7 @@ def extract_client_auth(request):
"""
auth_header = request.META.get('HTTP_AUTHORIZATION', '')

if re.compile('^Basic\s{1}.+$').match(auth_header):
if re.compile(r'^Basic\s{1}.+$').match(auth_header):
b64_user_pass = auth_header.split()[1]
try:
user_pass = b64decode(b64_user_pass).decode('utf-8').split(':')
Expand All @@ -63,7 +62,7 @@ def protected_resource_view(scopes=None):
scopes = []

def wrapper(view):
def view_wrapper(request, *args, **kwargs):
def view_wrapper(request, *args, **kwargs):
access_token = extract_access_token(request)

try:
Expand All @@ -86,7 +85,7 @@ def view_wrapper(request, *args, **kwargs):
error.code, error.description)
return response

return view(request, *args, **kwargs)
return view(request, *args, **kwargs)

return view_wrapper

Expand Down
7 changes: 4 additions & 3 deletions oidc_provider/lib/utils/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,12 @@ def create_id_token(token, user, aud, nonce='', at_hash='', request=None, scope=

# Inlude (or not) user standard claims in the id_token.
if settings.get('OIDC_IDTOKEN_INCLUDE_CLAIMS'):
standard_claims = StandardScopeClaims(token)
dic.update(standard_claims.create_response_dic())
if settings.get('OIDC_EXTRA_SCOPE_CLAIMS'):
custom_claims = settings.get('OIDC_EXTRA_SCOPE_CLAIMS', import_str=True)(token)
dic.update(custom_claims.create_response_dic())
claims = custom_claims.create_response_dic()
else:
claims = StandardScopeClaims(token).create_response_dic()
dic.update(claims)

dic = run_processing_hook(
dic, 'OIDC_IDTOKEN_PROCESSING_HOOK',
Expand Down
21 changes: 20 additions & 1 deletion oidc_provider/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import binascii
from hashlib import md5, sha256
import json
import datetime

from django.db import models
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.conf import settings

from . import settings as oidc_settings

CLIENT_TYPE_CHOICES = [
('confidential', 'Confidential'),
Expand Down Expand Up @@ -217,6 +218,24 @@ class Meta:
def id_token(self):
return json.loads(self._id_token) if self._id_token else None

def has_expired_refresh_token(self):
if not oidc_settings.get('OIDC_REFRESH_TOKEN_EXPIRE'):
return False

if oidc_settings.get('OIDC_REFRESH_TOKEN_EXPIRE') < oidc_settings.get('OIDC_TOKEN_EXPIRE'):
raise ValueError('Invalid setting for OIDC_REFRESH_TOKEN_EXPIRE')

# Note: Increasing expiration time settings could make previously
# expired refresh tokens usable. Therefore, clear all the
# refresh tokens when increasing refresh token expire time.
offset = (
oidc_settings.get('OIDC_REFRESH_TOKEN_EXPIRE')
- oidc_settings.get('OIDC_TOKEN_EXPIRE')
)
expires_at = self.expires_at + datetime.timedelta(seconds=offset)

return timezone.now() >= expires_at

@id_token.setter
def id_token(self, value):
self._id_token = json.dumps(value)
Expand Down
13 changes: 13 additions & 0 deletions oidc_provider/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,19 @@ def OIDC_TOKEN_EXPIRE(self):
"""
return 60*60

@property
def OIDC_REFRESH_TOKEN_EXPIRE(self):
"""
OPTIONAL. Refresh token expiration time expressed in seconds.

Zero: Refresh token doesn't expire.
Note: Increasing expiration time settings could make previously
expired refresh tokens usable. Hence, increase expiry time with care.
Ex: delete existing refresh tokens before increasing
refresh token expire time.
"""
return 0

@property
def OIDC_USERINFO(self):
"""
Expand Down
50 changes: 50 additions & 0 deletions oidc_provider/tests/cases/test_token_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import uuid

from base64 import b64encode
from datetime import timedelta

from django.utils import timezone

try:
from urllib.parse import urlencode
Expand Down Expand Up @@ -429,6 +432,53 @@ def do_refresh_token_check(self, scope=None):
response = self._post_request(post_data)
self.assertIn('invalid_grant', response.content.decode('utf-8'))

# Refresh token doesn't expires with default settings
post_data = self._refresh_token_post_data(response_dic2['refresh_token'], scope)
expiry_time = timezone.now() + timedelta(
seconds=24*60*60,
)
with patch('oidc_provider.models.timezone.now') as time_func:
time_func.return_value = expiry_time
response = self._post_request(post_data)
self.assertIn('refresh_token', response.content.decode('utf-8'))

@override_settings(OIDC_IDTOKEN_INCLUDE_CLAIMS=True)
@override_settings(OIDC_REFRESH_TOKEN_EXPIRE=24 * 60 * 60)
def test_refresh_token_expiry(self):
"""
Refresh token expiry
Make sure that refresh token expires properly.
"""
response = self._refresh_request(elapsed_time=24 * 60 * 60)
self.assertIn('invalid_token', response.content.decode('utf-8'))

@override_settings(OIDC_IDTOKEN_INCLUDE_CLAIMS=True)
@override_settings(OIDC_REFRESH_TOKEN_EXPIRE=24 * 60 * 60)
def test_refresh_token_not_expired(self, scope=None):
"""
Refresh token expiry
Make sure that refresh token is not expired.
"""
response = self._refresh_request(elapsed_time=(24 * 60 * 60) - 1)
self.assertIn('id_token', response.content.decode('utf-8'))

def _refresh_request(self, elapsed_time):
code = self._create_code()
self.assertEqual(code.scope, ['openid', 'email'])
post_data = self._auth_code_post_data(code=code.code)
response = self._post_request(post_data)
response_dic1 = json.loads(response.content.decode('utf-8'))
# Use refresh token to obtain new token
post_data = self._refresh_token_post_data(
response_dic1['refresh_token'], code.scope
)
current_time = timezone.now() + timedelta(seconds=elapsed_time)
with patch('oidc_provider.models.timezone.now') as time_func:
time_func.return_value = current_time
response = self._post_request(post_data)

return response

def test_client_redirect_uri(self):
"""
Validate that client redirect URIs exactly match registered
Expand Down