Skip to content

Commit 60a7a40

Browse files
authored
Require auth token but skip verification for tasks queue functions. (#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
1 parent c32db65 commit 60a7a40

File tree

5 files changed

+42
-58
lines changed

5 files changed

+42
-58
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Improve authorization for tasks. (#1073)

spec/common/providers/tasks.spec.ts

Lines changed: 27 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,12 @@
2121
// SOFTWARE.
2222

2323
import { expect } from 'chai';
24-
import * as sinon from 'sinon';
2524
import * as firebase from 'firebase-admin';
2625

2726
import { checkAuthContext, runHandler } from '../../helper';
2827
import {
2928
generateIdToken,
3029
generateUnsignedIdToken,
31-
mockFetchPublicKeys,
3230
mockRequest,
3331
} from '../../fixtures/mockrequest';
3432
import {
@@ -39,7 +37,6 @@ import {
3937
import { apps as appsNamespace } from '../../../src/apps';
4038
import * as mocks from '../../fixtures/credential/key.json';
4139
import * as https from '../../../src/common/providers/https';
42-
import * as debug from '../../../src/common/debug';
4340

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

83+
function mockEnqueueRequest(
84+
data: unknown,
85+
contentType: string = 'application/json',
86+
context: { authorization: string } = { authorization: 'Bearer abc' }
87+
): ReturnType<typeof mockRequest> {
88+
return mockRequest(data, contentType, context);
89+
}
90+
8691
before(() => {
8792
const credential = {
8893
getAccessToken: () => {
@@ -111,7 +116,7 @@ describe('onEnqueueHandler', () => {
111116

112117
it('should handle success', () => {
113118
return runTaskTest({
114-
httpRequest: mockRequest({ foo: 'bar' }),
119+
httpRequest: mockEnqueueRequest({ foo: 'bar' }),
115120
expectedData: { foo: 'bar' },
116121
expectedStatus: 204,
117122
});
@@ -129,22 +134,22 @@ describe('onEnqueueHandler', () => {
129134

130135
it('should ignore charset', () => {
131136
return runTaskTest({
132-
httpRequest: mockRequest(null, 'application/json; charset=utf-8'),
137+
httpRequest: mockEnqueueRequest(null, 'application/json; charset=utf-8'),
133138
expectedData: null,
134139
expectedStatus: 204,
135140
});
136141
});
137142

138143
it('should reject bad content type', () => {
139144
return runTaskTest({
140-
httpRequest: mockRequest(null, 'text/plain'),
145+
httpRequest: mockEnqueueRequest(null, 'text/plain'),
141146
expectedData: null,
142147
expectedStatus: 400,
143148
});
144149
});
145150

146151
it('should reject extra body fields', () => {
147-
const req = mockRequest(null);
152+
const req = mockEnqueueRequest(null);
148153
req.body.extra = 'bad';
149154
return runTaskTest({
150155
httpRequest: req,
@@ -155,7 +160,7 @@ describe('onEnqueueHandler', () => {
155160

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

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

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

198203
it('should handle auth', async () => {
199-
const mock = mockFetchPublicKeys();
200204
const projectId = appsNamespace().admin.options.projectId;
201205
const idToken = generateIdToken(projectId);
202206
await runTaskTest({
203-
httpRequest: mockRequest(null, 'application/json', {
207+
httpRequest: mockEnqueueRequest(null, 'application/json', {
204208
authorization: 'Bearer ' + idToken,
205209
}),
206210
expectedData: null,
@@ -214,49 +218,25 @@ describe('onEnqueueHandler', () => {
214218
},
215219
expectedStatus: 204,
216220
});
217-
mock.done();
218221
});
219222

220-
it('should reject bad auth', async () => {
223+
it('should accept unsigned auth too', async () => {
221224
const projectId = appsNamespace().admin.options.projectId;
222225
const idToken = generateUnsignedIdToken(projectId);
223226
await runTaskTest({
224-
httpRequest: mockRequest(null, 'application/json', {
227+
httpRequest: mockEnqueueRequest(null, 'application/json', {
225228
authorization: 'Bearer ' + idToken,
226229
}),
227230
expectedData: null,
228-
expectedStatus: 401,
229-
});
230-
});
231-
232-
describe('skip token verification debug mode support', () => {
233-
before(() => {
234-
sinon
235-
.stub(debug, 'isDebugFeatureEnabled')
236-
.withArgs('skipTokenVerification')
237-
.returns(true);
238-
});
239-
240-
after(() => {
241-
sinon.verifyAndRestore();
242-
});
243-
244-
it('should skip auth token verification', async () => {
245-
const projectId = appsNamespace().admin.options.projectId;
246-
const idToken = generateUnsignedIdToken(projectId);
247-
await runTaskTest({
248-
httpRequest: mockRequest(null, 'application/json', {
249-
authorization: 'Bearer ' + idToken,
250-
}),
251-
expectedData: null,
252-
taskFunction: (data, context) => {
253-
checkAuthContext(context, projectId, mocks.user_id);
254-
},
255-
taskFunction2: (request) => {
256-
checkAuthContext(request, projectId, mocks.user_id);
257-
},
258-
expectedStatus: 204,
259-
});
231+
taskFunction: (data, context) => {
232+
checkAuthContext(context, projectId, mocks.user_id);
233+
return null;
234+
},
235+
taskFunction2: (request) => {
236+
checkAuthContext(request, projectId, mocks.user_id);
237+
return null;
238+
},
239+
expectedStatus: 204,
260240
});
261241
});
262242
});

spec/v1/providers/tasks.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ describe('#onDispatch', () => {
146146
},
147147
{
148148
'content-type': 'application/json',
149+
authorization: 'Bearer abc',
149150
}
150151
);
151152
req.method = 'POST';

spec/v2/providers/tasks.spec.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -149,16 +149,9 @@ describe('onTaskDispatched', () => {
149149
});
150150

151151
it('has a .run method', async () => {
152-
const request: any = {
153-
data: 'data',
154-
auth: {
155-
uid: 'abc',
156-
token: 'token',
157-
},
158-
};
152+
const request: any = { data: 'data' };
159153
const cf = onTaskDispatched((r) => {
160154
expect(r.data).to.deep.equal(request.data);
161-
expect(r.auth).to.deep.equal(request.auth);
162155
});
163156

164157
await cf.run(request);
@@ -173,6 +166,7 @@ describe('onTaskDispatched', () => {
173166
},
174167
{
175168
'content-type': 'application/json',
169+
authorization: 'Bearer abc',
176170
origin: 'example.com',
177171
}
178172
);

src/common/providers/tasks.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,20 @@ export function onDispatchHandler<Req = any>(
110110
throw new https.HttpsError('invalid-argument', 'Bad Request');
111111
}
112112

113-
const context: TaskContext = {};
114-
const status = await https.checkAuthToken(req, context);
113+
const authHeader = req.header('Authorization') || '';
114+
const token = authHeader.match(/^Bearer (.*)$/)?.[1];
115115
// Note: this should never happen since task queue functions are guarded by IAM.
116-
if (status === 'INVALID') {
116+
if (!token) {
117117
throw new https.HttpsError('unauthenticated', 'Unauthenticated');
118118
}
119+
// We skip authenticating the token since tq functions are guarded by IAM.
120+
const authToken = await https.unsafeDecodeIdToken(token);
121+
const context: TaskContext = {
122+
auth: {
123+
uid: authToken.uid,
124+
token: authToken,
125+
},
126+
};
119127

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

0 commit comments

Comments
 (0)