react-oauth2-code-pkce
Advanced tools
Comparing version 1.2.5 to 1.3.0-alpha.1
@@ -34,2 +34,3 @@ "use strict"; | ||
const validateAuthConfig_1 = require("./validateAuthConfig"); | ||
const FALLBACK_EXPIRE_TIME = 600; // 10minutes | ||
exports.AuthContext = (0, react_1.createContext)({ | ||
@@ -41,4 +42,6 @@ token: '', | ||
const AuthProvider = ({ authConfig, children }) => { | ||
const [refreshToken, setRefreshToken] = (0, Hooks_1.default)('ROCP_refreshToken', null); | ||
const [refreshToken, setRefreshToken] = (0, Hooks_1.default)('ROCP_refreshToken', undefined); | ||
const [refreshTokenExpire, setRefreshTokenExpire] = (0, Hooks_1.default)('ROCP_refreshTokenExpire', (0, authentication_1.timeOfExpire)(FALLBACK_EXPIRE_TIME)); | ||
const [token, setToken] = (0, Hooks_1.default)('ROCP_token', ''); | ||
const [tokenExpire, setTokenExpire] = (0, Hooks_1.default)('ROCP_tokenExpire', (0, authentication_1.timeOfExpire)(FALLBACK_EXPIRE_TIME)); | ||
const [idToken, setIdToken] = (0, Hooks_1.default)('ROCP_idToken', undefined); | ||
@@ -49,6 +52,17 @@ const [loginInProgress, setLoginInProgress] = (0, Hooks_1.default)('ROCP_loginInProgress', false); | ||
let interval; | ||
(0, validateAuthConfig_1.validateAuthConfig)(authConfig); | ||
// Set default values and override from passed config | ||
const { decodeToken = true, scope = '', preLogin = () => null, postLogin = () => null } = authConfig; | ||
const config = { | ||
decodeToken: decodeToken, | ||
scope: scope, | ||
preLogin: preLogin, | ||
postLogin: postLogin, | ||
...authConfig, | ||
}; | ||
(0, validateAuthConfig_1.validateAuthConfig)(config); | ||
function logOut() { | ||
setRefreshToken(null); | ||
setRefreshToken(undefined); | ||
setToken(''); | ||
setTokenExpire((0, authentication_1.timeOfExpire)(FALLBACK_EXPIRE_TIME)); | ||
setRefreshTokenExpire((0, authentication_1.timeOfExpire)(FALLBACK_EXPIRE_TIME)); | ||
setIdToken(undefined); | ||
@@ -59,13 +73,20 @@ setTokenData(undefined); | ||
function handleTokenResponse(response) { | ||
setRefreshToken(response.refresh_token); | ||
setRefreshToken(response?.refresh_token); | ||
setToken(response.access_token); | ||
setIdToken(response?.id_token || 'None'); | ||
setTokenExpire((0, authentication_1.timeOfExpire)(response.expires_in || FALLBACK_EXPIRE_TIME)); | ||
setRefreshTokenExpire((0, authentication_1.timeOfExpire)(response.refresh_token_expires_in || FALLBACK_EXPIRE_TIME)); | ||
setIdToken(response?.id_token); | ||
setLoginInProgress(false); | ||
setTokenData((0, authentication_1.decodeToken)(response.access_token)); | ||
try { | ||
if (config.decodeToken) | ||
setTokenData((0, authentication_1.decodeJWT)(response.access_token)); | ||
} | ||
catch (e) { | ||
setError(e.message); | ||
} | ||
} | ||
function refreshAccessToken() { | ||
if (refreshToken) { | ||
if (token && (0, authentication_1.tokenExpired)(token)) { | ||
// The client has an expired token. Will try to get a new one with the refreshToken | ||
(0, authentication_1.fetchWithRefreshToken)({ authConfig, refreshToken }) | ||
if (token && (0, authentication_1.tokenExpired)(tokenExpire)) { | ||
if (refreshToken && !(0, authentication_1.tokenExpired)(refreshTokenExpire)) { | ||
(0, authentication_1.fetchWithRefreshToken)({ config, refreshToken }) | ||
.then((result) => handleTokenResponse(result)) | ||
@@ -76,12 +97,12 @@ .catch((error) => { | ||
logOut(); | ||
(0, authentication_1.login)(authConfig); | ||
(0, authentication_1.logIn)(config); | ||
} | ||
}); | ||
} | ||
else { | ||
// The refresh token has expired. Need to log in from scratch. | ||
logOut(); | ||
(0, authentication_1.logIn)(config); | ||
} | ||
} | ||
else { | ||
// No refresh_token | ||
console.error('Tried to refresh access_token without a refresh_token.'); | ||
setError('Bad authorization state. Refreshing the page might solve the issue.'); | ||
} | ||
} | ||
@@ -107,3 +128,3 @@ // Register the 'check for soon expiring access token' interval (Every minute) | ||
// Request token from auth server with the auth code | ||
(0, authentication_1.fetchTokens)(authConfig) | ||
(0, authentication_1.fetchTokens)(config) | ||
.then((tokens) => { | ||
@@ -113,4 +134,4 @@ handleTokenResponse(tokens); | ||
// Call any postLogin function in authConfig | ||
if (authConfig?.postLogin) | ||
authConfig.postLogin(); | ||
if (config?.postLogin) | ||
config.postLogin(); | ||
}) | ||
@@ -125,6 +146,13 @@ .catch((error) => { | ||
setLoginInProgress(true); | ||
(0, authentication_1.login)(authConfig); | ||
(0, authentication_1.logIn)(config); | ||
} | ||
else { | ||
setTokenData((0, authentication_1.decodeToken)(token)); | ||
if (decodeToken) { | ||
try { | ||
setTokenData((0, authentication_1.decodeJWT)(token)); | ||
} | ||
catch (e) { | ||
setError(e.message); | ||
} | ||
} | ||
refreshAccessToken(); // Check if token should be updated | ||
@@ -131,0 +159,0 @@ } |
@@ -1,18 +0,19 @@ | ||
import { TAuthConfig, TTokenData, TTokenResponse } from './Types'; | ||
import { TInternalConfig, TTokenData, TTokenResponse } from './Types'; | ||
export declare const EXPIRED_REFRESH_TOKEN_ERROR_CODES: string[]; | ||
export declare function login(authConfig: TAuthConfig): Promise<void>; | ||
export declare const fetchTokens: (authConfig: TAuthConfig) => Promise<TTokenResponse>; | ||
export declare function logIn(config: TInternalConfig): Promise<void>; | ||
export declare const fetchTokens: (config: TInternalConfig) => Promise<TTokenResponse>; | ||
export declare const fetchWithRefreshToken: (props: { | ||
authConfig: TAuthConfig; | ||
config: TInternalConfig; | ||
refreshToken: string; | ||
}) => Promise<TTokenResponse>; | ||
/** | ||
* Decodes the the base64 encoded JWT. Returns a TToken. | ||
* Decodes the base64 encoded JWT. Returns a TToken. | ||
*/ | ||
export declare const decodeToken: (token: string) => TTokenData; | ||
export declare const decodeJWT: (token: string) => TTokenData; | ||
export declare const timeOfExpire: (validTimeDelta: number) => number; | ||
/** | ||
* Check if the Access Token has expired by looking at the 'exp' JWT header. | ||
* Will return True if the token has expired, OR there is less than 10min until it expires. | ||
* Check if the Access Token has expired. | ||
* Will return True if the token has expired, OR there is less than 5min until it expires. | ||
*/ | ||
export declare const tokenExpired: (token: string) => boolean; | ||
export declare function tokenExpired(tokenExpire: number): boolean; | ||
export declare const errorMessageForExpiredRefreshToken: (errorMessage: string) => boolean; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.errorMessageForExpiredRefreshToken = exports.tokenExpired = exports.decodeToken = exports.fetchWithRefreshToken = exports.fetchTokens = exports.login = exports.EXPIRED_REFRESH_TOKEN_ERROR_CODES = void 0; | ||
exports.errorMessageForExpiredRefreshToken = exports.tokenExpired = exports.timeOfExpire = exports.decodeJWT = exports.fetchWithRefreshToken = exports.fetchTokens = exports.logIn = exports.EXPIRED_REFRESH_TOKEN_ERROR_CODES = void 0; | ||
const pkceUtils_1 = require("./pkceUtils"); | ||
@@ -8,3 +8,3 @@ const codeVerifierStorageKey = 'PKCE_code_verifier'; | ||
exports.EXPIRED_REFRESH_TOKEN_ERROR_CODES = ['AADSTS700084']; | ||
async function login(authConfig) { | ||
async function logIn(config) { | ||
// Create and store a random string in localStorage, used as the 'code_verifier' | ||
@@ -18,5 +18,5 @@ const codeVerifier = (0, pkceUtils_1.generateRandomString)(40); | ||
response_type: 'code', | ||
client_id: authConfig.clientId, | ||
scope: authConfig.scope || '', | ||
redirect_uri: authConfig.redirectUri, | ||
client_id: config.clientId, | ||
scope: config.scope, | ||
redirect_uri: config.redirectUri, | ||
code_challenge: codeChallenge, | ||
@@ -26,8 +26,8 @@ code_challenge_method: 'S256', | ||
// Call any preLogin function in authConfig | ||
if (authConfig?.preLogin) | ||
authConfig.preLogin(); | ||
window.location.replace(`${authConfig.authorizationEndpoint}?${params.toString()}`); | ||
if (config?.preLogin) | ||
config.preLogin(); | ||
window.location.replace(`${config.authorizationEndpoint}?${params.toString()}`); | ||
}); | ||
} | ||
exports.login = login; | ||
exports.logIn = logIn; | ||
// This is called a "type predicate". Which allow use to know which kind of response we got, in a type safe way. | ||
@@ -41,18 +41,19 @@ function isTokenResponse(body) { | ||
body: formData, | ||
}) | ||
.then((response) => response.json().then((body) => { | ||
if (isTokenResponse(body)) { | ||
return body; | ||
}).then((response) => { | ||
if (!response.ok) { | ||
console.error(response); | ||
throw Error(response.statusText); | ||
} | ||
else { | ||
console.error(body.error_description); | ||
throw body.error_description; | ||
} | ||
})) | ||
.catch((error) => { | ||
console.error(error); | ||
throw error.message; | ||
return response.json().then((body) => { | ||
if (isTokenResponse(body)) { | ||
return body; | ||
} | ||
else { | ||
console.error(body); | ||
throw Error(body.error_description); | ||
} | ||
}); | ||
}); | ||
} | ||
const fetchTokens = (authConfig) => { | ||
const fetchTokens = (config) => { | ||
/* | ||
@@ -75,45 +76,55 @@ The browser has been redirected from the authentication endpoint with | ||
formData.append('code', authCode); | ||
formData.append('scope', authConfig.scope || ''); | ||
formData.append('client_id', authConfig.clientId); | ||
formData.append('redirect_uri', authConfig.redirectUri); | ||
formData.append('scope', config.scope); | ||
formData.append('client_id', config.clientId); | ||
formData.append('redirect_uri', config.redirectUri); | ||
formData.append('code_verifier', codeVerifier); | ||
return postWithFormData(authConfig.tokenEndpoint, formData); | ||
return postWithFormData(config.tokenEndpoint, formData); | ||
}; | ||
exports.fetchTokens = fetchTokens; | ||
const fetchWithRefreshToken = (props) => { | ||
const { authConfig, refreshToken } = props; | ||
const { config, refreshToken } = props; | ||
const formData = new FormData(); | ||
formData.append('grant_type', 'refresh_token'); | ||
formData.append('refresh_token', refreshToken); | ||
formData.append('scope', authConfig.scope || ''); | ||
formData.append('client_id', authConfig.clientId); | ||
formData.append('redirect_uri', authConfig.redirectUri); | ||
return postWithFormData(authConfig.tokenEndpoint, formData); | ||
formData.append('scope', config.scope); | ||
formData.append('client_id', config.clientId); | ||
formData.append('redirect_uri', config.redirectUri); | ||
return postWithFormData(config.tokenEndpoint, formData); | ||
}; | ||
exports.fetchWithRefreshToken = fetchWithRefreshToken; | ||
/** | ||
* Decodes the the base64 encoded JWT. Returns a TToken. | ||
* Decodes the base64 encoded JWT. Returns a TToken. | ||
*/ | ||
const decodeToken = (token) => { | ||
const base64Url = token.split('.')[1]; | ||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); | ||
const jsonPayload = decodeURIComponent(atob(base64) | ||
.split('') | ||
.map(function (c) { | ||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); | ||
}) | ||
.join('')); | ||
return JSON.parse(jsonPayload); | ||
const decodeJWT = (token) => { | ||
try { | ||
const base64Url = token.split('.')[1]; | ||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); | ||
const jsonPayload = decodeURIComponent(atob(base64) | ||
.split('') | ||
.map(function (c) { | ||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); | ||
}) | ||
.join('')); | ||
return JSON.parse(jsonPayload); | ||
} | ||
catch (e) { | ||
console.error(e); | ||
throw Error('Failed to decode the access token.\n\tIs it a proper Java Web Token?\n\t' + | ||
"You can disable JWT decoding by setting the 'decodeToken' value to 'false' the configuration."); | ||
} | ||
}; | ||
exports.decodeToken = decodeToken; | ||
exports.decodeJWT = decodeJWT; | ||
// Returns epoch time (in seconds) for when the token will expire | ||
const timeOfExpire = (validTimeDelta) => Math.round(Date.now() / 1000 + validTimeDelta); | ||
exports.timeOfExpire = timeOfExpire; | ||
/** | ||
* Check if the Access Token has expired by looking at the 'exp' JWT header. | ||
* Will return True if the token has expired, OR there is less than 10min until it expires. | ||
* Check if the Access Token has expired. | ||
* Will return True if the token has expired, OR there is less than 5min until it expires. | ||
*/ | ||
const tokenExpired = (token) => { | ||
const bufferTimeInMilliseconds = 10 * 60 * 1000; // minutes * seconds * toMilliseconds | ||
const { exp } = (0, exports.decodeToken)(token); | ||
const expirationTimeWithBuffer = new Date(exp * 1000 - bufferTimeInMilliseconds); | ||
return new Date() > expirationTimeWithBuffer; | ||
}; | ||
function tokenExpired(tokenExpire) { | ||
const now = Math.round(Date.now()) / 1000; | ||
const bufferTimeInSeconds = 5 * 60; // minutes * seconds | ||
const nowWithBuffer = now + bufferTimeInSeconds; | ||
return nowWithBuffer >= tokenExpire; | ||
} | ||
exports.tokenExpired = tokenExpired; | ||
@@ -120,0 +131,0 @@ const errorMessageForExpiredRefreshToken = (errorMessage) => { |
@@ -6,8 +6,7 @@ "use strict"; | ||
const [storedValue, setStoredValue] = (0, react_1.useState)(() => { | ||
const item = localStorage.getItem(key); | ||
try { | ||
const item = localStorage.getItem(key); | ||
return item ? JSON.parse(item) : initialValue; | ||
} | ||
catch (error) { | ||
console.log(error); | ||
return initialValue; | ||
@@ -23,3 +22,3 @@ } | ||
catch (error) { | ||
console.log(error); | ||
console.log(`Failed to store value '${value}' for key '${key}'`); | ||
} | ||
@@ -26,0 +25,0 @@ }; |
@@ -6,2 +6,11 @@ import { ReactNode } from 'react'; | ||
}; | ||
export declare type TTokenResponse = { | ||
access_token: string; | ||
scope: string; | ||
token_type: string; | ||
expires_in?: number; | ||
refresh_token?: string; | ||
refresh_token_expires_in?: number; | ||
id_token?: string; | ||
}; | ||
export interface IAuthProvider { | ||
@@ -28,9 +37,4 @@ authConfig: TAuthConfig; | ||
postLogin?: () => void; | ||
decodeToken?: boolean; | ||
}; | ||
export declare type TTokenResponse = { | ||
access_token: string; | ||
refresh_token: string; | ||
expires_in: number; | ||
id_token?: string; | ||
}; | ||
export declare type TAzureADErrorResponse = { | ||
@@ -40,1 +44,11 @@ error_description: string; | ||
}; | ||
export declare type TInternalConfig = { | ||
clientId: string; | ||
authorizationEndpoint: string; | ||
tokenEndpoint: string; | ||
redirectUri: string; | ||
scope: string; | ||
preLogin?: () => void; | ||
postLogin?: () => void; | ||
decodeToken: boolean; | ||
}; |
@@ -1,2 +0,2 @@ | ||
import { TAuthConfig } from './Types'; | ||
export declare function validateAuthConfig(authConfig: TAuthConfig): void; | ||
import { TInternalConfig } from './Types'; | ||
export declare function validateAuthConfig(config: TInternalConfig): void; |
@@ -8,12 +8,12 @@ "use strict"; | ||
} | ||
function validateAuthConfig(authConfig) { | ||
if (stringIsUnset(authConfig?.clientId)) | ||
function validateAuthConfig(config) { | ||
if (stringIsUnset(config?.clientId)) | ||
throw Error("'clientId' must be set in the 'AuthConfig' object passed to 'react-oauth2-code-pkce' AuthProvider"); | ||
if (stringIsUnset(authConfig?.authorizationEndpoint)) | ||
if (stringIsUnset(config?.authorizationEndpoint)) | ||
throw Error("'authorizationEndpoint' must be set in the 'AuthConfig' object passed to 'react-oauth2-code-pkce' AuthProvider"); | ||
if (stringIsUnset(authConfig?.tokenEndpoint)) | ||
if (stringIsUnset(config?.tokenEndpoint)) | ||
throw Error("'tokenEndpoint' must be set in the 'AuthConfig' object passed to 'react-oauth2-code-pkce' AuthProvider"); | ||
if (stringIsUnset(authConfig?.redirectUri)) | ||
if (stringIsUnset(config?.redirectUri)) | ||
throw Error("'redirectUri' must be set in the 'AuthConfig' object passed to 'react-oauth2-code-pkce' AuthProvider"); | ||
} | ||
exports.validateAuthConfig = validateAuthConfig; |
{ | ||
"name": "react-oauth2-code-pkce", | ||
"version": "1.2.5", | ||
"version": "1.3.0-alpha.1", | ||
"description": "Plug-and-play react package for OAuth2 Authorization Code flow with PKCE", | ||
@@ -16,3 +16,3 @@ "main": "dist/index.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1", | ||
"test": "ts-jest", | ||
"start": "yarn run start" | ||
@@ -19,0 +19,0 @@ }, |
@@ -16,2 +16,10 @@ # react-oauth2-pkce | ||
## Features | ||
- Authorization server agnostic, works equally well with all OAuth2 auth servers following the OAuth2 spec | ||
- Supports OpenID Connect (idTokens) | ||
- Pre- and Post login callbacks | ||
- Silently refreshes short lived access tokens in the background | ||
- Decodes JWT's | ||
## Example | ||
@@ -22,20 +30,26 @@ | ||
import ReactDOM from 'react-dom' | ||
import { AuthContext, AuthProvider } from "react-oauth2-code-pkce" | ||
import { AuthContext, AuthProvider, TAuthConfig } from "react-oauth2-code-pkce" | ||
const authConfig = { | ||
const authConfig: TAuthConfig = { | ||
clientId: 'myClientID', | ||
authorizationEndpoint: 'myAuthEndpoint', | ||
tokenEndpoint: 'myTokenEndpoint', | ||
// Where ever your application is running. Must match whats configured in authorization server | ||
// Whereever your application is running. Must match configuration on authorization server | ||
redirectUri: 'http://localhost:3000/', | ||
// Optional | ||
scope: 'someScope', | ||
scope: 'someScope openid', | ||
// Optional | ||
logoutEndpoint: '', | ||
// Optional | ||
logoutRedirect: '' | ||
logoutRedirect: '', | ||
// Example to redirect back to original path after login has completed | ||
preLogin: () => localStorage.setItem('preLoginPath', location.pathname), | ||
postLogin: () => location.replace(localStorage.getItem('preLoginPath')), | ||
// Wether or not to try and decode the access token. | ||
// Stops errors from being printed in the console for non-JWT access tokens, etc. from Github | ||
decodeToken: true | ||
} | ||
function LoginInfo() { | ||
const { tokenData, token, logOut } = useContext(AuthContext) | ||
const { tokenData, token, idToken, logOut, error } = useContext(AuthContext) | ||
@@ -72,4 +86,22 @@ return ( | ||
## Install | ||
The package is available on npmjs.com here; https://www.npmjs.com/package/react-oauth2-code-pkce | ||
```bash | ||
npm install react-oauth2-code-pkce | ||
``` | ||
and import | ||
```javascript | ||
import { AuthContext, AuthProvider } from "react-oauth2-code-pkce" | ||
``` | ||
## Develop | ||
1. Update the 'authConfig' object in `src/index.js` with config from your authorization server and application | ||
2. Install node_modules -> `$ yarn install` | ||
3. Run -> `$ yarn start` | ||
## Contribute | ||
You are welcome to create issues and pull requests :) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
23873
466
105