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.
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
directives are the toplevel keys.
directives are the top level keys.
.. 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.
@@ -22,8 +22,13 @@ Configuration directives
attribute_maps
^^^^^^^^^^^^^^
A simple key/value file that contains the unique name of attributes and
their friendly names separated by a blank, one attribute per line::
Format::
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.42 givenName
@@ -39,6 +44,10 @@ user friendly names and universally unique names.
cert_file
^^^^^^^^^
Format::
cert_file: ["cert.pem"]
A file that contains CA certificates that the service will use in
HTTPS sessions to verify the server certificate.
*cert_file* must be a PEM formatted certificate chain file.
@@ -46,16 +55,28 @@ HTTPS sessions to verify the server certificate.
debug
^^^^^
Whether debug information should be sent to the logfile.
Format::
debug: 1
Whether debug information should be sent to the log file.
entityid
^^^^^^^^
Format::
entityid: "http://saml.example.com/sp"
The globally unique identifier of the entity.
key_file
^^^^^^^^
Format::
key_file: ["key.pem"]
*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
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"
],
"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"
and "aa".
So if one server is supposted to be both SP and AA (attribute authority) then
the configuration could look something like this::
So if one server is supposed to be both Service Provider (SP) and
Attribute Authority (AA) then the configuration could look something like
this::
"service": {
"aa":{
@@ -103,10 +125,12 @@ Both IdPs and AAs can have the option 'assertions'
assertions (idp/aa)
"""""""""""""""""""
If the server is an IdP or and AA then there might be reasons to things
differently depending on who is asking, this is where that is specified.
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.
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::
"assertions": {
@@ -129,7 +153,8 @@ attribute.
By default there is no restrictions as to which attributes should be
return. Instead all the attributes and values that is gathered by the
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
'givenName' and 'surName' are to be returned. There is no limitations as to
what values on these attributes that can be returned.
@@ -141,21 +166,22 @@ regular expressions.::
"urn:mace:umu.se:saml:roland:sp": {
"lifetime": {"minutes": 5},
"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)
""""""""
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
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.
A typical configuration would look something like this::
attempt is made to pick it out of the metadata.
A typical configuration, when metadata is present, would look something
like this::
"idp": {
"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
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
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
metadata file might be overkill so this construct can be used instead::
metadata might be overkill so this construct can be used instead::
"idp": {
"" : "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
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
is an empty dictionary, in which case all the IdP present in the metadata
will be regarded as eligable services to use. ::
There is a third choice and that is to leave the configuration blank, in
which case all the IdP present in the metadata
will be regarded as eligible services to use. ::
"idp": {
},
@@ -190,11 +216,19 @@ optional_attributes (sp)
Attributes that this SP would like to receive from IdPs.
Example::
"optional_attributes": ["title"],
required_attributes (sp)
""""""""""""""""""""""""
Attributes that this SP demands to receive from IdPs.
Example::
"required_attributes": ["surName", "givenName", "mail"],
subject_data
^^^^^^^^^^^^
@@ -202,11 +236,15 @@ subject_data
The name of a shelve database where the map between a local identifier and
a distributed identifier is kept.
Example::
"subject_data": "./idp.subject.db",
xmlsec_binary
^^^^^^^^^^^^^
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
^^^^^^^^^^^^^^^^^^^^
@@ -221,7 +259,7 @@ Gives information about common identifiers for virtual_organizations::
},
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
for users that are part of the VO.
@@ -251,3 +289,4 @@ We start with a simple Service provider configuration::
},
"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
provider. You have to understand how repoze.who works in order to understand
how the identity provider is supposted to work.
provider, mainly because you have to divide the functionality between
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:
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
user since no login has been attempted.
1.1 Identifiers are checked on ingress and none of them will be able to
identify the user since no login has been done.
- The application states that a 401 reponse should be returned if a
user can not be identified.
1.2 After the ingress plugins have had their turn, the control is passed
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.
The configuration of this plugin is::
1.3 On a 401 response the egress challenger, in this case the plugin 'form',
is activated.
[plugin:form]
use = s2repoze.plugins.formswithhidden:make_plugin
login_form_qs = __do_login
rememberer_name = auth_tkt
The configuration of this plugin is::
[plugin:form]
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
query part of the original GET request in hidden fields.
What's special with this form plugin is that the form carries the
query part of the original GET request in hidden fields.
- The form is displayed, the user enters the user name and password and
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])
1.4 The form is displayed, the user enters the user name and password and
submits the form.
* 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"]
userid = environ["repoze.who.identity"]['repoze.who.userid']
2.2 The function that is bound to the path of the IdP now gets to act.
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::
authn_resp = authn_response(identity, identifier, consumer, spid,
name_id_policy, userid)
* then for the user information::
identity = environ["repoze.who.identity"]["user"]
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
attribute aggregation.
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
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
pySAML2 configuration file which you can read more about here
: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
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.
debug
Debug state, and integer. Presently just on/off.
Debug state, an integer. Presently just on (!= 0)/off (0) is supported.
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
is given that file will be used for persistent storage of the cache.
@@ -68,15 +70,18 @@ An example::
[plugin:saml2sp]
use = s2repoze.plugins.sp:make_plugin
rememberer_name = auth_tkt
saml_conf = sp.conf
virtual_organization=urn:mace:umu.se:vo:it-enheten:cms
rememberer_name = auth_tkt
debug = 1
cache = /tmp/sp.cache
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
`Middleware responsibilities <http://docs.repoze.org/who/narr.html>`_ ::
A typical SP configuration would be to use it in all aspects::
[identifiers]
plugins =
saml2sp

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
import time
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
try:
@@ -350,6 +350,9 @@ def kd_subject(text="", **kwargs):
def kd_authn_statement(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):
kwargs.update({
"version": VERSION,
@@ -370,20 +373,42 @@ def kd_response(signature=False, encrypt=False, **kwargs):
pass
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):
attrs = []
for key, val in identity.items():
dic = {}
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)
attrval = _attrval(val)
if attrval:
dic["attribute_value"] = attrval
if isinstance(key, basestring):
dic["name"] = key
elif isinstance(key, tuple): # 3-tuple

View File

@@ -289,3 +289,37 @@ class TestServer():
assert _eq(ava.keys(), ["mail"])
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])
assert ava.keys() == ["serialNumber"]
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