Implementing end_session_endpoint feature with post_logout_redirect_uri.

This commit is contained in:
Ignacio Fiorentino 2016-10-31 17:07:06 -03:00
parent ecba16ed36
commit 5d07111a18
4 changed files with 105 additions and 26 deletions

View file

@ -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

View file

@ -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

View 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'),
),
]

View file

@ -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):