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 ###