package auth import ( "context" "encoding/json" "fmt" "log/slog" "os" firebase "firebase.google.com/go/v4" firebaseAuth "firebase.google.com/go/v4/auth" "google.golang.org/api/option" ) // TokenVerifier abstracts Firebase token verification for testability. type TokenVerifier interface { VerifyToken(ctx context.Context, idToken string) (uid, email, name, avatarURL string, err error) } type FirebaseAuth struct { client *firebaseAuth.Client } // noopTokenVerifier is used in local development when Firebase credentials are not available. // It rejects all tokens with a clear error message. type noopTokenVerifier struct{} func (n *noopTokenVerifier) VerifyToken(_ context.Context, _ string) (uid, email, name, avatarURL string, err error) { return "", "", "", "", fmt.Errorf("Firebase auth is not configured (running in noop mode)") } // NewFirebaseAuthOrNoop tries to initialize Firebase auth from the credentials file. // If the file is missing, empty, or not a valid service account, it logs a warning // and returns a noop verifier that rejects all tokens. func NewFirebaseAuthOrNoop(credentialsFile string) (TokenVerifier, error) { data, err := os.ReadFile(credentialsFile) if err != nil || len(data) == 0 { slog.Warn("firebase credentials not found, running without Firebase auth", "file", credentialsFile) return &noopTokenVerifier{}, nil } var creds map[string]interface{} if err := json.Unmarshal(data, &creds); err != nil || creds["type"] == nil { slog.Warn("firebase credentials invalid, running without Firebase auth", "file", credentialsFile) return &noopTokenVerifier{}, nil } fa, err := NewFirebaseAuth(credentialsFile) if err != nil { return nil, err } return fa, nil } func NewFirebaseAuth(credentialsFile string) (*FirebaseAuth, error) { opt := option.WithCredentialsFile(credentialsFile) app, err := firebase.NewApp(context.Background(), nil, opt) if err != nil { return nil, err } client, err := app.Auth(context.Background()) if err != nil { return nil, err } return &FirebaseAuth{client: client}, nil } func (f *FirebaseAuth) VerifyToken(ctx context.Context, idToken string) (uid, email, name, avatarURL string, err error) { token, err := f.client.VerifyIDToken(ctx, idToken) if err != nil { return "", "", "", "", err } uid = token.UID if v, ok := token.Claims["email"].(string); ok { email = v } if v, ok := token.Claims["name"].(string); ok { name = v } if v, ok := token.Claims["picture"].(string); ok { avatarURL = v } return uid, email, name, avatarURL, nil }