react-auth-guard v0.2.0
react-auth-guard
A React context component to provide authentication through the app. The provider is decoupled from user persistance layout. It does not use/need Redux.
Breaking changes in 0.2.0
Persistance strategy
The persistance strategy used to return a raw value from the persistance. While most storage drivers will use async workflows (like AsyncStorage
), a promise is more suitable for this use case.
That being said, if you implemented your own persistance strategy, the only change you need to do to still be compliant is to return a promise that resolves.
Example:
// Before
let token = null
const strategy = {
get: () => {
return token
}
// ...
}
// After
let token = null
const strategy = {
get: () => {
return new Promise(resolve => resolve(token))
}
// ...
}
Installation
Install the package using the following command
yarn add react-auth-guard
//
npm i -s react-auth-guard
Setup
"Talking is cheap show me the code" checkout the src/demo
folder to get example implementation.
Setup provider
Setup the application provider as the following. The only required props is the fetchUser
function which should return a Promise. That Promise is used to check the validity of the token. If it resolves, then the app is considered authenticated, otherwise the app is considered not authenticated. As an example, this function can be a redux action to get the current user object from the server and stores it the Redux store.
import fetchUser from './actions'
import Provider from 'react-auth-guard'
const App = () => (
<Provider
fetchUser={fetchUser}
>
{({ authenticating, authenticated }) => (
/* Render your application */
)}
</Provider>
)
Handle authentication flow
I like using a <Loading />
components while the API authenticates. So if you reach to the URL and have a token in your local storage, it'll show a spinner the fetchUser
promise is either resolved or reject.
import fetchUser from './actions'
import Provider from 'react-auth-guard'
import NotAuthenticated from './NotAuthenticated'
import Authenticated from './Authenticated'
const Loading = ({ isLoading, children }) => (isLoading
? <h1>Loading</h1>
: children
)
const App = () => (
<Provider
fetchUser={() => new Promise(resolve => resolve())}
getters={authGetters}
onLogout={() => alert('You\'ve been logged out')}
onLogin={() => alert('Welcome to the magical world of the internet')}
onLoginFail={() => alert('You shall not pass!')}
>
{({ authenticating, authenticated }) => (
<Loading isLoading={authenticating}>
{
authenticated
? <Authenticated />
: <NotAuthenticated />
}
</Loading>
)}
</Provider>
)
Login the provider
To login the provider, you have to call the updateToken
function from the render prop or the consumer (You may also use the withAuth
higher-order components to connect the auth props).
As shown below, the auth object is provided by the withAuth
HOC to the component.
const NotAuthenticated = ({ auth }) => {
const login = () => loginUser().then(({ token }) => {
auth.updateToken(token)
})
return (
<div>
<h1>NotAuthenticated</h1>
<button type="button" onClick={login}>Login</button>
</div>
)
}
export default withAuth(NotAuthenticated)
Using the hook
The same strategy could be achieve using the useAuth
hook.
import { useAuth } from 'react-auth-provider'
const NotAuthenticated = () => {
const auth = useAuth()
const login = () => loginUser().then(({ token }) => {
auth.updateToken(token)
})
return (
<div>
<h1>NotAuthenticated</h1>
<button type="button" onClick={login}>Login</button>
</div>
)
}
export default NotAuthenticated
Logout the provider
The Consumer (and the HOC) also exposes a logout
function that clears the token from the persistance strategy and unauthenticate the provider.
const Navbar = ({ auth }) => {(
<div>
<button type="button" onClick={auth.logout}>Logout</button>
</div>
)
export default withAuth(NotAuthenticated)
Props
fetchUser
: required Callbacks that takes no parameter and returns a Promise.getters
: Object that helps getting things based on the provider state see GettersdecodeToken
: Function that decodes the token. Usesjwt-decode
as defaultgetDecodedUserId
: Function that decodes the user id from the decoded token. By default is returns thesub
part of the token.persistStrategy
: A persistancy strategy object. May be useful to override if you want to persist the token inAsyncStorage
for a React-Native usecase. Defaults to a localStorage handle, refer to Persistance strategy for more info.onLogout
: Callback that'll be executed when logout is firedonLogin
: Callback that'll be executed when login is successful (thefetchUser
did resolve)onLoginFail
: Callback that'll be executed when login is failure (thefetchUser
did reject)children
: Function that expose the render props
Render props
token
: The persisted tokenauthenticating
:true
if thefetchUser
prop is being called.authenticated
:true
if thefetchUser
prop has resolveduserId
: Decoded user id from the tokenupdateToken: (token: string) => void
: Updates the token persisted token for the provided one and dispatch thefetchUser
function prop.logout: () => void
: Clears the persisted token and sets authenticated to false
Getters
Why?
What makes the provider easy to use, is because he's decoupled with app state. So if you're using GraphQL or json:api compliant api. The provider don't needs to know.
Getters allows you to inject function that get available in your auth provider without composing HOC or things. For instance, I used a json:api compliant app where my local user is stored in redux other entities. Without getters, I was forced to always use Redux's connect composed with withAuth
HOC everytime I wanted to use my current user. I added a getUser
getter to the provider which calls calls my Redux store.
How?
Every getters receive the provider state as the first parameters like the following. Let say our app really requires a way to pad the user id.
const authGetters = {
paddedId: (providerState) => providerState.userId.toString().padStart(5, '0')
}
const Navbar = ({ auth }) => {(
<div>
<h1>{auth.paddedId()}</h1> /* renders "00001" if the user id is 1 */
<button type="button" onClick={auth.logout}>Logout</button>
</div>
)
export default withAuth(NotAuthenticated)
Persistance strategy
As said earlier, you may not want to persist the token in the localStorage. You may be working on a React-Native app that needs to store it in AsyncStorage
.
A persistance strategy is only an object that implements three functions:
get: () => Promise<void>
: Gets the token from the persistance using a promisepersist: (token: string) => void
: Persists the given tokenclear: () => void
: Removes the token
Flow
The provider mounts with authenticated
to false
and authenticating
to true
to load a loading screen.
By defaults, it uses the localStorage
strategy to handle token persistancy in the localStorage
. When the provider is mounted, the get
method from the strategy is called to get the currently persisted token.
If a token is present (not null
), the provider will call the fetchUser
function which should be used as a test with the server to validate the token. The function must return a Promise and must resolve only if the token is valid.
If the promise resolves, authenticated
will be true and authenticating
will be set to false. So you may render the app as authenticated.
If the promise rejects, authenticated
will be false and authenticating
will also be set to false. The app will render not authenticated.