はじめに
Firebase Authenticationを使ってログインした際に、IDトークン(JWT)が取得できます。
例えば、SPAでログイン認証を行う際、クライアント(JavaScript)でFirebaseを使ったログインを行い、取得したトークンをサーバに送信し、JWT内のuidとアプリで管理しているユーザを紐づけたい場合があると思います。
今回は、サーバサイド(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)
}
}
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検証|
@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形式に対応していないためか取得できませんでした。