Skip to content

Commit 2265a10

Browse files
fix: correct security schema logic for OR verification (#946)
1 parent f022d21 commit 2265a10

File tree

3 files changed

+73
-46
lines changed

3 files changed

+73
-46
lines changed

Diff for: src/middlewares/openapi.security.ts

+29-45
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {
2-
OpenAPIV3,
2+
HttpError,
3+
InternalServerError,
34
OpenApiRequest,
4-
SecurityHandlers,
5-
OpenApiRequestMetadata,
65
OpenApiRequestHandler,
7-
InternalServerError,
8-
HttpError,
6+
OpenApiRequestMetadata,
7+
OpenAPIV3,
8+
SecurityHandlers,
99
} from '../framework/types';
1010

1111
const defaultSecurityHandler = (
@@ -17,11 +17,30 @@ const defaultSecurityHandler = (
1717
type SecuritySchemesMap = {
1818
[key: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.SecuritySchemeObject;
1919
};
20+
2021
interface SecurityHandlerResult {
2122
success: boolean;
2223
status?: number;
2324
error?: string;
2425
}
26+
27+
function extractErrorsFromResults(results: (SecurityHandlerResult | SecurityHandlerResult[])[]) {
28+
return results.map(result => {
29+
if (Array.isArray(result)) {
30+
return result.map(it => it).filter(it => !it.success);
31+
}
32+
return [result].filter(it => !it.success);
33+
}).flatMap(it => [...it]);
34+
}
35+
36+
function didAllSecurityRequirementsPass(results: SecurityHandlerResult[]) {
37+
return results.every(it => it.success);
38+
}
39+
40+
function didOneSchemaPassValidation(results: (SecurityHandlerResult | SecurityHandlerResult[])[]) {
41+
return results.some(result => Array.isArray(result) ? didAllSecurityRequirementsPass(result) : result.success);
42+
}
43+
2544
export function security(
2645
apiDoc: OpenAPIV3.Document,
2746
securityHandlers: SecurityHandlers,
@@ -62,50 +81,13 @@ export function security(
6281

6382
// TODO handle AND'd and OR'd security
6483
// This assumes OR only! i.e. at least one security passed authentication
65-
let firstError: SecurityHandlerResult = null;
66-
let success = false;
67-
68-
function checkErrorWithOr(res) {
69-
return res.forEach((r) => {
70-
if (r.success) {
71-
success = true;
72-
} else if (!firstError) {
73-
firstError = r;
74-
}
75-
});
76-
}
77-
78-
function checkErrorsWithAnd(res) {
79-
let allSuccess = false;
80-
81-
res.forEach((r) => {
82-
if (!r.success) {
83-
allSuccess = false;
84-
if (!firstError) {
85-
firstError = r;
86-
}
87-
} else if (!firstError) {
88-
allSuccess = true;
89-
}
90-
});
91-
92-
if (allSuccess) {
93-
success = true;
94-
}
95-
}
96-
97-
results.forEach((result) => {
98-
if (Array.isArray(result) && result.length > 1) {
99-
checkErrorsWithAnd(result);
100-
} else {
101-
checkErrorWithOr(result);
102-
}
103-
});
84+
const success = didOneSchemaPassValidation(results);
10485

10586
if (success) {
10687
next();
10788
} else {
108-
throw firstError;
89+
const errors = extractErrorsFromResults(results)
90+
throw errors[0]
10991
}
11092
} catch (e) {
11193
const message = e?.error?.message || 'unauthorized';
@@ -129,6 +111,7 @@ class SecuritySchemes {
129111
private securitySchemes: SecuritySchemesMap;
130112
private securityHandlers: SecurityHandlers;
131113
private securities: OpenAPIV3.SecurityRequirementObject[];
114+
132115
constructor(
133116
securitySchemes: SecuritySchemesMap,
134117
securityHandlers: SecurityHandlers,
@@ -213,6 +196,7 @@ class AuthValidator {
213196
private scheme;
214197
private path: string;
215198
private scopes: string[];
199+
216200
constructor(req: OpenApiRequest, scheme, scopes: string[] = []) {
217201
const openapi = <OpenApiRequestMetadata>req.openapi;
218202
this.req = req;

Diff for: test/resources/security.yaml

+13
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,19 @@ paths:
9090
"401":
9191
description: unauthorized
9292

93+
/multi_auth:
94+
get:
95+
security:
96+
- ApiKeyAuth: []
97+
BearerAuth: []
98+
- BasicAuth: []
99+
CookieAuth: []
100+
responses:
101+
"200":
102+
description: OK
103+
"401":
104+
description: unauthorized
105+
93106
/oauth2:
94107
get:
95108
security:

Diff for: test/security.defaults.spec.ts

+31-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ describe('security.defaults', () => {
2121
.get(`/cookie_auth`, (req, res) => res.json({ logged_in: true }))
2222
.get(`/bearer`, (req, res) => res.json({ logged_in: true }))
2323
.get(`/basic`, (req, res) => res.json({ logged_in: true }))
24-
.get('/no_security', (req, res) => res.json({ logged_in: true })),
24+
.get('/no_security', (req, res) => res.json({ logged_in: true }))
25+
.get('/multi_auth', (req, res) => res.json({ logged_in: true }))
2526
);
2627
});
2728

@@ -64,4 +65,33 @@ describe('security.defaults', () => {
6465
.that.equals('cookie \'JSESSIONID\' required');
6566
});
6667
});
68+
69+
it("Should return 200 WHEN one of multiple security rules is met GIVEN a request with apiKey and bearer token", () => {
70+
return request(app)
71+
.get(`${basePath}/multi_auth`)
72+
.set({
73+
"Authorization": "Bearer rawww",
74+
"X-API-Key": "hello world"
75+
})
76+
.expect(200)
77+
})
78+
79+
it("Should return 200 WHEN one of multiple security rules is met GIVEN a request with cookie and basic auth", () => {
80+
return request(app)
81+
.get(`${basePath}/multi_auth`)
82+
.set({
83+
"Authorization": "Basic ZGVtbzpwQDU1dzByZA==",
84+
"Cookie": "JSESSIONID=dwdwdwdwd"
85+
})
86+
.expect(200)
87+
})
88+
89+
it("Should return 401 WHEN none of multiple security rules is met GIVEN a request with only cookie auth", () => {
90+
return request(app)
91+
.get(`${basePath}/multi_auth`)
92+
.set({
93+
"Cookie": "JSESSIONID=dwdwdwdwd"
94+
})
95+
.expect(401)
96+
})
6797
});

0 commit comments

Comments
 (0)