diff --git a/hiboo/sso/saml.py b/hiboo/sso/saml.py index 71e4daf15682922d2046bc92c3adcec7172ea69c..b55d91c5684631744dbb99c60c884a01f20845f8 100644 --- a/hiboo/sso/saml.py +++ b/hiboo/sso/saml.py @@ -7,8 +7,8 @@ store and removes all unnecessary bits for Hiboo, keeping only the basics of request parsing and response crafting. """ -from hiboo.sso import blueprint, forms -from hiboo import models, utils, profile, security +from hiboo.sso import blueprint, get_service +from hiboo import profile from cryptography import x509 from cryptography.hazmat import primitives, backends @@ -20,73 +20,42 @@ import datetime import flask -class Config(object): - """ Handles service configuration and forms. +IDP_CERT_NAME = "{}-idp" +SP_CERT_NAME = "{}-sp" +RSA_KEY_LENGTH = 2048 - 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): - return type("DerivedSAMLForm", (forms.SAMLForm, form), {}) +def fill_service(service): + if "idp_cert" not in service.config: + key, cert = generate_saml_key(IDP_CERT_NAME.format(service.uuid)) + service.config.update({"idp_key": key, "idp_cert": cert}) + if "sp_cert" not in service.config: + key, cert = generate_saml_key(SP_CERT_NAME.format(service.uuid)) + service.config.update({"sp_key": key, "sp_cert": cert}) - @classmethod - def populate_service(cls, form, service): - service.config.update({ - "acs": form.acs.data, - "entityid": form.entityid.data, - "sign_mode": form.sign_mode.data - }) - cls.update_keys(service) - @classmethod - def populate_form(cls, service, form): - form.process( - obj=service, - acs=service.config.get("acs"), - entityid=service.config.get("entityid"), - sign_mode=service.config.get("sign_mode") - ) - - @classmethod - def update_keys(cls, service): - if "idp_cert" not in service.config: - 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(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=cls.RSA_KEY_LENGTH, public_exponent=65535, - backend=backends.default_backend() - ) - now = datetime.datetime.utcnow() - subject = x509.Name([x509.NameAttribute(x509.NameOID.COMMON_NAME, cn)]) - cert = x509.CertificateBuilder().subject_name(subject)\ - .issuer_name(subject).serial_number(x509.random_serial_number())\ - .public_key(key.public_key())\ - .not_valid_before(now)\ - .not_valid_after(now + datetime.timedelta(days=3650))\ - .sign(key, primitives.hashes.SHA256(), backends.default_backend()) - return ( - key.private_bytes(primitives.serialization.Encoding.PEM, - format=primitives.serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=primitives.serialization.NoEncryption() - ).decode("ascii"), - cert.public_bytes(primitives.serialization.Encoding.PEM).decode("ascii") - ) +def generate_saml_key(cn): + """ Generate an RSA key and self signed certificate for SAML. + """ + key = primitives.asymmetric.rsa.generate_private_key( + key_size=RSA_KEY_LENGTH, public_exponent=65535, + backend=backends.default_backend() + ) + now = datetime.datetime.utcnow() + subject = x509.Name([x509.NameAttribute(x509.NameOID.COMMON_NAME, cn)]) + cert = x509.CertificateBuilder().subject_name(subject)\ + .issuer_name(subject).serial_number(x509.random_serial_number())\ + .public_key(key.public_key())\ + .not_valid_before(now)\ + .not_valid_after(now + datetime.timedelta(days=3650))\ + .sign(key, primitives.hashes.SHA256(), backends.default_backend()) + return ( + key.private_bytes(primitives.serialization.Encoding.PEM, + format=primitives.serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=primitives.serialization.NoEncryption() + ).decode("ascii"), + cert.public_bytes(primitives.serialization.Encoding.PEM).decode("ascii") + ) class MetaData(mdstore.InMemoryMetaData): @@ -160,8 +129,7 @@ class MetaData(mdstore.InMemoryMetaData): @blueprint.route("/saml/redirect/<service_uuid>", methods=["GET", "POST"]) def saml_redirect(service_uuid): - service = models.Service.query.get(service_uuid) or flask.abort(404) - service.protocol == "saml" or flask.abort(404) + service = get_service(service_uuid, "saml") # Get the profile from user input (implies redirects) picked = profile.get_profile(service, intent=True) or flask.abort(403) # Parse the authentication request (which checks the signature) @@ -192,8 +160,7 @@ def saml_redirect(service_uuid): @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) + service = get_service(service_uuid, "saml") config = MetaData.get_config(service) xml, _ = metadata.entities_descriptor( [metadata.entity_descriptor(config)], diff --git a/hiboo/sso/templates/protocol_oidc.html b/hiboo/sso/templates/protocol_oidc.html deleted file mode 100644 index ce0ac0f7fe0da8de897edd0fa05ffa9b011b4259..0000000000000000000000000000000000000000 --- a/hiboo/sso/templates/protocol_oidc.html +++ /dev/null @@ -1,16 +0,0 @@ -{% macro name() %}OIDC{% endmacro %} -{% macro description() %}{% trans %}OpenID Connect (OIDC) is JWT based authentication and authorization protocol{% endtrans %}{% endmacro %} - -{% macro describe(service) %} -<dt>{% trans %}Authorization endpoint{% endtrans %}</dt> -<dd><pre>{{ url_for("sso.oidc_authorize", service_uuid=service.uuid, _external=True) }}</pre></dd> - -<dt>{% trans %}Token endpoint{% endtrans %}</dt> -<dd><pre>{{ url_for("sso.oidc_token", service_uuid=service.uuid, _external=True) }}</pre></dd> - -<dt>{% trans %}Client ID{% endtrans %}</dt> -<dd><pre>{{ service.config["client_id"] }}</pre></dd> - -<dt>{% trans %}Client secret{% endtrans %}</dt> -<dd><pre>{{ service.config["client_secret"] }}</dd> -{% endmacro %} diff --git a/hiboo/sso/templates/protocol_saml.html b/hiboo/sso/templates/protocol_saml.html deleted file mode 100644 index c4074b52e302f411a9c5aaf10682615bade078af..0000000000000000000000000000000000000000 --- a/hiboo/sso/templates/protocol_saml.html +++ /dev/null @@ -1,31 +0,0 @@ -{% macro name() %}SAML2{% endmacro %} -{% 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 %}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><pre>{{ service.config["acs"] }}</pre></dd> - -<dt>{% trans %}IDP certificate{% endtrans %}</dt> -<dd><pre>{{ service.config["idp_cert"] }}</pre></dd> - -<dt>{% trans %}Short IDP certificate{% endtrans %}</dt> -<dd><pre>{{ "".join(service.config["idp_cert"].strip().split("\n")[1:-1]) }}</pre></dd> - -<dt{% trans %}>SP certificate{% endtrans %}</dt> -<dd><pre>{{ service.config["sp_cert"] }}</pre></dd> - -<dt>{% trans %}Short SP certificate{% endtrans %}</dt> -<dd><pre>{{ "".join(service.config["sp_cert"].strip().split("\n")[1:-1]) }}</pre></dd> - -<dt>{% trans %}SP private key{% endtrans %}</dt> -<dd><pre>{{ service.config["sp_key"] }}</pre></dd> - -<dt>{% trans %}Short SP private key{% endtrans %}</dt> -<dd><pre>{{ "".join(service.config["sp_key"].strip().split("\n")[1:-1]) }}</pre></dd> -{% endmacro %} diff --git a/migrations/versions/24626feb96c7_protocol_to_application.py b/migrations/versions/24626feb96c7_protocol_to_application.py new file mode 100644 index 0000000000000000000000000000000000000000..6e77cbdb523c0e12c85f94d24301917cbece5aec --- /dev/null +++ b/migrations/versions/24626feb96c7_protocol_to_application.py @@ -0,0 +1,41 @@ +""" Support applications instead of simple SSO protocols + +Revision ID: 24626feb96c7 +Revises: ccae1d6b9c13 +Create Date: 2020-03-19 16:58:13.343455 +""" + +from alembic import op +import sqlalchemy as sa +import hiboo + + +revision = '24626feb96c7' +down_revision = 'ccae1d6b9c13' +branch_labels = None +depends_on = None + + +service_table = sa.Table( + 'service', + sa.MetaData(), + sa.Column('protocol', sa.String(length=255), nullable=False), + sa.Column('application_id', sa.String(length=255), nullable=False), +) + + +def upgrade(): + with op.batch_alter_table('service') as batch_op: + batch_op.add_column(sa.Column('application_id', sa.String(length=255), nullable=True)) + connection = op.get_bind() + connection.execute(service_table.update().values(application_id=service_table.c.protocol)) + with op.batch_alter_table('service') as batch_op: + batch_op.drop_column('application') + batch_op.drop_column('protocol') + + +def downgrade(): + with op.batch_alter_table('service') as batch_op: + batch_op.add_column(sa.Column('protocol', sa.VARCHAR(length=25), nullable=True)) + batch_op.add_column(sa.Column('application', sa.VARCHAR(length=255), nullable=True)) + batch_op.drop_column('application_id') diff --git a/migrations/versions/fa59f288c9f2_initial_database_creation.py b/migrations/versions/fa59f288c9f2_initial_database_creation.py index 5447507a5348c7bdd04e8e7e4ee047f8baf6d127..7ad0b479fa16127d887d2e4d048e65891a9057c7 100644 --- a/migrations/versions/fa59f288c9f2_initial_database_creation.py +++ b/migrations/versions/fa59f288c9f2_initial_database_creation.py @@ -17,7 +17,6 @@ depends_on = None def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### op.create_table('service', sa.Column('protocol', sa.String(length=25), nullable=True), sa.Column('name', sa.String(length=255), nullable=True), @@ -87,14 +86,11 @@ def upgrade(): sa.ForeignKeyConstraint(['user_uuid'], ['user.uuid'], ), sa.PrimaryKeyConstraint('uuid') ) - # ### end Alembic commands ### def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### op.drop_table('history') op.drop_table('profile') op.drop_table('auth') op.drop_table('user') op.drop_table('service') - # ### end Alembic commands ###