Merge remote-tracking branch 'rohe/master' into ci_testing

Conflicts:
	oidc_example/rp3/modules/opchoice.mako.py
	src/oic/oic/provider.py
	src/oic/utils/http_util.py
	src/oic/utils/keyio.py
This commit is contained in:
Manuel Jeckelmann 2014-11-23 21:19:21 +01:00
commit 7d4b4f4467
27 changed files with 384 additions and 413 deletions

4
.travis.yml Normal file
View File

@ -0,0 +1,4 @@
language: python
script:
- ./setup.py test

View File

@ -0,0 +1 @@
{"client_1":{"client_secret": "hemlig"}}

View File

@ -4,12 +4,11 @@
baseurl = "https://localhost"
#baseurl = "https://lingon.ladok.umu.se"
issuer = "%s:%%d" % baseurl
keys = {
"RSA": {
"key": "cp_keys/key.pem",
"usage": ["enc", "sig"]
}
}
keys = [
{"type": "RSA", "key": "cp_keys/key.pem", "use": ["enc", "sig"]},
{"type": "EC", "crv": "P-256", "use": ["sig"]},
{"type": "EC", "crv": "P-256", "use": ["enc"]}
]
CAS_SERVER = "https://cas.umu.se"
SERVICE_URL = "%s/verify" % issuer

View File

@ -1,5 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import json
import sys
import os
import traceback
@ -14,7 +15,7 @@ from urlparse import parse_qs
from oic.utils.authn.client import verify_client
from oic.utils.authz import AuthzHandling
from oic.utils.keyio import KeyBundle, dump_jwks
from oic.utils.keyio import keyjar_init
from oic.utils.userinfo import UserInfo
from oic.utils.webfinger import WebFinger
from oic.utils.webfinger import OIC_ISSUER
@ -22,7 +23,6 @@ from oic.utils.authn.authn_context import AuthnBroker
__author__ = 'rohe0002'
import logging
import re
from logging.handlers import BufferingHandler
@ -404,7 +404,7 @@ def application(environ, start_response):
logger.info("callback: %s" % callback)
try:
return callback(environ, start_response, logger)
except Exception, err:
except Exception as err:
print >> sys.stderr, "%s" % err
message = traceback.format_exception(*sys.exc_info())
print >> sys.stderr, message
@ -592,28 +592,19 @@ if __name__ == '__main__':
OAS.baseurl += "/"
try:
OAS.keyjar[""] = []
kbl = []
for typ, info in config.keys.items():
typ = typ.upper()
LOGGER.info("OC server key init: %s, %s" % (typ, info))
kb = KeyBundle(source="file://%s" % info["key"], fileformat="der",
keytype=typ)
OAS.keyjar.add_kb("", kb)
kbl.append(kb)
try:
new_name = "static/jwks.json"
dump_jwks(kbl, new_name)
OAS.jwks_uri.append("%s%s" % (OAS.baseurl, new_name))
except KeyError:
pass
for b in OAS.keyjar[""]:
LOGGER.info("OC3 server keys: %s" % b)
jwks = keyjar_init(OAS, config.keys)
except Exception, err:
LOGGER.error("Key setup failed: %s" % err)
OAS.key_setup("static", sig={"format": "jwk", "alg": "rsa"})
else:
new_name = "static/jwks.json"
f = open(new_name, "w")
f.write(json.dumps(jwks))
f.close()
OAS.jwks_uri.append("%s%s" % (OAS.baseurl, new_name))
for b in OAS.keyjar[""]:
LOGGER.info("OC3 server keys: %s" % b)
if config.USERINFO == "LDAP":
from oic.utils.userinfo.ldap_info import UserInfoLDAP

View File

@ -1,3 +1,2 @@
#!/bin/sh
./claims_provider.py -p 8093 -d cp_config.json &
./oc_server.py -p 8092 -d oc_config &

View File

@ -3,12 +3,11 @@
baseurl = "https://localhost"
issuer = "%s:%%d" % baseurl
keys = {
"RSA": {
"key": "cp_keys/key.pem",
"usage": ["enc", "sig"]
}
}
keys = [
{"type": "RSA", "key": "cp_keys/key.pem", "use": ["enc", "sig"]},
{"type": "EC", "crv": "P-256", "use": ["sig"]},
{"type": "EC", "crv": "P-256", "use": ["enc"]}
]
SERVICE_URL = "%s/verify" % issuer

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import json
import sys
import os
import traceback
@ -13,20 +13,21 @@ from exceptions import AttributeError
from exceptions import KeyboardInterrupt
from urlparse import parse_qs
from saml2 import BINDING_HTTP_REDIRECT, BINDING_HTTP_POST
from saml2.extension.idpdisc import BINDING_DISCO
from oic.utils.authn.javascript_login import JavascriptFormMako
from oic.utils.authn.client import verify_client
from oic.utils.authn.multi_auth import setup_multi_auth, AuthnIndexedEndpointWrapper
from oic.utils.authn.multi_auth import setup_multi_auth
from oic.utils.authn.multi_auth import AuthnIndexedEndpointWrapper
from oic.utils.authn.saml import SAMLAuthnMethod
from oic.utils.authn.user import UsernamePasswordMako
from oic.utils.authz import AuthzHandling
from oic.utils.keyio import KeyBundle, dump_jwks
from oic.utils.keyio import keyjar_init
from oic.utils.userinfo import UserInfo
from oic.utils.userinfo.aa_info import AaUserInfo
from oic.utils.webfinger import WebFinger
from oic.utils.webfinger import OIC_ISSUER
from oic.utils.authn.authn_context import AuthnBroker, make_auth_verify
from oic.utils.authn.authn_context import AuthnBroker
from oic.utils.authn.authn_context import make_auth_verify
__author__ = 'rohe0002'
@ -248,6 +249,22 @@ def static(environ, start_response, logger, path):
return resp(environ, start_response)
# ----------------------------------------------------------------------------
def key_rollover(environ, start_response, _):
# expects a post containing the necessary information
_jwks = json.loads(get_post(environ))
OAS.do_key_rollover(_jwks, "key_%d_%%d" % int(time.time()))
resp = Response("OK")
return resp(environ, start_response)
def clear_keys(environ, start_response, _):
OAS.remove_inactive_keys()
resp = Response("OK")
return resp(environ, start_response)
# ----------------------------------------------------------------------------
from oic.oic.provider import AuthorizationEndpoint
from oic.oic.provider import TokenEndpoint
from oic.oic.provider import UserinfoEndpoint
@ -269,6 +286,8 @@ URLS = [
# (r'^.well-known/webfinger', webfinger),
(r'.+\.css$', css),
(r'safe', safe),
(r'^keyrollover', key_rollover),
(r'^clearkeys', clear_keys)
# (r'tracelog', trace_log),
]
@ -552,30 +571,20 @@ if __name__ == '__main__':
if not OAS.baseurl.endswith("/"):
OAS.baseurl += "/"
# Add own keys for signing/encrypting JWTs
try:
OAS.keyjar[""] = []
kbl = []
for typ, info in config.keys.items():
typ = typ.upper()
LOGGER.info("OC server key init: %s, %s" % (typ, info))
kb = KeyBundle(source="file://%s" % info["key"], fileformat="der",
keytype=typ)
OAS.keyjar.add_kb("", kb)
kbl.append(kb)
try:
new_name = "static/jwks.json"
dump_jwks(kbl, new_name)
OAS.jwks_uri.append("%s%s" % (OAS.baseurl, new_name))
except KeyError:
pass
for b in OAS.keyjar[""]:
LOGGER.info("OC3 server keys: %s" % b)
jwks = keyjar_init(OAS, config.keys)
except Exception, err:
LOGGER.error("Key setup failed: %s" % err)
OAS.key_setup("static", sig={"format": "jwk", "alg": "rsa"})
else:
new_name = "static/jwks.json"
f = open(new_name, "w")
f.write(json.dumps(jwks))
f.close()
OAS.jwks_uri.append("%s%s" % (OAS.baseurl, new_name))
for b in OAS.keyjar[""]:
LOGGER.info("OC3 server keys: %s" % b)
# Setup the web server
SRV = wsgiserver.CherryPyWSGIServer(('0.0.0.0', args.port), application)

View File

@ -1,175 +0,0 @@
import os
from saml2 import saml
from saml2 import BINDING_HTTP_REDIRECT
from saml2 import BINDING_HTTP_POST
from saml2.extension.idpdisc import BINDING_DISCO
from saml2.saml import NAME_FORMAT_URI
from saml2.entity_category.edugain import COC
from saml2.entity_category.swamid import RESEARCH_AND_EDUCATION
from saml2.entity_category.swamid import HEI
from saml2.entity_category.swamid import SFS_1993_1153
from saml2.entity_category.swamid import NREN
from saml2.entity_category.swamid import EU
try:
from saml2.sigver import get_xmlsec_binary
except ImportError:
get_xmlsec_binary = None
if get_xmlsec_binary:
xmlsec_path = get_xmlsec_binary(["/opt/local/bin", "/usr/local/bin"])
else:
xmlsec_path = '/usr/local/bin/xmlsec1'
#Url to a discovery server for SAML. None implies not using one.
DISCOSRV = None
#DISCOSRV = "http://localhost/idp.ds"
#Url to a wayf for SAML. None implies not using one.
WAYF = None
#Full URL to the SP. You must have the same base as the OP.
BASE = "https://localhost:8092"#"%s"
#Discovery endpoint
DISCOENDPOINT = "saml_verify_disco"
#The BASE url where the Idp performs the redirect after a authn request from the SP.
#For the cookies to work do not use subfolders.
ASCREDIRECT = 'saml_verify_redirect'
#The BASE url where the Idp performs a post after a authn request from the SP.
#For the cookies to work do not use subfolders.
ASCPOST = 'saml_verify_post'
MULTI_AUTH_POST = "multi_saml_pass_verify"
MULTI_AUTH_DISCO_END_POINT = "multi_saml_pass_disco"
MULTI_ASC_REDIRECT = "multi_saml_pass_redirect"
#Must point to the complete path on disk to this file!
#Needed by the script create_metadata.sh and the SP to find all configurations.
#No need to change this!
FULL_PATH = os.path.dirname(os.path.abspath(__file__))
#This is the directory for the SP.
WORKING_DIR = FULL_PATH + "/"
#This is a map for Open Id connect to Saml2.
#The proxy will give the same response for OAuth2.
OPENID2SAMLMAP = {
"sub": "uid",
"name": "displayName",
"given_name": "givenname",
"family_name": "sn",
"middle_name": "",
"nickname": "eduPersonNickname",
"preferred_username": "uid",
"profile": "member",
#STUDENTNESS example for studentness
#"profile": "eduPersonScopedAffiliation",
"picture": "jpegPhoto",
"website": "labeledURI",
"email": "email",
#"email_verified": "Missing
"gender": "",
"birthdate": "norEduPersonNIN",
#zoneinfo timezone
"locale": "c",
"phone_number": "telephoneNumber",
#phone_number_verified
"address": "registeredAddress",
"updated_at": "" # When information was updated
}
#Traditional pysaml2 configuration for a SP. View more documentation for pysaml2.
CONFIG = {
"entityid": "%s/testpyoidcsp.xml" % BASE,
"description": "Test pyoidc SP",
"entity_category": [COC, RESEARCH_AND_EDUCATION, HEI, SFS_1993_1153, NREN, EU],
"service": {
"sp": {
"name": "Test pyoidc SP",
"authn_requests_signed": "true",
"want_response_signed": "true",
"endpoints": {
"assertion_consumer_service": [
(BASE + "/" + ASCREDIRECT, BINDING_HTTP_REDIRECT, 0),
(BASE + "/" + MULTI_ASC_REDIRECT, BINDING_HTTP_REDIRECT, 1),
(BASE + "/" + ASCPOST, BINDING_HTTP_POST, 0),
(BASE + "/" + MULTI_AUTH_POST, BINDING_HTTP_POST, 1),
],
"required_attributes": ["uid"],
"discovery_response": [
("%s/%s" % (BASE, DISCOENDPOINT), BINDING_DISCO),
("%s/%s" % (BASE, MULTI_AUTH_DISCO_END_POINT), BINDING_DISCO)
],
}
},
},
"key_file": WORKING_DIR+"sp_cert/localhost.key",
"cert_file": WORKING_DIR+"sp_cert/localhost.crt",
"xmlsec_binary": xmlsec_path,
"metadata": {
"local": ["[..]/pysaml2/example/idp2/idp.xml"],
#"remote": [{"url": "http://localhost/idp.xml", "cert": None}],
},
"name_form": NAME_FORMAT_URI,
"organization": {
"name": "Test pyoidc SP",
"display_name": [("Test pyoidc SP", "en")],
"url": BASE,
},
"contact_person": [
{
"contact_type": "technical",
"given_name": "Test",
"sur_name": "Testsson",
"email_address": "test.testsson@test.se"
},
],
"logger": {
"rotating": {
"filename": "sp.log",
"maxBytes": 100000,
"backupCount": 5,
},
"loglevel": "debug",
}
}
#Cache for transferring information between SAML authn and user info AA
SAML_CACHE = {}
# If AA_ATTRIBUTE_SAML_IDP is true the AA attributes and the IDP attributes will be concatenated. Else only the
# attributes listed in AA_ATTRIBUTE_SAML_IDP_WHITELIST will be returned. If AA_ATTRIBUTE_SAML_IDP_WHITELIST is None all
# attributes will be returned
AA_ATTRIBUTE_SAML_IDP = True
AA_ATTRIBUTE_SAML_IDP_WHITELIST = None
#STUDENTNESS example for studentness
#AA_ATTRIBUTE_SAML_IDP_WHITELIST = ["eduPersonScopedAffiliation"]
AA_REQUEST_ATTRIBUTES = None
#Contains all valid attributes and valid values for that attribute.
VALID_ATTRIBUTE_RESPONSE=None
#STUDENTNESS example configuration for studentness
#VALID_ATTRIBUTE_RESPONSE = {
# "eduPersonScopedAffiliation": ["student"]
#}
#Contains all attributes that will be returned.
#Only value that contains the values in the value list will be returned. If None will all values be returned.
ATTRIBUTE_WHITELIST=None
#STUDENTNESS example configuration for studentness
#ATTRIBUTE_WHITELIST = {
# "eduPersonScopedAffiliation": ["student"]
#}
#Attribute authority only used if USERINFO = "AA" in config.py
AA_ENTITY_ID = None
# If None name id is used otherwise the first value for the given attribute name in the SAML response will be used.
AA_NAMEID_ATTRIBUTE = None
#AA_NAMEID_ATTRIBUTE = "eduPersonTargetedID"
AA_NAMEID_FORMAT = saml.NAMEID_FORMAT_PERSISTENT

2
oidc_example/op2/start.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/sh
./server.py -p 8092 -d config &

View File

@ -13,8 +13,6 @@ from oic.oic.message import AuthorizationRequest
from oic.oic.message import AccessTokenResponse
from oic.utils.webfinger import WebFinger
__author__ = 'rolandh'
logger = logging.getLogger(__name__)
@ -57,13 +55,13 @@ class OpenIDConnect(object):
self.authn_method = None
self.registration_info = registration_info
def dynamic(self, server_env, callback, logoutCallback, session, key):
def dynamic(self, server_env, callback, logout_callback, session, key):
try:
client = server_env["OIC_CLIENT"][key]
except KeyError:
client = self.client_cls(client_authn_method=CLIENT_AUTHN_METHOD)
client.redirect_uris = [callback]
client.post_logout_redirect_uris = [logoutCallback]
client.post_logout_redirect_uris = [logout_callback]
_me = self.registration_info.copy()
_me["redirect_uris"] = [callback]
@ -86,14 +84,14 @@ class OpenIDConnect(object):
server_env["OIC_CLIENT"] = {key: client}
return client
def static(self, server_env, callback, logoutCallback, key):
def static(self, server_env, callback, logout_callback, key):
try:
client = server_env["OIC_CLIENT"][key]
logger.debug("Static client: %s" % server_env["OIC_CLIENT"])
except KeyError:
client = self.client_cls(client_authn_method=CLIENT_AUTHN_METHOD)
client.redirect_uris = [callback]
client.post_logout_redirect_uris = [logoutCallback]
client.post_logout_redirect_uris = [logout_callback]
for typ in ["authorization", "token", "userinfo"]:
endpoint = "%s_endpoint" % typ
setattr(client, endpoint, self.extra[endpoint])
@ -130,13 +128,17 @@ class OpenIDConnect(object):
if not resp.ok and resp.status_code == 400:
client = None
server_env["OIC_CLIENT"].pop(key, None)
_state = ""
if client is None:
callback = server_env["base_url"] + key
logoutCallback = server_env["base_url"]
logout_callback = server_env["base_url"]
if self.srv_discovery_url:
client = self.dynamic(server_env, callback, logoutCallback, session, key)
client = self.dynamic(server_env, callback, logout_callback,
session, key)
else:
client = self.static(server_env, callback, logoutCallback, key)
client = self.static(server_env, callback, logout_callback,
key)
_state = session.getState()
session.setClient(client)
@ -144,10 +146,11 @@ class OpenIDConnect(object):
try:
acr_values = client.provider_info["acr_values_supported"]
session.set_acr_values(acr_values)
except:
except KeyError:
acr_values = None
if acr_value is None and acr_values is not None and len(acr_values) > 1:
if acr_value is None and acr_values is not None and \
len(acr_values) > 1:
resp_headers = [("Location", str("/rpAcr"))]
start_response("302 Found", resp_headers)
return []

2
oidc_example/rp2/start.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
./rp2.py &

View File

@ -1,46 +0,0 @@
# -*- coding:utf-8 -*-
from mako import runtime
UNDEFINED = runtime.UNDEFINED
__M_dict_builtin = dict
__M_locals_builtin = locals
_magic_number = 9
_modified_time = 1400148619.346786
_enable_loop = True
_template_filename = 'htdocs/opchoice.mako'
_template_uri = 'opchoice.mako'
_source_encoding = 'utf-8'
_exports = []
# SOURCE LINE 1
def op_choice(op_list):
"""
Creates a dropdown list of OpenID Connect providers
"""
element = "<select name=\"op\">"
for name in op_list:
element += "<option value=\"%s\">%s</option>" % (name, name)
element += "</select>"
return element
def render_body(context, **pageargs):
__M_caller = context.caller_stack._push_frame()
try:
__M_locals = __M_dict_builtin(pageargs=pageargs)
op_list = context.get('op_list', UNDEFINED)
__M_writer = context.writer()
# SOURCE LINE 11
__M_writer(
u'\n\n<!DOCTYPE html>\n\n<html>\n <head>\n <title>pyoidc RP</title>\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <!-- Bootstrap -->\n <link href="static/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen">\n <link href="static/style.css" rel="stylesheet" media="all">\n\n <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->\n <!--[if lt IE 9]>\n <script src="../../assets/js/html5shiv.js"></script>\n <script src="../../assets/js/respond.min.js"></script>\n <![endif]-->\n </head>\n <body>\n\n <!-- Static navbar -->\n <div class="navbar navbar-default navbar-fixed-top">\n <div class="navbar-header">\n <a class="navbar-brand" href="#">pyoidc RP</a>\n </div>\n </div>\n\n <div class="container">\n <!-- Main component for a primary marketing message or call to action -->\n <div class="jumbotron">\n <form class="form-signin" action="rp" method="get">\n <h1>OP by UID</h1>\n <h3>Chose the OpenID Connect Provider: </h3>\n <p>From this list</p>\n ')
# SOURCE LINE 45
__M_writer(unicode(op_choice(op_list)))
__M_writer(
u'\n <p> OR by providing your unique identifier at the OP. </p>\n <input type="text" id="uid" name="uid" class="form-control" placeholder="UID" autofocus>\n <button class="btn btn-lg btn-primary btn-block" type="submit">Start</button>\n </form>\n </div>\n\n </div> <!-- /container -->\n <!-- jQuery (necessary for Bootstrap\'s JavaScript plugins) -->\n <script src="/static/jquery.min.1.9.1.js"></script>\n <!-- Include all compiled plugins (below), or include individual files as needed -->\n <script src="/static/bootstrap/js/bootstrap.min.js"></script>\n\n </body>\n</html>')
return ''
finally:
context.caller_stack._pop_frame()

2
oidc_example/rp3/start.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
./rp3.py conf &

21
setup.py Normal file → Executable file
View File

@ -16,12 +16,28 @@
#
from setuptools import setup
from setuptools.command.test import test as TestCommand
import sys
__author__ = 'rohe0002'
class PyTest(TestCommand):
def finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = []
self.test_suite = True
def run_tests(self):
#import here, cause outside the eggs aren't loaded
import pytest
errno = pytest.main(self.test_args)
sys.exit(errno)
setup(
name="oic",
version="0.6.0beta",
version="0.6.gamma",
description="Python implementation of OAuth2 and OpenID Connect",
author = "Roland Hedberg",
author_email = "roland.hedberg@adm.umu.se",
@ -36,6 +52,7 @@ setup(
install_requires = ['requests', "pycrypto>=2.6.1", "cherrypy==3.2.4",
"mako", "pyjwkest", "beaker", "alabaster", "importlib",
"argparse", "pyOpenSSL"],
tests_require=['pytest'],
zip_safe=False,
cmdclass={'test': PyTest},
)

View File

@ -70,6 +70,8 @@ def address_deser(val, sformat="urlencoded"):
if not isinstance(val, basestring):
val = json.dumps(val)
sformat = "json"
elif sformat == "dict":
sformat = "json"
return AddressClaim().deserialize(val, sformat)

View File

@ -13,7 +13,7 @@ from oic.utils.authn.user import NoSuchAuthentication
from oic.utils.authn.user import ToOld
from oic.utils.authn.user import TamperAllert
from oic.utils.time_util import utc_time_sans_frac
from oic.utils.keyio import KeyBundle
from oic.utils.keyio import KeyBundle, dump_jwks
from oic.utils.keyio import key_export
from oic.oauth2.message import by_schema
from oic.oic.message import RefreshAccessTokenRequest
@ -35,6 +35,9 @@ from oic.oic.message import DiscoveryRequest
from oic.oic.message import ProviderConfigurationResponse
from oic.oic.message import DiscoveryResponse
from jwkest import jws, jwe
from jwkest.jws import alg2keytype
from jwkest.jws import NoSuitableSigningKeys
__author__ = 'rohe0002'
@ -185,7 +188,11 @@ class Provider(AProvider):
self.seed = ""
self.sso_ttl = 0
self.test_mode = False
# where the jwks file kan be found by outsiders
self.jwks_uri = []
# Local filename
self.jwks_name = ""
self.authn_as = None
self.preferred_id_type = "public"
@ -210,6 +217,7 @@ class Provider(AProvider):
else:
self.capabilities = self.provider_features()
self.capabilities["issuer"] = self.name
self.kid = {"sig": {}, "enc": {}}
def set_mode(self, mode):
"""
@ -276,7 +284,8 @@ class Provider(AProvider):
if "" in self.keyjar:
for b in self.keyjar[""]:
logger.debug("OC3 server keys: %s" % b)
ckey = self.keyjar.get_signing_key(alg2keytype(alg), "")
ckey = self.keyjar.get_signing_key(alg2keytype(alg), "",
alg=alg)
else:
ckey = None
logger.debug("ckey: %s" % ckey)
@ -768,9 +777,10 @@ class Provider(AProvider):
_idtoken = self.sign_encrypt_id_token(
_info, client_info, req, user_info=userinfo,
auth_time=_info["auth_time"])
except JWEException:
except (JWEException, NoSuitableSigningKeys) as err:
logger.warning(str(err))
return self._error(error="access_denied",
descr="Could not encrypt id_token")
descr="Could not sign/encrypt id_token")
_sdb.update_by_token(_access_code, "id_token", _idtoken)
@ -905,7 +915,7 @@ class Provider(AProvider):
algo = self.jwx_def["sign_alg"]["userinfo"]
# Use my key for signing
key = self.keyjar.get_signing_key(alg2keytype(algo), "")
key = self.keyjar.get_signing_key(alg2keytype(algo), "", alg=algo)
if not key:
return self._error(error="access_denied",
descr="Missing signing key")
@ -1057,14 +1067,43 @@ class Provider(AProvider):
if "redirect_uris" in request:
ruri = []
client_type = request["application_type"]
if client_type == "web":
if request["response_types"] == ["code"]:
must_https = False
else: # one has to be implicit or hybrid
must_https = True
else:
must_https = False
for uri in request["redirect_uris"]:
if urlparse.urlparse(uri).fragment:
p = urlparse.urlparse(uri)
err = None
if client_type == "native" and p.scheme == "http":
if p.hostname != "localhost":
err = ClientRegistrationErrorResponse(
error="invalid_configuration_parameter",
error_description="Http redirect_uri must use localhost")
elif must_https and p.scheme != "https":
err = ClientRegistrationErrorResponse(
error="invalid_configuration_parameter",
error_description="None https redirect_uri not allowed")
elif p.fragment:
err = ClientRegistrationErrorResponse(
error="invalid_configuration_parameter",
error_description="redirect_uri contains fragment")
# This rule will break local testing.
# elif must_https and p.hostname == "localhost":
# err = ClientRegistrationErrorResponse(
# error="invalid_configuration_parameter",
# error_description="https redirect_uri with host localhost")
if err:
return Response(err.to_json(),
content="application/json",
status="400 Bad Request")
base, query = urllib.splitquery(uri)
if query:
ruri.append((base, urlparse.parse_qs(query)))
@ -1136,6 +1175,17 @@ class Provider(AProvider):
"invalid_configuration_parameter",
descr="%s pointed to illegal URL" % item)
# necessary keys ?
for item in ["id_token_signed_response_alg",
"userinfo_signed_response_alg"]:
if item in request:
if request[item] in self.capabilities[PREFERENCE2PROVIDER[item]]:
ktyp = jws.alg2keytype(request[item])
# do I have this ktyp and for EC type keys the curve
_k = self.keyjar.get_signing_key(ktyp, alg=request[item])
if not _k:
del _cinfo[item]
try:
self.keyjar.load_keys(request, client_id)
try:
@ -1644,6 +1694,56 @@ class Provider(AProvider):
"""
return self.end_session_endpoint(request, **kwargs)
def do_key_rollover(self, jwks, kid_template):
"""
Handle key roll-over by importing new keys and inactivating the
ones in the keyjar that are of the same type and usage.
:param jwk: A JWK
"""
kb = KeyBundle()
kb.do_keys(jwks["keys"])
kid = 0
for k in kb.keys():
if not k.kid:
k.kid = kid_template % kid
kid += 1
self.kid[k.use][k.kty] = k.kid
# find the old key for this key type and usage and mark that
# as inactive
for _kb in self.keyjar.issuer_keys[""]:
for key in _kb.keys():
if key.kty == k.kty and key.use == k.use:
if k.kty == "EC":
if key.crv == k.crv:
key.inactive_since = time.time()
else:
key.inactive_since = time.time()
self.keyjar.add_kb("", kb)
if self.jwks_name:
# print to the jwks file
dump_jwks(self.keyjar[""], self.jwks_name)
def remove_inactive_keys(self, more_then=3600):
"""
Remove all keys that has been inactive 'more_then' seconds
:param more_then: An integer (default = 3600 seconds == 1 hour)
"""
now = time.time()
for kb in self.keyjar.issuer_keys[""]:
for key in kb.keys():
if key.inactive_since:
if now - key.inactive_since > more_then:
kb.remove(key)
if len(kb) == 0:
self.keyjar.issuer_keys[""].remove(kb)
# -----------------------------------------------------------------------------
@ -1660,5 +1760,3 @@ class Endpoint(object):
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)

View File

@ -259,7 +259,8 @@ class JWSAuthnMethod(ClientAuthnMethod):
return algorithm
def get_signing_key(self, algorithm):
return self.cli.keyjar.get_signing_key(alg2keytype(algorithm))
return self.cli.keyjar.get_signing_key(alg2keytype(algorithm),
alg=algorithm)
def get_key_by_kid(self, kid, algorithm):
_key = self.cli.keyjar.get_key_by_kid(kid)
@ -327,9 +328,14 @@ class JWSAuthnMethod(ClientAuthnMethod):
logger.debug("authntoken: %s" % bjwt.to_dict())
# logger.debug("known clients: %s" % self.cli.cdb.keys())
try:
cid = kwargs["client_id"]
except KeyError:
cid = bjwt["iss"]
try:
# There might not be a client_id in the request
assert str(bjwt["iss"]) in self.cli.cdb # It's a client I know
assert str(cid) in self.cli.cdb # It's a client I know
except KeyError:
pass
@ -347,7 +353,7 @@ class JWSAuthnMethod(ClientAuthnMethod):
except AssertionError:
raise NotForMe("Not for me!")
return True
return cid
class ClientSecretJWT(JWSAuthnMethod):
@ -362,7 +368,8 @@ class ClientSecretJWT(JWSAuthnMethod):
return JWSAuthnMethod.choose_algorithm(self, entity, **kwargs)
def get_signing_key(self, algorithm):
return self.cli.keyjar.get_signing_key(alg2keytype(algorithm))
return self.cli.keyjar.get_signing_key(alg2keytype(algorithm),
alg=algorithm)
class PrivateKeyJWT(JWSAuthnMethod):
@ -374,7 +381,8 @@ class PrivateKeyJWT(JWSAuthnMethod):
return JWSAuthnMethod.choose_algorithm(self, entity, **kwargs)
def get_signing_key(self, algorithm):
return self.cli.keyjar.get_signing_key(alg2keytype(algorithm), "")
return self.cli.keyjar.get_signing_key(alg2keytype(algorithm), "",
alg=algorithm)
# from oic.utils.authn.client_saml import SAML2_BEARER_ASSERTION_TYPE
@ -409,7 +417,7 @@ def verify_client(inst, areq, authn, type_method=TYPE_METHOD):
elif "client_assertion" in areq: # client_secret_jwt or public_key_jwt
for typ, method in type_method:
if areq["client_assertion_type"] == typ:
return method(inst).verify(areq)
return method(inst).verify(areq, client_id=client_id)
else:
raise UnknownAssertionType(areq["client_assertion_type"], areq)
else:

View File

@ -1,3 +1,5 @@
import logging
__author__ = 'rohe0002'
import cgi
@ -13,6 +15,10 @@ from oic.utils import time_util
from oic.utils.aes import encrypt
from oic.utils.aes import decrypt
from Cookie import SimpleCookie
logger = logging.getLogger(__name__)
class Response(object):
_template = None
@ -343,13 +349,13 @@ def wsgi_wrapper(environ, start_response, func, **kwargs):
args = func(**kwargs)
try:
resp, argv = args
return resp(environ, start_response, **argv)
resp, state = args
return resp(environ, start_response)
except TypeError:
resp = args
return resp(environ, start_response)
except Exception as err:
# logger.error("%s" % err)
logger.error("%s" % err)
raise

View File

@ -1,7 +1,7 @@
import json
import time
from Crypto.PublicKey import RSA
from cryptlib.ecc import NISTEllipticCurve
from oic.exception import MessageException
@ -39,7 +39,6 @@ K2C = {
"RSA": RSAKey,
"EC": ECKey,
"oct": SYMKey,
# "pkix": PKIX_key
}
@ -107,9 +106,6 @@ class KeyBundle(object):
except KeyError:
continue
else:
_key.dc()
if _typ == "EC":
_key.ser = True
self._keys.append(_key)
flag = 1
break
@ -120,20 +116,14 @@ class KeyBundle(object):
self.do_keys(json.loads(open(filename).read())["keys"])
def do_local_der(self, filename, keytype, keyusage):
_bkey = None
if keytype == "RSA":
_bkey = rsa_load(filename)
# This is only for RSA keys
_bkey = rsa_load(filename)
if not keyusage:
keyusage = ["enc", "sig"]
for use in keyusage:
_key = K2C[keytype]()
_key.key = _bkey
if _bkey:
_key.serialize()
_key = RSAKey().load_key(_bkey)
_key.use = use
self._keys.append(_key)
@ -217,7 +207,7 @@ class KeyBundle(object):
return self._keys
def remove(self, typ, val=None):
def remove_key(self, typ, val=None):
"""
:param typ: Type of key (rsa, ec, oct, ..)
@ -239,6 +229,9 @@ class KeyBundle(object):
def append(self, key):
self._keys.append(key)
def remove(self, key):
self._keys.remove(key)
def __len__(self):
return len(self._keys)
@ -290,7 +283,8 @@ def dump_jwks(kbl, target):
res = {"keys": []}
for kb in kbl:
# ignore simple keys
res["keys"].extend([k.to_dict() for k in kb.keys() if k.kty != 'oct'])
res["keys"].extend([k.to_dict() for k in kb.keys() if
k.kty != 'oct' and not k.inactive_since])
try:
f = open(target, 'w')
@ -374,7 +368,7 @@ class KeyJar(object):
self.issuer_keys[issuer] = val
def get(self, use, key_type="", issuer="", kid=None):
def get(self, use, key_type="", issuer="", kid=None, **kwargs):
"""
:param use: A key useful for this usage (enc, dec, sig, ver)
@ -421,19 +415,33 @@ class KeyJar(object):
lst.append(key)
if kid and lst:
break
# if elliptic curve have to check I have a key of the right curve
if key_type == "EC" and "alg" in kwargs:
name = "P-{}".format(kwargs["alg"][2:]) # the type
_lst = []
for key in lst:
try:
assert name == key.crv
except AssertionError:
pass
else:
_lst.append(key)
lst = _lst
return lst
def get_signing_key(self, key_type="", owner="", kid=None):
return self.get("sig", key_type, owner, kid)
def get_signing_key(self, key_type="", owner="", kid=None, **kwargs):
return self.get("sig", key_type, owner, kid, **kwargs)
def get_verify_key(self, key_type="", owner="", kid=None):
return self.get("ver", key_type, owner, kid)
def get_verify_key(self, key_type="", owner="", kid=None, **kwargs):
return self.get("ver", key_type, owner, kid, **kwargs)
def get_encrypt_key(self, key_type="", owner="", kid=None):
return self.get("enc", key_type, owner, kid)
def get_encrypt_key(self, key_type="", owner="", kid=None, **kwargs):
return self.get("enc", key_type, owner, kid, **kwargs)
def get_decrypt_key(self, key_type="", owner="", kid=None):
return self.get("dec", key_type, owner, kid)
def get_decrypt_key(self, key_type="", owner="", kid=None, **kwargs):
return self.get("dec", key_type, owner, kid, **kwargs)
def get_key_by_kid(self, kid, owner=""):
"""
@ -491,8 +499,8 @@ class KeyJar(object):
return
for kc in kcs:
kc.remove(key_type, key)
if len(kc._keys) == 0:
kc.remove_key(key_type, key)
if len(kc) == 0:
self.issuer_keys[issuer].remove(kc)
def update(self, kj):
@ -516,8 +524,11 @@ class KeyJar(object):
def __str__(self):
_res = {}
for k, vs in self.issuer_keys.items():
_res[k] = [str(v) for v in vs]
for _id, kbs in self.issuer_keys.items():
_l = []
for kb in kbs:
_l.extend(json.loads(kb.jwks())["keys"])
_res[_id] = {"keys": _l}
return "%s" % (_res,)
def keys(self):
@ -599,13 +610,13 @@ class RedirectStdStreams(object):
def __enter__(self):
self.old_stdout, self.old_stderr = sys.stdout, sys.stderr
self.old_stdout.flush();
self.old_stdout.flush()
self.old_stderr.flush()
sys.stdout, sys.stderr = self._stdout, self._stderr
# noinspection PyUnusedLocal
def __exit__(self, exc_type, exc_value, traceback):
self._stdout.flush();
#noinspection PyUnusedLocal
def __exit__(self, exc_type, exc_value, trace_back):
self._stdout.flush()
self._stderr.flush()
sys.stdout = self.old_stdout
sys.stderr = self.old_stderr
@ -740,52 +751,68 @@ def proper_path(path):
return path
# ================= create certificate ======================
# heavily influenced by
# http://svn.osafoundation.org/m2crypto/trunk/tests/test_x509.py
#
#
# def make_req(bits, fqdn="example.com", rsa=None):
# pk = EVP.PKey()
# x = X509.Request()
# if not rsa:
# rsa = RSA.gen_key(bits, 65537, lambda: None)
# pk.assign_rsa(rsa)
# # Because rsa is messed with
# rsa = pk.get_rsa()
# x.set_pubkey(pk)
# name = x.get_subject()
# name.C = "SE"
# name.CN = "OpenID Connect Test Server"
# if fqdn:
# ext1 = X509.new_extension('subjectAltName', fqdn)
# extstack = X509.X509_Extension_Stack()
# extstack.push(ext1)
# x.add_extensions(extstack)
# x.sign(pk, 'sha1')
# return x, pk, rsa
#
#
# def make_cert(bits, fqdn="example.com", rsa=None):
# req, pk, rsa = make_req(bits, fqdn=fqdn, rsa=rsa)
# pkey = req.get_pubkey()
# sub = req.get_subject()
# cert = X509.X509()
# cert.set_serial_number(1)
# cert.set_version(2)
# cert.set_subject(sub)
# t = long(time.time()) + time.timezone
# now = ASN1.ASN1_UTCTIME()
# now.set_time(t)
# nowPlusYear = ASN1.ASN1_UTCTIME()
# nowPlusYear.set_time(t + 60 * 60 * 24 * 365)
# cert.set_not_before(now)
# cert.set_not_after(nowPlusYear)
# issuer = X509.X509_Name()
# issuer.CN = 'The code tester'
# issuer.O = 'Umea University'
# cert.set_issuer(issuer)
# cert.set_pubkey(pkey)
# cert.sign(pk, 'sha1')
# return cert, rsa
def ec_init(spec):
"""
:param spec: Key specifics of the form
{"type": "EC", "crv": "P-256", "use": ["sig"]},
:return: A KeyBundle instance
"""
typ = spec["type"].upper()
_key = NISTEllipticCurve.by_name(spec["crv"])
kb = KeyBundle(keytype=typ, keyusage=spec["use"])
for use in spec["use"]:
priv, pub = _key.key_pair()
ec = ECKey(x=pub[0], y=pub[1], d=priv, crv=spec["crv"])
ec.serialize()
ec.use = use
kb.append(ec)
return kb
def keyjar_init(instance, key_conf, kid_template="a%d"):
"""
Configuration of the type:
keys = [
{"type": "RSA", "key": "cp_keys/key.pem", "use": ["enc", "sig"]},
{"type": "EC", "crv": "P-256", "use": ["sig"]},
{"type": "EC", "crv": "P-256", "use": ["enc"]}
]
:param instance: server/client instance
:param key_conf: The key configuration
:param kid_template: A template by which to build the kids
:return: a JWKS
"""
if instance.keyjar is None:
instance.keyjar = KeyJar()
kid = 0
jwks = {"keys": []}
for spec in key_conf:
typ = spec["type"].upper()
if typ == "RSA":
kb = KeyBundle(source="file://%s" % spec["key"],
fileformat="der",
keytype=typ, keyusage=spec["use"])
elif typ == "EC":
kb = ec_init(spec)
for k in kb.keys():
k.kid = kid_template % kid
kid += 1
instance.kid[k.use][k.kty] = k.kid
jwks["keys"].extend([k.to_dict()
for k in kb.keys() if k.kty != 'oct'])
# for k in kb.keys():
# k.deserialize()
instance.keyjar.add_kb("", kb)
return jwks

View File

@ -16,7 +16,7 @@ from oic.utils.keyio import KeyBundle, keybundle_from_local_file
from oic.utils.keyio import KeyJar
BASE_PATH = os.path.dirname(__file__)
BASE_PATH = os.path.dirname(os.path.abspath(__file__))
CLIENT_CONFIG = {
"client_id": "number5",

View File

@ -21,7 +21,7 @@ __author__ = 'rolandh'
PASSWD = {"user": "hemligt"}
BASE_PATH = os.path.dirname(__file__)
BASE_PATH = os.path.dirname(os.path.abspath(__file__))
ROOT = '%s/../oidc_example/op2/' % BASE_PATH
tl = TemplateLookup(directories=[ROOT + 'templates', ROOT + 'htdocs'],
@ -197,4 +197,4 @@ def test_private_key_jwt():
assert header == {'alg': 'RS256'}
if __name__ == "__main__":
test_3()
test_client_secret_jwt()

View File

@ -166,12 +166,14 @@ def test_kid_usage():
def test_dump_own_keys():
kb = keybundle_from_local_file("file://%s/jwk.json" % BASE_PATH, "jwk", ["ver", "sig"])
kb = keybundle_from_local_file("file://%s/jwk.json" % BASE_PATH, "jwk",
["ver", "sig"])
assert len(kb) == 1
kj = KeyJar()
kj.issuer_keys[""] = [kb]
res = kj.dump_issuer_keys("")
print res
assert len(res) == 1
assert res[0] == {
'use': u'sig',
@ -203,9 +205,6 @@ def test_enc_hmac():
kj.issuer_keys["abcdefgh"] = [kb]
keys = kj.get_encrypt_key(owner="abcdefgh")
for key in keys:
key.deserialize()
_enctxt = _jwe.encrypt(keys, context="public")
assert _enctxt
@ -216,4 +215,4 @@ def test_enc_hmac():
assert json.loads(msg) == payload
if __name__ == "__main__":
test_enc_hmac()
test_dump_own_keys()

View File

@ -54,9 +54,10 @@ def _eq(l1, l2):
CLIENT_SECRET = "abcdefghijklmnop"
CLIENT_ID = "client_1"
KC_SYM_S = KeyBundle({"kty": "oct", "key": "abcdefghijklmnop", "use": "sig"})
KC_SYM_S = KeyBundle({"kty": "oct", "key": "abcdefghijklmnop", "use": "sig",
"alg": "HS256"})
BASE_PATH = os.path.dirname(__file__)
BASE_PATH = os.path.dirname(os.path.abspath(__file__))
_key = rsa_load("%s/rsa.key" % BASE_PATH)
KC_RSA = KeyBundle({"key": _key, "kty": "RSA", "use": "sig"})
@ -1161,8 +1162,8 @@ def test_make_id_token():
if __name__ == "__main__":
t = TestOICClient()
t.setup_class()
t.test_access_token_request()
# t = TestOICClient()
# t.setup_class()
# t.test_access_token_request()
#test_userinfo_request()
test_client_secret_jwt()

View File

@ -26,7 +26,7 @@ CLIENT_ID = "client_1"
KC_SYM_VS = KeyBundle({"kty": "oct", "key": "abcdefghijklmnop", "use": "ver"})
KC_SYM_S = KeyBundle({"kty": "oct", "key": "abcdefghijklmnop", "use": "sig"})
BASE_PATH = os.path.dirname(__file__)
BASE_PATH = os.path.dirname(os.path.abspath(__file__))
KC_RSA = keybundle_from_local_file("%s/rsa.key" % BASE_PATH,
"rsa", ["ver", "sig"])

View File

@ -1,4 +1,6 @@
import json
import os
from time import sleep
from mako.lookup import TemplateLookup
from oic.oauth2 import rndstr
@ -10,7 +12,7 @@ from oic.utils.userinfo import UserInfo
from oic.exception import RedirectURIError
from oic.utils.keyio import KeyBundle
from oic.utils.keyio import KeyBundle, ec_init
from oic.utils.keyio import KeyJar
from oic.utils.keyio import keybundle_from_local_file
@ -70,7 +72,7 @@ SERVER_INFO = {
#"x509_url":"https://connect-op.heroku.com/cert.pem"
}
BASE_PATH = os.path.dirname(__file__)
BASE_PATH = os.path.dirname(os.path.abspath(__file__))
CLIENT_SECRET = "abcdefghijklmnop"
CLIENT_ID = "client_1"
@ -557,6 +559,7 @@ def test_registration_endpoint():
req["client_name"] = "My super service"
req["redirect_uris"] = ["http://example.com/authz"]
req["contacts"] = ["foo@example.com"]
req["response_types"] = ["code"]
print req.to_dict()
@ -570,7 +573,7 @@ def test_registration_endpoint():
'client_secret_expires_at',
'registration_access_token',
'client_id', 'client_secret',
'client_id_issued_at'])
'client_id_issued_at', 'response_types'])
def test_provider_key_setup():
@ -597,7 +600,8 @@ def _client_id(cdb):
def test_registered_redirect_uri_without_query_component():
provider = Provider("FOO", {}, {}, None, None, None, None, "")
rr = RegistrationRequest(operation="register",
redirect_uris=["http://example.org/cb"])
redirect_uris=["http://example.org/cb"],
response_types=["code"])
registration_req = rr.to_json()
@ -646,7 +650,8 @@ def test_registered_redirect_uri_with_query_component():
provider2 = Provider("FOOP", {}, {}, None, None, None, None, "")
rr = RegistrationRequest(operation="register",
redirect_uris=["http://example.org/cb?foo=bar"])
redirect_uris=["http://example.org/cb?foo=bar"],
response_types=["code"])
registration_req = rr.to_json()
resp = provider2.registration_endpoint(request=registration_req)
@ -690,5 +695,23 @@ def test_registered_redirect_uri_with_query_component():
print resp
assert resp is None
def test_key_rollover():
provider2 = Provider("FOOP", {}, {}, None, None, None, None, "")
provider2.keyjar = KEYJAR
# Number of KeyBundles
assert len(provider2.keyjar.issuer_keys[""]) == 1
kb = ec_init({"type": "EC", "crv": "P-256", "use": ["sig"]})
provider2.do_key_rollover(json.loads(kb.jwks()), "b%d")
print provider2.keyjar
assert len(provider2.keyjar.issuer_keys[""]) == 2
kb = ec_init({"type": "EC", "crv": "P-256", "use": ["sig"]})
provider2.do_key_rollover(json.loads(kb.jwks()), "b%d")
print provider2.keyjar
assert len(provider2.keyjar.issuer_keys[""]) == 3
sleep(1)
provider2.remove_inactive_keys(0)
assert len(provider2.keyjar.issuer_keys[""]) == 2
if __name__ == "__main__":
test_server_authenticated_2()
test_key_rollover()