Przeglądaj źródła

Fix authentication workflow

* use refresh token to refresh access token if expired
 * change access token duration to 15 minutes
 * change refresh token validity to 1 week

Fixes: 539
Fixes: 236
pull/544/head
Guillaume Vincent 4 lat temu
rodzic
commit
e70c4bdfe1
15 zmienionych plików z 182 dodań i 179 usunięć
  1. +4
    -6
      containers/backend/lesspass/settings.py
  2. +1
    -0
      packages/lesspass-pure/src/api/default.js
  3. +54
    -0
      packages/lesspass-pure/src/api/http.js
  4. +11
    -31
      packages/lesspass-pure/src/api/password.js
  5. +21
    -22
      packages/lesspass-pure/src/api/password.test.js
  6. +14
    -18
      packages/lesspass-pure/src/api/user.js
  7. +8
    -21
      packages/lesspass-pure/src/api/user.test.js
  8. +6
    -6
      packages/lesspass-pure/src/store/actions.js
  9. +3
    -6
      packages/lesspass-pure/src/store/index.js
  10. +7
    -4
      packages/lesspass-pure/src/store/mutations.js
  11. +0
    -23
      packages/lesspass-pure/src/store/mutations.test.js
  12. +30
    -16
      packages/lesspass-pure/src/views/Login.vue
  13. +5
    -12
      packages/lesspass-pure/src/views/PasswordReset.vue
  14. +1
    -7
      packages/lesspass-pure/src/views/PasswordResetConfirm.vue
  15. +17
    -7
      packages/lesspass-pure/src/views/Passwords.vue

+ 4
- 6
containers/backend/lesspass/settings.py Wyświetl plik

@@ -127,13 +127,11 @@ LOGGING = {

AUTH_USER_MODEL = "api.LessPassUser"

JWT_AUTH = {
"JWT_EXPIRATION_DELTA": datetime.timedelta(days=7),
"JWT_ALLOW_REFRESH": True,
}

SIMPLE_JWT = {
'AUTH_HEADER_TYPES': ('Bearer', 'JWT',),
'AUTH_HEADER_TYPES': ('Bearer', 'JWT',),
'ACCESS_TOKEN_LIFETIME': datetime.timedelta(minutes=15),
'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
}

REST_FRAMEWORK = {


+ 1
- 0
packages/lesspass-pure/src/api/default.js Wyświetl plik

@@ -0,0 +1 @@
export const defaultbaseURL = "https://lesspass.com";

+ 54
- 0
packages/lesspass-pure/src/api/http.js Wyświetl plik

@@ -0,0 +1,54 @@
import axios from "axios";
import { defaultbaseURL } from "./default";

axios.interceptors.request.use(config => {
const baseURL = localStorage.getItem("baseURL") || defaultbaseURL;
config.baseURL = baseURL;
const access_token = localStorage.getItem("access_token");
if (access_token) {
config.headers["Authorization"] = `Bearer ${access_token}`;
}
return config;
});

axios.interceptors.response.use(
response => {
return response;
},
error => {
const refresh = localStorage.getItem("refresh_token");
if (
(error.response && error.response.status !== 401) ||
(error.config && error.config.url.includes("/api/auth/jwt/")) ||
refresh === null
) {
return new Promise((resolve, reject) => {
reject(error);
});
}
const baseURL = localStorage.getItem("baseURL");
return axios
.post("/api/auth/jwt/refresh/", { refresh }, { baseURL })
.then(response => {
const access_token = response.data.access;
localStorage.setItem("access_token", access_token);
const config = error.config;
config.headers["Authorization"] = `Bearer ${access_token}`;
return new Promise((resolve, reject) => {
axios
.request(config)
.then(response => {
resolve(response);
})
.catch(error => {
reject(error);
});
});
})
.catch(error => {
Promise.reject(error);
});
}
);

export default axios;

+ 11
- 31
packages/lesspass-pure/src/api/password.js Wyświetl plik

@@ -1,39 +1,19 @@
import axios from "axios";
import http from "./http";

export default {
addAuthorizationHeader(config) {
return {
...config,
headers: { Authorization: `Bearer ${config.access_token}` }
};
all() {
return http.get("/api/passwords/");
},
all(config) {
return axios.get("/api/passwords/", this.addAuthorizationHeader(config));
create(resource) {
return http.post("/api/passwords/", resource);
},
create(resource, config) {
return axios.post(
"/api/passwords/",
resource,
this.addAuthorizationHeader(config)
);
read(resource) {
return http.get(`/api/passwords/${resource.id}/`);
},
read(resource, config) {
return axios.get(
"/api/passwords/" + resource.id + "/",
this.addAuthorizationHeader(config)
);
update(resource) {
return http.put(`/api/passwords/${resource.id}/`, resource);
},
update(resource, config) {
return axios.put(
"/api/passwords/" + resource.id + "/",
resource,
this.addAuthorizationHeader(config)
);
},
delete(resource, config) {
return axios.delete(
"/api/passwords/" + resource.id + "/",
this.addAuthorizationHeader(config)
);
delete(resource) {
return http.delete(`/api/passwords/${resource.id}/`);
}
};

+ 21
- 22
packages/lesspass-pure/src/api/password.test.js Wyświetl plik

@@ -4,14 +4,10 @@ import Passwords from "./password";

const mock = new MockAdapter(axios);

const config = { baseURL: "https://lesspass.com", access_token: "abc" };

test("Passwords.create", () => {
const password = { login: "text@example.org" };
mock
.onPost("/api/passwords/", password)
.reply(201, { ...password, id: "1" });
return Passwords.create(password, config).then(response => {
mock.onPost("/api/passwords/", password).reply(201, { ...password, id: "1" });
return Passwords.create(password).then(response => {
const passwordCreated = response.data;
expect(passwordCreated.id).toBe("1");
expect(passwordCreated.login).toBe(password.login);
@@ -20,7 +16,7 @@ test("Passwords.create", () => {

test("Passwords.all", () => {
mock.onGet("https://lesspass.com/api/passwords/").reply(200, {});
return Passwords.all(config).then(response => {
return Passwords.all().then(response => {
expect(response.status).toBe(200);
});
});
@@ -31,12 +27,11 @@ test("Passwords.get", () => {
"https://lesspass.com/api/passwords/c8e4f983-8ffe-b705-4064-d3b7aa4a4782/"
)
.reply(200, {});
return Passwords.read(
{ id: "c8e4f983-8ffe-b705-4064-d3b7aa4a4782" },
config
).then(response => {
expect(response.status).toBe(200);
});
return Passwords.read({ id: "c8e4f983-8ffe-b705-4064-d3b7aa4a4782" }).then(
response => {
expect(response.status).toBe(200);
}
);
});

test("Passwords.update", () => {
@@ -45,21 +40,25 @@ test("Passwords.update", () => {
login: "test@example.org"
};
mock
.onPut("https://lesspass.com/api/passwords/c8e4f983-4064-8ffe-b705-d3b7aa4a4782/", password)
.onPut(
"https://lesspass.com/api/passwords/c8e4f983-4064-8ffe-b705-d3b7aa4a4782/",
password
)
.reply(200, {});
return Passwords.update(password, config).then(response => {
return Passwords.update(password).then(response => {
expect(response.status).toBe(200);
});
});

test("Passwords.delete", () => {
mock
.onDelete("https://lesspass.com/api/passwords/c8e4f983-8ffe-4064-b705-d3b7aa4a4782/")
.onDelete(
"https://lesspass.com/api/passwords/c8e4f983-8ffe-4064-b705-d3b7aa4a4782/"
)
.reply(204);
return Passwords.delete(
{ id: "c8e4f983-8ffe-4064-b705-d3b7aa4a4782" },
config
).then(response => {
expect(response.status).toBe(204);
});
return Passwords.delete({ id: "c8e4f983-8ffe-4064-b705-d3b7aa4a4782" }).then(
response => {
expect(response.status).toBe(204);
}
);
});

+ 14
- 18
packages/lesspass-pure/src/api/user.js Wyświetl plik

@@ -1,25 +1,21 @@
import axios from "axios";
import http from "./http";

export default {
login({ email, password }, config) {
return axios.post("/api/auth/jwt/create/", { email, password }, config);
login({ email, password }) {
return http.post("/api/auth/jwt/create/", { email, password });
},
register({ email, password }, config) {
return axios.post("/api/auth/users/", { email, password }, config);
register({ email, password }) {
return http.post("/api/auth/users/", { email, password });
},
resetPassword({ email }, config) {
return axios.post("/api/auth/users/reset_password/", { email }, config);
resetPassword({ email }) {
return http.post("/api/auth/users/reset_password/", { email });
},
confirmResetPassword({ uid, token, password }, config) {
return axios.post(
"/api/auth/users/reset_password_confirm/",
{
uid,
token,
new_password: password,
re_new_password: password
},
config
);
confirmResetPassword({ uid, token, password }) {
return http.post("/api/auth/users/reset_password_confirm/", {
uid,
token,
new_password: password,
re_new_password: password
});
}
};

+ 8
- 21
packages/lesspass-pure/src/api/user.test.js Wyświetl plik

@@ -6,14 +6,10 @@ const mock = new MockAdapter(axios);

test("login", () => {
const access = "12345";
const refresh = "67890"
const refresh = "67890";
const user = { email: "test@example.org", password: "password" };
mock
.onPost("/api/auth/jwt/create/", user)
.reply(201, { access, refresh });
return User.login(user, {
baseURL: "https://lesspass.com"
}).then(response => {
mock.onPost("/api/auth/jwt/create/", user).reply(201, { access, refresh });
return User.login(user).then(response => {
expect(response.data.access).toBe(access);
expect(response.data.refresh).toBe(refresh);
});
@@ -24,22 +20,15 @@ test("register", () => {
mock
.onPost("/api/auth/users/", user)
.reply(201, { email: user.email, pk: 1 });
return User.register(user, {
baseURL: "https://lesspass.com"
}).then(response => {
return User.register(user).then(response => {
expect(response.data.email).toBe(user.email);
});
});

test("resetPassword", () => {
var email = "test@lesspass.com";
mock
.onPost("/api/auth/users/reset_password/", { email })
.reply(204);
return User.resetPassword(
{ email },
{ baseURL: "https://lesspass.com" }
).then(response => {
mock.onPost("/api/auth/users/reset_password/", { email }).reply(204);
return User.resetPassword({ email }).then(response => {
expect(response.status).toBe(204);
});
});
@@ -49,7 +38,7 @@ test("confirmResetPassword", () => {
uid: "MQ",
token: "5g1-2bd69bd6f6dcd73f8124",
new_password: "password1",
re_new_password: "password1",
re_new_password: "password1"
};
mock
.onPost("/api/auth/users/reset_password_confirm/", newPassword)
@@ -57,9 +46,7 @@ test("confirmResetPassword", () => {
return User.confirmResetPassword({
uid: "MQ",
token: "5g1-2bd69bd6f6dcd73f8124",
password: "password1",
}, {
baseURL: "https://lesspass.com"
password: "password1"
}).then(response => {
expect(response.status).toBe(204);
});


+ 6
- 6
packages/lesspass-pure/src/store/actions.js Wyświetl plik

@@ -41,15 +41,15 @@ export const logout = ({ commit }) => {
commit(types.RESET_PASSWORD);
};

export const getPasswords = ({ commit, state }) => {
if (state.authenticated) {
return Password.all(state).then(response => {
export const getPasswords = ({ commit }) => {
return Password.all()
.then(response => {
commit(types.LOGIN);
const passwords = response.data.results;
commit(types.SET_PASSWORDS, { passwords });
return passwords;
});
}
return Promise.resolve([]);
})
.catch(() => logout({ commit }));
};

export const saveOrUpdatePassword = ({ commit, state }) => {


+ 3
- 6
packages/lesspass-pure/src/store/index.js Wyświetl plik

@@ -9,15 +9,12 @@ import defaultPassword from "./defaultPassword";
Vue.use(Vuex);

const state = {
authenticated: false,
authenticated: localStorage.getItem("access_token") !== null,
password: Object.assign({}, defaultPassword),
passwords: [],
message: "",
defaultPassword: defaultPassword,
showOptions: false,
access_token: null,
refresh_token: null,
baseURL: "https://lesspass.com"
showOptions: false
};

export default new Vuex.Store({
@@ -28,7 +25,7 @@ export default new Vuex.Store({
plugins: [
createPersistedState({
key: "lesspass",
paths: ["access_token", "refresh_token", "baseURL", "authenticated", "defaultPassword"]
paths: ["defaultPassword"]
})
]
});

+ 7
- 4
packages/lesspass-pure/src/store/mutations.js Wyświetl plik

@@ -5,13 +5,16 @@ export default {
state.authenticated = true;
},
[types.SET_TOKENS](state, { refresh_token, access_token }) {
state.refresh_token = refresh_token;
state.access_token = access_token
localStorage.setItem("access_token", access_token);
localStorage.setItem("refresh_token", refresh_token);
},
[types.LOGOUT](state) {
state.authenticated = false;
state.token = null;
state.passwords = [];
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
localStorage.removeItem("baseURL");
localStorage.removeItem("lesspass");
},
[types.RESET_PASSWORD](state) {
state.password = { ...state.defaultPassword };
@@ -34,7 +37,7 @@ export default {
}
},
[types.SET_BASE_URL](state, { baseURL }) {
state.baseURL = baseURL;
localStorage.setItem("baseURL", baseURL);
},
[types.SET_SITE](state, { site }) {
state.password.site = site;


+ 0
- 23
packages/lesspass-pure/src/store/mutations.test.js Wyświetl plik

@@ -1,4 +1,3 @@
import timekeeper from "timekeeper";
import mutations from "./mutations";
import * as types from "./mutation-types";
import defaultPassword from "./defaultPassword";
@@ -25,13 +24,11 @@ test("RESET_PASSWORD set default password", () => {
test("LOGOUT clean user personal info", () => {
const LOGOUT = mutations[types.LOGOUT];
const state = {
token: "123456",
password: { counter: 2 },
passwords: [{ id: "1", site: "test@example.org" }],
defaultPassword: { counter: 1 }
};
LOGOUT(state);
expect(state.token === null).toBe(true);
expect(state.passwords.length).toBe(0);
expect(state.password.counter).toBe(2);
});
@@ -43,16 +40,6 @@ test("LOGIN", () => {
expect(state.authenticated).toBe(true);
});

test("SET_TOKENS", () => {
const access_token = "123456";
const refresh_token = "7890"
const SET_TOKENS = mutations[types.SET_TOKENS];
const state = { token: null };
SET_TOKENS(state, { access_token, refresh_token });
expect(state.access_token).toBe(access_token);
expect(state.refresh_token).toBe(refresh_token);
});

test("SET_PASSWORD", () => {
const SET_PASSWORD = mutations[types.SET_PASSWORD];
const state = { password: null };
@@ -121,16 +108,6 @@ test("DELETE_PASSWORD replace state.password with state.defaultPassword", () =>
expect(state.password.length).toBe(16);
});

test("SET_BASE_URL", () => {
const SET_BASE_URL = mutations[types.SET_BASE_URL];
const state = {
baseURL: "https://lesspass.com"
};
const baseURL = "https://example.org";
SET_BASE_URL(state, { baseURL: baseURL });
expect(state.baseURL).toBe(baseURL);
});

test("LOAD_PASSWORD_PROFILE", () => {
const state = {
password: {


+ 30
- 16
packages/lesspass-pure/src/views/Login.vue Wyświetl plik

@@ -50,7 +50,9 @@
</div>
<div class="form-group row no-gutters mb-0">
<div class="col">
<button id="signInButton" class="btn btn-primary btn-block">{{$t('Sign In')}}</button>
<button id="signInButton" class="btn btn-primary btn-block">
{{ $t("Sign In") }}
</button>
</div>
<div class="col">
<button
@@ -58,7 +60,9 @@
class="btn btn-secondary btn-block"
type="button"
v-on:click="register"
>{{$t('Register')}}</button>
>
{{ $t("Register") }}
</button>
</div>
</div>
<div class="form-group mb-0">
@@ -66,15 +70,16 @@
id="login__forgot-password-btn"
type="button"
class="btn btn-link btn-sm p-0"
v-on:click="$router.push({name: 'passwordReset'})"
v-on:click="$router.push({ name: 'passwordReset' })"
>
<small>{{$t('ForgotPassword', 'Forgot your password?')}}</small>
<small>{{ $t("ForgotPassword", "Forgot your password?") }}</small>
</button>
</div>
</form>
</template>
<script type="text/ecmascript-6">
import User from "../api/user";
import { defaultbaseURL } from "../api/default";
import MasterPassword from "../components/MasterPassword.vue";
import message from "../services/message";

@@ -83,7 +88,7 @@ export default {
return {
email: "",
password: "",
baseURL: "https://lesspass.com"
baseURL: localStorage.getItem("baseURL") || defaultbaseURL
};
},
components: {
@@ -105,21 +110,19 @@ export default {
signIn() {
if (this.formIsValid()) {
const baseURL = this.baseURL;
User.login({ email: this.email, password: this.password }, { baseURL })
this.$store.dispatch("setBaseURL", { baseURL });
User.login({ email: this.email, password: this.password })
.then(response => {
this.$store.dispatch("login", response.data);
this.$store.dispatch("setBaseURL", { baseURL });
this.$store.dispatch("cleanMessage");
this.$router.push({ name: "home" });
})
.catch(err => {
if (
err.response === undefined &&
baseURL !== "https://lesspass.com"
) {
if (err.response === undefined && baseURL !== defaultbaseURL) {
message.error(
this.$t("DBNotRunning", "Your LessPass Database is not running")
);
} else if (err.response && err.response.status === 400) {
} else if (err.response && err.response.status === 401) {
message.error(
this.$t(
"LoginIncorrectError",
@@ -135,9 +138,9 @@ export default {
register() {
if (this.formIsValid()) {
const baseURL = this.baseURL;
this.$store.dispatch("setBaseURL", { baseURL });
User.register(
{ email: this.email, password: this.password },
{ baseURL }
)
.then(() => {
message.success(
@@ -150,8 +153,10 @@ export default {
this.signIn();
})
.catch(err => {
if (
err.response &&
if ( err.response === undefined && baseURL !== defaultbaseURL) {
message.error(this.$t("DBNotRunning", "Your LessPass Database is not running"));
}else if (
err.response && err.response.data &&
typeof err.response.data.email !== "undefined"
) {
if (err.response.data.email[0].indexOf("already exists") !== -1) {
@@ -167,6 +172,16 @@ export default {
this.$t("EmailInvalid", "Please enter a valid email")
);
}
} else if (
err.response && err.response.data &&
typeof err.response.data.password !== "undefined"
) {
if (err.response.data.password[0].indexOf("too short") !== -1) {
message.error(this.$t("PasswordTooShort", "This password is too short. It must contain at least 8 characters."));
}
if (err.response.data.password[0].indexOf("too common") !== -1) {
message.error(this.$t("PasswordTooCommon", "This password is too common."));
}
} else {
message.displayGenericError();
}
@@ -176,4 +191,3 @@ export default {
}
};
</script>


+ 5
- 12
packages/lesspass-pure/src/views/PasswordReset.vue Wyświetl plik

@@ -34,27 +34,20 @@
email: '',
};
},
computed: {
...mapState(['baseURL'])
},
methods: {
resetPassword() {
const baseURL = this.baseURL;
if (!baseURL) {
message.displayGenericError();
return;
}

if (!this.email) {
message.error(this.$t('EmailRequiredError', 'We need an email to find your account.'));
return;
}

User.resetPassword({email: this.email}, {baseURL})
User.resetPassword({email: this.email})
.then(() => {
const successMessage = this.$t('resetPasswordSuccess',
const successMessage = this.$t(
'resetPasswordSuccess',
'If the email address {email} is associated with a LessPass account, you will shortly receive an email from LessPass with instructions on how to reset your password.',
{email: this.email});
{email: this.email}
);
message.success(successMessage);
})
.catch(() => {


+ 1
- 7
packages/lesspass-pure/src/views/PasswordResetConfirm.vue Wyświetl plik

@@ -51,9 +51,6 @@ export default {
password: ''
};
},
computed: mapState([
'baseURL'
]),
methods: {
resetPasswordConfirm(){
if (!this.password) {
@@ -66,15 +63,12 @@ export default {
uid: this.$route.params.uid,
token: this.$route.params.token,
password: this.password
},
{
baseURL: this.baseURL
}
)
.then(() => {
message.success(this.$t('PasswordResetSuccessful', 'Your password was reset successfully.'));
User
.login({ email: this.email, password: this.password }, { baseURL: this.baseURL })
.login({ email: this.email, password: this.password })
.then(response => {
this.$store.dispatch("login", response.data);
this.$router.push({ name: "home" });


+ 17
- 7
packages/lesspass-pure/src/views/Passwords.vue Wyświetl plik

@@ -33,24 +33,31 @@
<div v-if="passwords.length === 0">
<div class="row">
<div class="col">
{{$t('NoPassword', "You don't have any password profile saved in your database.")}}
<router-link
:to="{ name: 'home'}"
>{{$t('CreatePassword', 'Would you like to create one?')}}</router-link>
{{
$t(
"NoPassword",
"You don't have any password profile saved in your database."
)
}}
<router-link :to="{ name: 'home' }">{{
$t("CreatePassword", "Would you like to create one?")
}}</router-link>
</div>
</div>
</div>
<div v-if="filteredPasswords.length === 0 && passwords.length > 0">
<div class="row">
<div class="col">
{{$t('NoMatchFor', 'Oops! There are no matches for')}} "{{searchQuery}}".
{{$t('UpdateYourSearch', 'Please try broadening your search.')}}
{{ $t("NoMatchFor", "Oops! There are no matches for") }} "{{
searchQuery
}}".
{{ $t("UpdateYourSearch", "Please try broadening your search.") }}
</div>
</div>
</div>
<password-profile
v-bind:password="password"
v-on:deleted="pagination.currentPage=1"
v-on:deleted="pagination.currentPage = 1"
v-for="password in filteredPasswords"
:key="password.id"
></password-profile>
@@ -113,6 +120,9 @@ export default {
);
}
},
beforeMount() {
this.$store.dispatch("getPasswords");
},
methods: {
setCurrentPage(page) {
this.pagination.currentPage = page;


Ładowanie…
Anuluj
Zapisz