Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project]
name = 'quorra'
dynamic = ["version"]
description = 'Quorra API server'
description = 'Quorra server'
readme = 'README.md'
requires-python = '>=3.8'
license = {file = 'LICENSE'}
Expand All @@ -24,13 +24,13 @@ dependencies = [
"pillow",
"python-multipart",
"deepmerge",
"python-jose"
"python-jose",
"bech32 @ git+https://github.com/Quorra-Auth/bech32.git"
]

[tool.setuptools]
packages = ["quorra", "quorra.routers"]

# Doesn't work, need to figure out how to add static files to Python project
[tool.setuptools.package-data]
"quorra" = ["fe/**"]

Expand Down
45 changes: 20 additions & 25 deletions quorra/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,29 +34,14 @@ class QRDataResponse(BaseModel):
qr_image: str

class DeviceRegistrationRequest(SQLModel):
pubkey: str
pubkey: str = Field(unique=True)
name: str | None = None

class Device(DeviceRegistrationRequest, table=True):
id: str = Field(primary_key=True)
user_id: str = Field(default=None, foreign_key="user.id")


class AQRMobileStateEnum(str, Enum):
accepted = "accepted"
rejected = "rejected"

# TODO: Send a device UUID as well so that the server can get a hint
class AQRMobileIdentifyRequest(BaseModel):
signature: str
message: str

class AQRMobileAuthenticateRequest(BaseModel):
state: AQRMobileStateEnum
signature: str
message: str


class TokenResponse(BaseModel):
access_token: str
token_type: Literal["Bearer"] = "Bearer"
Expand All @@ -65,7 +50,7 @@ class TokenResponse(BaseModel):

class TransactionTypes(str, Enum):
onboarding = "onboarding"
aqr_oidc_login = "aqr-oidc-login"
ln_oidc_login = "ln-oidc-login"

class TransactionGetRequest(BaseModel):
tx_type: TransactionTypes
Expand All @@ -84,7 +69,7 @@ class Transaction(BaseModel):
tx_id: str | None = None

# TODO: shorten
_expiry: int = 500
_expiry: int = 30
_key_name: str | None = None

def __init__(self, **data):
Expand Down Expand Up @@ -139,8 +124,10 @@ def add_private_data(self, path, data):
def set_contents(self, contents):
vk.json().set(self._key_name, Path.root_path(), contents)

def prolong(self):
vk.expire(self._key_name, self._expiry)
def prolong(self, expiry: int | None = None):
if expiry is None:
expiry = self._expiry
vk.expire(self._key_name, expiry)

def delete(self):
vk.delete(self._key_name)
Expand All @@ -154,12 +141,20 @@ class OnboardingTransaction(Transaction):
# TODO: Move transition checks here
tx_type: TransactionTypes = TransactionTypes.onboarding

class AqrOIDCLoginTransaction(Transaction):
tx_type: TransactionTypes = TransactionTypes.aqr_oidc_login
class LnOIDCLoginTransaction(Transaction):
tx_type: TransactionTypes = TransactionTypes.ln_oidc_login

class AqrOIDCLoginTransactionStates(str, Enum):
class LnOIDCLoginTransactionStates(str, Enum):
created = "created"
identified = "identified"
confirmed = "confirmed"
rejected = "rejected"
token_issued = "token-issued"
finished = "finished"


class LNStatusEnum(str, Enum):
ok = "OK"
error = "error"

class LNStatusResponse(BaseModel):
status: LNStatusEnum
reason: str | None = None
44 changes: 23 additions & 21 deletions quorra/fe/auth/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ window.onload = async function() {
await startAqr();
};

async function findReplace(objClass, text) {
document.querySelectorAll(`.${objClass}`).forEach(el => {
el.textContent = text;
});
}

async function startAqr() {
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
Expand All @@ -12,17 +18,24 @@ async function startAqr() {
if (params.nonce) {
args = args + `&nonce=${params.nonce}`
}
const response = await fetch(`/login/start?${args}`);
const response = await fetch(`../../processes/login/start?${args}`);
if (!response.ok) throw new Error("Request failed");
data = await response.json();
txId = data.tx_id;
await findReplace("clientName", params.client_name);
const url = new URL(params.redirect_uri);
var urlString = url.origin
if (url.protocol !== "https:") {
urlString = `⚠️ ${url.origin} ⚠️`
}
await findReplace("redirectURI", urlString);
await showQrCode();
startPolling();
}

async function showQrCode() {
const payload = { "tx_type": "aqr-oidc-login", "tx_id": txId };
const response = await fetch("/login/qr", {
const payload = { "tx_type": "ln-oidc-login", "tx_id": txId };
const response = await fetch("../../lnurl-auth/qr", {
method: "POST",
headers: {
"Content-Type": "application/json"
Expand All @@ -35,8 +48,8 @@ async function showQrCode() {
}

function startPolling() {
const pollingUrl = `/tx/transaction`;
const payload = { "tx_id": txId, "tx_type": "aqr-oidc-login" }
const pollingUrl = `../../tx/transaction`;
const payload = { "tx_id": txId, "tx_type": "ln-oidc-login" }

const encodeGetParams = p =>
Object.entries(p).map(kv => kv.map(encodeURIComponent).join("=")).join("&");
Expand All @@ -55,32 +68,21 @@ function startPolling() {
return response.json();
})
.then(data => {
console.log("Polling data:", data);
if (data.state == "identified") {
// Hide qr_code_div and show identified_div
if (!qr_div.classList.contains("hidden")) {
qr_div.classList.add("hidden");
}
if (identified_div.classList.contains("hidden")) {
identified_div.classList.remove("hidden");
showStep("identified_div");
}
}
// TODO: rejected state
else if (data.state == "confirmed") {
// Hide identified_div and qr_code_div, show finished_div
if (!qr_div.classList.contains("hidden")) {
qr_div.classList.add("hidden");
}
if (!identified_div.classList.contains("hidden")) {
identified_div.classList.add("hidden");
}
if (finished_div.classList.contains("hidden")) {
finished_div.classList.remove("hidden");
}
showStep("finished_div");

clearInterval(intervalId);
redirectParams = {"code": data.data.oidc_data.code, "state": params.state, "nonce": params.nonce};
window.location.href = params.redirect_uri + "?" + encodeGetParams(redirectParams);
const redirectAddress = params.redirect_uri + "?" + encodeGetParams(redirectParams);
manual_redirect.href = redirectAddress;
window.location.href = redirectAddress;
}
})
.catch(error => {
Expand Down
22 changes: 13 additions & 9 deletions quorra/fe/auth/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,33 @@
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="/fe/style.css"/>
<link rel="stylesheet" href="../style.css"/>
<title>Quorra</title>
</head>
<body>

<h1>Quorra</h1>
<div id="qr_div">
<h2>Scan this QR code on your device</h2>
<div id="qr_div" class="step_div">
<h2>You're signing into</h2>
<h1><span class="clientName"></span></h1>
<small class="redirectURI"></small>
<p>Scan the below code using your device to proceed</p>
<img alt="AQR Code" id="qr"></img>
<div class="hr-text"><span>OR</span></div>
<a id="local_link">Use a local install</a>
<a id="local_link">Use a local application</a>
</div>

<div id="identified_div" class="hidden">
<div id="identified_div" class="hidden step_div">
<h2>Waiting for confirmation on your device...</h2>
</div>

<div id="finished_div" class="hidden">
<h2>And you're logged in!</h2>
<h3>We're redirecting you back to the application</h3>
<div id="finished_div" class="hidden step_div">
<h2>✨ And you're logged in! ✨</h2>
<p>We're redirecting you back to <span class="clientName">the application</span>...</p>
<small>If the redirect doesn't work, click <a id="manual_redirect">here</a></small>
</div>

<script src="auth.js"></script>
<script src="../transitions.js"></script>
</body>
</html>

43 changes: 26 additions & 17 deletions quorra/fe/onboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,30 @@
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="/fe/style.css"/>
<link rel="stylesheet" href="../style.css"/>
<title>Quorra Onboarding</title>
</head>
<body>
<h1 id="welcome_h1">Welcome to Quorra!</h1>
<div id="status_container"></div>
<div id="initial">
<h2>Quorra uses a mobile token to sign you in</h2>
<h3>Download one of our apps to get started</h3>
<div id="initial" class="step_div">
<h1>Welcome to Quorra!</h1>
<h3>Quorra allows you to sign in using QR codes</h3>
<p>Download one of our apps to get started:</p>
<p><a href="https://github.com/k8ieone/voucher">Voucher</a> for Linux</p>
<p><a href="https://github.com/Quorra-Auth/flare">Flare</a> for Android</p>
<button id="start_b" onclick="startOnboarding()">Proceed</button>
<div class="hr-text"><span>OR</span></div>
<p>
You can use your Lightning wallet, <br>
Quorra is powered by <a href=https://lightninglogin.live/learn>Lightning</a> ⚡
</p>
<button id="start_b" onclick="startOnboarding()">Let's get started!</button>
</div>

<div id="details_form_div" class="hidden">
<h2>Please fill in your details</h2>
<div id="details_form_div" class="hidden step_div">
<h1>First, tell us a bit about you</h1>
<h3>Some services like to display a name and email</h3>
<p>Providing this information allows Quorra to pass this information along to your applications.</p>
<form id="details_form">
<label for="username">Name:</label>
<label for="username">Username:</label>
<input id="username_input" name="username" type="text" required />

<label for="email">Email:</label>
Expand All @@ -30,20 +36,23 @@ <h2>Please fill in your details</h2>
</form>
</div>

<div id="qr_div" class="hidden">
<h2>Almost there!</h2>
<h3>Finish your registration by scanning this code with your mobile token</h3>
<div id="qr_div" class="hidden step_div">
<h1>Almost there!</h1>
<h3>Finish your registration by adding your first device</h3>
<p>Scan the below code using your chosen application</p>
<img id="qr"/>
<div class="hr-text"><span>OR</span></div>
<a id="local_link">Use a local install</a>
<a id="local_link">Use a local application</a>
</div>

<div id="finished_div" class="hidden">
<h1>And you're done!</h1>
<h2>You can now start using your newly registered device to sign in with Quorra!</h2>
<div id="finished_div" class="hidden step_div">
<h1>Amazing!</h1>
<h3>✨ You're good to go ✨</h3>
<p>You can now start using your newly registered device to sign in with Quorra!</p>
</div>

<script src="onboarding.js"></script>
<script src="../transitions.js"></script>
</body>
</html>

20 changes: 8 additions & 12 deletions quorra/fe/onboard/onboarding.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ async function getLink() {
}

async function createOnboardingLink() {
const response = await fetch("/onboarding/create", {
const response = await fetch("../../processes/onboarding/create", {
method: "GET",
headers: {
"Content-Type": "application/json"
Expand All @@ -22,7 +22,7 @@ async function createOnboardingLink() {
async function startOnboardingTransaction(onboardingLink) {
const payload = { "link_id": onboardingLink };

const response = await fetch("/onboarding/init", {
const response = await fetch("../../processes/onboarding/init", {
method: "POST",
headers: {
"Content-Type": "application/json"
Expand All @@ -39,7 +39,7 @@ async function startOnboardingTransaction(onboardingLink) {
async function getOnboardingData() {
const payload = { "tx_type": "onboarding", "tx_id": txId };

const response = await fetch("/onboarding/qr", {
const response = await fetch("../../lnurl-auth/qr", {
method: "POST",
headers: {
"Content-Type": "application/json"
Expand All @@ -57,8 +57,7 @@ function startOnboarding() {
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
document.getElementById("initial").classList.add("hidden");
document.getElementById("details_form_div").classList.remove("hidden");
showStep("details_form_div");

document.getElementById("details_form").addEventListener("submit", async function(e) {
e.preventDefault();
Expand All @@ -69,7 +68,7 @@ function startOnboarding() {
const payload = { "tx_id": txId, "data": { "username": name, "email": email }, "tx_type": "onboarding" };

try {
const response = await fetch("/onboarding/entry", {
const response = await fetch("../../processes/onboarding/entry", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
Expand All @@ -80,10 +79,9 @@ function startOnboarding() {
const result = await response.json();
const onboardingData = await getOnboardingData();

document.getElementById("details_form_div").classList.add("hidden");
document.getElementById("qr_div").classList.remove("hidden");
document.getElementById("qr").src = onboardingData.qr_image;
document.getElementById("local_link").href = onboardingData.link;
showStep("qr_div");

} catch (error) {
console.error(error);
Expand All @@ -94,7 +92,7 @@ function startOnboarding() {
}

function startPolling() {
const pollingUrl = `/tx/transaction`;
const pollingUrl = `../../tx/transaction`;
const payload = { "tx_id": txId, "tx_type": "onboarding" }

const intervalId = setInterval(() => {
Expand All @@ -112,9 +110,7 @@ function startPolling() {
if (data.state == "finished") {
// Hide qr_code_div and show identified_div
clearInterval(intervalId);
document.getElementById("qr_div").classList.add("hidden");
document.getElementById("welcome_h1").classList.add("hidden");
document.getElementById("finished_div").classList.remove("hidden");
showStep("finished_div");
}
})
.catch(error => {
Expand Down
Loading