Skip to content

Commit

Permalink
fix: emulator auth_time (#3608) (#3611)
Browse files Browse the repository at this point in the history
* fix: emulator auth_time (#3608)

Made emulator auth_time match how auth_time is populated in production. (auth_time should match user's lastLoginAt in seconds)

* Check not null just in case lastLoginAt is 0 because of unit test clock mocking

* Advance clock to verify auth_time is not refresh time

* assert user.lastLoginAt is not undefined

* Apply suggestions from code review

* Format code.

Co-authored-by: Yuchen Shi <yuchenshi@google.com>
  • Loading branch information
lovelle-cardoso and yuchenshi committed Jul 28, 2021
1 parent e4ab4cc commit 8e8043b
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
- Makes auth_time emulator behavior match production (auth_time is now set to user's last sign in time). (#3608)
- Fixes Auth Emulator errors when importing many users. (#3577)
- Fixes support for `--except` flag when used for deploying Hosting. (#3397)
8 changes: 7 additions & 1 deletion src/emulator/auth/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1726,7 +1726,12 @@ function generateJwt(
// This field is only set for anonymous sign-in but not for any other
// provider (such as email or Google) in production. Let's match that.
provider_id: signInProvider === "anonymous" ? signInProvider : undefined,
auth_time: toUnixTimestamp(new Date()),
auth_time:
user.lastLoginAt != null
? toUnixTimestamp(new Date(user.lastLoginAt))
: user.lastRefreshAt != null
? toUnixTimestamp(new Date(user.lastRefreshAt))
: toUnixTimestamp(new Date()),
user_id: user.localId,
firebase: {
identities,
Expand Down Expand Up @@ -2122,6 +2127,7 @@ export interface FirebaseJwtPayload {
exp: number; // expiresAt (in seconds since epoch)
iss: string; // issuer
aud: string; // audience (=projectId)
auth_time: number; // lastLoginAt (in seconds since epoch)
// ...and other fields that we don't care for now.

// Firebase-specific fields:
Expand Down
36 changes: 34 additions & 2 deletions src/test/emulators/auth/misc.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { expect } from "chai";
import { decode as decodeJwt, JwtHeader } from "jsonwebtoken";
import { UserInfo } from "../../../emulator/auth/state";
import { PROJECT_ID, signInWithPhoneNumber, TEST_PHONE_NUMBER } from "./helpers";
import {
getAccountInfoByIdToken,
PROJECT_ID,
signInWithPhoneNumber,
TEST_PHONE_NUMBER,
} from "./helpers";
import { describeAuthEmulator } from "./setup";
import {
expectStatusCode,
Expand All @@ -16,7 +21,7 @@ import {
} from "../../../emulator/auth/operations";
import { toUnixTimestamp } from "../../../emulator/auth/utils";

describeAuthEmulator("token refresh", ({ authApi }) => {
describeAuthEmulator("token refresh", ({ authApi, getClock }) => {
it("should exchange refresh token for new tokens", async () => {
const { refreshToken, localId } = await registerAnonUser(authApi());
await authApi()
Expand All @@ -39,6 +44,33 @@ describeAuthEmulator("token refresh", ({ authApi }) => {
});
});

it("should populate auth_time to match lastLoginAt (in seconds since epoch)", async () => {
const emailUser = { email: "alice@example.com", password: "notasecret" };
const { refreshToken } = await registerUser(authApi(), emailUser);

getClock().tick(2000); // Wait 2 seconds before refreshing.

const res = await authApi()
.post("/securetoken.googleapis.com/v1/token")
.type("form")
// snake_case parameters also work, per OAuth 2.0 spec.
.send({ refresh_token: refreshToken, grantType: "refresh_token" })
.query({ key: "fake-api-key" });

const idToken = res.body.id_token;
const user = await getAccountInfoByIdToken(authApi(), idToken);
expect(user.lastLoginAt).not.to.be.undefined;
const lastLoginAtSeconds = toUnixTimestamp(new Date(user.lastLoginAt!));
const decoded = decodeJwt(idToken, { complete: true }) as {
header: JwtHeader;
payload: FirebaseJwtPayload;
} | null;
expect(decoded, "JWT returned by emulator is invalid").not.to.be.null;
expect(decoded!.header.alg).to.eql("none");
// This should match login time, not token refresh time.
expect(decoded!.payload.auth_time).to.equal(lastLoginAtSeconds);
});

it("should error if user is disabled", async () => {
const { refreshToken, localId } = await registerAnonUser(authApi());
await updateAccountByLocalId(authApi(), localId, { disableUser: true });
Expand Down

0 comments on commit 8e8043b

Please sign in to comment.