Skip to content

Commit

Permalink
feat: better validation for optional auth (stoplightio#2401)
Browse files Browse the repository at this point in the history
  • Loading branch information
billiegoose committed Oct 9, 2023
1 parent e8acebd commit e2d9f0f
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,63 @@ describe('validateSecurity', () => {
});
});

describe('when security scheme is optional', () => {
const securityScheme: HttpSecurityScheme[][] = [
[
{
id: faker.random.word(),
scheme: 'bearer',
type: 'http',
key: 'sec',
extensions: { 'x-test': faker.random.word() },
},
],
[], // yeah that's how you do optional in OpenAPI
];

it('passes if no security scheme is used', () => {
assertRight(
validateSecurity({
element: { ...baseRequest, headers: {} },
resource: {
security: securityScheme,
},
})
);
});

it('passes the validation', () => {
assertRight(
validateSecurity({
element: { ...baseRequest, headers: { authorization: 'Bearer abc123' } },
resource: {
security: securityScheme,
},
})
);
});

it('fails with an invalid security scheme error', () => {
assertLeft(
validateSecurity({
element: { ...baseRequest, headers: { authorization: 'Basic abc123' } },
resource: {
security: securityScheme,
},
}),
res =>
expect(res).toStrictEqual([
{
code: 401,
message: 'Invalid security scheme used',
severity: DiagnosticSeverity.Error,
tags: ['Bearer', 'None'],
},
])
);
});
});

describe('OR relation between security schemes', () => {
const securityScheme: HttpSecurityScheme[][] = [
[
Expand Down Expand Up @@ -617,4 +674,144 @@ describe('validateSecurity', () => {
});
});
});

describe('Mix of AND and OR security schemes', () => {
const headerScheme: HttpSecurityScheme = {
id: faker.random.word(),
in: 'header' as const,
type: 'apiKey' as const,
name: 'x-api-key' as const,
key: 'sec' as const,
extensions: { 'x-test': 'test' },
};

const queryScheme: HttpSecurityScheme = {
id: faker.random.word(),
in: 'query' as const,
type: 'apiKey' as const,
name: 'x-api-key' as const,
key: 'sec' as const,
extensions: { 'x-test': 'test' },
};

const cookieScheme: HttpSecurityScheme = {
id: faker.random.word(),
in: 'cookie' as const,
type: 'apiKey' as const,
name: 'x-api-key' as const,
key: 'sec' as const,
extensions: { 'x-test': 'test' },
};

const bearerScheme: HttpSecurityScheme = {
id: faker.random.word(),
scheme: 'bearer',
type: 'http',
key: 'sec',
extensions: { 'x-test': faker.random.word() },
};

const oauth2Scheme: HttpSecurityScheme = {
id: faker.random.word(),
type: 'oauth2',
flows: {},
key: 'sec',
extensions: { 'x-test': faker.random.word() },
};

const openIdScheme: HttpSecurityScheme = {
id: faker.random.word(),
type: 'openIdConnect',
openIdConnectUrl: 'https://google.it',
key: 'sec',
extensions: { 'x-test': faker.random.word() },
};

const securityScheme: HttpSecurityScheme[][] = [
// one of
[
// all of
cookieScheme,
],
[
// all of
queryScheme,
oauth2Scheme,
],
[
// all of
bearerScheme,
headerScheme,
openIdScheme,
],
];

it('case 1 passes the validation', () => {
assertRight(
validateSecurity({
element: {
...baseRequest,
headers: { cookie: 'x-api-key=abc123' },
},
resource: {
security: securityScheme,
},
})
);
});

it('case 2 passes the validation', () => {
assertRight(
validateSecurity({
element: {
...baseRequest,
headers: { authorization: 'Bearer abc123' },
url: { path: '/', query: { 'x-api-key': 'abc123' } },
},
resource: {
security: securityScheme,
},
})
);
});

it('case 3 passes the validation', () => {
assertRight(
validateSecurity({
element: {
...baseRequest,
headers: { 'x-api-key': 'abc123', authorization: 'Bearer abc123' },
url: { path: '/', query: { 'x-api-key': 'abc123' } },
},
resource: {
security: securityScheme,
},
})
);
});

it('fails with an invalid security scheme error', () => {
assertLeft(
validateSecurity({
element: {
...baseRequest,
headers: { 'x-api-key': 'abc123' },
url: { path: '/', query: { 'x-api-key': 'abc123' } },
},
resource: {
security: securityScheme,
},
}),
res =>
expect(res).toStrictEqual([
{
code: 401,
message: 'Invalid security scheme used',
severity: DiagnosticSeverity.Error,
tags: ['OAuth2', 'Bearer', 'OpenID'],
},
])
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { apiKeyInCookie, apiKeyInHeader, apiKeyInQuery } from './apiKey';
import { httpBasic } from './basicAuth';
import { httpDigest } from './digestAuth';
import { bearer, oauth2, openIdConnect } from './bearer';
import { none } from './none';
import { HttpSecurityScheme, DiagnosticSeverity } from '@stoplight/types';
import { ValidateSecurityFn } from './utils';
import { Either, fromNullable } from 'fp-ts/Either';
Expand All @@ -20,6 +21,7 @@ const securitySchemeHandlers: {
basic: ValidateSecurityFn;
bearer: ValidateSecurityFn;
};
none: ValidateSecurityFn;
} = {
openIdConnect,
oauth2,
Expand All @@ -33,6 +35,7 @@ const securitySchemeHandlers: {
basic: httpBasic,
bearer,
},
none,
};

function createDiagnosticFor(scheme: string): IPrismDiagnostic {
Expand All @@ -55,3 +58,5 @@ export function findSecurityHandler(scheme: HttpSecurityScheme): Either<IPrismDi
}
return fromNullable(createDiagnosticFor(scheme.type))(securitySchemeHandlers[scheme.type]);
}

export { none as noneSecurityHandler };
12 changes: 12 additions & 0 deletions packages/http/src/validator/validators/security/handlers/none.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { get } from 'lodash';

import { when } from './utils';
import { Dictionary } from '@stoplight/types';
import { IHttpRequest } from '../../../../types';

// Makes sure there aren't any auth headers
function isNoAuth(inputHeaders: Dictionary<string>) {
return get(inputHeaders, 'authorization') == undefined;
}

export const none = (input: Pick<IHttpRequest, 'headers' | 'url'>) => when(isNoAuth(input.headers || {}), 'None');
12 changes: 10 additions & 2 deletions packages/http/src/validator/validators/security/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as O from 'fp-ts/Option';
import { pipe } from 'fp-ts/function';
import { flatten } from 'lodash';
import { set } from 'lodash/fp';
import { findSecurityHandler } from './handlers';
import { findSecurityHandler, noneSecurityHandler } from './handlers';
import { NonEmptyArray, getSemigroup } from 'fp-ts/NonEmptyArray';
import { isNonEmpty, sequence } from 'fp-ts/Array';
import { IPrismDiagnostic, ValidatorFn } from '@stoplight/prism-core';
Expand Down Expand Up @@ -35,7 +35,15 @@ function getAuthenticationArray(securitySchemes: HttpSecurityScheme[][], input:
E.mapLeft<IPrismDiagnostic, NonEmptyArray<IPrismDiagnostic>>(e => [e])
)
);

// an empty array indicates "optional" security,
// in which case we run the special `None` validator
if (securitySchemePairs.length === 0) {
const optionalCheck = pipe(
noneSecurityHandler(input),
E.mapLeft<IPrismDiagnostic, NonEmptyArray<IPrismDiagnostic>>(e => [e])
);
authResults.push(optionalCheck);
}
return eitherSequence(authResults);
});
}
Expand Down

0 comments on commit e2d9f0f

Please sign in to comment.