Skip to content

Commit

Permalink
Require auth token but skip verification for tasks queue functions. (#…
Browse files Browse the repository at this point in the history
…1073)

Previously, we allowed TQ functions to either:

1) Not have any auth token
2) Have admin SDK verified auth tokens.

Since TQ functions are guarded by IAM, we instead implement the following behavior:

1) MUST have auth token
2) We do not verify auth token - instead we just decode it and pass it to the handler
  • Loading branch information
taeold committed Apr 12, 2022
1 parent c32db65 commit 60a7a40
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 58 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Improve authorization for tasks. (#1073)
74 changes: 27 additions & 47 deletions spec/common/providers/tasks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,12 @@
// SOFTWARE.

import { expect } from 'chai';
import * as sinon from 'sinon';
import * as firebase from 'firebase-admin';

import { checkAuthContext, runHandler } from '../../helper';
import {
generateIdToken,
generateUnsignedIdToken,
mockFetchPublicKeys,
mockRequest,
} from '../../fixtures/mockrequest';
import {
Expand All @@ -39,7 +37,6 @@ import {
import { apps as appsNamespace } from '../../../src/apps';
import * as mocks from '../../fixtures/credential/key.json';
import * as https from '../../../src/common/providers/https';
import * as debug from '../../../src/common/debug';

/** Represents a test case for a Task Queue Function */
interface TaskTest {
Expand Down Expand Up @@ -83,6 +80,14 @@ export async function runTaskTest(test: TaskTest): Promise<any> {
describe('onEnqueueHandler', () => {
let app: firebase.app.App;

function mockEnqueueRequest(
data: unknown,
contentType: string = 'application/json',
context: { authorization: string } = { authorization: 'Bearer abc' }
): ReturnType<typeof mockRequest> {
return mockRequest(data, contentType, context);
}

before(() => {
const credential = {
getAccessToken: () => {
Expand Down Expand Up @@ -111,7 +116,7 @@ describe('onEnqueueHandler', () => {

it('should handle success', () => {
return runTaskTest({
httpRequest: mockRequest({ foo: 'bar' }),
httpRequest: mockEnqueueRequest({ foo: 'bar' }),
expectedData: { foo: 'bar' },
expectedStatus: 204,
});
Expand All @@ -129,22 +134,22 @@ describe('onEnqueueHandler', () => {

it('should ignore charset', () => {
return runTaskTest({
httpRequest: mockRequest(null, 'application/json; charset=utf-8'),
httpRequest: mockEnqueueRequest(null, 'application/json; charset=utf-8'),
expectedData: null,
expectedStatus: 204,
});
});

it('should reject bad content type', () => {
return runTaskTest({
httpRequest: mockRequest(null, 'text/plain'),
httpRequest: mockEnqueueRequest(null, 'text/plain'),
expectedData: null,
expectedStatus: 400,
});
});

it('should reject extra body fields', () => {
const req = mockRequest(null);
const req = mockEnqueueRequest(null);
req.body.extra = 'bad';
return runTaskTest({
httpRequest: req,
Expand All @@ -155,7 +160,7 @@ describe('onEnqueueHandler', () => {

it('should handle unhandled error', () => {
return runTaskTest({
httpRequest: mockRequest(null),
httpRequest: mockEnqueueRequest(null),
expectedData: null,
taskFunction: (data, context) => {
throw new Error(`ceci n'est pas une error`);
Expand All @@ -169,7 +174,7 @@ describe('onEnqueueHandler', () => {

it('should handle unknown error status', () => {
return runTaskTest({
httpRequest: mockRequest(null),
httpRequest: mockEnqueueRequest(null),
expectedData: null,
taskFunction: (data, context) => {
throw new https.HttpsError('THIS_IS_NOT_VALID' as any, 'nope');
Expand All @@ -183,7 +188,7 @@ describe('onEnqueueHandler', () => {

it('should handle well-formed error', () => {
return runTaskTest({
httpRequest: mockRequest(null),
httpRequest: mockEnqueueRequest(null),
expectedData: null,
taskFunction: (data, context) => {
throw new https.HttpsError('not-found', 'i am error');
Expand All @@ -196,11 +201,10 @@ describe('onEnqueueHandler', () => {
});

it('should handle auth', async () => {
const mock = mockFetchPublicKeys();
const projectId = appsNamespace().admin.options.projectId;
const idToken = generateIdToken(projectId);
await runTaskTest({
httpRequest: mockRequest(null, 'application/json', {
httpRequest: mockEnqueueRequest(null, 'application/json', {
authorization: 'Bearer ' + idToken,
}),
expectedData: null,
Expand All @@ -214,49 +218,25 @@ describe('onEnqueueHandler', () => {
},
expectedStatus: 204,
});
mock.done();
});

it('should reject bad auth', async () => {
it('should accept unsigned auth too', async () => {
const projectId = appsNamespace().admin.options.projectId;
const idToken = generateUnsignedIdToken(projectId);
await runTaskTest({
httpRequest: mockRequest(null, 'application/json', {
httpRequest: mockEnqueueRequest(null, 'application/json', {
authorization: 'Bearer ' + idToken,
}),
expectedData: null,
expectedStatus: 401,
});
});

describe('skip token verification debug mode support', () => {
before(() => {
sinon
.stub(debug, 'isDebugFeatureEnabled')
.withArgs('skipTokenVerification')
.returns(true);
});

after(() => {
sinon.verifyAndRestore();
});

it('should skip auth token verification', async () => {
const projectId = appsNamespace().admin.options.projectId;
const idToken = generateUnsignedIdToken(projectId);
await runTaskTest({
httpRequest: mockRequest(null, 'application/json', {
authorization: 'Bearer ' + idToken,
}),
expectedData: null,
taskFunction: (data, context) => {
checkAuthContext(context, projectId, mocks.user_id);
},
taskFunction2: (request) => {
checkAuthContext(request, projectId, mocks.user_id);
},
expectedStatus: 204,
});
taskFunction: (data, context) => {
checkAuthContext(context, projectId, mocks.user_id);
return null;
},
taskFunction2: (request) => {
checkAuthContext(request, projectId, mocks.user_id);
return null;
},
expectedStatus: 204,
});
});
});
1 change: 1 addition & 0 deletions spec/v1/providers/tasks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ describe('#onDispatch', () => {
},
{
'content-type': 'application/json',
authorization: 'Bearer abc',
}
);
req.method = 'POST';
Expand Down
10 changes: 2 additions & 8 deletions spec/v2/providers/tasks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,9 @@ describe('onTaskDispatched', () => {
});

it('has a .run method', async () => {
const request: any = {
data: 'data',
auth: {
uid: 'abc',
token: 'token',
},
};
const request: any = { data: 'data' };
const cf = onTaskDispatched((r) => {
expect(r.data).to.deep.equal(request.data);
expect(r.auth).to.deep.equal(request.auth);
});

await cf.run(request);
Expand All @@ -173,6 +166,7 @@ describe('onTaskDispatched', () => {
},
{
'content-type': 'application/json',
authorization: 'Bearer abc',
origin: 'example.com',
}
);
Expand Down
14 changes: 11 additions & 3 deletions src/common/providers/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,20 @@ export function onDispatchHandler<Req = any>(
throw new https.HttpsError('invalid-argument', 'Bad Request');
}

const context: TaskContext = {};
const status = await https.checkAuthToken(req, context);
const authHeader = req.header('Authorization') || '';
const token = authHeader.match(/^Bearer (.*)$/)?.[1];
// Note: this should never happen since task queue functions are guarded by IAM.
if (status === 'INVALID') {
if (!token) {
throw new https.HttpsError('unauthenticated', 'Unauthenticated');
}
// We skip authenticating the token since tq functions are guarded by IAM.
const authToken = await https.unsafeDecodeIdToken(token);
const context: TaskContext = {
auth: {
uid: authToken.uid,
token: authToken,
},
};

const data: Req = https.decode(req.body.data);
if (handler.length === 2) {
Expand Down

0 comments on commit 60a7a40

Please sign in to comment.