diff --git a/CHANGELOG.md b/CHANGELOG.md index 82c58f9..44cab22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. ### [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 ##### Added diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index 781dac0..50a92ae 100644 --- a/oidc_provider/lib/utils/token.py +++ b/oidc_provider/lib/utils/token.py @@ -7,6 +7,7 @@ from django.utils import timezone from jwkest.jwk import RSAKey as jwk_RSAKey from jwkest.jwk import SYMKey from jwkest.jws import JWS +from jwkest.jwt import JWT from oidc_provider.lib.utils.common import get_issuer 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. See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken - Return a dic. """ 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 - def encode_id_token(payload, client): """ Represent the ID Token as a JSON Web Token (JWT). - Return a hash. """ - alg = client.jwt_alg - if 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 alg == 'HS256': - keys = [SYMKey(key=client.client_secret, alg=alg)] - else: - raise Exception('Unsupported key algorithm.') - - _jws = JWS(payload, alg=alg) - + keys = get_client_alg_keys(client) + _jws = JWS(payload, alg=client.jwt_alg) 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): """ Create and populate a Token object. - Return a Token object. """ token = Token() @@ -109,12 +108,10 @@ def create_token(user, client, scope, id_token_dic=None): return token - def create_code(user, client, scope, nonce, is_authentication, code_challenge=None, code_challenge_method=None): """ Create and populate a Code object. - Return a Code object. """ code = Code() @@ -134,3 +131,21 @@ def create_code(user, client, scope, nonce, is_authentication, code.is_authentication = is_authentication 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 diff --git a/oidc_provider/migrations/0020_post_logout_redirect_uris.py b/oidc_provider/migrations/0020_post_logout_redirect_uris.py new file mode 100644 index 0000000..9390739 --- /dev/null +++ b/oidc_provider/migrations/0020_post_logout_redirect_uris.py @@ -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'), + ), + ] diff --git a/oidc_provider/views.py b/oidc_provider/views.py index 3220ded..cd82be3 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -1,7 +1,15 @@ 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 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.http import JsonResponse from django.shortcuts import render @@ -21,9 +29,18 @@ from oidc_provider.lib.errors import ( RedirectUriError, 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.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 @@ -239,8 +256,29 @@ class JwksView(View): class LogoutView(View): def get(self, request, *args, **kwargs): - # We should actually verify if the requested redirect URI is safe - return logout(request, next_page=request.GET.get('post_logout_redirect_uri')) + id_token_hint = request.GET.get('id_token_hint', '') + 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):