-
Notifications
You must be signed in to change notification settings - Fork 33
Implement OpenID Connect authentication #48
base: master
Are you sure you want to change the base?
Changes from all commits
2eb9005
28d4758
610039f
7ce8e69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,15 +5,18 @@ | |
from flask_login import login_user as login | ||
from flask_login import logout_user as logout | ||
|
||
import config | ||
import constants.api | ||
import database.user | ||
from api.decorators import require_form_args | ||
from api.decorators import require_local_auth | ||
from modern_paste import app | ||
from uri.authentication import * | ||
from util.exception import * | ||
|
||
|
||
@app.route(LoginUserURI.path, methods=['POST']) | ||
@require_local_auth | ||
@require_form_args(['username', 'password']) | ||
def login_user(): | ||
""" | ||
|
@@ -70,3 +73,45 @@ def auth_status(): | |
'user_id': getattr(current_user, 'user_id', None), | ||
}, | ||
}), constants.api.SUCCESS_CODE | ||
|
||
|
||
def oidc_request_loader(): | ||
""" | ||
flask_login request loader for OpenID Connect. | ||
""" | ||
if current_user.is_authenticated: | ||
return | ||
|
||
from modern_paste import oidc | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move this import to the top of the file? |
||
info = None | ||
if flask.g.oidc_id_token is not None: | ||
# Auth succeeded with flask-oidc OIDC client flow | ||
username = oidc.user_getfield('sub') | ||
try: | ||
login(database.user.get_user_by_username(username)) | ||
return | ||
except UserDoesNotExistException: | ||
info = oidc.user_getinfo(['sub', 'name', 'email']) | ||
|
||
elif flask.request.headers.get('Authorization', '').startswith('Bearer '): | ||
# An OAuth2 Bearer Token was sent | ||
token = flask.request.headers['Authorization'].split(' ', 1)[1].strip() | ||
valid = oidc.validate_token(token, config.AUTH_OIDC_SCOPE) | ||
if valid is True: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
username = flask.g.oidc_token_info['sub'] | ||
try: | ||
login(database.user.get_user_by_username(username)) | ||
except UserDoesNotExistException: | ||
info = oidc.user_getinfo(['sub', 'name', 'email'], token) | ||
else: | ||
return flask.jsonify(constants.api.AUTH_FAILURE), constants.api.AUTH_FAILURE_CODE | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function isn't an API handler, what is the meaning of returning an API response here? |
||
|
||
if info is not None: | ||
new_user = database.user.create_new_user( | ||
username=info['sub'], | ||
password=None, | ||
signup_ip=flask.request.remote_addr, | ||
name=info.get('name'), | ||
email=info.get('email') | ||
) | ||
login(new_user) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,7 +39,10 @@ def view_function(): | |
""" | ||
@wraps(func) | ||
def decorated_view(*args, **kwargs): | ||
template, context = func(*args, **kwargs) | ||
response = func(*args, **kwargs) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change seems to be unrelated to this PR. What is the motivation? |
||
if not isinstance(response, tuple): | ||
return response | ||
template, context = response | ||
if 'config' in context: | ||
context['config'].update(context_config()) | ||
else: | ||
|
@@ -80,7 +83,7 @@ def require_login_api(func): | |
""" | ||
A custom implementation of Flask-login's built-in @login_required decorator. | ||
This decorator will allow usage of the API endpoint if the user is either currently logged in via the app | ||
or if the user authenticates with an API key in the POST JSON parameters. | ||
or if the user authenticates with an API key in the POST JSON parameters and local auth is used. | ||
This implementation overrides the behavior taken when the current user is not authenticated by | ||
returning the predefined AUTH_FAILURE JSON response with HTTP status code 401. | ||
This decorator is intended for use with API endpoints. | ||
|
@@ -91,7 +94,7 @@ def decorator(*args, **kwargs): | |
if current_user.is_authenticated: | ||
return func(*args, **kwargs) | ||
try: | ||
if data and data.get('api_key'): | ||
if config.AUTH_METHOD == 'local' and data and data.get('api_key'): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add |
||
user = database.user.get_user_by_api_key(data['api_key'], active_only=True) | ||
login_user(user) | ||
del data['api_key'] | ||
|
@@ -107,7 +110,7 @@ def optional_login_api(func): | |
""" | ||
This decorator is similar in behavior to require_login_api, but is intended for use with endpoints that offer | ||
extended functionality with a login, but can still be used without any authentication. | ||
The decorator will set current_user if authentication via an API key is provided, and will continue without error | ||
The decorator will set current_user if authentication via an API key is provided and local auth is used, and will continue without error | ||
otherwise. | ||
This decorator is intended for use with API endpoints. | ||
""" | ||
|
@@ -117,7 +120,7 @@ def decorator(*args, **kwargs): | |
if current_user.is_authenticated: | ||
return func(*args, **kwargs) | ||
try: | ||
if data and data.get('api_key'): | ||
if config.AUTH_METHOD == 'local' and data and data.get('api_key'): | ||
user = database.user.get_user_by_api_key(data['api_key'], active_only=True) | ||
login_user(user) | ||
del data['api_key'] | ||
|
@@ -160,3 +163,15 @@ def decorated_view(*args, **kwargs): | |
return func(*args, **kwargs) | ||
return decorated_view | ||
return decorator | ||
|
||
|
||
def require_local_auth(func): | ||
""" | ||
Makes the current API endpoint only work with local auth, otherwise returns an error. | ||
""" | ||
@wraps(func) | ||
def decorator(*args, **kwargs): | ||
if config.AUTH_METHOD != 'local': | ||
return jsonify(AUTH_METHOD_DISABLED_FAILURE), AUTH_METHOD_DISABLED_FAILURE_CODE | ||
return func(*args, **kwargs) | ||
return decorator |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -59,6 +59,13 @@ | |
} | ||
USER_REGISTRATION_DISABLED_FAILURE_CODE = 403 | ||
|
||
AUTH_METHOD_DISABLED_FAILURE = { | ||
RESULT: RESULT_FAULURE, | ||
MESSAGE: 'The auth method you attempted to use is disabled on this server.', | ||
FAILURE: 'auth_method_disabled_failure' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: trailing comma |
||
} | ||
AUTH_METHOD_DISABLED_FAILURE_CODE = 403 | ||
|
||
PASTE_ATTACHMENTS_DISABLED_FAILURE = { | ||
RESULT: RESULT_FAULURE, | ||
MESSAGE: 'The server administrator has disabled paste attachments.', | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
|
||
app = Flask(__name__) | ||
app.config.from_object('flask_config') | ||
|
||
db = flask_sqlalchemy.SQLAlchemy(app, session_options={ | ||
'expire_on_commit': False, | ||
}) | ||
|
@@ -12,10 +13,19 @@ | |
login_manager = flask_login.LoginManager() | ||
login_manager.init_app(app) | ||
|
||
|
||
import models | ||
from views import * | ||
|
||
def setup_oidc(): | ||
from flask_oidc import OpenIDConnect | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move these imports to the top of the file? |
||
from api.authentication import oidc_request_loader | ||
app.config['OIDC_CLIENT_SECRETS'] = config.AUTH_OIDC_CLIENT_SECRETS | ||
oidc = OpenIDConnect(app) | ||
app.before_request(oidc_request_loader) | ||
|
||
if config.AUTH_METHOD == 'oidc': | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto here about the |
||
setup_oidc() | ||
|
||
|
||
if __name__ == '__main__': | ||
app.run(host='0.0.0.0', debug=config.BUILD_ENVIRONMENT == constants.build_environment.DEV) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,15 +34,19 @@ | |
</p> | ||
<a href="#" class="pastes-list-group-item settings-item list-group-item sans-serif semibold active" data-section="pastes-settings">MY PASTES</a> | ||
<a href="#" class="user-profile-list-group-item settings-item list-group-item" data-section="user-profile-settings">USER PROFILE</a> | ||
<a href="#" class="api-key-list-group-item settings-item list-group-item" data-section="api-key-settings">API KEY</a> | ||
<a href="#" class="deactivate-list-group-item settings-item list-group-item" data-section="deactivate-settings">DEACTIVATE</a> | ||
{% if auth_method == 'local' %} | ||
<a href="#" class="api-key-list-group-item settings-item list-group-item" data-section="api-key-settings">API KEY</a> | ||
<a href="#" class="deactivate-list-group-item settings-item list-group-item" data-section="deactivate-settings">DEACTIVATE</a> | ||
{% endif %} | ||
</div> | ||
|
||
<div class="mobile-account-settings-list-group btn-group sans-serif regular gray less-spaced size-2" role="group"> | ||
<button type="button" class="pastes-list-group-item settings-item btn btn-default sans-serif semibold active" data-section="pastes-settings">MY PASTES</button> | ||
<button type="button" class="user-profile-list-group-item settings-item btn btn-default" data-section="user-profile-settings">USER PROFILE</button> | ||
<button type="button" class="api-key-list-group-item settings-item btn btn-default" data-section="api-key-settings">API KEY</button> | ||
<button type="button" class="deactivate-list-group-item settings-item btn btn-default" data-section="deactivate-settings">DEACTIVATE</button> | ||
{% if auth_method == 'local' %} | ||
<button type="button" class="api-key-list-group-item settings-item btn btn-default" data-section="api-key-settings">API KEY</button> | ||
<button type="button" class="deactivate-list-group-item settings-item btn btn-default" data-section="deactivate-settings">DEACTIVATE</button> | ||
{% endif %} | ||
</div> | ||
|
||
<!--My pastes--> | ||
|
@@ -113,6 +117,7 @@ | |
</div> | ||
</div> | ||
|
||
{% if auth_method == 'local' %} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indent the following HTML block? |
||
<!--API key--> | ||
<div class="api-key-settings settings-section-container hidden-setting"> | ||
<div class="settings-header"> | ||
|
@@ -164,6 +169,7 @@ | |
<button type="button" class="deactivate-account-button btn btn-danger sans-serif semibold white size-1 less-spaced" autocomplete="off">DEACTIVATE MY ACCOUNT</button> | ||
</div> | ||
</div> | ||
{% endif %} | ||
</div> | ||
|
||
<!--Modal for paste deactivation confirmation--> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,20 @@ | |
from modern_paste import app | ||
|
||
|
||
@app.context_processor | ||
def add_config(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if this is necessary; there is already an abstraction in place to provide the config to the template during server-side rendering in |
||
""" | ||
Templating utility to retrieve specific configuration information. | ||
|
||
Currently injected: | ||
|
||
enabled_user_registration | ||
auth_method | ||
""" | ||
return dict(auth_method=config.AUTH_METHOD, | ||
enable_user_registration=config.ENABLE_USER_REGISTRATION) | ||
|
||
|
||
@app.context_processor | ||
def import_static_resources(): | ||
""" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is not an API handler and doesn't belong in this file. Maybe create a new file
app/util/oidc.py
and move this there?