feat(api): support OAuth2.0 and OIDC authentication, it has been tested with casdoor

feat(api): support OAuth2.0 and OIDC authentication, it has been tested with casdoor
This commit is contained in:
pycook 2023-12-12 20:29:57 +08:00 committed by GitHub
parent b84d5d717e
commit 093065551b
10 changed files with 340 additions and 10 deletions

View File

@ -19,7 +19,9 @@ from flask.json.provider import DefaultJSONProvider
import api.views.entry import api.views.entry
from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd) from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd)
from api.extensions import inner_secrets from api.extensions import inner_secrets
from api.flask_cas import CAS from api.lib.perm.authentication.cas import CAS
from api.lib.perm.authentication.oauth2 import OAuth2
from api.lib.perm.authentication.oidc import OIDC
from api.lib.secrets.secrets import InnerKVManger from api.lib.secrets.secrets import InnerKVManger
from api.models.acl import User from api.models.acl import User
@ -96,6 +98,8 @@ def create_app(config_object="settings"):
register_shell_context(app) register_shell_context(app)
register_commands(app) register_commands(app)
CAS(app) CAS(app)
OIDC(app)
OAuth2(app)
app.wsgi_app = ReverseProxy(app.wsgi_app) app.wsgi_app = ReverseProxy(app.wsgi_app)
configure_upload_dir(app) configure_upload_dir(app)

View File

@ -0,0 +1 @@
# -*- coding:utf-8 -*-

View File

@ -15,7 +15,7 @@ try:
except ImportError: except ImportError:
from flask import _request_ctx_stack as stack from flask import _request_ctx_stack as stack
from api.flask_cas import routing from . import routing
class CAS(object): class CAS(object):

View File

@ -3,8 +3,13 @@ import uuid
import bs4 import bs4
from flask import Blueprint from flask import Blueprint
from flask import current_app, session, request, url_for, redirect from flask import current_app
from flask_login import login_user, logout_user from flask import redirect
from flask import request
from flask import session
from flask import url_for
from flask_login import login_user
from flask_login import logout_user
from six.moves.urllib_request import urlopen from six.moves.urllib_request import urlopen
from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.cache import UserCache
@ -15,7 +20,8 @@ from .cas_urls import create_cas_validate_url
blueprint = Blueprint('cas', __name__) blueprint = Blueprint('cas', __name__)
@blueprint.route('/api/sso/login') @blueprint.route('/api/cas/login')
# @blueprint.route('/api/sso/login')
def login(): def login():
""" """
This route has two purposes. First, it is used by the user This route has two purposes. First, it is used by the user
@ -63,7 +69,8 @@ def login():
return redirect(redirect_url) return redirect(redirect_url)
@blueprint.route('/api/sso/logout') @blueprint.route('/api/cas/logout')
# @blueprint.route('/api/sso/logout')
def logout(): def logout():
""" """
When the user accesses this route they are logged out. When the user accesses this route they are logged out.

View File

@ -0,0 +1,26 @@
# -*- coding:utf-8 -*-
from flask import current_app
from . import routing
class OAuth2(object):
def __init__(self, app=None, url_prefix=None):
self._app = app
if app is not None:
self.init_app(app, url_prefix)
@staticmethod
def init_app(app, url_prefix=None):
# Configuration defaults
app.config.setdefault('OAUTH2_GRANT_TYPE', 'authorization_code')
app.config.setdefault('OAUTH2_RESPONSE_TYPE', 'code')
app.config.setdefault('OAUTH2_AFTER_LOGIN', '/')
# Register Blueprint
app.register_blueprint(routing.blueprint, url_prefix=url_prefix)
@property
def app(self):
return self._app or current_app

View File

@ -0,0 +1,118 @@
# -*- coding:utf-8 -*-
import secrets
import uuid
import requests
from flask import Blueprint
from flask import abort
from flask import current_app
from flask import redirect
from flask import request
from flask import session
from flask import url_for
from flask_login import login_user, logout_user
from six.moves.urllib.parse import urlencode
from api.lib.perm.acl.cache import UserCache
blueprint = Blueprint('oauth2', __name__)
@blueprint.route('/api/oauth2/login')
@blueprint.route('/api/sso/login')
def login():
if request.values.get("next"):
session["next"] = request.values.get("next")
session['oauth2_state'] = secrets.token_urlsafe(16)
qs = urlencode({
'client_id': current_app.config['OAUTH2_CLIENT_ID'],
'redirect_uri': url_for('oauth2.callback', _external=True),
'response_type': current_app.config['OAUTH2_RESPONSE_TYPE'],
'scope': ' '.join(current_app.config['OAUTH2_SCOPES'] or []),
'state': session['oauth2_state'],
})
return redirect("{}?{}".format(current_app.config['OAUTH2_AUTHORIZE_URL'].split('?')[0], qs))
@blueprint.route('/api/oauth2/callback')
def callback():
redirect_url = session.get("next") or current_app.config.get("OAUTH2_AFTER_LOGIN")
if request.values['state'] != session.get('oauth2_state'):
return abort(401, "state is invalid")
if 'code' not in request.values:
return abort(401, 'code is invalid')
response = requests.post(current_app.config['OAUTH2_TOKEN_URL'], data={
'client_id': current_app.config['OAUTH2_CLIENT_ID'],
'client_secret': current_app.config['OAUTH2_CLIENT_SECRET'],
'code': request.values['code'],
'grant_type': current_app.config['OAUTH2_GRANT_TYPE'],
'redirect_uri': url_for('oauth2.callback', _external=True),
}, headers={'Accept': 'application/json'})
if response.status_code != 200:
current_app.logger.error(response.text)
return abort(401)
oauth2_token = response.json().get('access_token')
if not oauth2_token:
return abort(401)
response = requests.get(current_app.config['OAUTH2_USER_INFO']['url'], headers={
'Authorization': 'Bearer {}'.format(oauth2_token),
'Accept': 'application/json',
})
if response.status_code != 200:
return abort(401)
email = current_app.config['OAUTH2_USER_INFO']['email'](response.json())
username = current_app.config['OAUTH2_USER_INFO']['username'](response.json())
user = UserCache.get(username)
if user is None:
current_app.logger.info("create user: {}".format(username))
from api.lib.perm.acl.user import UserCRUD
user_dict = dict(username=username, email=email)
user_dict['password'] = uuid.uuid4().hex
user = UserCRUD.add(**user_dict)
# log the user in
login_user(user)
from api.lib.perm.acl.acl import ACLManager
user_info = ACLManager.get_user_info(username)
session["acl"] = dict(uid=user_info.get("uid"),
avatar=user.avatar if user else user_info.get("avatar"),
userId=user_info.get("uid"),
rid=user_info.get("rid"),
userName=user_info.get("username"),
nickName=user_info.get("nickname") or user_info.get("username"),
parentRoles=user_info.get("parents"),
childRoles=user_info.get("children"),
roleName=user_info.get("role"))
session["uid"] = user_info.get("uid")
return redirect(redirect_url)
@blueprint.route('/api/oauth2/logout')
@blueprint.route('/api/sso/logout')
def logout():
"acl" in session and session.pop("acl")
"uid" in session and session.pop("uid")
'oauth2_state' in session and session.pop('oauth2_state')
"next" in session and session.pop("next")
redirect_url = url_for('oauth2.login', _external=True, next=request.referrer)
logout_user()
current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))
return redirect(redirect_url)

View File

@ -0,0 +1,25 @@
# -*- coding:utf-8 -*-
from flask import current_app
from . import routing
class OIDC(object):
def __init__(self, app=None, url_prefix=None):
self._app = app
if app is not None:
self.init_app(app, url_prefix)
@staticmethod
def init_app(app, url_prefix=None):
# Configuration defaults
app.config.setdefault('OIDC_GRANT_TYPE', 'authorization_code')
app.config.setdefault('OIDC_RESPONSE_TYPE', 'code')
app.config.setdefault('OIDC_AFTER_LOGIN', '/')
# Register Blueprint
app.register_blueprint(routing.blueprint, url_prefix=url_prefix)
@property
def app(self):
return self._app or current_app

View File

@ -0,0 +1,118 @@
# -*- coding:utf-8 -*-
import secrets
import uuid
import requests
from flask import Blueprint
from flask import abort
from flask import current_app
from flask import redirect
from flask import request
from flask import session
from flask import url_for
from flask_login import login_user, logout_user
from six.moves.urllib.parse import urlencode
from api.lib.perm.acl.cache import UserCache
blueprint = Blueprint('oidc', __name__)
@blueprint.route('/api/oidc/login')
@blueprint.route('/api/sso/login')
def login():
if request.values.get("next"):
session["next"] = request.values.get("next")
session['oidc_state'] = secrets.token_urlsafe(16)
qs = urlencode({
'client_id': current_app.config['OIDC_CLIENT_ID'],
'redirect_uri': url_for('oidc.callback', _external=True),
'response_type': current_app.config['OIDC_RESPONSE_TYPE'],
'scope': ' '.join(current_app.config['OIDC_SCOPES'] or []),
'state': session['oidc_state'],
})
return redirect("{}?{}".format(current_app.config['OIDC_AUTHORIZE_URL'].split('?')[0], qs))
@blueprint.route('/api/oidc/callback')
def callback():
redirect_url = session.get("next") or current_app.config.get("OIDC_AFTER_LOGIN")
if request.values['state'] != session.get('oidc_state'):
return abort(401, "state is invalid")
if 'code' not in request.values:
return abort(401, 'code is invalid')
response = requests.post(current_app.config['OIDC_TOKEN_URL'], data={
'client_id': current_app.config['OIDC_CLIENT_ID'],
'client_secret': current_app.config['OIDC_CLIENT_SECRET'],
'code': request.values['code'],
'grant_type': current_app.config['OIDC_GRANT_TYPE'],
'redirect_uri': url_for('oidc.callback', _external=True),
}, headers={'Accept': 'application/json'})
if response.status_code != 200:
current_app.logger.error(response.text)
return abort(401)
oidc_token = response.json().get('access_token')
if not oidc_token:
return abort(401)
response = requests.get(current_app.config['OIDC_USER_INFO']['url'], headers={
'Authorization': 'Bearer {}'.format(oidc_token),
'Accept': 'application/json',
})
if response.status_code != 200:
return abort(401)
email = current_app.config['OIDC_USER_INFO']['email'](response.json())
username = current_app.config['OIDC_USER_INFO']['username'](response.json())
user = UserCache.get(username)
if user is None:
current_app.logger.info("create user: {}".format(username))
from api.lib.perm.acl.user import UserCRUD
user_dict = dict(username=username, email=email)
user_dict['password'] = uuid.uuid4().hex
user = UserCRUD.add(**user_dict)
# log the user in
login_user(user)
from api.lib.perm.acl.acl import ACLManager
user_info = ACLManager.get_user_info(username)
session["acl"] = dict(uid=user_info.get("uid"),
avatar=user.avatar if user else user_info.get("avatar"),
userId=user_info.get("uid"),
rid=user_info.get("rid"),
userName=user_info.get("username"),
nickName=user_info.get("nickname") or user_info.get("username"),
parentRoles=user_info.get("parents"),
childRoles=user_info.get("children"),
roleName=user_info.get("role"))
session["uid"] = user_info.get("uid")
return redirect(redirect_url)
@blueprint.route('/api/oidc/logout')
@blueprint.route('/api/sso/logout')
def logout():
"acl" in session and session.pop("acl")
"uid" in session and session.pop("uid")
'oidc_state' in session and session.pop('oidc_state')
"next" in session and session.pop("next")
redirect_url = url_for('oidc.login', _external=True, next=request.referrer)
logout_user()
current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))
return redirect(redirect_url)

View File

@ -67,10 +67,12 @@ ONCE = {
} }
} }
# # SSO # =============================== Authentication ===========================================================
# # CAS
AUTH_WITH_CAS = False AUTH_WITH_CAS = False
CAS_SERVER = "http://sso.xxx.com" CAS_SERVER = "https://{your-casdoor-hostname}"
CAS_VALIDATE_SERVER = "http://sso.xxx.com" CAS_VALIDATE_SERVER = "https://{your-casdoor-hostname}"
CAS_LOGIN_ROUTE = "/cas/built-in/cas/login" CAS_LOGIN_ROUTE = "/cas/built-in/cas/login"
CAS_LOGOUT_ROUTE = "/cas/built-in/cas/logout" CAS_LOGOUT_ROUTE = "/cas/built-in/cas/logout"
CAS_VALIDATE_ROUTE = "/cas/built-in/cas/serviceValidate" CAS_VALIDATE_ROUTE = "/cas/built-in/cas/serviceValidate"
@ -83,11 +85,40 @@ CAS_USER_MAP = {
"avatar": {"tag": "cas:attribute", "attrs": {"name": "avatar"}}, "avatar": {"tag": "cas:attribute", "attrs": {"name": "avatar"}},
} }
# # ldap # # OAuth2.0
AUTH_WITH_OAUTH2 = False
OAUTH2_CLIENT_ID = ""
OAUTH2_CLIENT_SECRET = ""
OAUTH2_AUTHORIZE_URL = "https://{your-casdoor-hostname}/login/oauth/authorize"
OAUTH2_TOKEN_URL = "https://{your-casdoor-hostname}/api/login/oauth/access_token"
OAUTH2_USER_INFO = {
"url": "https://{your-casdoor-hostname}/api/userinfo",
"email": lambda x: x['email'],
"username": lambda x: x['name']
}
OAUTH2_SCOPES = ["profile email"]
OAUTH2_AFTER_LOGIN = "/"
# # OIDC
AUTH_WITH_OIDC = False
OIDC_CLIENT_ID = ""
OIDC_CLIENT_SECRET = ""
OIDC_AUTHORIZE_URL = "https://{your-casdoor-hostname}/login/oauth/authorize"
OIDC_TOKEN_URL = "https://{your-casdoor-hostname}/api/login/oauth/access_token"
OIDC_USER_INFO = {
"url": "https://{your-casdoor-hostname}/api/userinfo",
"email": lambda x: x['email'],
"username": lambda x: x['name']
}
OIDC_SCOPES = ["openid profile email"]
OIDC_AFTER_LOGIN = "/"
# # LDAP
AUTH_WITH_LDAP = False AUTH_WITH_LDAP = False
LDAP_SERVER = '' LDAP_SERVER = ''
LDAP_DOMAIN = '' LDAP_DOMAIN = ''
LDAP_USER_DN = 'cn={},ou=users,dc=xxx,dc=com' LDAP_USER_DN = 'cn={},ou=users,dc=xxx,dc=com'
# ==========================================================================================================
# # pagination # # pagination
DEFAULT_PAGE_COUNT = 50 DEFAULT_PAGE_COUNT = 50