Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/inmagik/use-eazy-auth
React hooks for handling auth stuff
https://github.com/inmagik/use-eazy-auth
auth authentication hooks react react-hooks typescript
Last synced: 20 days ago
JSON representation
React hooks for handling auth stuff
- Host: GitHub
- URL: https://github.com/inmagik/use-eazy-auth
- Owner: inmagik
- License: mit
- Created: 2019-04-08T19:50:49.000Z (over 5 years ago)
- Default Branch: master
- Last Pushed: 2023-03-15T03:16:41.000Z (over 1 year ago)
- Last Synced: 2024-08-08T02:13:00.632Z (3 months ago)
- Topics: auth, authentication, hooks, react, react-hooks, typescript
- Language: TypeScript
- Homepage:
- Size: 1.47 MB
- Stars: 57
- Watchers: 3
- Forks: 7
- Open Issues: 5
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# use-eazy-auth
[![Build Status](https://travis-ci.com/inmagik/use-eazy-auth.svg?branch=master)](https://travis-ci.com/inmagik/use-eazy-auth)
[![npm version](https://badge.fury.io/js/use-eazy-auth.svg)](https://www.npmjs.com/package/use-eazy-auth)React components and hooks to deal with token based authentication
This project takes the main concepts and algorithms (but also the name) from the [eazy-auth](https://github.com/inmagik/eazy-auth) library, and aims at providing equivalent functionality in contexts where the usage of `eazy-auth` with its strong dependency on `redux` and `redux-saga` is just too constraining.
## Installation
```
yarn add use-eazy-auth
npm install --save use-eazy-auth
```## Api
### `` Component
The top level component where you are able to configure authentication behaviours.Token based authentication is based on the usage of a token as a proof of identity. As such, the library has to deal with acquiring a token, storing it for later use, validating it, refreshing it when it expires, and deleting it when no refresh is possible or the token is revoked.
Moreover, the token is strictly tied to a user (as it is the proof of his identity), and so it is usually a good idea to keep the user object around while the token is valid.
This concepts are common to the majority of token based authentication system, even if implementation of them can be really different. Given this, `use-eazy-auth` gives you full customization freedom to integrate with your specific implementation and to tailor its own behaviours by passing props to the `` component.
The `` component creates React contexts that are used by any hook, so it is mandatory to make it a common ancestor for all components that need to deal with authentication, and advisable to put it as near as possible to the root of the React application tree.
The following properties are required:
* **loginCall**: the login call implements the process of acquiring a valid token, usually by means of some credentials (in the majority of cases, this is just a *username*, *password* pair, but it is not required). The signature of this function must be```js
(credentials: any) =>
Promise<{ accessToken: string, refreshToken?: string }, any> |
Observable<{ accessToken: string, refreshToken?: string }, any>
```Has you can see, the function is expected to return a promise which rejects in case of unsuccessful authentication (the error shape is up to you) or resolves in case of successful authentication. The required `accessToken` property in the resolution argument must hold the token which will be used to authenticate the user when interacting with the server. The optional `refreshToken` property, if present, must hold a token which is never used for API calls, but it is used to get a new token when that returned by the login expires without having the user go through the login procedure again. In case the access token expires and no refresh is possible, the user will experience a forced logout
* **meCall**: the me call implements the process of validating a previously stored token while gathering information about the owner user. This is used both to read user information from server and make them available throughout the application and to validate a token that has been recalled after some time from storage (see later). The signature of this function must be
```js
(accessToken: string) =>
Promise | Observable
```Has you can see, this function is expected to retrieve user information given an access token. In case the process succeeds, it is expected to return the object that describes the user (the shape of this is again completely up to you). In case the process cannot succeed, the promise is expected to be rejected with a status code. In this last situation, the `accessToken` cannot be considered valid anymore.
If a `refreshToken` was provided, `refreshTokenCall` is set on the `` object and the error status code is 401, the library will attempt to refresh the token and eventually repeat the me call with the refreshed token. If for any reason the token cannot be refreshed the user will be logged out.
* **refreshTokenCall**: some authentication schemes allow the usage of some kind of refresh token to obtain a fresh access token when the currently used one expires. This property allows to pass a function that implements the refresh procedure. As such, its signature is
```js
(refreshToken: string) =>
Promise<{ accessToken: string, refreshToken?: string }, any> |
Observable<{ accessToken: string, refreshToken?: string }, any>
```Considerations about the login call hold just the same for this api, the only difference is that the `credentials` parameter is replaced by the `refreshToken`
* **storageBackend**: the storage of `accessToken` and `refreshToken` allows the website to remember the user identity and to skip the authentication procedure in a subsequent visit. You are free to choose any synchronous or asynchronous storage backend like `localStorage`, `sessionStorage` (or `AsyncStorage` when using ReactNative). A storage object must meet the following signature
```js
type Storage = {
getItem: (key: string) => string | Promise,
setItem: (key: string, value: string) => void | Promise,
removeItem: (key: string) => void | Promise
}
```This property defaults to `window.localStorage` if available, or to `no storage` otherwise. In case you want to completely disable token storage, set this property to `false`
* **storageNamespace**: in case you did not opt-out token storage, you can customize the key under which the tokens are stored by setting this property (it must be a string). If you don't set this property, it defaults to the string `auth`
* **onLogout**: An optional callback inoked when user explicit logout
(calling `logout` action) or is kicked out from `401` rejection in call api functions.Here is a usage example
> Please note that the login call and the me call are **not** real life examples: always validate your users against your authentication backend!
```js
import React from 'react'
import Auth from 'use-eazy-auth'const loginCall = ({ username, password }) => new Promise((resolve, reject) =>
(username === 'alice' && password === 'my-super-secret-password')
? resolve({ accessToken: 'alice-is-allowed-to-access' })
: reject('Unauthorized!')
)const meCall = token => new Promise((resolve, reject) =>
(token === 'alice-is-allowed-to-access')
? resolve({ username: 'alice', status: 'Administrator' })
: reject('Unauthorized!')
)const App = () => (
{
/* react-router or in any case the restricted section of
* your application should be put here
*/
}
)```
You can also use the `render` prop.
```js
function App() {
return (
/* render my children */}
/>
)
}
```### `useAuthState()` hook
This hooks returns the current auth state. The auth state is the operational state of the library, which can tell you if some operation is in progress, like initialization or login. The state object is a plain object with the following properties
* **bootstrappedAuth** (bool): this flag tells whether the library has loaded or loading is still in progress. Loading means that the library is fetching stored tokens and validating them with a me call.
* **authenticated** (bool): this flag tells whether the user is authenticated (i.e. the library has a valid access token ready for use) or not
* **loginLoading** (bool): this flag tells whether a login operation is in progress
* **loginError** (any): this property holds the result of the last rejected promise (it is not cleared after a successful login call, you need to clear it explictly by calling `clearLoginError` - see example)Usage example
```jsx
import React, { useState } from 'react'
import { useAuthState, useAuthActions } from 'use-eazy-auth'const Screens = () => {
const { authenticated, bootstrappedAuth } = useAuthState()
if (!bootstrappedAuth) {
returnPlease wait, we are logging you in...
}
return authenticated ? :
}const Login = () => {
const { loginLoading, loginError } = useAuthState()
const { login, clearLoginError } = useAuthActions()const [username, setUsername] = useState('')
const [password, setPassword] = useState('')return (
{
e.preventDefault()
if (username !== '' && password !== '') {
login({ username, password })
}
}}>
{
clearLoginError()
setUsername(e.target.value)
}}
/>
{
clearLoginError()
setPassword(e.target.value)
}}
/>
{!loginLoading ? 'Login!' : 'Logged in...'}
{loginError &&Bad combination of username and password}
)
}
```### `useAuthActions()` hook
This hook allows to invoke some auth related behaviours. It returns a plain JavaScript object whose properties are functions.* **callAuthApiPromise**
This function performs an authenticated API call. The first parameter is a factory function (a function which returns a fucntion) that is expected to create the real api call function (i.e. the function that implements the real api call, you can use XHR, Axios, SuperAgent or whatever you like inside this). The factory function is invoked with the access token, and is expected to return again a function - the api call function. Any additional parameter supplied to the **callAuthApiPromise** will be used as a parameter to invoke the api call function. The api call must return a promise. If all is fine, that promise is expected to resolve. In case it rejects, the rejection value must be an object with a status property carrying the status code of the request. A 401 code will trigger the refresh token operation (if available) and repeat the api call invocation with the new token. If even this second call is rejected, the user will be logged out.* **callAuthApiObservable**
This behaves like **callAuthApiPromise** except that the api call function is expected to return an `Observable` from `RxJS`. Promise rejection is replaced by error raising.* **login**
This function triggers a login operation. It is expected to be called with a single argument (the credentials object) which is used to invoke the `loginCall` provided to the `` component as a property* **logout**
This function triggers a logout operation. This means clearing the stored tokens and set the library `authenticated` state to `false`. No api call is performed here.* **clearLoginError**
This function clear the current login error.* **updateUser**
This function update the current auth user with given *User* object.* **patchUser**
This function shallow merge the given *User* object with current *User* object.* **setTokens**
```js
({ accessToken: string, refreshToken?: string }) => void
```
This function explicit set new tokens, this function write new tokens in storage as well.All these functions are stable across renders, so it is safe to add them as dependencies of some `useEffect` or `useMemo`, they will never trigger any unnecessary re-renders.
Here is some example
```js
import React, { useState, useEffect } from 'react'
import { useAuthActions } from 'use-eazy-auth'const authenticatedGetTodos = (token) => (category) => new Promise((resolve, reject) => {
return (token === 23)
? resolve([
'Learn React',
'Prepare the dinner',
])
: reject({ status: 401, error: 'Go out' })
})const Home = () => {
const [todos, setTodos] = useState([])
const { logout, callAuthApiPromise } = useAuthActions()useEffect(() => {
callAuthApiPromise(authenticatedGetTodos, 'all')
.then(todos => setTodos(todos))
}, [callAuthApiPromise])return (
Todos
{todos.map((todo, i) => (
- {todo}
))}
Logout
)
}
```### `useAuthUser()` hook
This hook returns the current user object (in the shape you chose to return from the `meCall` supplied to the `` component) and the current token as props of a plain JavaScript object. If user is not logged in, both properties result in `null` values.```js
import { useAuthUser } from 'use-eazy-auth'
const Home = () => {
const { user, token } = useAuthUser()return (
Logged in user {user.username}
identified by token {token}
)
}
```## Provide initial data
In certain scenarios (Server Side Rendering), you need to provide initial data to your `` and avoid
all the side effects appening during first renders (check tokens, perform `meCall` ecc).You can do that using the `initialData` prop:
```jsx
const App = () => (
{/* ... */}
)
```When both `user` and `token` are not null the initial state is **authenticated** otherwise no.
The `initialData` typing is:
```ts
interface InitialAuthData {
accessToken: A | null
refreshToken?: R | null
expires?: number | null
user: U | null
}
```## React Router Integration
This library ships with components useful to integrate routing (by react-router) and authentication. You are not forced to do this: you can use any routing library you wish and write the integration yourself, maybe taking our react-router integration as an exampleThe integration is done by providing three specialized `Route` components: `GuestRoute`, `AuthRoute` and `MaybeAuthRoute`. A `GuestRoute` can be accessed only by non authenticated users, and will redirect authenticated users. An `AuthRoute` can be accessed just by authenticated users, and will redirect any non authenticated visitor. A `MaybeAuthRoute` will accept authenticated just as non authenticated users. If in some route you don't care about authentication, a vanilla `Route` can still be used.
You can import those components from `use-eazy-auth/routes`
### `` component
When the auth is booting render an optional spinner, when the user is authenticated render a ``
otherwise act as a normal ``.The `` component accepts the following props
* **redirectTo**: the path to redirect authenticated users to
* **redirectToReferrer**: if set to `true`, users that are redirected to this page from an `` because they are not authenticated will be redirected back after login instead of being redirected to the path set by `redirectTo`. Note that it is mandatory to set the `redirectTo` property as unauthenticated users may land directly on a `GuestRoute` and so they may not have a referrer
* **spinnerComponent**: an optional spinner component to render instead of content until the auth initialization is not complete
* **spinner**: an optional spinner react element to render instead of content until the auth initialization is not complete
* any other property accepted by `````ts
type GuestRouteProps = {
redirectTo?: string | Location
redirectToReferrer?: boolean
spinner?: ReactNode
spinnerComponent?: ComponentType
} & RouteProps
```### `` component
When the auth is booting render an optional spinner, when the user is authenticated act as ``
otherwise act as a normal ``.Can also redirect your user by a given `redirectTest`.
The `` component accepts the following props
* **redirectTo**: the path to redirect a non authenticated user to
* **rememberReferrer**: whether to enable the referrer in order to redirect the user back after login
* **redirectTest**: a function to test if current authenticated user can access your route, take user as only parameter and if falsy is returned the user can acccess the route, otherwise the return value is expected to be a valid path used to redirect the user.
* **spinnerComponent**: an optional spinner component to render instead of content until the auth initialization is not complete
* **spinner**: an optional spinner react element to render instead of content until the auth initialization is not complete
* any other property accepted by `````ts
type AuthRouteProps = {
redirectTest?: null | ((user: U) => string | null | undefined | Location)
redirectTo?: string | Location
spinner?: ReactNode
spinnerComponent?: ComponentType
rememberReferrer?: boolean
} & RouteProps
```### `` component
When the auth is booting render an optional spinner otherwise act as ``.The `` component accepts the following props
* **spinnerComponent**: an optional spinner component to render instead of content until the auth initialization is not complete
* **spinner**: an optional spinner react element to render instead of content until the auth initialization is not complete
* any other property accepted by `````ts
export type MaybeAuthRouteProps = {
spinner?: ReactNode
spinnerComponent?: ComponentType
} & RouteProps
```## Fetching libraries integrations
### [SWR](https://github.com/vercel/swr)
```jsx
import useSWR, { SWRConfig } from 'swr'
import { useAuthActions } from 'use-eazy-auth'
import { meCall, refreshTokenCall, loginCall } from './authCalls'function Dashboard() {
const { data: todos } = useSWR('/api/todos')
// ...
}function ConfigureAuthFetch({ children }) {
const { callAuthApiPromise } = useAuthActions()
return (
callAuthApiPromise(
token => (url, options) =>
fetch(url, {
...options,
headers: {
...options?.headers,
Authorization: `Bearer ${token}`,
},
})
// NOTE: use-eazy-auth needs a Rejection with shape:
// { status: number }
.then(res => (res.ok ? res.json() : Promise.reject(res))),
...args
),
}}
>
{children}
)
}function App() {
return (
)
}
```### [react-query](https://github.com/tannerlinsley/react-query)
```jsx
import { useQuery } from 'react-query'
import { useAuthActions } from 'use-eazy-auth'export default function Dashboard() {
const { callAuthApiPromise } = useAuthActions()
const { data: todos } = useQuery(['todos'], () =>
callAuthApiPromise((token) => () =>
fetch(`/api/todos`, {
headers: {
Authorization: `Bearer ${token}`,
},
}).then((res) => (res.ok ? res.json() : Promise.reject(res)))
)
)
// ...
}
```### [react-rocketjump](https://github.com/inmagik/react-rocketjump)
```jsx
import { ConfigureRj, rj, useRunRj } from 'react-rocketjump'
import { useAuthActions } from 'use-eazy-auth'const Todos = rj({
effectCaller: rj.configured(),
effect: (token) => () =>
fetch(`/api/todos/`, {
headers: {
Authorization: `Bearer ${token}`,
},
}).then((res) => (res.ok ? res.json() : Promise.reject(res))),
})export default function Dashboard() {
const [{ data: todos }] = useRunRj(Todos)
// ...
}function ConfigureAuthFetch({ children }) {
const { callAuthApiObservable } = useAuthActions()
// NOTE: react-rocketjump supports RxJs Observables
return (
{children}
)
}function App() {
return (
)
}
```## Run example
This repository contains a runnable basic example of the main functionalities of the library```sh
git clone https://github.com/inmagik/use-eazy-auth.git
cd use-eazy-auth
yarn install
yarn dev
```