Last active
April 12, 2018 11:01
-
-
Save tiarebalbi/62bb6dd94454cadfc09367275c9f5707 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @flow | |
import type { AuthConfig, AuthTokenType, Storage } from '../types'; | |
import axios, { Axios, AxiosXHRConfigBase } from 'axios'; | |
import { AuthenticationError } from './authenticationError'; | |
import { decode } from '../helpers/htmlEntitiesEncoding'; | |
import { LocalStorage } from '../helpers/storage'; | |
import Authentication from './authentication'; | |
import ErrorMapping from './errorMapping'; | |
export default class AuthenticationService { | |
client: Axios; | |
config: AuthConfig; | |
storage: Storage; | |
_storageEntries = { | |
authenticationToken: 'authenticationToken', | |
}; | |
_errors = new ErrorMapping(); | |
constructor(authConfig: AuthConfig) { | |
const config: AxiosXHRConfigBase<*> = { | |
timeout: 1500, | |
baseURL: 'http://localhost:8080', | |
...authConfig, | |
}; | |
this.config = authConfig; | |
this.client = axios.create(config); | |
this.storage = new LocalStorage({ | |
version: { scope: 1 }, | |
}); | |
if (!this.config.basicKey) { | |
const errorCode = 601; | |
throw new AuthenticationError(this._errors.get(errorCode), errorCode); | |
} | |
} | |
/** | |
* Method to authenticate a user to the system using OAuth 2.0 | |
* | |
* @param username from the user | |
* @param password from the user | |
* @returns {Promise<*>} | |
*/ | |
async login(username: string, password: string): Promise<AuthTokenType> { | |
try { | |
const endpoint = '/oauth/token?grant_type=password'; | |
const data = `username=${username}&password=${password}`; | |
const response = await this.client.post(endpoint, data, { | |
headers: { | |
Authorization: `Basic ${this.config.basicKey}`, | |
'Content-Type': 'application/x-www-form-urlencoded', | |
}, | |
}); | |
const authenticationData = response.data; | |
this._saveAuthenticationDetails(authenticationData); | |
return authenticationData; | |
} catch (error) { | |
let message = error.message; | |
let errorCode = error.response.status; | |
if (error.response.data) { | |
message = decode(error.response.data[ 'error_description' ]); | |
} | |
throw new AuthenticationError(message, errorCode); | |
} | |
} | |
/** | |
* Method to logout and expire tokens from the user | |
* @returns {Promise<void>} | |
*/ | |
async logout(): Promise<void> { | |
try { | |
const key = this._storageEntries.authenticationToken; | |
this.storage.remove(key); | |
const endpoint = '/api/v1/sessoes'; | |
await this.client.delete(endpoint, { | |
headers: { | |
Authorization: this.getAuthenticationDetails().getBearerToken(), | |
'Content-Type': 'application/x-www-form-urlencoded', | |
}, | |
}); | |
} catch (e) { | |
} | |
} | |
/** | |
* Method to refresh token if authentication details are present | |
* @returns {Promise<void>} | |
*/ | |
async refreshToken(): Promise<void> { | |
const key = this._storageEntries.authenticationToken; | |
try { | |
const t = this.getAuthenticationDetails().getRefreshToken(); | |
const endpoint = `/oauth/token?grant_type=refresh_token&refresh_token=${t}`; | |
const response = await this.client.post(endpoint, { | |
headers: { | |
Authorization: `Basic ${this.config.basicKey}`, | |
'Content-Type': 'application/x-www-form-urlencoded', | |
}, | |
}); | |
this.storage.remove(key); | |
this._saveAuthenticationDetails(response.data); | |
} catch (error) { | |
this.storage.remove(key); | |
if (error.response) { | |
const errorCode = error.response.status; | |
throw new AuthenticationError(error.message, errorCode); | |
} | |
const errorCode = 410; | |
throw new AuthenticationError(this._errors.get(errorCode), errorCode); | |
} | |
} | |
/** | |
* Store authentication details to localStorage | |
* | |
* @param authenticationData | |
* @private | |
*/ | |
_saveAuthenticationDetails(authenticationData: AuthTokenType) { | |
const key = this._storageEntries.authenticationToken; | |
this.storage.save(key, authenticationData, authenticationData.expires_in); | |
} | |
/** | |
* Get authentication details from localStorage | |
* | |
* @returns {Authentication} | |
* @throws AuthenticationError if authentication details are not available | |
*/ | |
getAuthenticationDetails(): Authentication { | |
const key = this._storageEntries.authenticationToken; | |
return new Authentication(this.storage.get(key)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import nock from 'nock'; | |
import AuthenticationService from '../authenticationService'; | |
import loginTokenSuccessData from '../__fixture__/loginTokenSuccess.json'; | |
import loginTokenRefresh from '../__fixture__/loginTokenRefresh.json'; | |
import loginTokenFailData from '../__fixture__/loginTokenFail.json'; | |
describe('authenticationService', () => { | |
const nockInstance = nock('http://server.com').defaultReplyHeaders({ | |
'Access-Control-Allow-Origin': '*', | |
'Access-Control-Allow-Headers': 'Authorization', | |
}); | |
const authClient = new AuthenticationService({ | |
baseURL: 'http://server.com', | |
basicKey: '123', | |
}); | |
it('should accept custom timeout', () => { | |
const localClient = new AuthenticationService({ | |
baseURL: 'http://server.com', | |
basicKey: '123', | |
timeout: 2500, | |
}); | |
expect(localClient.config.timeout).toBe(2500); | |
expect(localClient.client.defaults.timeout).toBe(2500); | |
}); | |
it('should use default baseURL', () => { | |
const localClient = new AuthenticationService({ | |
basicKey: '123', | |
}); | |
expect(localClient.config.baseURL).not.toBeDefined(); | |
expect(localClient.client.defaults.baseURL).toBe('http://localhost:8080'); | |
}); | |
describe('login', () => { | |
beforeEach(() => { | |
nockInstance.options('/oauth/token?grant_type=password').reply(204); | |
}); | |
it('should be able to login to the system', async () => { | |
nockInstance | |
.post( | |
'/oauth/token?grant_type=password', | |
{ | |
username: 'login', | |
password: '481200', | |
}, | |
{ | |
headers: { | |
Authorization: 'Basic 123', | |
}, | |
}, | |
) | |
.reply(200, loginTokenSuccessData); | |
const data = await authClient.login('login', '481200'); | |
expect(data).not.toBeNull(); | |
expect(data.access_token).toBe(loginTokenSuccessData[ 'access_token' ]); | |
}); | |
it('should store token returned from request', async () => { | |
nockInstance | |
.post( | |
'/oauth/token?grant_type=password', | |
{ | |
username: 'login', | |
password: '481200', | |
}, | |
{ | |
headers: { | |
Authorization: 'Basic 123', | |
}, | |
}, | |
) | |
.reply(200, loginTokenSuccessData); | |
await authClient.login('login', '481200'); | |
const token = authClient.getAuthenticationDetails().getAccessToken(); | |
expect(token).toBe(loginTokenSuccessData[ 'access_token' ]); | |
}); | |
it('should fail if basicKey is not present', async () => { | |
try { | |
new AuthenticationService({ | |
baseURL: 'http://server.com', | |
}); | |
} catch (e) { | |
expect(e.message).toBe('[601] - authentication.missing-basic-key'); | |
} | |
}); | |
it('should fail if any data is invalid', async () => { | |
nockInstance | |
.post( | |
'/oauth/token?grant_type=password', | |
{ | |
username: 'login', | |
password: '481200', | |
}, | |
{ | |
headers: { | |
Authorization: 'Basic 123', | |
}, | |
}, | |
) | |
.reply(400, loginTokenFailData); | |
try { | |
await authClient.login('login', '481200'); | |
} catch (e) { | |
expect(e.errorCode).toBe(400); | |
expect(e.message).toContain('Usuário não existe'); | |
} | |
}); | |
it('should fail if service is unavailable', async () => { | |
nockInstance | |
.post( | |
'/oauth/token?grant_type=password', | |
{ | |
username: 'login', | |
password: '481200', | |
}, | |
{ | |
headers: { | |
Authorization: 'Basic 123', | |
}, | |
}, | |
) | |
.reply(503); | |
try { | |
await authClient.login('login', '481200'); | |
} catch (e) { | |
expect(e.errorCode).toBe(503); | |
expect(e.message).toContain('Request failed with status code 503'); | |
} | |
}); | |
}); | |
describe('logout', () => { | |
const localStorage = authClient.storage; | |
it('should logout a user and delete credentials', async () => { | |
nockInstance | |
.delete( | |
'/oauth/token?grant_type=password', | |
{ | |
headers: { | |
Authorization: 'Bearer 123', | |
}, | |
}, | |
) | |
.reply(200); | |
localStorage.save('authenticationToken', loginTokenSuccessData); | |
await authClient.logout(); | |
expect(localStorage.get('authenticationToken')).not.toBeDefined(); | |
}); | |
it('should succeed if logout request fail', async () => { | |
nockInstance | |
.delete( | |
'/oauth/token?grant_type=password', | |
{ | |
headers: { | |
Authorization: 'Bearer 123', | |
}, | |
}, | |
) | |
.reply(503); | |
localStorage.save('authenticationToken', loginTokenSuccessData); | |
await authClient.logout(); | |
expect(localStorage.get('authenticationToken')).not.toBeDefined(); | |
}); | |
it('should ignore logout request if auth details are not stored', async () => { | |
nockInstance | |
.delete( | |
'/oauth/token?grant_type=password', | |
{ | |
headers: { | |
Authorization: 'Bearer 123', | |
}, | |
}, | |
) | |
.reply(503); | |
await authClient.logout(); | |
expect(localStorage.get('authenticationToken')).not.toBeDefined(); | |
}); | |
}); | |
describe('refreshToken', () => { | |
beforeEach(() => authClient.storage.remove('authenticationToken')); | |
it('should refresh token and get new credentials', async () => { | |
authClient.storage.save('authenticationToken', loginTokenSuccessData); | |
const t = loginTokenSuccessData[ 'refresh_token' ]; | |
nockInstance | |
.post(`/oauth/token?grant_type=refresh_token&refresh_token=${t}`) | |
.reply(200, loginTokenRefresh); | |
await authClient.refreshToken(); | |
const newToken = authClient.storage.get('authenticationToken'); | |
expect(newToken).toEqual(loginTokenRefresh); | |
expect(newToken).not.toEqual(loginTokenSuccessData); | |
}); | |
it('should remove tokens if the request fail', async () => { | |
authClient.storage.save('authenticationToken', loginTokenSuccessData); | |
const t = loginTokenSuccessData[ 'refresh_token' ]; | |
nockInstance | |
.post(`/oauth/token?grant_type=refresh_token&refresh_token=${t}`) | |
.reply(500); | |
try { | |
await authClient.refreshToken(); | |
} catch (e) { | |
const newToken = authClient.storage.get('authenticationToken'); | |
expect(newToken).not.toBeDefined(); | |
expect(e.errorCode).toBe(500); | |
} | |
}); | |
it('should fail if old tokens are not present', async () => { | |
nockInstance | |
.post('/oauth/token?grant_type=refresh_token&refresh_token=123') | |
.reply(500); | |
try { | |
await authClient.refreshToken(); | |
} catch (e) { | |
const newToken = authClient.storage.get('authenticationToken'); | |
expect(newToken).not.toBeDefined(); | |
expect(e.errorCode).toBe(410); | |
} | |
}); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment