Implement Matrix login with web interface

This commit is contained in:
Tulir Asokan
2018-07-23 11:49:42 -04:00
parent f3e1c755eb
commit 4736686454
8 changed files with 311 additions and 25 deletions
+28
View File
@@ -23,6 +23,8 @@ from telethon.errors import *
from ...commands.auth import enter_password
from ...util import format_duration
from ...puppet import Puppet
from ...user import User
class AuthAPI(abc.ABC):
@@ -36,6 +38,32 @@ class AuthAPI(abc.ABC):
errcode=""):
raise NotImplementedError()
@abstractmethod
def get_mx_login_response(self, status=200, state="", username="", mxid="", message="",
error="", errcode=""):
raise NotImplementedError()
async def post_matrix_token(self, user: User, token):
puppet = Puppet.get(user.tgid)
if puppet.is_real_user:
return self.get_mx_login_response(state="already-logged-in", status=409,
error="You have already logged in with your Matrix "
"account.", errcode="already-logged-in")
resp = await puppet.switch_mxid(token, user.mxid)
if resp == 2:
return self.get_mx_login_response(status=403, errcode="only-login-self",
error="You can only log in as your own Matrix user.")
elif resp == 1:
return self.get_mx_login_response(status=401, errcode="invalid-access-token",
error="Failed to verify access token.")
return self.get_mx_login_response(mxid=user.mxid, status=200, state="logged-in")
async def post_matrix_password(self, user, password):
return self.get_mx_login_response(mxid=user.mxid, status=501, error="Not yet implemented",
errcode="not-yet-implemented")
async def post_login_phone(self, user, phone):
try:
await user.client.sign_in(phone or "+123")
@@ -297,6 +297,10 @@ class ProvisioningAPI(AuthAPI):
"errcode": errcode,
}, status=status)
def get_mx_login_response(self, status=200, state="", username="", mxid="", message="",
error="", errcode=""):
raise NotImplementedError()
def get_login_response(self, status=200, state="", username="", mxid="", message="", error="",
errcode="") -> web.Response:
if username:
+64 -5
View File
@@ -24,6 +24,7 @@ import time
from ...util import sign_token, verify_token
from ...user import User
from ...puppet import Puppet
from ..common import AuthAPI
@@ -38,28 +39,35 @@ class PublicBridgeWebsite(AuthAPI):
self.login = Template(
pkg_resources.resource_string("mautrix_telegram", "web/public/login.html.mako"))
self.mx_login = Template(
pkg_resources.resource_string("mautrix_telegram", "web/public/matrix-login.html.mako"))
self.app = web.Application(loop=loop)
self.app.router.add_route("GET", "/login", self.get_login)
self.app.router.add_route("POST", "/login", self.post_login)
self.app.router.add_route("GET", "/matrix-login", self.get_matrix_login)
self.app.router.add_route("POST", "/matrix-login", self.post_matrix_login)
self.app.router.add_static("/", pkg_resources.resource_filename("mautrix_telegram",
"web/public/"))
def make_token(self, mxid, expires_in=900):
def make_token(self, mxid, endpoint="/login", expires_in=900):
return sign_token(self.secret_key, {
"mxid": mxid,
"endpoint": endpoint,
"expiry": int(time.time()) + expires_in,
})
def verify_token(self, token):
def verify_token(self, token, endpoint="/login"):
token = verify_token(self.secret_key, token)
if token and token.get("expiry", 0) > int(time.time()):
if token and (token.get("expiry", 0) > int(time.time()) and
token.get("endpoint", None) == endpoint):
return token.get("mxid", None)
return None
async def get_login(self, request):
state = "bot_token" if request.rel_url.query.get("mode", "") == "bot" else "request"
mxid = self.verify_token(request.rel_url.query.get("token", None))
mxid = self.verify_token(request.rel_url.query.get("token", None), endpoint="/login")
if not mxid:
return self.get_login_response(status=401, state="invalid-token")
user = User.get_by_mxid(mxid, create=False) if mxid else None
@@ -75,14 +83,65 @@ class PublicBridgeWebsite(AuthAPI):
return self.get_login_response(mxid=user.mxid, username=user.username)
async def get_matrix_login(self, request):
mxid = self.verify_token(request.rel_url.query.get("token", None), endpoint="/matrix-login")
if not mxid:
return self.get_mx_login_response(status=401, state="invalid-token")
user = User.get_by_mxid(mxid, create=False) if mxid else None
if not user:
return self.get_mx_login_response(mxid=mxid)
elif not user.puppet_whitelisted:
return self.get_mx_login_response(mxid=user.mxid, error="You are not whitelisted.",
status=403)
await user.ensure_started()
if not await user.is_logged_in():
return self.get_mx_login_response(mxid=user.mxid, status=403,
error="You are not logged in to Telegram.")
puppet = Puppet.get(user.tgid)
if puppet.is_real_user:
return self.get_mx_login_response(state="already-logged-in", status=409)
return self.get_mx_login_response(mxid=user.mxid)
def get_login_response(self, status=200, state="", username="", mxid="", message="", error="",
errcode=""):
return web.Response(status=status, content_type="text/html",
text=self.login.render(username=username, state=state, error=error,
message=message, mxid=mxid))
def get_mx_login_response(self, status=200, state="", username="", mxid="", message="",
error="", errcode=""):
return web.Response(status=status, content_type="text/html",
text=self.mx_login.render(username=username, state=state, error=error,
message=message, mxid=mxid))
async def post_matrix_login(self, request):
mxid = self.verify_token(request.rel_url.query.get("token", None), endpoint="/matrix-login")
if not mxid:
return self.get_mx_login_response(status=401, state="invalid-token")
data = await request.post()
user = await User.get_by_mxid(mxid).ensure_started()
if not user.puppet_whitelisted:
return self.get_mx_login_response(mxid=user.mxid, error="You are not whitelisted.",
status=403)
elif not await user.is_logged_in():
return self.get_mx_login_response(mxid=user.mxid, status=403,
error="You are not logged in to Telegram.")
mode = data.get("mode", "access_token")
if mode == "password":
return await self.post_matrix_password(user, data["value"])
elif mode == "access_token":
return await self.post_matrix_token(user, data["value"])
return self.get_mx_login_response(mxid=user.mxid, status=400,
error="You must provide an access token or "
"password.")
async def post_login(self, request):
mxid = self.verify_token(request.rel_url.query.get("token", None))
mxid = self.verify_token(request.rel_url.query.get("token", None), endpoint="/login")
if not mxid:
return self.get_login_response(status=401, state="invalid-token")
+51 -2
View File
@@ -19,8 +19,8 @@ form > div {
display: none;
}
form[data-status="request"] > div.status-request,
form[data-status="code"] > div.status-code,
form[data-status="request"] > div.status-request,
form[data-status="code"] > div.status-code,
form[data-status="password"] > div.status-password {
display: initial;
}
@@ -48,3 +48,52 @@ form[data-status="password"] > div.status-password {
background-color: #d4edda;
color: #155724;
}
[type="checkbox"], [type="radio"] {
position: absolute;
opacity: 0;
}
[type="checkbox"] + label, [type="radio"] + label {
position: relative;
padding-left: 2.5rem;
cursor: pointer;
display: inline-block;
}
[type="checkbox"] + label:before, [type="radio"] + label:before {
content: '';
position: absolute;
left: 0;
top: 0.4rem;
width: 1.8rem;
height: 1.8rem;
border: 0.1rem solid #d1d1d1;
}
[type="radio"] + label:before, [type="radio"] + label:after {
border-radius: 50%;
}
[type="checkbox"]:checked + label:after,
[type="radio"]:checked + label:after {
content: '';
width: 0.8rem;
height: 0.8rem;
background: #9b4dca;
position: absolute;
top: 0.9rem;
left: 0.5rem;
}
[type="radio"]:disabled + label:before, [type="checkbox"]:disabled + label:before {
background-color: #d1d1d1;
}
[type="radio"]:disabled + label, [type="checkbox"]:disabled + label {
color: #d1d1d1;
}
[type="radio"]:disabled:checked + label:after, [type="checkbox"]:disabled:checked + label:after {
background: #606c76;
}
+5 -5
View File
@@ -18,9 +18,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<!DOCTYPE html>
<html>
<head>
<title>Mautrix-Telegram bridge</title>
<title>Login - Mautrix-Telegram bridge</title>
<link rel="icon" type="image/png" href="favicon.png"/>
<meta property="og:title" content="Mautrix-Telegram bridge">
<meta property="og:title" content="Login - Mautrix-Telegram bridge">
<meta property="og:description" content="A hybrid puppeting/relaybot Matrix-Telegram bridge">
<meta property="og:image" content="favicon.png">
<meta charset="utf-8">
@@ -40,10 +40,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
function goBack() {
let params = new URLSearchParams(location.search.slice(1))
const mxid = params.get("mxid")
const token = params.get("token")
params = new URLSearchParams()
if (mxid) {
params.set("mxid", mxid)
if (token) {
params.set("token", token)
}
location.replace(location.href.split("?")[0] + "?" + params.toString())
}
@@ -0,0 +1,78 @@
<!--
mautrix-telegram - A Matrix-Telegram puppeting bridge
Copyright (C) 2018 Tulir Asokan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html>
<head>
<title>Matrix login - Mautrix-Telegram bridge</title>
<link rel="icon" type="image/png" href="favicon.png"/>
<meta property="og:title" content="Matrix login - Mautrix-Telegram bridge">
<meta property="og:description" content="A hybrid puppeting/relaybot Matrix-Telegram bridge">
<meta property="og:image" content="favicon.png">
<meta charset="utf-8">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,700">
<link rel="stylesheet" href="//cdn.rawgit.com/necolas/normalize.css/master/normalize.css">
<link rel="stylesheet"
href="//cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css">
<link rel="stylesheet" href="login.css"/>
</head>
<body>
<main class="container">
% if state == "logged-in":
<h1>Logged in successfully!</h1>
<p>
Logged in as ${mxid}.
You can now close this page.
</p>
% elif state == "already-logged-in":
<h1>You're already logged in!</h1>
<p>
If you want to log in with another account, log out using the
<code>logout-matrix</code> management command first.
</p>
% elif state == "invalid-token":
<h1>Invalid or expired token</h1>
<div class="error">Please ask the bridge bot for a new login link.</div>
% else:
<h1>Log in to Matrix</h1>
% if error:
<div class="error">${error}</div>
% endif
% if message:
<div class="message">${message}</div>
% endif
<form method="post">
<fieldset>
<label for="mxid">Matrix ID</label>
<input type="text" id="mxid" name="mxid" disabled value="${mxid}"/>
<input id="access_token" type="radio" name="mode" value="access_token" checked>
<label for="access_token">Access token</label><br>
<input id="password" type="radio" name="mode" value="password">
<label for="password">Password</label><br>
<label for="value">Value</label>
<input type="text" id="value" name="value"
placeholder="Enter Matrix access token or password"/>
<button type="submit">Sign in</button>
</fieldset>
</form>
% endif
</main>
</body>
</html>