OktaとCognitoを使ってReactでログイン機能を実装する

# 成果物
サイトトップ
![completion_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/e9c3552b-f56f-2877-3340-72a5ecdee7c7.png)
「ログイン」→CognitoUI→Oktaログイン画面
![completion_02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/e60bdd8f-e99e-5b3e-9100-117e0cb7f4ad.jpeg)
サイトトップに戻り（リダイレクト）、Oktaのユーザー情報が表示される。
![completion_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/48c3b65e-891c-0e52-a83d-12bc0f5bf58a.png)



# 想定する読者

- 初めてSSOするウェブアプリを作る（React）
- ↑をOKTAとCognitoで作りたい
- とりあえず動くものを作りたい

まさに自分がこの読者像でした。

成果物に辿り着くまで、色んな記事の色んなサンプルコードを組み合わせたりと苦労したので、１箇所にまとめてみようというモチベーションです。

# OktaとAWS Cognitoを連携させる

物凄く長くなったので、別記事にししました。

[OktaとAWS Cognito連携 ~SAML編~](https://qiita.com/gossan/items/074b1cc4653ab0be7e43)

上記で作成したOkta✖️Cognitoを使う想定で進めます。

# Cognitoの設定を変更する

↑で作ったCognitoの設定を一部変更します。

[変更点]

- 許可されているコールバックURLを変更
    -  ローカルPCのReactで使うので*http\://localhost:3000*に設定
- 許可されているサインアウトURLを追加
    -  同様の理由で*http\://localhost:3000*に設定
- OAuth2.0許可タイプを「認証コード付与」に変更
    -  「暗黙的な付与」のままだとログイン後リダイレクトする際に、URLのパラメーターに認証コードが付与されてしまいます。
    -  横取りされる危険があり、世の中的には「使ってくれるなよ」という流れのようです。
- OpenID 接続スコープに「aws.cognito.signin.user.admin」を追加
    - ログイン後に受け取るレスポンスに`attributes`という項目が追加されます。
    - `attributes`にユーザー情報が入っています。
    - ちなみにこの設定をしないでもIDトークンからユーザー情報の取得は出来ます。ただ、ネストが深い、、、

マネコンでCognitoに移動して作成したユーザープールを選択する。

「アプリケーションの統合」タブを選択。
![c_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/ccb1f99d-e70c-de38-6b89-9aa2f168e775.png)
スクロールして作成したアプリケーションクライアントを選択する。
![c_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/5afe5904-78aa-3743-0ea4-eb25d5265ece.png)
アプリケーションクライアントの画面に遷移する。
![c_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/e36caa05-e392-37e4-827c-cfd8f6c0d00b.png)
「ホストされたUI」の項目までスクロールし「編集」する。
![c_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/34df0c95-6414-f252-b891-0a90434d10cc.png)
- [許可されているコールバック URL]
    - http\://localhost:3000
- [許可されているサインアウト URL - オプション]
    - http\://localhost:3000

![c_5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/ff5de350-8e45-8c69-5909-2aba01dcf1cd.png)
- [OAuth 2.0 許可タイプ]
    - 認証コード付与
- [OAuth 2.0 許可タイプ]
    - OpenID
    - aws.cognito.signin.user.admin
    
![スクリーンショット 2023-03-12 9.43.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/70519dad-1e3c-ab47-1ce7-cfeb49da3c09.png)
「変更を保存」します。

# OktaでCORSの許可設定をする

OktaのAPIを*https\://localhost:3000*から叩けるように許可してあげる必要があります。

Oktaの管理画面に入り左側のメニューから Security > API > Trusted Originsタブを開きます。
![スクリーンショット 2023-03-21 16.54.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/2a18cbcf-d839-f46b-19bf-4026e44160fa.png)

「＋ Add Origin」ボタンをクリックし下記のように入力して「Save」します。

- [Origin name]
    - http\://localhost:3000
- [Origin URL]
    - http\://localhost:3000
- 下記２項目をチェック
    - Cross-Origin Resource Sharing(CORS)
    - Redirect

![スクリーンショット 2023-03-21 17.05.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/576df72d-cb58-a6c1-2f75-ed3ce78011d2.png)
↓
![スクリーンショット 2023-03-21 17.05.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/5cf888c5-643d-9728-7f78-753025047fd9.png)


# Reactアプリの作成
create-react-appで作成します。

VITEなど他のツールで作成する場合は、ポートが変わるので上記で設定したコールバックURLの変更が必要になりますので注意してください。

ご自身の作業ディレクトリでReactプロジェクトを作成します。
```
npx create-react-app auth-app-20230312
```
## 追加ライブラリ

`aws-amplify`のみです。

このライブラリの中からCognitoのサインイン・サインアウトのための関数などを使って行きます。

```
yarn add aws-amplify
```

## ディレクトリ構成

完成品のディレクトリ構成です。

```
.
├── node_modules/
├── 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
```
## 実装
下記手順で進めます。
1. config/auth.jsを作成
1. hooks/use-auth.jsを作成
1. index.jsを編集
1. App.jsを編集
1. App.cssを編集

### config/auth.jsを作成

```javascript:config/auth.js
// 例) ap-northeast-1
const CognitoRegion = 'リージョン'; 
//例) ap-northeast-1_ACCDEFGHI
const CognitoUserPool = 'ユーザープール ID'; 
// 例) 12345abcdefg67890hijklmnop
const CognitoUserPoolClient = 'クライアント ID'; 

// 例) okta-sso-20230301
// Cognitoのマネコン > アプリケーションの統合 > ドメイン
const CognitoDomainPrefix = 'クライアントのドメインの頭の部分'; 

const config = {
  region: CognitoRegion,
  userPoolId: CognitoUserPool,
  userPoolWebClientId : CognitoUserPoolClient,
  oauth: {
    domain: `${CognitoDomainPrefix}.auth.${CognitoRegion}.amazoncognito.com`,
    scope: ['openid','aws.cognito.signin.user.admin'],
    redirectSignIn: 'http://localhost:3000',
    redirectSignOut: 'http://localhost:3000',
    responseType: 'code',
  }
}

export default config;
```

ちなみにscopeやredirectSignInがCognitoで設定した情報と一致しない場合は、上手く動かないので注意して下さい。

### hooks/use-auth.jsを作成

```javascript:hooks/use-auth.js
import { Amplify, Auth } from 'aws-amplify';
import React, { createContext, useContext, useEffect, useState } from 'react';
import AuthConfig  from '../config/auth';

Amplify.configure({ Auth: AuthConfig });

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 [isLoading, setIsLoading ] = useState(true);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [username, setUsrname] = useState('ゲスト');
  const [user, setUser] = useState(null);

  useEffect(() => {
    Auth.currentAuthenticatedUser()
      .then(result => {
        console.log(result);
        setUser(result.attributes);
        setUsrname(result.attributes.nickname);
        setIsAuthenticated(true);
        setIsLoading(false);
      })
      .catch(() => {
        setIsAuthenticated(false);
        setIsLoading(false);
      });

  }, []);

  const signIn = () => {
    Auth.federatedSignIn();
  };

  const signOut = async () => {
    try {
      await Auth.signOut();
      setUsrname('');
      setIsAuthenticated(false);
    } catch (error) {
      return {
        success: false,
        message: 'ログアウトに失敗しました。'
      }
    }
  };

  return {
    isLoading,
    isAuthenticated,
    user,
    username,
    signIn,
    signOut,
  }

}
```

### index.jsを編集
`hooks/use-auth.js`の中で作成したcontextを使えるよう設定しています。

```javascript:index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { ProvideAuth } from './hooks/use-auth';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <ProvideAuth>
      <App />
    </ProvideAuth>
  </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を編集
```javascript:App.js
import { useAuth } from './hooks/use-auth';
import './App.css';

function App() {

  const { isAuthenticated, username, signIn, signOut } = useAuth();

  return (
    <>
      <main className='App'>
        <p>ようこそ、{ username }さん</p>
        {
          isAuthenticated ? (
            <button onClick={() => signOut()}>ログアアウト</button>
          ) : (
            <button onClick={() => signIn()}>ログイン</button>
          )
        }
        
      </main>
    </>
  );

}

export default App;
```
### App.cssを編集
```css: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);
}
```

以上です。

http\://localhost:3000にアクセスすると下図画面が表示されるはずです。
![completion_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/bb38c9c3-847a-c8fa-293e-323e2d130cbd.png)
ログインすると下記のような情報を取得出来ます。

今回はattributesの中からnicknameのみ使用しています。

ちなみにattributesはCognitoの設定でOpenID 接続スコープに「aws.cognito.signin.user.admin」を追加した事によって、取得出来るようになる項目です。

![console.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/f1b2aaff-4102-c19d-4cc1-3cdf9fec8ddf.jpeg)

# おわりに
今回初めてSSOするアプリを作りました。

SSOの全体のイメージも無い状態から始めたので、シンプルなものを作るにもなかなか苦労しました。

当初は、Okta独自のコードを書いて行かないといけないんだろう、と想像していましたが、終わってみれば、フロント側でOktaを意識する事はありませんでした。

Oktaを使うCognitoでのログイン画面を実装するというが表現的には正しいでしょうか。

ここまで長かったぜー。