Python中的SAML 2.0服务提供程序

时间:2015-01-13 22:46:32

标签: python flask saml saml-2.0

我希望用Python实现基于SAML 2.0的服务提供程序。

我的网络应用程序目前都是Flask应用程序。我打算制作一个Flask蓝图/装饰器,允许我将单点登录功能放到预先存在的应用程序中。

我已经广泛地研究过python-saml,不幸的是,存在一些不值得解决的依赖性问题,因为我有太多预先存在的服务器/应用程序,而这些服务器/应用程序环境不会兼容。

PySAML2看起来可行,但是文档很少,而且我可以理解哪些文档很难理解。 Flask应用程序中没有使用PySAML2的示例。

我拥有的身份提供商是Okta。我有Okta设置,以便在我登录Okta后,我被重定向到我的应用程序。

任何人都可以提供有关使用PySAML2的任何建议,或者建议如何使用访问我的应用程序的SAML 2.0对用户进行最佳身份验证?

1 个答案:

答案 0 :(得分:16)

更新:关于using PySAML2 with Okta的详细说明现在在developer.okta.com上。

下面是一些在Python / Flask中实现SAML SP的示例代码。此示例代码演示了几个方面:

  1. 支持多个IdP。
  2. 使用Flask-Login进行用户管理。
  3. 使用“SSO URL”作为受众限制(以简化IdP上的配置)。
  4. 及时配置用户(“SAML JIT”)
  5. 在“属性语句”中传递其他用户信息。
  6. 演示的内容是执行SP发起的身份验证请求 - 稍后我将跟进。

    在某些时候,我希望在pysaml2周围创建一个包含默认值的包装器。

    最后,与python-saml一样,pysaml2库使用xmlsec1二进制文件。这可能还会导致服务器环境中的依赖性问题。如果是这种情况,您需要考虑将xmlsec1替换为signxml库。

    以下示例中的所有内容均可使用以下设置:

    $ virtualenv venv
    $ source venv/bin/activate
    $ pip install flask flask-login pysaml2
    

    最后,你需要对Okta方面的事情做一些工作。

    首先:在Okta应用程序配置的常规选项卡中,配置应用程序以发送“FirstName”和“LastName”属性语句。 Adding Attribute Statements to an Okta application

    第二:在Okta应用程序配置的 Single Sign On 选项卡中,取出url并将它们放在名为example.okta.com.metadata的文件中。您可以使用如下命令执行此操作。

    $ curl [the metadata url for your Okta application] > example.okta.com.metadata
    

    Where to find the metadata url for an Okta application

    以下是Python / Flask应用程序处理IdP发起的SAML请求所需的内容:

    # -*- coding: utf-8 -*-
    import base64
    import logging
    import os
    import urllib
    import uuid
    import zlib
    
    from flask import Flask
    from flask import redirect
    from flask import request
    from flask import url_for
    from flask.ext.login import LoginManager
    from flask.ext.login import UserMixin
    from flask.ext.login import current_user
    from flask.ext.login import login_required
    from flask.ext.login import login_user
    from saml2 import BINDING_HTTP_POST
    from saml2 import BINDING_HTTP_REDIRECT
    from saml2 import entity
    from saml2.client import Saml2Client
    from saml2.config import Config as Saml2Config
    
    # PER APPLICATION configuration settings.
    # Each SAML service that you support will have different values here.
    idp_settings = {
        u'example.okta.com': {
            u"metadata": {
                "local": [u'./example.okta.com.metadata']
            }
        },
    }
    app = Flask(__name__)
    app.secret_key = str(uuid.uuid4())  # Replace with your secret key
    login_manager = LoginManager()
    login_manager.setup_app(app)
    logging.basicConfig(level=logging.DEBUG)
    # Replace this with your own user store
    user_store = {}
    
    
    class User(UserMixin):
        def __init__(self, user_id):
            user = {}
            self.id = None
            self.first_name = None
            self.last_name = None
            try:
                user = user_store[user_id]
                self.id = unicode(user_id)
                self.first_name = user['first_name']
                self.last_name = user['last_name']
            except:
                pass
    
    
    @login_manager.user_loader
    def load_user(user_id):
        return User(user_id)
    
    
    @app.route("/")
    def main_page():
        return "Hello"
    
    
    @app.route("/saml/sso/<idp_name>", methods=['POST'])
    def idp_initiated(idp_name):
        settings = idp_settings[idp_name]
        settings['service'] = {
            'sp': {
                'endpoints': {
                    'assertion_consumer_service': [
                        (request.url, BINDING_HTTP_REDIRECT),
                        (request.url, BINDING_HTTP_POST)
                    ],
                },
                # Don't verify that the incoming requests originate from us via
                # the built-in cache for authn request ids in pysaml2
                'allow_unsolicited': True,
                'authn_requests_signed': False,
                'logout_requests_signed': True,
                'want_assertions_signed': True,
                'want_response_signed': False,
            },
        }
    
        spConfig = Saml2Config()
        spConfig.load(settings)
        spConfig.allow_unknown_attributes = True
    
        cli = Saml2Client(config=spConfig)
        try:
            authn_response = cli.parse_authn_request_response(
                request.form['SAMLResponse'],
                entity.BINDING_HTTP_POST)
            authn_response.get_identity()
            user_info = authn_response.get_subject()
            username = user_info.text
            valid = True
        except Exception as e:
            logging.error(e)
            valid = False
            return str(e), 401
    
        # "JIT provisioning"
        if username not in user_store:
            user_store[username] = {
                'first_name': authn_response.ava['FirstName'][0],
                'last_name': authn_response.ava['LastName'][0],
                }
        user = User(username)
        login_user(user)
        # TODO: If it exists, redirect to request.form['RelayState']
        return redirect(url_for('user'))
    
    
    @app.route("/user")
    @login_required
    def user():
        msg = u"Hello {user.first_name} {user.last_name}".format(user=current_user)
        return msg
    
    
    if __name__ == "__main__":
        port = int(os.environ.get('PORT', 5000))
        if port == 5000:
            app.debug = True
        app.run(host='0.0.0.0', port=port)