diff --git a/.gitignore b/.gitignore index efdf22c74..be7ea1be2 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,11 @@ config/* /static/dist/ package-lock.json index.php + +#oauth2 +oauth2/config/* +!oauth2/config/config.ini.example +oauth2/__pycache__ +oauth2/env +oauth2/oauth2.sock +oauth2/server_info.json \ No newline at end of file diff --git a/oauth2/bot.py b/oauth2/bot.py new file mode 100644 index 000000000..ec319accb --- /dev/null +++ b/oauth2/bot.py @@ -0,0 +1,34 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +import asyncio +import discord +import json +import os +from oauth2 import get_path, get_args + +args = get_args() + +class Bot(discord.Client): + + async def on_ready(self): + await self.change_presence(status=discord.Status.invisible) + for server in self.servers: + for role in server.role_hierarchy: + if role.name == args.verified_role: + verified_role = role + with open(get_path('server_info.json')) as server_file: + user_info = json.load(server_file) + for member in self.get_all_members(): + if member.id == user_info['active']['id']: + if member.top_role >= verified_role: + user_info[member.id]['verified'] = True + if 'authorized' not in user_info: + user_info['authorized'] = [user_info['active']['recent_ip']] + else: + user_info['authorized'].append(user_info['active']['recent_ip']) + else: + user_info[member.id]['verified'] = False + with open(get_path('server_info.json'), 'w') as server_file: + json.dump(user_info, server_file, indent=4) + await self.close() diff --git a/oauth2/config/config.ini.example b/oauth2/config/config.ini.example new file mode 100644 index 000000000..7d03193f5 --- /dev/null +++ b/oauth2/config/config.ini.example @@ -0,0 +1,6 @@ +OAUTH2_CLIENT_ID: ##bot client id +OAUTH2_CLIENT_SECRET: ##bot client secret +OAUTH2_REDIRECT_URI: ##bot redirect uri +bot_token: ##bot token +verified_role: ##minimum role for access (default=@everyone) +two-factor_auth: ##whether 2FA is required for access or not (default=False) \ No newline at end of file diff --git a/oauth2/oauth2.py b/oauth2/oauth2.py new file mode 100644 index 000000000..18307ad2a --- /dev/null +++ b/oauth2/oauth2.py @@ -0,0 +1,158 @@ +#!/usr/bin/python3 + +import os +import sys +import asyncio +import json +import subprocess +import configargparse +from datetime import datetime +from flask import Flask, g, session, redirect, request, url_for, jsonify +from requests_oauthlib import OAuth2Session +from bot import Bot, get_path + +def get_path(path): + if not os.path.isabs(path): + path = os.path.join(os.path.dirname(__file__), path) + return path + + +def get_args(): + if '-cf' not in sys.argv and '--config' not in sys.argv: + config_files = [get_path('config/config.ini')] + parser = configargparse.ArgParser(default_config_files=config_files) + parser.add_argument('-cf', '--config', is_config_file=True, + help='Configuration file') + parser.add_argument('-ocid', '--OAUTH2_CLIENT_ID', type=str, required=True) + parser.add_argument('-ocs', '--OAUTH2_CLIENT_SECRET', type=str, required=True) + parser.add_argument('-oru', '--OAUTH2_REDIRECT_URI', type=str, required=True) + parser.add_argument('-token', '--bot_token', type=str, required=True) + parser.add_argument('-role', '--verified_role', type=str, default='@everyone') + parser.add_argument('-2fa', 'two-factor_auth', action='store_true', default=False) + parser.add_argument('--workers', type=int) + parser.add_argument('--bind', type=str) + parser.add_argument('-m', type=str) + parser.add_argument('wsgi:app', type=str) + + args = parser.parse_args() + + return args + +args = get_args() + +OAUTH2_CLIENT_ID = args.OAUTH2_CLIENT_ID +OAUTH2_CLIENT_SECRET = args.OAUTH2_CLIENT_SECRET +OAUTH2_REDIRECT_URI = args.OAUTH2_REDIRECT_URI +bot_token = args.bot_token + +API_BASE_URL = os.environ.get('API_BASE_URL', 'https://discordapp.com/api') +AUTHORIZATION_BASE_URL = API_BASE_URL + '/oauth2/authorize' +TOKEN_URL = API_BASE_URL + '/oauth2/token' + +app = Flask(__name__) +app.debug = True +app.config['SECRET_KEY'] = OAUTH2_CLIENT_SECRET + +if 'http://' in OAUTH2_REDIRECT_URI: + os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = 'true' + + +def token_updater(token): + session['oauth2_token'] = token + + +def make_session(token=None, state=None, scope=None): + return OAuth2Session( + client_id=OAUTH2_CLIENT_ID, + token=token, + state=state, + scope=scope, + redirect_uri=OAUTH2_REDIRECT_URI, + auto_refresh_kwargs={ + 'client_id': OAUTH2_CLIENT_ID, + 'client_secret': OAUTH2_CLIENT_SECRET, + }, + auto_refresh_url=TOKEN_URL, + token_updater=token_updater) + + +@app.route('/login') +def index(): + scope = request.args.get( + 'scope', + 'identify guilds') + discord = make_session(scope=scope.split(' ')) + authorization_url, state = discord.authorization_url(AUTHORIZATION_BASE_URL) + session['oauth2_state'] = state + return redirect(authorization_url) + + +@app.route('/login/callback') +def callback(): + if request.values.get('error'): + return request.values['error'] + discord = make_session(state=session.get('oauth2_state')) + token = discord.fetch_token( + TOKEN_URL, + client_secret=OAUTH2_CLIENT_SECRET, + authorization_response=request.url) + session['oauth2_token'] = token + return redirect(url_for('.me')) + + +@app.route('/login/me', methods=["GET"]) +def me(): + discord = make_session(token=session.get('oauth2_token')) + user = discord.get(API_BASE_URL + '/users/@me').json() + guilds = discord.get(API_BASE_URL + '/users/@me/guilds').json() + + with open(get_path('server_info.json')) as server_file: + data = json.load(server_file) + + try: + if user['id'] not in data: + data[user['id']] = {'username': user['username'], + 'recent_ip': request.headers["X-Forwarded-For"].split(',')[0]} + if args.two-factor_auth is True: + data[user['id']]['mfa_enabled'] = user['mfa_enabled'] + data['active'] = data[user['id']] + data['active']['id'] = user['id'] + with open(get_path('server_info.json'), 'w') as server_file: + json.dump(data, server_file, indent=4) + else: + data[user['id']]['last_login'] = str(datetime.today()) + data[user['id']]['recent_ip'] = request.headers["X-Forwarded-For"].split(',')[0] + if args.two-factor_auth is True: + data[user['id']]['mfa_enabled'] = user['mfa_enabled'] + data['active'] = data[user['id']] + data['active']['id'] = user['id'] + with open(get_path('server_info.json'), 'w') as server_file: + json.dump(data, server_file, indent=4) + + loop = asyncio.new_event_loop() + client = Bot(loop=loop) + asyncio.set_event_loop(loop) + client.loop = loop + loop.run_until_complete(client.login(bot_token)) + loop.run_until_complete(client.connect()) + + with open(get_path('server_info.json')) as server_file: + data = json.load(server_file) + + if 'verified' not in data[user['id']]: + return "ACCESS DENIED" + elif data[user['id']]['verified'] is False: + return "INSUFFICIENT ROLE FOR ACCESS" + elif args.two-factor_auth is True and data[user['id']]['mfa_enabled'] is False: + return "PLEASE ENABLE 2-FACTOR AUTHORIZATION IN DISCORD" + else: + return redirect('/') + except: + with open(get_path('user_info.json'), 'w') as server_file: + json.dump(user, server_file, indent=4) + + +if __name__ == '__main__': + app.run('158.69.213.118') + + diff --git a/oauth2/oauth2.service b/oauth2/oauth2.service new file mode 100644 index 000000000..bd56ca142 --- /dev/null +++ b/oauth2/oauth2.service @@ -0,0 +1,13 @@ +[Unit] +Description=Gunicorn instance to serve oauth2 +After=network.target + +[Service] +User=root +Group=www-data +WorkingDirectory=/var/www/html/PMSF/oauth2 +Environment="PATH=/var/www/html/PMSF/oauth2/env/bin" +ExecStart=/var/www/html/PMSF/oauth2/env/bin/gunicorn --workers 3 --bind unix:oauth2.sock -m 007 wsgi:app + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/oauth2/requirements.txt b/oauth2/requirements.txt new file mode 100644 index 000000000..2ac855aed --- /dev/null +++ b/oauth2/requirements.txt @@ -0,0 +1,5 @@ +discord.py +requests_oauthlib +flask +gunicorn +configargparse \ No newline at end of file diff --git a/oauth2/wsgi.py b/oauth2/wsgi.py new file mode 100644 index 000000000..8dcef1659 --- /dev/null +++ b/oauth2/wsgi.py @@ -0,0 +1,4 @@ +from oauth2 import app + +if __name__ == "__main__": + app.run()