HerokuアドオンのAuth0から全ユーザ情報取得 (1000件以内)

#前提
1. Herokuの対象アプリにAuth0アドオン導入済
1. Spring Boot
1. コンソールアプリ
1. Maven
1. テストコードなし
1. APIコール数制限考慮なし
1. セキュリティ考慮なし(ログや標準出力にトークンや個人情報出力)
1. 取得ユーザ数1000までしか取得できない(Auth0の制限)

# Auth0に接続アプリ作成
1. Herokuの対象アプリの画面のアドオン一覧の「Auth0」を押下してAuth0の管理画面に入る
1. 左サイドバーの「Applications」押下
1. 右上の「+ CREATE APPLICATION」押下
1. Nameは適当、typeは「Machine to Machine」を選択して「CREATE」押下
1. 「Select an API...」欄は「Auth0 Management API」を選択、SCOPESは「read:users」をチェック、「AUTHORIZE」押下
1. 「Settings」タブのDomain/Client ID/Client Secretの値を取得しておく

#アプリ作成、起動
1. eclipse起動
1. 新規＞Springスターター・プロジェクト
1. 適当に入力して(思い通りにならないことがあるので、ロケーションだけはここでちゃんと入れた方がいいかも)次へ
1. 以下選択して完了
 1. Spring Web
 1. Lombok
1. pom.xmlからspring-boot-starter-testのdependency削除
1. src/testフォルダ削除
1. 以下についてeclipseのリファクタリング機能等で好きに修正
 1. プロジェクト名
 1. パッケージ
 1. メインクラス名
 1. pom.xmlのgroupId/artifactId/version/name/description
1. src/main/＜パッケージ＞/Auth0Service.javaを作成、後述の内容を記述
1. メインクラスに「implements CommandLineRunner」追加、実装されていないメソッドがある旨のエラーになるので後述の内容を記述
1. application.propertiesに後述の内容を記述
1. eclipseのBootダッシュボードで作成プロジェクトの構成を開き、環境変数AUTH0_DOMAIN/AUTH0_CLIENT_ID/AUTH0_CLIENT_SECRET作成、Auth0から取得しておいた値を設定
1. eclipseのBootダッシュボードで起動

#GitHubに登録
1. GitHubでリポジトリを作成してURL取得
1. 以下コマンドプロンプトで対象フォルダに移動してから
1. git init
1. git remote add origin ＜GitHubのURL＞
1. git add .
1. git commit -m "1st commit"
1. git push --set-upstream origin master
1. eclipseでプロジェクト名右クリック、チーム＞プロジェクトの共用、Git、完了

#ソース

```Auth0Service.java
@Component
@Slf4j
public class Auth0Service {
    @Value("${auth0.domain}")
    protected String auth0Domain;

    @Value("${auth0.clientId}")
    protected String auth0ClientId;

    @Value("${auth0.clientSecret}")
    protected String auth0ClientSecret;

    // 全ユーザ取得 ------------------------------------------------------------

    // 全件取るまでやめないのでAPIコール数制限等に注意

    public List<User> getAllUsers() {
        log.info("全ユーザを取得します。");

        List<User> users = new ArrayList<>();
        int page = 0;
        while (true) {
            // URI作成
            URI uri = UriComponentsBuilder.fromHttpUrl("https://" + auth0Domain + "/api/v2/users")
                    .queryParam("page", page)
                    .queryParam("per_page", 30)
                    .queryParam("include_totals", true)
                    .queryParam("sort", "created_at:1")
                    .queryParam("fields", "email,name,user_id,user_metadata,last_login,logins_count") // 許可なく個人情報を取得しないように注意
                    // .queryParam("q", "logins_count:[1 TO *]") // ログイン回数1回以上
                    .build().toUri();

            // ヘッダ作成
            HttpHeaders httpHeaders = new HttpHeaders();
            addAuthorizationHeader(httpHeaders);

            // リクエスト
            ResponseEntity<UsersResponseData> responseEntity = restTemplate.exchange(
                    uri,
                    HttpMethod.GET,
                    new HttpEntity<>(httpHeaders),
                    UsersResponseData.class);

            // 結果取り出し
            UsersResponseData usersResponseData = responseEntity.getBody();
            users.addAll(usersResponseData.getUsers());
            if (usersResponseData.getLength() < usersResponseData.getLimit()) {
                log.info("全ユーザを取得しました。({}件)", users.size());
                return users;
            }

            // 次ページ取得準備
            page++;
        }
    }

    @Data
    private static class UsersResponseData { // staticにしないとnon-static inner classes like this can only by instantiated using default, no-argument constructorエラー
        // レスポンスJSONのうち取得したい項目だけフィールド定義する
        private Integer start;
        private Integer limit;
        private Integer length;
        private List<User> users;
        private Integer total;
    }

    @Data
    public static class User {
        private String email;
        private String name;
        private String user_id; // userIdにしたいがJSONがuser_idなのでやむなし、Lombokで回避できないか？
        private UserMetadata user_metadata;
        private String last_login;
        private Integer logins_count;
    }

    @Data
    public static class UserMetadata {
        private String accountId;
        private String contactId;
    }

    // 認証 --------------------------------------------------------------------

    private OAuthTokenResponseData oauthTokenResponseData;

    private void addAuthorizationHeader(HttpHeaders httpHeaders) { // expireには非対応
        if (oauthTokenResponseData == null) {
            oauthTokenResponseData = restTemplate.postForObject("https://" + auth0Domain + "/oauth/token", new OAuthTokenRequestData(auth0ClientId, auth0ClientSecret, "https://" + auth0Domain + "/api/v2/", "client_credentials"), OAuthTokenResponseData.class);
        }
        httpHeaders.add(HttpHeaders.AUTHORIZATION, oauthTokenResponseData.getToken_type() + " " + oauthTokenResponseData.getAccess_token());
    }

    @Data
    private class OAuthTokenRequestData {
        final private String client_id;
        final private String client_secret;
        final private String audience;
        final private String grant_type;
    }

    @Data
    private static class OAuthTokenResponseData {
        private String access_token;
        private String scope;
        private Integer expires_in;
        private String token_type;
    }

    // RestTemplate初期化 ------------------------------------------------------

    @Autowired
    RestTemplate restTemplate;

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
        if (log.isTraceEnabled()) {
            restTemplateBuilder = restTemplateBuilder
                    .requestFactory(() -> new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()))
                    .additionalInterceptors(new ClientHttpRequestInterceptorImpl());
        }
        return restTemplateBuilder.build();
    }

    // ログ出力 ----------------------------------------------------------------

    private class ClientHttpRequestInterceptorImpl implements ClientHttpRequestInterceptor {
        @Override
        public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] body, ClientHttpRequestExecution execution) throws IOException {
            UUID uuid = UUID.randomUUID(); // リクエストとレスポンスの紐付け用
            logRequest(uuid, httpRequest, body);
            long startTimeMillis = System.currentTimeMillis();
            ClientHttpResponse clientHttpResponse = execution.execute(httpRequest, body);
            long responseTimeMillis = System.currentTimeMillis() - startTimeMillis;
            logResponse(uuid, clientHttpResponse, responseTimeMillis);
            return clientHttpResponse;
        }

        private void logRequest(UUID uuid, HttpRequest httpRequest, byte[] body) throws IOException {
            log.trace("HttpRequest - UUID: {}, Method: {}, URI: {}", uuid, httpRequest.getMethodValue(), httpRequest.getURI());
            logHeaders(httpRequest.getHeaders());
            logBody(httpRequest.getHeaders().getContentType(), new String(body));
        }

        private void logResponse(UUID uuid, ClientHttpResponse clientHttpResponse, long responseTimeMillis) throws IOException {
            log.trace("ClientHttpResponse - UUID: {}, Response Time: {} ms, Status: {} {}", uuid, responseTimeMillis, clientHttpResponse.getRawStatusCode(), clientHttpResponse.getStatusText());
            logHeaders(clientHttpResponse.getHeaders());
            logBody(clientHttpResponse.getHeaders().getContentType(), StreamUtils.copyToString(clientHttpResponse.getBody(), StandardCharsets.UTF_8));
        }

        private void logHeaders(HttpHeaders httpHeaders) {
            StringBuilder stringBuilder = new StringBuilder();
            httpHeaders.forEach((key, values) -> {
                values.forEach((value) -> {
                    stringBuilder.append(key + ": " + value + System.lineSeparator());
                });
            });
            log.trace("-- Headers -----------------------------" + System.lineSeparator() + "{}", stringBuilder.toString().trim());
        }

        private void logBody(MediaType contentType, String body /* バイナリ非対応 */) throws IOException {
            if (body != null && !body.isEmpty()) {
                log.trace("-- Body --------------------------------" + System.lineSeparator() + "{}", body);
            }
            if (MediaType.APPLICATION_JSON.includes(contentType)) {
                log.trace("-- Body formatted ----------------------" + System.lineSeparator() + "{}", ppJson(body));
            }
        }

        private String ppJson(String jsonString) throws JsonProcessingException, JsonMappingException {
            return (new ObjectMapper()).readTree(jsonString).toPrettyString();
        }
    }
}
```

```メインクラス.java
    @Autowired
    private Auth0Service auth0Service;

    @Override
    public void run(String... args) throws Exception {
        List<User> allUsers = auth0Service.getAllUsers();
        System.out.println(allUsers.get(1).getEmail());
        System.out.println(allUsers.get(1).getName());
        System.out.println(allUsers.get(1).getUser_id());
        System.out.println(allUsers.get(1).getUser_metadata().getAccountId());
        System.out.println(allUsers.get(1).getUser_metadata().getContactId());
        System.out.println(allUsers.get(1).getLast_login());
        System.out.println(allUsers.get(1).getLogins_count());
    }
```

```application.properties
# Webサーバ起動抑止
spring.main.web-application-type=none

# ログレベルを環境変数から取得、デフォルトはTRACE
logging.level.＜パッケージ＞.Auth0Service=${LOG_LEVEL:TRACE}

# Auth0接続情報を環境変数から取得
auth0.domain=${AUTH0_DOMAIN}
auth0.clientId=${AUTH0_CLIENT_ID}
auth0.clientSecret=${AUTH0_CLIENT_SECRET}
```

#1000を超えるユーザを取得しようとした際のレスポンス
400 Bad Requestが返る、ボディは以下

```json
{
  "statusCode" : 400,
  "error" : "Bad Request",
  "message" : "You can only page through the first 1000 records. See https://auth0.com/docs/users/search/v3/view-search-results-by-page#limitation",
  "errorCode" : "invalid_paging"
}
```

#感謝
ネットのいろんな記事を参考にしまくった、多謝