Python 中的 SAML 2.0 服务提供者

Posted

技术标签:

【中文标题】Python 中的 SAML 2.0 服务提供者【英文标题】:SAML 2.0 Service Provider in Python 【发布时间】:2015-03-12 01:03:13 【问题描述】:

我希望在 Python 中实现一个基于 SAML 2.0 的服务提供者。

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

我对@9​​87654321@ 进行了广泛的研究,但不幸的是,存在不值得解决的依赖问题,因为我有太多预先存在的服务器/应用程序,它们的环境将不兼容。

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

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

谁能提供关于使用 PySAML2 的任何建议,或者关于如何使用 SAML 2.0 最好地对访问我的应用程序的用户进行身份验证的建议?

【问题讨论】:

【参考方案1】:

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

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

    支持多个 IdP。 使用Flask-Login 进行用户管理。 使用“SSO URL”作为受众限制(以简化 IdP 上的配置)。 及时提供用户(“SAML JIT”) 在属性语句中传递其他用户信息。

没有展示的是执行 SP 发起的身份验证请求 - 我稍后会跟进。

在某个时候,我希望围绕 pysaml2 创建一个具有自以为是的默认值的包装器。

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

以下示例中的所有内容都应使用以下设置:

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

最后,您需要在 Okta 方面做一些事情才能使其正常工作。

First:在 Okta 应用程序配置的 General 选项卡中,配置应用程序以发送“FirstName”和“LastName”属性语句。

第二:在 Okta 应用程序配置的 单点登录 选项卡中,获取 URL 并将它们放入名为 example.okta.com.metadata 的文件中。您可以使用如下命令执行此操作。

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

以下是您的 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)

【讨论】:

非常感谢。这很有帮助。如果你有一个 SP 发起的例子,那就太棒了!你也在这里使用最新版本的 pysaml2 吗? 不客气!是的,我计划在几天内添加一个由 SP 发起的示例。 我不确定pysaml2是最新的哪个版本,根据pip freeze上面例子中我使用的版本是pysaml2==2.2.0 嘿,你有没有开始构建一个由 SP 发起的示例?我一直在研究一个,但我生成的 AuthNRequest 还不正确。 实际上,在接下来的一周左右,我有一些带宽可以解决这个问题。联系我(使用我网页上的链接),我会帮助您使您的代码正常工作,然后我们可以在这里发布结果?

以上是关于Python 中的 SAML 2.0 服务提供者的主要内容,如果未能解决你的问题,请参考以下文章

添加受信任的提供者 (SAML 2.0) 时遇到问题:无法解析我的服务提供者元数据

服务提供者之间的 SAML 2.0 身份验证断言(C#、.net、MVC4、组件空间)

使用 SAML 2.0 的自签名证书

Spring - 如何使用 SAML 2.0 实现单点登录

Shibboleth SP 是 SAML 2.0 的实现吗?

为已通过身份验证的用户向 SP 发起 SAML 2.0 发布