Created
November 20, 2019 17:24
-
-
Save KidkArolis/4cd2dfbeb7f7e6447a5017ffd76c2c5b to your computer and use it in GitHub Desktop.
Feathers Client with robust reauthentication
This file contains hidden or 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 createFeathersClient from '@feathersjs/feathers' | |
import auth from '@feathersjs/authentication-client' | |
import socketio from '@feathersjs/socketio-client' | |
import io from 'socket.io-client' | |
export function createFeathers() { | |
const socket = io() | |
const feathers = createFeathersClient() | |
feathers.configure(socketio(socket)) | |
feathers.configure( | |
auth({ | |
storage: window.localStorage, | |
storageKey: 'access-token', | |
path: '/api/authentication', | |
Authentication: AuthenticationClient, | |
}), | |
) | |
feathers.hooks({ | |
before: { | |
all: [ensureConnected(), markConnected()], | |
}, | |
error: { | |
all: [handleUnauthenticated()], | |
}, | |
}) | |
return feathers | |
} | |
class AuthenticationClient extends auth.AuthenticationClient { | |
handleSocket(socket) { | |
// Connection events happen on every reconnect | |
const connected = this.app.io ? 'connect' : 'open' | |
socket.on(connected, () => { | |
this.connected = true | |
// Only reconnect when `reAuthenticate()` or `authenticate()` | |
// has been called explicitly first | |
if (this.authenticated) { | |
// Force reauthentication with the server | |
this.reAuthenticate(true) | |
.then(() => { | |
while ((this.waiting || []).length) { | |
this.waiting.pop()() | |
} | |
}) | |
.catch(err => { | |
if (err.code === 401) { | |
// failed to reauthenticate a socket in the background, | |
// let's reload the page so that we're taken to /login | |
if (window.location.pathname !== '/login') { | |
window.location.reload() | |
} | |
} | |
}) | |
} | |
}) | |
socket.on('disconnect', () => { | |
this.connected = false | |
}) | |
} | |
isConnected() { | |
if (this.connected) { | |
return Promise.resolve() | |
} else { | |
this.waiting = this.waiting || [] | |
return new Promise(resolve => { | |
this.waiting.push(resolve) | |
}) | |
} | |
} | |
handleError(error, type) { | |
if (error.code === 401 || error.code === 404) { | |
const promise = this.removeAccessToken().then(() => this.reset()) | |
return type === 'logout' | |
? promise | |
: promise.then(() => Promise.reject(error)) | |
} | |
return Promise.reject(error) | |
} | |
} | |
function ensureConnected() { | |
return async context => { | |
const authServicePath = | |
context.app.authentication.options.path || 'authentication' | |
if (context.path !== authServicePath) { | |
await context.app.authentication.isConnected() | |
} | |
} | |
} | |
function markConnected() { | |
return context => { | |
const { app } = context | |
const { authentication } = app | |
context.params.connected = authentication.connected | |
} | |
} | |
function handleUnauthenticated() { | |
return context => { | |
const { params, error } = context | |
// The socket is connected, but server is responding with | |
// 401s, that means the token is expired, invalid, etc. | |
// The other case is when the socket is not connected, then we | |
// do not want to reload the page, we just want to wait for the socket | |
// to reconnect and reauthenticate, this is typically due to temporary | |
// loss of server connection | |
if (error.name === 'NotAuthenticated' && params.connected) { | |
if (window.location.pathname !== '/login') { | |
window.location.reload() | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment