Implementation of RSA Keys using Models. Also providing DOC.

This commit is contained in:
juanifioren 2016-01-25 17:52:24 -03:00
parent 9f86a47cce
commit 998ea5fcd1
14 changed files with 129 additions and 49 deletions

31
DOC.md
View file

@ -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/``.

View file

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

View file

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

View file

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

View file

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

View file

@ -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()),
],
),
]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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'] = '*'