Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Non-JWT tokens, replace old logger to tslog, using Buffer API… #33

Closed
wants to merge 2 commits into from
Closed
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
40 changes: 37 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Disclaimer

In general, I would suggest using cookie sessions for web applications.
But still, you may have a case when you have to use JWT tokens and store them in the local storage,
But still, you may have a case when you have to use JWT/Non-JWT tokens and store them in the local storage,
here this library might be useful.
React Native is also a good reason for using such a library.

Expand Down Expand Up @@ -32,7 +32,7 @@ is valid.
This library has typings for TypeScript code, but nothing prevents you from just
remove types from the examples and use as a JavaScript code.

### Access and Refresh tokens
### Access and Refresh JWT tokens

Let's assume you have a backend that uses `accessToken` and `refreshToken` to auth
users and provides services `/login`, `/register`, and `/update-token`. All services
Expand All @@ -50,7 +50,10 @@ The first step you need to do is to create an instance of an `authProvider`.
```typescript
import { createAuthProvider } from 'react-token-auth';

type Session = { accessToken: string; refreshToken: string };
type Session = {
accessToken: string
refreshToken: string
};

export const { useAuth, authFetch, login, logout } = createAuthProvider<Session>({
getAccessToken: session => session.accessToken,
Expand Down Expand Up @@ -144,13 +147,43 @@ export const getUser = (userId: string) => (dispatch: Dispatch) => {
Also if the token already saved in the `localStorage`, it
will be restored after refreshing the page.


### Non-JWT tokens
By default, the access_token expiration time is determined by the JWT token, but you can provide the expiration time yourself.

For example, the session expiration time can be stored in the session object.

To do this, you just need to override `getExpirationTime` in the configuration:
```typescript
import { createAuthProvider } from 'react-token-auth';

type Session = {
accessToken: string
refreshToken: string
expirationTime: number
};

export const { useAuth, authFetch, login, logout } = createAuthProvider<Session>({
getAccessToken: session => session.accessToken,
getExpirationTime: session => session.expirationTime,
storage: localStorage,
onUpdateToken: token =>
fetch('/update-token', {
method: 'POST',
body: token.refreshToken,
}).then(r => r.json()),
});
```


## API

### `createAuthProvider<Session>(config: IAuthProviderConfig<Session>)` -> `IAuthProvider<Session>`

#### `IAuthProviderConfig<Session>`

- `getAccessToken?: (session: Session) => TokenString` - function which allows to extract access token from the whole session object
- `getExpirationTime?: (session: Session) => Maybe<number>` - function which allows to extract expiration time from the whole session object. Default time from JWT token.
- `storageKey?: string = 'REACT_TOKEN_AUTH_KEY'` - key that will be used to store value in local storage
- `onUpdateToken?: (session: Session) => Promise<Maybe<Session>>` - function to update access token when it is expired
- `onHydratation?: (session: Maybe<Session>) => void` - function to process your tokens when `useAuth` is called.
Expand All @@ -172,6 +205,7 @@ will be restored after refreshing the page.
#### `IAsyncAuthProviderConfig<Session>`

- `getAccessToken?: (session: Session) => TokenString` - function which allows to extract access token from the whole session object
- `getExpirationTime?: (session: Session) => Maybe<number>` - function which allows to extract expiration time from the whole session object. Default time from JWT token.
- `storageKey?: string = 'REACT_TOKEN_AUTH_KEY'` - key that will be used to store value in local storage
- `onUpdateToken?: (session: Session) => Promise<Maybe<Session>>` - function to update access token when it is expired
- `onHydratation?: (session: Maybe<Session>) => void` - function to process your tokens when `useAuth` is called.
Expand Down
4 changes: 0 additions & 4 deletions jest.config.js

This file was deleted.

61 changes: 47 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-token-auth",
"version": "2.3.8",
"version": "2.4.0",
"description": "React Token Auth",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down Expand Up @@ -40,5 +40,10 @@
},
"files": [
"lib/**/*"
]
],
"dependencies": {
"@types/buffer-from": "^1.1.0",
"buffer-from": "^1.1.2",
"tslog": "^4.7.1"
}
}
32 changes: 25 additions & 7 deletions src/__tests__/isTokenExpired.test.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,52 @@
import { isTokenExpired } from '../isTokenExpired';
import { isTokenExpired, jwtExp } from '../isTokenExpired';
import { createJWTTokenWithExp, getExpiredJWTToken, getNonExpiredJWTToken } from '../test-utils/jwt';

describe('isTokenExpired', () => {
describe('isTokenExpired-jwt', () => {
it('expired token', () => {
const token = getExpiredJWTToken();

expect(isTokenExpired(token)).toBeTruthy();
expect(isTokenExpired(jwtExp(token))).toBeTruthy();
});

it('valid token', () => {
const token = getNonExpiredJWTToken();

expect(isTokenExpired(token)).toBeFalsy();
expect(isTokenExpired(jwtExp(token))).toBeFalsy();
});

it('token expires in 1 second', () => {
const token = createJWTTokenWithExp(Math.floor(Date.now() / 1000) + 1);

expect(isTokenExpired(token, 5000)).toBeTruthy();
expect(isTokenExpired(jwtExp(token), 5000)).toBeTruthy();
});

it('token expired 1 second ago', () => {
const token = createJWTTokenWithExp(Math.floor(Date.now() / 1000) - 1);

expect(isTokenExpired(token, 5000)).toBeTruthy();
expect(isTokenExpired(jwtExp(token), 5000)).toBeTruthy();
});

it('hardcoded token is expired', () => {
const token =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6IkpzYWRrZmphZGpraGZAcXdlcXdlcyIsInN1YiI6IjE1IiwiaWF0IjoxNjM3NzUxMjM5LCJleHAiOjE2Mzc3NTEyOTl9.ZjcVgbVvoZrIZHzjIckYgFwY5rnlyxlHGvGNHg_CRRk';
expect(isTokenExpired(token, 5000)).toBeTruthy();
expect(isTokenExpired(jwtExp(token), 5000)).toBeTruthy();
});
});

describe('isTokenExpired-custom-time', () => {
it('expired token', () => {
expect(isTokenExpired(1000)).toBeTruthy();
});

it('valid token', () => {
expect(isTokenExpired(Date.now() + 5000 * 1000)).toBeFalsy();
});

it('token expires in 1 second', () => {
expect(isTokenExpired(Date.now() + 1, 5000)).toBeTruthy();
});

it('token expired 1 second ago', () => {
expect(isTokenExpired(Date.now() - 1, 5000)).toBeTruthy();
});
});
34 changes: 19 additions & 15 deletions src/createAsyncAuthProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createListenersContainer } from './createListenersContainer';
import { createAsyncTokenProvider } from './createTokenProvider';
import { isTokenExpired } from './isTokenExpired';
import { isTokenExpired, jwtExp } from './isTokenExpired';
import { createLogger } from './logger';
import { createTokenUpdater } from './tokenUpdater';
import { Getter, IAsyncAuthStorage, Maybe, TokenString } from './types';
Expand All @@ -11,6 +11,7 @@ import { extractAccessToken } from './utils/extractAccessToken';

export interface IAsyncAuthProviderConfig<Session> {
getAccessToken?: (session: Session) => TokenString;
getExpirationTime?: (session: Session) => Maybe<number>;
storageKey?: string;
onUpdateToken?: (session: Session) => Promise<Maybe<Session>>;
onHydratation?: (session: Maybe<Session>) => void;
Expand Down Expand Up @@ -39,6 +40,7 @@ export const createAsyncAuthProvider = <Session>({
getAccessToken,
expirationThresholdMillisec = 5000,
debug = false,
getExpirationTime,
}: IAsyncAuthProviderConfig<Session>): IAsyncAuthProvider<Session> => {
const logger = createLogger(debug);
const listenersContainer = createListenersContainer();
Expand All @@ -52,7 +54,7 @@ export const createAsyncAuthProvider = <Session>({

let _session: Maybe<Session> = null;
const updateSession = async (session: Maybe<Session>) => {
logger.log('updateSession', 'session', session);
logger?.debug('updateSession', 'session', session);
await tokenProvider.setToken(session);
_session = session;
listenersContainer.notify();
Expand All @@ -64,8 +66,7 @@ export const createAsyncAuthProvider = <Session>({
.then(() => {
initiationPromise = null;
})
// tslint:disable-next-line:no-console
.catch(console.error);
.catch(logger?.warn);

const waitInit = () => initiationPromise;

Expand All @@ -77,20 +78,23 @@ export const createAsyncAuthProvider = <Session>({

const getSession = async () => {
const accessToken = extractAccessToken(getSessionState(), getAccessToken);
logger.log('getSession', 'accessToken', accessToken);
logger.log('getSession', 'tokenUpdater', tokenUpdater);
if (accessToken) {
logger.log(
logger?.debug('getSession', 'accessToken', accessToken);
logger?.debug('getSession', 'tokenUpdater', tokenUpdater);
if (_session && accessToken) {
const getExpTime = getExpirationTime || (() => jwtExp(accessToken));
logger?.debug(
'getSession',
'isTokenExpired(accessToken, expirationThresholdMillisec)',
isTokenExpired(accessToken, expirationThresholdMillisec, logger),
'isTokenExpired(getExpTime(_session), expirationThresholdMillisec)',
isTokenExpired(getExpTime(_session), expirationThresholdMillisec, logger),
);
}

if (_session && tokenUpdater && accessToken && isTokenExpired(accessToken, expirationThresholdMillisec)) {
const updatedSession = await tokenUpdater.updateToken(_session);
logger.log('getSession', 'updatedSession', accessToken);
await updateSession(updatedSession);
if (tokenUpdater) {
if (isTokenExpired(getExpTime(_session), expirationThresholdMillisec)) {
const updatedSession = await tokenUpdater.updateToken(_session);
logger?.debug('getSession', 'updatedSession', accessToken);
await updateSession(updatedSession);
}
}
}

return getSessionState();
Expand Down
Loading