Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
Hiboo
Manage
Activity
Members
Labels
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Package Registry
Container Registry
Model registry
Operate
Terraform modules
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Terms and privacy
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
felinn-glotte
Hiboo
Commits
b1c0acf8
Commit
b1c0acf8
authored
5 years ago
by
kaiyou
Browse files
Options
Downloads
Patches
Plain Diff
Overal review of the SAML code and expose SAML metadata
parent
f8a7c460
No related branches found
Branches containing commit
No related tags found
Tags containing commit
No related merge requests found
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
hiboo/sso/saml.py
+97
-46
97 additions, 46 deletions
hiboo/sso/saml.py
hiboo/sso/templates/protocol_saml.html
+6
-3
6 additions, 3 deletions
hiboo/sso/templates/protocol_saml.html
hiboo/sso/templates/saml_metadata.xml
+19
-0
19 additions, 0 deletions
hiboo/sso/templates/saml_metadata.xml
with
122 additions
and
49 deletions
hiboo/sso/saml.py
+
97
−
46
View file @
b1c0acf8
"""
The SAML SSO provider relies heavily on pysaml2
Instead of simply using pysaml2 however, it overrides the original MetaData
store and removes all unnecessary bits for Hiboo, keeping only the basics of
request parsing and response crafting.
Also, instead of using xmlsec directly through the binary, which requires
writing temporary files, it leverages the Python bindings to xmlsec.
"""
# We monkey-patch the security context factory, so that we can silently
# replace it with our own xmlsec-based implementation.
# replace it with our own Python xmlsec-based implementation.
# This needs to be done before importing other pysaml2 components.
def
security_context
(
conf
):
return
SecurityContext
(
conf
)
from
saml2
import
sigver
...
...
@@ -7,27 +18,38 @@ sigver.security_context = security_context
from
hiboo.sso
import
blueprint
,
forms
from
hiboo
import
models
,
utils
,
profile
,
security
from
saml2
import
server
,
saml
,
config
,
mdstore
,
assertion
from
cryptography
import
x509
from
cryptography.hazmat
import
primitives
,
backends
from
saml2
import
mdstore
,
server
import
saml2
,
base64
,
datetime
,
flask
,
xmlsec
,
lxml
.
etree
,
flask_login
import
saml2
import
base64
import
datetime
import
flask
import
xmlsec
import
lxml.etree
class
Config
(
object
):
"""
Handles service configuration and forms.
Settings are:
- acs: the assertion consuming service (on the SP side)
- entityid: the SP entity id (IDP entity id is its metadata endpoint)
- sign_mode: response signature mode (either the assertion or the response)
"""
IDP_CERT_NAME
=
"
{}-idp
"
SP_CERT_NAME
=
"
{}-sp
"
RSA_KEY_LENGTH
=
2048
@classmethod
def
derive_form
(
cls
,
form
):
"""
Add required fields to a form.
"""
return
type
(
'
NewForm
'
,
(
forms
.
SAMLForm
,
form
),
{})
return
type
(
"
DerivedSAMLForm
"
,
(
forms
.
SAMLForm
,
form
),
{})
@classmethod
def
populate_service
(
cls
,
form
,
service
):
"""
Populate a service from a form
"""
service
.
config
.
update
({
"
acs
"
:
form
.
acs
.
data
,
"
entityid
"
:
form
.
entityid
.
data
,
...
...
@@ -37,8 +59,6 @@ class Config(object):
@classmethod
def
populate_form
(
cls
,
service
,
form
):
"""
Populate a form from a service
"""
form
.
process
(
obj
=
service
,
acs
=
service
.
config
.
get
(
"
acs
"
),
...
...
@@ -49,16 +69,18 @@ class Config(object):
@classmethod
def
update_keys
(
cls
,
service
):
if
"
idp_cert
"
not
in
service
.
config
:
key
,
cert
=
cls
.
generate_key
(
service
.
uuid
+
"
-idp
"
)
key
,
cert
=
cls
.
generate_key
(
cls
.
IDP_CERT_NAME
.
format
(
service
.
uuid
)
)
service
.
config
.
update
({
"
idp_key
"
:
key
,
"
idp_cert
"
:
cert
})
if
"
sp_cert
"
not
in
service
.
config
:
key
,
cert
=
cls
.
generate_key
(
service
.
uuid
+
"
-sp
"
)
key
,
cert
=
cls
.
generate_key
(
cls
.
SP_CERT_NAME
.
format
(
service
.
uuid
)
)
service
.
config
.
update
({
"
sp_key
"
:
key
,
"
sp_cert
"
:
cert
})
@classmethod
def
generate_key
(
cls
,
cn
):
"""
Generate an RSA key and self signed certificate for SAML.
"""
key
=
primitives
.
asymmetric
.
rsa
.
generate_private_key
(
key_size
=
2048
,
public_exponent
=
65535
,
key_size
=
cls
.
RSA_KEY_LENGTH
,
public_exponent
=
65535
,
backend
=
backends
.
default_backend
()
)
now
=
datetime
.
datetime
.
utcnow
()
...
...
@@ -85,12 +107,15 @@ class MetaData(mdstore.InMemoryMetaData):
"""
def
__init__
(
self
,
attrc
,
entityid
,
**
kwargs
):
(
super
(
MetaData
,
self
).
__init__
)
(
attrc
,
**
kwargs
)
super
(
MetaData
,
self
).
__init__
(
attrc
,
**
kwargs
)
self
.
entityid
=
entityid
def
load
(
self
,
*
args
,
**
kwargs
):
"""
Load the service metadata for asserion generation.
"""
# We simply load a dummy metadata for an SPSSO descriptor, with a
# standard ACS and no required attribute (since attributes are fixed
# when using Hiboo)
self
.
entity
.
update
({
self
.
entityid
:
{
'
spsso_descriptor
'
:
[
{
'
attribute_consuming_service
'
:
[{
'
requested_attribute
'
:
[]}]}
]}})
...
...
@@ -99,32 +124,31 @@ class MetaData(mdstore.InMemoryMetaData):
def
get_config
(
cls
,
service
):
"""
Load the IDP configuration.
"""
# Very simple IDP configuration, no specific restriction or logic
# since those are implemented in the Flask views
idp_service
=
{
'
endpoints
'
:{},
'
policy
'
:{
'
default
'
:
{
'
lifetime
'
:{
'
minutes
'
:
15
},
'
attribute_restrictions
'
:
None
,
'
name_form
'
:
saml2
.
saml
.
NAME_FORMAT_URI
,
'
entity_categories
'
:[]}},
'
endpoints
'
:{},
'
policy
'
:{
'
default
'
:
{
'
lifetime
'
:{
'
minutes
'
:
15
},
'
attribute_restrictions
'
:
None
,
'
name_form
'
:
saml2
.
saml
.
NAME_FORMAT_URI
,
'
entity_categories
'
:[]}
},
# Name ids are only profile uuid and usernames, so they comply with
# the persistent standard
'
name_id_format
'
:
[
saml2
.
saml
.
NAMEID_FORMAT_PERSISTENT
]
}
config_dict
=
{
'
key_file
'
:
""
.
join
(
service
.
config
[
"
idp_key
"
].
strip
().
split
(
"
\n
"
)[
1
:
-
1
]),
'
cert_file
'
:
""
.
join
(
service
.
config
[
"
sp_cert
"
].
strip
().
split
(
"
\n
"
)[
1
:
-
1
]),
'
service
'
:{
'
idp
'
:
idp_service
},
'
metadata
'
:[
{
'
class
'
:
'
hiboo.sso.saml.MetaData
'
,
'
metadata
'
:[(
service
.
config
[
"
entityid
"
],
)]}
]
# The IDP only has one metadata, because we spawn one IDP per SP
'
metadata
'
:[{
'
class
'
:
'
hiboo.sso.saml.MetaData
'
,
'
metadata
'
:[(
service
.
config
[
"
entityid
"
],
)]
}]
}
return
config
.
config_factory
(
'
idp
'
,
config_dict
)
class
CertHandler
(
object
):
"""
Dummy implementation of a CertHandler that we can instanciate for
the security context.
"""
def
generate_cert
(
self
):
return
False
return
saml2
.
config
.
config_factory
(
'
idp
'
,
config_dict
)
class
SecurityContext
(
sigver
.
SecurityContext
):
...
...
@@ -136,19 +160,24 @@ class SecurityContext(sigver.SecurityContext):
"""
def
__init__
(
self
,
conf
):
self
.
cert_handler
=
CertHandler
()
self
.
cert_handler
=
self
self
.
conf
=
conf
def
_check_signature
(
self
,
decoded_xml
,
item
,
node_name
=
None
,
origdoc
=
None
,
id_attr
=
''
,
must
=
False
,
only_valid_cert
=
False
,
issuer
=
None
):
"""
Override the signature checking functino and use xmlsec
"""
# TODO: actually check the signature from authentication requests
# (not critical, but required for production)
return
item
def
sign_statement
(
self
,
statement
,
node_name
,
key
=
None
,
key_file
=
None
,
node_id
=
None
,
id_attr
=
''
):
"""
Override the statement signature function and use xmlsec
"""
xml
=
lxml
.
etree
.
fromstring
(
statement
)
# Specify the id attribute so that xmlsec can find the object referenced
# in the signature block.
xmlsec
.
tree
.
add_ids
(
xml
,
[
'
ID
'
])
# Actually perform the signature
signature
=
xmlsec
.
tree
.
find_node
(
xml
,
xmlsec
.
constants
.
NodeSignature
)
context
=
xmlsec
.
SignatureContext
()
context
.
key
=
xmlsec
.
Key
.
from_memory
(
...
...
@@ -158,35 +187,57 @@ class SecurityContext(sigver.SecurityContext):
context
.
sign
(
signature
)
return
lxml
.
etree
.
tostring
(
xml
)
def
generate_cert
(
self
):
"""
Dummy function so that the security context can act as its own
cert handler (which does nothing since certficiates are loaded separately)
"""
return
False
@blueprint.route
(
'
/saml/<service_uuid>
'
,
methods
=
[
"
GET
"
,
"
POST
"
])
@blueprint.route
(
"
/saml/
redirect/
<service_uuid>
"
,
methods
=
[
"
GET
"
,
"
POST
"
])
def
saml_redirect
(
service_uuid
):
# Get the profile from user input (implies redirects)
service
=
models
.
Service
.
query
.
get
(
service_uuid
)
or
flask
.
abort
(
404
)
service
.
protocol
==
"
saml
"
or
flask
.
abort
(
404
)
# Get the profile from user input (implies redirects)
picked
=
profile
.
get_profile
(
service
,
intent
=
True
)
or
flask
.
abort
(
403
)
# Parse the authentication request
and
check the
ACS
# Parse the authentication request
(which
check
s
the
signature)
idp
=
server
.
Server
(
config
=
(
MetaData
.
get_config
(
service
)))
xml
=
flask
.
request
.
args
[
"
SAMLRequest
"
]
request
=
idp
.
parse_authn_request
(
xml
,
saml2
.
BINDING_HTTP_REDIRECT
)
request
.
message
.
issuer
or
flask
.
abort
(
403
)
#service.config["acs"] == request.message.issuer.text or flask.abort(403)
# Check that the request was properly issued for us and by the configured SP
url
=
flask
.
url_for
(
"
.saml_redirect
"
,
service_uuid
=
service_uuid
,
_external
=
True
)
if
not
(
request
.
message
.
destination
and
request
.
message
.
issuer
and
request
.
message
.
destination
==
url
and
request
.
message
.
issuer
.
text
==
service
.
config
[
"
entityid
"
]):
flask
.
abort
(
403
)
# Provide a SAML response
response
=
idp
.
create_authn_response
(
userid
=
picked
.
uuid
,
identity
=
{
'
uid
'
:
picked
.
username
,
'
email
'
:
picked
.
email
"
uid
"
:
picked
.
username
,
"
email
"
:
picked
.
email
},
# We currently only authenticate by password, this will change
authn
=
{
"
class_ref
"
:
saml2
.
saml
.
AUTHN_PASSWORD
},
in_response_to
=
request
.
message
.
id
,
issuer
=
service_uuid
,
issuer
=
flask
.
url_for
(
"
.saml_metadata
"
,
service_uuid
=
service_uuid
,
_external
=
True
)
,
destination
=
service
.
config
[
"
acs
"
],
sp_entity_id
=
service
.
config
[
"
entityid
"
],
userid
=
picked
.
username
,
authn
=
{
'
class_ref
'
:
saml2
.
saml
.
AUTHN_PASSWORD
},
sign_response
=
service
.
config
[
"
sign_mode
"
]
==
"
response
"
,
sign_assertion
=
service
.
config
[
"
sign_mode
"
]
==
"
assertion
"
)
return
flask
.
render_template
(
'
sso_redirect.html
'
,
target
=
service
.
config
[
"
acs
"
],
data
=
{
'
SAMLResponse
'
:
base64
.
b64encode
(
response
).
decode
(
'
ascii
'
),
'
RelayState
'
:
flask
.
request
.
args
.
get
(
'
RelayState
'
,
''
)
return
flask
.
render_template
(
"
sso_redirect.html
"
,
target
=
service
.
config
[
"
acs
"
],
data
=
{
"
SAMLResponse
"
:
base64
.
b64encode
(
response
).
decode
(
"
ascii
"
),
"
RelayState
"
:
flask
.
request
.
args
.
get
(
"
RelayState
"
,
""
)
})
@blueprint.route
(
"
/saml/metadata/<service_uuid>.xml
"
)
def
saml_metadata
(
service_uuid
):
service
=
models
.
Service
.
query
.
get
(
service_uuid
)
or
flask
.
abort
(
404
)
service
.
protocol
==
"
saml
"
or
flask
.
abort
(
404
)
entityid
=
flask
.
url_for
(
"
.saml_metadata
"
,
service_uuid
=
service_uuid
,
_external
=
True
)
metadata
=
flask
.
render_template
(
"
saml_metadata.xml
"
,
service
=
service
,
entityid
=
entityid
)
response
=
flask
.
make_response
(
metadata
)
response
.
headers
[
"
Content-Type
"
]
=
"
application/xml
"
return
response
This diff is collapsed.
Click to expand it.
hiboo/sso/templates/protocol_saml.html
+
6
−
3
View file @
b1c0acf8
...
...
@@ -2,11 +2,14 @@
{% macro description() %}{% trans %}SAML2 is a legacy protocol based on XML security. Only redirect/post binding is supported.{% endtrans %}{% endmacro %}
{% macro describe(service) %}
<dt>
{% trans %}Endpoint{% endtrans %}
</dt>
<dd>
{{ url_for("sso.saml_redirect", service_uuid=service.uuid, _external=True) }}
</dd>
<dt>
{% trans %}SAML Metadata{% endtrans %}
</dt>
<dd><pre>
{{ url_for("sso.saml_metadata", service_uuid=service.uuid, _external=True) }}
</pre></dd>
<dt>
{% trans %}SSO redirect binding{% endtrans %}
</dt>
<dd><pre>
{{ url_for("sso.saml_redirect", service_uuid=service.uuid, _external=True) }}
</pre></dd>
<dt>
{% trans %}ACS{% endtrans %}
</dt>
<dd>
{{ service.config["acs"] }}
</dd>
<dd>
<pre>
{{ service.config["acs"] }}
</
pre></
dd>
<dt>
{% trans %}IDP certificate{% endtrans %}
</dt>
<dd><pre>
{{ service.config["idp_cert"] }}
</pre></dd>
...
...
This diff is collapsed.
Click to expand it.
hiboo/sso/templates/saml_metadata.xml
0 → 100644
+
19
−
0
View file @
b1c0acf8
<?xml version="1.0" encoding="UTF-8"?>
<EntitiesDescriptor
Name=
"{{ service.uuid }}"
xmlns=
"urn:oasis:names:tc:SAML:2.0:metadata"
xmlns:dsig=
"http://www.w3.org/2000/09/xmldsig#"
>
<EntityDescriptor
entityID=
"{{ entityid }}"
>
<IDPSSODescriptor
WantAuthnRequestsSigned=
"true"
protocolSupportEnumeration=
"urn:oasis:names:tc:SAML:2.0:protocol"
>
<KeyDescriptor
use=
"signing"
>
<dsig:KeyInfo>
<dsig:X509Data>
<dsig:X509Certificate>
{{ "".join(service.config["idp_cert"].strip().split("\n")[1:-1]) }}
</dsig:X509Certificate>
</dsig:X509Data>
</dsig:KeyInfo>
</KeyDescriptor>
<NameIDFormat>
urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
</NameIDFormat>
<NameIDFormat>
urn:oasis:names:tc:SAML:2.0:nameid-format:transient
</NameIDFormat>
<NameIDFormat>
urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
</NameIDFormat>
<NameIDFormat>
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
</NameIDFormat>
<SingleSignOnService
Binding=
"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location=
"{{ url_for("
sso.saml_redirect",
service_uuid=
service.uuid,
_external=
True)
}}"
/>
</IDPSSODescriptor>
</EntityDescriptor>
</EntitiesDescriptor>
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment