使用 Azure AD 登录的 Flask 舞蹈示例

Flask dance example for login with Azure AD

我正在尝试使用 flask-login 和 flask-dance 为我的一个应用程序实施 SSO。作为起点,我使用 Flask Dance 网站上提供的示例代码 - https://flask-dance.readthedocs.io/en/v1.2.0/quickstarts/sqla-multiuser.html

我所做的唯一更改是 - 我将 GitHub 替换为我的 Azure AD 凭据

请找到下面的代码:

import sys
from flask import Flask, redirect, url_for, flash, render_template
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm.exc import NoResultFound
from flask_dance.contrib.github import make_github_blueprint, github
from flask_dance.contrib.azure import make_azure_blueprint, azure
from flask_dance.consumer.storage.sqla import OAuthConsumerMixin, SQLAlchemyStorage
from flask_dance.consumer import oauth_authorized, oauth_error
from flask_login import (
    LoginManager, UserMixin, current_user,
    login_required, login_user, logout_user
)

# setup Flask application
app = Flask(__name__)
app.secret_key = "XXXXXXXXXXXXXX"
blueprint = make_azure_blueprint(
    client_id="XXXXXXXXXXXXXXXXXXXXX",
    client_secret="XXXXXXXXXXXXXXXXXXXXXXXX",
    tenant="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
   
)
app.register_blueprint(blueprint, url_prefix="/login")

# setup database models
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///multi.db"
db = SQLAlchemy()

class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    # Your User model can include whatever columns you want: Flask-Dance doesn't care.
    # Here are a few columns you might find useful, but feel free to modify them
    # as your application needs!
    username = db.Column(db.String(1028), unique=True)
    email = db.Column(db.String(1028), unique=True)
    name = db.Column(db.String(1028))

class OAuth(OAuthConsumerMixin, db.Model):
    provider_user_id = db.Column(db.String(1028), unique=True)
    user_id = db.Column(db.Integer, db.ForeignKey(User.id))
    user = db.relationship(User)

# setup login manager
login_manager = LoginManager()
login_manager.login_view = 'azure.login'

@login_manager.user_loader
def load_user(user_id):
    #print(User.query.get(int(user_id)))
    
    return User.query.get(int(user_id))

# setup SQLAlchemy backend
blueprint.storage = SQLAlchemyStorage(OAuth, db.session, user=current_user,user_required=False)

# create/login local user on successful OAuth login
@oauth_authorized.connect_via(blueprint)
def azure_logged_in(blueprint, token):
    if not token:
        #print(token)
        flash("Failed to log in with azure.", category="error")
        return False

    resp = blueprint.session.get("/user")
    if not resp.ok:
        #print(resp)
        msg = "Failed to fetch user info from Azure."
        flash(msg, category="error")
        return False

    azure_info = resp.json()
    azure_user_id = str(azure_info["id"])
    #print(azure_user_id)
    # Find this OAuth token in the database, or create it
    query = OAuth.query.filter_by(
        provider=blueprint.name,
        provider_user_id=azure_user_id,
    )
    try:
        oauth = query.one()
    except NoResultFound:
        oauth = OAuth(
            provider=blueprint.name,
            provider_user_id=azure_user_id,
            token=token,
        )

    if oauth.user:
        login_user(oauth.user)
        flash("Successfully signed in with Azure.")

    else:
        # Create a new local user account for this user
        user = User(
            # Remember that `email` can be None, if the user declines
            # to publish their email address on GitHub!
            email=azure_info["email"],
            name=azure_info["name"],
        )
        # Associate the new local user account with the OAuth token
        oauth.user = user
        # Save and commit our database models
        db.session.add_all([user, oauth])
        db.session.commit()
        # Log in the new local user account
        login_user(user)
        flash("Successfully signed in with Azure.")

    # Disable Flask-Dance's default behavior for saving the OAuth token
    return False

# notify on OAuth provider error
@oauth_error.connect_via(blueprint)
def azure_error(blueprint, error, error_description=None, error_uri=None):
    msg = (
        "OAuth error from {name}! "
        "error={error} description={description} uri={uri}"
    ).format(
        name=blueprint.name,
        error=error,
        description=error_description,
        uri=error_uri,
    )
    flash(msg, category="error")

@app.route("/logout")
@login_required
def logout():
    logout_user()
    flash("You have logged out")
    return redirect(url_for("index"))

@app.route("/")
def index():
    return render_template("home.html")

# hook up extensions to app
db.init_app(app)
login_manager.init_app(app)

if __name__ == "__main__":
    if "--setup" in sys.argv:
        with app.app_context():
            db.create_all()
            db.session.commit()
            print("Database tables created")
    else:
        app.run(debug=True,port=5011)

我还在 HTML 文件中为 'azure.login' 做了适当的修改。 所以在 运行 将它作为 python multi.py --setup 数据库表被创建之后 在我 运行 python multi.py Oauth 舞蹈实际上开始但最后我收到如下错误:

HTTP Response:

127.0.0.1 - - [28/Oct/2020 10:17:44] "?[32mGET /login/azure/authorized?code=0.<Token>HTTP/1.1?[0m" 302 -
127.0.0.1 - - [28/Oct/2020 10:17:44] "?[37mGET / HTTP/1.1?[0m" 200 -

我错过了什么吗?使用 Flask Dance 和 Flask Login 与 Azure AD 进行 SSO 是个好主意吗?或者我应该只与 Flask Session 一起使用 MSAL? 请提供您宝贵的意见..

由于您使用 Azure AD 作为 Flask 舞蹈提供程序,我们需要使用 Microsoft Graph 来获取用户信息。 URL 应该是 https://graph.microsoft.com/v1.0/me。所以请将方法 azure_logged_in中的代码resp = blueprint.session.get("/user")更新为resp = blueprint.session.get("/v1.0/me")。此外,请注意,azure ad 用户的信息有不同的名称。我们还需要更新创建用户的代码。

例如

@oauth_authorized.connect_via(blueprint)
def azure_logged_in(blueprint, token):
    if not token:
        # print(token)
        flash("Failed to log in with azure.", category="error")
        return False

    resp = blueprint.session.get("/v1.0/me")
    # azure.get
    if not resp.ok:
        # print(resp)
        msg = "Failed to fetch user info from Azure."
        flash(msg, category="error")
        return False

    azure_info = resp.json()
    azure_user_id = str(azure_info["id"])
    # print(azure_user_id)
    # Find this OAuth token in the database, or create it
    query = OAuth.query.filter_by(
        provider=blueprint.name,
        provider_user_id=azure_user_id,
    )
    try:
        oauth = query.one()
    except NoResultFound:
        oauth = OAuth(
            provider=blueprint.name,
            provider_user_id=azure_user_id,
            token=token,
        )

    if oauth.user:
        login_user(oauth.user)
        flash("Successfully signed in with Azure.")

    else:
        # Create a new local user account for this user
        user = User(
            # create user with user information from Microsoft Graph
            email=azure_info["mail"],
            username=azure_info["displayName"],
            name=azure_info["userPrincipalName"]
        )
        # Associate the new local user account with the OAuth token
        oauth.user = user
        # Save and commit our database models
        db.session.add_all([user, oauth])
        db.session.commit()
        # Log in the new local user account
        login_user(user)
        flash("Successfully signed in with Azure.")

    # Disable Flask-Dance's default behavior for saving the OAuth token
    return False

详情请参考here and here