Updated documentation
This commit is contained in:
@@ -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"],
|
||||
}
|
||||
|
||||
|
||||
@@ -4,20 +4,26 @@ 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.
|
||||
|
||||
1.3 On a 401 response the egress challenger, in this case the plugin 'form',
|
||||
is activated.
|
||||
|
||||
- The egress challenger, in this case the plugin 'form', is activated.
|
||||
The configuration of this plugin is::
|
||||
|
||||
[plugin:form]
|
||||
@@ -28,24 +34,29 @@ A GET request is received for /sso
|
||||
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
|
||||
1.4 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
|
||||
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.
|
||||
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.
|
||||
|
||||
2.2 The function that is bound to the path of the IdP now gets to act.
|
||||
This is just the main outline:
|
||||
|
||||
- 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])
|
||||
spid) = IDP.parse_authn_request(query["SAMLRequest"][0])
|
||||
|
||||
* then for the user information::
|
||||
|
||||
@@ -54,8 +65,9 @@ A GET request is received for /sso
|
||||
|
||||
* and finally build the response::
|
||||
|
||||
authn_resp = authn_response(identity, identifier, consumer, spid,
|
||||
name_id_policy, userid)
|
||||
|
||||
authn_resp = IDP.authn_response(identity, identifier, consumer,
|
||||
spid, name_id_policy, userid)
|
||||
|
||||
IDP is assumed to be an instance of saml2.server.Server
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.. _metadata:
|
||||
|
||||
***************************************************
|
||||
Base classes representing Saml2.0 protocol elements
|
||||
Base classes representing Saml2.0 MetaData elements
|
||||
***************************************************
|
||||
|
||||
:Author: Roland Hedberg
|
||||
|
||||
@@ -18,6 +18,8 @@ Base classes representing basic elements
|
||||
metadata
|
||||
xmldsig
|
||||
xmlenc
|
||||
client
|
||||
server
|
||||
|
||||
Module
|
||||
==========
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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,10 +373,7 @@ def kd_response(signature=False, encrypt=False, **kwargs):
|
||||
pass
|
||||
return kwargs
|
||||
|
||||
def do_attributes(identity):
|
||||
attrs = []
|
||||
for key, val in identity.items():
|
||||
dic = {}
|
||||
def _attrval(val):
|
||||
if isinstance(val, basestring):
|
||||
attrval = [kd_attribute_value(val)]
|
||||
elif isinstance(val, list):
|
||||
@@ -382,8 +382,33 @@ def do_attributes(identity):
|
||||
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 = {}
|
||||
|
||||
attrval = _attrval(val)
|
||||
if attrval:
|
||||
dic["attribute_value"] = attrval
|
||||
|
||||
if isinstance(key, basestring):
|
||||
dic["name"] = key
|
||||
elif isinstance(key, tuple): # 3-tuple
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user