Implementation of RSA Keys using Models. Also providing DOC.
This commit is contained in:
parent
9f86a47cce
commit
998ea5fcd1
14 changed files with 129 additions and 49 deletions
31
DOC.md
31
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/``.
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
22
oidc_provider/migrations/0008_rsakey.py
Normal file
22
oidc_provider/migrations/0008_rsakey.py
Normal 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()),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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 '')
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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'] = '*'
|
||||
|
|
Loading…
Reference in a new issue