|
1 | | -Review the simple password-only sign-in use case from the sample app. This use case is outlined in the following sequence diagram with your single-page app (SPA) as the client: |
| 1 | +Review the simple password-only sign-in use case from the sample app. |
2 | 2 |
|
| 3 | +<!-- The sequence diagram below is out of date. It shows the old Step Mode |
| 4 | +flow: a single idx.authenticate(username, password) call. The current flow |
| 5 | +instead primes and submits each remediation step in turn with proceed({ |
| 6 | +step }). This comment hides the diagram until the design team updates it. |
| 7 | +See OKTA-1220704 for the tracking ticket. |
3 | 8 | <div class="full"> |
4 | 9 |
|
5 | 10 |  |
6 | 11 |
|
7 | 12 | </div> |
| 13 | +--> |
8 | 14 |
|
9 | 15 | ### Set up the Okta configuration settings |
10 | 16 |
|
11 | | -Review the `src/config.js` file that references the required [app integration settings](#app-integration-settings) to initialize your Okta Auth JS instance. The `config.js` file references the values that you add to the `testenv` file. |
| 17 | +Review the `src/config.js` file that references the required [app integration settings](#app-integration-settings) to initialize your Okta Auth JS instance. The `config.js` file references the values that you add to the `.env` file. |
12 | 18 |
|
13 | 19 | ```JavaScript |
14 | | -const CLIENT_ID = process.env.SPA_CLIENT_ID || process.env.CLIENT_ID || '{clientId}'; |
15 | | -const ISSUER = process.env.ISSUER || 'https://{yourOktaDomain}/oauth2/default'; |
16 | | -const REDIRECT_URI = `{window.location.origin}/login/callback`; |
| 20 | +const CLIENT_ID = import.meta.env.VITE_CLIENT_ID || '{clientId}'; |
| 21 | +const ISSUER = import.meta.env.VITE_ISSUER || 'https://{yourOktaDomain}/oauth2/default'; |
| 22 | +const REDIRECT_URI = `${window.location.origin}/login/callback`; |
17 | 23 |
|
18 | | -// eslint-disable-next-line import/no-anonymous-default-export |
19 | 24 | export default { |
20 | 25 | clientId: CLIENT_ID, |
21 | 26 | issuer: ISSUER, |
22 | 27 | redirectUri: REDIRECT_URI, |
23 | 28 | scopes: ['openid', 'offline_access', 'profile', 'email'], |
| 29 | + pkce: true |
24 | 30 | }; |
25 | 31 | ``` |
26 | 32 |
|
| 33 | +> **Note:** Okta Auth JS 8.x requires every `idx.proceed()` call to name the remediation step it's submitting. This guide uses that Step Mode pattern throughout. See [Step Mode vs Legacy Mode](https://github.com/okta/okta-auth-js/blob/master/docs/idx.md#step-mode-vs-legacy-mode) in the Auth JS SDK docs if you're migrating an older app that relies on the deprecated, generic remediation-driven pattern. |
| 34 | +
|
27 | 35 | ### Instantiate the Okta Auth JS client |
28 | 36 |
|
29 | | -Review the React `app.js` file that imports the required libraries and instantiates the Okta Auth JS client with values from the `config.js`. |
| 37 | +Review the React `app.js` file that imports the required libraries and instantiates the Okta Auth JS client with values from the `config.js`. Wrap your app in the `Security` component from the Okta React SDK so it can manage the Auth JS instance for you. |
30 | 38 |
|
31 | 39 | ```JavaScript |
32 | 40 | import { useEffect, useState } from 'react'; |
33 | 41 | import { useHistory } from 'react-router-dom'; |
34 | | -import { OktaAuth, IdxStatus, urlParamsToObject, hasErrorInUrl } from '@okta/okta-auth-js'; |
| 42 | +import { OktaAuth, IdxStatus, toRelativeUrl } from '@okta/okta-auth-js'; |
| 43 | +import { Security } from '@okta/okta-react'; |
35 | 44 | import { formTransformer } from './formTransformer'; |
36 | 45 | import oidcConfig from './config'; |
37 | 46 | import './App.css'; |
38 | 47 |
|
39 | | -function createOktaAuthInstance() { |
40 | | - const { state } = urlParamsToObject(window.location.search); |
41 | | - return new OktaAuth(Object.assign({}, oidcConfig, { |
42 | | - state |
43 | | - })); |
| 48 | +const oktaAuth = new OktaAuth(oidcConfig); |
| 49 | + |
| 50 | +... |
| 51 | + |
| 52 | +function App() { |
| 53 | + const history = useHistory(); |
| 54 | + const restoreOriginalUri = async (_oktaAuth, originalUri) => { |
| 55 | + history.replace(toRelativeUrl(originalUri || '/', window.location.origin)); |
| 56 | + }; |
| 57 | + |
| 58 | + return ( |
| 59 | + <Security oktaAuth={oktaAuth} restoreOriginalUri={restoreOriginalUri}> |
| 60 | + ... |
| 61 | + </Security> |
| 62 | + ); |
44 | 63 | } |
| 64 | +``` |
45 | 65 |
|
46 | | -const oktaAuth = createOktaAuthInstance(); |
| 66 | +> **Note:** This guide's simplified example doesn't cover redirect callbacks (for example, social IdP sign-in or email magic links). If your app needs them, see [Redirect callbacks](https://github.com/okta/okta-auth-js/blob/master/docs/idx.md#redirect-callbacks) in the Auth JS SDK docs and the `LoginCallback` component in [Okta's reference sample app](https://github.com/okta/okta-auth-js/tree/master/samples/generated/react-embedded-auth-with-sdk). |
47 | 67 |
|
48 | | -... |
| 68 | +### Start the sign-in transaction |
| 69 | + |
| 70 | +Before you can render a sign-in form, you need an in-progress IDX transaction to drive it. Start one when your component mounts by calling `idx.start()`. `idx.start()` begins the transaction. It doesn't resolve a step's field data until you name that step. Call `idx.proceed()` with only a `step` name and no other values. The SDK then returns that step's `inputs` for rendering. For this password-only use case, the sign-in flow always begins with the `identify` step, which asks for `username`: |
| 71 | + |
| 72 | +```JavaScript |
| 73 | +const [transaction, setTransaction] = useState(null); |
| 74 | + |
| 75 | +useEffect(() => { |
| 76 | + const startTransaction = async () => { |
| 77 | + await oktaAuth.idx.start(); |
| 78 | + const newTransaction = await oktaAuth.idx.proceed({ step: 'identify' }); |
| 79 | + setTransaction(newTransaction); |
| 80 | + }; |
| 81 | + startTransaction(); |
| 82 | +}, []); |
49 | 83 | ``` |
50 | 84 |
|
| 85 | +Pass `transaction.nextStep` into `formTransformer` to render the form fields for the current step. See [Basic sign-in flow](#basic-sign-in-flow) for the full form code. Okta's Identity Engine collects the username first. It then challenges for the password on a separate step. This password-only flow therefore renders two forms in sequence, not one combined form. |
| 86 | + |
51 | 87 | ### Handle the password authentication |
52 | 88 |
|
53 | | -Review the `apps.js` file for details on handling a successful password authentication by receiving the `SUCCESS` status and storing the returned tokens: |
| 89 | +Name the step you're submitting on every `idx.proceed()` call. `transaction.nextStep.name` holds that name. It matches the step the form already rendered. Submitting a step's values reveals the name of the next step. It does not reveal that step's renderable field data. Call `idx.proceed()` again with just the step name to prime the next form before you render it. Review the `app.js` file for details on handling a successful password authentication. It receives the `SUCCESS` status and stores the returned tokens: |
54 | 90 |
|
55 | 91 | ```JavaScript |
56 | | -.... |
57 | | - |
58 | 92 | const handleSubmit = async e => { |
59 | | - e.preventDefault(); |
| 93 | + e.preventDefault(); |
60 | 94 |
|
61 | | - const newTransaction = await oktaAuth.idx.proceed(inputValues); // inputValues = username, password |
62 | | - console.log('Transaction:', newTransaction); |
| 95 | + const submittedTransaction = await oktaAuth.idx.proceed({ step: transaction.nextStep.name, ...inputValues }); |
| 96 | + console.log('Transaction:', submittedTransaction); |
| 97 | + setInputValues({}); |
63 | 98 |
|
64 | | - setInputValues({}); |
65 | | - if (newTransaction.status === IdxStatus.SUCCESS) { |
66 | | - oktaAuth.tokenManager.setTokens(newTransaction.tokens); |
67 | | - } else { |
| 99 | + if (submittedTransaction.status === IdxStatus.SUCCESS) { |
| 100 | + oktaAuth.tokenManager.setTokens(submittedTransaction.tokens); |
| 101 | + return; |
| 102 | + } |
| 103 | + |
| 104 | + const newTransaction = await oktaAuth.idx.proceed({ step: submittedTransaction.nextStep.name }); |
| 105 | + setTransaction(newTransaction); |
| 106 | +}; |
| 107 | +``` |
| 108 | + |
| 109 | +For this password-only use case, `handleSubmit` runs twice. The first run submits `username` from the `identify` step and primes the `challenge-authenticator` step. The second run submits `password` from that step and reaches `IdxStatus.SUCCESS`. |
| 110 | + |
| 111 | +### The full sign-in component code |
| 112 | + |
| 113 | +With the pieces above in place, here's the complete `App.jsx` for the password-only sign-in flow, with everything shown together: |
| 114 | + |
| 115 | +```JavaScript |
| 116 | +import { useEffect, useState } from 'react'; |
| 117 | +import { useHistory } from 'react-router-dom'; |
| 118 | +import { OktaAuth, IdxStatus, toRelativeUrl } from '@okta/okta-auth-js'; |
| 119 | +import { Security } from '@okta/okta-react'; |
| 120 | +import { formTransformer } from './formTransformer'; |
| 121 | +import oidcConfig from './config'; |
| 122 | +import './App.css'; |
| 123 | + |
| 124 | +const oktaAuth = new OktaAuth(oidcConfig); |
| 125 | + |
| 126 | +function SignInForm() { |
| 127 | + const [transaction, setTransaction] = useState(null); |
| 128 | + const [inputValues, setInputValues] = useState({}); |
| 129 | + |
| 130 | + useEffect(() => { |
| 131 | + const startTransaction = async () => { |
| 132 | + await oktaAuth.idx.start(); |
| 133 | + const newTransaction = await oktaAuth.idx.proceed({ step: 'identify' }); |
68 | 134 | setTransaction(newTransaction); |
| 135 | + }; |
| 136 | + startTransaction(); |
| 137 | + }, []); |
| 138 | + |
| 139 | + const handleChange = ({ target: { name, value } }) => { |
| 140 | + setInputValues({ ...inputValues, [name]: value }); |
| 141 | + }; |
| 142 | + |
| 143 | + const handleSubmit = async e => { |
| 144 | + e.preventDefault(); |
| 145 | + |
| 146 | + const submittedTransaction = await oktaAuth.idx.proceed({ step: transaction.nextStep.name, ...inputValues }); |
| 147 | + console.log('Transaction:', submittedTransaction); |
| 148 | + setInputValues({}); |
| 149 | + |
| 150 | + if (submittedTransaction.status === IdxStatus.SUCCESS) { |
| 151 | + oktaAuth.tokenManager.setTokens(submittedTransaction.tokens); |
| 152 | + return; |
69 | 153 | } |
| 154 | + |
| 155 | + const newTransaction = await oktaAuth.idx.proceed({ step: submittedTransaction.nextStep.name }); |
| 156 | + setTransaction(newTransaction); |
| 157 | + }; |
| 158 | + |
| 159 | + if (!transaction?.nextStep) { |
| 160 | + return null; |
| 161 | + } |
| 162 | + |
| 163 | + const { inputs } = formTransformer(transaction.nextStep)({}); |
| 164 | + |
| 165 | + return ( |
| 166 | + <form onSubmit={handleSubmit}> |
| 167 | + {inputs.map(({ label, name, type, required }) => ( |
| 168 | + <label key={name}> |
| 169 | + {label} |
| 170 | + <input |
| 171 | + name={name} |
| 172 | + type={type} |
| 173 | + required={required} |
| 174 | + value={inputValues[name] || ''} |
| 175 | + onChange={handleChange} |
| 176 | + /> |
| 177 | + </label> |
| 178 | + ))} |
| 179 | + <button type="submit">Sign in</button> |
| 180 | + </form> |
| 181 | + ); |
| 182 | +} |
| 183 | + |
| 184 | +function App() { |
| 185 | + const history = useHistory(); |
| 186 | + const restoreOriginalUri = async (_oktaAuth, originalUri) => { |
| 187 | + history.replace(toRelativeUrl(originalUri || '/', window.location.origin)); |
70 | 188 | }; |
71 | 189 |
|
72 | | - ... |
73 | | - ``` |
| 190 | + return ( |
| 191 | + <Security oktaAuth={oktaAuth} restoreOriginalUri={restoreOriginalUri}> |
| 192 | + <SignInForm /> |
| 193 | + </Security> |
| 194 | + ); |
| 195 | +} |
| 196 | + |
| 197 | +export default App; |
| 198 | +``` |
0 commit comments