Implementing end_session_endpoint feature with post_logout_redirect_uri.
This commit is contained in:
parent
ecba16ed36
commit
5d07111a18
4 changed files with 105 additions and 26 deletions
|
@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### [Unreleased]
|
### [Unreleased]
|
||||||
|
|
||||||
|
##### Added
|
||||||
|
- Session Management 1.0 support.
|
||||||
|
|
||||||
|
##### Fixed
|
||||||
|
- Bug when trying authorize with response_type id_token without openid scope.
|
||||||
|
|
||||||
### [0.4.2] - 2016-10-13
|
### [0.4.2] - 2016-10-13
|
||||||
|
|
||||||
##### Added
|
##### Added
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.utils import timezone
|
||||||
from jwkest.jwk import RSAKey as jwk_RSAKey
|
from jwkest.jwk import RSAKey as jwk_RSAKey
|
||||||
from jwkest.jwk import SYMKey
|
from jwkest.jwk import SYMKey
|
||||||
from jwkest.jws import JWS
|
from jwkest.jws import JWS
|
||||||
|
from jwkest.jwt import JWT
|
||||||
|
|
||||||
from oidc_provider.lib.utils.common import get_issuer
|
from oidc_provider.lib.utils.common import get_issuer
|
||||||
from oidc_provider.models import (
|
from oidc_provider.models import (
|
||||||
|
@ -21,7 +22,6 @@ def create_id_token(user, aud, nonce, at_hash=None, request=None, scope=[]):
|
||||||
"""
|
"""
|
||||||
Creates the id_token dictionary.
|
Creates the id_token dictionary.
|
||||||
See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
||||||
|
|
||||||
Return a dic.
|
Return a dic.
|
||||||
"""
|
"""
|
||||||
sub = settings.get('OIDC_IDTOKEN_SUB_GENERATOR', import_str=True)(user=user)
|
sub = settings.get('OIDC_IDTOKEN_SUB_GENERATOR', import_str=True)(user=user)
|
||||||
|
@ -63,35 +63,34 @@ def create_id_token(user, aud, nonce, at_hash=None, request=None, scope=[]):
|
||||||
|
|
||||||
return dic
|
return dic
|
||||||
|
|
||||||
|
|
||||||
def encode_id_token(payload, client):
|
def encode_id_token(payload, client):
|
||||||
"""
|
"""
|
||||||
Represent the ID Token as a JSON Web Token (JWT).
|
Represent the ID Token as a JSON Web Token (JWT).
|
||||||
|
|
||||||
Return a hash.
|
Return a hash.
|
||||||
"""
|
"""
|
||||||
alg = client.jwt_alg
|
keys = get_client_alg_keys(client)
|
||||||
if alg == 'RS256':
|
_jws = JWS(payload, alg=client.jwt_alg)
|
||||||
keys = []
|
|
||||||
for rsakey in RSAKey.objects.all():
|
|
||||||
keys.append(jwk_RSAKey(key=importKey(rsakey.key), kid=rsakey.kid))
|
|
||||||
|
|
||||||
if not keys:
|
|
||||||
raise Exception('You must add at least one RSA Key.')
|
|
||||||
elif alg == 'HS256':
|
|
||||||
keys = [SYMKey(key=client.client_secret, alg=alg)]
|
|
||||||
else:
|
|
||||||
raise Exception('Unsupported key algorithm.')
|
|
||||||
|
|
||||||
_jws = JWS(payload, alg=alg)
|
|
||||||
|
|
||||||
return _jws.sign_compact(keys)
|
return _jws.sign_compact(keys)
|
||||||
|
|
||||||
|
def decode_id_token(token, client):
|
||||||
|
"""
|
||||||
|
Represent the ID Token as a JSON Web Token (JWT).
|
||||||
|
Return a hash.
|
||||||
|
"""
|
||||||
|
keys = get_client_alg_keys(client)
|
||||||
|
return JWS().verify_compact(token, keys=keys)
|
||||||
|
|
||||||
|
def client_id_from_id_token(id_token):
|
||||||
|
"""
|
||||||
|
Extracts the client id from a JSON Web Token (JWT).
|
||||||
|
Returns a string or None.
|
||||||
|
"""
|
||||||
|
payload = JWT().unpack(id_token).payload()
|
||||||
|
return payload.get('aud', None)
|
||||||
|
|
||||||
def create_token(user, client, scope, id_token_dic=None):
|
def create_token(user, client, scope, id_token_dic=None):
|
||||||
"""
|
"""
|
||||||
Create and populate a Token object.
|
Create and populate a Token object.
|
||||||
|
|
||||||
Return a Token object.
|
Return a Token object.
|
||||||
"""
|
"""
|
||||||
token = Token()
|
token = Token()
|
||||||
|
@ -109,12 +108,10 @@ def create_token(user, client, scope, id_token_dic=None):
|
||||||
|
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
def create_code(user, client, scope, nonce, is_authentication,
|
def create_code(user, client, scope, nonce, is_authentication,
|
||||||
code_challenge=None, code_challenge_method=None):
|
code_challenge=None, code_challenge_method=None):
|
||||||
"""
|
"""
|
||||||
Create and populate a Code object.
|
Create and populate a Code object.
|
||||||
|
|
||||||
Return a Code object.
|
Return a Code object.
|
||||||
"""
|
"""
|
||||||
code = Code()
|
code = Code()
|
||||||
|
@ -134,3 +131,21 @@ def create_code(user, client, scope, nonce, is_authentication,
|
||||||
code.is_authentication = is_authentication
|
code.is_authentication = is_authentication
|
||||||
|
|
||||||
return code
|
return code
|
||||||
|
|
||||||
|
def get_client_alg_keys(client):
|
||||||
|
"""
|
||||||
|
Takes a client and returns the set of keys associated with it.
|
||||||
|
Returns a list of keys.
|
||||||
|
"""
|
||||||
|
if client.jwt_alg == 'RS256':
|
||||||
|
keys = []
|
||||||
|
for rsakey in RSAKey.objects.all():
|
||||||
|
keys.append(jwk_RSAKey(key=importKey(rsakey.key), kid=rsakey.kid))
|
||||||
|
if not keys:
|
||||||
|
raise Exception('You must add at least one RSA Key.')
|
||||||
|
elif client.jwt_alg == 'HS256':
|
||||||
|
keys = [SYMKey(key=client.client_secret, alg=client.jwt_alg)]
|
||||||
|
else:
|
||||||
|
raise Exception('Unsupported key algorithm.')
|
||||||
|
|
||||||
|
return keys
|
||||||
|
|
20
oidc_provider/migrations/0020_post_logout_redirect_uris.py
Normal file
20
oidc_provider/migrations/0020_post_logout_redirect_uris.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.2 on 2016-10-31 18:16
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('oidc_provider', '0019_auto_20161005_1552'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='client',
|
||||||
|
name='_post_logout_redirect_uris',
|
||||||
|
field=models.TextField(blank=True, default=b'', help_text='Enter each URI on a new line.', verbose_name='Post Logout Redirect URIs'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,7 +1,15 @@
|
||||||
import logging
|
import logging
|
||||||
|
try:
|
||||||
|
from urllib import urlencode
|
||||||
|
from urlparse import urlsplit, parse_qs, urlunsplit
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import urlsplit, parse_qs, urlunsplit, urlencode
|
||||||
|
|
||||||
from Cryptodome.PublicKey import RSA
|
from Cryptodome.PublicKey import RSA
|
||||||
from django.contrib.auth.views import redirect_to_login, logout
|
from django.contrib.auth.views import (
|
||||||
|
redirect_to_login,
|
||||||
|
logout,
|
||||||
|
)
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
@ -21,9 +29,18 @@ from oidc_provider.lib.errors import (
|
||||||
RedirectUriError,
|
RedirectUriError,
|
||||||
TokenError,
|
TokenError,
|
||||||
)
|
)
|
||||||
from oidc_provider.lib.utils.common import redirect, get_site_url, get_issuer
|
from oidc_provider.lib.utils.common import (
|
||||||
|
redirect,
|
||||||
|
get_site_url,
|
||||||
|
get_issuer,
|
||||||
|
)
|
||||||
from oidc_provider.lib.utils.oauth2 import protected_resource_view
|
from oidc_provider.lib.utils.oauth2 import protected_resource_view
|
||||||
from oidc_provider.models import RESPONSE_TYPE_CHOICES, RSAKey
|
from oidc_provider.lib.utils.token import client_id_from_id_token
|
||||||
|
from oidc_provider.models import (
|
||||||
|
Client,
|
||||||
|
RESPONSE_TYPE_CHOICES,
|
||||||
|
RSAKey,
|
||||||
|
)
|
||||||
from oidc_provider import settings
|
from oidc_provider import settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -239,8 +256,29 @@ class JwksView(View):
|
||||||
class LogoutView(View):
|
class LogoutView(View):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
# We should actually verify if the requested redirect URI is safe
|
id_token_hint = request.GET.get('id_token_hint', '')
|
||||||
return logout(request, next_page=request.GET.get('post_logout_redirect_uri'))
|
post_logout_redirect_uri = request.GET.get('post_logout_redirect_uri', '')
|
||||||
|
state = request.GET.get('state', '')
|
||||||
|
|
||||||
|
next_page = settings.get('LOGIN_URL')
|
||||||
|
|
||||||
|
if id_token_hint:
|
||||||
|
client_id = client_id_from_id_token(id_token_hint)
|
||||||
|
try:
|
||||||
|
client = Client.objects.get(client_id=client_id)
|
||||||
|
if post_logout_redirect_uri in client.post_logout_redirect_uris:
|
||||||
|
if state:
|
||||||
|
uri = urlsplit(post_logout_redirect_uri)
|
||||||
|
query_params = parse_qs(uri.query)
|
||||||
|
query_params['state'] = state
|
||||||
|
uri = uri._replace(query=urlencode(query_params, doseq=True))
|
||||||
|
next_page = urlunsplit(uri)
|
||||||
|
else:
|
||||||
|
next_page = post_logout_redirect_uri
|
||||||
|
except Client.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return logout(request, next_page=next_page)
|
||||||
|
|
||||||
|
|
||||||
class CheckSessionIframeView(View):
|
class CheckSessionIframeView(View):
|
||||||
|
|
Loading…
Reference in a new issue