From 998ea5fcd1133054df07a420caae04fa3ec9289d Mon Sep 17 00:00:00 2001 From: juanifioren Date: Mon, 25 Jan 2016 17:52:24 -0300 Subject: [PATCH] Implementation of RSA Keys using Models. Also providing DOC. --- DOC.md | 31 ++++++++++++++++++- oidc_provider/admin.py | 9 +++++- oidc_provider/lib/utils/common.py | 14 --------- oidc_provider/lib/utils/token.py | 15 ++++++--- .../management/commands/creatersakey.py | 11 ++++--- oidc_provider/migrations/0008_rsakey.py | 22 +++++++++++++ oidc_provider/models.py | 22 ++++++++++++- oidc_provider/settings.py | 7 ----- .../app/{OIDC_RSA_KEY.pem => RSAKEY.pem} | 0 oidc_provider/tests/app/utils.py | 16 +++++++++- .../tests/test_authorize_endpoint.py | 1 + .../tests/test_creatersakey_command.py | 1 + oidc_provider/tests/test_token_endpoint.py | 3 +- oidc_provider/views.py | 26 +++++++--------- 14 files changed, 129 insertions(+), 49 deletions(-) create mode 100644 oidc_provider/migrations/0008_rsakey.py rename oidc_provider/tests/app/{OIDC_RSA_KEY.pem => RSAKEY.pem} (100%) diff --git a/DOC.md b/DOC.md index 107bf8d..f7ba893 100644 --- a/DOC.md +++ b/DOC.md @@ -18,6 +18,7 @@ Before getting started there are some important things that you should know: - [Requirements](#requirements) - [Installation](#installation) - [Users And Clients](#users-and-clients) +- [Server RSA Keys](#rsa-keys) - [Templates](#templates) - [Standard Claims](#standard-claims) - [Server Endpoints](#server-endpoints) @@ -31,7 +32,6 @@ Before getting started there are some important things that you should know: - [OIDC_EXTRA_SCOPE_CLAIMS](#oidc_extra_scope_claims) - [OIDC_IDTOKEN_EXPIRE](#oidc_idtoken_expire) - [OIDC_IDTOKEN_SUB_GENERATOR](#oidc_idtoken_sub_generator) - - [OIDC_RSA_KEY_FOLDER](#oidc_rsa_key_folder) - [OIDC_SKIP_CONSENT_ENABLE](#oidc_skip_consent_enable) - [OIDC_SKIP_CONSENT_EXPIRE](#oidc_skip_consent_expire) - [OIDC_TOKEN_EXPIRE](#oidc_token_expire) @@ -122,6 +122,35 @@ Or create a client with Django shell: ``python manage.py shell``: >>> c.save() ``` +## Server RSA Keys + +Server keys are used to sign/encrypt ID Tokens. These keys are stored in the `RSAKey` model. So the package will automatically generate public keys and expose them in the `jwks_uri` endpoint. + +You can easily create them with the admin: + +![RSAKey Creation](http://i64.tinypic.com/vj2ma.png) + +Or use `python manage.py creatersakey` command. + +```curl +GET /openid/jwks HTTP/1.1 +Host: localhost:8000 +``` +```json +{ + "keys":[ + { + "use":"sig", + "e":"AQAB", + "kty":"RSA", + "alg":"RS256", + "n":"3Gm0pS7ij_SnY96wkbaki74MUYJrobXecO6xJhvmAEEhMHGpO0m4H2nbOWTf6Jc1FiiSvgvhObVk9xPOM6qMTQ5D5pfWZjNk99qDJXvAE4ImM8S0kCaBJGT6e8JbuDllCUq8aL71t67DhzbnoBsKCnVOE1GJffpMcDdBUYkAsx8", + "kid":"a38ea7fbf944cc060eaf5acc1956b0e3" + } + ] +} +``` + ## Templates Add your own templates files inside a folder named ``templates/oidc_provider/``. diff --git a/oidc_provider/admin.py b/oidc_provider/admin.py index 140ad74..d46fc8f 100644 --- a/oidc_provider/admin.py +++ b/oidc_provider/admin.py @@ -5,7 +5,8 @@ from uuid import uuid4 from django.forms import ModelForm from django.contrib import admin -from oidc_provider.models import Client, Code, Token +from oidc_provider.models import Client, Code, Token, RSAKey + class ClientForm(ModelForm): @@ -56,3 +57,9 @@ class TokenAdmin(admin.ModelAdmin): def has_add_permission(self, request): return False + + +@admin.register(RSAKey) +class RSAKeyAdmin(admin.ModelAdmin): + + readonly_fields = ['kid'] diff --git a/oidc_provider/lib/utils/common.py b/oidc_provider/lib/utils/common.py index 04191cc..2a529d9 100644 --- a/oidc_provider/lib/utils/common.py +++ b/oidc_provider/lib/utils/common.py @@ -27,20 +27,6 @@ def get_issuer(): return issuer -def get_rsa_key(): - """ - Load the rsa key previously created with `creatersakey` command. - """ - file_path = settings.get('OIDC_RSA_KEY_FOLDER') + '/OIDC_RSA_KEY.pem' - try: - with open(file_path, 'r') as f: - key = f.read() - except IOError: - raise IOError('We could not find your key file on: ' + file_path) - - return key - - class DefaultUserInfo(object): """ Default class for setting OIDC_USERINFO. diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index de850ba..9ad6f86 100644 --- a/oidc_provider/lib/utils/token.py +++ b/oidc_provider/lib/utils/token.py @@ -5,10 +5,10 @@ import uuid from Crypto.PublicKey.RSA import importKey from django.utils import timezone from hashlib import md5 -from jwkest.jwk import RSAKey +from jwkest.jwk import RSAKey as jwk_RSAKey from jwkest.jws import JWS -from oidc_provider.lib.utils.common import get_issuer, get_rsa_key +from oidc_provider.lib.utils.common import get_issuer from oidc_provider.models import * from oidc_provider import settings @@ -53,9 +53,16 @@ def encode_id_token(payload): Return a hash. """ - key_string = get_rsa_key().encode('utf-8') - keys = [ RSAKey(key=importKey(key_string), kid=md5(key_string).hexdigest()) ] + 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.') + _jws = JWS(payload, alg='RS256') + return _jws.sign_compact(keys) diff --git a/oidc_provider/management/commands/creatersakey.py b/oidc_provider/management/commands/creatersakey.py index 2f4d29f..1768f5e 100644 --- a/oidc_provider/management/commands/creatersakey.py +++ b/oidc_provider/management/commands/creatersakey.py @@ -1,8 +1,10 @@ import os + from Crypto.PublicKey import RSA +from django.core.management.base import BaseCommand from oidc_provider import settings -from django.core.management.base import BaseCommand +from oidc_provider.models import RSAKey class Command(BaseCommand): @@ -11,9 +13,8 @@ class Command(BaseCommand): def handle(self, *args, **options): try: key = RSA.generate(1024) - file_path = os.path.join(settings.get('OIDC_RSA_KEY_FOLDER'), 'OIDC_RSA_KEY.pem') - with open(file_path, 'wb') as f: - f.write(key.exportKey('PEM')) - self.stdout.write('RSA key successfully created at: ' + file_path) + rsakey = RSAKey(key=key.exportKey('PEM').decode('utf8')) + rsakey.save() + self.stdout.write(u'RSA key successfully created with kid: {0}'.format(rsakey.kid)) except Exception as e: self.stdout.write('Something goes wrong: {0}'.format(e)) diff --git a/oidc_provider/migrations/0008_rsakey.py b/oidc_provider/migrations/0008_rsakey.py new file mode 100644 index 0000000..6c76d6d --- /dev/null +++ b/oidc_provider/migrations/0008_rsakey.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2016-01-25 17:48 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('oidc_provider', '0007_auto_20160111_1844'), + ] + + operations = [ + migrations.CreateModel( + name='RSAKey', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.TextField()), + ], + ), + ] diff --git a/oidc_provider/models.py b/oidc_provider/models.py index c7cb0ed..5cdd9ab 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +from hashlib import md5 import json from django.db import models @@ -27,7 +29,6 @@ class Client(models.Model): verbose_name = _(u'Client') verbose_name_plural = _(u'Clients') - def __str__(self): return u'{0}'.format(self.name) @@ -107,3 +108,22 @@ class UserConsent(BaseCodeTokenModel): class Meta: unique_together = ('user', 'client') + + +class RSAKey(models.Model): + + key = models.TextField(help_text=_(u'Paste your private RSA Key here.')) + + class Meta: + verbose_name = _(u'RSA Key') + verbose_name_plural = _(u'RSA Keys') + + def __str__(self): + return u'{0}'.format(self.kid) + + def __unicode__(self): + return self.__str__() + + @property + def kid(self): + return u'{0}'.format(md5(self.key.encode('utf-8')).hexdigest() if self.key else '') diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index 5586c22..a52e4ec 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -58,13 +58,6 @@ class DefaultSettings(object): """ return 'oidc_provider.lib.utils.common.default_sub_generator' - @property - def OIDC_RSA_KEY_FOLDER(self): - """ - REQUIRED. - """ - return None - @property def OIDC_SKIP_CONSENT_ENABLE(self): """ diff --git a/oidc_provider/tests/app/OIDC_RSA_KEY.pem b/oidc_provider/tests/app/RSAKEY.pem similarity index 100% rename from oidc_provider/tests/app/OIDC_RSA_KEY.pem rename to oidc_provider/tests/app/RSAKEY.pem diff --git a/oidc_provider/tests/app/utils.py b/oidc_provider/tests/app/utils.py index caf1a90..4c707ba 100644 --- a/oidc_provider/tests/app/utils.py +++ b/oidc_provider/tests/app/utils.py @@ -1,8 +1,11 @@ -from django.contrib.auth.models import User +import os try: from urlparse import parse_qs, urlsplit except ImportError: from urllib.parse import parse_qs, urlsplit + +from django.contrib.auth.models import User + from oidc_provider.models import * @@ -44,6 +47,17 @@ def create_fake_client(response_type): return client +def create_rsakey(): + """ + Generate and save a sample RSA Key. + """ + fullpath = os.path.abspath(os.path.dirname(__file__)) + '/RSAKEY.pem' + + with open(fullpath, 'r') as f: + key = f.read() + RSAKey(key=key).save() + + def is_code_valid(url, user, client): """ Check if the code inside the url is valid. diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index 76c5df0..d57f7e8 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -309,6 +309,7 @@ class ImplicitFlowTestCase(TestCase): self.client = create_fake_client(response_type='id_token token') self.state = uuid.uuid4().hex self.nonce = uuid.uuid4().hex + create_rsakey() def test_missing_nonce(self): """ diff --git a/oidc_provider/tests/test_creatersakey_command.py b/oidc_provider/tests/test_creatersakey_command.py index 81f676e..d9424f6 100644 --- a/oidc_provider/tests/test_creatersakey_command.py +++ b/oidc_provider/tests/test_creatersakey_command.py @@ -4,6 +4,7 @@ from django.utils.six import StringIO class CreateRSAKeyTest(TestCase): + @override_settings(BASE_DIR='/tmp') def test_command_output(self): out = StringIO() diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index 574a25e..804499c 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -12,11 +12,11 @@ from django.test import TestCase from jwkest.jwk import KEYS from jwkest.jws import JWS from jwkest.jwt import JWT +from mock import patch from oidc_provider.lib.utils.token import * from oidc_provider.tests.app.utils import * from oidc_provider.views import * -from mock import patch class TokenTestCase(TestCase): @@ -30,6 +30,7 @@ class TokenTestCase(TestCase): self.factory = RequestFactory() self.user = create_fake_user() self.client = create_fake_client(response_type='code') + create_rsakey() def _auth_code_post_data(self, code): """ diff --git a/oidc_provider/views.py b/oidc_provider/views.py index 233d847..4d6aed9 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -8,14 +8,14 @@ from django.shortcuts import render from django.template.loader import render_to_string from django.views.decorators.http import require_http_methods from django.views.generic import View -from hashlib import md5 from jwkest import long_to_base64 from oidc_provider.lib.endpoints.authorize import * from oidc_provider.lib.endpoints.token import * from oidc_provider.lib.endpoints.userinfo import * from oidc_provider.lib.errors import * -from oidc_provider.lib.utils.common import redirect, get_issuer, get_rsa_key +from oidc_provider.lib.utils.common import redirect, get_issuer +from oidc_provider.models import Client, RSAKey logger = logging.getLogger(__name__) @@ -160,7 +160,6 @@ class ProviderInfoView(View): dic['userinfo_endpoint'] = SITE_URL + reverse('oidc_provider:userinfo') dic['end_session_endpoint'] = SITE_URL + reverse('oidc_provider:logout') - from oidc_provider.models import Client types_supported = [x[0] for x in Client.RESPONSE_TYPE_CHOICES] dic['response_types_supported'] = types_supported @@ -182,17 +181,16 @@ class JwksView(View): def get(self, request, *args, **kwargs): dic = dict(keys=[]) - key = get_rsa_key().encode('utf-8') - public_key = RSA.importKey(key).publickey() - - dic['keys'].append({ - 'kty': 'RSA', - 'alg': 'RS256', - 'use': 'sig', - 'kid': md5(key).hexdigest(), - 'n': long_to_base64(public_key.n), - 'e': long_to_base64(public_key.e), - }) + for rsakey in RSAKey.objects.all(): + public_key = RSA.importKey(rsakey.key).publickey() + dic['keys'].append({ + 'kty': 'RSA', + 'alg': 'RS256', + 'use': 'sig', + 'kid': rsakey.kid, + 'n': long_to_base64(public_key.n), + 'e': long_to_base64(public_key.e), + }) response = JsonResponse(dic) response['Access-Control-Allow-Origin'] = '*'