mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-21 02:01:35 +00:00
Merge pull request #1279 from balena-io/1249-support-bearer-token-header
Support Bearer scheme in Authorization header
This commit is contained in:
commit
c127adcf03
@ -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,
|
||||
|
132
test/27-supervisor-api-auth.spec.ts
Normal file
132
test/27-supervisor-api-auth.spec.ts
Normal 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);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user