Skip to content

feat: Add WWW-Authenticate Header #563

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGE_HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* fix: correctly handle default values of deepObject query params (#557) ([4ce0f89](https://github.com/cdimascio/express-openapi-validator/commit/4ce0f89)), closes [#557](https://github.com/cdimascio/express-openapi-validator/issues/557)
* doc: Clean up README and Nestjs Example (#559) ([305d5db](https://github.com/cdimascio/express-openapi-validator/commit/305d5db)), closes [#559](https://github.com/cdimascio/express-openapi-validator/issues/559)
* doc: update README ([09980a3](https://github.com/cdimascio/express-openapi-validator/commit/09980a3))
* feat: Add Allow Header on 405 (#560) ([45a40b7](https://github.com/cdimascio/express-openapi-validator/commit/45a40b7)), closes [#560](https://github.com/cdimascio/express-openapi-validator/issues/560) [#467](https://github.com/cdimascio/express-openapi-validator/issues/467) [#467](https://github.com/cdimascio/express-openapi-validator/issues/467)
* feat: Add Allow Header on 405 (#560) ([45a40b7](https://github.com/cdimascio/express-openapi-validator/commit/45a40b7)), closes [#560](https://github.com/cdimascio/express-openapi-validator/issues/560) [#467](https://github.com/cdimascio/express-openapi-validator/issues/467)



Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ app.post('/v1/pets/:id/photos', function (req, res, next) {
originalname: f.originalname,
encoding: f.encoding,
mimetype: f.mimetype,
// Buffer of file conents
// Buffer of file contents
buffer: f.buffer,
})),
});
Expand All @@ -155,10 +155,13 @@ app.post('/v1/pets/:id/photos', function (req, res, next) {
app.use((err, req, res, next) => {
// 7. Customize errors
console.error(err); // dump error to console for debug
res.status(err.status || 500).json({
message: err.message,
errors: err.errors,
});
res
.status(err.status ?? 500)
.header(err.headers)
.json({
message: err.message,
errors: err.errors,
});
});

http.createServer(app).listen(3000);
Expand Down
3 changes: 2 additions & 1 deletion examples/9-nestjs/src/filters/openapi-exception.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export class OpenApiExceptionFilter implements ExceptionFilter {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();

response.status(error.status).header(error.headers).json(error);
const { status, headers, ...data } = error;
response.status(status).header(headers).json(data);
}
}

Expand Down
1 change: 1 addition & 0 deletions examples/9-nestjs/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,

"outDir": "dist/",
"declaration": true,
Expand Down
4 changes: 4 additions & 0 deletions src/framework/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ export interface ValidationErrorItem {

interface ErrorHeaders {
Allow?: string;
'WWW-Authenticate'?: string;
}

export class HttpError extends Error implements ValidationError {
Expand Down Expand Up @@ -570,6 +571,7 @@ export class HttpError extends Error implements ValidationError {
status: number;
path: string;
message?: string;
headers?: ErrorHeaders;
errors?: ValidationErrorItem[];
}):
| InternalServerError
Expand Down Expand Up @@ -719,13 +721,15 @@ export class Unauthorized extends HttpError {
constructor(err: {
path: string;
message?: string;
headers?: ErrorHeaders;
overrideStatus?: number;
}) {
super({
status: err.overrideStatus || 401,
path: err.path,
name: 'Unauthorized',
message: err.message,
headers: err.headers,
});
}
}
Expand Down
67 changes: 51 additions & 16 deletions src/middlewares/openapi.security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,17 +109,17 @@ export function security(
}
} catch (e) {
const message = e?.error?.message || 'unauthorized';
const headers =
e?.error?.type === 'http' &&
e?.error?.scheme === 'basic'
? { 'WWW-Authenticate': 'Basic' }
: undefined;
const err = HttpError.create({
status: e.status,
path: path,
message: message,
headers,
});
/*const err =
e.status == 500
? new InternalServerError({ path: path, message: message })
: e.status == 403
? new Forbidden({ path: path, message: message })
: new Unauthorized({ path: path, message: message });*/
next(err);
}
};
Expand Down Expand Up @@ -245,21 +245,31 @@ class AuthValidator {
private validateHttp(): void {
const { req, scheme, path } = this;
if (['http'].includes(scheme.type.toLowerCase())) {
const authHeader =
req.headers['authorization'] &&
req.headers['authorization'].toLowerCase();
const authHeader = req.headers['authorization']?.toLowerCase();
const type = scheme.scheme?.toLowerCase();

if (!authHeader) {
throw Error(`Authorization header required`);
throw new SecurityError({
message: `Authorization header required`,
type: 'http',
scheme: type,
});
}

const type = scheme.scheme && scheme.scheme.toLowerCase();
if (type === 'bearer' && !authHeader.includes('bearer')) {
throw Error(`Authorization header with scheme 'Bearer' required`);
throw new SecurityError({
message: `Authorization header with scheme 'Bearer' required`,
type: 'http',
scheme: type,
});
}

if (type === 'basic' && !authHeader.includes('basic')) {
throw Error(`Authorization header with scheme 'Basic' required`);
throw new SecurityError({
message: `Authorization header with scheme 'Basic' required`,
type: 'http',
scheme: type,
});
}
}
}
Expand All @@ -269,15 +279,24 @@ class AuthValidator {
if (scheme.type === 'apiKey') {
if (scheme.in === 'header') {
if (!req.headers[scheme.name.toLowerCase()]) {
throw Error(`'${scheme.name}' header required`);
throw new SecurityError({
message: `'${scheme.name}' header required`,
type: 'apiKey',
});
}
} else if (scheme.in === 'query') {
if (!req.query[scheme.name]) {
throw Error(`query parameter '${scheme.name}' required`);
throw new SecurityError({
message: `query parameter '${scheme.name}' required`,
type: 'apiKey',
});
}
} else if (scheme.in === 'cookie') {
if (!req.cookies[scheme.name]) {
throw Error(`cookie '${scheme.name}' required`);
throw new SecurityError({
message: `cookie '${scheme.name}' required`,
type: 'apiKey',
});
}
}
}
Expand All @@ -293,3 +312,19 @@ class Util {
);
}
}

type SecurityType = OpenAPIV3.SecuritySchemeObject['type'];
type SecurityScheme = OpenAPIV3.HttpSecurityScheme['scheme'];
class SecurityError extends Error {
type: SecurityType;
scheme?: SecurityScheme;
constructor(err: {
message: string;
type: SecurityType;
scheme?: SecurityScheme;
}) {
super(err.message);
this.type = err.type;
this.scheme = err.scheme;
}
}
36 changes: 10 additions & 26 deletions test/allow.header.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { expect } from 'chai';
import * as express from 'express';
import { Server } from 'http';
import * as request from 'supertest';
import * as packageJson from '../package.json';
import * as OpenApiValidator from '../src';
import { OpenAPIV3 } from '../src/framework/types';
import { startServer } from './common/app.common';
import { createApp } from './common/app';

describe(packageJson.name, () => {
describe('Allow Header', () => {
let app = null;

before(async () => {
app = await createApp();
app = await createApp({ apiSpec: createApiSpec() }, 3001, (app) =>
app.use(
express
.Router()
.get('/v1/pets/:petId', () => ['cat', 'dog'])
.post('/v1/pets/:petId', (req, res) => res.json(req.body)),
),
);
});

after(() => {
Expand All @@ -30,26 +34,6 @@ describe(packageJson.name, () => {
}));
});

async function createApp(): Promise<express.Express & { server?: Server }> {
const app = express();

app.use(
OpenApiValidator.middleware({
apiSpec: createApiSpec(),
validateRequests: true,
}),
);
app.use(
express
.Router()
.get('/v1/pets/:petId', () => ['cat', 'dog'])
.post('/v1/pets/:petId', (req, res) => res.json(req.body)),
);

await startServer(app, 3001);
return app;
}

function createApiSpec(): OpenAPIV3.Document {
return {
openapi: '3.0.3',
Expand Down
13 changes: 8 additions & 5 deletions test/common/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as cookieParser from 'cookie-parser';
import * as bodyParser from 'body-parser';
import * as logger from 'morgan';

import * as OpenApiValidator from '../../src';
import * as OpenApiValidator from '../../src';
import { startServer, routes } from './app.common';
import { OpenApiValidatorOpts } from '../../src/framework/types';

Expand Down Expand Up @@ -43,10 +43,13 @@ export async function createApp(
// Register error handler
app.use((err, req, res, next) => {
// console.error(err);
res.status(err.status ?? 500).json({
message: err.message,
errors: err.errors,
});
res
.status(err.status ?? 500)
.header(err.headers)
.json({
message: err.message,
errors: err.errors,
});
});
}

Expand Down
2 changes: 1 addition & 1 deletion test/resources/security.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ paths:
/api_key_or_anonymous:
get:
security:
# {} means anonyous or no security - see https://github.com/OAI/OpenAPI-Specification/issues/14
# {} means anonymous or no security - see https://github.com/OAI/OpenAPI-Specification/issues/14
- {}
- ApiKeyAuth: []
responses:
Expand Down
43 changes: 43 additions & 0 deletions test/www-authenticate.header.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { expect } from 'chai';
import * as express from 'express';
import { join } from 'path';
import * as request from 'supertest';
import { createApp } from './common/app';

describe('WWW-Authenticate Header', () => {
let app = null;

before(async () => {
app = await createApp(
{ apiSpec: join(__dirname, 'resources/security.yaml') },
3001,
(app) =>
app.use(
express
.Router()
.get('/v1/basic', (req, res) => res.json({ logged_in: true }))
.get('/v1/bearer', (req, res) => res.json({ logged_in: true })),
),
);
});

after(() => {
app.server.close();
});

it('adds "WWW-Authenticate" header on 401 when using basic auth', async () =>
request(app)
.get('/v1/basic')
.expect(401)
.then((response) => {
expect(response.header['www-authenticate']).to.equal('Basic');
}));

it('does not add "WWW-Authenticate" header on 401 when using bearer auth', async () =>
request(app)
.get('/v1/bearer')
.expect(401)
.then((response) => {
expect(response.header['www-authenticate']).to.be.undefined;
}));
});