LoginSignup
65
55

More than 5 years have passed since last update.

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

Posted at

はじめに

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

スクリーンショット 2019-02-19 14.53.20.png

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

クライアントの実装

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

認証処理(抜粋)
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)
  }
}
~/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 HTTPクライアント
jjwt JWT検証
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でも試してみましたが、公開鍵を取得する際に、X.509形式に対応していないためか取得できませんでした。

参考サイト

65
55
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
65
55