Added Bearer Authorization spec

Closes: #1249
Change-type: minor
Signed-off-by: Miguel Casqueira <miguel@balena.io>
This commit is contained in:
Miguel Casqueira 2020-04-27 22:21:03 -04:00
parent 698435a5c8
commit 2007a98940
2 changed files with 76 additions and 21 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

@ -1,13 +1,16 @@
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: 48484,
listenPort: 12345,
timeout: 30000,
dbPath: './test/data/supervisor-api.sqlite',
};
const VALID_SECRET = 'secure_api_secret';
@ -16,11 +19,16 @@ const ALLOWED_INTERFACES = ['lo']; // Only need loopback since this is for testi
describe('SupervisorAPI authentication', () => {
let api: SupervisorAPI;
const mockedConfig = new Config({ db: new Database() });
// Set apiSecret that we can test with
mockedConfig.set({ apiSecret: VALID_SECRET });
before(() => {
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(),
@ -34,9 +42,13 @@ describe('SupervisorAPI authentication', () => {
);
});
after(done => {
after(async () => {
api.stop();
done();
try {
await fs.unlink(mockedOptions.dbPath);
} catch (e) {
/* noop */
}
});
it('finds no apiKey and rejects', async () => {
@ -56,17 +68,50 @@ describe('SupervisorAPI authentication', () => {
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 apiKey from Authorization header (ApiKey scheme)', async () => {
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> {