From 40a82cc8cb2f34de0b6b2ef9e60ccb8ace0f5a60 Mon Sep 17 00:00:00 2001 From: Jordan Melberg Date: Wed, 28 Mar 2018 15:55:28 -0700 Subject: [PATCH] :package: Prepping 1.0.0 react (#171) BREAKING CHANGE --- packages/okta-react/README.md | 73 ++++++++++--------- packages/okta-react/src/Auth.js | 19 +++-- .../test/e2e/harness/e2e/App.test.js | 12 ++- .../harness/e2e/page-objects/protected.po.js | 13 +++- .../test/e2e/harness/protractor.conf.js | 2 +- .../okta-react/test/e2e/harness/src/Home.js | 15 +++- .../test/e2e/harness/src/Protected.js | 25 ++++++- 7 files changed, 107 insertions(+), 52 deletions(-) diff --git a/packages/okta-react/README.md b/packages/okta-react/README.md index f87c8e81b..2ca5b783c 100644 --- a/packages/okta-react/README.md +++ b/packages/okta-react/README.md @@ -169,11 +169,14 @@ export default withAuth(class MessageList extends Component { Security is the top-most component of okta-react. This is where most of the configuration is provided. #### Configuration options -* **issuer** (required) - The OpenId Connect `issuer` -* **client_id** (required) - The OpenId Connect `client_id` -* **redirect_uri** (required) - Where the callback handler is hosted -* **onAuthRequired** (optional) -* **auth** (optional) - Provide an Auth object instead of the options above. This is helpful when integrating `okta-react` with external libraries that need access to the tokens. + +- **issuer** (required) - The OpenId Connect `issuer` +- **client_id** (required) - The OpenId Connect `client_id` +- **redirect_uri** (required) - Where the callback handler is hosted +- **scope** *(optional)*: Reserved or custom claims to be returned in the tokens +- **response_type** *(optional)*: Desired token grant types +- **onAuthRequired** (optional) +- **auth** (optional) - Provide an Auth object instead of the options above. This is helpful when integrating `okta-react` with external libraries that need access to the tokens. Accepts a callback to make a decision when authentication is required. If this is not supplied, `okta-react` redirects to Okta. This callback will receive `auth` and `history` parameters. This is triggered when: 1. `auth.login` is called @@ -259,60 +262,60 @@ export default App; `auth` provides methods that allow managing tokens and auth state. All of the methods return Promises. -* `auth.isAuthenticated()` +#### `auth.isAuthenticated()` - Returns `true` or `false`, depending on whether the user has an active access or id token. +Returns `true` or `false`, depending on whether the user has an active access or id token. -* `auth.getUser()` +#### `auth.getUser()` - Returns the result of the userinfo endpoint if an access token exists. +Returns the result of the OpenID Connect `/userinfo` endpoint if an access token exists. -* `auth.getIdToken()` +#### `auth.getIdToken()` - Retrieves the id token from storage if it exists. +Retrieves the id token from storage if it exists. -* `auth.getAccessToken()` +#### `auth.getAccessToken()` - Retrieves the access token from storage if it exists. +Retrieves the access token from storage if it exists. -* `auth.login()` +#### `auth.login(fromUri)` - Calls `onAuthRequired` or redirects to Okta if `onAuthRequired` is undefined. +Calls `onAuthRequired` or redirects to Okta if `onAuthRequired` is undefined. This method accepts a `fromUri` parameter to push the user to after successful authentication. -* `auth.logout()` +#### `auth.logout(uri)` - Removes all the tokens and redirects to `/`. +Terminates the user's session in Okta and clears all stored tokens. Accepts an optional `uri` parameter to push the user to after logout. -* `auth.redirect({sessionToken})` +#### `auth.redirect({sessionToken})` - Performs a redirect to Okta with an optional `sessionToken`. +Performs a redirect to Okta with an optional `sessionToken`. - Example: - ```typescript - auth.redirect({ - sessionToken: '{sampleSessionToken}' - }); - ``` +```typescript +auth.redirect({ + sessionToken: '{sampleSessionToken}' +}); +``` -* `auth.handleAuthentication()` +#### `auth.handleAuthentication()` - Parses tokens from the url and stores them. +Parses tokens from the url and stores them. ## Development + 1. Clone the repo: - - `git clone git@github.com:okta/okta-oidc-js.git` + - `git clone git@github.com:okta/okta-oidc-js.git` 2. Install the dependencies with lerna (install with `npm i lerna -g`): - - `lerna bootstrap` + - `lerna bootstrap` 3. Navigate into the `okta-react` package: - - `cd packages/okta-react` + - `cd packages/okta-react` 4. Make your changes to `okta-react/src/` 5. Set the following environment variables: - - `ISSUER` - your authorization server - - `CLIENT_ID` - the client id of your app - - `USERNAME` - username of org user, needed if you want to run tests - - `PASSWORD` - password of org user, needed if you want to run tests + - `ISSUER` - your authorization server + - `CLIENT_ID` - the client id of your app + - `USERNAME` - username of org user, needed if you want to run tests + - `PASSWORD` - password of org user, needed if you want to run tests 6. Start a sample server: - - `npm start` + - `npm start` ## Commands diff --git a/packages/okta-react/src/Auth.js b/packages/okta-react/src/Auth.js index 90ee10392..5768a2ed6 100644 --- a/packages/okta-react/src/Auth.js +++ b/packages/okta-react/src/Auth.js @@ -58,7 +58,16 @@ export default class Auth { async getUser() { const accessToken = this._oktaAuth.tokenManager.get('accessToken'); - return accessToken ? this._oktaAuth.token.getUserInfo(accessToken) : undefined; + const idToken = this._oktaAuth.tokenManager.get('idToken'); + if (accessToken && idToken) { + const userinfo = await this._oktaAuth.token.getUserInfo(accessToken); + if (userinfo.sub === idToken.claims.sub) { + // Only return the userinfo response if subjects match to + // mitigate token substitution attacks + return userinfo + } + } + return idToken ? idToken.claims : undefined; } async getIdToken() { @@ -71,8 +80,8 @@ export default class Auth { return accessToken ? accessToken.accessToken : undefined; } - async login() { - localStorage.setItem('secureRouterReferrerPath', this._history.location.pathname); + async login(fromUri) { + localStorage.setItem('secureRouterReferrerPath', fromUri || this._history.location.pathname); if (this._config.onAuthRequired) { const auth = this; const history = this._history; @@ -81,10 +90,10 @@ export default class Auth { await this.redirect(); } - async logout() { + async logout(path) { this._oktaAuth.tokenManager.clear(); await this._oktaAuth.signOut(); - this._history.push('/'); + this._history.push(path || '/'); } async redirect({sessionToken} = {}) { diff --git a/packages/okta-react/test/e2e/harness/e2e/App.test.js b/packages/okta-react/test/e2e/harness/e2e/App.test.js index 561dcd23a..f6ee4b303 100644 --- a/packages/okta-react/test/e2e/harness/e2e/App.test.js +++ b/packages/okta-react/test/e2e/harness/e2e/App.test.js @@ -42,6 +42,12 @@ describe('React + Okta App', () => { protectedPage.waitUntilVisible(); expect(protectedPage.getLogoutButton().isPresent()).toBeTruthy(); + protectedPage.waitForElement('userinfo-container'); + protectedPage.getUserInfo().getText() + .then(userInfo => { + expect(userInfo).toContain('email'); + }); + // Logout protectedPage.getLogoutButton().click(); @@ -62,11 +68,11 @@ describe('React + Okta App', () => { password: process.env.PASSWORD }); - appPage.waitUntilVisible(); - expect(appPage.getLogoutButton().isPresent()).toBeTruthy(); + protectedPage.waitUntilVisible(); + expect(protectedPage.getLogoutButton().isPresent()).toBeTruthy(); // Logout - appPage.getLogoutButton().click(); + protectedPage.getLogoutButton().click(); appPage.waitUntilLoggedOut(); }); diff --git a/packages/okta-react/test/e2e/harness/e2e/page-objects/protected.po.js b/packages/okta-react/test/e2e/harness/e2e/page-objects/protected.po.js index ef7ccad56..b64cfa4eb 100644 --- a/packages/okta-react/test/e2e/harness/e2e/page-objects/protected.po.js +++ b/packages/okta-react/test/e2e/harness/e2e/page-objects/protected.po.js @@ -18,11 +18,12 @@ export class ProtectedPage { } waitUntilVisible() { - browser.wait(ExpectedConditions.presenceOf(this.getHeader()), 5000); + browser.wait(ExpectedConditions.urlContains('/protected'), 5000); } - - getHeader() { - return element(by.tagName('h3')); + + waitForElement(id) { + const el = element(by.id(id)); + browser.wait(ExpectedConditions.presenceOf(el), 5000); } getLogoutButton() { @@ -32,4 +33,8 @@ export class ProtectedPage { getLoginButton() { return element(by.id('login-button')); } + + getUserInfo() { + return element(by.id('userinfo-container')); + } } diff --git a/packages/okta-react/test/e2e/harness/protractor.conf.js b/packages/okta-react/test/e2e/harness/protractor.conf.js index cf037ad66..978745f86 100644 --- a/packages/okta-react/test/e2e/harness/protractor.conf.js +++ b/packages/okta-react/test/e2e/harness/protractor.conf.js @@ -20,7 +20,7 @@ exports.config = { capabilities: { 'browserName': 'chrome', chromeOptions: { - args: ['--headless','--disable-gpu','--window-size=1600x1200', '--no-sandbox'] + args: ['--headless', '--disable-gpu', '--window-size=1600x1200', '--no-sandbox'] } }, directConnect: true, diff --git a/packages/okta-react/test/e2e/harness/src/Home.js b/packages/okta-react/test/e2e/harness/src/Home.js index 7ab0d086f..a1f746def 100644 --- a/packages/okta-react/test/e2e/harness/src/Home.js +++ b/packages/okta-react/test/e2e/harness/src/Home.js @@ -24,6 +24,9 @@ export default withAuth(class Home extends Component { this.checkAuthentication = this.checkAuthentication.bind(this); this.checkAuthentication(); + + this.login = this.login.bind(this); + this.logout = this.logout.bind(this); } async checkAuthentication() { @@ -33,6 +36,14 @@ export default withAuth(class Home extends Component { } } + async login() { + this.props.auth.login('/protected'); + } + + async logout() { + this.props.auth.logout('/'); + } + componentDidUpdate() { this.checkAuthentication(); } @@ -43,8 +54,8 @@ export default withAuth(class Home extends Component { } const button = this.state.authenticated ? - : - ; + : + ; return (
diff --git a/packages/okta-react/test/e2e/harness/src/Protected.js b/packages/okta-react/test/e2e/harness/src/Protected.js index 4f2566798..9966ff9a5 100644 --- a/packages/okta-react/test/e2e/harness/src/Protected.js +++ b/packages/okta-react/test/e2e/harness/src/Protected.js @@ -10,6 +10,27 @@ * See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { Component } from 'react'; +import { withAuth } from '@okta/okta-react'; -export default () =>

Protected

; +export default withAuth(class Protected extends Component { + constructor(props) { + super(props); + this.state = { userinfo: null }; + } + + async componentDidMount() { + const claims = await this.props.auth.getUser(); + const userinfo = JSON.stringify(claims, null, 4); + this.setState({ userinfo }); + } + + render() { + return ( +
+
Protected!
+ {this.state.userinfo &&
 {this.state.userinfo} 
} +
+ ); + } +});