mirror of https://github.com/veops/cmdb.git
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:
parent
b84d5d717e
commit
093065551b
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
# -*- coding:utf-8 -*-
|
|
@ -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):
|
|
@ -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.
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue