Firebase から取得したIDトークンをサーバサイド（Java+SpringBoot）で検証する

# はじめに

Firebase Authenticationを使ってログインした際に、IDトークン（JWT）が取得できます。
例えば、SPAでログイン認証を行う際、クライアント（JavaScript）でFirebaseを使ったログインを行い、取得したトークンをサーバに送信し、JWT内のuidとアプリで管理しているユーザを紐づけたい場合があると思います。

![スクリーンショット 2019-02-19 14.53.20.png](https://qiita-image-store.s3.amazonaws.com/0/53830/04f56be6-4bd8-069c-8fcb-a6f415d6c523.png)

今回は、サーバサイド（java+SpringBoot）でのJWTの検証方法をメインに書いていきます。

# クライアントの実装

今回はNuxtを利用します。
トークンの取得方法と送信方法です。

```javascript:認証処理（抜粋）
async signin() {
  try {
    // メールアドレスとパスワードを使って認証
    await firebase.auth().signInWithEmailAndPassword(this.email, this.password)

    // IDトークン（JWT）取得
    const token = await firebase.auth().currentUser.getIdToken(true)

    // ローカルストレージに保存
    localStorage.setItem('token', token)

    // 認証後のページに遷移
    this.$router.push('/')
  } catch (e) {
    // 認証エラー、トークン取得エラー時
    console.log(e)
  }
}
```

```javascript:~/plugins/axios.js
export default function ({ $axios }) {
  $axios.onRequest(config => {
    const token = localStorage.getItem('token');
    if (token) {
      // ローカルストレージにトークンがあったら、リクエストヘッダ（Authorization）に付与
      config.headers.common['Authorization'] = "Bearer " + token;
    }
  })
}
```

# サーバサイドの実装

SpringSecurityのOnecePerRequestFilterを利用します。

他、以下のライブラリを利用しています。

|ライブラリ名|用途|
|:--|:--|:--|
|[OkHttp](https://square.github.io/okhttp/)|HTTPクライアント|
|[jjwt](https://github.com/jwtk/jjwt)|JWT検証|

```java:LoginFilter.java
@Component
public class LoginFilter extends OncePerRequestFilter {

  // 予めObjectMapperをBean登録しておきます
  @Autowired
  ObjectMapper objectMapper;

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {

    // コンテキストにログインユーザ情報をセット
    SecurityContextHolder.getContext().setAuthentication(new PreAuthenticatedAuthenticationToken(
        auth(request), null));

    filterChain.doFilter(request, response);
  }

  // ログインユーザ情報を取得
  private LoginUser auth(HttpServletRequest request) {
    
    // リクエストヘッダからJWTを取り出す
    String token = getToken(request);

    try {

      // JWTを検証、クレーム取得
      // 検証に失敗したら、例外がスローされる。
      Jws<Claims> claim = Jwts.parser()
          .setSigningKeyResolver(new GoogleSigningKeyResolver()) // 独自のリゾルバが必要
          .parseClaimsJws(token);

      // クレームのボディ部分からuidを取得
      String uid = (String)claim.getBody().get("user_id");

      // uidを取得し、ユーザ情報を検索
      User user = userService.findByUid(uid);

      if(user == null){
        throw new BadCredentialsException("ユーザが存在しません");
      }

      return new LoginUser(user);

    } catch (Exception e) {
      throw new BadCredentialsException("トークンが無効です");
    }
  }

  // リクエストヘッダからトークンを取得します。
  private String getToken(HttpServletRequest request) {
    String token = request.getHeader("Authorization");
    if (token == null || !token.startsWith("Bearer ")) {
      return null;
    }
    return token.substring("Bearer ".length());
  }

  /**
   * 署名に利用する公開鍵を返却します。
   * 
   * @author h.ono
   *
   */
  public class GoogleSigningKeyResolver extends SigningKeyResolverAdapter {

    @Override
    public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {

      try {
        Map<String, Object> map = getJwks();

        if (map.isEmpty()) {
          return null;
        }

        String keyValue = (String) map.get(jwsHeader.getKeyId());

        if (keyValue == null) {
          return null;
        }
        // 重要ポイント
        // 開始（BEGIN）と終了（END）のラベルを除去する。
        keyValue = keyValue
            .replaceAll("-----BEGIN CERTIFICATE-----\n", "")
            .replaceAll("-----END CERTIFICATE-----\n", "");

        InputStream in = new ByteArrayInputStream(Base64.decodeBase64(keyValue.getBytes("UTF-8")));
        X509Certificate certificate = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(in);
        return certificate.getPublicKey();

      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }

    /**
     * GoogleからJWKSを取得します。
     * 
     * @return JWKS
     */
    private Map<String, Object> getJwks() {
      OkHttpClient client = new OkHttpClient();
      Request request = new Request.Builder()
          .url("https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com")
          .build();
      try (Response response = client.newCall(request).execute()) {
        return objectMapper.readValue(response.body().string(), new TypeReference<Map<String, Object>>() {
        });
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }
  }

}
```
# おわりに

Firebaseから取得したUIDをアプリでもつユーザと紐づけることができました。
色々、調べましたが、認証がクライアント側で完結しているものが多かったので、今回のような方法にたどり着くまでに苦労しました。。。

JWTの検証を行う際に、auth0が提供している[java-jwt](https://github.com/auth0/java-jwt)でも試してみましたが、公開鍵を取得する際に、X.509形式に対応していないためか取得できませんでした。


# 参考サイト

- [unable to extract public key from x509 cert](https://stackoverflow.com/questions/34548957/unable-to-extract-public-key-from-x509-cert)