zulip-fediverse-auth/auth.py

183 lines
6.0 KiB
Python
Raw Normal View History

2024-10-05 06:49:36 +00:00
from flask import Flask, Response, redirect
from flask import request
from markupsafe import escape
from mastodon import Mastodon
from email.headerregistry import Address
import jwt
from gevent.pywsgi import WSGIServer
import os
2024-10-05 10:38:36 +00:00
import sqlite3
import zulip
import random
import string
2024-10-05 06:49:36 +00:00
SECRET = os.environ["SECRET"]
ZULIP = f"https://{os.environ['ZULIP']}/accounts/login/jwt/"
2024-10-05 10:38:36 +00:00
REDIRECT = f"https://{os.environ['ZULIP']}/fedi-auth/callback"
DB = os.environ.get("DB", "/var/lib/fedi-zulip/db/applications")
2024-10-05 17:08:54 +00:00
scopes = ["read"]
2024-10-05 10:38:36 +00:00
print(f"""
Zulip is: {os.environ['ZULIP']}
DB location is: {DB}
""")
con = sqlite3.connect(DB)
cur = con.cursor()
zulip_client = zulip.Client()
def get_zulip_user(handle):
2024-10-05 17:08:54 +00:00
print(f"Querying Zulip for handle: {handle}")
2024-10-05 10:38:36 +00:00
zulip_client.call_endpoint(
url=f"/users/{handle}",
method="GET"
)
def create_zulip_user(handle):
2024-10-05 17:08:54 +00:00
print(f"Creating Zulip user with handle: {handle}")
password = ''.join(random.choices(string.ascii_uppercase + string.ascii_lowercase + string.digits, k=40))
full_name = handle.split('@')[0]
payload = {
2024-10-05 10:38:36 +00:00
"email": handle,
"password": password,
2024-10-05 17:08:54 +00:00
"full_name": full_name
}
2024-10-05 10:38:36 +00:00
2024-10-05 17:08:54 +00:00
response = zulip_client.create_user(payload)
2024-10-05 10:38:36 +00:00
2024-10-05 17:08:54 +00:00
if response["result"] == "success":
2024-10-16 16:20:32 +00:00
print(f"Zulip user created for handle: {handle}")
2024-10-05 17:08:54 +00:00
return True
elif response["result"] == "error" and response["msg"] == f"Email '{handle}' already in use":
print(response["msg"] + ", this is okay.")
return True
else:
print(response["msg"])
return False
2024-10-05 10:38:36 +00:00
cur.execute("CREATE TABLE IF NOT EXISTS applications(instance TEXT PRIMARY KEY, client TEXT, secret TEXT, disabled BOOLEAN DEFAULT FALSE)")
def get_application(instance):
res = cur.execute("SELECT client, secret FROM applications WHERE instance = ?", [instance])
return res.fetchone();
def set_application(instance, client, secret):
res = cur.execute("INSERT INTO applications(instance, client, secret) values (?, ?, ?)", (instance, client, secret));
con.commit();
2024-10-05 06:49:36 +00:00
app = Flask(__name__)
2024-10-05 10:38:36 +00:00
@app.get("/fedi-auth/")
2024-10-05 06:49:36 +00:00
def index():
return f"""
<!DOCTYPE html>
<html lang="en">
<body>
<h1>Login to Pleroma Chat Using Handle</h1>
<p>
You can use this page to login to {os.environ['ZULIP']} using your
2024-10-05 17:08:54 +00:00
Pleroma, Akkoma or Mastodon handle. Format is <code>nickname@server.tld</code>.
2024-10-05 06:49:36 +00:00
</p>
2024-10-05 10:38:36 +00:00
<form action="/fedi-auth/login" method="post">
2024-10-05 06:49:36 +00:00
<label for="nickname">Fediverse handle</label>
<br>
<input type="email" id="nickname" name="nickname">
<button>login</button>
</form>
</body>
</html>
"""
2024-10-05 10:38:36 +00:00
@app.post("/fedi-auth/login")
2024-10-05 06:49:36 +00:00
def login():
2024-10-05 10:38:36 +00:00
print("Login POST", flush=True)
2024-10-05 06:49:36 +00:00
instance = Address(addr_spec=request.form["nickname"]).domain
2024-10-05 10:38:36 +00:00
print(f"Instance is: {instance}.", flush=True)
2024-10-05 06:49:36 +00:00
try:
2024-10-05 10:38:36 +00:00
app = get_application(instance)
2024-10-05 06:49:36 +00:00
if app == None:
2024-10-05 17:08:54 +00:00
print(f"There is no OAuth application for {instance} so creating one.")
2024-10-05 06:49:36 +00:00
(client, secret) = Mastodon.create_app(
"zulip",
2024-10-05 17:08:54 +00:00
scopes=scopes,
2024-10-05 06:49:36 +00:00
redirect_uris=REDIRECT,
api_base_url=f"https://{instance}",
)
2024-10-05 10:38:36 +00:00
app = (client, secret)
set_application(instance, client, secret)
2024-10-05 06:49:36 +00:00
2024-10-05 10:38:36 +00:00
(client, secret) = app
2024-10-05 06:49:36 +00:00
masto = Mastodon(
client_id=client,
client_secret=secret,
api_base_url=f"https://{instance}",
)
2024-10-05 10:38:36 +00:00
print(f"Getting login URI for {instance}.", flush=True)
2024-10-05 06:49:36 +00:00
oauth = Mastodon.auth_request_url(
masto,
2024-10-05 17:08:54 +00:00
scopes=scopes,
2024-10-05 06:49:36 +00:00
force_login=True,
redirect_uris=REDIRECT,
state=instance,
)
2024-10-05 10:38:36 +00:00
print(f"Sending user to: {REDIRECT}", flush=True)
2024-10-05 06:49:36 +00:00
return redirect(oauth)
except Exception as e:
print(e)
return Response("fail", status=400)
2024-10-05 10:38:36 +00:00
@app.get("/fedi-auth/callback")
2024-10-05 06:49:36 +00:00
def callback():
oauth = request.args.get("code")
instance = request.args.get("state")
2024-10-05 10:38:36 +00:00
print(f"oauth: {oauth is not None} instance: {instance is not None}")
2024-10-05 06:49:36 +00:00
if oauth != None and instance != None:
try:
2024-10-05 10:38:36 +00:00
app = get_application(instance)
2024-10-05 06:49:36 +00:00
if app != None:
masto = Mastodon(
client_id=app[0],
client_secret=app[1],
api_base_url=f"https://{instance}",
)
2024-10-05 17:08:54 +00:00
Mastodon.log_in(masto, code=oauth, scopes=scopes)
2024-10-05 06:49:36 +00:00
creds = Mastodon.account_verify_credentials(masto)
2024-10-05 17:08:54 +00:00
if hasattr(creds, "error"):
print(f"Verifying credentials for instance: {instance} error: {creds.error}")
return Response("fail", status=400)
2024-10-05 10:38:36 +00:00
handle = f"{creds.acct}@{instance}"
2024-10-05 17:19:56 +00:00
success = create_zulip_user(handle)
2024-10-05 17:08:54 +00:00
if not success:
return Response("fail", status=400)
2024-10-05 10:38:36 +00:00
2024-10-05 06:49:36 +00:00
token = jwt.encode(
2024-10-05 10:38:36 +00:00
{"email": handle},
2024-10-05 06:49:36 +00:00
SECRET,
algorithm="HS256",
)
return f"""
<!DOCTYPE html>
<html lang="en">
<body>
2024-10-05 17:08:54 +00:00
<p>Please wait while you are logged in...</p>
2024-10-05 06:49:36 +00:00
<form name="zulip" action="{ZULIP}" method="post">
<input type="hidden" name="token" value={escape(token)}>
</form>
<script type="text/javascript">
document.zulip.submit();
</script>
</body>
</html>
"""
2024-10-05 10:38:36 +00:00
except Exception as e:
print(e)
2024-10-05 06:49:36 +00:00
pass
2024-10-05 10:38:36 +00:00
print("Some field wasn't set.")
2024-10-05 06:49:36 +00:00
return Response("fail", status=400)