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) - [Requirements](#requirements)
- [Installation](#installation) - [Installation](#installation)
- [Users And Clients](#users-and-clients) - [Users And Clients](#users-and-clients)
- [Server RSA Keys](#rsa-keys)
- [Templates](#templates) - [Templates](#templates)
- [Standard Claims](#standard-claims) - [Standard Claims](#standard-claims)
- [Server Endpoints](#server-endpoints) - [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_EXTRA_SCOPE_CLAIMS](#oidc_extra_scope_claims)
- [OIDC_IDTOKEN_EXPIRE](#oidc_idtoken_expire) - [OIDC_IDTOKEN_EXPIRE](#oidc_idtoken_expire)
- [OIDC_IDTOKEN_SUB_GENERATOR](#oidc_idtoken_sub_generator) - [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_ENABLE](#oidc_skip_consent_enable)
- [OIDC_SKIP_CONSENT_EXPIRE](#oidc_skip_consent_expire) - [OIDC_SKIP_CONSENT_EXPIRE](#oidc_skip_consent_expire)
- [OIDC_TOKEN_EXPIRE](#oidc_token_expire) - [OIDC_TOKEN_EXPIRE](#oidc_token_expire)
@ -122,6 +122,35 @@ Or create a client with Django shell: ``python manage.py shell``:
>>> c.save() >>> 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 ## Templates
Add your own templates files inside a folder named ``templates/oidc_provider/``. 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.forms import ModelForm
from django.contrib import admin 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): class ClientForm(ModelForm):
@ -56,3 +57,9 @@ class TokenAdmin(admin.ModelAdmin):
def has_add_permission(self, request): def has_add_permission(self, request):
return False return False
@admin.register(RSAKey)
class RSAKeyAdmin(admin.ModelAdmin):
readonly_fields = ['kid']

View file

@ -27,20 +27,6 @@ def get_issuer():
return 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): class DefaultUserInfo(object):
""" """
Default class for setting OIDC_USERINFO. Default class for setting OIDC_USERINFO.

View file

@ -5,10 +5,10 @@ import uuid
from Crypto.PublicKey.RSA import importKey from Crypto.PublicKey.RSA import importKey
from django.utils import timezone from django.utils import timezone
from hashlib import md5 from hashlib import md5
from jwkest.jwk import RSAKey from jwkest.jwk import RSAKey as jwk_RSAKey
from jwkest.jws import JWS 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.models import *
from oidc_provider import settings from oidc_provider import settings
@ -53,9 +53,16 @@ def encode_id_token(payload):
Return a hash. Return a hash.
""" """
key_string = get_rsa_key().encode('utf-8') keys = []
keys = [ RSAKey(key=importKey(key_string), kid=md5(key_string).hexdigest()) ]
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') _jws = JWS(payload, alg='RS256')
return _jws.sign_compact(keys) return _jws.sign_compact(keys)

View file

@ -1,8 +1,10 @@
import os import os
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from django.core.management.base import BaseCommand
from oidc_provider import settings from oidc_provider import settings
from django.core.management.base import BaseCommand from oidc_provider.models import RSAKey
class Command(BaseCommand): class Command(BaseCommand):
@ -11,9 +13,8 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
try: try:
key = RSA.generate(1024) key = RSA.generate(1024)
file_path = os.path.join(settings.get('OIDC_RSA_KEY_FOLDER'), 'OIDC_RSA_KEY.pem') rsakey = RSAKey(key=key.exportKey('PEM').decode('utf8'))
with open(file_path, 'wb') as f: rsakey.save()
f.write(key.exportKey('PEM')) self.stdout.write(u'RSA key successfully created with kid: {0}'.format(rsakey.kid))
self.stdout.write('RSA key successfully created at: ' + file_path)
except Exception as e: except Exception as e:
self.stdout.write('Something goes wrong: {0}'.format(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 import json
from django.db import models from django.db import models
@ -27,7 +29,6 @@ class Client(models.Model):
verbose_name = _(u'Client') verbose_name = _(u'Client')
verbose_name_plural = _(u'Clients') verbose_name_plural = _(u'Clients')
def __str__(self): def __str__(self):
return u'{0}'.format(self.name) return u'{0}'.format(self.name)
@ -107,3 +108,22 @@ class UserConsent(BaseCodeTokenModel):
class Meta: class Meta:
unique_together = ('user', 'client') 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' return 'oidc_provider.lib.utils.common.default_sub_generator'
@property
def OIDC_RSA_KEY_FOLDER(self):
"""
REQUIRED.
"""
return None
@property @property
def OIDC_SKIP_CONSENT_ENABLE(self): def OIDC_SKIP_CONSENT_ENABLE(self):
""" """

View file

@ -1,8 +1,11 @@
from django.contrib.auth.models import User import os
try: try:
from urlparse import parse_qs, urlsplit from urlparse import parse_qs, urlsplit
except ImportError: except ImportError:
from urllib.parse import parse_qs, urlsplit from urllib.parse import parse_qs, urlsplit
from django.contrib.auth.models import User
from oidc_provider.models import * from oidc_provider.models import *
@ -44,6 +47,17 @@ def create_fake_client(response_type):
return client 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): def is_code_valid(url, user, client):
""" """
Check if the code inside the url is valid. 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.client = create_fake_client(response_type='id_token token')
self.state = uuid.uuid4().hex self.state = uuid.uuid4().hex
self.nonce = uuid.uuid4().hex self.nonce = uuid.uuid4().hex
create_rsakey()
def test_missing_nonce(self): def test_missing_nonce(self):
""" """

View file

@ -4,6 +4,7 @@ from django.utils.six import StringIO
class CreateRSAKeyTest(TestCase): class CreateRSAKeyTest(TestCase):
@override_settings(BASE_DIR='/tmp') @override_settings(BASE_DIR='/tmp')
def test_command_output(self): def test_command_output(self):
out = StringIO() out = StringIO()

View file

@ -12,11 +12,11 @@ from django.test import TestCase
from jwkest.jwk import KEYS from jwkest.jwk import KEYS
from jwkest.jws import JWS from jwkest.jws import JWS
from jwkest.jwt import JWT from jwkest.jwt import JWT
from mock import patch
from oidc_provider.lib.utils.token import * from oidc_provider.lib.utils.token import *
from oidc_provider.tests.app.utils import * from oidc_provider.tests.app.utils import *
from oidc_provider.views import * from oidc_provider.views import *
from mock import patch
class TokenTestCase(TestCase): class TokenTestCase(TestCase):
@ -30,6 +30,7 @@ class TokenTestCase(TestCase):
self.factory = RequestFactory() self.factory = RequestFactory()
self.user = create_fake_user() self.user = create_fake_user()
self.client = create_fake_client(response_type='code') self.client = create_fake_client(response_type='code')
create_rsakey()
def _auth_code_post_data(self, code): 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.template.loader import render_to_string
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.views.generic import View from django.views.generic import View
from hashlib import md5
from jwkest import long_to_base64 from jwkest import long_to_base64
from oidc_provider.lib.endpoints.authorize import * from oidc_provider.lib.endpoints.authorize import *
from oidc_provider.lib.endpoints.token import * from oidc_provider.lib.endpoints.token import *
from oidc_provider.lib.endpoints.userinfo import * from oidc_provider.lib.endpoints.userinfo import *
from oidc_provider.lib.errors 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__) logger = logging.getLogger(__name__)
@ -160,7 +160,6 @@ class ProviderInfoView(View):
dic['userinfo_endpoint'] = SITE_URL + reverse('oidc_provider:userinfo') dic['userinfo_endpoint'] = SITE_URL + reverse('oidc_provider:userinfo')
dic['end_session_endpoint'] = SITE_URL + reverse('oidc_provider:logout') 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] types_supported = [x[0] for x in Client.RESPONSE_TYPE_CHOICES]
dic['response_types_supported'] = types_supported dic['response_types_supported'] = types_supported
@ -182,14 +181,13 @@ class JwksView(View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
dic = dict(keys=[]) dic = dict(keys=[])
key = get_rsa_key().encode('utf-8') for rsakey in RSAKey.objects.all():
public_key = RSA.importKey(key).publickey() public_key = RSA.importKey(rsakey.key).publickey()
dic['keys'].append({ dic['keys'].append({
'kty': 'RSA', 'kty': 'RSA',
'alg': 'RS256', 'alg': 'RS256',
'use': 'sig', 'use': 'sig',
'kid': md5(key).hexdigest(), 'kid': rsakey.kid,
'n': long_to_base64(public_key.n), 'n': long_to_base64(public_key.n),
'e': long_to_base64(public_key.e), 'e': long_to_base64(public_key.e),
}) })