0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GoのFirebase Admin SDKに存在しないサインイン機能をローカルで利用する

Last updated at Posted at 2025-09-18

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)
}
0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?