Merge pull request #1279 from balena-io/1249-support-bearer-token-header

Support Bearer scheme in Authorization header
This commit is contained in:
M. Casqueira 2020-05-04 13:34:00 -04:00 committed by GitHub
commit c127adcf03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 155 additions and 13 deletions

View File

@ -12,18 +12,21 @@ import { checkTruthy } from './lib/validation';
import log from './lib/supervisor-console';
function getKeyFromReq(req: express.Request): string | null {
const queryKey = req.query.apikey;
if (queryKey != null) {
return queryKey;
function getKeyFromReq(req: express.Request): string | undefined {
// Check query for key
if (req.query.apikey) {
return req.query.apikey;
}
const maybeHeaderKey = req.get('Authorization');
if (!maybeHeaderKey) {
return null;
// Get Authorization header to search for key
const authHeader = req.get('Authorization');
// Check header for key
if (!authHeader) {
return undefined;
}
const match = maybeHeaderKey.match(/^ApiKey (\w+)$/);
return match != null ? match[1] : null;
// Check authHeader with various schemes
const match = authHeader.match(/^(?:ApiKey|Bearer) (\w+)$/i);
// Return key from match or undefined
return match?.[1];
}
function authenticate(config: Config): express.RequestHandler {
@ -87,6 +90,14 @@ export class SupervisorAPI {
private api = express();
private server: Server | null = null;
// Holds the function which should apply iptables rules
private applyRules: SupervisorAPI['applyListeningRules'] =
process.env.TEST === '1'
? () => {
// don't try to alter iptables
// rules while we're running in tests
}
: this.applyListeningRules.bind(this);
public constructor({
config,
@ -176,13 +187,12 @@ export class SupervisorAPI {
apiTimeout: number,
): Promise<void> {
const localMode = await this.config.get('localMode');
await this.applyListeningRules(localMode || false, port, allowedInterfaces);
await this.applyRules(localMode || false, port, allowedInterfaces);
// Monitor the switching of local mode, and change which interfaces will
// be listened to based on that
this.config.on('change', changedConfig => {
if (changedConfig.localMode != null) {
this.applyListeningRules(
this.applyRules(
changedConfig.localMode || false,
port,
allowedInterfaces,

View File

@ -0,0 +1,132 @@
import { expect } from 'chai';
import { fs } from 'mz';
import * as requestLib from 'request';
import Config from '../src/config';
import Database from '../src/db';
import EventTracker from '../src/event-tracker';
import SupervisorAPI from '../src/supervisor-api';
const mockedOptions = {
listenPort: 12345,
timeout: 30000,
dbPath: './test/data/supervisor-api.sqlite',
};
const VALID_SECRET = 'secure_api_secret';
const INVALID_SECRET = 'bad_api_secret';
const ALLOWED_INTERFACES = ['lo']; // Only need loopback since this is for testing
describe('SupervisorAPI authentication', () => {
let api: SupervisorAPI;
before(async () => {
const db = new Database({
databasePath: mockedOptions.dbPath,
});
await db.init();
const mockedConfig = new Config({ db });
await mockedConfig.init();
// Set apiSecret that we can test with
await mockedConfig.set({ apiSecret: VALID_SECRET });
api = new SupervisorAPI({
config: mockedConfig,
eventTracker: new EventTracker(),
routers: [],
healthchecks: [],
});
return api.listen(
ALLOWED_INTERFACES,
mockedOptions.listenPort,
mockedOptions.timeout,
);
});
after(async () => {
api.stop();
try {
await fs.unlink(mockedOptions.dbPath);
} catch (e) {
/* noop */
}
});
it('finds no apiKey and rejects', async () => {
const response = await postAsync('/v1/blink');
expect(response.statusCode).to.equal(401);
});
it('finds apiKey from query', async () => {
const response = await postAsync(`/v1/blink?apikey=${VALID_SECRET}`);
expect(response.statusCode).to.equal(200);
});
it('finds apiKey from Authorization header (ApiKey scheme)', async () => {
const response = await postAsync(`/v1/blink`, {
Authorization: `ApiKey ${VALID_SECRET}`,
});
expect(response.statusCode).to.equal(200);
});
it('finds apiKey from Authorization header (Bearer scheme)', async () => {
const response = await postAsync(`/v1/blink`, {
Authorization: `Bearer ${VALID_SECRET}`,
});
expect(response.statusCode).to.equal(200);
});
it('finds apiKey from Authorization header (case insensitive)', async () => {
const randomCases = [
'Bearer',
'bearer',
'BEARER',
'BeAReR',
'ApiKey',
'apikey',
'APIKEY',
'ApIKeY',
];
for (const scheme of randomCases) {
const response = await postAsync(`/v1/blink`, {
Authorization: `${scheme} ${VALID_SECRET}`,
});
expect(response.statusCode).to.equal(200);
}
});
it('rejects invalid apiKey from query', async () => {
const response = await postAsync(`/v1/blink?apikey=${INVALID_SECRET}`);
expect(response.statusCode).to.equal(401);
});
it('rejects invalid apiKey from Authorization header (ApiKey scheme)', async () => {
const response = await postAsync(`/v1/blink`, {
Authorization: `ApiKey ${INVALID_SECRET}`,
});
expect(response.statusCode).to.equal(401);
});
it('rejects invalid apiKey from Authorization header (Bearer scheme)', async () => {
const response = await postAsync(`/v1/blink`, {
Authorization: `Bearer ${INVALID_SECRET}`,
});
expect(response.statusCode).to.equal(401);
});
});
function postAsync(path: string, headers = {}): Promise<any> {
return new Promise((resolve, reject) => {
requestLib.post(
{
url: `http://127.0.0.1:${mockedOptions.listenPort}${path}`,
headers,
},
(error: Error, response: requestLib.Response) => {
if (error) {
reject(error);
}
resolve(response);
},
);
});
}