mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-22 18:22:41 +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';
|
import log from './lib/supervisor-console';
|
||||||
|
|
||||||
function getKeyFromReq(req: express.Request): string | null {
|
function getKeyFromReq(req: express.Request): string | undefined {
|
||||||
const queryKey = req.query.apikey;
|
// Check query for key
|
||||||
if (queryKey != null) {
|
if (req.query.apikey) {
|
||||||
return queryKey;
|
return req.query.apikey;
|
||||||
}
|
}
|
||||||
const maybeHeaderKey = req.get('Authorization');
|
// Get Authorization header to search for key
|
||||||
if (!maybeHeaderKey) {
|
const authHeader = req.get('Authorization');
|
||||||
return null;
|
// Check header for key
|
||||||
|
if (!authHeader) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
// Check authHeader with various schemes
|
||||||
const match = maybeHeaderKey.match(/^ApiKey (\w+)$/);
|
const match = authHeader.match(/^(?:ApiKey|Bearer) (\w+)$/i);
|
||||||
return match != null ? match[1] : null;
|
// Return key from match or undefined
|
||||||
|
return match?.[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
function authenticate(config: Config): express.RequestHandler {
|
function authenticate(config: Config): express.RequestHandler {
|
||||||
@ -87,6 +90,14 @@ export class SupervisorAPI {
|
|||||||
|
|
||||||
private api = express();
|
private api = express();
|
||||||
private server: Server | null = null;
|
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({
|
public constructor({
|
||||||
config,
|
config,
|
||||||
@ -176,13 +187,12 @@ export class SupervisorAPI {
|
|||||||
apiTimeout: number,
|
apiTimeout: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const localMode = await this.config.get('localMode');
|
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
|
// Monitor the switching of local mode, and change which interfaces will
|
||||||
// be listened to based on that
|
// be listened to based on that
|
||||||
this.config.on('change', changedConfig => {
|
this.config.on('change', changedConfig => {
|
||||||
if (changedConfig.localMode != null) {
|
if (changedConfig.localMode != null) {
|
||||||
this.applyListeningRules(
|
this.applyRules(
|
||||||
changedConfig.localMode || false,
|
changedConfig.localMode || false,
|
||||||
port,
|
port,
|
||||||
allowedInterfaces,
|
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