2015-01-13 20 views
11

Tôi đang tìm cách triển khai nhà cung cấp dịch vụ dựa trên SAML 2.0 bằng Python.Nhà cung cấp dịch vụ SAML 2.0 bằng Python

Ứng dụng web của tôi hiện là tất cả các ứng dụng Flask. Tôi dự định tạo ra một bản thiết kế/trang trí Flask cho phép tôi giảm khả năng đăng nhập một lần vào các ứng dụng từ trước.

Tôi đã xem xét python-saml rộng rãi và không may có các vấn đề phụ thuộc không đáng giải quyết, vì tôi có quá nhiều máy chủ/ứng dụng từ trước sẽ không tương thích.

PySAML2 có vẻ như nó có thể hoạt động, tuy nhiên có rất ít tài liệu và tài liệu nào có sẵn mà tôi gặp khó khăn khi hiểu. Không có ví dụ về PySAML2 được sử dụng trong ứng dụng Flask.

Nhà cung cấp nhận dạng tôi có là Okta. Tôi đã thiết lập Okta để sau khi đăng nhập tại Okta, tôi được chuyển hướng đến ứng dụng của mình.

Có ai có thể đưa ra bất kỳ lời khuyên nào về việc sử dụng PySAML2 hay có thể là lời khuyên về cách xác thực tốt nhất người dùng bằng SAML 2.0 đang truy cập vào đơn đăng ký của tôi?

Trả lời

14

Cập nhật: Giải thích chi tiết về using PySAML2 with Okta hiện đã có trên developer.okta.com.

Dưới đây là một số mã mẫu để triển khai SAML SP trong Python/Flask. Mã mẫu này thể hiện một số điều:

  1. Hỗ trợ nhiều IdP.
  2. Sử dụng Flask-Login để quản lý người dùng.
  3. Sử dụng "URL SSO" làm hạn chế đối tượng (để đơn giản hóa cấu hình trên IdP).
  4. Chỉ trong thời gian cung cấp người dùng ("SAML JIT")
  5. Chuyển thông tin người dùng bổ sung trong Bảng sao kê thuộc tính.

không phải là đã chứng minh là đang thực hiện yêu cầu xác thực SP bắt đầu - Tôi sẽ theo dõi sau này.

Tại một thời điểm nào đó, tôi hy vọng sẽ tạo một trình bao bọc xung quanh pysaml2 có mặc định được đề xuất.

Cuối cùng, như python-saml, thư viện pysaml2 sử dụng mã nhị phân xmlsec1. Điều này cũng có thể gây ra các vấn đề phụ thuộc trong môi trường máy chủ của bạn. Nếu đúng như vậy, bạn sẽ muốn xem xét thay thế xmlsec1 bằng thư viện signxml.

Tất cả mọi thứ trong ví dụ dưới đây nên làm việc với các thiết lập sau:

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

Cuối cùng, bạn sẽ cần phải làm gì để mọi thứ ở phía Okta để làm việc này.

Đầu tiên: Trong tab Chung trong cấu hình ứng dụng Okta của bạn, định cấu hình ứng dụng để gửi báo cáo thuộc tính "FirstName" và "LastName".Adding Attribute Statements to an Okta application

Thứ hai: Trong Single Sign On tab cấu hình ứng dụng Okta của bạn, hãy url và đặt chúng trong một file có tên example.okta.com.metadata. Bạn có thể làm điều này với một lệnh như dưới đây.

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

Where to find the metadata url for an Okta application

Dưới đây là những gì bạn sẽ cần cho các ứng dụng Python/Flask bạn để xử lý IdP khởi xướng yêu cầu 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) 
+0

Cảm ơn rất nhiều cho việc này. Điều này rất hữu ích. Nếu bạn có một ví dụ khởi đầu SP sẽ tuyệt vời! Bạn cũng đang sử dụng phiên bản mới nhất của pysaml2 tại đây? – steve

+0

Bạn được chào đón! Có, tôi dự định thêm một ví dụ khởi đầu SP trong một vài ngày. –

+0

Tôi không chắc chắn phiên bản nào của pysaml2 là phiên bản mới nhất, theo 'pip freeze' phiên bản tôi đã sử dụng trong ví dụ trên là' pysaml2 == 2.2.0' –

Các vấn đề liên quan