Skip to content

Commit d448ab2

Browse files
Decouple Security from react-router [OKTA-333775]
- Added required prop `restoreOriginalUri` to `Security` - Updated readme with setting `restoreOriginalUri` callback for `react-router`
1 parent b9e628f commit d448ab2

File tree

8 files changed

+184
-59
lines changed

8 files changed

+184
-59
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# 5.0.0
2+
3+
### Breaking Changes
4+
5+
- [#71](https://github.com/okta/okta-react/pull/71) Adds required prop `restoreOriginalUri` to `Security` that will override `restoreOriginalUri` callback of `oktaAuth`
6+
17
# 4.1.0
28

39
### Other

README.md

Lines changed: 89 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -121,40 +121,51 @@ This example defines 3 routes:
121121
// src/App.js
122122

123123
import React, { Component } from 'react';
124-
import { BrowserRouter as Router, Route } from 'react-router-dom';
124+
import { BrowserRouter as Router, Route, withRouter } from 'react-router-dom';
125125
import { SecureRoute, Security, LoginCallback } from '@okta/okta-react';
126-
import { OktaAuth } from '@okta/okta-auth-js';
126+
import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js';
127127
import Home from './Home';
128128
import Protected from './Protected';
129129

130-
const oktaAuth = new OktaAuth({
131-
issuer: 'https://{yourOktaDomain}.com/oauth2/default',
132-
clientId: '{clientId}',
133-
redirectUri: window.location.origin + '/login/callback'
134-
});
135130
class App extends Component {
131+
constructor(props) {
132+
super(props);
133+
this.oktaAuth = new OktaAuth({
134+
issuer: 'https://{yourOktaDomain}.com/oauth2/default',
135+
clientId: '{clientId}',
136+
redirectUri: window.location.origin + '/login/callback'
137+
});
138+
this.restoreOriginalUri = async (_oktaAuth, originalUri) => {
139+
props.history.replace(toRelativeUrl(originalUri, window.location.origin));
140+
};
141+
}
142+
136143
render() {
137144
return (
138-
<Router>
139-
<Security oktaAuth={oktaAuth}>
140-
<Route path='/' exact={true} component={Home}/>
141-
<SecureRoute path='/protected' component={Protected}/>
142-
<Route path='/login/callback' component={LoginCallback} />
143-
</Security>
144-
</Router>
145+
<Security oktaAuth={this.oktaAuth} restoreOriginalUri={this.restoreOriginalUri} >
146+
<Route path='/' exact={true} component={Home} />
147+
<SecureRoute path='/protected' component={Protected} />
148+
<Route path='/login/callback' component={LoginCallback} />
149+
</Security>
145150
);
146151
}
147152
}
148153

149-
export default App;
154+
const AppWithRouterAccess = withRouter(App);
155+
export default class extends Component {
156+
render() {
157+
return (<Router><AppWithRouterAccess/></Router>);
158+
}
159+
}
150160
```
151161

152162
#### Creating React Router Routes with function-based components
153163

154164
```jsx
155165
import React from 'react';
156166
import { SecureRoute, Security, LoginCallback } from '@okta/okta-react';
157-
import { OktaAuth } from '@okta/okta-auth-js';
167+
import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js';
168+
import { useHistory } from 'react-router-dom';
158169
import Home from './Home';
159170
import Protected from './Protected';
160171

@@ -163,15 +174,23 @@ const oktaAuth = new OktaAuth({
163174
clientId: '{clientId}',
164175
redirectUri: window.location.origin + '/login/callback'
165176
});
166-
const App = () => (
167-
<Router>
168-
<Security oktaAuth={oktaAuth}>
169-
<Route path='/' exact={true} component={Home}/>
170-
<SecureRoute path='/protected' component={Protected}/>
171-
<Route path='/login/callback' component={LoginCallback} />
172-
</Security>
173-
</Router>
174-
);
177+
178+
const App = () => {
179+
const history = useHistory();
180+
const restoreOriginalUri = async (_oktaAuth, originalUri) => {
181+
history.replace(toRelativeUrl(originalUri, window.location.origin));
182+
};
183+
184+
return (
185+
<Router>
186+
<Security oktaAuth={oktaAuth} restoreOriginalUri={restoreOriginalUri}>
187+
<Route path='/' exact={true} component={Home} />
188+
<SecureRoute path='/protected' component={Protected} />
189+
<Route path='/login/callback' component={LoginCallback} />
190+
</Security>
191+
</Router>
192+
);
193+
};
175194

176195
export default App;
177196
```
@@ -338,6 +357,10 @@ export default MessageList = () => {
338357

339358
*(required)* The pre-initialized [oktaAuth][Okta Auth SDK] instance. See [Configuration Reference](https://github.com/okta/okta-auth-js#configuration-reference) for details of how to initialize the instance.
340359

360+
#### restoreOriginalUri
361+
362+
*(required)* Callback function. Called to restore original () during [oktaAuth.handleLoginRedirect()](https://github.com/okta/okta-auth-js#handleloginredirecttokens) is called. Will override [restoreOriginalUri option of oktaAuth](https://github.com/okta/okta-auth-js#restoreoriginaluri)
363+
341364
#### onAuthRequired
342365

343366
*(optional)* Callback function. Called when authentication is required. If this is not supplied, `okta-react` redirects to Okta. This callback will receive [oktaAuth][Okta Auth SDK] instance as the first function parameter. This is triggered when a [SecureRoute](#secureroute) is accessed without authentication. A common use case for this callback is to redirect users to a custom login route when authentication is required for a [SecureRoute](#secureroute).
@@ -346,7 +369,7 @@ export default MessageList = () => {
346369

347370
```jsx
348371
import { useHistory } from 'react-router-dom';
349-
import { OktaAuth } from '@okta/okta-auth-js';
372+
import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js';
350373

351374
const oktaAuth = new OktaAuth({
352375
issuer: 'https://{yourOktaDomain}.com/oauth2/default',
@@ -363,10 +386,15 @@ export default App = () => {
363386
history.push('/login');
364387
};
365388

389+
const restoreOriginalUri = async (_oktaAuth, originalUri) => {
390+
history.replace(toRelativeUrl(originalUri, window.location.origin));
391+
};
392+
366393
return (
367394
<Security
368395
oktaAuth={oktaAuth}
369396
onAuthRequired={customAuthHandler}
397+
restoreOriginalUri={restoreOriginalUri}
370398
>
371399
<Route path='/login' component={CustomLoginComponent}>
372400
{/* some routes here */}
@@ -395,8 +423,8 @@ class App extends Component {
395423
render() {
396424
return (
397425
<Router>
398-
<Security oktaAuth={oktaAuth}>
399-
<Route path='/' exact={true} component={Home}/>
426+
<Security oktaAuth={oktaAuth} restoreOriginalUri={restoreOriginalUri}>
427+
<Route path='/' exact={true} component={Home} />
400428
<Route path='/login/callback' component={LoginCallback} />
401429
</Security>
402430
</Router>
@@ -452,6 +480,39 @@ export default MyComponent = () => {
452480

453481
## Migrating between versions
454482

483+
### Migrating from 4.x to 5.x
484+
485+
From version 5.0, the Security component explicitly requires prop [restoreOriginalUri](#restoreoriginaluri) to decouple from `react-router`.
486+
Example of implementation of this callback for `react-router`:
487+
488+
```jsx
489+
import { Security } from '@okta/okta-react';
490+
import { useHistory } from 'react-router-dom';
491+
import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js';
492+
493+
const oktaAuth = new OktaAuth({
494+
issuer: 'https://{yourOktaDomain}.com/oauth2/default',
495+
clientId: '{clientId}',
496+
redirectUri: window.location.origin + '/login/callback'
497+
});
498+
499+
export default App = () => {
500+
const history = useHistory();
501+
const restoreOriginalUri = async (_oktaAuth, originalUri) => {
502+
history.replace(toRelativeUrl(originalUri, window.location.origin));
503+
};
504+
505+
return (
506+
<Security
507+
oktaAuth={oktaAuth}
508+
restoreOriginalUri={restoreOriginalUri}
509+
>
510+
{/* some routes here */}
511+
</Security>
512+
);
513+
};
514+
```
515+
455516
### Migrating from 3.x to 4.x
456517

457518
#### Updating the Security component

src/OktaContext.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { AuthState, OktaAuth } from '@okta/okta-auth-js';
1414

1515
export type OnAuthRequiredFunction = (oktaAuth: OktaAuth) => Promise<void> | void;
1616

17+
export type RestoreOriginalUriFunction = (oktaAuth: OktaAuth, originalUri: string) => Promise<void> | void;
18+
1719
export interface IOktaContext {
1820
oktaAuth: OktaAuth;
1921
authState: AuthState;

src/Security.tsx

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,21 @@
1111
*/
1212

1313
import * as React from 'react';
14-
import { useHistory } from 'react-router-dom';
15-
import { toRelativeUrl, AuthSdkError, OktaAuth } from '@okta/okta-auth-js';
16-
import OktaContext, { OnAuthRequiredFunction } from './OktaContext';
14+
import { AuthSdkError, OktaAuth } from '@okta/okta-auth-js';
15+
import OktaContext, { OnAuthRequiredFunction, RestoreOriginalUriFunction } from './OktaContext';
1716
import OktaError from './OktaError';
1817

1918
const Security: React.FC<{
20-
oktaAuth: OktaAuth,
19+
oktaAuth: OktaAuth,
20+
restoreOriginalUri: RestoreOriginalUriFunction,
2121
onAuthRequired?: OnAuthRequiredFunction,
2222
children?: React.ReactNode
2323
} & React.HTMLAttributes<HTMLDivElement>> = ({
24-
oktaAuth,
24+
oktaAuth,
25+
restoreOriginalUri,
2526
onAuthRequired,
26-
children
27+
children
2728
}) => {
28-
const history = useHistory();
2929
const [authState, setAuthState] = React.useState(() => {
3030
if (!oktaAuth) {
3131
return {
@@ -39,16 +39,17 @@ const Security: React.FC<{
3939
});
4040

4141
React.useEffect(() => {
42-
if (!oktaAuth) {
42+
if (!oktaAuth || !restoreOriginalUri) {
4343
return;
4444
}
4545

4646
// Add default restoreOriginalUri callback
47-
if (!oktaAuth.options.restoreOriginalUri) {
48-
oktaAuth.options.restoreOriginalUri = async (_, originalUri) => {
49-
history.replace(toRelativeUrl(originalUri, window.location.origin));
50-
};
47+
if (oktaAuth.options.restoreOriginalUri && restoreOriginalUri) {
48+
console.warn('Two custom restoreOriginalUri callbacks are detected. The one from the OktaAuth configuration will be overridden by the provided restoreOriginalUri prop from the Security component.');
5149
}
50+
oktaAuth.options.restoreOriginalUri = async (oktaAuth: any, originalUri: string) => {
51+
restoreOriginalUri(oktaAuth, originalUri);
52+
};
5253

5354
// Add okta-react userAgent
5455
oktaAuth.userAgent = `${process.env.PACKAGE_NAME}/${process.env.PACKAGE_VERSION} ${oktaAuth.userAgent}`;
@@ -64,13 +65,18 @@ const Security: React.FC<{
6465
}
6566

6667
return () => oktaAuth.authStateManager.unsubscribe();
67-
}, [oktaAuth, history]);
68+
}, [oktaAuth, restoreOriginalUri]);
6869

6970
if (!oktaAuth) {
7071
const err = new AuthSdkError('No oktaAuth instance passed to Security Component.');
7172
return <OktaError error={err} />;
7273
}
7374

75+
if (!restoreOriginalUri) {
76+
const err = new AuthSdkError('No restoreOriginalUri callback passed to Security Component.');
77+
return <OktaError error={err} />;
78+
}
79+
7480
return (
7581
<OktaContext.Provider value={{
7682
oktaAuth,

test/e2e/harness/src/App.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import * as React from 'react';
1414
import { Route, Switch, useHistory } from 'react-router-dom';
15-
import { OktaAuth } from '@okta/okta-auth-js';
15+
import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js';
1616
import { Security, LoginCallback, SecureRoute } from '@okta/okta-react';
1717
import Home from './Home';
1818
import Protected from './Protected';
@@ -29,11 +29,16 @@ const App: React.FC<{
2929
history.push('/login');
3030
};
3131

32+
const restoreOriginalUri = async (_oktaAuth: OktaAuth, originalUri: string) => {
33+
history.replace(toRelativeUrl(originalUri, window.location.origin));
34+
};
35+
3236
return (
3337
<React.StrictMode>
3438
<Security
3539
oktaAuth={oktaAuth}
3640
onAuthRequired={customLogin ? onAuthRequired : undefined}
41+
restoreOriginalUri={restoreOriginalUri}
3742
>
3843
<Switch>
3944
<Route path='/login' component={CustomLogin}/>

test/jest/loginCallback.test.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ describe('<LoginCallback />', () => {
1919
let oktaAuth;
2020
let authState;
2121
let mockProps;
22+
const restoreOriginalUri = async (_, url) => {
23+
location.href = url;
24+
};
2225
beforeEach(() => {
2326
authState = {
2427
isPending: true
@@ -33,7 +36,10 @@ describe('<LoginCallback />', () => {
3336
isLoginRedirect: jest.fn().mockImplementation(() => false),
3437
handleLoginRedirect: jest.fn()
3538
};
36-
mockProps = { oktaAuth };
39+
mockProps = {
40+
oktaAuth,
41+
restoreOriginalUri
42+
};
3743
});
3844

3945
it('renders the component', () => {

test/jest/secureRoute.test.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@
1313
import * as React from 'react';
1414
import { mount } from 'enzyme';
1515
import { act } from 'react-dom/test-utils';
16-
import { MemoryRouter, Route } from 'react-router-dom';
16+
import { MemoryRouter, Route, RouteProps } from 'react-router-dom';
1717
import SecureRoute from '../../src/SecureRoute';
1818
import Security from '../../src/Security';
1919

2020
describe('<SecureRoute />', () => {
2121
let oktaAuth;
2222
let authState;
2323
let mockProps;
24+
const restoreOriginalUri = async (_, url) => {
25+
location.href = url;
26+
};
2427

2528
beforeEach(() => {
2629
authState = {
@@ -39,7 +42,10 @@ describe('<SecureRoute />', () => {
3942
signInWithRedirect: jest.fn(),
4043
setOriginalUri: jest.fn()
4144
};
42-
mockProps = { oktaAuth };
45+
mockProps = {
46+
oktaAuth,
47+
restoreOriginalUri
48+
};
4349
});
4450

4551
describe('With changing authState', () => {
@@ -305,7 +311,8 @@ describe('<SecureRoute />', () => {
305311
</MemoryRouter>
306312
);
307313
const secureRoute = wrapper.find(SecureRoute);
308-
expect(secureRoute.find(Route).props().exact).toBe(true);
314+
const props: RouteProps = secureRoute.find(Route).props();
315+
expect(props.exact).toBe(true);
309316
expect(wrapper.find(MyComponent).html()).toBe('<div>hello world</div>');
310317
});
311318

@@ -322,7 +329,8 @@ describe('<SecureRoute />', () => {
322329
</MemoryRouter>
323330
);
324331
const secureRoute = wrapper.find(SecureRoute);
325-
expect(secureRoute.find(Route).props().strict).toBe(true);
332+
const props: RouteProps = secureRoute.find(Route).props();
333+
expect(props.strict).toBe(true);
326334
expect(wrapper.find(MyComponent).html()).toBe('<div>hello world</div>');
327335
});
328336

@@ -339,7 +347,8 @@ describe('<SecureRoute />', () => {
339347
</MemoryRouter>
340348
);
341349
const secureRoute = wrapper.find(SecureRoute);
342-
expect(secureRoute.find(Route).props().sensitive).toBe(true);
350+
const props: RouteProps = secureRoute.find(Route).props();
351+
expect(props.sensitive).toBe(true);
343352
expect(wrapper.find(MyComponent).html()).toBe('<div>hello world</div>');
344353
});
345354

0 commit comments

Comments
 (0)