Security header setup for the Website service (#8)
This PR contains the work done to add a `SecurityHeadersMiddleware` middleware that stamps hardened security-related HTTP headers onto every response. To provide further details about the work: * Implemented the `SecurityHeadersMiddleware` middleware, which precomputes headers once from a `Configuration` object and applies them to every response: * _Content-Security-Policy_, * _X-Content-Type-Options_, * _X-Frame-Options_, * _Referrer-Policy_, * _Permissions-Policy_, * _Strict-Transport-Security_ (optional). * Integrated this middleware into the router (near the top of the chain), reading each value from configuration with hardened defaults. * The _Strict-Transport-Security_ has no default value — omitted unless explicitly set, so it stays off in plain-HTTP during development and on only behind TLS. * Added security-header constants keys and values. Reviewed-on: rock-n-code/loud-amsterdam#8 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit is contained in:
@@ -33,7 +33,9 @@ struct AppTests {
|
||||
|
||||
@Test
|
||||
func `landing page to be served at root`() async throws {
|
||||
try await app.test(.router) { client in
|
||||
try await app(
|
||||
staticFilesPath: staticFilesPath
|
||||
).test(.router) { client in
|
||||
try await client.execute(
|
||||
uri: "/",
|
||||
method: .get
|
||||
@@ -51,7 +53,9 @@ struct AppTests {
|
||||
func `static files to be served`(
|
||||
staticFile file: StaticFile
|
||||
) async throws {
|
||||
try await app.test(.router) { client in
|
||||
try await app(
|
||||
staticFilesPath: staticFilesPath
|
||||
).test(.router) { client in
|
||||
try await client.execute(
|
||||
uri: "/\(file.relativePath)",
|
||||
method: .get
|
||||
@@ -73,7 +77,9 @@ struct AppTests {
|
||||
|
||||
@Test
|
||||
func `response to be compressed when the client supports it`() async throws {
|
||||
try await app.test(.router) { client in
|
||||
try await app(
|
||||
staticFilesPath: staticFilesPath
|
||||
).test(.router) { client in
|
||||
try await client.execute(
|
||||
uri: "/",
|
||||
method: .get,
|
||||
@@ -87,7 +93,9 @@ struct AppTests {
|
||||
|
||||
@Test
|
||||
func `response to not be compressed when the client does not support it`() async throws {
|
||||
try await app.test(.router) { client in
|
||||
try await app(
|
||||
staticFilesPath: staticFilesPath
|
||||
).test(.router) { client in
|
||||
try await client.execute(
|
||||
uri: "/",
|
||||
method: .get
|
||||
@@ -100,7 +108,9 @@ struct AppTests {
|
||||
|
||||
@Test
|
||||
func `error page to be served when not found`() async throws {
|
||||
try await app.test(.router) { client in
|
||||
try await app(
|
||||
staticFilesPath: staticFilesPath
|
||||
).test(.router) { client in
|
||||
try await client.execute(
|
||||
uri: "/this-path-does-not-exist",
|
||||
method: .get
|
||||
@@ -114,37 +124,100 @@ struct AppTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `security headers to be applied to the landing page`() async throws {
|
||||
try await app(
|
||||
staticFilesPath: staticFilesPath
|
||||
).test(.router) { client in
|
||||
try await client.execute(
|
||||
uri: "/",
|
||||
method: .get
|
||||
) { response in
|
||||
#expect(response.status == .ok)
|
||||
#expect(response.headers[.contentSecurityPolicy] == String.Security.contentSecurityPolicy)
|
||||
#expect(response.headers[.xContentTypeOptions] == String.Security.contentTypeOptions)
|
||||
#expect(response.headers[.frameOptions] == String.Security.frameOptions)
|
||||
#expect(response.headers[.referrerPolicy] == String.Security.referrerPolicy)
|
||||
#expect(response.headers[.permissionsPolicy] == String.Security.permissionsPolicy)
|
||||
#expect(response.headers[.strictTransportSecurity] == nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `security headers to be applied to the error page`() async throws {
|
||||
try await app(
|
||||
staticFilesPath: staticFilesPath
|
||||
).test(.router) { client in
|
||||
try await client.execute(
|
||||
uri: "/this-path-does-not-exist",
|
||||
method: .get
|
||||
) { response in
|
||||
#expect(response.status == .notFound)
|
||||
#expect(response.headers[.contentSecurityPolicy] == String.Security.contentSecurityPolicy)
|
||||
#expect(response.headers[.xContentTypeOptions] == String.Security.contentTypeOptions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `strict-transport-security to be applied when configured`() async throws {
|
||||
try await app(
|
||||
staticFilesPath: staticFilesPath,
|
||||
strictTransportSecurity: "max-age=31536000; includeSubDomains"
|
||||
).test(.router) { client in
|
||||
try await client.execute(
|
||||
uri: "/",
|
||||
method: .get
|
||||
) { response in
|
||||
#expect(response.status == .ok)
|
||||
#expect(response.headers[.strictTransportSecurity] == "max-age=31536000; includeSubDomains")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension AppTests {
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
var app: some ApplicationProtocol {
|
||||
get async {
|
||||
await application(
|
||||
reader: reader(
|
||||
staticFilesPath: staticFilesPath
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func reader(
|
||||
staticFilesPath: String
|
||||
) -> ConfigReader {
|
||||
ConfigReader(providers: [
|
||||
InMemoryProvider(values: [
|
||||
.HTTP.host: "127.0.0.1",
|
||||
.HTTP.port: "0",
|
||||
.Log.level: "trace",
|
||||
.Path.staticFiles: .init(stringLiteral: staticFilesPath),
|
||||
])
|
||||
])
|
||||
func app(
|
||||
staticFilesPath: String,
|
||||
strictTransportSecurity: String? = nil
|
||||
) async -> some ApplicationProtocol {
|
||||
await application(
|
||||
reader: reader(
|
||||
staticFilesPath: staticFilesPath,
|
||||
strictTransportSecurity: strictTransportSecurity
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func reader(
|
||||
staticFilesPath: String,
|
||||
strictTransportSecurity: String? = nil
|
||||
) -> ConfigReader {
|
||||
ConfigReader(providers: [{
|
||||
if let strictTransportSecurity {
|
||||
InMemoryProvider(values: [
|
||||
.HTTP.host: "127.0.0.1",
|
||||
.HTTP.port: "0",
|
||||
.Log.level: "trace",
|
||||
.Path.staticFiles: .init(stringLiteral: staticFilesPath),
|
||||
.Security.strictTransportSecurity: .init(stringLiteral: strictTransportSecurity)
|
||||
])
|
||||
} else {
|
||||
InMemoryProvider(values: [
|
||||
.HTTP.host: "127.0.0.1",
|
||||
.HTTP.port: "0",
|
||||
.Log.level: "trace",
|
||||
.Path.staticFiles: .init(stringLiteral: staticFilesPath),
|
||||
])
|
||||
}
|
||||
}()])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user