Переглянути джерело

implement jwt strategy with some tests

pull/342/head
Guillaume Vincent 8 роки тому
джерело
коміт
4ca49de39b
11 змінених файлів з 261 додано та 376 видалено
  1. +2
    -5
      src/app/Entries/newEntry.vue
  2. +0
    -155
      src/app/Index.vue
  3. +51
    -50
      src/app/Login.vue
  4. +2
    -2
      src/app/LoginBar.vue
  5. +3
    -3
      src/app/Navbar/Navbar.vue
  6. +5
    -5
      src/router.js
  7. +46
    -17
      src/services/auth.js
  8. +14
    -6
      src/services/entries.js
  9. +11
    -9
      src/services/http.js
  10. +0
    -18
      tests/helper.js
  11. +127
    -106
      tests/services.tests.js

+ 2
- 5
src/app/Entries/newEntry.vue Переглянути файл

@@ -31,13 +31,10 @@
placeholder="{{ $t('passwordgenerator.who_are_you') }}"
value="{{email}}"
v-model="email"
v-on:blur="updateMasterPassword"
autofocus
autocomplete="off"
autocorrect="off"
autocapitalize="none">
<!-- remove autofill for pg-masterpassword -->
<input type="password" id="password" style="display: none">
</div>
</div>
<div class="form-group row">
@@ -143,7 +140,7 @@
<script type="text/ecmascript-6">
import 'bootstrap/dist/js/umd/modal';
import topDomains from '../../landing-page/PasswordGenerator/top-domains.json';
import entries from '../../services/entries';
import http from '../../services/http';
import logging from '../../services/logging';

export default {
@@ -174,7 +171,7 @@
logging.error(this.$t('entries.site_mandatory'));
return;
}
entries.create(entry)
http.entries.create(entry)
.then(() => {
$('#newEntryModal').modal('hide');
logging.success(this.$t('entries.entry_created'));


+ 0
- 155
src/app/Index.vue Переглянути файл

@@ -22,161 +22,6 @@
<new-entry></new-entry>
</div>
</div>
<div class="row m-t-3">
<div class="col-lg-12">
<div class="card-columns">
<div class="card card-block">
<blockquote class="card-blockquote">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a
ante.</p>
<footer>
<small class="text-muted">
Someone famous in <cite title="Source Title">Source Title</cite>
</small>
</footer>
</blockquote>
</div>
<div class="card card-block card-inverse card-primary">
<blockquote class="card-blockquote">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat.</p>
<footer>
<small>
Someone famous in <cite title="Source Title">Source Title</cite>
</small>
</footer>
</blockquote>
</div>
<div class="card card-block">
<h4 class="card-title">Card title</h4>
<p class="card-text">This card has supporting text below as a natural lead-in to additional
content.</p>
<p class="card-text">
<small class="text-muted">Last updated 3 mins ago</small>
</p>
</div>
<div class="card card-block">
<blockquote class="card-blockquote">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a
ante.</p>
<footer>
<small class="text-muted">
Someone famous in <cite title="Source Title">Source Title</cite>
</small>
</footer>
</blockquote>
</div>
<div class="card card-block">
<h4 class="card-title">Card title</h4>
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to
additional content. This card has even longer content than the first to show that equal
height action.</p>
<p class="card-text">
<small class="text-muted">Last updated 3 mins ago</small>
</p>
</div>

<div class="card card-block">
<blockquote class="card-blockquote">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a
ante.</p>
<footer>
<small class="text-muted">
Someone famous in <cite title="Source Title">Source Title</cite>
</small>
</footer>
</blockquote>
</div>
<div class="card card-block card-inverse card-danger">
<blockquote class="card-blockquote">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat.</p>
<footer>
<small>
Someone famous in <cite title="Source Title">Source Title</cite>
</small>
</footer>
</blockquote>
</div>
<div class="card card-block">
<h4 class="card-title">Card title</h4>
<p class="card-text">This card has supporting text below as a natural lead-in to additional
content.</p>
<p class="card-text">
<small class="text-muted">Last updated 3 mins ago</small>
</p>
</div>
<div class="card card-block">
<blockquote class="card-blockquote">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a
ante.</p>
<footer>
<small class="text-muted">
Someone famous in <cite title="Source Title">Source Title</cite>
</small>
</footer>
</blockquote>
</div>
<div class="card card-block">
<h4 class="card-title">Card title</h4>
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to
additional content. This card has even longer content than the first to show that equal
height action.</p>
<p class="card-text">
<small class="text-muted">Last updated 3 mins ago</small>
</p>
</div>

<div class="card card-block">
<blockquote class="card-blockquote">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a
ante.</p>
<footer>
<small class="text-muted">
Someone famous in <cite title="Source Title">Source Title</cite>
</small>
</footer>
</blockquote>
</div>
<div class="card card-block card-inverse card-warning">
<blockquote class="card-blockquote">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat.</p>
<footer>
<small>
Someone famous in <cite title="Source Title">Source Title</cite>
</small>
</footer>
</blockquote>
</div>
<div class="card card-block">
<h4 class="card-title">Card title</h4>
<p class="card-text">This card has supporting text below as a natural lead-in to additional
content.</p>
<p class="card-text">
<small class="text-muted">Last updated 3 mins ago</small>
</p>
</div>
<div class="card card-block">
<blockquote class="card-blockquote">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a
ante.</p>
<footer>
<small class="text-muted">
Someone famous in <cite title="Source Title">Source Title</cite>
</small>
</footer>
</blockquote>
</div>
<div class="card card-block">
<h4 class="card-title">Card title</h4>
<p class="card-text">This is a wider card with supporting text below as a natural lead-in to
additional content. This card has even longer content than the first to show that equal
height action.</p>
<p class="card-text">
<small class="text-muted">Last updated 3 mins ago</small>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>


+ 51
- 50
src/app/Login.vue Переглянути файл

@@ -1,58 +1,59 @@
<template>
<div id="login">
<div class="container">
<div class="row p-t-3">
<div class="col-md-6 col-md-offset-3 col-lg-4 col-lg-offset-4 bg-card-white">
<h2>{{{ $t('login.login') }}}</h2>
<form v-on:submit.prevent>
<fieldset class="form-group">
<label class="sr-only" for="email">{{$t('login.Email')}}</label>
<input type="email" class="form-control" id="email"
placeholder="{{$t('login.Email')}}"
v-model="credentials.email">
</fieldset>
<fieldset class="form-group">
<label class="sr-only" for="password">{{$t('login.Password')}}</label>
<input type="password" class="form-control" id="password"
placeholder="{{$t('login.Password')}}"
v-model="credentials.password">
</fieldset>
<button type="submit" class="btn btn-primary btn-block" @click="signin()">
{{$t('login.Sign_in')}}
</button>
</form>
<div id="login">
<div class="container">
<div class="row p-t-3">
<div class="col-md-6 col-md-offset-3 col-lg-4 col-lg-offset-4 bg-card-white">
<h2>{{{ $t('login.login') }}}</h2>
<form v-on:submit.prevent>
<fieldset class="form-group">
<label class="sr-only" for="email">{{$t('login.Email')}}</label>
<input type="email" class="form-control" id="email"
placeholder="{{$t('login.Email')}}"
v-model="credentials.email">
</fieldset>
<fieldset class="form-group">
<label class="sr-only" for="password">{{$t('login.Password')}}</label>
<input type="password" class="form-control" id="password"
placeholder="{{$t('login.Password')}}"
v-model="credentials.password">
</fieldset>
<button type="submit" class="btn btn-primary btn-block" @click="signin()">
{{$t('login.Sign_in')}}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script type="text/ecmascript-6">
import auth from '../services/auth';
import logging from '../services/logging';
import http from '../services/http';
import logging from '../services/logging';

export default {
data() {
return {
user: auth.user,
credentials: {
email: '',
password: '',
export default {
data() {
return {
user: http.auth.user,
credentials: {
email: '',
password: '',
},
};
},
};
},
methods: {
signin() {
const credentials = {
email: this.credentials.email,
password: this.credentials.password,
};
auth.login(credentials)
.then((data) => {
this.$router.go('/app/');
}).catch((error) => {
logging.error(this.$t('login.credentials_invalids'));
});
},
},
};
methods: {
signin() {
const credentials = {
email: this.credentials.email,
password: this.credentials.password,
};
http.auth.login(credentials)
.then(() => {
this.$router.go('/app/');
})
.catch(() => {
logging.error(this.$t('login.credentials_invalids'));
});
},
},
};
</script>

+ 2
- 2
src/app/LoginBar.vue Переглянути файл

@@ -32,12 +32,12 @@
</div>
</template>
<script type="text/ecmascript-6">
import auth from '../services/auth';
import http from '../services/http';

export default {
data() {
return {
user: auth.user,
user: http.auth.user,
};
},
};


+ 3
- 3
src/app/Navbar/Navbar.vue Переглянути файл

@@ -36,19 +36,19 @@
</div>
</template>
<script type="text/ecmascript-6">
import auth from '../../services/auth';
import http from '../../services/http';
import logging from '../../services/logging';


export default {
data() {
return {
user: auth.user,
user: http.auth.user,
};
},
methods: {
logout() {
auth.logout().then(() => {
http.auth.logout().then(() => {
logging.success(this.$t('login.logout_ok'));
this.$router.go('/');
});


+ 5
- 5
src/router.js Переглянути файл

@@ -7,7 +7,7 @@ import LandingPage from './landing-page/LandingPage';
import LoginPage from './app/Login';
import RegisterPage from './app/Register';
import LessPassConnected from './app/Index';
import Auth from './services/auth';
import Http from './services/http';

const router = new Router({
history: true,
@@ -25,7 +25,7 @@ router.map({
component: RegisterPage,
},
'/app/': {
auth_required: true,
auth_required: false,
component: LessPassConnected,
},
});
@@ -34,14 +34,14 @@ router.redirect({
'*': '/',
});

Auth.checkAuth();
Vue.config.debug = true;

router.beforeEach(transition => {
if (transition.to.path === '/' && Auth.user.authenticated) {
if (transition.to.path === '/' && Http.auth.user.authenticated) {
transition.redirect('/app/');
}

if (transition.to.auth_required && !Auth.user.authenticated) {
if (transition.to.auth_required && !Http.auth.user.authenticated) {
transition.redirect('/login/');
}



+ 46
- 17
src/services/auth.js Переглянути файл

@@ -1,33 +1,62 @@
import request from 'axios';

export default {
user: {
authenticated: false,
},
export default class Auth {
constructor(localStorage = global.localStorage) {
this.localStorage = localStorage;
this.user = {
authenticated: false,
}
}

login(credentials) {
return request.post('/api/sessions/', credentials)
var self = this;
return request.post('/api/token-auth/', credentials)
.then((response) => {
localStorage.setItem('token', response.data.token);
this.user.authenticated = true;
self.localStorage.setItem('token', response.data.token);
self.user.authenticated = true;
return response;
});
},
}

refreshToken(token) {
return request
.post('/api/token-refresh/', {token: token})
.then((response) => {
return response.data.token;
})
.catch((err) => {
throw err;
});
}

checkAuth() {
var self = this;
const token = self.localStorage.getItem('token');
if (token) {
return request
.post('/api/token-verify/', {token: token})
.then((response) => {
self.user.authenticated = true;
return response;
})
.catch(() => {
self.user.authenticated = false;
self.localStorage.removeItem('token');
throw err;
});
}
}

logout() {
var self = this;
return new Promise((resolve, reject) => {
try {
localStorage.removeItem('token');
this.user.authenticated = false;
self.localStorage.removeItem('token');
self.user.authenticated = false;
resolve();
} catch (e) {
reject(e);
}
});
},

checkAuth() {
const jwt = localStorage.getItem('token');
this.user.authenticated = !!jwt;
},
};
}
}

+ 14
- 6
src/services/entries.js Переглянути файл

@@ -1,14 +1,22 @@
import { getHTTP } from './http';
import axios from 'axios';

export default class Entry {
constructor(localStorage = global.localStorage) {
this.localStorage = localStorage;
this.request = axios.create({
headers: {'Authorization': 'JWT ' + this.localStorage.getItem('token')}
});
}

export default {
create(entry) {
return getHTTP(localStorage).post('/api/entries/', entry)
return this.request.post('/api/entries/', entry)
.then((response) => {
return response;
});
},
all(){
return getHTTP(localStorage).get('/api/entries/')
}

all() {
return this.request.get('/api/entries/')
.then((response) => {
return response;
});


+ 11
- 9
src/services/http.js Переглянути файл

@@ -1,12 +1,14 @@
import axios from 'axios';
import AuthService from './auth';
import EntryServices from './entries';

let request = null;
let Auth = new AuthService();
let Entries = new EntryServices();

export function getHTTP(localStorage) {
if (!request) {
request = axios.create({
headers: {'Authorization': 'JWT ' + localStorage.getItem('token')}
});
}
return request;
export {Auth};
export {Entries};

export default {
auth: Auth,
entries: Entries
}


+ 0
- 18
tests/helper.js Переглянути файл

@@ -1,18 +0,0 @@
import assert from 'assert';
import nock from 'nock';
import {LocalStorage} from 'node-localstorage';

// setup
before(()=> {
global.localStorage = new LocalStorage('./tests/localStorage');
global.assert = assert;
global.nock = nock;
});
beforeEach(()=> {
});

// teardown
after(()=> {
});
afterEach(()=> {
});

+ 127
- 106
tests/services.tests.js Переглянути файл

@@ -1,136 +1,157 @@
import auth from '../src/services/auth';
import entries from '../src/services/entries';
import assert from 'assert';
import nock from 'nock';

suite('request', () => {
test('should send requests with localStorage token', (done) => {
var token = 'ZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFt';
localStorage.setItem('token', token);
var stub = nock('http://localhost/', {
reqheaders: {'Authorization': 'JWT '+ token}
});
stub.get('/api/entries/').reply(200, {entries: []}, {'Content-Type': 'application/json'});
entries.all().then((response) => {
done();
});
});
});

suite('entries', () => {
var entry = {
"site": "twitter.com",
"password": {
"counter": 1,
"settings": [
"lowercase",
"uppercase",
"numbers",
"symbols"
],
"length": 12
},
"email": "guillaume@oslab.fr",
};

test('should make a post request to create an entry', (done) => {
nock('http://localhost/')
.post('/api/entries/', entry)
.reply(201, {}, {'Content-Type': 'application/json'});
entries.create(entry)
.then((response) => {
assert.equal(201, response.status);
done();
});
});
import Auth from '../src/services/auth';
import Entries from '../src/services/entries';
import {LocalStorage} from 'node-localstorage';

test('should get all entries', (done) => {
nock('http://localhost/')
.get('/api/entries/')
.reply(200, {entries: []}, {'Content-Type': 'application/json'});
entries.all()
.then((response) => {
assert.equal(200, response.status);
assert.equal(0, response.data.entries.length);
done();
});
});
});
const url = 'http://localhost/';

suite('auth', () => {
var credentials = {
email: 'test@lesspass.com',
password: 'password'
};
var token = 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9';

var auth, localStorage, token, credentials;

beforeEach(() => {
nock('http://localhost/')
.post('/api/sessions/', credentials)
.reply(201, {token: token}, {'Content-Type': 'application/json'});
credentials = {
email: 'test@lesspass.com',
password: 'password'
};
token = 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9';
localStorage = new LocalStorage('./tests/localStorage');
auth = new Auth(localStorage);
});

test('should make a post request to create a session', (done) => {
auth.login(credentials)
.then(() => {
done();
});
nock(url).post('/api/token-auth/', credentials).reply(201, {token: token});
auth.login(credentials).then(() => {
done();
});
});

test('should throw error if bad request', (done) => {
nock.cleanAll();
var badCredentials = {email: 'test@lesspass.com', password: '黑客'};
nock('//lesspass.com/').post('/api/sessions/', badCredentials).reply(400, {});
auth.login(credentials)
.catch((error) => {
done();
});
nock(url).post('/api/token-auth/', credentials).reply(400, {});
auth.login(credentials).catch(() => {
done();
});
});

test('should store token in localStorage', (done) => {
auth.login(credentials)
.then((data) => {
assert.equal(token, localStorage.getItem('token'));
done();
});
nock(url).post('/api/token-auth/', credentials).reply(201, {token: token});
auth.login(credentials).then(() => {
assert.equal(token, localStorage.getItem('token'));
done();
});
});

test('should authenticate the user', (done) => {
auth.user.authenticated = false;
auth.login(credentials)
.then((data) => {
assert(auth.user.authenticated);
done();
});
nock(url).post('/api/token-auth/', credentials).reply(201, {token: token});
auth.login(credentials).then(() => {
assert(auth.user.authenticated);
done();
});
});

test('check auth', (done) => {
auth.login(credentials)
.then((data) => {
assert(auth.user.authenticated);
localStorage.removeItem('token');
auth.checkAuth();
assert(!auth.user.authenticated);
done();
});
test('check auth with a valid token', (done) => {
nock(url).post('/api/token-verify/', {"token": token}).reply(200);
localStorage.setItem('token', token);
auth.checkAuth().then(() => {
assert(auth.user.authenticated);
done();
});
});

test('check auth with an invalid token', (done) => {
nock(url).post('/api/token-verify/', {"token": token}).reply(400);
localStorage.setItem('token', token);
auth.checkAuth().catch(() => {
assert(!auth.user.authenticated);
done();
});
});

test('check refresh token non-expired', (done) => {
var new_token = 'wibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9eyJzdWIiOiIxMjM0NTY3ODkwIi';
nock(url).post('/api/token-refresh/', {"token": token}).reply(200, {token: new_token});
auth.refreshToken(token).then((t) => {
assert.equal(new_token, t);
done();
});
});

test('check refresh token expired', (done) => {
localStorage.setItem('token', token);
nock(url).post('/api/token-refresh/', {"token": token}).reply(400);
auth.refreshToken(token).catch(() => {
done();
});
});

test('logout', (done) => {
auth.login(credentials)
.then((data) => {
assert(auth.user.authenticated);
auth.logout();
assert(!auth.user.authenticated);
assert(localStorage.getItem('token') === null);
auth.user.authenticated = true;
auth.logout().then(()=> {
assert(!auth.user.authenticated);
assert(localStorage.getItem('token') === null);
done();
});
});

after(()=> {
localStorage._deleteLocation()
});
});


suite('entries', () => {
var entries, entry, localStorage, token;

beforeEach(() => {
token = 'ZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFt';
localStorage = new LocalStorage('./tests/localStorageEntries');
localStorage.setItem('token', token);
entries = new Entries(localStorage);
entry = {
"site": "twitter.com",
"password": {
"counter": 1,
"settings": [
"lowercase",
"uppercase",
"numbers",
"symbols"
],
"length": 12
},
"email": "guillaume@oslab.fr",
};
});

test('should send requests with Authorization header', (done) => {
var headers = {reqheaders: {'Authorization': 'JWT ' + token}};
nock(url, headers).get('/api/entries/').reply(200, {entries: []});
entries.all().then(() => {
done();
});
});

test('should make a post request to create an entry', (done) => {
nock(url).post('/api/entries/', entry).reply(201, {});
entries.create(entry)
.then((response) => {
assert.equal(201, response.status);
done();
});
});

test('logout return promise', (done) => {
auth.logout().then(done)
test('should get all entries', (done) => {
nock(url).get('/api/entries/').reply(200, {entries: []});
entries.all().then((response) => {
assert.equal(200, response.status);
assert.equal(0, response.data.entries.length);
done();
});
});

after(() => {
global.localStorage._deleteLocation()
})
});

after(()=> {
localStorage._deleteLocation()
});
});

Завантаження…
Відмінити
Зберегти