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
15 changes: 15 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@ module.exports = {
node: true,
browser: true
}
},
{
// SecureRoute/SecureOutlet must import the OktaContext object via the package's
// own self-import so bundlers dedupe it to the same instance <Security> provides;
// importing it from the relative path would give this file its own separate Context.
files: ['src/SecureRoute.tsx', 'src/SecureOutlet.tsx'],
rules: {
'no-restricted-imports': ['error', {
paths: [{
name: './OktaContext',
importNames: ['default'],
message: "Import OktaContext from '@okta/okta-react' instead of './OktaContext' here - see the comment above this import."
}]
}]
}
}
]
}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 7.0.0

### Breaking Changes

- `SecureRoute` and `SecureOutlet` are no longer exported from `@okta/okta-react`. Import `SecureRoute` from `@okta/okta-react/react-router-5` and `SecureOutlet` from `@okta/okta-react/react-router-6` instead. This ensures `react-router-dom` version-specific code is only pulled into your bundle if you actually use it, and avoids build-time errors from unused router APIs. Minimum supported Node version is now `12.17.0`.

# 6.11.0

### Other
Expand Down
89 changes: 80 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,10 @@ npm install --save react-router-dom # see note below
npm install --save @okta/okta-auth-js # requires at least version 5.3.1
```

> ⚠️ NOTE ⚠️<br> The [SecureRoute](#secureroute) component packaged in this SDK only works with `react-router-dom` `5.x`.
If you're using `react-router-dom` `6.x`, you'll have to write your own `SecureRoute` component.<br><br>See these [samples](https://github.com/okta/okta-react/tree/master/samples/routing) to get started
> ⚠️ NOTE ⚠️<br> The [SecureRoute](#secureroute) component only works with `react-router-dom` `5.x`, and is imported from `@okta/okta-react/react-router-5`.
If you're using `react-router-dom` `6.x` or later, use [SecureOutlet](#secureoutlet) instead, imported from `@okta/okta-react/react-router-6`.

> ⚠️ Upgrading to `7.x` ⚠️<br> As of `7.0.0`, `SecureRoute` and `SecureOutlet` are no longer exported from the `@okta/okta-react` top-level package. Update your imports to `import { SecureRoute } from '@okta/okta-react/react-router-5';` or `import { SecureOutlet } from '@okta/okta-react/react-router-6';`. This keeps `react-router-dom` version-specific code out of your bundle unless you actually use it. All other exports (`Security`, `withOktaAuth`, `useOktaAuth`, `OktaContext`, `LoginCallback`) are unaffected.

## Usage

Expand All @@ -111,10 +113,8 @@ If you're using `react-router-dom` `6.x`, you'll have to write your own `SecureR

`okta-react` provides a number of pre-built components to connect a `react-router`-based SPA to Okta OIDC information. You can use these components directly, or use them as a basis for building your own components.

- [SecureRoute](#secureroute) - A normal `Route` except authentication is needed to render the component.

> ⚠️ NOTE ⚠️<br> The [SecureRoute](#secureroute) component packaged in this SDK only works with `react-router-dom` `5.x`.
If you're using `react-router-dom` `6.x`, you'll have to write your own `SecureRoute` component.<br><br>See these [samples](https://github.com/okta/okta-react/tree/master/samples/routing) to get started
- [SecureRoute](#secureroute) - A normal `Route` except authentication is needed to render the component. Only works with `react-router-dom` `5.x`.
- [SecureOutlet](#secureoutlet) - A normal `Outlet` except authentication is needed to render the nested routes. Works with `react-router-dom` `6.x` and later.

### General components

Expand Down Expand Up @@ -154,7 +154,8 @@ This example defines 3 routes:

import React, { Component } from 'react';
import { BrowserRouter as Router, Route, withRouter } from 'react-router-dom';
import { SecureRoute, Security, LoginCallback } from '@okta/okta-react';
import { Security, LoginCallback } from '@okta/okta-react';
import { SecureRoute } from '@okta/okta-react/react-router-5';
import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js';
import Home from './Home';
import Protected from './Protected';
Expand Down Expand Up @@ -195,7 +196,8 @@ export default class extends Component {

```jsx
import React from 'react';
import { SecureRoute, Security, LoginCallback } from '@okta/okta-react';
import { Security, LoginCallback } from '@okta/okta-react';
import { SecureRoute } from '@okta/okta-react/react-router-5';
import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js';
import { BrowserRouter as Router, Route, useHistory } from 'react-router-dom';
import Home from './Home';
Expand Down Expand Up @@ -231,6 +233,51 @@ const AppWithRouterAccess = () => (
export default AppWithRouterAccess;
```

#### Creating React Router v6+ Routes with SecureOutlet

```jsx
import React from 'react';
import { Security, LoginCallback } from '@okta/okta-react';
import { SecureOutlet } from '@okta/okta-react/react-router-6';
import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js';
import { BrowserRouter as Router, Routes, Route, useNavigate } from 'react-router-dom';
import Home from './Home';
import Protected from './Protected';

const oktaAuth = new OktaAuth({
issuer: 'https://{yourOktaDomain}/oauth2/default',
clientId: '{clientId}',
redirectUri: window.location.origin + '/login/callback'
});

const App = () => {
const navigate = useNavigate();
const restoreOriginalUri = async (_oktaAuth, originalUri) => {
navigate(toRelativeUrl(originalUri || '/', window.location.origin));
};

return (
<Security oktaAuth={oktaAuth} restoreOriginalUri={restoreOriginalUri}>
<Routes>
<Route path='/' element={<Home />} />
<Route path='/login/callback' element={<LoginCallback />} />
<Route element={<SecureOutlet />}>
<Route path='/protected' element={<Protected />} />
</Route>
</Routes>
</Security>
);
};

const AppWithRouterAccess = () => (
<Router>
<App />
</Router>
);

export default AppWithRouterAccess;
```

#### Show Login and Logout Buttons (class-based)

```jsx
Expand Down Expand Up @@ -470,7 +517,7 @@ class App extends Component {

### `SecureRoute`

`SecureRoute` ensures that a route is only rendered if the user is authenticated. If the user is not authenticated, it calls [onAuthRequired](#onauthrequired) if it exists, otherwise, it redirects to Okta.
Import from `@okta/okta-react/react-router-5`. `SecureRoute` ensures that a route is only rendered if the user is authenticated. If the user is not authenticated, it calls [onAuthRequired](#onauthrequired) if it exists, otherwise, it redirects to Okta.

#### onAuthRequired

Expand All @@ -490,6 +537,30 @@ As with `Route` from `react-router-dom`, `<SecureRoute>` can take one of:
- a `render` prop that is passed a function that returns a component. This function will be passed any additional props that react-router injects (such as `history` or `match`)
- children components

### `SecureOutlet`

Import from `@okta/okta-react/react-router-6`. `SecureOutlet` is the `react-router-dom` `6.x`+ equivalent of [SecureRoute](#secureroute). It renders an `Outlet` for its nested routes only if the user is authenticated. If the user is not authenticated, it calls [onAuthRequired](#onauthrequired) if it exists, otherwise, it redirects to Okta.

Use it as the `element` of a parent `Route` that wraps the routes you want to protect:

```jsx
<Route element={<SecureOutlet />}>
<Route path='/protected' element={<Protected />} />
</Route>
```

#### onAuthRequired

`SecureOutlet` accepts `onAuthRequired` as an optional prop, it overrides [onAuthRequired](#onauthrequired) from the [Security](#security) component if exists.

#### errorComponent

`SecureOutlet` runs internal `handleLogin` process which may throw Error when `authState.isAuthenticated` is false. By default, the Error will be rendered with `OktaError` component. If you wish to customise the display of such error messages, you can pass your own component as an `errorComponent` prop to `<SecureOutlet>`. The error value will be passed to the `errorComponent` as the `error` prop.

#### loadingElement

By default, `SecureOutlet` will display nothing while the user is not yet authenticated. If you wish to customize this, you can pass your React element (not component) as `loadingElement` prop to `<SecureOutlet>`. Example: `<p>Loading...</p>`

### `LoginCallback`

`LoginCallback` handles the callback after the redirect to and back from the Okta-hosted login page. By default, it parses the tokens from the uri, stores them, then redirects to `/`. If a `SecureRoute` caused the redirect, then the callback redirects to the secured route. For more advanced cases, this component can be copied to your own source tree and modified as needed.
Expand Down
18 changes: 17 additions & 1 deletion build.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const fs = require('fs');

const NPM_DIR = `dist`;
const BUNDLE_CMD = 'yarn bundle';
const TYPES_CMD = 'yarn types';
const BANNER_CMD = `yarn banners`;

shell.echo(`Start building...`);
Expand All @@ -18,6 +19,13 @@ if (shell.exec(BUNDLE_CMD).code !== 0) {
shell.exit(1);
}

// Emit type declarations (kept separate from the rollup bundles above so the
// multiple entry points don't fight over writing .d.ts files to the same directory)
if (shell.exec(TYPES_CMD).code !== 0) {
shell.echo(chalk.red(`Error: Type declaration generation failed`));
shell.exit(1);
}

// Maintain banners
if (shell.exec(BANNER_CMD).code !== 0) {
shell.echo(chalk.red(`Error: Maintain banners failed`));
Expand All @@ -38,11 +46,19 @@ delete packageJSON.workspaces; // remove yarn workspace section

// Remove "build/" from the entrypoint paths.
['main', 'module', 'types'].forEach(function(key) {
if (packageJSON[key]) {
if (packageJSON[key]) {
packageJSON[key] = packageJSON[key].replace(`${NPM_DIR}/`, '');
}
});

['.', './react-router-5', './react-router-6'].forEach(function(name) {
['types', 'import', 'require', 'default'].forEach(function(key) {
if (packageJSON['exports'][name][key]) {
packageJSON['exports'][name][key] = packageJSON['exports'][name][key].replace(`${NPM_DIR}/`, '');
}
});
});

fs.writeFileSync(`./${NPM_DIR}/package.json`, JSON.stringify(packageJSON, null, 4));

shell.echo(chalk.green(`End building`));
6 changes: 5 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ module.exports = {
// avoid react conflict in yarn workspace
'^react$': '<rootDir>/node_modules/react',
'^react-dom$': '<rootDir>/node_modules/react-dom',
'^react-router-dom$': '<rootDir>/node_modules/react-router-dom'
'^react-router-dom$': '<rootDir>/node_modules/react-router-dom',
// resolve self-imports of OktaContext used by SecureRoute/SecureOutlet
'^@okta/okta-react$': '<rootDir>/src',
'^@okta/okta-react/react-router-5$': '<rootDir>/src/react-router-5.ts',
'^@okta/okta-react/react-router-6$': '<rootDir>/src/react-router-6.ts'
},
roots: [
'./test/jest'
Expand Down
30 changes: 28 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@okta/okta-react",
"version": "6.12.0",
"version": "7.0.0",
"description": "React support for Okta",
"private": true,
"scripts": {
Expand All @@ -16,6 +16,7 @@
"test:e2e": "yarn workspace @okta/test.e2e test",
"test:unit": "jest",
"bundle": "rollup -c",
"types": "tsc -p tsconfig.json --declaration --emitDeclarationOnly --declarationDir dist/bundles/types",
"dev": "yarn bundle --watch",
"generate": "yarn --cwd generator install && yarn --cwd generator generate"
},
Expand All @@ -29,14 +30,33 @@
"main": "dist/bundles/okta-react.cjs.js",
"module": "dist/bundles/okta-react.esm.js",
"types": "dist/bundles/types",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dist/bundles/types/index.d.ts",
"import": "./dist/bundles/okta-react.esm.js",
"require": "./dist/bundles/okta-react.cjs.js",
"default": "./dist/bundles/okta-react.umd.js"
},
"./react-router-5": {
"types": "./dist/bundles/types/react-router-5.d.ts",
"import": "./dist/bundles/okta-react-router-5.esm.js",
"require": "./dist/bundles/okta-react-router-5.cjs.js"
},
"./react-router-6": {
"types": "./dist/bundles/types/react-router-6.d.ts",
"import": "./dist/bundles/okta-react-router-6.esm.js",
"require": "./dist/bundles/okta-react-router-6.cjs.js"
}
},
"author": "",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/okta/okta-react/issues"
},
"homepage": "https://github.com/okta/okta-react#readme",
"engines": {
"node": ">=10.3",
"node": ">=12.17.0",
"yarn": "^1.7.0"
},
"resolutions": {
Expand All @@ -61,6 +81,11 @@
"react-dom": ">=16.8.0",
"react-router-dom": ">=5.1.0"
},
"peerDependenciesMeta": {
"react-router-dom": {
"optional": true
}
},
"devDependencies": {
"@babel/cli": "^7.19.3",
"@babel/core": "^7.19.3",
Expand Down Expand Up @@ -106,6 +131,7 @@
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-router-dom": "5.2.0",
"react-router-dom-v6": "npm:react-router-dom@^6.4.0",
"rollup": "^4.62.3",
"rollup-plugin-cleanup": "^3.2.1",
"rollup-plugin-terser": "^7.0.2",
Expand Down
74 changes: 71 additions & 3 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const makeExternalPredicate = () => {
const externalArr = [
...Object.keys(pkg.peerDependencies || {}),
...Object.keys(pkg.dependencies || {}),
'@okta/okta-react',
];

if (externalArr.length === 0) {
Expand All @@ -26,10 +27,13 @@ const extensions = ['js', 'jsx', 'ts', 'tsx'];

const input = 'src/index.ts';
const external = makeExternalPredicate();

// Type declarations are emitted separately (see `yarn types`, a single whole-program
// `tsc --emitDeclarationOnly` pass) rather than by this plugin, so multiple entry points
// below can share one `typescript()` instance without fighting over declaration output.
const commonPlugins = [
typescript({
typescript: ts,
useTsconfigDeclarationDir: true
typescript: ts
}),
replace({
values: {
Expand All @@ -45,7 +49,7 @@ const commonPlugins = [
delimiters: ['\\b', '\\b'],
preventAssignment: true
}),
cleanup({
cleanup({
extensions,
comments: 'none'
})
Expand Down Expand Up @@ -112,5 +116,69 @@ export default [
sourcemap: true
}
]
},
{
input: 'src/react-router-5.ts',
external,
plugins: [
...commonPlugins,
babel({
babelHelpers: 'runtime',
presets: [
'@babel/preset-env',
'@babel/preset-react'
],
plugins: [
'@babel/plugin-transform-runtime'
],
extensions
}),
],
output: [
{
format: 'cjs',
file: 'dist/bundles/okta-react-router-5.cjs.js',
exports: 'named',
sourcemap: true
},
{
format: 'esm',
file: 'dist/bundles/okta-react-router-5.esm.js',
exports: 'named',
sourcemap: true
}
]
},
{
input: 'src/react-router-6.ts',
external,
plugins: [
...commonPlugins,
babel({
babelHelpers: 'runtime',
presets: [
'@babel/preset-env',
'@babel/preset-react'
],
plugins: [
'@babel/plugin-transform-runtime'
],
extensions
}),
],
output: [
{
format: 'cjs',
file: 'dist/bundles/okta-react-router-6.cjs.js',
exports: 'named',
sourcemap: true
},
{
format: 'esm',
file: 'dist/bundles/okta-react-router-6.esm.js',
exports: 'named',
sourcemap: true
}
]
}
];
Loading