Repository: firebase/firebase-admin-go
Pull request #419 (2021-02-20 13:01:17)

feat(auth): Add token verification logic for emulator mode
### Discussion

#409

This PR adds token verification with auth emulator for auth client, in addition to the changes introduced at #414.

maku693: We should add integration tests for this PR, like Node.js SDK implementation. https://github.com/firebase/firebase-admin-node/pull/1148

But I'm not sure how much should we add tests for this feature, and how to setup CI. any suggestions?
maku693: Thank you for reviewing! I've fixed the points you've commented.
robinmitra: Would be awesome to get this merged in if it all looks good, as this is breaking my local development flow.
maku693: Hi @hiranya911 and @yuchenshi, do you have any plans to release this in the near future?

Patch:
diff --git a/auth/auth.go b/auth/auth.go
index 50e69d5..17c5279 100644
--- a/auth/auth.go
+++ b/auth/auth.go
@@ -103,12 +103,12 @@ func NewClient(ctx context.Context, conf *internal.AuthConfig) (*Client, error)
 		}
 	}
 
-	idTokenVerifier, err := newIDTokenVerifier(ctx, conf.ProjectID, isEmulator)
+	idTokenVerifier, err := newIDTokenVerifier(ctx, conf.ProjectID)
 	if err != nil {
 		return nil, err
 	}
 
-	cookieVerifier, err := newSessionCookieVerifier(ctx, conf.ProjectID, isEmulator)
+	cookieVerifier, err := newSessionCookieVerifier(ctx, conf.ProjectID)
 	if err != nil {
 		return nil, err
 	}
@@ -144,6 +144,7 @@ func NewClient(ctx context.Context, conf *internal.AuthConfig) (*Client, error)
 		cookieVerifier:         cookieVerifier,
 		signer:                 signer,
 		clock:                  internal.SystemClock,
+		isEmulator:             isEmulator,
 	}
 	return &Client{
 		baseClient:    base,
@@ -265,6 +266,7 @@ type baseClient struct {
 	cookieVerifier         *tokenVerifier
 	signer                 cryptoSigner
 	clock                  internal.Clock
+	isEmulator             bool
 }
 
 func (c *baseClient) withTenantID(tenantID string) *baseClient {
@@ -281,63 +283,68 @@ func (c *baseClient) withTenantID(tenantID string) *baseClient {
 // https://firebase.google.com/docs/auth/admin/verify-id-tokens#retrieve_id_tokens_on_clients for
 // more details on how to obtain an ID token in a client app.
 //
-// This function does not make any RPC calls most of the time. The only time it makes an RPC call
-// is when Google public keys need to be refreshed. These keys get cached up to 24 hours, and
-// therefore the RPC overhead gets amortized over many invocations of this function.
+// In non-emulator mode, this function does not make any RPC calls most of the time.
+// The only time it makes an RPC call is when Google public keys need to be refreshed.
+// These keys get cached up to 24 hours, and therefore the RPC overhead gets amortized
+// over many invocations of this function.
 //
 // This does not check whether or not the token has been revoked. Use `VerifyIDTokenAndCheckRevoked()`
 // when a revocation check is needed.
 func (c *baseClient) VerifyIDToken(ctx context.Context, idToken string) (*Token, error) {
-	decoded, err := c.idTokenVerifier.VerifyToken(ctx, idToken)
-	if err == nil && c.tenantID != "" && c.tenantID != decoded.Firebase.Tenant {
-		return nil, &internal.FirebaseError{
-			ErrorCode: internal.InvalidArgument,
-			String:    fmt.Sprintf("invalid tenant id: %q", decoded.Firebase.Tenant),
-			Ext: map[string]interface{}{
-				authErrorCode: tenantIDMismatch,
-			},
-		}
-	}
-
-	return decoded, err
-}
-
-// IsTenantIDMismatch checks if the given error was due to a mismatched tenant ID in a JWT.
-func IsTenantIDMismatch(err error) bool {
-	return hasAuthErrorCode(err, tenantIDMismatch)
+	return c.verifyIDToken(ctx, idToken, false)
 }
 
 // VerifyIDTokenAndCheckRevoked verifies the provided ID token, and additionally checks that the
 // token has not been revoked.
 //
-// This function uses `VerifyIDToken()` internally to verify the ID token JWT. However, unlike
-// `VerifyIDToken()` this function must make an RPC call to perform the revocation check.
+// Unlike `VerifyIDToken()`, this function must make an RPC call to perform the revocation check.
 // Developers are advised to take this additional overhead into consideration when including this
 // function in an authorization flow that gets executed often.
 func (c *baseClient) VerifyIDTokenAndCheckRevoked(ctx context.Context, idToken string) (*Token, error) {
-	decoded, err := c.VerifyIDToken(ctx, idToken)
-	if err != nil {
-		return nil, err
-	}
+	return c.verifyIDToken(ctx, idToken, true)
+}
 
-	revoked, err := c.checkRevoked(ctx, decoded)
+func (c *baseClient) verifyIDToken(ctx context.Context, idToken string, checkRevoked bool) (*Token, error) {
+	decoded, err := c.idTokenVerifier.VerifyToken(ctx, idToken, c.isEmulator)
 	if err != nil {
 		return nil, err
 	}
 
-	if revoked {
+	if c.tenantID != "" && c.tenantID != decoded.Firebase.Tenant {
 		return nil, &internal.FirebaseError{
 			ErrorCode: internal.InvalidArgument,
-			String:    "ID token has been revoked",
+			String:    fmt.Sprintf("invalid tenant id: %q", decoded.Firebase.Tenant),
 			Ext: map[string]interface{}{
-				authErrorCode: idTokenRevoked,
+				authErrorCode: tenantIDMismatch,
 			},
 		}
 	}
 
+	if c.isEmulator || checkRevoked {
+		revoked, err := c.checkRevoked(ctx, decoded)
+		if err != nil {
+			return nil, err
+		}
+
+		if revoked {
+			return nil, &internal.FirebaseError{
+				ErrorCode: internal.InvalidArgument,
+				String:    "ID token has been revoked",
+				Ext: map[string]interface{}{
+					authErrorCode: idTokenRevoked,
+				},
+			}
+		}
+	}
+
 	return decoded, nil
 }
 
+// IsTenantIDMismatch checks if the given error was due to a mismatched tenant ID in a JWT.
+func IsTenantIDMismatch(err error) bool {
+	return hasAuthErrorCode(err, tenantIDMismatch)
+}
+
 // IsIDTokenRevoked checks if the given error was due to a revoked ID token.
 //
 // When IsIDTokenRevoked returns true, IsIDTokenInvalid is guranteed to return true.
@@ -352,41 +359,47 @@ func IsIDTokenRevoked(err error) bool {
 // decoded claims in the input JWT. See https://firebase.google.com/docs/auth/admin/manage-cookies for more details on
 // how to obtain a session cookie.
 //
-// This function does not make any RPC calls most of the time. The only time it makes an RPC call
-// is when Google public keys need to be refreshed. These keys get cached up to 24 hours, and
-// therefore the RPC overhead gets amortized over many invocations of this function.
+// In non-emulator mode, this function does not make any RPC calls most of the time.
+// The only time it makes an RPC call is when Google public keys need to be refreshed.
+// These keys get cached up to 24 hours, and therefore the RPC overhead gets amortized
+// over many invocations of this function.
 //
 // This does not check whether or not the cookie has been revoked. Use `VerifySessionCookieAndCheckRevoked()`
 // when a revocation check is needed.
 func (c *Client) VerifySessionCookie(ctx context.Context, sessionCookie string) (*Token, error) {
-	return c.cookieVerifier.VerifyToken(ctx, sessionCookie)
+	return c.verifySessionCookie(ctx, sessionCookie, false)
 }
 
 // VerifySessionCookieAndCheckRevoked verifies the provided session cookie, and additionally checks that the
 // cookie has not been revoked.
 //
-// This function uses `VerifySessionCookie()` internally to verify the cookie JWT. However, unlike
-// `VerifySessionCookie()` this function must make an RPC call to perform the revocation check.
+// Unlike `VerifySessionCookie()`, this function must make an RPC call to perform the revocation check.
 // Developers are advised to take this additional overhead into consideration when including this
 // function in an authorization flow that gets executed often.
 func (c *Client) VerifySessionCookieAndCheckRevoked(ctx context.Context, sessionCookie string) (*Token, error) {
-	decoded, err := c.VerifySessionCookie(ctx, sessionCookie)
-	if err != nil {
-		return nil, err
-	}
+	return c.verifySessionCookie(ctx, sessionCookie, true)
+}
 
-	revoked, err := c.checkRevoked(ctx, decoded)
+func (c *Client) verifySessionCookie(ctx context.Context, sessionCookie string, checkRevoked bool) (*Token, error) {
+	decoded, err := c.cookieVerifier.VerifyToken(ctx, sessionCookie, c.isEmulator)
 	if err != nil {
 		return nil, err
 	}
 
-	if revoked {
-		return nil, &internal.FirebaseError{
-			ErrorCode: internal.InvalidArgument,
-			String:    "session cookie has been revoked",
-			Ext: map[string]interface{}{
-				authErrorCode: sessionCookieRevoked,
-			},
+	if c.isEmulator || checkRevoked {
+		revoked, err := c.checkRevoked(ctx, decoded)
+		if err != nil {
+			return nil, err
+		}
+
+		if revoked {
+			return nil, &internal.FirebaseError{
+				ErrorCode: internal.InvalidArgument,
+				String:    "session cookie has been revoked",
+				Ext: map[string]interface{}{
+					authErrorCode: sessionCookieRevoked,
+				},
+			}
 		}
 	}
 
diff --git a/auth/token_verifier.go b/auth/token_verifier.go
index 3e83a8e..b323941 100644
--- a/auth/token_verifier.go
+++ b/auth/token_verifier.go
@@ -100,10 +100,9 @@ type tokenVerifier struct {
 	expiredTokenCode  string
 	keySource         keySource
 	clock             internal.Clock
-	isEmulator        bool
 }
 
-func newIDTokenVerifier(ctx context.Context, projectID string, isEmulator bool) (*tokenVerifier, error) {
+func newIDTokenVerifier(ctx context.Context, projectID string) (*tokenVerifier, error) {
 	noAuthHTTPClient, _, err := transport.NewHTTPClient(ctx, option.WithoutAuthentication())
 	if err != nil {
 		return nil, err
@@ -119,11 +118,10 @@ func newIDTokenVerifier(ctx context.Context, projectID string, isEmulator bool)
 		expiredTokenCode:  idTokenExpired,
 		keySource:         newHTTPKeySource(idTokenCertURL, noAuthHTTPClient),
 		clock:             internal.SystemClock,
-		isEmulator:        isEmulator,
 	}, nil
 }
 
-func newSessionCookieVerifier(ctx context.Context, projectID string, isEmulator bool) (*tokenVerifier, error) {
+func newSessionCookieVerifier(ctx context.Context, projectID string) (*tokenVerifier, error) {
 	noAuthHTTPClient, _, err := transport.NewHTTPClient(ctx, option.WithoutAuthentication())
 	if err != nil {
 		return nil, err
@@ -139,7 +137,6 @@ func newSessionCookieVerifier(ctx context.Context, projectID string, isEmulator
 		expiredTokenCode:  sessionCookieExpired,
 		keySource:         newHTTPKeySource(sessionCookieCertURL, noAuthHTTPClient),
 		clock:             internal.SystemClock,
-		isEmulator:        isEmulator,
 	}, nil
 }
 
@@ -156,18 +153,14 @@ func newSessionCookieVerifier(ctx context.Context, projectID string, isEmulator
 //
 // If any of the above conditions are not met, an error is returned. Otherwise a pointer to a
 // decoded Token is returned.
-func (tv *tokenVerifier) VerifyToken(ctx context.Context, token string) (*Token, error) {
-	if tv.isEmulator {
-		panic("not implemented")
-	}
-
+func (tv *tokenVerifier) VerifyToken(ctx context.Context, token string, isEmulator bool) (*Token, error) {
 	if tv.projectID == "" {
 		// Configuration error.
 		return nil, errors.New("project id not available")
 	}
 
 	// Validate the token content first. This is fast and cheap.
-	payload, err := tv.verifyContent(token)
+	payload, err := tv.verifyContent(token, isEmulator)
 	if err != nil {
 		return nil, err
 	}
@@ -176,6 +169,11 @@ func (tv *tokenVerifier) VerifyToken(ctx context.Context, token string) (*Token,
 		return nil, err
 	}
 
+	// In emulator mode, skip signature verification
+	if isEmulator {
+		return payload, nil
+	}
+
 	// Verifying the signature requires syncronized access to a key cache and
 	// potentially issues an http request. Therefore we do it last.
 	if err := tv.verifySignature(ctx, token); err != nil {
@@ -185,7 +183,7 @@ func (tv *tokenVerifier) VerifyToken(ctx context.Context, token string) (*Token,
 	return payload, nil
 }
 
-func (tv *tokenVerifier) verifyContent(token string) (*Token, error) {
+func (tv *tokenVerifier) verifyContent(token string, isEmulator bool) (*Token, error) {
 	if token == "" {
 		return nil, &internal.FirebaseError{
 			ErrorCode: internal.InvalidArgument,
@@ -194,7 +192,7 @@ func (tv *tokenVerifier) verifyContent(token string) (*Token, error) {
 		}
 	}
 
-	payload, err := tv.verifyHeaderAndBody(token)
+	payload, err := tv.verifyHeaderAndBody(token, isEmulator)
 	if err != nil {
 		return nil, &internal.FirebaseError{
 			ErrorCode: internal.InvalidArgument,
@@ -249,7 +247,7 @@ func (tv *tokenVerifier) verifySignature(ctx context.Context, token string) erro
 	return nil
 }
 
-func (tv *tokenVerifier) verifyHeaderAndBody(token string) (*Token, error) {
+func (tv *tokenVerifier) verifyHeaderAndBody(token string, isEmulator bool) (*Token, error) {
 	var (
 		header  jwtHeader
 		payload Token
@@ -269,13 +267,13 @@ func (tv *tokenVerifier) verifyHeaderAndBody(token string) (*Token, error) {
 	}
 
 	issuer := tv.issuerPrefix + tv.projectID
-	if header.KeyID == "" {
+	if !isEmulator && header.KeyID == "" {
 		if payload.Audience == firebaseAudience {
 			return nil, fmt.Errorf("expected %s but got a custom token", tv.articledShortName)
 		}
 		return nil, fmt.Errorf("%s has no 'kid' header", tv.shortName)
 	}
-	if header.Algorithm != "RS256" {
+	if !isEmulator && header.Algorithm != "RS256" {
 		return nil, fmt.Errorf("%s has invalid algorithm; expected 'RS256' but got %q",
 			tv.shortName, header.Algorithm)
 	}


Test patch:
diff --git a/auth/auth_test.go b/auth/auth_test.go
index 51bb395..854f3f6 100644
--- a/auth/auth_test.go
+++ b/auth/auth_test.go
@@ -24,9 +24,11 @@ import (
 	"net/http"
 	"os"
 	"strings"
+	"syscall"
 	"testing"
 	"time"
 
+	"firebase.google.com/go/v4/errorutils"
 	"firebase.google.com/go/v4/internal"
 	"golang.org/x/oauth2/google"
 	"google.golang.org/api/option"
@@ -697,25 +699,88 @@ func TestVerifyIDTokenWithNoProjectID(t *testing.T) {
 	}
 }
 
-func TestVerifyIDTokenInEmulatorMode(t *testing.T) {
-	os.Setenv(emulatorHostEnvVar, "localhost:9099")
-	defer os.Unsetenv(emulatorHostEnvVar)
+func TestVerifyIDTokenUnsigned(t *testing.T) {
+	token := getEmulatedIDToken(nil)
+
+	client := &Client{
+		baseClient: &baseClient{
+			idTokenVerifier: testIDTokenVerifier,
+		},
+	}
+	_, err := client.VerifyIDToken(context.Background(), token)
+	if !IsIDTokenInvalid(err) {
+		t.Errorf("VerifyIDToken(Unsigned) = %v; want = IDTokenInvalid", err)
+	}
+}
+
+func TestEmulatorVerifyIDToken(t *testing.T) {
+	s := echoServer(testGetUserResponse, t)
+	defer s.Close()
+
+	s.Client.idTokenVerifier = testIDTokenVerifier
+	s.Client.isEmulator = true
+
+	token := getEmulatedIDToken(nil)
+	ft, err := s.Client.VerifyIDToken(context.Background(), token)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	now := testClock.Now().Unix()
+	if ft.AuthTime != now-100 {
+		t.Errorf("AuthTime = %d; want = %d", ft.AuthTime, now-100)
+	}
+	if ft.Firebase.SignInProvider != "custom" {
+		t.Errorf("SignInProvider = %q; want = %q", ft.Firebase.SignInProvider, "custom")
+	}
+	if ft.Firebase.Tenant != "" {
+		t.Errorf("Tenant = %q; want = %q", ft.Firebase.Tenant, "")
+	}
+	if ft.Claims["admin"] != true {
+		t.Errorf("Claims['admin'] = %v; want = true", ft.Claims["admin"])
+	}
+	if ft.UID != ft.Subject {
+		t.Errorf("UID = %q; Sub = %q; want UID = Sub", ft.UID, ft.Subject)
+	}
+}
+
+func TestEmulatorVerifyIDTokenExpiredError(t *testing.T) {
+	s := echoServer(testGetUserResponse, t)
+	defer s.Close()
+
+	s.Client.idTokenVerifier = testIDTokenVerifier
+	s.Client.isEmulator = true
+
+	now := testClock.Now().Unix()
+	token := getEmulatedIDToken(mockIDTokenPayload{
+		"iat": now - 1000,
+		"exp": now - clockSkewSeconds - 1,
+	})
+
+	_, err := s.Client.VerifyIDToken(context.Background(), token)
+	if !IsIDTokenExpired(err) {
+		t.Errorf("VerifyIDToken(Expired) = %v; want = IDTokenExpired", err)
+	}
+}
 
+func TestEmulatorVerifyIDTokenUnreachableEmulator(t *testing.T) {
 	conf := &internal.AuthConfig{
-		ProjectID: "",
 		Opts:      optsWithTokenSource,
+		ProjectID: testProjectID,
+		Version:   testVersion,
 	}
-	c, err := NewClient(context.Background(), conf)
+	client, err := NewClient(context.Background(), conf)
 	if err != nil {
 		t.Fatal(err)
 	}
+	client.httpClient.Client.Transport = eConnRefusedTransport{}
+	client.isEmulator = true
 
-	defer func() {
-		if r := recover(); r == nil {
-			t.Error("VeridyIDToken() didn't panic")
-		}
-	}()
-	c.VerifyIDToken(context.Background(), testIDToken)
+	token := getEmulatedIDToken(nil)
+	_, err = client.VerifyIDToken(context.Background(), token)
+	if err == nil || !errorutils.IsUnavailable(err) || !strings.HasPrefix(err.Error(), "failed to establish a connection") {
+		t.Errorf("VerifyIDToken(UnreachableEmulator) = %v; want = Unavailable", err)
+	}
 }
 
 func TestCustomTokenVerification(t *testing.T) {
@@ -737,7 +802,7 @@ func TestCustomTokenVerification(t *testing.T) {
 }
 
 func TestCertificateRequestError(t *testing.T) {
-	tv, err := newIDTokenVerifier(context.Background(), testProjectID, false)
+	tv, err := newIDTokenVerifier(context.Background(), testProjectID)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -1067,6 +1132,90 @@ func TestCookieRevocationCheckUserMgtError(t *testing.T) {
 	}
 }
 
+func TestVerifySessionCookieUnsigned(t *testing.T) {
+	token := getEmulatedSessionCookie(nil)
+
+	client := &Client{
+		baseClient: &baseClient{
+			cookieVerifier: testCookieVerifier,
+		},
+	}
+	_, err := client.VerifySessionCookie(context.Background(), token)
+	if !IsSessionCookieInvalid(err) {
+		t.Errorf("VerifySessionCookie(Unsigned) = %v; want = IDTokenInvalid", err)
+	}
+}
+
+func TestEmulatorVerifySessionCookie(t *testing.T) {
+	s := echoServer(testGetUserResponse, t)
+	defer s.Close()
+
+	s.Client.cookieVerifier = testCookieVerifier
+	s.Client.isEmulator = true
+
+	token := getEmulatedSessionCookie(nil)
+	ft, err := s.Client.VerifySessionCookie(context.Background(), token)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	now := testClock.Now().Unix()
+	if ft.AuthTime != now-100 {
+		t.Errorf("AuthTime = %d; want = %d", ft.AuthTime, now-100)
+	}
+	if ft.Firebase.SignInProvider != "custom" {
+		t.Errorf("SignInProvider = %q; want = %q", ft.Firebase.SignInProvider, "custom")
+	}
+	if ft.Firebase.Tenant != "" {
+		t.Errorf("Tenant = %q; want = %q", ft.Firebase.Tenant, "")
+	}
+	if ft.Claims["admin"] != true {
+		t.Errorf("Claims['admin'] = %v; want = true", ft.Claims["admin"])
+	}
+	if ft.UID != ft.Subject {
+		t.Errorf("UID = %q; Sub = %q; want UID = Sub", ft.UID, ft.Subject)
+	}
+}
+
+func TestEmulatorVerifySessionCookieExpiredError(t *testing.T) {
+	s := echoServer(testGetUserResponse, t)
+	defer s.Close()
+
+	s.Client.cookieVerifier = testCookieVerifier
+	s.Client.isEmulator = true
+
+	now := testClock.Now().Unix()
+	token := getEmulatedSessionCookie(mockIDTokenPayload{
+		"iat": now - 1000,
+		"exp": now - clockSkewSeconds - 1,
+	})
+
+	_, err := s.Client.VerifySessionCookie(context.Background(), token)
+	if !IsSessionCookieExpired(err) {
+		t.Errorf("VerifySessionCookie(Expired) = %v; want = IDTokenExpired", err)
+	}
+}
+
+func TestEmulatorVerifySessionCookieUnreachableEmulator(t *testing.T) {
+	conf := &internal.AuthConfig{
+		Opts:      optsWithTokenSource,
+		ProjectID: testProjectID,
+		Version:   testVersion,
+	}
+	client, err := NewClient(context.Background(), conf)
+	if err != nil {
+		t.Fatal(err)
+	}
+	client.httpClient.Client.Transport = eConnRefusedTransport{}
+	client.isEmulator = true
+
+	token := getEmulatedSessionCookie(nil)
+	_, err = client.VerifySessionCookie(context.Background(), token)
+	if err == nil || !errorutils.IsUnavailable(err) || !strings.HasPrefix(err.Error(), "failed to establish a connection") {
+		t.Errorf("VerifyIDToken(UnreachableEmulator) = %v; want = Unavailable", err)
+	}
+}
+
 func signerForTests(ctx context.Context) (cryptoSigner, error) {
 	creds, err := transport.Creds(ctx, optsWithServiceAcct...)
 	if err != nil {
@@ -1077,7 +1226,7 @@ func signerForTests(ctx context.Context) (cryptoSigner, error) {
 }
 
 func idTokenVerifierForTests(ctx context.Context) (*tokenVerifier, error) {
-	tv, err := newIDTokenVerifier(ctx, testProjectID, false)
+	tv, err := newIDTokenVerifier(ctx, testProjectID)
 	if err != nil {
 		return nil, err
 	}
@@ -1091,7 +1240,7 @@ func idTokenVerifierForTests(ctx context.Context) (*tokenVerifier, error) {
 }
 
 func cookieVerifierForTests(ctx context.Context) (*tokenVerifier, error) {
-	tv, err := newSessionCookieVerifier(ctx, testProjectID, false)
+	tv, err := newSessionCookieVerifier(ctx, testProjectID)
 	if err != nil {
 		return nil, err
 	}
@@ -1134,21 +1283,47 @@ func (p mockIDTokenPayload) decodeFrom(s string) error {
 	return decode(s, &p)
 }
 
+type eConnRefusedTransport struct{}
+
+func (eConnRefusedTransport) RoundTrip(*http.Request) (*http.Response, error) {
+	return nil, syscall.ECONNREFUSED
+}
+
 func getSessionCookie(p mockIDTokenPayload) string {
+	return getSessionCookieWithSigner(testSigner, p)
+}
+
+func getEmulatedSessionCookie(p mockIDTokenPayload) string {
+	return getSessionCookieWithSigner(emulatedSigner{}, p)
+}
+
+func getSessionCookieWithSigner(signer cryptoSigner, p mockIDTokenPayload) string {
 	pCopy := map[string]interface{}{
 		"iss": "https://session.firebase.google.com/" + testProjectID,
 	}
 	for k, v := range p {
 		pCopy[k] = v
 	}
-	return getIDToken(pCopy)
+	return getIDTokenWithSigner(signer, pCopy)
+}
+
+func getIDTokenWithSigner(signer cryptoSigner, p mockIDTokenPayload) string {
+	return getIDTokenWithSignerAndKid(signer, "mock-key-id-1", p)
 }
 
 func getIDToken(p mockIDTokenPayload) string {
-	return getIDTokenWithKid("mock-key-id-1", p)
+	return getIDTokenWithSigner(testSigner, p)
 }
 
 func getIDTokenWithKid(kid string, p mockIDTokenPayload) string {
+	return getIDTokenWithSignerAndKid(testSigner, kid, p)
+}
+
+func getEmulatedIDToken(p mockIDTokenPayload) string {
+	return getIDTokenWithSignerAndKid(emulatedSigner{}, "mock-key-id-1", p)
+}
+
+func getIDTokenWithSignerAndKid(signer cryptoSigner, kid string, p mockIDTokenPayload) string {
 	pCopy := mockIDTokenPayload{
 		"aud":       testProjectID,
 		"iss":       "https://securetoken.google.com/" + testProjectID,
@@ -1168,13 +1343,13 @@ func getIDTokenWithKid(kid string, p mockIDTokenPayload) string {
 
 	info := &jwtInfo{
 		header: jwtHeader{
-			Algorithm: "RS256",
+			Algorithm: signer.Algorithm(),
 			Type:      "JWT",
 			KeyID:     kid,
 		},
 		payload: pCopy,
 	}
-	token, err := info.Token(context.Background(), testSigner)
+	token, err := info.Token(context.Background(), signer)
 	logFatal(err)
 	return token
 }
diff --git a/auth/token_verifier_test.go b/auth/token_verifier_test.go
index b60aa11..f226fd3 100644
--- a/auth/token_verifier_test.go
+++ b/auth/token_verifier_test.go
@@ -28,7 +28,7 @@ import (
 )
 
 func TestNewIDTokenVerifier(t *testing.T) {
-	tv, err := newIDTokenVerifier(context.Background(), testProjectID, true)
+	tv, err := newIDTokenVerifier(context.Background(), testProjectID)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -48,9 +48,6 @@ func TestNewIDTokenVerifier(t *testing.T) {
 	if ks.KeyURI != idTokenCertURL {
 		t.Errorf("tokenVerifier.certURL = %q; want = %q", ks.KeyURI, idTokenCertURL)
 	}
-	if tv.isEmulator != true {
-		t.Errorf("tokenVerifier.isEmulator = %t; want = %t", tv.isEmulator, true)
-	}
 }
 
 func TestHTTPKeySource(t *testing.T) {