注意
この記事ではgo-webauthn
を使用したバックエンド側のWebAuthnの実装について解説しています。
1からWebAuthnを実装したい場合は RFC や W3C を参照してください。
この記事は、四工大アドベントカレンダーのカレンダー | Advent Calendar 2023 - Qiita の9日目です。
こんにちは、id:cateiru です。元四工大で、今はWebエンジニアとして働いています。
突然ですが、皆さんはパスキーを使っていますか?パスキーはFIDOの認証をiCloudのようなクラウド環境で同期することで複数端末でパスワードレスでログインできるようにする multi-device FIDO credential
です。
技術としては、FIDO2が使われており、Webで使う場合はWebAuthnを使うことで簡単に実装することが可能です。
本記事では、WebAuthnのバックエンド側をGoで実装していこうと思います。
前提知識
Webuathは以下のように登録・認証されます。バックエンド側でチャレンジというランダムな文字列を作成し、それを使用して認証するような形です。
Goで実装する
早速実装していこうと思いますが、WebAuthnの認証を1から実装するとものすごい大変なのでgo-webauthn
を使っていきます。
作る必要があるエンドポイントは4つで、
- 登録時のチャレンジを返す
- 登録する
- ログイン認証時にチャレンジを返す
- ログイン認証する
が必要です。
登録時のチャレンジを返す
WebAuthnには、サーバーがチャレンジを生成し、返すというフローがあります。Goのgo-webauthn
では、BeginRegistration
というメソッドが用意されており、そこにUserを渡します。渡すUserはUserのInterfaceを満たす必要があります。これは自分で頑張りましょう。
コード例はこのようになります。
handlerの実装は以下のようになり、BeginRegistration
の戻り値options
はjsonとして返します。また、session
は次の登録する際に必要となるため、RedisやRDSに保存しておきます。jsonとして保存しておくのがおすすめです。このsession
は一意のトークンを付与してCookieなどに保存しましょう。また、Userも使用するので保存しておきます。
type User struct { ID []byte `json:"id"` Name string `json:"name"` DisplayName string `json:"display_name,omitempty"` Credentials []webauthn.Credential `json:"-"` } user := &User{ ID: []byte(RandomString(20)), Name: name, DisplayName: displayName, } options, session, err := webauthn.BeginRegistration(user) if err != nil { return err } ...
登録する
次に、実際にWebAuthnを登録するHandlerを作成します。
このHandlerはapplication/json
でリクエストを受け取ることをおすすめします。そうすることで、protocol.ParseCredentialCreationResponseBody
で受け取ることができます。
response, err := protocol.ParseCredentialCreationResponseBody(req.Body) err != nil { return err }
その後、Cookieからsession
を呼び出します。さらに、チャレンジ時に生成したUserも使うので呼び出します。
credential, err := webauthn.CreateCredential(user, session, response) if err != nil { return err }
このcredential
にユーザーの認証情報が入っています。この認証情報をUserのWebAuthnCredentials
メソッドで返せるように実装します。
ログイン認証時にチャレンジを返す
次に、ログインです。登録時と同じく、ログイン時にもチャレンジを生成できる便利なメソッドが用意されています。BeginDiscoverableLogin
を使うことで、生成できます。今回もcredential
はレスポンスで返し、session
はDBなどに保存します。
credential, session, err := webauthn.BeginDiscoverableLogin() if err != nil { return err }
ログイン認証する
ログインでは、ValidateDiscoverableLogin
とValidateLogin
の2つのメソッドが用意されています。ValidateDiscoverableLogin
は引数にDiscoverableUserHandler
を指定することができ、このHandler内でDBからユーザーを引いてくるといったことができます。逆にValidateLogin
は引数にUserを指定する必要があります。
GitHubのようなパスキー単体でログインできるような場合はValidateDiscoverableLogin
を使用し、Googleなど最初にメールアドレスを入力してからパスキーを求める場合などはValidateLogin
を使用します。
ValidateDiscoverableLogin
で実装すると以下のようになります。
var loggedInUser *User = nil handler := func(rawID, userHandle []byte) (user webauthn.User, err error) { u, err := GetUserById(userHandle) if err != nil { return nil, err } loggedInUser = u return u, nil } _, err = webauthn.ValidateDiscoverableLogin(handler, session, response) if err != nil { return err }
これで、パスキーを使ったログインができようになります。
終わりに
いかがでしたでしょうか。パスキーはユーザーのログインは手軽ですが、実装が手間です。しかし、今後普及するかと思うのでぜひ実装してみてくださいね。
今回説明したコードなどは、以下のリポジトリにあるので気になったらぜひ見てください!
ではでは ノシ