成果物
サイトトップ
「ログイン」 -> Oktaのサイイン画面
サイトトップに戻り(リダイレクト)、Oktaのユーザー情報が表示される。
環境
- React
- ^18.2.0
- react-router-dom
- ^6.9.0
- @okta/okta-react
- ^6.7.0
- @okta/okta-auth-js
- ^7.2.0
※主なもののみ抜粋
想定する読者
- Oktaのみでログイン機能を実装してみたい人
- 次に紹介する「ハマったポイント」にハマっている人
※Okta+CognitoでReactみたいな事をしたい方は↓↓
ハマったポイント
idTokenに含まれるユーザー情報が少ない問題
事象
OktaユーザーのProfile情報が全て含まれている事を期待していたが違った。
例えば下記OktaユーザーのFirst nameとLast nameを取得したかったがidTokenの中に無かった。
解決方法
サイイン後にgetUserInfo()
という関数を呼ぶ必要があった。
OktaのAPIを叩く際にCORSではねられる問題
事象
解決方法
Oktaの設定画面からSecurity > API > Trusted OriginsでCORSの許可設定をする。
何やらWarningが出てる問題(動くものの)
事象
Two custom restoreOriginalUri callbacks are detected. The one from the OktaAuth configuration will be overridden by the provided restoreOriginalUri prop from the Security component.
解決方法
ReactのStrictModeでコンポーネントが2回描画される事に起因する問題だった。上記リンクを参考に、useEffectのクリーンアップ関数でゴニョゴニョする。
Oktaの設定
基本設定
okta developerトップ -> 左メニュー「Applications」 -> 「Create App Integration」
- [Sign-in method]
- OIDC - OpenID Connect
- [Application type]
- Single-Page Application
- [App integration name]
- My React App(※任意でOK)
- [Grant type]
- Authorization Code
- [Sign-in redirect URIs]
- http://localhost:3000/login/callback
- [Sign-out redirect URIs]
- http://localhost:3000
- Base URIs
- 空でOK
- Assignments
- Skip group assignment for now
ユーザーをアサインする
上記で作ったサイインの仕組みを使えるOktaユーザーをアサインします。
「My React App」 -> 「Assignments」タブ -> 「Assign」ボタン -> 「Assign to People」
アカウントに紐付いたユーザー一覧が出てくるので、ユーザーを「Assign」します。Assignしたユーザーが、これから作るReactアプリにログイン出来るユーザーになります。
↓「Assign」後のイメージ↓
CORSの許可
OktaのAPIを叩けるドメインの設定をします。
「ハマったポイント」で紹介した「サインアウト時に下記エラーが発生してしまった」問題の原因です。
これをしないとOktaのAPIを上手く叩けずにアプリケーション側(React側)でエラーが吐き出されます。
左メニュー「Security」 -> 「API」 -> 「Trusted Origins」 タブ -> 「+ Add origin」
- [Origin name]
- localhost:3000
- [Origin URL]
- http://localhost:3000
- [Choose Type]
- 下記2つをチェック
- Cross-Origin Resource Sharing(CORS)
- Redirect
- 下記2つをチェック
Oktaの設定は以上になります。
Reactアプリの作成
create-react-appで作成します。
VITEなど他のツールで作成する場合は、ポートが変わるので先ほど「Add Origin」で設定したコールバックURLの変更が必要になりますので注意してください。
ご自身の作業ディレクトリでReactプロジェクトを作成します。
npx create-react-app react-okta-20230326
追加ライブラリ
上記で作成したReactプロジェクトに必要なライブラリを追加していきます。
yarn add @okta/okta-react @okta/okta-auth-js react-router-dom
ディレクトリ構成
完成品のディレクトリ構成です。
.
├── node_modules/
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── config
│ │ └── auth.js
│ ├── hooks
│ │ └── use-auth.js
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── reportWebVitals.js
│ └── setupTests.js
└── yarn.lock
実装
下記手順で進めます。
- config/auth.jsを作成
- hooks/use-auth.jsを作成
- index.jsを編集
- App.jsを編集
- App.cssを編集
config/auth.jsを作成
Client ID
アカウントのドメイン
// 例)0123456789abcdefghij
const CLIENT_ID = 'Client ID';
// 例)https://dev-12345678.okta.com/oauth2/default
const ISSUER = 'https://アカウントのドメイン/oauth2/default';
const REDIRECT_URI = `${window.location.origin}/login/callback`;
// eslint-disable-next-line
export default {
oidc: {
clientId: CLIENT_ID,
issuer: ISSUER,
redirectUri: REDIRECT_URI,
scopes: [
"openid",
"email",
"profile",
],
pkce: true,
}
};
hooks/use-auth.jsを作成
import React, { createContext, useContext, useEffect, useState } from 'react';
import { useOktaAuth } from '@okta/okta-react';
import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js';
import config from '../config/auth';
export const oktaAuth = new OktaAuth(config.oidc);
const authContext = createContext({});
export const ProvideAuth = ({children}) => {
const auth = useProvideAuth();
return (
<authContext.Provider value={auth}>
{children}
</authContext.Provider>
)
;
}
export const useAuth = () => {
return useContext(authContext);
}
const useProvideAuth = () => {
const { authState } = useOktaAuth();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [username, setUserName] = useState('ゲスト');
const [user, setUser] = useState(null);
useEffect(() => {
if(!authState || !authState.isAuthenticated) return;
console.log(authState);
oktaAuth.token.getUserInfo()
.then(e => {
setUser(e);
setUserName(e.name);
setIsAuthenticated(true);
});
}, [authState]);
const signIn = () => {
const originalUri = toRelativeUrl(window.location.href, window.location.origin);
oktaAuth.setOriginalUri(originalUri);
oktaAuth.signInWithRedirect();
};
const signOut = async () => {
oktaAuth.signOut();
setUser(null);
setIsAuthenticated(false);
};
return {
isAuthenticated,
user,
username,
signIn,
signOut
}
}
ポイントはuseProvideAuth
のuseEffect
でoktaAuth.token.getUserInfo()
を使ってユーザー情報を取得している点です。
「ハマったポイント」で挙げた「idTokenに含まれるユーザー情報が少ない問題」の解決方法になります。
とは言え、idTokenにもユーザー名やemailなど基本的な情報は入っているので、それで事足りる場合は、上記関数を呼ばずともidTokenから取得出来ます。
ちなみに、Oktaでユーザー情報の追加は、 左メニュー「Directory」->「People」からユーザーを選択後、「Profile」タブで出来ます。
index.jsを編集
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
App.jsを編集
import './App.css';
import { useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { toRelativeUrl } from '@okta/okta-auth-js';
import { Security } from '@okta/okta-react';
import { Routes, Route } from 'react-router-dom';
import { LoginCallback } from '@okta/okta-react';
import { useAuth, oktaAuth, ProvideAuth } from './hooks/use-auth';
const Profile = () => {
const { isAuthenticated, user, username, signIn, signOut } = useAuth();
return (
<>
<main className='App'>
<p>ようこそ、{ username }さん</p>
{
isAuthenticated ? (
<>
<ul>
{
Object.keys(user)
.filter(key => key !== 'headers')
.map(key => (
<li key={key}> {key} : {user[key]} </li>
))
}
</ul>
<button onClick={() => signOut()}>ログアアウト</button>
</>
) : (
<button onClick={() => signIn()}>ログイン</button>
)
}
</main>
</>
);
}
const App = () => {
const navigate = useNavigate();
const restoreOriginalUri = useCallback((_oktaAuth, originalUri) => {
navigate(toRelativeUrl(originalUri || '/', window.location.origin));
}, [navigate])
useEffect(() => {
return () => {
oktaAuth.options.restoreOriginalUri = undefined
}
}, [])
return (
<>
<Security oktaAuth={oktaAuth} restoreOriginalUri={restoreOriginalUri}>
<ProvideAuth>
<Routes>
<Route path="/" exact={true} element={<Profile />}/>
<Route path="login/callback" element={<LoginCallback />}/>
</Routes>
</ProvideAuth>
</Security>
</>
);
};
export default App;
ポイントはuseEffect
でクリーンアップ関数を設定している点です。
「ハマったポイント」で挙げた「何やらWarningが出てる問題(動くものの)」の解決方法になります。
App.cssを編集
.App {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
button {
font-size: calc(10px + 2vmin);
}
p {
margin-bottom: 0;
}
ul {
list-style: none;
font-size: 16px;
border: 1px solid #fff;
padding: 1em;
margin: 1em 0;
}
以上です。
http://localhost:3000にアクセスすると下図画面が表示されるはずです。
ログインする際は「Oktaの設定」>「ユーザーをアサインする」でアサインしたユーザーでログインして下さい。
ログイン成功すると下記のように自分の情報が表示されるはずです。
おわりに
「idTokenに含まれるユーザー情報が少ない問題」の解決はちょっと大変でした。
休日に手を動かしていると、どうしても楽しようとしてしまい、何も考えずにアレやこれやと手を動かしてしまう、結果だいぶ時間を溶かした気がします。
休日でも( 業務中でも )公式ドキュメントをサボらずに読む強い心が欲しいですー。