Skip to content
This repository has been archived by the owner on Jan 26, 2025. It is now read-only.

Commit

Permalink
Add: react hooks (#677)
Browse files Browse the repository at this point in the history
* feat(context): migrate to new context api and use hooks

- migrate to React.createContext api, fix #428
- provide useOkta hook api, fix #429

* updates README to include hook usage

* updates README

* Updates to React Hooks; allows non-react-router routers

* feat(context): migrate to new context api and use hooks

- migrate to React.createContext api, fix #428
- provide useOkta hook api, fix #429

* updates README to include hook usage

* updates README

* Updates to React Hooks; allows non-react-router routers

* Updates typos and build errors

* reduces dependency on react-router, updates tests (still broken)

* Aaron WIP

* Aaron WIP (#681)

Co-authored-by: Aaron Granick <[email protected]>

* updates internal eventId/Count to _subscriberXXX

* fix e2e

* fix error messages from unit tests

* Updates unit tests, changes error handling

* removes FIXME

* replaces Auth with AuthService and merges hooks into useOktaAuth

* updates docs/tests.  Adds error rendering

* adds race management (#682)

* Proposal for handling updating AuthState while handleAuthentication waiting
* isolating update pending to updateAuthState
* remembers to clear authStateUpdatePending
* prevents overlapping calls to handleAuthentication from confusing things
* Adds getAuthState()
* adds small fixes, tests (#683)
Co-authored-by: Aaron Granick <[email protected]>

* cleans up README, removes misleading returns

* fixes handleAuthentication to check getAuthState

* Updates README per PR comments

Co-authored-by: Sibelius Seraphini <[email protected]>
Co-authored-by: Aaron Granick <[email protected]>
  • Loading branch information
3 people authored Mar 3, 2020
1 parent 3b450d4 commit ccc33b5
Show file tree
Hide file tree
Showing 26 changed files with 5,805 additions and 3,399 deletions.
9 changes: 6 additions & 3 deletions packages/okta-react/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ module.exports = {
'eslint:recommended',
'plugin:react/recommended'
],
parser: "babel-eslint",
parser: 'babel-eslint',
plugins: [
"react"
'react',
'react-hooks'
],
env: {
browser: true,
es6: true,
jest: true
},
rules: {
'react/prop-types': 0
'react/prop-types': 0,
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn'
},
settings: {
react: {
Expand Down
27 changes: 27 additions & 0 deletions packages/okta-react/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
# 2.0.0

### Features

- Now offers synchronous access to the authentication state (after the first asynchronous determination)
- Now offers the following React Hook (2.x requires React 16.8+)
- `useOktaAuth`
- Now can be used with other routers than react-router
- React Router 5 continues to be supported, but is now optional
- Routers other than React-Router will have to write their own version of `LoginCallback` component

### Breaking Changes
- Requires React 16.8+
- If using react-router, requires react-router 5+
- See the `Migration from 1.x to 2.0` section of the README for details on migrating your applications
- `Auth.js` and the `auth` parameter to `<Security>` have been renamed to `AuthService.js` and `authService`
- `<ImplicitCallback>` has been replaced with `<LoginCallback>`
- `auth.IsAuthenticated()` has been removed
- instead use the `.isAuthenticated` property of the `authState` object
- `withAuth` has been replaced with `withOktaAuth`, which gives slightly different parameters
- provides `authService` instead of `auth`
- also provides the `authState` object
- the arguments passed to the optional `onAuthRequired()` callback provided to the `<Security>` component have changed
- error handling for authentication is now handled by putting the error into the `authState.error` property
- `auth.setFromUri()` is now `authService.setFromUri()` and is passed a string (instead of an object)
- `auth.getFromUri()` is now `authService.getFromUri()` and returns a string (instead of an object)

# 1.4.1

### Bug Fixes
Expand Down
401 changes: 296 additions & 105 deletions packages/okta-react/README.md

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions packages/okta-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@okta/okta-react",
"version": "1.4.1",
"version": "2.0.0",
"description": "React support for Okta",
"main": "./dist/index.js",
"scripts": {
Expand Down Expand Up @@ -38,8 +38,8 @@
"prop-types": "^15.5.10"
},
"peerDependencies": {
"react": ">=15",
"react-router": ">=4"
"react": ">=16.8.0",
"react-router-dom": ">=5.1.0"
},
"devDependencies": {
"babel-cli": "^6.26.0",
Expand All @@ -56,15 +56,16 @@
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-jsx-a11y": "^6.0.2",
"eslint-plugin-react": "^7.3.0",
"eslint-plugin-react-hooks": "^2.5.0",
"eslint-watch": "^3.1.2",
"jasmine-core": "~2.6.2",
"jasmine-spec-reporter": "~4.1.0",
"jest": "^23.6.0",
"polished": "^1.7.0",
"protractor": "^5.4.2",
"react": "^16.4.2",
"react-dom": "^16.4.2",
"react-router-dom": "^4.2.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-router-dom": "^5.1.0",
"rimraf": "^2.6.2",
"styled-components": "^2.1.2",
"webdriver-manager": "^12.1.4"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ import OktaAuth from '@okta/okta-auth-js';

import packageInfo from './packageInfo';

const containsAuthTokens = /id_token|access_token|code/;

export default class Auth {
class AuthService {
constructor(config) {
const testing = {
// If the config is undefined, cast it to false
Expand All @@ -43,45 +41,124 @@ export default class Auth {
this._oktaAuth = new OktaAuth(authConfig);
this._oktaAuth.userAgent = `${packageInfo.name}/${packageInfo.version} ${this._oktaAuth.userAgent}`;
this._config = authConfig; // use normalized config
this._history = config.history;
this._listeners = {};
this._pending = {}; // manage overlapping async calls

this.handleAuthentication = this.handleAuthentication.bind(this);
this.isAuthenticated = this.isAuthenticated.bind(this);
this.updateAuthState = this.updateAuthState.bind(this);
this.clearAuthState = this.clearAuthState.bind(this);
this.emitAuthState = this.emitAuthState.bind(this);
this.getAuthState = this.getAuthState.bind(this);
this.getUser = this.getUser.bind(this);
this.getIdToken = this.getIdToken.bind(this);
this.getAccessToken = this.getAccessToken.bind(this);
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
this.redirect = this.redirect.bind(this);
this.emit = this.emit.bind(this);
this.on = this.on.bind(this);

this._subscriberCount = 0;

this.clearAuthState();
}

getTokenManager() {
return this._oktaAuth.tokenManager;
}

async handleAuthentication() {
let tokens = await this._oktaAuth.token.parseFromUrl();
tokens = Array.isArray(tokens) ? tokens : [tokens];
for (let token of tokens) {
if (token.idToken) {
this._oktaAuth.tokenManager.add('idToken', token);
} else if (token.accessToken) {
this._oktaAuth.tokenManager.add('accessToken', token);
}
if(this._pending.handleAuthentication) {
// Don't trigger second round
return null;
}
try {
this._pending.handleAuthentication = true;
let tokens = await this._oktaAuth.token.parseFromUrl();
tokens = Array.isArray(tokens) ? tokens : [tokens];

for (let token of tokens) {
if (token.idToken) {
this._oktaAuth.tokenManager.add('idToken', token);
} else if (token.accessToken) {
this._oktaAuth.tokenManager.add('accessToken', token);
}
}
await this.updateAuthState();
const authState = this.getAuthState();
if(authState.isAuthenticated) {
const location = this.getFromUri();
window.location.assign(location);
}
this._pending.handleAuthentication = null;
} catch(error) {
this._pending.handleAuthentication = null;
this.emitAuthState({
isAuthenticated: false,
error,
idToken: null,
accessToken: null,
});
}
return;
}

clearAuthState(state={}) {
this.emitAuthState({ ...AuthService.DEFAULT_STATE, ...state });
return;
}

emitAuthState(state) {
this._authState = state;
this.emit('authStateChange', this.getAuthState());
return;
}

getAuthState() {
return this._authState;
}

async isAuthenticated() {
// Support a user-provided method to check authentication
if (this._config.isAuthenticated) {
return (this._config.isAuthenticated)();
async updateAuthState() {
// avoid concurrent updates
if( this._pending.authStateUpdate ) {
return this._pending.authStateUpdate.promise;
}

// If there could be tokens in the url
if (location && location.hash && containsAuthTokens.test(location.hash)) return null;
// create a promise to return in case of multiple parallel requests
this._pending.authStateUpdate = {};
this._pending.authStateUpdate.promise = new Promise( (resolve) => {
// Promise can only resolve any error is in the resolve value
// and uncaught exceptions make Front SDKs angry
this._pending.authStateUpdate.resolve = resolve;
});
// copy to return after emitAuthState has cleared the pending object
const authStateUpdate = this._pending.authStateUpdate;

// Return true if either the access or id token exist in client storage
return !!(await this.getAccessToken()) || !!(await this.getIdToken());
try {
const accessToken = await this.getAccessToken();
const idToken = await this.getIdToken();

// Use external check, or default to isAuthenticated if either the access or id token exist
const isAuthenticated = this._config.isAuthenticated ? await this._config.isAuthenticated() : !! ( accessToken || idToken );


this._pending.authStateUpdate = null;
this.emitAuthState({
isAuthenticated,
idToken,
accessToken,
});
} catch (error) {
this._pending.authStateUpdate = null;
this.emitAuthState({
isAuthenticated: false,
error,
idToken: null,
accessToken: null,
});
}
authStateUpdate.resolve();
return authStateUpdate.promise;
}

async getUser() {
Expand All @@ -92,7 +169,7 @@ export default class Auth {
if (userinfo.sub === idToken.claims.sub) {
// Only return the userinfo response if subjects match to
// mitigate token substitution attacks
return userinfo
return userinfo;
}
}
return idToken ? idToken.claims : undefined;
Expand Down Expand Up @@ -126,9 +203,7 @@ export default class Auth {
// Save the current url before redirect
this.setFromUri(fromUri); // will save current location if fromUri is undefined
if (this._config.onAuthRequired) {
const auth = this;
const history = this._history;
return this._config.onAuthRequired({ auth, history });
return this._config.onAuthRequired(this);
}
return this.redirect(additionalParams);
}
Expand All @@ -140,11 +215,20 @@ export default class Auth {
path = options;
options = {};
}

return this._oktaAuth.signOut(options)
.then(() => {
if (!options.postLogoutRedirectUri && !this._config.postLogoutRedirectUri) {
this._history.push(path || '/');
let redirectUri = path || '/';
// If a relative path was passed, convert to absolute URI
if (redirectUri.charAt(0) === '/') {
redirectUri = window.location.origin + redirectUri;
}
window.location.assign(redirectUri);
}
})
.finally( () => {
this.clearAuthState();
});
}

Expand All @@ -164,21 +248,44 @@ export default class Auth {
return this._oktaAuth.token.getWithRedirect(params);
}

setFromUri (fromUri) {
// Use current history location if fromUri was not passed
const referrerPath = fromUri
? { pathname: fromUri }
: this._history.location;
localStorage.setItem(
'secureRouterReferrerPath',
JSON.stringify(referrerPath)
);
setFromUri(fromUri) {
// Use current location if fromUri was not passed
fromUri = fromUri || window.location.href;
// If a relative path was passed, convert to absolute URI
if (fromUri.charAt(0) === '/') {
fromUri = window.location.origin + fromUri;
}
localStorage.setItem( 'secureRouterReferrerPath', fromUri );
}

getFromUri () {
getFromUri() {
const referrerKey = 'secureRouterReferrerPath';
const location = JSON.parse(localStorage.getItem(referrerKey) || '{ "pathname": "/" }');
const location = localStorage.getItem(referrerKey) || window.location.origin;
localStorage.removeItem(referrerKey);
return location;
}

on( event, callback ) {
const subscriberId = this._subscriberCount++;
this._listeners[event] = this._listeners[event] || {};
this._listeners[event][subscriberId] = callback;
return () => {
delete this._listeners[event][subscriberId];
}
}

emit(event, message ) {
this._listeners[event] = this._listeners[event] || {};
Object.values(this._listeners[event]).forEach( listener => listener(message) );
}

}

AuthService.DEFAULT_STATE = {
isPending: true,
isAuthenticated: null,
idToken: null,
accessToken: null,
};

export default AuthService;
47 changes: 0 additions & 47 deletions packages/okta-react/src/ImplicitCallback.js

This file was deleted.

Loading

0 comments on commit ccc33b5

Please sign in to comment.