Firebase Emulatorを利用したローカルでの実行を前提としています。これをもとに、サーバー実装に組み込まないでください。
Identity Platformを使った認証を使ったサービスを開発していく上で、ローカルでは、エミュレーターを使って進めていくことができる。
アカウントの作成は、Admin SDKから行うことができるが、ログインに関しては、iOS/Android/Web等のクライアント側のSDKにしか提供されていない。
これに関して個人的に、公式に提供されなくても良いと思う。バックエンドサービス用のSDKにログインの処理があるということは、パスワードをバックエンドサービスが受け取ってしまうことになり、セキュリティ上のリスクが高まってしまう。そのため、SDK自体にログインの処理を敢えて含めないことで、リスクを軽減できる。
とはいえ、SDKにできないだけで、直接APIを使うことで、バックエンドサービスのログイン処理を実装することができる。 また、Emulatorでも、当然ながらAPIが利用できる。
ということで、Goをサンプルに、ローカルでIDトークンの発行を行う。
前提
環境変数にFIREBASE_AUTH_EMULATOR_HOST
をセットしておく。
export FIREBASE_AUTH_EMULATOR_HOST=localhost:9099
IDトークンについて
エミュレーターで作成されるIDトークンには、Signature部分が存在しない、header.payload.
という形になっている。そのおかげで、本番環境では利用できない、エミュレーター専用のトークンとなっている。
IDトークンの検証では、エミュレーターの場合、Signature検証がスキップされる。
実装
SignUp()
とVerifyIDToken()
に関しては、公式パッケージで提供されているので、APIを直接利用するSignIn()
を実装する。
package demo
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
firebase "firebase.google.com/go/v4"
"firebase.google.com/go/v4/auth"
"github.com/cockroachdb/errors"
"google.golang.org/api/option"
)
var emulatorHostEnvVar = "FIREBASE_AUTH_EMULATOR_HOST"
type (
SignUpPayload struct {
DisplayName string
Email string
Password string
}
SignInPayload struct {
Email string
Password string
}
loginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
ReturnSecureToken bool `json:"returnSecureToken"`
}
LoginResponse struct {
Kind string `json:"kind"`
LocalID string `json:"localId"`
Email string `json:"email"`
DisplayName string `json:"displayName"`
IDToken string `json:"idToken"`
Registered bool `json:"registered"`
ProfilePicture string `json:"profilePicture"`
OAuthAccessToken string `json:"oauthAccessToken"`
OAuthExpireIn int `json:"oauthExpireIn"`
OAuthAuthorizationCode string `json:"oauthAuthorizationCode"`
RefreshToken string `json:"refreshToken"`
ExpiresIn string `json:"expiresIn"`
}
)
type Client struct {
http *http.Client
auth *auth.Client
host string
}
func NewClient(ctx context.Context, projectID string) (*Client, error) {
if os.Getenv(emulatorHostEnvVar) == "" {
return nil, errors.New("emulator host not set")
}
httpClient := &http.Client{}
app, err := firebase.NewApp(
ctx,
&firebase.Config{
ProjectID: projectID,
},
option.WithHTTPClient(httpClient),
)
if err != nil {
return nil, errors.Wrap(err, "initialize firebase app")
}
authClient, err := app.Auth(ctx)
if err != nil {
return nil, errors.Wrap(err, "initialize firebase authClient")
}
return &Client{
auth: authClient,
http: httpClient,
host: os.Getenv("FIREBASE_AUTH_EMULATOR_HOST"),
}, nil
}
func (c *Client) CreateUser(ctx context.Context, payload SignUpPayload) (*auth.UserRecord, error) {
params := (&auth.UserToCreate{}).
Email(payload.Email).
EmailVerified(true).
Password(payload.Password).
DisplayName(payload.DisplayName)
u, err := c.auth.CreateUser(ctx, params)
if err != nil {
return nil, errors.Wrap(err, "create user")
}
return u, nil
}
func (c *Client) SignIn(ctx context.Context, payload SignInPayload) (*LoginResponse, error) {
b, err := json.Marshal(loginRequest{
Email: payload.Email,
Password: payload.Password,
ReturnSecureToken: true,
})
if err != nil {
return nil, fmt.Errorf("marshal payload: %w", err)
}
endpoint := url.URL{
Scheme: "http",
Host: c.host,
Path: "identitytoolkit.googleapis.com/v1/accounts:signInWithPassword",
}
q := url.Values{}
q.Set("key", "demo-key")
endpoint.RawQuery = q.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(b))
if err != nil {
return nil, errors.Wrap(err, "create request")
}
req.Header.Set("Content-Type", "application/json")
res, err := c.http.Do(req)
if err != nil {
return nil, errors.Wrap(err, "send request")
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, errors.Newf("unexpected status code: %d", res.StatusCode)
}
var result LoginResponse
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
return &result, nil
}
func (c *Client) VerifyIDToken(ctx context.Context, token string) (*auth.Token, error) {
return c.auth.VerifyIDToken(ctx, token)
}