Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 137 additions & 95 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,53 @@ Looking for a web alternative? Check out [axios-jwt](https://github.com/jetbridg

## What does it do?

Applies a request interceptor to your `axios` instance.
Applies a request interceptor to your axios instance.

The interceptor automatically adds a header (default: `Authorization`) with an access token to all requests.
The interceptor automatically adds an access token header (default: `Authorization`) to all requests.
It stores `accessToken` and `refreshToken` in `AsyncStorage` and reads them when needed.

It parses the expiration time of your access token and checks to see if it is expired before every request. If it has expired, a request to
refresh and store a new access token is automatically performed before the request proceeds.

## Installation

### 1. Install [React Native Async Storage](https://github.com/react-native-async-storage/async-storage)
### 1. Install async-storage and react-native-keychain

#### Install package

With npm:

```bash
npm install @react-native-async-storage/async-storage react-native-keychain
```

With Yarn:

```bash
yarn add @react-native-async-storage/async-storage react-native-keychain
```

With Expo CLI:

```bash
expo install @react-native-async-storage/async-storage react-native-keychain
```

#### Link Android & iOS packages

- **React Native 0.60+**

```bash
npx pod-install
```

- **React Native <= 0.59**

```bash
react-native link @react-native-async-storage/async-storage react-native-keychain
```

Please follow the [async-storage installation instructions](https://react-native-async-storage.github.io/async-storage/docs/install/) if you encounter any problems while installing async-storage

### 2. Install this library

Expand All @@ -34,147 +70,153 @@ yarn add react-native-axios-jwt

## How do I use it?

1. Create an `axios` instance.
2. Define a token refresh function.
3. Configure the interceptor.
4. Store tokens on login with `setAuthTokens` function.
5. Clear tokens on logout with `clearAuthTokens` function.
1. Create an axios instance
2. Define a token refresh function
3. Define `keychainOptions: Keychain.BaseOptions` - optional
4. Configure the interceptor
5. Store tokens on login with `setAuthTokens(keychainOptions)`
6. Clear tokens on logout with `clearAuthTokens()`

### Applying the interceptor

```typescript
// api.ts

import { IAuthTokens, TokenRefreshRequest, applyAuthTokenInterceptor } from 'react-native-axios-jwt'
import axios from 'axios'
import {
type AuthTokenInterceptorConfig,
type AuthTokens,
type TokenRefreshRequest,
applyAuthTokenInterceptor,
} from 'react-native-axios-jwt'

const BASE_URL = 'https://api.example.com'

// 1. Create an axios instance that you wish to apply the interceptor to
export const axiosInstance = axios.create({
baseURL: BASE_URL,
})
export const axiosInstance = axios.create({ baseURL: BASE_URL })

// 2. Define token refresh function.
// It is an async function that takes a refresh token and returns a promise
// that resolves in fresh access token and refresh token.
// You can also return only an access token in a case when a refresh token stays the same.
const requestRefresh: TokenRefreshRequest = async (
refreshToken: string,
): Promise<AuthTokens> => {
const requestRefresh: TokenRefreshRequest = async (refreshToken: string): Promise<string> => {

// Important! Do NOT use the axios instance that you supplied to applyAuthTokenInterceptor
// because this will result in an infinite loop when trying to refresh the token.
// Use the global axios client or a different instance.
const response = await axios.post(`${BASE_URL}/auth/refresh_token`, {
token: refreshToken,
})

const {
access_token: newAccessToken,
refresh_token: newRefreshToken,
} = response.data

return {
accessToken: newAccessToken,
refreshToken: newAccessToken,
}
}
// Use the global axios client or a different instance
const response = await axios.post(`${BASE_URL}/auth/refresh_token`, { token: refreshToken })

const config: AuthTokenInterceptorConfig = {
requestRefresh,
return response.data.access_token
}

// 3. Add interceptor to your axios instance
applyAuthTokenInterceptor(axiosInstance, config)
applyAuthTokenInterceptor(axiosInstance, { requestRefresh })
```

### Login
### Login/logout

```typescript
// login.ts

import { setAuthTokens } from 'react-native-axios-jwt'

import { isLoggedIn, setAuthTokens, clearAuthTokens, getAccessToken, getRefreshToken } from 'react-native-axios-jwt'
import { axiosInstance } from './api'

// 4. Log in with POST request with the email and password.
// Get access token and refresh token in response.
// Call `setAuthTokens` with the tokens.
const login = async (params: LoginRequestParams): void => {
// 4. Log in by POST-ing the email and password and get tokens in return
// and call setAuthTokens with the result.
const login = async (params: ILoginRequest) => {
const response = await axiosInstance.post('/auth/login', params)

const {
access_token: accessToken,
refresh_token: refreshToken,
} = response.data

// Save tokens to AsyncStorage.
// save tokens to storage
await setAuthTokens({
accessToken,
refreshToken,
accessToken: response.data.access_token,
refreshToken: response.data.refresh_token
})
}

// 5. Log out by clearing the auth tokens from AsyncStorage
const logout = () => clearAuthTokens()

// Check if refresh token exists
if (isLoggedIn()) {
// assume we are logged in because we have a refresh token
}

// Get access to tokens
const accessToken = getAccessToken().then(accessToken => console.log(accessToken))
const refreshToken = getRefreshToken().then(refreshToken => console.log(refreshToken))
```

### Usage
## Configuration

```typescript
// usage.ts
applyAuthTokenInterceptor(axiosInstance, {
requestRefresh, // async function that takes a refreshToken and returns a promise the resolves in a fresh accessToken
header = "Authorization", // header name
headerPrefix = "Bearer ", // header value prefix
})
```

import {
getAccessToken,
getRefreshToken,
isLoggedIn,
} from 'react-native-axios-jwt';
## Caveats

// Check if the user is logged in.
if (isLoggedIn()) {
// Assume the user is logged in because we have a refresh token stored in AsyncStorage.
}
- Your backend should allow a few seconds of leeway between when the token expires and when it actually becomes unusable.

// Use access token.
const doSomethingWithAccessToken = async (): void => {
const accessToken = await getAccessToken()
## Non-TypeScript implementation

console.log(accessToken)
}
```javascript
//api.js

// Use refresh token.
const doSomethingWithRefreshToken = async (): void => {
const refreshToken = await getRefreshToken()
import { applyAuthTokenInterceptor } from 'react-native-axios-jwt';
import axios from 'axios';

console.log(refreshToken)
}
const BASE_URL = 'https://api.example.com'

// 1. Create an axios instance that you wish to apply the interceptor to
export const axiosInstance = axios.create({ baseURL: BASE_URL })

// 2. Define token refresh function.
const requestRefresh = async (refresh) => {
// Notice that this is the global axios instance, not the axiosInstance!
const response = await axios.post(`${BASE_URL}/auth/refresh_token`, { refresh })

return response.data.access_token
};

// 3. Apply interceptor
// Notice that this uses the axiosInstance instance.
applyAuthTokenInterceptor(axiosInstance, { requestRefresh });
```
### Login/logout

### Logout
```javascript
//login.js

```typescript
// logout.ts
import {
isLoggedIn,
setAuthTokens,
clearAuthTokens,
getAccessToken,
getRefreshToken,
} from 'react-native-axios-jwt';
import { axiosInstance } from '../api';

import { clearAuthTokens } from 'react-native-axios-jwt'
// 4. Log in by POST-ing the email and password and get tokens in return
// and call setAuthTokens with the result.
const login = async (params) => {
const response = await axiosInstance.post('/auth/login', params)

// 5. Log out by clearing the auth tokens from AsyncStorage.
const logout = async (): void => {
await clearAuthTokens()
// save tokens to storage
await setAuthTokens({
accessToken: response.data.access_token,
refreshToken: response.data.refresh_token
})
}
```

## Configuration
// 5. Log out by clearing the auth tokens from AsyncStorage
const logout = () => clearAuthTokens()

```typescript
applyAuthTokenInterceptor(axiosInstance, {
header = 'Authorization', // header name
headerPrefix = 'Bearer ', // header value prefix
requestRefresh, // async function that resolves in fresh access token (and refresh token)
})
```
// Check if refresh token exists
if (isLoggedIn()) {
// assume we are logged in because we have a refresh token
}

## Caveats
// Get access to tokens
const accessToken = getAccessToken().then(accessToken => console.log(accessToken))
const refreshToken = getRefreshToken().then(refreshToken => console.log(refreshToken))

- Your backend should allow a few seconds of leeway between when the token expires and when it actually becomes unusable.

// Now just make all requests using your axiosInstance instance
axiosInstance.get('/api/endpoint/that/requires/login').then(response => { })

```
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,15 @@
"jest-environment-jsdom": "^29.0.3",
"jsonwebtoken": "^8.5.1",
"prettier": "^2.2.1",
"react-native": "0.72.10",
"react-native-keychain": "9.0.0",
"ts-jest": "^29.0.1",
"typescript": "^4.4.4"
},
"peerDependencies": {
"react-native": "*",
"@react-native-async-storage/async-storage": "*",
"react-native-keychain": "^9.0.0",
"axios": "*"
},
"dependencies": {
Expand Down
27 changes: 19 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import jwtDecode, { JwtPayload } from 'jwt-decode'

import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
import Keychain, { Options as KeychainOptions } from 'react-native-keychain'
import AsyncStorage from '@react-native-async-storage/async-storage'

// a little time before expiration to try refresh (seconds)
const EXPIRE_FUDGE = 10
export const STORAGE_KEY = `auth-tokens-${process.env.NODE_ENV}`
export const STORAGE_KEY = `dfhgsdf-${process.env.NODE_ENV}` // secure field name)))
const TOKEN_ID = `ojhaslidfgu`
const ASYNC_KC_CONFIG_ID = `lhkagsdfljk`

type Token = string
export interface AuthTokens {
accessToken: Token
refreshToken: Token
}


// EXPORTS

/**
Expand All @@ -31,8 +34,10 @@ export const isLoggedIn = async (): Promise<boolean> => {
* @param {AuthTokens} tokens - Access and Refresh tokens
* @returns {Promise}
*/
export const setAuthTokens = (tokens: AuthTokens): Promise<void> =>
AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(tokens))
export const setAuthTokens = async (tokens: AuthTokens, keychainOptions?: KeychainOptions): Promise<void> => {
keychainOptions && await AsyncStorage.setItem(ASYNC_KC_CONFIG_ID, JSON.stringify(keychainOptions));
await Keychain.setInternetCredentials(STORAGE_KEY, TOKEN_ID, JSON.stringify(tokens), keychainOptions)
}

/**
* Sets the access token
Expand All @@ -54,7 +59,7 @@ export const setAccessToken = async (token: Token): Promise<void> => {
* @async
* @returns {Promise}
*/
export const clearAuthTokens = (): Promise<void> => AsyncStorage.removeItem(STORAGE_KEY)
export const clearAuthTokens = async (): Promise<void> => Keychain.resetInternetCredentials(STORAGE_KEY)

/**
* Returns the stored refresh token
Expand Down Expand Up @@ -118,8 +123,14 @@ export const applyAuthTokenInterceptor = (axios: AxiosInstance, config: AuthToke
* @async
* @returns {Promise<AuthTokens>} Object containing refresh and access tokens
*/
const getAuthTokens = async (): Promise<AuthTokens | undefined> => {
const rawTokens = await AsyncStorage.getItem(STORAGE_KEY)
const getAuthTokens = async (keychainOptions?: KeychainOptions): Promise<AuthTokens | undefined> => {
let kcConfig = keychainOptions;
if(!kcConfig) {
const cfgString = await AsyncStorage.getItem(ASYNC_KC_CONFIG_ID)
kcConfig = cfgString ? JSON.parse(cfgString) : undefined
}
const kcResult = await Keychain.getInternetCredentials(STORAGE_KEY, kcConfig)
const rawTokens = kcResult && kcResult.password
if (!rawTokens) return

try {
Expand Down Expand Up @@ -195,7 +206,7 @@ const refreshToken = async (requestRefresh: TokenRefreshRequest): Promise<Token>
const status = error.response?.status
if (status === 401 || status === 422) {
// The refresh token is invalid so remove the stored tokens
await AsyncStorage.removeItem(STORAGE_KEY)
await clearAuthTokens()
error.message = `Got ${status} on token refresh; clearing both auth tokens`
}

Expand Down
Loading