[ui/api] check authentication on pages that need it / add endpoint to verify authentication
This commit is contained in:
parent
f72cee6774
commit
33a0c077d1
|
@ -0,0 +1,86 @@
|
||||||
|
import {fetch_timeout} from "./util";
|
||||||
|
import {t} from "./i18n";
|
||||||
|
import {API_ROOT} from "./config";
|
||||||
|
import {Logger, logSetup} from "./logger";
|
||||||
|
import {getCookie} from "./cookie";
|
||||||
|
|
||||||
|
logSetup();
|
||||||
|
const logger = new Logger("auth.ts");
|
||||||
|
|
||||||
|
export function redact_token(token: string) {
|
||||||
|
const stars = "*".repeat(token.length - 5);
|
||||||
|
return token.substring(5) + stars;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function enforce_session(): Promise<[boolean, string]> {
|
||||||
|
logger.info("Checking session authentication");
|
||||||
|
const session_token = getCookie("sessionToken");
|
||||||
|
if (session_token === "") {
|
||||||
|
logger.error("No session token is present");
|
||||||
|
return [false, ""];
|
||||||
|
}
|
||||||
|
logger.info(`Session token is ${redact_token(session_token)}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch_timeout(`${API_ROOT}/v1/auth/check_session`, {
|
||||||
|
'method': 'POST',
|
||||||
|
'headers': {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${session_token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
const rawerror = JSON.parse(await resp.text()).errors[0].message;
|
||||||
|
return [false, rawerror];
|
||||||
|
} else {
|
||||||
|
// session ok
|
||||||
|
return [true, session_token];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// error in http request
|
||||||
|
return [false, `${e}`]
|
||||||
|
}
|
||||||
|
return [false, ""];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function enforce_auth(): Promise<[boolean, string]> {
|
||||||
|
logger.info("Checking mfa authentication");
|
||||||
|
|
||||||
|
const session_result = await enforce_session();
|
||||||
|
|
||||||
|
if (!session_result[0]) {
|
||||||
|
// session token is invalid
|
||||||
|
logger.error("Session token is invalid, therefore auth token cannot be valid");
|
||||||
|
return [false, session_result[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
const session_token = session_result[1];
|
||||||
|
|
||||||
|
const auth_token = getCookie("authToken");
|
||||||
|
if (auth_token === "") {
|
||||||
|
logger.error("No auth token is present");
|
||||||
|
return [false, ""];
|
||||||
|
}
|
||||||
|
logger.info(`MFA token is ${redact_token(auth_token)}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch_timeout(`${API_ROOT}/v1/auth/check_auth`, {
|
||||||
|
'method': 'POST',
|
||||||
|
'headers': {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${session_token} ${auth_token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
const rawerror = JSON.parse(await resp.text()).errors[0].message;
|
||||||
|
return [false, rawerror];
|
||||||
|
} else {
|
||||||
|
// session ok
|
||||||
|
return [true, `${session_token} ${auth_token}`];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// error in http request
|
||||||
|
return [false, `${e}`]
|
||||||
|
}
|
||||||
|
return [false, ""];
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ export function setCookie(name: string, value: string, expires: number) {
|
||||||
document.cookie = name + "=" + value + ";" + expires_at + ";path=/";
|
document.cookie = name + "=" + value + ";" + expires_at + ";path=/";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCookie(name: string): string {
|
export function getCookie(name: string): string {
|
||||||
const name_with_equals = name + "=";
|
const name_with_equals = name + "=";
|
||||||
const decodedCookie = decodeURIComponent(document.cookie);
|
const decodedCookie = decodeURIComponent(document.cookie);
|
||||||
const ca = decodedCookie.split(';');
|
const ca = decodedCookie.split(';');
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
"magicLinkExplainer": "We sent you a link, click on it to continue logging in.",
|
"magicLinkExplainer": "We sent you a link, click on it to continue logging in.",
|
||||||
"apierror": {
|
"apierror": {
|
||||||
"authorization was provided but it is expired or invalid": "User does not exist, maybe consider creating an account?",
|
"authorization was provided but it is expired or invalid": "User does not exist, maybe consider creating an account?",
|
||||||
"xhrerror": "unable to contact server, please try again later"
|
"TypeError": "unable to contact server, please try again later"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
export async function fetch_timeout(resource: RequestInfo | URL, options = {}) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
const { timeout = 8000 } = options;
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const id = setTimeout(() => controller.abort(), timeout);
|
||||||
|
const response = await fetch(resource, {
|
||||||
|
...options,
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
clearTimeout(id);
|
||||||
|
return response;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte";
|
||||||
|
import {enforce_auth, enforce_session} from "../../lib/auth";
|
||||||
|
|
||||||
|
// this page requires session and mfa auth.
|
||||||
|
onMount(() => {
|
||||||
|
let st_result = enforce_session();
|
||||||
|
if (!st_result[0]) {
|
||||||
|
// Session token is invalid. redirect to login
|
||||||
|
window.location = "/auth/login";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let at_result = enforce_auth();
|
||||||
|
if (!at_result[0]) {
|
||||||
|
// Auth token is invalid. Redirect to mfa page.
|
||||||
|
window.location = "/auth/mfa";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const api_token = at_result[1];
|
||||||
|
|
||||||
|
// user is fully authenticated and permitted to proceed
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -1,6 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {t} from "$lib/i18n";
|
import {t} from "$lib/i18n";
|
||||||
import {API_ROOT} from "$lib/config";
|
import {API_ROOT} from "$lib/config";
|
||||||
|
import {fetch_timeout} from "$lib/util";
|
||||||
|
import {Logger, logSetup} from "../../../lib/logger";
|
||||||
|
|
||||||
let email = "";
|
let email = "";
|
||||||
let isloading = false;
|
let isloading = false;
|
||||||
|
@ -8,31 +10,29 @@
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
let error = "";
|
let error = "";
|
||||||
|
|
||||||
function generateMagicLink() {
|
logSetup();
|
||||||
|
let logger = new Logger("login/+page.svelte");
|
||||||
|
|
||||||
|
async function generateMagicLink() {
|
||||||
if (isloading) {
|
if (isloading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isloading = true;
|
isloading = true;
|
||||||
|
|
||||||
let xhr = new XMLHttpRequest();
|
|
||||||
xhr.timeout = 10000;
|
try {
|
||||||
xhr.open('POST', `${API_ROOT}/v1/auth/magic-link`);
|
let resp = await fetch_timeout(`${API_ROOT}/v1/auth/magic-link`, {
|
||||||
xhr.setRequestHeader("Content-Type", "application/json");
|
'method': 'POST',
|
||||||
xhr.send(JSON.stringify({
|
'headers': {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
'body': JSON.stringify({
|
||||||
email: email
|
email: email
|
||||||
}));
|
})
|
||||||
|
});
|
||||||
xhr.ontimeout = () => {
|
if (!resp.ok) {
|
||||||
hasError = true;
|
hasError = true;
|
||||||
error = t('login.apierror.timeout');
|
const rawerror = JSON.parse(await resp.text()).errors[0].message;
|
||||||
isloading = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onload = () => {
|
|
||||||
if (xhr.status != 200) {
|
|
||||||
// error
|
|
||||||
hasError = true;
|
|
||||||
const rawerror = JSON.parse(xhr.responseText).errors[0].message;
|
|
||||||
|
|
||||||
error = t(`login.apierror.${rawerror}`);
|
error = t(`login.apierror.${rawerror}`);
|
||||||
|
|
||||||
|
@ -41,13 +41,12 @@
|
||||||
isloading = false;
|
isloading = false;
|
||||||
isFinished = true;
|
isFinished = true;
|
||||||
}
|
}
|
||||||
};
|
} catch (e) {
|
||||||
|
|
||||||
xhr.onerror = () => {
|
|
||||||
hasError = true;
|
hasError = true;
|
||||||
error = t('login.apierror.xhrerror');
|
logger.error(`Error requesting magic link from api: ${e}`);
|
||||||
|
error = t(`login.apierror.${e.name}`);
|
||||||
isloading = false;
|
isloading = false;
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,9 @@
|
||||||
|
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
hasError = false;
|
hasError = false;
|
||||||
|
|
||||||
|
// redirect them to the homepage
|
||||||
|
window.location.href = "/admin"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,11 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
crate::routes::v1::verify_totp_authenticator::verify_totp_authenticator_request,
|
crate::routes::v1::verify_totp_authenticator::verify_totp_authenticator_request,
|
||||||
crate::routes::v1::verify_totp_authenticator::options,
|
crate::routes::v1::verify_totp_authenticator::options,
|
||||||
crate::routes::v1::auth::totp::totp_request,
|
crate::routes::v1::auth::totp::totp_request,
|
||||||
crate::routes::v1::auth::totp::options
|
crate::routes::v1::auth::totp::options,
|
||||||
|
crate::routes::v1::auth::check_session::check_session,
|
||||||
|
crate::routes::v1::auth::check_session::check_session_auth,
|
||||||
|
crate::routes::v1::auth::check_session::options,
|
||||||
|
crate::routes::v1::auth::check_session::options_auth
|
||||||
])
|
])
|
||||||
.register("/", catchers![
|
.register("/", catchers![
|
||||||
crate::routes::handler_400,
|
crate::routes::handler_400,
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
use rocket::{post, options};
|
||||||
|
use crate::auth::{PartialUserInfo, TOTPAuthenticatedUserInfo};
|
||||||
|
|
||||||
|
#[options("/v1/auth/check_session")]
|
||||||
|
pub async fn options() -> &'static str {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[post("/v1/auth/check_session")]
|
||||||
|
pub async fn check_session(_user: PartialUserInfo) -> &'static str {
|
||||||
|
"ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[options("/v1/auth/check_auth")]
|
||||||
|
pub async fn options_auth() -> &'static str {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[post("/v1/auth/check_auth")]
|
||||||
|
pub async fn check_session_auth(_user: TOTPAuthenticatedUserInfo) -> &'static str {
|
||||||
|
"ok"
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod verify_magic_link;
|
pub mod verify_magic_link;
|
||||||
pub mod magic_link;
|
pub mod magic_link;
|
||||||
pub mod totp;
|
pub mod totp;
|
||||||
|
pub mod check_session;
|
Loading…
Reference in New Issue