Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions spec/common/providers/https.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,175 @@ describe("onCallHandler", () => {
});
});

describe("enforceAuth", () => {
it("should handle valid auth token with enforcement enabled", async () => {
const projectId = getApp().options.projectId;
const idToken = generateIdToken(projectId);
await runCallableTest({
httpRequest: mockRequest(null, "application/json", {
authorization: "Bearer " + idToken,
}),
expectedData: null,
callableOption: {
cors: { origin: true, methods: "POST" },
enforceAuth: true,
},
callableFunction: (data, context) => {
checkAuthContext(context, projectId, mocks.user_id);
return null;
},
callableFunction2: (request) => {
checkAuthContext(request, projectId, mocks.user_id);
return null;
},
expectedHttpResponse: {
status: 200,
headers: expectedResponseHeaders,
body: { result: null },
},
});
});

it("should reject invalid auth token with enforcement enabled (default)", async () => {
const projectId = getApp().options.projectId;
const idToken = generateUnsignedIdToken(projectId);
await runCallableTest({
httpRequest: mockRequest(null, "application/json", {
authorization: "Bearer " + idToken,
}),
expectedData: null,
callableOption: {
cors: { origin: true, methods: "POST" },
},
callableFunction: () => {
return;
},
callableFunction2: () => {
return;
},
expectedHttpResponse: {
status: 401,
headers: expectedResponseHeaders,
body: {
error: {
message: "Unauthenticated",
status: "UNAUTHENTICATED",
},
},
},
});
});

it("should reject invalid auth token with enforceAuth explicitly true", async () => {
const projectId = getApp().options.projectId;
const idToken = generateUnsignedIdToken(projectId);
await runCallableTest({
httpRequest: mockRequest(null, "application/json", {
authorization: "Bearer " + idToken,
}),
expectedData: null,
callableOption: {
cors: { origin: true, methods: "POST" },
enforceAuth: true,
},
callableFunction: () => {
return;
},
callableFunction2: () => {
return;
},
expectedHttpResponse: {
status: 401,
headers: expectedResponseHeaders,
body: {
error: {
message: "Unauthenticated",
status: "UNAUTHENTICATED",
},
},
},
});
});

it("should allow invalid auth token with enforcement disabled", async () => {
const projectId = getApp().options.projectId;
const idToken = generateUnsignedIdToken(projectId);
await runCallableTest({
httpRequest: mockRequest(null, "application/json", {
authorization: "Bearer " + idToken,
}),
expectedData: null,
callableOption: {
cors: { origin: true, methods: "POST" },
enforceAuth: false,
},
callableFunction: (data, context) => {
expect(context.auth).to.be.undefined;
return null;
},
callableFunction2: (request) => {
expect(request.auth).to.be.undefined;
return null;
},
expectedHttpResponse: {
status: 200,
headers: expectedResponseHeaders,
body: { result: null },
},
});
});

it("should allow bad authorization header with enforcement disabled", async () => {
await runCallableTest({
httpRequest: mockRequest(null, "application/json", {
authorization: "Beaver heyyall",
}),
expectedData: null,
callableOption: {
cors: { origin: true, methods: "POST" },
enforceAuth: false,
},
callableFunction: (data, context) => {
expect(context.auth).to.be.undefined;
return null;
},
callableFunction2: (request) => {
expect(request.auth).to.be.undefined;
return null;
},
expectedHttpResponse: {
status: 200,
headers: expectedResponseHeaders,
body: { result: null },
},
});
});

it("should allow missing auth token with enforcement enabled", async () => {
await runCallableTest({
httpRequest: mockRequest(null, "application/json"),
expectedData: null,
callableOption: {
cors: { origin: true, methods: "POST" },
enforceAuth: true,
},
callableFunction: (data, context) => {
expect(context.auth).to.be.undefined;
return null;
},
callableFunction2: (request) => {
expect(request.auth).to.be.undefined;
return null;
},
expectedHttpResponse: {
status: 200,
headers: expectedResponseHeaders,
body: { result: null },
},
});
});
});

describe("AppCheck", () => {
describe("verify token", () => {
let mock: nock.Scope;
Expand Down
15 changes: 15 additions & 0 deletions spec/v2/providers/https.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,21 @@ describe("onCall", () => {
}
});

it("should allow boolean params for enforceAuth", async () => {
const enforceAuth = defineBoolean("ENFORCE_AUTH");
try {
process.env.ENFORCE_AUTH = "false";
const func = https.onCall({ enforceAuth }, () => 42);

const req = request({ headers: { authorization: "Beaver invalid" } }); // Invalid auth token
const resp = await runHandler(func, req);
expect(resp.status).to.equal(200);
} finally {
delete process.env.ENFORCE_AUTH;
clearParams();
}
});

it("should allow boolean params for consumeAppCheckToken", async () => {
const consumeAppCheckToken = defineBoolean("CONSUME_APP_CHECK_TOKEN");
sinon.stub(debug, "isDebugFeatureEnabled").withArgs("skipTokenVerification").returns(true);
Expand Down
10 changes: 9 additions & 1 deletion src/common/providers/https.ts
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,7 @@
export interface CallableOptions<T = any> {
cors: cors.CorsOptions;
enforceAppCheck?: boolean;
enforceAuth?: boolean;
consumeAppCheckToken?: boolean;
/* @deprecated */
authPolicy?: (token: AuthData | null, data: T) => boolean | Promise<boolean>;
Expand Down Expand Up @@ -818,8 +819,15 @@
}

const tokenStatus = await checkTokens(req, context, options);
// enforceAuth defaults to true (unlike enforceAppCheck which defaults to false)
if (tokenStatus.auth === "INVALID") {
throw new HttpsError("unauthenticated", "Unauthenticated");
if (options.enforceAuth !== false) {
throw new HttpsError("unauthenticated", "Unauthenticated");
} else {
logger.warn(

Check failure on line 827 in src/common/providers/https.ts

View workflow job for this annotation

GitHub Actions / lint (22.x)

Replace `⏎············"Allowing·request·with·invalid·auth·token·because·enforcement·is·disabled"⏎··········` with `"Allowing·request·with·invalid·auth·token·because·enforcement·is·disabled"`
"Allowing request with invalid auth token because enforcement is disabled"
);
}
Comment on lines +826 to +830
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure that context.auth is strictly undefined when an invalid token is provided and enforcement is disabled (as stated in the documentation), it should be explicitly cleared. This is particularly important because the emulator hook (lines 803-812) might have already populated context.auth with mock data, which should not be used if an actual (but invalid) token was sent in the request.

        } else {
          logger.warn(
            "Allowing request with invalid auth token because enforcement is disabled"
          );
          context.auth = undefined;
        }

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context.auth is already undefined in the INVALID path since checkAuthToken only sets it on success. This matches the existing enforceAppCheck behavior which also does not explicitly reset context.app.

}
if (tokenStatus.app === "INVALID") {
if (options.enforceAppCheck) {
Expand Down
10 changes: 10 additions & 0 deletions src/v1/function-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,16 @@ export interface RuntimeOptions {
*/
enforceAppCheck?: boolean;

/**
* Determines whether Firebase Auth is enforced. Defaults to true.
*
* @remarks
* When true, requests with invalid auth tokens autorespond with a 401
* (Unauthorized) error.
* When false, requests with invalid tokens set context.auth to undefined.
*/
enforceAuth?: boolean;

/**
* Determines whether Firebase App Check token is consumed on request. Defaults to false.
*
Expand Down
1 change: 1 addition & 0 deletions src/v1/providers/https.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export function _onCallWithOptions(
onCallHandler(
{
enforceAppCheck: options.enforceAppCheck,
enforceAuth: options.enforceAuth,
consumeAppCheckToken: options.consumeAppCheckToken,
cors: { origin: true, methods: "POST" },
},
Expand Down
12 changes: 11 additions & 1 deletion src/v2/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,16 @@
*/
enforceAppCheck?: boolean;

/**
* Determines whether Firebase Auth is enforced. Defaults to true.
*
* @remarks
* When true, requests with invalid auth tokens autorespond with a 401
* (Unauthorized) error.
* When false, requests with invalid tokens set `event.auth` to `undefined`.
*/
enforceAuth?: boolean;

/**
* Controls whether function configuration modified outside of function source is preserved. Defaults to false.
*
Expand Down Expand Up @@ -299,7 +309,7 @@
/**
* Additional fields that can be set on any event-handling function.
*/
export interface EventHandlerOptions extends Omit<GlobalOptions, "enforceAppCheck"> {
export interface EventHandlerOptions extends Omit<GlobalOptions, "enforceAppCheck" | "enforceAuth"> {

Check failure on line 312 in src/v2/options.ts

View workflow job for this annotation

GitHub Actions / lint (22.x)

Insert `⏎·`
/** Type of the event. */
eventType?: string;

Expand Down
16 changes: 15 additions & 1 deletion src/v2/providers/https.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
/**
* Options that can be set on an onRequest HTTPS function.
*/
export interface HttpsOptions extends Omit<GlobalOptions, "region" | "enforceAppCheck"> {
export interface HttpsOptions extends Omit<GlobalOptions, "region" | "enforceAppCheck" | "enforceAuth"> {

Check failure on line 58 in src/v2/providers/https.ts

View workflow job for this annotation

GitHub Actions / lint (22.x)

Insert `⏎·`
/**
* If true, do not deploy or emulate this function.
*/
Expand Down Expand Up @@ -185,6 +185,14 @@
*/
enforceAppCheck?: boolean | Expression<boolean>;

/**
* Determines whether Firebase Auth is enforced. Defaults to true.
* When true, requests with invalid tokens autorespond with a 401
* (Unauthorized) error.
* When false, requests with invalid tokens set event.auth to undefined.
*/
enforceAuth?: boolean | Expression<boolean>;

/**
* Determines whether Firebase App Check token is consumed on request. Defaults to false.
*
Expand Down Expand Up @@ -461,6 +469,11 @@
enforceAppCheck = enforceAppCheck.value();
}

let enforceAuth = opts.enforceAuth ?? options.getGlobalOptions().enforceAuth;
if (enforceAuth instanceof Expression) {
enforceAuth = enforceAuth.value();
}

let consumeAppCheckToken = opts.consumeAppCheckToken;
if (consumeAppCheckToken instanceof Expression) {
consumeAppCheckToken = consumeAppCheckToken.value();
Expand All @@ -470,6 +483,7 @@
{
cors: { origin, methods: "POST" },
enforceAppCheck,
enforceAuth,
consumeAppCheckToken,
heartbeatSeconds: opts.heartbeatSeconds,
authPolicy: opts.authPolicy,
Expand Down
Loading