AWS Cognitoのカスタム属性を使ってセキュリティ診断会社の指摘に対応した

## はじめに

セキュリティ診断会社から指摘された点と対策内容をまとめました
参考になれば幸いです

## 前提となる開発環境

本記事で想定する開発環境は以下になります。

* フロントエンド - JavaScript(Nuxt.js)
* バックエンド - Java(Spring)
* ユーザー管理 - AWS Cognito

## 指摘されたこと

### ■ 他人の情報を取得・変更するようなAPIを実行できないようにしましょう

URLにIDなど含んでる場合は他のユーザーが叩けないように工夫しましょう、というものでした。
Nuxt.js のルーティングだとやりたくなっちゃう「動的なルーティング」ですね。

Nuxt.js | 動的なルーティング
https://develop365.gitlab.io/nuxtjs-2.8.X-doc/ja/guide/routing/#%E5%8B%95%E7%9A%84%E3%81%AA%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0

`/pages/users/_id.vue` のように、URLにIDを含んだものですね。
このようなページでURLに含まれているIDを使ってAPIを呼び出すことはよくあると思います。

```javascript:/pages/users/_id.vue
axios.get(`http://localhost:3000/users/${this.$route.params.id}`)
.then((res) => {
  // 取得した情報を何かしらする！
});
```

URLに含まれているIDをそのまま使う場合、**ブラウザ上のURLを直接書き換えることで他人の情報を取得できてしまう**恐れがあります。
(そもそも他人のIDを知っている必要があるので、ハードルは高い気がしますが…)

### ■ 管理者用のAPIを一般ユーザーが実行できないようにしましょう

管理者用APIのような重要な処理をするものには権限チェックをつけ、一般ユーザーが呼び出せないようにしましょう。

一般ユーザー用の画面上からは呼び出せなくても、Postmanのようなツールから直接APIを実行できてしまっては本末転倒です。


## 対応した内容

### ■ ユーザーにカスタム属性を付与した

AWS Cognitoを使ってユーザー管理をしている場合、カスタム属性にユーザー識別用のIDを含めることで対応が可能です。

AWS Cognito | ユーザープール属性
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/user-pool-settings-attributes.html

Spring Securityでの認証時にカスタム属性を読み込んでおきます。

色々端折っていますが、例えばこのようなコードになります。

```java:MyUserDetailsService.java
public class MyUserDetailsService implements UserDetailsService {
  @Override
  public UserDetails loadUserByUsername(String username) {
    // Cognitoからユーザーを検索
    AdminGetUserResult getUserResult = new AdminGetUserRequest()
      .withUserPoolId("UserPoolId")
      .withUsername("email");

    String userId = "";
    Set<Role> role = new HashSet<>();

    // カスタム属性を読み込む
    for (AttributeType item : getUserResult.getUserAttributes()) {
      switch (item.getName()) {
        case "custom:user_id":
          userId = item.getValue();
          break;
        case "custom:role":
          role.add(item.getValue());
          break;
      }
    }

    // 整形して返却
    UserDto user = UserDto.builder()
      .user_id(userId).roles(role).build();
    return MyUserDetails.build(userDto);
  }
}
```

```java:MyUserDetails.java
public class MyUserDetails implements UserDetails {
  private String userId;
  private Collection<? extends GrantedAuthority> authorities;

  // roleを整形してセットする
  public static MyUserDetails build(UserDto userDto) {
    List<GrantedAuthority> authorities =
        userDto.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority(role.name()))
            .collect(Collectors.toList());

    return new MyUserDetails(userDto.getId(), userDto.getUsername(), userDto.getCompany_id(), authorities);
  }

  // ...Getter, Setterと続く
}
```

これでセッションユーザーにカスタム属性をセットできました。

### ■ 各種APIに本人チェックを追加した

先ほどセッションユーザーに仕込んだ`userId`属性を利用します。

パスパラメータに含まれるユーザーIDとCognitoのトークンにカスタム属性として設定されているユーザーIDを比較し、本人のみが情報を取得できるようにしました。

![スクリーンショット 2023-01-15 17.01.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302049/ec3b5e8b-9e4b-e2e0-aaa2-69b0d0ca3ad3.png)

以下のようなコードになります。
`@AuthenticationPrincipal` でセッションユーザーの情報を読み込んでいます。

```java:UserController.java
@PostMapping("/users/{user_id}")
public User getUser(
    @PathVariable("user_id") String userId,
    // CognitoのIDトークンから取得したユーザー情報
    @AuthenticationPrincipal MyUserDetails user) {

  // パスパラメータとCognitoの情報を比較して本人確認
  if (!user.getUserId().equals(userId)) {
    // 本人ではない場合はエラーを返却
    throw new Exception();
  }

  // 本人ならデータを取得して返却する
  return service.getUser(userId);
}
```

この例ではユーザーIDをカスタム属性に含めていますが、例えばグループIDをカスタム属性にし「自分が所属しているグループの情報のみ閲覧可能」のような使い方ができるかと思います。

### ■ 各種APIに権限チェックを追加した

先ほどセッションユーザーに仕込んだ`GrantedAuthority`を利用します。
Cognito上の権限をチェックし、セッションユーザーがそのAPIを呼び出しても問題ないかチェックします。

![スクリーンショット 2023-01-15 20.28.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302049/b1928e0a-7a83-e721-fd42-e3501428b04e.png)

以下のようなコードになります。
GrantedAuthorityが付与されていれば、`@PreAuthorize`を使って権限チェックが可能です。

```java:UserController.java
// adminのみ実行可能
@PreAuthorize("hasRole('ROLE_admin')")
@DeleteMapping("/users/{user_id}")
public void deleteUser(
    @PathVariable("user_id") String userId) {
  service.deleteUser(userId);
}
```

## まとめ

AWS Cognitoのカスタム属性を使って本人識別用のIDや権限情報を管理し、Spring Securityでの認証時に保持する仕組みを実装しました。

各APIに本人チェック・権限チェックする処理を挟むことで、指摘されたセキュリティリスクに対応しました。

カスタム属性、超便利ですね…
Azure ADでも似たようなことができるそうですよ。