Skip to content

Commit

Permalink
Leta autologin bug fixes (#162)
Browse files Browse the repository at this point in the history
* Format account before attempting login
* Refactor invalid account error handling
* Remove uneeded expiry cookie
* Renew cookie every 23h in a browser session
  • Loading branch information
ruihildt committed Jun 15, 2023
1 parent 16e1990 commit d513a0b
Show file tree
Hide file tree
Showing 9 changed files with 60 additions and 48 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,7 @@ Mullvad Browser Extension requires the following permissions:

**Optional**

- `webRequest` to automatically login to Mullvad Leta (optional)
- `webRequestBlocking` to automatically login to Mullvad Leta (optional)
- `alarms` to automatically renew Mullvad Leta cookie (optional)

_Permissions are automatically accepted when testing the extension._

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "mullvad-browser-extension",
"displayName": "Mullvad Browser Extension",
"version": "0.8.0",
"version": "0.8.1",
"description": "Improve your Mullvad VPN experience, in your browser.",
"private": true,
"engines": {
Expand Down
24 changes: 23 additions & 1 deletion src/background/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { onMessage } from 'webext-bridge/background';

import { addExtListeners } from '@/helpers/extensions';
import { DataAccount, initLetaLogin, letaLogin, letaLogout } from '@/helpers/leta';
import {
DataAccount,
getMullvadAccount,
initLetaLogin,
letaLogin,
letaLogout,
} from '@/helpers/leta';

// only on dev mode
if (import.meta.hot) {
Expand All @@ -25,6 +31,22 @@ onMessage('leta-logout', () => {
letaLogout();
});

// Alarm to refresh Mullvad Leta auth cookie
// The cookie expires after 24h, so renewal is set for every 23h
browser.alarms.create('leta-cookie-refresh', {
delayInMinutes: 1380,
periodInMinutes: 1380,
});

const refreshCookie = async (alarm: browser.alarms.Alarm) => {
if (alarm.name === 'leta-cookie-refresh') {
const account = await getMullvadAccount();
await letaLogin(account);
}
};

browser.alarms.onAlarm.addListener(refreshCookie);

// `browser.cookies` operations are only available in the background context
export const setCookie = async (cookie: browser.cookies._SetDetails) => {
try {
Expand Down
32 changes: 18 additions & 14 deletions src/components/LetaSettings.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed, Ref, ref } from 'vue';
import { NCard } from 'naive-ui';
import Button from '@/components/Buttons/Button.vue';
Expand All @@ -8,32 +8,34 @@ import FeEye from '@/components/Icons/FeEye.vue';
import FeEyeOff from '@/components/Icons/FeEyeOff.vue';
import IconLabel from '@/components/IconLabel.vue';
import { checkFormat, formatAccount, FormatType } from '@/helpers/account';
import { checkAccountFormat, formatAccount, FormatType } from '@/helpers/account';
import { closePopup } from '@/helpers/closePopup';
import useLeta from '@/composables/useLeta';
import useLeta, { ErrorMessage } from '@/composables/useLeta';
import useConnection from '@/composables/useConnection';
const { account, login, logout, loginError } = useLeta();
const { connection } = useConnection();
const invalidNumber = ref(false);
const invalidAccount: Ref<ErrorMessage> = ref({ error: false, message: '' });
const isAccountVisible = ref(false);
const password = ref('');
const connected = computed(() => connection.value.isMullvad);
const handleLogin = () => {
loginError.value = { error: false, message: '' };
invalidNumber.value = false;
invalidAccount.value = { error: false, message: '' };
const isValidNumber = checkFormat(password.value);
const isValidAccount = checkAccountFormat(password.value);
if (isValidNumber) {
invalidNumber.value = false;
login(password.value);
if (isValidAccount) {
invalidAccount.value = { error: false, message: '' };
const account = formatAccount(password.value, FormatType.clean);
login(account);
} else {
invalidNumber.value = true;
invalidAccount.value = { error: true, message: 'Invalid account number' };
}
};
Expand All @@ -53,18 +55,20 @@ const accountString = computed(() => {
<template>
<n-card id="leta-settings" :bordered="false" class="mb-4">
<div class="flex justify-between">
<h2 class="text-lg">Mullvad Leta Auto Login</h2>
<h2 class="text-lg">Mullvad Leta autologin</h2>
</div>

<p>Enter your Mullvad VPN account number to automatically login to Mullvad Leta.</p>
<p class="pt-4">
Enter your Mullvad VPN account to automatically login when the browser starts.
</p>

<div v-if="connected" class="pt-4">
<div v-if="account === ''">
<div class="flex">
<input v-model="password" type="password" placeholder="Enter your account number" />
</div>
<div v-if="invalidNumber" class="py-4 flex items-center">
<IconLabel text="The account entered is not a 16 digits number." type="warning" />
<div v-if="invalidAccount.error" class="py-4 flex items-center">
<IconLabel :text="invalidAccount.message" type="warning" />
</div>
<div v-if="loginError.error" class="py-4 flex items-center">
<IconLabel :text="loginError.message" type="warning" />
Expand Down
7 changes: 3 additions & 4 deletions src/composables/useLeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@ import { ref } from 'vue';
import { onMessage, sendMessage } from 'webext-bridge/popup';

import { DataAccount, MullvadAccount } from '@/helpers/leta';
import { FormatType, formatAccount } from '@/helpers/account';
import useStore from '@/composables/useStore';

const { mullvadAccount } = useStore();
const loginError = ref({ error: false, message: '' });

type DataError = {
export type ErrorMessage = {
error: boolean;
message: string;
};

onMessage<DataError>('login-error', async ({ data }) => {
onMessage<ErrorMessage>('login-error', async ({ data }) => {
const { message } = data;
loginError.value = { error: true, message };
});
Expand All @@ -22,7 +21,7 @@ onMessage<DataAccount>('login-success', async ({ data }) => {
const { account } = data;
loginError.value = { error: false, message: '' };
// Save account to extension storage
mullvadAccount.value = formatAccount(account, FormatType.clean);
mullvadAccount.value = account;
});

const login = async (account: MullvadAccount) => {
Expand Down
10 changes: 7 additions & 3 deletions src/helpers/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@ export enum FormatType {
'hidden',
}

export const checkFormat = (value: string): boolean => {
const containsSixteenDigits = /^(\d[\s-]*){16}$/;
return containsSixteenDigits.test(value);
export const checkAccountFormat = (value: string): boolean => {
// The string contains between 9 and 16 digits.
// It can also contain spaces or dashes but no other characters.
const regex = /^(?=(?:\D*\d){9,16}\D*$)[\d -]+$/;
return regex.test(value);
};

export const formatAccount = (accountNumber: string, type: FormatType) => {
switch (type) {
case FormatType.clean:
// Remove space and dashes
return accountNumber.replace(/-|\s/g, '');
case FormatType.hidden:
return '•••• •••• •••• ••••';
case FormatType.prettify:
// Add space every 4 chars
return accountNumber.match(/.{1,4}/g)!.join(' ');
default:
return '';
Expand Down
19 changes: 2 additions & 17 deletions src/helpers/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type CookieData = {
isFPI: boolean;
};

export const setLetaCookies = (data: CookieData) => {
export const setAuthCookie = (data: CookieData) => {
const { expiry, accessToken, isFPI } = data;

// Convert to required format: UNIX epoch in seconds
Expand All @@ -30,30 +30,15 @@ export const setLetaCookies = (data: CookieData) => {
value: accessToken,
} as browser.cookies._SetDetails;

const expiryCookie = {
expirationDate,
firstPartyDomain,
name: `letaCookieExpiry`,
url: 'https://leta.mullvad.net',
value: expiry,
};

setCookie(accessCookie);
setCookie(expiryCookie);
};

export const removeLetaCookies = (isFPI: IsFPI) => {
export const removeAuthCookie = (isFPI: IsFPI) => {
const firstPartyDomain = isFPI ? 'mullvad.net' : '';

removeCookie({
firstPartyDomain,
name: 'accessToken',
url: 'https://leta.mullvad.net',
});

removeCookie({
firstPartyDomain,
name: 'letaCookieExpiry',
url: 'https://leta.mullvad.net',
});
};
8 changes: 4 additions & 4 deletions src/helpers/leta.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { sendMessage } from 'webext-bridge/background';

import { removeLetaCookies, setLetaCookies } from './cookies';
import { removeAuthCookie, setAuthCookie } from './cookies';
import { checkFPI } from './fpi';

export type MullvadAccount = string;
Expand Down Expand Up @@ -32,7 +32,7 @@ export const letaLogin = async (account: string) => {
if (!response.ok) {
const error =
data.code === 'INVALID_ACCOUNT'
? `Invalid account`
? `Invalid account number`
: `Server error, please try again later.`;

sendMessage('login-error', { message: error }, 'popup');
Expand All @@ -44,7 +44,7 @@ export const letaLogin = async (account: string) => {
const { expiry, access_token: accessToken } = data;
const isFPI = await checkFPI();

setLetaCookies({
setAuthCookie({
expiry,
accessToken,
isFPI,
Expand All @@ -56,7 +56,7 @@ export const letaLogin = async (account: string) => {

export const letaLogout = async () => {
const isFPI = await checkFPI();
removeLetaCookies(isFPI);
removeAuthCookie(isFPI);
};

export const initLetaLogin = async () => {
Expand Down
3 changes: 1 addition & 2 deletions src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,14 @@ export async function getManifest() {
'256': './assets/icon.svg',
},
permissions: [
'alarms',
'cookies',
'management',
'privacy',
'proxy',
'search',
'storage',
'*://*.mullvad.net/*',
'webRequest',
'webRequestBlocking',
],
browser_specific_settings: {
gecko: {
Expand Down

0 comments on commit d513a0b

Please sign in to comment.