-
-
Notifications
You must be signed in to change notification settings - Fork 753
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Authentication v3 client (#1240)
BREAKING CHANGE: Rewrite for authentication v3
- Loading branch information
Showing
27 changed files
with
1,025 additions
and
1,916 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
const { NotAuthenticated } = require('@feathersjs/errors'); | ||
|
||
exports.Storage = class Storage { | ||
constructor () { | ||
this.store = {}; | ||
} | ||
|
||
getItem (key) { | ||
return this.store[key]; | ||
} | ||
|
||
setItem (key, value) { | ||
return (this.store[key] = value); | ||
} | ||
|
||
removeItem (key) { | ||
delete this.store[key]; | ||
return this; | ||
} | ||
}; | ||
|
||
exports.AuthenticationClient = class AuthenticationClient { | ||
constructor (app, options) { | ||
const socket = app.io || app.primus; | ||
|
||
this.app = app; | ||
this.app.set('storage', this.app.get('storage') || options.storage); | ||
this.options = options; | ||
this.authenticated = false; | ||
|
||
if (socket) { | ||
this.handleSocket(socket); | ||
} | ||
} | ||
|
||
get service () { | ||
return this.app.service(this.options.path); | ||
} | ||
|
||
get storage () { | ||
return this.app.get('storage'); | ||
} | ||
|
||
handleSocket (socket) { | ||
// Connection events happen on every reconnect | ||
const connected = this.app.io ? 'connect' : 'open'; | ||
|
||
socket.on(connected, () => { | ||
// Only reconnect when `reAuthenticate()` or `authenticate()` | ||
// has been called explicitly first | ||
if (this.authenticated) { | ||
// Force reauthentication with the server | ||
this.reauthenticate(true); | ||
} | ||
}); | ||
} | ||
|
||
setJwt (accessToken) { | ||
return Promise.resolve(this.storage.setItem(this.options.storageKey, accessToken)); | ||
} | ||
|
||
getJwt () { | ||
return Promise.resolve(this.storage.getItem(this.options.storageKey)); | ||
} | ||
|
||
removeJwt () { | ||
return Promise.resolve(this.storage.removeItem(this.options.storageKey)); | ||
} | ||
|
||
reset () { | ||
// Reset the internal authentication state | ||
// but not the accessToken from storage | ||
const authResult = this.app.get('authentication'); | ||
|
||
this.app.set('authentication', null); | ||
this.authenticated = false; | ||
|
||
return authResult; | ||
} | ||
|
||
reauthenticate (force = false) { | ||
// Either returns the authentication state or | ||
// tries to re-authenticate with the stored JWT and strategy | ||
const authPromise = this.app.get('authentication'); | ||
|
||
if (!authPromise || force === true) { | ||
return this.getJwt().then(accessToken => { | ||
if (!accessToken) { | ||
throw new NotAuthenticated('No accessToken found in storage'); | ||
} | ||
|
||
return this.authenticate({ | ||
strategy: this.options.jwtStrategy, | ||
accessToken | ||
}); | ||
}); | ||
} | ||
|
||
return authPromise; | ||
} | ||
|
||
authenticate (authentication) { | ||
if (!authentication) { | ||
return this.reauthenticate(); | ||
} | ||
|
||
const promise = this.service.create(authentication) | ||
.then(authResult => { | ||
const { accessToken } = authResult; | ||
|
||
this.authenticated = true; | ||
|
||
return this.setJwt(accessToken).then(() => authResult); | ||
}); | ||
|
||
this.app.set('authentication', promise); | ||
|
||
return promise; | ||
} | ||
|
||
logout () { | ||
return this.app.get('authentication') | ||
.then(() => this.service.remove(null)) | ||
.then(() => this.removeJwt()) | ||
.then(() => this.reset()); | ||
} | ||
}; |
19 changes: 19 additions & 0 deletions
19
packages/authentication-client/lib/hooks/authentication.js
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
const { stripSlashes } = require('@feathersjs/commons'); | ||
|
||
module.exports = () => { | ||
return context => { | ||
const { app, params, path, method, app: { authentication } } = context; | ||
|
||
if (stripSlashes(authentication.options.path) === path && method === 'create') { | ||
return context; | ||
} | ||
|
||
return Promise.resolve(app.get('authentication')).then(authResult => { | ||
if (authResult) { | ||
context.params = Object.assign({}, authResult, params); | ||
} | ||
|
||
return context; | ||
}); | ||
}; | ||
}; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,5 @@ | ||
const authentication = require('./authentication'); | ||
const populateHeader = require('./populate-header'); | ||
const populateAccessToken = require('./populate-access-token'); | ||
const populateEntity = require('./populate-entity'); | ||
|
||
let hooks = { | ||
populateHeader, | ||
populateAccessToken, | ||
populateEntity | ||
}; | ||
|
||
module.exports = hooks; | ||
exports.authentication = authentication; | ||
exports.populateHeader = populateHeader; |
13 changes: 0 additions & 13 deletions
13
packages/authentication-client/lib/hooks/populate-access-token.js
This file was deleted.
Oops, something went wrong.
39 changes: 0 additions & 39 deletions
39
packages/authentication-client/lib/hooks/populate-entity.js
This file was deleted.
Oops, something went wrong.
25 changes: 12 additions & 13 deletions
25
packages/authentication-client/lib/hooks/populate-header.js
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,18 @@ | ||
module.exports = function populateHeader (options = {}) { | ||
if (!options.header) { | ||
throw new Error(`You need to pass 'options.header' to the populateHeader() hook.`); | ||
} | ||
module.exports = () => { | ||
return context => { | ||
const { app, params: { accessToken } } = context; | ||
const authentication = app.authentication; | ||
|
||
return function (hook) { | ||
if (hook.type !== 'before') { | ||
return Promise.reject(new Error(`The 'populateHeader' hook should only be used as a 'before' hook.`)); | ||
} | ||
// Set REST header if necessary | ||
if (app.rest && accessToken) { | ||
const { scheme, header } = authentication.options; | ||
const authHeader = `${scheme} ${accessToken}`; | ||
|
||
if (hook.params.accessToken) { | ||
hook.params.headers = Object.assign({}, { | ||
[options.header]: options.prefix ? `${options.prefix} ${hook.params.accessToken}` : hook.params.accessToken | ||
}, hook.params.headers); | ||
context.params.headers = Object.assign({}, { | ||
[header]: authHeader | ||
}, context.params.headers); | ||
} | ||
|
||
return Promise.resolve(hook); | ||
return context; | ||
}; | ||
}; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,52 +1,37 @@ | ||
const hooks = require('./hooks/index'); | ||
const Passport = require('./passport'); | ||
|
||
const { AuthenticationClient, Storage } = require('./core'); | ||
const hooks = require('./hooks'); | ||
const defaults = { | ||
header: 'Authorization', | ||
cookie: 'feathers-jwt', | ||
scheme: 'Bearer', | ||
storageKey: 'feathers-jwt', | ||
jwtStrategy: 'jwt', | ||
path: '/authentication', | ||
entity: 'user', | ||
service: 'users', | ||
timeout: 5000 | ||
Authentication: AuthenticationClient | ||
}; | ||
|
||
function init (config = {}) { | ||
const options = Object.assign({}, defaults, config); | ||
|
||
return function () { | ||
const app = this; | ||
|
||
app.passport = new Passport(app, options); | ||
app.authenticate = app.passport.authenticate.bind(app.passport); | ||
app.logout = app.passport.logout.bind(app.passport); | ||
|
||
// Set up hook that adds token and user to params so that | ||
// it they can be accessed by client side hooks and services | ||
app.mixins.push(function (service) { | ||
// if (typeof service.hooks !== 'function') { | ||
if (app.version < '3.0.0') { | ||
throw new Error(`This version of @feathersjs/authentication-client only works with @feathersjs/feathers v3.0.0 or later.`); | ||
} | ||
|
||
service.hooks({ | ||
before: hooks.populateAccessToken(options) | ||
}); | ||
module.exports = _options => { | ||
const options = Object.assign({}, { | ||
storage: new Storage() | ||
}, defaults, _options); | ||
const { Authentication } = options; | ||
|
||
return app => { | ||
const authentication = new Authentication(app, options); | ||
|
||
app.authentication = authentication; | ||
app.authenticate = authentication.authenticate.bind(authentication); | ||
app.reauthenticate = authentication.reauthenticate.bind(authentication); | ||
app.logout = authentication.logout.bind(authentication); | ||
|
||
app.hooks({ | ||
before: [ | ||
hooks.authentication(), | ||
hooks.populateHeader() | ||
] | ||
}); | ||
|
||
// Set up hook that adds authorization header for REST provider | ||
if (app.rest) { | ||
app.mixins.push(function (service) { | ||
service.hooks({ | ||
before: hooks.populateHeader(options) | ||
}); | ||
}); | ||
} | ||
}; | ||
} | ||
|
||
module.exports = init; | ||
}; | ||
|
||
module.exports.default = init; | ||
module.exports.defaults = defaults; | ||
Object.assign(module.exports, { | ||
AuthenticationClient, Storage, hooks, defaults | ||
}); |
Oops, something went wrong.