From 26afd9f46ad6e14bb5145ca5330c80d2d1f5e7cf Mon Sep 17 00:00:00 2001 From: Kristian Kraljic Date: Thu, 31 Aug 2023 23:32:22 +0200 Subject: [PATCH] feat: more session settings in ServerConfig --- docs/usage/verticles/ServerVerticle.md | 6 + ...ernal.verticle.ServerVerticle.example.yaml | 120 ++++++------ .../neonbee/config/ServerConfigConverter.java | 41 ++++ .../java/io/neonbee/config/ServerConfig.java | 182 ++++++++++++++++++ .../factories/SessionHandlerFactory.java | 10 +- 5 files changed, 304 insertions(+), 55 deletions(-) diff --git a/docs/usage/verticles/ServerVerticle.md b/docs/usage/verticles/ServerVerticle.md index 9e777414..64e7ff72 100644 --- a/docs/usage/verticles/ServerVerticle.md +++ b/docs/usage/verticles/ServerVerticle.md @@ -18,7 +18,13 @@ property. Some defaults are overridden, and additional config options are provid | `port` | integer | No | `8080` | Sets the port on which the application server listens. | | `useAlpn` | boolean | No | `true` | Whether to use application-layer protocol negotiation or not. | | [`sessionHandling`](#sessionhandling) | string | No | `NONE` | Sets the type of session handling. Must be one of one of: `NONE`, `LOCAL`, or `CLUSTERED`. | +| `sessionTimeout` | integer | No | `30` | Session timeout in minutes. | | `sessionCookieName` | string | No | `neonbee-web.session` | Sets the name of the session cookie. | +| `sessionCookiePath` | string | No | `/` | Sets the path of the session cookie. | +| `secureSessionCookie` | boolean | No | `false` | Whether to set the `secure` flag of the session cookie. | +| `httpOnlySessionCookie` | boolean | No | `false` | Whether to set the `HttpOnly` flag of the session cookie. | +| `sessionCookieSameSitePolicy` | string | No | `null` | Which `SameSite` policy to use for the cookie. One of: `null`, `NONE`, `STRICT` or `LAX`. | +| `minSessionIdLength` | integer | No | `32` | The minimum length of the session ID. | | `decompressionSupported` | boolean | No | `true` | Enables server-side decompression of request bodies. | | `compressionSupported` | boolean | No | `true` | Enables server-side support for compression. | | `compressionLevel` | integer | No | `true` | Sets the level of compression, if compression is enabled. | diff --git a/resources/config/io.neonbee.internal.verticle.ServerVerticle.example.yaml b/resources/config/io.neonbee.internal.verticle.ServerVerticle.example.yaml index c059c1e2..f0ce9565 100644 --- a/resources/config/io.neonbee.internal.verticle.ServerVerticle.example.yaml +++ b/resources/config/io.neonbee.internal.verticle.ServerVerticle.example.yaml @@ -11,8 +11,20 @@ config: useAlpn: true # one of: NONE, LOCAL or CLUSTERED, defaults to NONE sessionHandling: NONE + # the session timeout in minutes, defaults to 30 + sessionTimeout: 30 # the name of the session cookie, defaults to neonbee-web.session sessionCookieName: neonbee-web.session + # the path of the session cookie, defaults to / + sessionCookiePath: / + # sets whether to set the `secure` flag of the session cookie, defaults to false + secureSessionCookie: false + # sets whether to set the `HttpOnly` flag of the session cookie, defaults to false + httpOnlySessionCookie: false + # one of: null, none, strict, or lax, defaults to null + sessionCookieSameSitePolicy: ~ + # the minimum length of the session id, defaults to 32 + minSessionIdLength: 32 # sets whether the server should decompress request bodies, defaults to true decompressionSupported: true @@ -39,69 +51,69 @@ config: # specific endpoint configuration, defaults to the object seen below endpoints: - # provides a OData V4 compliant endpoint, for accessing entity verticle data - - type: io.neonbee.endpoint.odatav4.ODataV4Endpoint - # enable the OData endpoint, defaults to true - enabled: true - # the base path to map this endpoint to, defaults to /odata/ - basePath: /odata/ - # endpoint specific authentication chain, defaults to null and using the default authentication chain - authenticationChain: ~ - # namespace and service name URI mapping (STRICT, or LOOSE based on CDS) - uriConversion: STRICT - # a block / allow list of verticles to expose via this endpoint (defaults to empty / all entities exposed) - # the value of block / allow must be an array with Strings representing a regexp. - exposedEntities: - block: [any_allow_list_of_regexp_here] - allow: [any_block_list_of_regexp_here] + # provides a OData V4 compliant endpoint, for accessing entity verticle data + - type: io.neonbee.endpoint.odatav4.ODataV4Endpoint + # enable the OData endpoint, defaults to true + enabled: true + # the base path to map this endpoint to, defaults to /odata/ + basePath: /odata/ + # endpoint specific authentication chain, defaults to null and using the default authentication chain + authenticationChain: ~ + # namespace and service name URI mapping (STRICT, or LOOSE based on CDS) + uriConversion: STRICT + # a block / allow list of verticles to expose via this endpoint (defaults to empty / all entities exposed) + # the value of block / allow must be an array with Strings representing a regexp. + exposedEntities: + block: [any_allow_list_of_regexp_here] + allow: [any_block_list_of_regexp_here] - # provides a REST endpoint (JSON, text, binary), for accessing data verticles - - type: io.neonbee.endpoint.raw.RawEndpoint - # enable the raw endpoint, defaults to true - enabled: true - # the base path to map this endpoint to, defaults to /raw/ - basePath: /raw/ - # endpoint specific authentication chain, defaults to null and using the default authentication chain - authenticationChain: ~ - # whether or not to expose hidden verticles, defaults to false - exposeHiddenVerticles: false - # a block / allow list of verticles to expose via this endpoint (defaults to empty all verticles exposed) - # the value of block / allow must be an array with Strings representing a regexp. - exposedVerticles: - block: [any_allow_list_of_regexp_here] - allow: [any_block_list_of_regexp_here] + # provides a REST endpoint (JSON, text, binary), for accessing data verticles + - type: io.neonbee.endpoint.raw.RawEndpoint + # enable the raw endpoint, defaults to true + enabled: true + # the base path to map this endpoint to, defaults to /raw/ + basePath: /raw/ + # endpoint specific authentication chain, defaults to null and using the default authentication chain + authenticationChain: ~ + # whether or not to expose hidden verticles, defaults to false + exposeHiddenVerticles: false + # a block / allow list of verticles to expose via this endpoint (defaults to empty all verticles exposed) + # the value of block / allow must be an array with Strings representing a regexp. + exposedVerticles: + block: [any_allow_list_of_regexp_here] + allow: [any_block_list_of_regexp_here] - # provides an Prometheus scraping endpoint for Micrometer.io metrics - - type: io.neonbee.endpoint.metrics.MetricsEndpoint - # enable the metrics endpoint, defaults to true - enabled: true - # the base path to map this endpoint to, defaults to /metrics/ - basePath: /metrics/ - # endpoint specific authentication chain, (special case!) defaults to an empty array [] and no authentication required - authenticationChain: [] + # provides an Prometheus scraping endpoint for Micrometer.io metrics + - type: io.neonbee.endpoint.metrics.MetricsEndpoint + # enable the metrics endpoint, defaults to true + enabled: true + # the base path to map this endpoint to, defaults to /metrics/ + basePath: /metrics/ + # endpoint specific authentication chain, (special case!) defaults to an empty array [] and no authentication required + authenticationChain: [] # default authentication chain, defaults to an empty array (no authentication), use: authenticationChain: # any of: BASIC, DIGEST, JWT, OAUTH2, REDIRECT, mandatory attribute - - type: string - # ... more authentication handler options (see the specific handler implementations) + - type: string + # ... more authentication handler options (see the specific handler implementations) - # the authentication provider to be set for this handler - provider: - # the authentication provider to be set for this handler# any of: HTDIGEST, HTPASSWD, JDBC, JWT, MONGO, OAUTH2, mandatory attribute - type: string - # ... more authentication provider options (see the specific provider implementations) + # the authentication provider to be set for this handler + provider: + # the authentication provider to be set for this handler# any of: HTDIGEST, HTPASSWD, JDBC, JWT, MONGO, OAUTH2, mandatory attribute + type: string + # ... more authentication provider options (see the specific provider implementations) # default handler factories. The order of handler factories has to take the priority of the returned handlers into account. handlerFactories: - - io.neonbee.internal.handler.factories.LoggerHandlerFactory - - io.neonbee.internal.handler.factories.InstanceInfoHandlerFactory - - io.neonbee.internal.handler.factories.CorrelationIdHandlerFactory - - io.neonbee.internal.handler.factories.TimeoutHandlerFactory - - io.neonbee.internal.handler.factories.SessionHandlerFactory - - io.neonbee.internal.handler.factories.CacheControlHandlerFactory - - io.neonbee.internal.handler.factories.CorsHandlerFactory - - io.neonbee.internal.handler.factories.DisallowingFileUploadBodyHandlerFactory + - io.neonbee.internal.handler.factories.LoggerHandlerFactory + - io.neonbee.internal.handler.factories.InstanceInfoHandlerFactory + - io.neonbee.internal.handler.factories.CorrelationIdHandlerFactory + - io.neonbee.internal.handler.factories.TimeoutHandlerFactory + - io.neonbee.internal.handler.factories.SessionHandlerFactory + - io.neonbee.internal.handler.factories.CacheControlHandlerFactory + - io.neonbee.internal.handler.factories.CorsHandlerFactory + - io.neonbee.internal.handler.factories.DisallowingFileUploadBodyHandlerFactory # configures the CORS handler (Disabled by default) cors: @@ -119,4 +131,4 @@ config: exposedHeaders: # The exposed headers. - foobar maxAgeSeconds: 1337 # Set how long the browser should cache the information. - allowCredentials: false # Set whether credentials are allowed or not. \ No newline at end of file + allowCredentials: false # Set whether credentials are allowed or not. diff --git a/src/generated/java/io/neonbee/config/ServerConfigConverter.java b/src/generated/java/io/neonbee/config/ServerConfigConverter.java index d93533a3..a08b2c99 100644 --- a/src/generated/java/io/neonbee/config/ServerConfigConverter.java +++ b/src/generated/java/io/neonbee/config/ServerConfigConverter.java @@ -71,17 +71,48 @@ static void fromJson(Iterable> json, ServerC obj.setHandlerFactoriesClassNames(list); } break; + case "httpOnlySessionCookie": + if (member.getValue() instanceof Boolean) { + obj.setHttpOnlySessionCookie((Boolean) member.getValue()); + } + break; + case "minSessionIdLength": + if (member.getValue() instanceof Number) { + obj.setMinSessionIdLength(((Number) member.getValue()).intValue()); + } + break; + case "secureSessionCookie": + if (member.getValue() instanceof Boolean) { + obj.setSecureSessionCookie((Boolean) member.getValue()); + } + break; case "sessionCookieName": if (member.getValue() instanceof String) { obj.setSessionCookieName((String) member.getValue()); } break; + case "sessionCookiePath": + if (member.getValue() instanceof String) { + obj.setSessionCookiePath((String) member.getValue()); + } + break; + case "sessionCookieSameSitePolicy": + if (member.getValue() instanceof String) { + obj.setSessionCookieSameSitePolicy( + io.vertx.core.http.CookieSameSite.valueOf((String) member.getValue())); + } + break; case "sessionHandling": if (member.getValue() instanceof String) { obj.setSessionHandling( io.neonbee.config.ServerConfig.SessionHandling.valueOf((String) member.getValue())); } break; + case "sessionTimeout": + if (member.getValue() instanceof Number) { + obj.setSessionTimeout(((Number) member.getValue()).intValue()); + } + break; case "timeout": if (member.getValue() instanceof Number) { obj.setTimeout(((Number) member.getValue()).intValue()); @@ -128,12 +159,22 @@ static void toJson(ServerConfig obj, java.util.Map json) { obj.getHandlerFactoriesClassNames().forEach(item -> array.add(item)); json.put("handlerFactoriesClassNames", array); } + json.put("httpOnlySessionCookie", obj.isHttpOnlySessionCookie()); + json.put("minSessionIdLength", obj.getMinSessionIdLength()); + json.put("secureSessionCookie", obj.isSecureSessionCookie()); if (obj.getSessionCookieName() != null) { json.put("sessionCookieName", obj.getSessionCookieName()); } + if (obj.getSessionCookiePath() != null) { + json.put("sessionCookiePath", obj.getSessionCookiePath()); + } + if (obj.getSessionCookieSameSitePolicy() != null) { + json.put("sessionCookieSameSitePolicy", obj.getSessionCookieSameSitePolicy().name()); + } if (obj.getSessionHandling() != null) { json.put("sessionHandling", obj.getSessionHandling().name()); } + json.put("sessionTimeout", obj.getSessionTimeout()); json.put("timeout", obj.getTimeout()); json.put("timeoutStatusCode", obj.getTimeoutStatusCode()); } diff --git a/src/main/java/io/neonbee/config/ServerConfig.java b/src/main/java/io/neonbee/config/ServerConfig.java index 35e8d9a9..16ecaf64 100644 --- a/src/main/java/io/neonbee/config/ServerConfig.java +++ b/src/main/java/io/neonbee/config/ServerConfig.java @@ -2,6 +2,10 @@ import static io.neonbee.internal.helper.ConfigHelper.rephraseConfigNames; import static io.netty.handler.codec.http.HttpResponseStatus.GATEWAY_TIMEOUT; +import static io.vertx.ext.web.handler.SessionHandler.DEFAULT_COOKIE_HTTP_ONLY_FLAG; +import static io.vertx.ext.web.handler.SessionHandler.DEFAULT_COOKIE_SECURE_FLAG; +import static io.vertx.ext.web.handler.SessionHandler.DEFAULT_SESSION_COOKIE_PATH; +import static io.vertx.ext.web.handler.SessionHandler.DEFAULT_SESSION_TIMEOUT; import java.util.ArrayList; import java.util.Collections; @@ -35,8 +39,11 @@ import io.neonbee.internal.verticle.ServerVerticle; import io.vertx.codegen.annotations.DataObject; import io.vertx.codegen.annotations.Fluent; +import io.vertx.codegen.annotations.GenIgnore; +import io.vertx.codegen.annotations.Nullable; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.ClientAuth; +import io.vertx.core.http.CookieSameSite; import io.vertx.core.http.Http2Settings; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.HttpServerRequest; @@ -72,7 +79,13 @@ * timeout: number, // the number of seconds before the router timeout applies, defaults to 30 * timeoutStatusCode: number, // the status code for the default timeout, defaults to 504 * sessionHandling: string, // one of: none, local or clustered, defaults to none + * sessionTimeout: integer, // the session timeout in minutes, defaults to 30 * sessionCookieName: string, // the name of the session cookie, defaults to neonbee-web.session + * sessionCookiePath: string, // the path of the session cookie, defaults to / + * secureSessionCookie: boolean, // sets whether to set the `secure` flag of the session cookie, defaults to false + * httpOnlySessionCookie: boolean, // sets whether to set the `HttpOnly` flag of the session cookie, defaults to false + * sessionCookieSameSitePolicy: string, // one of: null, none, strict, or lax, defaults to null + * minSessionIdLength: integer, // the minimum length of the session id, defaults to 32 * correlationStrategy: string, // one of: request_header, generate_uuid, defaults to request_header * endpoints: [ // endpoint configurations, defaults to the objects seen below * { @@ -215,6 +228,11 @@ public String getCorrelationId(RoutingContext routingContext) { */ public static final String DEFAULT_SESSION_COOKIE_NAME = "neonbee-web.session"; + /** + * The default minimum length of the session ID. + */ + public static final int DEFAULT_SESSIONID_MIN_LENGTH = 32; + /** * List of instances of {@link RoutingHandlerFactory} that are loaded by default. */ @@ -241,8 +259,20 @@ public String getCorrelationId(RoutingContext routingContext) { private SessionHandling sessionHandling = SessionHandling.NONE; + private int sessionTimeout = (int) TimeUnit.MICROSECONDS.toSeconds(DEFAULT_SESSION_TIMEOUT); + private String sessionCookieName = DEFAULT_SESSION_COOKIE_NAME; + private String sessionCookiePath = DEFAULT_SESSION_COOKIE_PATH; + + private boolean secureSessionCookie = DEFAULT_COOKIE_SECURE_FLAG; + + private boolean httpOnlySessionCookie = DEFAULT_COOKIE_HTTP_ONLY_FLAG; + + private CookieSameSite sessionCookieSameSitePolicy; + + private int minSessionIdLength = DEFAULT_SESSIONID_MIN_LENGTH; + private CorrelationStrategy correlationStrategy = CorrelationStrategy.REQUEST_HEADER; private List endpointConfigs = new ArrayList<>(DEFAULT_ENDPOINT_CONFIGS); @@ -357,6 +387,27 @@ public ServerConfig setSessionHandling(SessionHandling sessionHandling) { return this; } + /** + * Get the session timeout in minutes. + * + * @return the session timeout in minutes + */ + public int getSessionTimeout() { + return sessionTimeout; + } + + /** + * Set the session timeout in minutes. + * + * @param sessionTimeout the session timeout in minutes + * @return the {@link ServerConfig} for chaining + */ + @Fluent + public ServerConfig setSessionTimeout(int sessionTimeout) { + this.sessionTimeout = sessionTimeout; + return this; + } + /** * Get the cookie name to use to store the session identifier. * @@ -378,6 +429,137 @@ public ServerConfig setSessionCookieName(String sessionCookieName) { return this; } + /** + * Get the path to use for storing the session cookie. + * + * @return the cookie path to use + */ + public String getSessionCookiePath() { + return sessionCookiePath; + } + + /** + * Set the cookie path to use for storing the session cookie. + * + * @param sessionCookiePath the cookie path to set + * @return the {@link ServerConfig} for chaining + */ + @Fluent + public ServerConfig setSessionCookiePath(String sessionCookiePath) { + this.sessionCookiePath = sessionCookiePath; + return this; + } + + /** + * Returns if the {@code Secure} property should be used for the session cookie. + * + * @return true if the {@code Secure} property should be used for the session cookie + */ + public boolean useSecureSessionCookie() { + return secureSessionCookie; + } + + /** + * Returns if the {@code Secure} property should be used for the session cookie. + * + * Note that this method only used for the Vert.x code generation, as "use..." are not recognized as getters. + * + * @see #useSecureSessionCookie() + * @return true if the {@code Secure} property should be used for the session cookie + */ + public boolean isSecureSessionCookie() { + return useSecureSessionCookie(); + } + + /** + * Set the {@code Secure} property for the session cookie. + * + * @param secureSessionCookie if the {@code Secure} property should be set for the session cookie + * @return the {@link ServerConfig} for chaining + */ + @Fluent + public ServerConfig setSecureSessionCookie(boolean secureSessionCookie) { + this.secureSessionCookie = secureSessionCookie; + return this; + } + + /** + * Returns if the {@code HttpOnly} property should be used for the session cookie. + * + * @return true if the {@code HttpOnly} property should be used for the session cookie + */ + @GenIgnore + public boolean useHttpOnlySessionCookie() { + return httpOnlySessionCookie; + } + + /** + * Returns if the {@code HttpOnly} property should be used for the session cookie. + * + * Note that this method only used for the Vert.x code generation, as "use..." are not recognized as getters. + * + * @see #useHttpOnlySessionCookie() + * @return true if the {@code HttpOnly} property should be used for the session cookie + */ + public boolean isHttpOnlySessionCookie() { + return useHttpOnlySessionCookie(); + } + + /** + * Set the {@code HttpOnly} property for the session cookie. + * + * @param httpOnlySessionCookie if the {@code HttpOnly} property should be set for the session cookie + * @return the {@link ServerConfig} for chaining + */ + @Fluent + public ServerConfig setHttpOnlySessionCookie(boolean httpOnlySessionCookie) { + this.httpOnlySessionCookie = httpOnlySessionCookie; + return this; + } + + /** + * Get the {@code SameSite} policy for the session cookie. + * + * @return the {@code SameSite} property + */ + public CookieSameSite getSessionCookieSameSitePolicy() { + return sessionCookieSameSitePolicy; + } + + /** + * Set the {@code SameSite} policy for the session cookie. + * + * @param sessionCookieSameSitePolicy the {@code SameSite} property + * @return the {@link ServerConfig} for chaining + */ + @Fluent + @Nullable + public ServerConfig setSessionCookieSameSitePolicy(CookieSameSite sessionCookieSameSitePolicy) { + this.sessionCookieSameSitePolicy = sessionCookieSameSitePolicy; + return this; + } + + /** + * Get the minimum length of the session ID. + * + * @return the minimum number of characters of the session ID + */ + public int getMinSessionIdLength() { + return minSessionIdLength; + } + + /** + * Set the minimum length of the session ID. + * + * @param minSessionIdLength the minimum number of characters of the session ID + * @return the {@link ServerConfig} for chaining + */ + @Fluent + public ServerConfig setMinSessionIdLength(int minSessionIdLength) { + this.minSessionIdLength = minSessionIdLength; + return this; + } + /** * Get the strategy to determine a correlation ID for a given request. * diff --git a/src/main/java/io/neonbee/internal/handler/factories/SessionHandlerFactory.java b/src/main/java/io/neonbee/internal/handler/factories/SessionHandlerFactory.java index 0133667a..d6ea7369 100644 --- a/src/main/java/io/neonbee/internal/handler/factories/SessionHandlerFactory.java +++ b/src/main/java/io/neonbee/internal/handler/factories/SessionHandlerFactory.java @@ -1,6 +1,7 @@ package io.neonbee.internal.handler.factories; import java.util.Optional; +import java.util.concurrent.TimeUnit; import com.google.common.annotations.VisibleForTesting; @@ -37,7 +38,14 @@ public Future> createHandler() { ServerConfig config = NeonBee.get().getServerConfig(); Handler sh = createSessionStore(NeonBee.get().getVertx(), config.getSessionHandling()).map(SessionHandler::create) - .map(sessionHandler -> sessionHandler.setSessionCookieName(config.getSessionCookieName())) + .map(sessionHandler -> sessionHandler + .setSessionTimeout(TimeUnit.SECONDS.toMillis(config.getSessionTimeout())) + .setSessionCookieName(config.getSessionCookieName()) + .setSessionCookiePath(config.getSessionCookiePath()) + .setCookieSecureFlag(config.useSecureSessionCookie()) + .setCookieHttpOnlyFlag(config.useHttpOnlySessionCookie()) + .setCookieSameSite(config.getSessionCookieSameSitePolicy()) + .setMinLength(config.getMinSessionIdLength())) .map(sessionHandler -> (Handler) sessionHandler).orElseGet(NoOpHandler::new); return Future.succeededFuture(sh); }