* 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: 236pull/544/head
@@ -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 = { | |||
@@ -0,0 +1 @@ | |||
export const defaultbaseURL = "https://lesspass.com"; |
@@ -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; |
@@ -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}/`); | |||
} | |||
}; |
@@ -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); | |||
} | |||
); | |||
}); |
@@ -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 | |||
}); | |||
} | |||
}; |
@@ -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); | |||
}); | |||
@@ -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 }) => { | |||
@@ -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"] | |||
}) | |||
] | |||
}); |
@@ -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; | |||
@@ -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: { | |||
@@ -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> | |||
@@ -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(() => { | |||
@@ -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" }); | |||
@@ -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; | |||