Spring Security 5のOAuth 2.0 LoginでGitHubを利用したときにemailがnullになる問題をなんとかする

# Spring Security 5のOAuth 2.0 Login

ものすごく大雑把に言ってしまえば、Spring Security 5のOAuth 2.0 Loginは以下だけで外部IdP（下記はGitHub）と連携してログインを実装できます。

```yml
####################################################################################################
# spring
####################################################################################################
spring:
  security.oauth2.client.registration:
    github:
      client-id: xxxxx
      client-secret: xxxxx
```

で、そのときにユーザの情報も取得できるのですが、GoogleとGitHubアカウントでのログインの実装をしたところ、GitHubだけメールアドレスが取得できないという問題が起きました。

# なぜ？
CommonOAuth2Providerを見ると、GitHubの場合は`https://api.github.com/user`のAPIを実行することでアカウント情報を取得しているのですが、このエンドポイントでは**GitHubでpublicになっているemail**だけが取得できます。

なので、単純にログインしてOAuth2AuthenticationTokenからemailを取得しようとすると、アカウントがメールアドレスを公開していない場合、emailの値がnullになります。

[参考:CommonOAuth2Provider](https://github.com/spring-projects/spring-security/blob/master/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java)

# どうするか

GitHubには`https://api.github.com/user/emails`という**privateなemail**も取得できるAPIがあります。

なので、以下の流れでGitHubからメールアドレスを取得してみました。

1. Spring Security 5のOAuth 2.0 Loginでログインし、Authenticationを取得
2. Authenticationからアクセストークンを取得
3. トークンをRestTemplateにセットして、`https://api.github.com/user/emails`からメールアドレスを取得


参考：
https://stackoverflow.com/questions/35373995/github-user-email-is-null-despite-useremail-scope

# 実装

## application.yml

```yml
####################################################################################################
# spring
####################################################################################################
spring:
  security.oauth2.client.registration:
    github:
      client-id: xxxxx
      client-secret: xxxxx
      scope: user:email
```

追加でscopeとして`user:email`を指定する必要があります。

## Controllerの実装

```java
package work.inabajun.redman.web;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;


/**
 * Controller for login.
 */
@Controller
@Slf4j
public class LoginController {

    @Autowired
    private OAuth2AuthorizedClientService oAuth2AuthorizedClientService;

    @GetMapping("/")
    @ResponseBody
    public String index(final OAuth2AuthenticationToken authentication) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        // ログイン時に取得した情報を表示（https://api.github.com/user/emails）
        log.info(objectMapper.writeValueAsString(authentication.getPrincipal().getAttributes()));

        OAuth2AuthorizedClient client =
                oAuth2AuthorizedClientService.loadAuthorizedClient(
                        authentication.getAuthorizedClientRegistrationId(),
                        authentication.getName());

        // トークンを取得
        String accessToken = client.getAccessToken().getTokenValue();
        RestTemplate restTemplate = new RestTemplate();

        // RestTemplateにトークンをセット
        restTemplate.getInterceptors()
                .add(getBearerTokenInterceptor(accessToken));

        // メールアドレスを取得（https://api.github.com/user/emails）
        GitHubEmail[] forObject = restTemplate.getForObject("https://api.github.com/user/emails", GitHubEmail[].class);
        log.info(objectMapper.writeValueAsString(forObject));
        return "test";
    }

    private ClientHttpRequestInterceptor getBearerTokenInterceptor(String accessToken) {
        ClientHttpRequestInterceptor interceptor =
                (request, bytes, execution) -> {
                    request.getHeaders().add("Authorization", "Bearer " + accessToken);
                    return execution.execute(request, bytes);
                };
        return interceptor;
    }

    /**
     * Response for https://api.github.com/user/emails.
     */
    @Value
    static class GitHubEmail{

        private String email;

        private boolean verified;

        private boolean primary;

        private String visibility;
    }
}


```

トークンを取得してRestTemplateにセットする流れは以下を参考にさせていただきました。

[参考:Using Spring Security 5 to integrate with OAuth 2-secured services such as Facebook and GitHub](https://spring.io/blog/2018/03/06/using-spring-security-5-to-integrate-with-oauth-2-secured-services-such-as-facebook-and-github)

# まとめ
 * Spring Security 5のOAuth 2.0 LoginはGitHubをIdPに指定するとメールアドレスが取得できない可能性がある
 * 別のprivateなメールアドレスを取得できるAPIがあるのでそちらから別途取得可能