Updated documentation

This commit is contained in:
Roland Hedberg
2009-11-29 11:49:16 +01:00
parent 64daafdc10
commit 8063698951
10 changed files with 233 additions and 83 deletions

View File

@@ -9,10 +9,10 @@ file is the same disregarding which type of service you plan to run.
What differs is the directives. What differs is the directives.
Below you will find a list of all the used directives in alphabetic order. Below you will find a list of all the used directives in alphabetic order.
The configuration is written as a python dictionary which means that the The configuration is written as a python dictionary which means that the
directives are the toplevel keys. directives are the top level keys.
.. note:: You can build metadata files directly from the configuration. .. note:: You can build metadata files directly from the configuration.
The make_metadata.py script in the pySAML2 tools directory can do it The make_metadata.py script in the pySAML2 tools directory will do it
for you. for you.
@@ -22,8 +22,13 @@ Configuration directives
attribute_maps attribute_maps
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
A simple key/value file that contains the unique name of attributes and Format::
their friendly names separated by a blank, one attribute per line::
attribute_maps: ["attribute.map"]
Points to simple key/value files that, most commonly, contains the unique
name of attributes and their friendly names separated by a blank, one
attribute per line::
urn:oid:2.5.4.4, surName urn:oid:2.5.4.4, surName
urn:oid:2.5.4.42 givenName urn:oid:2.5.4.42 givenName
@@ -39,6 +44,10 @@ user friendly names and universally unique names.
cert_file cert_file
^^^^^^^^^ ^^^^^^^^^
Format::
cert_file: ["cert.pem"]
A file that contains CA certificates that the service will use in A file that contains CA certificates that the service will use in
HTTPS sessions to verify the server certificate. HTTPS sessions to verify the server certificate.
*cert_file* must be a PEM formatted certificate chain file. *cert_file* must be a PEM formatted certificate chain file.
@@ -46,16 +55,28 @@ HTTPS sessions to verify the server certificate.
debug debug
^^^^^ ^^^^^
Whether debug information should be sent to the logfile. Format::
debug: 1
Whether debug information should be sent to the log file.
entityid entityid
^^^^^^^^ ^^^^^^^^
Format::
entityid: "http://saml.example.com/sp"
The globally unique identifier of the entity. The globally unique identifier of the entity.
key_file key_file
^^^^^^^^ ^^^^^^^^
Format::
key_file: ["key.pem"]
*key_file* is the name of a PEM formatted file that contains the private key *key_file* is the name of a PEM formatted file that contains the private key
of the service. This is presently used both to encrypt assertions and as of the service. This is presently used both to encrypt assertions and as
client key in a HTTPS session. client key in a HTTPS session.
@@ -71,7 +92,7 @@ a file accessible on the server the service runs on or somewhere on the net.::
"metadata.xml", "vo_metadata.xml" "metadata.xml", "vo_metadata.xml"
], ],
"remote": [ "remote": [
"https://kalmar2.org/simplesaml/module.php/aggregator/?id=kalmarcentral&set=saml2" "https://kalmar2.org/aggregator/?id=kalmarcentral&set=saml2"
], ],
}, },
@@ -80,8 +101,9 @@ service
Which services the server will provide, those are combinations of "idp","sp" Which services the server will provide, those are combinations of "idp","sp"
and "aa". and "aa".
So if one server is supposted to be both SP and AA (attribute authority) then So if one server is supposed to be both Service Provider (SP) and
the configuration could look something like this:: Attribute Authority (AA) then the configuration could look something like
this::
"service": { "service": {
"aa":{ "aa":{
@@ -103,10 +125,12 @@ Both IdPs and AAs can have the option 'assertions'
assertions (idp/aa) assertions (idp/aa)
""""""""""""""""""" """""""""""""""""""
If the server is an IdP or and AA then there might be reasons to things If the server is an IdP and/or an AA then there might be reasons to do things
differently depending on who is asking, this is where that is specified. differently depending on who is asking; this is where that is specified.
The keys are 'default' and SP entity identifiers, default is used whenever The keys are 'default' and SP entity identifiers, default is used whenever
there is no entry for a specific SP. there is no entry for a specific SP. The reasoning is also that if there is
no default and only SP entity identifiers as keys, then the server will only
except connections from the specified SPs.
An example might be:: An example might be::
"assertions": { "assertions": {
@@ -129,7 +153,8 @@ attribute.
By default there is no restrictions as to which attributes should be By default there is no restrictions as to which attributes should be
return. Instead all the attributes and values that is gathered by the return. Instead all the attributes and values that is gathered by the
database backends will be returned if nothing else is stated. database backends will be returned if nothing else is stated.
In the example above the SP with the entityid "urn:mace:umu.se:saml:roland:sp" In the example above the SP with the entity identifier
"urn:mace:umu.se:saml:roland:sp"
has an attribute restriction: only the attributes has an attribute restriction: only the attributes
'givenName' and 'surName' are to be returned. There is no limitations as to 'givenName' and 'surName' are to be returned. There is no limitations as to
what values on these attributes that can be returned. what values on these attributes that can be returned.
@@ -141,21 +166,22 @@ regular expressions.::
"urn:mace:umu.se:saml:roland:sp": { "urn:mace:umu.se:saml:roland:sp": {
"lifetime": {"minutes": 5}, "lifetime": {"minutes": 5},
"attribute_restrictions":{ "attribute_restrictions":{
"mail": [".*\.umu\.se$"], "mail": [".*.umu.se$"],
} }
} }
} }
Here only mailaddresses that ends with ".umu.se" will be returned. Here only mail addresses that ends with ".umu.se" will be returned.
idp (sp) idp (sp)
"""""""" """"""""
Defines the set of IdPs that this SP can use. If there is a metadata loaded Defines the set of IdPs that this SP can use. If there is metadata loaded
then the value is expected to be a dictionary with entity identifiers as then the value is expected to be a dictionary with entity identifiers as
keys and possibly the IdP url as values. If the url is not defined then an keys and possibly the IdP url as values. If the url is not defined then an
attempt is made to learn it from the metadata. attempt is made to pick it out of the metadata.
A typical configuration would look something like this:: A typical configuration, when metadata is present, would look something
like this::
"idp": { "idp": {
"urn:mace:umu.se:saml:roland:idp": None, "urn:mace:umu.se:saml:roland:idp": None,
@@ -166,9 +192,9 @@ you are using SAML for services within one organization. At configuration
time the url of the IdP might not be know so the evaluation of it is left time the url of the IdP might not be know so the evaluation of it is left
until a metadata file is present. If more than one IdP can be used then until a metadata file is present. If more than one IdP can be used then
the WAYF function (NOT IMPLEMENTED YET) would use the metadata file to the WAYF function (NOT IMPLEMENTED YET) would use the metadata file to
find out the names for the different IdPs. find out the names, to be presented to the user, for the different IdPs.
On the other hand if the SP only uses one specific IdP then the usage of On the other hand if the SP only uses one specific IdP then the usage of
metadata file might be overkill so this construct can be used instead:: metadata might be overkill so this construct can be used instead::
"idp": { "idp": {
"" : "https://example.com/saml2/idp/SSOService.php", "" : "https://example.com/saml2/idp/SSOService.php",
@@ -176,11 +202,11 @@ metadata file might be overkill so this construct can be used instead::
Since the user is immediately sent to the IdP the entity identifier of the IdP Since the user is immediately sent to the IdP the entity identifier of the IdP
is immaterial. In this case the key is expected to be the user friendly is immaterial. In this case the key is expected to be the user friendly
name of the IdP. name of the IdP. Which again if no WAYF is used is immaterial.
There is a third choice and that is to leave the configuration blank, that There is a third choice and that is to leave the configuration blank, in
is an empty dictionary, in which case all the IdP present in the metadata which case all the IdP present in the metadata
will be regarded as eligable services to use. :: will be regarded as eligible services to use. ::
"idp": { "idp": {
}, },
@@ -190,11 +216,19 @@ optional_attributes (sp)
Attributes that this SP would like to receive from IdPs. Attributes that this SP would like to receive from IdPs.
Example::
"optional_attributes": ["title"],
required_attributes (sp) required_attributes (sp)
"""""""""""""""""""""""" """"""""""""""""""""""""
Attributes that this SP demands to receive from IdPs. Attributes that this SP demands to receive from IdPs.
Example::
"required_attributes": ["surName", "givenName", "mail"],
subject_data subject_data
^^^^^^^^^^^^ ^^^^^^^^^^^^
@@ -202,11 +236,15 @@ subject_data
The name of a shelve database where the map between a local identifier and The name of a shelve database where the map between a local identifier and
a distributed identifier is kept. a distributed identifier is kept.
Example::
"subject_data": "./idp.subject.db",
xmlsec_binary xmlsec_binary
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
Presently xmlsec1 binaries are use for all the signing and encryption stuff. Presently xmlsec1 binaries are use for all the signing and encryption stuff.
This option defines where the binary is situatied. This option defines where the binary is situated.
virtual_organization virtual_organization
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
@@ -221,7 +259,7 @@ Gives information about common identifiers for virtual_organizations::
}, },
Keys are identifiers for virtual organizations, the arguments per organization Keys are identifiers for virtual organizations, the arguments per organization
is 'nameid_format' and 'common_identifier'. Usefull if all the IdPs and AAs is 'nameid_format' and 'common_identifier'. Useful if all the IdPs and AAs
that are involved in a virtual organization has common attribute values that are involved in a virtual organization has common attribute values
for users that are part of the VO. for users that are part of the VO.
@@ -251,3 +289,4 @@ We start with a simple Service provider configuration::
}, },
"attribute_maps": ["attribute.map"], "attribute_maps": ["attribute.map"],
} }

View File

@@ -4,58 +4,70 @@ How to make a SAML2 identity provider.
====================================== ======================================
To make an SAML2 identity provider is a bit tricker than doing a service To make an SAML2 identity provider is a bit tricker than doing a service
provider. You have to understand how repoze.who works in order to understand provider, mainly because you have to divide the functionality between
how the identity provider is supposted to work. the application and the plugins.
Now, to do that you have to understand how repoze.who works.
Basically on every request; the ingress plugins first gets to do there stuff,
then the application and finally the egress plugins.
So in broad terms this is what happens: So in broad terms this is what happens:
A GET request is received for /sso 1. A GET request is received for where ever the IdP is supposted to be listing.
- Identifiers are checked and none of them will be able to identify the 1.1 Identifiers are checked on ingress and none of them will be able to
user since no login has been attempted. identify the user since no login has been done.
- The application states that a 401 reponse should be returned if a 1.2 After the ingress plugins have had their turn, the control is passed
user can not be identified. to the application, which must state that a 401 reponse should be
returned if a user tries to access the IdP without an identification.
- The egress challenger, in this case the plugin 'form', is activated. 1.3 On a 401 response the egress challenger, in this case the plugin 'form',
The configuration of this plugin is:: is activated.
[plugin:form] The configuration of this plugin is::
use = s2repoze.plugins.formswithhidden:make_plugin
login_form_qs = __do_login [plugin:form]
rememberer_name = auth_tkt use = s2repoze.plugins.formswithhidden:make_plugin
login_form_qs = __do_login
rememberer_name = auth_tkt
What's special with this form plugin is that the form carries the What's special with this form plugin is that the form carries the
query part of the original GET request in hidden fields. query part of the original GET request in hidden fields.
- The form is displayed, the user enters the user name and password and 1.4 The form is displayed, the user enters the user name and password and
submits the form. submits the form.
- The ingress identifier gets the form and extracts login and password
and passes it on to the authentication plugin. It will also extract
the query parameters from the hidden fields and store them in an
environment variable ('s2repoze.qinfo').
If the login and password was correct a cookie is issued. If there is a
mdprovider plugin defined it will now add extra information about the
individual. After this the control is passed on to the application.
- The function sso() now gets to act. This just the main outline:
* It finds the query parameters in the
environment and parses it::
query = environ["s2repoze.qinfo"]
(consumer, identifier, name_id_policy,
spid) = idp.parse_authn_request(query["SAMLRequest"][0])
* then for the user information:: 2. The log in form reply is received by the server
2.1 The ingress identifier gets the form and extracts login and password
and passes it on to the authentication plugin. It will also extract
the query parameters from the hidden fields and store them in an
environment variable ('s2repoze.qinfo').
If the login and password was correct a cookie is issued. If there is
a mdprovider plugin defined it will now add extra information about
the individual. After this the control is passed on to the
application.
identity = environ["repoze.who.identity"]["user"] 2.2 The function that is bound to the path of the IdP now gets to act.
userid = environ["repoze.who.identity"]['repoze.who.userid'] This is just the main outline:
* It finds the query parameters in the
environment and parses it::
query = environ["s2repoze.qinfo"]
(consumer, identifier, name_id_policy,
spid) = IDP.parse_authn_request(query["SAMLRequest"][0])
* and finally build the response:: * then for the user information::
authn_resp = authn_response(identity, identifier, consumer, spid, identity = environ["repoze.who.identity"]["user"]
name_id_policy, userid) userid = environ["repoze.who.identity"]['repoze.who.userid']
* and finally build the response::
authn_resp = IDP.authn_response(identity, identifier, consumer,
spid, name_id_policy, userid)
IDP is assumed to be an instance of saml2.server.Server

View File

@@ -9,7 +9,8 @@ How it works
A SP handles authentication, by the use of an Identity Provider, and possibly A SP handles authentication, by the use of an Identity Provider, and possibly
attribute aggregation. attribute aggregation.
Both of these functions can be seen as parts of the normal Repoze.who Both of these functions can be seen as parts of the normal Repoze.who
setup. Namely the Challenger, Identifier and MetadataProvider parts. setup. Namely the Challenger, Identifier and MetadataProvider parts so that
is how it is thought to be implemented.
Normal for Repoze.who Identifier and MetadataProvider plugins are that Normal for Repoze.who Identifier and MetadataProvider plugins are that
they place information they gather in environment variables. The convention is they place information they gather in environment variables. The convention is
@@ -40,8 +41,9 @@ The set up
There are two configuration files you have to deal with, first the There are two configuration files you have to deal with, first the
pySAML2 configuration file which you can read more about here pySAML2 configuration file which you can read more about here
:ref:`howto_config` and secondly the repoze.who configuration file. :ref:`howto_config` and secondly the repoze.who configuration file.
And it is the later one I will deal with here.
The plugin configuration has the following arguments The **sp** plugin configuration has the following arguments
use use
Which module to use and which factory function in that module that should Which module to use and which factory function in that module that should
@@ -57,10 +59,10 @@ virtual_organization
Which virtual organization this SP belongs to, can only be none or one. Which virtual organization this SP belongs to, can only be none or one.
debug debug
Debug state, and integer. Presently just on/off. Debug state, an integer. Presently just on (!= 0)/off (0) is supported.
cache cache
If no cache file is defined, a in memory cache will be used to If no cache file is defined, an in-memory cache will be used to
remember information received from IdPs and AAs. If a file name remember information received from IdPs and AAs. If a file name
is given that file will be used for persistent storage of the cache. is given that file will be used for persistent storage of the cache.
@@ -68,15 +70,18 @@ An example::
[plugin:saml2sp] [plugin:saml2sp]
use = s2repoze.plugins.sp:make_plugin use = s2repoze.plugins.sp:make_plugin
rememberer_name = auth_tkt
saml_conf = sp.conf saml_conf = sp.conf
virtual_organization=urn:mace:umu.se:vo:it-enheten:cms virtual_organization=urn:mace:umu.se:vo:it-enheten:cms
rememberer_name = auth_tkt
debug = 1 debug = 1
cache = /tmp/sp.cache
Once you have configured the plugin you have to tell the server to use the Once you have configured the plugin you have to tell the server to use the
plugin in different ingress and egress operations as specified in plugin in different ingress and egress operations as specified in
`Middleware responsibilities <http://docs.repoze.org/who/narr.html>`_ :: `Middleware responsibilities <http://docs.repoze.org/who/narr.html>`_ ::
A typical SP configuration would be to use it in all aspects::
[identifiers] [identifiers]
plugins = plugins =
saml2sp saml2sp

View File

@@ -1,7 +1,7 @@
.. _metadata: .. _metadata:
*************************************************** ***************************************************
Base classes representing Saml2.0 protocol elements Base classes representing Saml2.0 MetaData elements
*************************************************** ***************************************************
:Author: Roland Hedberg :Author: Roland Hedberg

View File

@@ -18,6 +18,8 @@ Base classes representing basic elements
metadata metadata
xmldsig xmldsig
xmlenc xmlenc
client
server
Module Module
========== ==========

View File

@@ -42,13 +42,14 @@ FORM_SPEC = """<form method="post" action="%s">
<input type="submit" value="Submit" /> <input type="submit" value="Submit" />
</form>""" </form>"""
LAX = True LAX = False
SESSION_INFO = {"ava":{}, "came from":"", "not_on_or_after":0, SESSION_INFO = {"ava":{}, "came from":"", "not_on_or_after":0,
"issuer":"", "session_id":-1} "issuer":"", "session_id":-1}
class Saml2Client: class Saml2Client(object):
""" The basic pySAML2 service provider class """
def __init__(self, environ, config=None): def __init__(self, environ, config=None):
""" """

View File

@@ -18,6 +18,7 @@
"""Contains classes and functions that a SAML2.0 Identity provider (IdP) """Contains classes and functions that a SAML2.0 Identity provider (IdP)
or attribute authority (AA) may use to conclude its tasks. or attribute authority (AA) may use to conclude its tasks.
""" """
import shelve import shelve
from saml2 import saml, samlp, VERSION from saml2 import saml, samlp, VERSION
@@ -41,6 +42,7 @@ from saml2.cache import Cache
class Server(object): class Server(object):
""" A class that does things that IdPs or AAs do """
def __init__(self, config_file="", config=None, cache="", def __init__(self, config_file="", config=None, cache="",
log=None, debug=0): log=None, debug=0):
if config_file: if config_file:
@@ -384,12 +386,14 @@ class Server(object):
identity, # identity as dictionary identity, # identity as dictionary
name_id, name_id,
) )
except MissingValue: except MissingValue, exc:
resp = self.do_sso_response( resp = self.do_sso_response(
destination, # consumer_url destination, # consumer_url
in_response_to, # in_response_to in_response_to, # in_response_to
spid, # sp_entity_id spid, # sp_entity_id
name_id, name_id,
status = kd_status_from_exception(exc)
) )

View File

@@ -2,7 +2,7 @@
import time import time
import base64 import base64
from saml2 import samlp, saml, VERSION, sigver from saml2 import samlp, saml, VERSION, sigver, NAME_FORMAT_URI
from saml2.time_util import instant from saml2.time_util import instant
try: try:
@@ -350,6 +350,9 @@ def kd_subject(text="", **kwargs):
def kd_authn_statement(text="", **kwargs): def kd_authn_statement(text="", **kwargs):
return klassdict(saml.Subject, text, **kwargs) return klassdict(saml.Subject, text, **kwargs)
def kd_name_id_policy(text="", **kwargs):
return klassdict(samlp.NameIDPolicy, text, **kwargs)
def kd_assertion(text="", **kwargs): def kd_assertion(text="", **kwargs):
kwargs.update({ kwargs.update({
"version": VERSION, "version": VERSION,
@@ -370,20 +373,42 @@ def kd_response(signature=False, encrypt=False, **kwargs):
pass pass
return kwargs return kwargs
def _attrval(val):
if isinstance(val, basestring):
attrval = [kd_attribute_value(val)]
elif isinstance(val, list):
attrval = [kd_attribute_value(v) for v in val]
elif val == None:
attrval = None
else:
raise OtherError("strange value type on: %s" % val)
return attrval
def ava_to_attributes(ava, bmap):
attrs = []
for key, val in ava.items():
dic = {}
attrval = _attrval(val)
if attrval:
dic["attribute_value"] = attrval
dic["friendly_name"] = key
dic["name"] = bmap[key]
dic["name_format"] = NAME_FORMAT_URI
attrs.append(kd_attribute(**dic))
return attrs
def do_attributes(identity): def do_attributes(identity):
attrs = [] attrs = []
for key, val in identity.items(): for key, val in identity.items():
dic = {} dic = {}
if isinstance(val, basestring):
attrval = [kd_attribute_value(val)] attrval = _attrval(val)
elif isinstance(val, list):
attrval = [kd_attribute_value(v) for v in val]
elif val == None:
attrval = None
else:
raise OtherError("strange value type on: %s" % val)
if attrval: if attrval:
dic["attribute_value"] = attrval dic["attribute_value"] = attrval
if isinstance(key, basestring): if isinstance(key, basestring):
dic["name"] = key dic["name"] = key
elif isinstance(key, tuple): # 3-tuple elif isinstance(key, tuple): # 3-tuple

View File

@@ -289,3 +289,37 @@ class TestServer():
assert _eq(ava.keys(), ["mail"]) assert _eq(ava.keys(), ["mail"])
assert ava["mail"] == ["dj@example.com"] assert ava["mail"] == ["dj@example.com"]
def test_authn_response_0(self):
# reset
del self.server.conf["service"]["idp"]["assertions"][
"urn:mace:example.com:saml:roland:sp"]
ava = { "givenName": ["Derek"], "surName": ["Jeter"],
"mail": ["derek@nyy.mlb.com"]}
resp_str = self.server.authn_response(ava,
"1", "http://local:8087/",
"urn:mace:example.com:saml:roland:sp",
utils.make_instance(samlp.NameIDPolicy,
utils.kd_name_id_policy(
format=saml.NAMEID_FORMAT_TRANSIENT,
allow_create="true")),
"foba0001@example.com")
response = samlp.response_from_string("\n".join(resp_str))
print response.keyswv()
assert _eq(response.keyswv(),['status', 'destination', 'assertion',
'in_response_to', 'issue_instant', 'version',
'issuer', 'id'])
print response.assertion[0].keyswv()
assert len(response.assertion) == 1
assert _eq(response.assertion[0].keyswv(), ['authn_statement',
'attribute_statement', 'subject', 'issue_instant',
'version', 'conditions', 'id'])
assertion = response.assertion[0]
assert len(assertion.attribute_statement) == 1
astate = assertion.attribute_statement[0]
print astate
assert len(astate.attribute) == 3

View File

@@ -498,3 +498,31 @@ def test_filter_values_req_opt_1():
ava = utils.filter_on_attributes(ava, [r], [o]) ava = utils.filter_on_attributes(ava, [r], [o])
assert ava.keys() == ["serialNumber"] assert ava.keys() == ["serialNumber"]
assert _eq(ava["serialNumber"], ["12345","54321"]) assert _eq(ava["serialNumber"], ["12345","54321"])
def _givenName(a):
assert a["name"] == "urn:oid:2.5.4.42"
assert a["friendly_name"] == "givenName"
assert len(a["attribute_value"]) == 1
assert a["attribute_value"] == [{"text":"Derek"}]
def _surName(a):
assert a["name"] == "urn:oid:2.5.4.4"
assert a["friendly_name"] == "surName"
assert len(a["attribute_value"]) == 1
assert a["attribute_value"] == [{"text":"Jeter"}]
def test_ava_to_attributes():
(forward, backward) = utils.parse_attribute_map(["tests/attribute.map"])
attrs = utils.ava_to_attributes(AVA[0], backward)
assert len(attrs) == 2
a = attrs[0]
if a["name"] == "urn:oid:2.5.4.42":
_givenName(a)
_surName(attrs[1])
elif a["name"] == "urn:oid:2.5.4.4":
_surName(a)
_givenName(attrs[1])
else:
print a
assert False