SvelteKitでJWT認証を実装してみる
これはローカルにファイルを保存して認証する方式にした
基本的に簡単なアプリケーションを作るうえでこれで困ることはないけど、
認証リクエストを受け取ったローカルにしかsessionファイルがないことで、
水平スケーリングした際にはリクエストを正しく処理できない可能性が出てくる
また今回はファイルベースなのでファイルアクセスだけでコストは小さめではありつつ、
これがDBにsessionを保存して水平スケーリングに対応していたりすると、
DBに対してリクエストのたびにクエリが走ってコストがかかってしまう
などなど、セッションベースの認証にはコストが掛かるよねーってことでJWTも試してみる
ライブラリを入れてぱっと済ませることが出来るならそれでもいいけど、
中でどんなことをしているのかは理解したいからある程度自前で実装してみたいとは思ってる
ローカルで完結させたいから、外部の認証局やサービスを頼るようなことはしたくない
JWTを理解する
JWT(JSON Web Token)は、主に認証と情報の安全な伝達に使用されるオープンスタンダード(RFC 7519)
ってことで、RFC7519をちらっと見に行く
何度も登場する用語をいくつか調べておく
トークン
JWTは Json Web Tokenの頭文字をとったもので、Json形式のWeb上でやり取りできる文字で構成されたトークン。
なので、この時のトークンはJWTという仕組みで生成された文字列程度の意味で良いと思う
ただ全くの意味のない文字列ってわけじゃないみたいで、情報を持っている文字列っぽい
クレーム
直感的には不満みたいな感じなんかなーと思ったけど、
トークンに含めることができる情報のことっぽい
認証で使おうとするなら、セッションではユーザーを特定するためのIDをセッションに持っておき、
誰からのアクセスなのかを特定する必要があるけど、
そういった情報も含めてクレームといい、トークンに含めることができるらしい
Claimsは「要求」とか「主張」とかの意味っぽい
JOSE
そもそもJWTはJOSEの一部
JOSEは Json Object Signing and Encryption の頭文字をとったもの
Signingはサインすることで署名、Encryptionは暗号化
JSON形式の情報を安全に伝達するためのプロトコルについてまとめたもの
JWS
こちらもJWTと同じくJOSEの一部
JWSは Json Web Signature の頭文字をとったもの
JWTに署名をほどこして、クレームが改ざんされた場合に検知できることを目的としている
改ざん自体を防ぐんじゃなくて、改ざんされたときに貴方の情報は信じられませんよーと言えたらOKって感じ
ペイロード
JWTのトークンはヘッダー、ペイロード、シグネチャの3つで出来ている
ペイロードはデータ格納部分
ペイロードのデータ格納部分にはクレームが入る
ペイロードはヘッダーと同じく、JSON形式の文字列をBase64エンコードした文字列
シグネチャはヘッダーとペイロードを繋いで秘密鍵で暗号化したもの
これくらいの理解があれば、あとは実装を見れば理解できそう
基本的な実装
ファイルセッションと同じく、必要な機能はだいたい同じ
今回特別に必要なのは署名をするための秘密鍵
秘密鍵がないとペイロードを改ざんされたことを検知することができない
routes/login
routes/logout
hooks.server.ts
lib/server/token.ts
秘密鍵
またJWTはHTTPリクエストヘッダーのAuthenticationに入れるという情報がいっぱい出てくるけど、
プログレッシブエンハンスメントを有効にするためにもcookieに入れることを考える
hooks.server.tsはluciaを参考にしたときと同じく、
cookieにtokenがなければすぐにリクエストを解決して終了
cookieにtokenがあれば、tokenの有効性をチェックし、必要であればuserも取ってきてlocalsにいれ、
そのあとリクエストを解決して終了する
loginやlogoutも同じように作る
ちがうの秘密鍵
これはopensshを使って発行するようなものではないけど、
誰にもバレず、ちょっと試して作れるようなものでもない文字列が必要
秘密鍵が変わるとtokenの検証もできなくなるので、ずっと使うつもりで作る必要がある
今回は試してみるだけなので、 sample-secret-key くらい簡単なものにするけど、
実際に運用する際には、かなり長い文字列を作って利用したいと思う
利用するライブラリ
JWTの仕様自体は明確なので、わざわざモジュールをインストールしてくる必要はないけど、
ポピュラーな実装方法としてこのモジュール使ったらいいよーというのが見つかったので利用する
$ npm install jsonwebtoken @types/jsonwebtoken --save-dev
tokenの有効性チェックのためのメソッドが提供されていないので、
try catchしてチェックする関数を自前で用意する必要がある
それ以外はtokenを作るのと、tokenからpayloadを取り出すだけなのでシンプル
code:token.ts
// ペイロードを渡したらtokenを取得できる関数
export const getPayload = (token: string): Payload => {
return jwt.verify(token, secretKey) as Payload;
};
// ペイロードを渡したらtokenを取得できる関数
export const createToken = (payload: Payload): string => {
return jwt.sign(payload, secretKey, {algorithm: 'HS256'});
}
// tokenを渡してその有効性をチェックできる関数
export const isTokenValid = (token: string): boolean => {
try {
jwt.verify(token, secretKey);
return true; // トークンが有効である
} catch (err) {
return false; // トークンが無効またはエラー
}
};
動くところまで作った後の感想
自前でfile sessionを作ることを考えたらめちゃくちゃ簡単
それにcookieを使ってtokenを受渡しているので、
ログアウトはcookieからtokenを削除するだけなので簡単
プログレッシブエンハンスメントを考えなくても、apiでなければcookieを使ってtokenを渡すのが良いと思う
あとは有効期限もオプションで設定出来たりするみたいなので、
そのへんも使えば特に問題はなさそう
cookieのところに
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ0ZXN0LXVzZXItaWQiLCJpYXQiOjE3NDEyMzU2MTF9.8IFvxhb-jHkbOzO7X9G5znNrcGMGgrc3N3DMLRTF6tE
ってな感じのtokenが渡されるんやけど、
真ん中の eyJ1c2VySWQiOiJ0ZXN0LXVzZXItaWQiLCJpYXQiOjE3NDEyMzU2MTF9 の部分をbase64decodeすると中身が見れる
ってことで、基本的には外に漏れても問題ない情報をいれるだけにした
payload、claimには非公開のもあるみたいなので、そのあたりの仕様を見れば何でも入れていいようになるかもやけど、
ログイン/ログアウトを実装するだけならそこまでしなくてもいいかも
tokenが漏れたらどうするの?!みたいなのは気にしなくていい
そもそもtokenが漏れるときはcookieが漏れてるので、
それをブラウザに食わせれば代わりにログインされてしまう
どうしても嫌なら、useagentとか接続元のipとか、そういう情報もpayloadに詰め込んで検証することで、
次のリクエストからも同じ端末、同じブラウザであることが確認できるはず
そのへんも変更することが出来るとか言い出したらキリがないのでそれはもう知らん
結論、簡単に実装出来て、目的を達成できて、session cookieと同じように使えることから、
この方法でsveltekitのログインを実装しようと思う
更新履歴
2025/03/06 かきおわり
2025/03/05 かきはじめ