From 300bb6d504b9e4ae9b49efd69194dcd7dabc30ae Mon Sep 17 00:00:00 2001 From: David Kral Date: Fri, 15 Dec 2023 12:04:56 +0100 Subject: [PATCH 1/5] OIDC id token authentication and access token refreshing Signed-off-by: David Kral --- ..._security_providers_oidc_OidcProvider.adoc | 31 +- ...rity_providers_oidc_common_OidcConfig.adoc | 31 +- .../microprofile/security/SecurityFilter.java | 12 + .../security/SecurityFilterCommon.java | 5 + .../providers/oidc/common/OidcConfig.java | 185 ++++++++++- .../security/providers/oidc/OidcFeature.java | 16 +- .../oidc/TenantAuthenticationHandler.java | 294 +++++++++++++++--- .../integration/oidc/CommonLoginBase.java | 6 + .../tests/integration/oidc/IdTokenIT.java | 184 +++++++++++ .../integration/oidc/QueryBasedLoginIT.java | 1 + .../integration/oidc/RefreshTokenIT.java | 118 +++++++ .../test/resources/application-no-cookie.yaml | 19 ++ .../security/SecurityHttpFeature.java | 1 + 13 files changed, 850 insertions(+), 53 deletions(-) create mode 100644 tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/IdTokenIT.java create mode 100644 tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/RefreshTokenIT.java create mode 100644 tests/integration/oidc/src/test/resources/application-no-cookie.yaml diff --git a/docs/src/main/asciidoc/config/io_helidon_security_providers_oidc_OidcProvider.adoc b/docs/src/main/asciidoc/config/io_helidon_security_providers_oidc_OidcProvider.adoc index d2250f8dd30..a2b6a09436c 100644 --- a/docs/src/main/asciidoc/config/io_helidon_security_providers_oidc_OidcProvider.adoc +++ b/docs/src/main/asciidoc/config/io_helidon_security_providers_oidc_OidcProvider.adoc @@ -71,6 +71,21 @@ This type provides the following service implementations: |`client-timeout-millis` |Duration |`30000` |Timeout of calls using web client. |`cookie-domain` |string |{nbsp} |Domain the cookie is valid for. Not used by default. +|`cookie-encryption-enabled` |boolean |`false` |Whether to encrypt token cookie created by this microservice. + Defaults to `false`. +|`cookie-encryption-enabled-id-token` |boolean |`true` |Whether to encrypt id token cookie created by this microservice. + Defaults to `true`. +|`cookie-encryption-enabled-refresh-token` |boolean |`true` |Whether to encrypt refresh token cookie created by this microservice. + Defaults to `true`. +|`cookie-encryption-enabled-tenant-name` |boolean |`true` |Whether to encrypt tenant name cookie created by this microservice. + Defaults to `true`. +|`cookie-encryption-name` |string |{nbsp} |Name of the encryption configuration available through Security#encrypt(String, byte[]) and + Security#decrypt(String, String). + If configured and encryption is enabled for any cookie, + Security MUST be configured in global or current `io.helidon.common.context.Context` (this + is done automatically in Helidon MP). +|`cookie-encryption-password` |char[] |{nbsp} |Master password for encryption/decryption of cookies. This must be configured to the same value on each microservice + using the cookie. |`cookie-http-only` |boolean |`true` |When using cookie, if set to true, the HttpOnly attribute will be configured. Defaults to `OidcCookieHandler.Builder#DEFAULT_HTTP_ONLY`. |`cookie-max-age-seconds` |long |{nbsp} |When using cookie, used to set MaxAge attribute of the cookie, defining how long @@ -78,6 +93,13 @@ This type provides the following service implementations: Not used by default. |`cookie-name` |string |`JSESSIONID` |Name of the cookie to use. Defaults to `DEFAULT_COOKIE_NAME`. +|`cookie-name-id-token` |string |`JSESSIONID_2` |Name of the cookie to use for id token. + Defaults to `DEFAULT_COOKIE_NAME`_2. + + This cookie is only used when logout is enabled, as otherwise it is not needed. + Content of this cookie is encrypted. +|`cookie-name-refresh-token` |string |`JSESSIONID_3` |The name of the cookie to use for the refresh token. + Defaults to `DEFAULT_REFRESH_COOKIE_NAME`. |`cookie-name-tenant` |string |`HELIDON_TENANT` |The name of the cookie to use for the tenant name. Defaults to `DEFAULT_TENANT_COOKIE_NAME`. |`cookie-path` |string |`/` |Path the cookie is valid for. @@ -97,6 +119,9 @@ This type provides the following service implementations: process header containing a JWT. Default is "Authorization" header with a prefix "bearer ". |`header-use` |boolean |`true` |Whether to expect JWT in a header field. +|`id-token-signature-validation` |boolean |`true` |Whether id token signature check should be enabled. + Signature check is enabled by default, and it is highly recommended to not change that. + Change this setting only when you really know what you are doing, otherwise it could case security issues. |`identity-uri` |URI |{nbsp} |URI of the identity server, base used to retrieve OIDC metadata. |`introspect-endpoint-uri` |URI |{nbsp} |Endpoint to use to validate JWT. Either use this or set #signJwk(JwkKeys) or #signJwk(Resource). @@ -123,7 +148,8 @@ This type provides the following service implementations: Defaults to `DEFAULT_PROXY_PORT` |`proxy-protocol` |string |`http` |Proxy protocol to use when proxy is used. Defaults to `DEFAULT_PROXY_PROTOCOL`. -|`query-param-name` |string |`accessToken` |Name of a query parameter that contains the JWT token when parameter is used. +|`query-id-token-param-name` |string |`id_token` |Name of a query parameter that contains the JWT id token when parameter is used. +|`query-param-name` |string |`accessToken` |Name of a query parameter that contains the JWT access token when parameter is used. |`query-param-tenant-name` |string |`h_tenant` |Name of a query parameter that contains the tenant name when the parameter is used. Defaults to #DEFAULT_TENANT_PARAM_NAME. |`query-param-use` |boolean |`false` |Whether to use a query parameter to send JWT token from application to this @@ -167,6 +193,9 @@ This type provides the following service implementations: code. If not defined, it is obtained from #oidcMetadata(Resource), if that is not defined an attempt is made to use #identityUri(URI)/oauth2/v1/token. +|`token-signature-validation` |boolean |`true` |Whether access token signature check should be enabled. + Signature check is enabled by default, and it is highly recommended to not change that. + Change this setting only when you really know what you are doing, otherwise it could case security issues. |`use-jwt-groups` |boolean |`true` |Claim `groups` from JWT will be used to automatically add groups to current subject (may be used with jakarta.annotation.security.RolesAllowed annotation). |`validate-jwt-with-jwk` |boolean |`true` |Use JWK (a set of keys to validate signatures of JWT) to validate tokens. diff --git a/docs/src/main/asciidoc/config/io_helidon_security_providers_oidc_common_OidcConfig.adoc b/docs/src/main/asciidoc/config/io_helidon_security_providers_oidc_common_OidcConfig.adoc index f10d23d0d37..d813d29811e 100644 --- a/docs/src/main/asciidoc/config/io_helidon_security_providers_oidc_common_OidcConfig.adoc +++ b/docs/src/main/asciidoc/config/io_helidon_security_providers_oidc_common_OidcConfig.adoc @@ -60,6 +60,21 @@ Type: link:{javadoc-base-url}/io.helidon.security.providers.oidc.common/io/helid |`client-timeout-millis` |Duration |`30000` |Timeout of calls using web client. |`cookie-domain` |string |{nbsp} |Domain the cookie is valid for. Not used by default. +|`cookie-encryption-enabled` |boolean |`false` |Whether to encrypt token cookie created by this microservice. + Defaults to `false`. +|`cookie-encryption-enabled-id-token` |boolean |`true` |Whether to encrypt id token cookie created by this microservice. + Defaults to `true`. +|`cookie-encryption-enabled-refresh-token` |boolean |`true` |Whether to encrypt refresh token cookie created by this microservice. + Defaults to `true`. +|`cookie-encryption-enabled-tenant-name` |boolean |`true` |Whether to encrypt tenant name cookie created by this microservice. + Defaults to `true`. +|`cookie-encryption-name` |string |{nbsp} |Name of the encryption configuration available through Security#encrypt(String, byte[]) and + Security#decrypt(String, String). + If configured and encryption is enabled for any cookie, + Security MUST be configured in global or current `io.helidon.common.context.Context` (this + is done automatically in Helidon MP). +|`cookie-encryption-password` |char[] |{nbsp} |Master password for encryption/decryption of cookies. This must be configured to the same value on each microservice + using the cookie. |`cookie-http-only` |boolean |`true` |When using cookie, if set to true, the HttpOnly attribute will be configured. Defaults to `OidcCookieHandler.Builder#DEFAULT_HTTP_ONLY`. |`cookie-max-age-seconds` |long |{nbsp} |When using cookie, used to set MaxAge attribute of the cookie, defining how long @@ -67,6 +82,13 @@ Type: link:{javadoc-base-url}/io.helidon.security.providers.oidc.common/io/helid Not used by default. |`cookie-name` |string |`JSESSIONID` |Name of the cookie to use. Defaults to `DEFAULT_COOKIE_NAME`. +|`cookie-name-id-token` |string |`JSESSIONID_2` |Name of the cookie to use for id token. + Defaults to `DEFAULT_COOKIE_NAME`_2. + + This cookie is only used when logout is enabled, as otherwise it is not needed. + Content of this cookie is encrypted. +|`cookie-name-refresh-token` |string |`JSESSIONID_3` |The name of the cookie to use for the refresh token. + Defaults to `DEFAULT_REFRESH_COOKIE_NAME`. |`cookie-name-tenant` |string |`HELIDON_TENANT` |The name of the cookie to use for the tenant name. Defaults to `DEFAULT_TENANT_COOKIE_NAME`. |`cookie-path` |string |`/` |Path the cookie is valid for. @@ -86,6 +108,9 @@ Type: link:{javadoc-base-url}/io.helidon.security.providers.oidc.common/io/helid process header containing a JWT. Default is "Authorization" header with a prefix "bearer ". |`header-use` |boolean |`true` |Whether to expect JWT in a header field. +|`id-token-signature-validation` |boolean |`true` |Whether id token signature check should be enabled. + Signature check is enabled by default, and it is highly recommended to not change that. + Change this setting only when you really know what you are doing, otherwise it could case security issues. |`identity-uri` |URI |{nbsp} |URI of the identity server, base used to retrieve OIDC metadata. |`introspect-endpoint-uri` |URI |{nbsp} |Endpoint to use to validate JWT. Either use this or set #signJwk(JwkKeys) or #signJwk(Resource). @@ -107,7 +132,8 @@ Type: link:{javadoc-base-url}/io.helidon.security.providers.oidc.common/io/helid Defaults to `DEFAULT_PROXY_PORT` |`proxy-protocol` |string |`http` |Proxy protocol to use when proxy is used. Defaults to `DEFAULT_PROXY_PROTOCOL`. -|`query-param-name` |string |`accessToken` |Name of a query parameter that contains the JWT token when parameter is used. +|`query-id-token-param-name` |string |`id_token` |Name of a query parameter that contains the JWT id token when parameter is used. +|`query-param-name` |string |`accessToken` |Name of a query parameter that contains the JWT access token when parameter is used. |`query-param-tenant-name` |string |`h_tenant` |Name of a query parameter that contains the tenant name when the parameter is used. Defaults to #DEFAULT_TENANT_PARAM_NAME. |`query-param-use` |boolean |`false` |Whether to use a query parameter to send JWT token from application to this @@ -151,6 +177,9 @@ Type: link:{javadoc-base-url}/io.helidon.security.providers.oidc.common/io/helid code. If not defined, it is obtained from #oidcMetadata(Resource), if that is not defined an attempt is made to use #identityUri(URI)/oauth2/v1/token. +|`token-signature-validation` |boolean |`true` |Whether access token signature check should be enabled. + Signature check is enabled by default, and it is highly recommended to not change that. + Change this setting only when you really know what you are doing, otherwise it could case security issues. |`validate-jwt-with-jwk` |boolean |`true` |Use JWK (a set of keys to validate signatures of JWT) to validate tokens. Use this method when you want to use default values for JWK or introspection endpoint URI. diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java index 536fca24de1..7439a9583df 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java @@ -43,6 +43,7 @@ import io.helidon.security.integration.common.SecurityTracing; import io.helidon.security.internal.SecurityAuditEvent; import io.helidon.security.providers.common.spi.AnnotationAnalyzer; +import io.helidon.webserver.security.SecurityHttpFeature; import jakarta.annotation.PostConstruct; import jakarta.annotation.Priority; @@ -56,6 +57,7 @@ import jakarta.ws.rs.container.ContainerResponseFilter; import jakarta.ws.rs.core.Application; import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; import org.glassfish.jersey.server.ExtendedUriInfo; import org.glassfish.jersey.server.model.AbstractResourceModelVisitor; @@ -182,6 +184,16 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont } else { return; } + io.helidon.common.context.Context helidonContext = Contexts.context() + .orElseThrow(() -> new IllegalStateException("Context must be available in Jersey")); + helidonContext.get(SecurityHttpFeature.CONTEXT_RESPONSE_HEADERS, Map.class) + .map(it -> (Map>) it) + .ifPresent(it -> { + MultivaluedMap headers = responseContext.getHeaders(); + for (Map.Entry> entry : it.entrySet()) { + entry.getValue().forEach(value -> headers.add(entry.getKey(), value)); + } + }); SecurityFilterContext fc = (SecurityFilterContext) requestContext.getProperty(PROP_FILTER_CONTEXT); SecurityDefinition methodSecurity = jerseySecurityContext.methodSecurity(); diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java index eb5de88cf5b..2bbc346fea1 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java @@ -24,6 +24,7 @@ import io.helidon.common.HelidonServiceLoader; import io.helidon.common.config.Config; +import io.helidon.common.context.Contexts; import io.helidon.common.uri.UriQuery; import io.helidon.microprofile.security.spi.SecurityResponseMapper; import io.helidon.security.AuthenticationResponse; @@ -37,6 +38,7 @@ import io.helidon.security.integration.common.AtnTracing; import io.helidon.security.integration.common.AtzTracing; import io.helidon.security.integration.common.SecurityTracing; +import io.helidon.webserver.security.SecurityHttpFeature; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.container.ContainerRequestContext; @@ -198,6 +200,9 @@ protected void processAuthentication(SecurityFilterContext context, switch (responseStatus) { case SUCCESS -> { //everything is fine, we can continue with processing + io.helidon.common.context.Context helidonContext = Contexts.context() + .orElseThrow(() -> new IllegalStateException("Context must be available in Jersey")); + helidonContext.register(SecurityHttpFeature.CONTEXT_RESPONSE_HEADERS, response.responseHeaders()); } case FAILURE_FINISH -> { if (methodSecurity.authenticationOptional()) { diff --git a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java index 9e72aa2038e..1cc2d076fd7 100644 --- a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java +++ b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java @@ -324,10 +324,30 @@ public final class OidcConfig extends TenantConfigImpl { * Default name of the header we expect JWT in. */ public static final String PARAM_HEADER_NAME = "X_OIDC_TOKEN_HEADER"; + /** + * Default name of the header we expect JWT in. + */ + public static final String PARAM_ID_HEADER_NAME = "X_OIDC_ID_TOKEN_HEADER"; /** * Default tenant query param name. */ public static final String DEFAULT_TENANT_PARAM_NAME = "h_tenant"; + /** + * Default access token cookie name. + */ + public static final String DEFAULT_COOKIE_NAME = "JSESSIONID"; + /** + * Default id token cookie name. + */ + public static final String DEFAULT_ID_COOKIE_NAME = DEFAULT_COOKIE_NAME + "_2"; + /** + * Default refresh token cookie name. + */ + public static final String DEFAULT_REFRESH_COOKIE_NAME = DEFAULT_COOKIE_NAME + "_3"; + /** + * Default tenant cookie name. + */ + public static final String DEFAULT_TENANT_COOKIE_NAME = "HELIDON_TENANT"; static final String DEFAULT_REDIRECT_URI = "/oidc/redirect"; static final String DEFAULT_LOGOUT_URI = "/oidc/logout"; static final boolean DEFAULT_REDIRECT = true; @@ -340,11 +360,10 @@ public final class OidcConfig extends TenantConfigImpl { static final String DEFAULT_PROXY_PROTOCOL = "http"; static final String TENANT_IDENT = "name"; static final String DEFAULT_PARAM_NAME = "accessToken"; + static final String DEFAULT_ID_TOKEN_PARAM_NAME = "id_token"; static final boolean DEFAULT_PARAM_USE = false; static final boolean DEFAULT_HEADER_USE = false; static final boolean DEFAULT_COOKIE_USE = true; - static final String DEFAULT_COOKIE_NAME = "JSESSIONID"; - static final String DEFAULT_TENANT_COOKIE_NAME = "HELIDON_TENANT"; private static final System.Logger LOGGER = System.getLogger(OidcConfig.class.getName()); @@ -366,13 +385,17 @@ public final class OidcConfig extends TenantConfigImpl { private final LazyValue defaultTenant; private final boolean useParam; private final String paramName; + private final String idTokenParamName; private final String tenantParamName; private final boolean useHeader; private final TokenHandler headerHandler; private final boolean useCookie; private final OidcCookieHandler tokenCookieHandler; private final OidcCookieHandler idTokenCookieHandler; + private final OidcCookieHandler refreshTokenCookieHandler; private final OidcCookieHandler tenantCookieHandler; + private final boolean tokenSignatureValidation; + private final boolean idTokenSignatureValidation; private OidcConfig(Builder builder) { super(builder); @@ -393,6 +416,7 @@ private OidcConfig(Builder builder) { this.useParam = builder.useParam; this.paramName = builder.paramName; + this.idTokenParamName = builder.idTokenParamName; this.tenantParamName = builder.tenantParamName; this.useHeader = builder.useHeader; this.headerHandler = builder.headerHandler; @@ -400,6 +424,9 @@ private OidcConfig(Builder builder) { this.tokenCookieHandler = builder.tokenCookieBuilder.build(); this.idTokenCookieHandler = builder.idTokenCookieBuilder.build(); this.tenantCookieHandler = builder.tenantCookieBuilder.build(); + this.refreshTokenCookieHandler = builder.refreshTokenCookieBuilder.build(); + this.tokenSignatureValidation = builder.tokenSignatureValidation; + this.idTokenSignatureValidation = builder.idTokenSignatureValidation; this.webClientBuilderSupplier = builder.webClientBuilderSupplier; this.defaultTenant = LazyValue.create(() -> Tenant.create(this, this)); @@ -449,6 +476,16 @@ public String paramName() { return paramName; } + /** + * Query id token parameter name. + * + * @return name of the query parameter to use + * @see Builder#idTokenParamName(String) + */ + public String idTokenParamName() { + return idTokenParamName; + } + /** * Tenant query parameter name. * @@ -516,6 +553,15 @@ public OidcCookieHandler tenantCookieHandler() { return tenantCookieHandler; } + /** + * Cookie handler to create cookies or unset cookies for refresh token. + * + * @return a new cookie handler + */ + public OidcCookieHandler refreshTokenCookieHandler() { + return refreshTokenCookieHandler; + } + /** * Redirection URI. * @@ -737,6 +783,24 @@ public URI introspectUri() { return defaultTenant.get().introspectUri(); } + /** + * Whether access token signature should be validated. + * + * @return validate access token signature + */ + public boolean tokenSignatureValidation() { + return tokenSignatureValidation; + } + + /** + * Whether id token signature should be validated. + * + * @return validate id token signature + */ + public boolean idTokenSignatureValidation() { + return idTokenSignatureValidation; + } + Supplier webClientBuilderSupplier() { return webClientBuilderSupplier; } @@ -839,16 +903,22 @@ public static class Builder extends BaseBuilder { private WebClient webClient; private Supplier webClientBuilderSupplier; private String paramName = DEFAULT_PARAM_NAME; + private String idTokenParamName = DEFAULT_ID_TOKEN_PARAM_NAME; private String tenantParamName = DEFAULT_TENANT_PARAM_NAME; private boolean useHeader = DEFAULT_HEADER_USE; private boolean useParam = DEFAULT_PARAM_USE; private final OidcCookieHandler.Builder tenantCookieBuilder = OidcCookieHandler.builder() + .encryptionEnabled(true) .cookieName(DEFAULT_TENANT_COOKIE_NAME); private final OidcCookieHandler.Builder tokenCookieBuilder = OidcCookieHandler.builder() .cookieName(DEFAULT_COOKIE_NAME); private final OidcCookieHandler.Builder idTokenCookieBuilder = OidcCookieHandler.builder() - .cookieName(DEFAULT_COOKIE_NAME + "_2"); + .encryptionEnabled(true) + .cookieName(DEFAULT_ID_COOKIE_NAME); + private final OidcCookieHandler.Builder refreshTokenCookieBuilder = OidcCookieHandler.builder() + .encryptionEnabled(true) + .cookieName(DEFAULT_REFRESH_COOKIE_NAME); private TokenHandler headerHandler = TokenHandler.builder() .tokenHeader("Authorization") .tokenPrefix("bearer ") @@ -856,6 +926,8 @@ public static class Builder extends BaseBuilder { private boolean useCookie = DEFAULT_COOKIE_USE; private boolean cookieSameSiteDefault = true; private boolean relativeUris = DEFAULT_RELATIVE_URIS; + private boolean tokenSignatureValidation = true; + private boolean idTokenSignatureValidation = true; protected Builder() { } @@ -893,10 +965,6 @@ public OidcConfig build() { } } - if (logoutEnabled) { - idTokenCookieBuilder.encryptionEnabled(true); - } - this.webClientBuilderSupplier = () -> OidcUtil.webClientBaseBuilder(proxyProtocol, proxyHost, proxyPort, @@ -904,6 +972,17 @@ public OidcConfig build() { clientTimeout()); this.webClient = webClientBuilderSupplier.get().build(); + if (!tokenSignatureValidation) { + LOGGER.log(Level.WARNING, "You have disabled access token signature validation. " + + "This option should never be disabled for production environment " + + "since it could cause security issues"); + } + if (!idTokenSignatureValidation) { + LOGGER.log(Level.WARNING, "You have disabled id token signature validation. " + + "This option should never be disabled for production environment " + + "since it could cause security issues"); + } + return new OidcConfig(this); } @@ -936,6 +1015,7 @@ public Builder config(Config config) { config.get("cookie-name").asString().ifPresent(this::cookieName); config.get("cookie-name-id-token").asString().ifPresent(this::cookieNameIdToken); config.get("cookie-name-tenant").asString().ifPresent(this::cookieTenantName); + config.get("cookie-name-refresh-token").asString().ifPresent(this::cookieRefreshTokenName); config.get("cookie-domain").asString().ifPresent(this::cookieDomain); config.get("cookie-path").asString().ifPresent(this::cookiePath); config.get("cookie-max-age-seconds").asLong().ifPresent(this::cookieMaxAgeSeconds); @@ -946,6 +1026,7 @@ public Builder config(Config config) { config.get("cookie-encryption-enabled").asBoolean().ifPresent(this::cookieEncryptionEnabled); config.get("cookie-encryption-id-enabled").asBoolean().ifPresent(this::cookieEncryptionEnabledIdToken); config.get("cookie-encryption-tenant-enabled").asBoolean().ifPresent(this::cookieEncryptionEnabledTenantName); + config.get("cookie-encryption-refresh-enabled").asBoolean().ifPresent(this::cookieEncryptionEnabledRefreshToken); config.get("cookie-encryption-password").as(String.class) .map(String::toCharArray) .ifPresent(this::cookieEncryptionPassword); @@ -967,6 +1048,9 @@ public Builder config(Config config) { config.get("token-refresh-before-expiration").as(Duration.class).ifPresent(this::tokenRefreshSkew); + config.get("token-signature-validation").asBoolean().ifPresent(this::tokenSignatureValidation); + config.get("id-token-signature-validation").asBoolean().ifPresent(this::idTokenSignatureValidation); + config.get("tenants").asList(Config.class) .ifPresent(confList -> confList.forEach(tenantConfig -> tenantFromConfig(config, tenantConfig))); @@ -1224,7 +1308,7 @@ public Builder useHeader(Boolean useHeader) { } /** - * Name of a query parameter that contains the JWT token when parameter is used. + * Name of a query parameter that contains the JWT access token when parameter is used. * * @param paramName name of the query parameter to expect * @return updated builder instance @@ -1235,6 +1319,18 @@ public Builder paramName(String paramName) { return this; } + /** + * Name of a query parameter that contains the JWT id token when parameter is used. + * + * @param idTokenParamName name of the query parameter to expect + * @return updated builder instance + */ + @ConfiguredOption(key = "query-id-token-param-name", value = DEFAULT_ID_TOKEN_PARAM_NAME) + public Builder idTokenParamName(String idTokenParamName) { + this.idTokenParamName = idTokenParamName; + return this; + } + /** * Name of a query parameter that contains the tenant name when the parameter is used. * Defaults to {@link #DEFAULT_TENANT_PARAM_NAME}. @@ -1272,10 +1368,12 @@ public Builder useParam(Boolean useParam) { * @param cookieEncryptionName name of the encryption configuration in security used to encrypt/decrypt cookies * @return updated builder */ + @ConfiguredOption public Builder cookieEncryptionName(String cookieEncryptionName) { this.tokenCookieBuilder.encryptionName(cookieEncryptionName); this.idTokenCookieBuilder.encryptionName(cookieEncryptionName); this.tenantCookieBuilder.encryptionName(cookieEncryptionName); + this.refreshTokenCookieBuilder.encryptionName(cookieEncryptionName); return this; } @@ -1286,10 +1384,12 @@ public Builder cookieEncryptionName(String cookieEncryptionName) { * @param cookieEncryptionPassword encryption password * @return updated builder */ + @ConfiguredOption public Builder cookieEncryptionPassword(char[] cookieEncryptionPassword) { this.tokenCookieBuilder.encryptionPassword(cookieEncryptionPassword); this.idTokenCookieBuilder.encryptionPassword(cookieEncryptionPassword); this.tenantCookieBuilder.encryptionPassword(cookieEncryptionPassword); + this.refreshTokenCookieBuilder.encryptionPassword(cookieEncryptionPassword); return this; } @@ -1301,6 +1401,7 @@ public Builder cookieEncryptionPassword(char[] cookieEncryptionPassword) { * OIDC server {@code false} * @return updated builder instance */ + @ConfiguredOption(value = "false") public Builder cookieEncryptionEnabled(boolean cookieEncryptionEnabled) { this.tokenCookieBuilder.encryptionEnabled(cookieEncryptionEnabled); return this; @@ -1314,6 +1415,7 @@ public Builder cookieEncryptionEnabled(boolean cookieEncryptionEnabled) { * OIDC server {@code false} * @return updated builder instance */ + @ConfiguredOption(value = "true") public Builder cookieEncryptionEnabledIdToken(boolean cookieEncryptionEnabled) { this.idTokenCookieBuilder.encryptionEnabled(cookieEncryptionEnabled); return this; @@ -1323,15 +1425,29 @@ public Builder cookieEncryptionEnabledIdToken(boolean cookieEncryptionEnabled) { * Whether to encrypt tenant name cookie created by this microservice. * Defaults to {@code true}. * - * @param cookieEncryptionEnabled whether cookie should be encrypted {@code true}, or as obtained from - * OIDC server {@code false} + * @param cookieEncryptionEnabled whether cookie should be encrypted {@code true}, or as plain text name {@code false} * @return updated builder instance */ + @ConfiguredOption(value = "true") public Builder cookieEncryptionEnabledTenantName(boolean cookieEncryptionEnabled) { this.tenantCookieBuilder.encryptionEnabled(cookieEncryptionEnabled); return this; } + /** + * Whether to encrypt refresh token cookie created by this microservice. + * Defaults to {@code true}. + * + * @param cookieEncryptionEnabled whether cookie should be encrypted {@code true}, or as obtained from + * OIDC server {@code false} + * @return updated builder instance + */ + @ConfiguredOption(value = "true") + public Builder cookieEncryptionEnabledRefreshToken(boolean cookieEncryptionEnabled) { + this.refreshTokenCookieBuilder.encryptionEnabled(cookieEncryptionEnabled); + return this; + } + /** * When using cookie, used to set the SameSite cookie value. Can be * "Strict" or "Lax" @@ -1355,6 +1471,7 @@ public Builder cookieSameSite(SetCookie.SameSite sameSite) { this.tokenCookieBuilder.sameSite(sameSite); this.idTokenCookieBuilder.sameSite(sameSite); this.tenantCookieBuilder.sameSite(sameSite); + this.refreshTokenCookieBuilder.sameSite(sameSite); this.cookieSameSiteDefault = false; return this; } @@ -1371,6 +1488,7 @@ public Builder cookieSecure(Boolean secure) { this.tokenCookieBuilder.secure(secure); this.idTokenCookieBuilder.secure(secure); this.tenantCookieBuilder.secure(secure); + this.refreshTokenCookieBuilder.secure(secure); return this; } @@ -1386,6 +1504,7 @@ public Builder cookieHttpOnly(Boolean httpOnly) { this.tokenCookieBuilder.httpOnly(httpOnly); this.idTokenCookieBuilder.httpOnly(httpOnly); this.tenantCookieBuilder.httpOnly(httpOnly); + this.refreshTokenCookieBuilder.httpOnly(httpOnly); return this; } @@ -1402,6 +1521,7 @@ public Builder cookieMaxAgeSeconds(long age) { this.tokenCookieBuilder.maxAge(age); this.idTokenCookieBuilder.maxAge(age); this.tenantCookieBuilder.maxAge(age); + this.refreshTokenCookieBuilder.maxAge(age); return this; } @@ -1417,6 +1537,7 @@ public Builder cookiePath(String path) { this.tokenCookieBuilder.path(path); this.idTokenCookieBuilder.path(path); this.tenantCookieBuilder.path(path); + this.refreshTokenCookieBuilder.path(path); return this; } @@ -1432,6 +1553,7 @@ public Builder cookieDomain(String domain) { this.tokenCookieBuilder.domain(domain); this.idTokenCookieBuilder.domain(domain); this.tenantCookieBuilder.domain(domain); + this.refreshTokenCookieBuilder.domain(domain); return this; } @@ -1458,6 +1580,7 @@ public Builder cookieName(String cookieName) { * @param cookieName name of a cookie * @return updated builder instance */ + @ConfiguredOption(key = "cookie-name-id-token", value = DEFAULT_ID_COOKIE_NAME) public Builder cookieNameIdToken(String cookieName) { this.idTokenCookieBuilder.cookieName(cookieName); return this; @@ -1476,6 +1599,19 @@ public Builder cookieTenantName(String cookieName) { return this; } + /** + * The name of the cookie to use for the refresh token. + * Defaults to {@value #DEFAULT_REFRESH_COOKIE_NAME}. + * + * @param cookieName name of a cookie + * @return updated builder instance + */ + @ConfiguredOption(key = "cookie-name-refresh-token", value = DEFAULT_REFRESH_COOKIE_NAME) + public Builder cookieRefreshTokenName(String cookieName) { + this.refreshTokenCookieBuilder.cookieName(cookieName); + return this; + } + /** * Whether to use cookie to store JWT between requests. * Defaults to {@value #DEFAULT_COOKIE_USE}. @@ -1500,5 +1636,34 @@ public Builder addTenantConfig(TenantConfig tenantConfig) { tenantConfigurations.put(tenantConfig.name(), tenantConfig); return this; } + + /** + * Whether access token signature check should be enabled. + * Signature check is enabled by default, and it is highly recommended to not change that. + * Change this setting only when you really know what you are doing, otherwise it could case security issues. + * + * @param enabled whether access token signature check is enabled + * @return updated builder instance + */ + @ConfiguredOption("true") + public Builder tokenSignatureValidation(boolean enabled) { + tokenSignatureValidation = enabled; + return this; + } + + /** + * Whether id token signature check should be enabled. + * Signature check is enabled by default, and it is highly recommended to not change that. + * Change this setting only when you really know what you are doing, otherwise it could case security issues. + * + * @param enabled whether id token signature check is enabled + * @return updated builder instance + */ + @ConfiguredOption("true") + public Builder idTokenSignatureValidation(boolean enabled) { + idTokenSignatureValidation = enabled; + return this; + } + } } diff --git a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcFeature.java b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcFeature.java index 2a09b47cf04..a870c943382 100644 --- a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcFeature.java +++ b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcFeature.java @@ -151,6 +151,7 @@ public final class OidcFeature implements HttpFeature { private final OidcConfig oidcConfig; private final OidcCookieHandler tokenCookieHandler; private final OidcCookieHandler idTokenCookieHandler; + private final OidcCookieHandler refreshTokenCookieHandler; private final OidcCookieHandler tenantCookieHandler; private final boolean enabled; private final CorsSupport corsSupport; @@ -160,6 +161,7 @@ private OidcFeature(Builder builder) { this.enabled = builder.enabled; this.tokenCookieHandler = oidcConfig.tokenCookieHandler(); this.idTokenCookieHandler = oidcConfig.idTokenCookieHandler(); + this.refreshTokenCookieHandler = oidcConfig.refreshTokenCookieHandler(); this.tenantCookieHandler = oidcConfig.tenantCookieHandler(); this.corsSupport = prepareCrossOriginSupport(oidcConfig.redirectUri(), oidcConfig.crossOriginConfig()); this.oidcConfigFinders = List.copyOf(builder.tenantConfigFinders); @@ -321,6 +323,7 @@ private void logoutWithTenant(ServerRequest req, ServerResponse res, Tenant tena headers.addCookie(tokenCookieHandler.removeCookie().build()); headers.addCookie(idTokenCookieHandler.removeCookie().build()); headers.addCookie(tenantCookieHandler.removeCookie().build()); + headers.addCookie(refreshTokenCookieHandler.removeCookie().build()); res.status(Status.TEMPORARY_REDIRECT_307) .header(HeaderNames.LOCATION, sb.toString()) @@ -450,14 +453,18 @@ private String processJsonResponse(ServerRequest req, ServerResponse res, JsonObject json, String tenantName) { - String tokenValue = json.getString("access_token"); + String accessToken = json.getString("access_token"); String idToken = json.getString("id_token", null); + String refreshToken = json.getString("refresh_token", null); //redirect to "state" String state = req.query().first(STATE_PARAM_NAME).orElse(DEFAULT_REDIRECT); res.status(Status.TEMPORARY_REDIRECT_307); if (oidcConfig.useParam()) { - state += (state.contains("?") ? "&" : "?") + oidcConfig.paramName() + "=" + tokenValue; + state += (state.contains("?") ? "&" : "?") + encode(oidcConfig.paramName()) + "=" + accessToken; + if (idToken != null) { + state += "&" + encode(oidcConfig.idTokenParamName()) + "=" + idToken; + } if (!DEFAULT_TENANT_ID.equals(tenantName)) { state += "&" + encode(oidcConfig.tenantParamName()) + "=" + encode(tenantName); } @@ -473,9 +480,10 @@ private String processJsonResponse(ServerRequest req, OidcCookieHandler tenantCookieHandler = oidcConfig.tenantCookieHandler(); headers.addCookie(tenantCookieHandler.createCookie(tenantName).build()); //Add tenant name cookie - headers.addCookie(tokenCookieHandler.createCookie(tokenValue).build()); //Add token cookie + headers.addCookie(tokenCookieHandler.createCookie(accessToken).build()); //Add token cookie + headers.addCookie(refreshTokenCookieHandler.createCookie(refreshToken).build()); //Add refresh token cookie - if (idToken != null && oidcConfig.logoutEnabled()) { + if (idToken != null) { headers.addCookie(idTokenCookieHandler.createCookie(idToken).build()); //Add token id cookie } res.send(); diff --git a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/TenantAuthenticationHandler.java b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/TenantAuthenticationHandler.java index f499878c4ad..41a662caa38 100644 --- a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/TenantAuthenticationHandler.java +++ b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/TenantAuthenticationHandler.java @@ -18,6 +18,7 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedList; @@ -62,6 +63,7 @@ import io.helidon.security.util.TokenHandler; import io.helidon.webclient.api.HttpClientRequest; import io.helidon.webclient.api.HttpClientResponse; +import io.helidon.webclient.api.WebClient; import jakarta.json.JsonObject; @@ -73,6 +75,7 @@ class TenantAuthenticationHandler { private static final System.Logger LOGGER = System.getLogger(TenantAuthenticationHandler.class.getName()); private static final TokenHandler PARAM_HEADER_HANDLER = TokenHandler.forHeader(OidcConfig.PARAM_HEADER_NAME); + private static final TokenHandler PARAM_ID_HEADER_HANDLER = TokenHandler.forHeader(OidcConfig.PARAM_ID_HEADER_NAME); private final boolean optional; private final OidcConfig oidcConfig; @@ -169,7 +172,53 @@ class TenantAuthenticationHandler { AuthenticationResponse authenticate(String tenantId, ProviderRequest providerRequest) { /* - 1. Get token from request - if available, validate it and continue + 1. Get id token from request - if available, validate it and process access token + 2. If not - skip to access token validation directly + */ + Optional idToken = Optional.empty(); + try { + if (oidcConfig.useParam()) { + idToken = idToken.or(() -> PARAM_ID_HEADER_HANDLER.extractToken(providerRequest.env().headers())); + if (idToken.isEmpty()) { + idToken = idToken.or(() -> providerRequest.env().queryParams().first(oidcConfig.idTokenParamName()).asOptional()); + } + } + if (oidcConfig.useCookie() && idToken.isEmpty()) { + // only do this for cookies + Optional cookie = oidcConfig.idTokenCookieHandler() + .findCookie(providerRequest.env().headers()); + if (cookie.isPresent()) { + try { + String idTokenValue = cookie.get(); + return validateIdToken(tenantId, providerRequest, idTokenValue); + } catch (Exception e) { + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "Invalid id token in cookie", e); + } + return errorResponse(providerRequest, + Status.UNAUTHORIZED_401, + null, + "Invalid id token", + tenantId); + } + } + } + } catch (SecurityException e) { + LOGGER.log(System.Logger.Level.DEBUG, "Failed to extract token from one of the configured locations", e); + return failOrAbstain("Failed to extract one of the configured tokens" + e); + } + if (idToken.isPresent()) { + return validateIdToken(tenantId, providerRequest, idToken.get()); + } else { + return processAccessToken(tenantId, providerRequest, null); + } + + } + + private AuthenticationResponse processAccessToken(String tenantId, ProviderRequest providerRequest, Jwt idToken) { + /* + Access token is mandatory! + 1. Get access token from request - if available, validate it and continue 2. If not - Redirect to login page */ List missingLocations = new LinkedList<>(); @@ -206,34 +255,33 @@ AuthenticationResponse authenticate(String tenantId, ProviderRequest providerReq } else { try { String tokenValue = cookie.get(); - return validateToken(tenantId, providerRequest, tokenValue); + return validateAccessToken(tenantId, providerRequest, tokenValue, idToken); } catch (Exception e) { if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { - LOGGER.log(System.Logger.Level.DEBUG, "Invalid token in cookie", e); + LOGGER.log(System.Logger.Level.DEBUG, "Invalid access token in cookie", e); } return errorResponse(providerRequest, Status.UNAUTHORIZED_401, null, - "Invalid token", + "Invalid access token", tenantId); } } } } } catch (SecurityException e) { - LOGGER.log(System.Logger.Level.DEBUG, "Failed to extract token from one of the configured locations", e); + LOGGER.log(System.Logger.Level.DEBUG, "Failed to extract access token from one of the configured locations", e); return failOrAbstain("Failed to extract one of the configured tokens" + e); } if (token.isPresent()) { - return validateToken(tenantId, providerRequest, token.get()); + return validateAccessToken(tenantId, providerRequest, token.get(), idToken); } else { - LOGGER.log(System.Logger.Level.DEBUG, () -> "Missing token, could not find in either of: " + missingLocations); + LOGGER.log(System.Logger.Level.DEBUG, () -> "Missing access token, could not find in either of: " + missingLocations); return errorResponse(providerRequest, Status.UNAUTHORIZED_401, null, - "Missing token, could not find in either of: " - + missingLocations, + "Missing access token, could not find in either of: " + missingLocations, tenantId); } } @@ -283,7 +331,7 @@ private AuthenticationResponse errorResponse(ProviderRequest providerRequest, StringBuilder scopes = new StringBuilder(tenantConfig.baseScopes()); for (String expectedScope : expectedScopes) { - if (scopes.length() > 0) { + if (!scopes.isEmpty()) { // space after base scopes scopes.append(' '); } @@ -307,14 +355,12 @@ private AuthenticationResponse errorResponse(ProviderRequest providerRequest, + encode(oidcConfig.tenantParamName()) + "=" + encode(tenantId)); } - - StringBuilder queryString = new StringBuilder("?"); - queryString.append("client_id=").append(tenantConfig.clientId()).append("&"); - queryString.append("response_type=code&"); - queryString.append("redirect_uri=").append(redirectUri).append("&"); - queryString.append("scope=").append(scopeString).append("&"); - queryString.append("nonce=").append(nonce).append("&"); - queryString.append("state=").append(encode(state)); + String queryString = "?" + "client_id=" + tenantConfig.clientId() + "&" + + "response_type=code&" + + "redirect_uri=" + redirectUri + "&" + + "scope=" + scopeString + "&" + + "nonce=" + nonce + "&" + + "state=" + encode(state); // must redirect return AuthenticationResponse @@ -332,7 +378,7 @@ private AuthenticationResponse errorResponse(ProviderRequest providerRequest, private String redirectUri(SecurityEnvironment env) { for (Map.Entry> entry : env.headers().entrySet()) { if (entry.getKey().equalsIgnoreCase("host") && !entry.getValue().isEmpty()) { - String firstHost = entry.getValue().get(0); + String firstHost = entry.getValue().getFirst(); return oidcConfig.redirectUriWithHost(oidcConfig.forceHttpsRedirects() ? "https" : env.transport() + "://" + firstHost); } @@ -404,14 +450,62 @@ private String origUri(ProviderRequest providerRequest) { origUri = List.of(providerRequest.env().targetUri().getPath()); } - return origUri.get(0); + return origUri.getFirst(); } private String encode(String state) { return URLEncoder.encode(state, StandardCharsets.UTF_8); } - private AuthenticationResponse validateToken(String tenantId, ProviderRequest providerRequest, String token) { + private AuthenticationResponse validateIdToken(String tenantId, ProviderRequest providerRequest, String idToken) { + SignedJwt signedJwt; + try { + signedJwt = SignedJwt.parseToken(idToken); + } catch (Exception e) { + //invalid token + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "Could not parse inbound id token", e); + } + return AuthenticationResponse.failed("Invalid id token", e); + } + + try { + Errors errors; + if (oidcConfig.idTokenSignatureValidation()) { + errors = jwtValidator.apply(signedJwt, Errors.collector()).collect(); + } else { + errors = Errors.collector().collect(); + } + Jwt jwt = signedJwt.getJwt(); + Errors validationErrors = jwt.validate(tenant.issuer(), + tenantConfig.clientId(), + true); + + if (errors.isValid() && validationErrors.isValid()) { + return processAccessToken(tenantId, providerRequest, jwt); + } else { + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + errors.log(LOGGER); + validationErrors.log(LOGGER); + } + return errorResponse(providerRequest, + Status.UNAUTHORIZED_401, + "invalid_id_token", + "Id token not valid", + tenantId); + } + } catch (Exception e) { + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "Failed to validate request", e); + } + return AuthenticationResponse.failed("Failed to validate JWT", e); + } + } + + private AuthenticationResponse validateAccessToken(String tenantId, + ProviderRequest providerRequest, + String token, + Jwt idToken) { SignedJwt signedJwt; try { signedJwt = SignedJwt.parseToken(token); @@ -424,8 +518,26 @@ private AuthenticationResponse validateToken(String tenantId, ProviderRequest pr } try { - Errors.Collector collector = jwtValidator.apply(signedJwt, Errors.collector()); - return processValidationResult(providerRequest, signedJwt, tenantId, collector); + Errors.Collector collector; + if (oidcConfig.tokenSignatureValidation()) { + collector = jwtValidator.apply(signedJwt, Errors.collector()); + } else { + collector = Errors.collector(); + } + Errors timeErrors = signedJwt.getJwt().validate(Jwt.defaultTimeValidators()); + if (timeErrors.isValid()) { + return processValidationResult(providerRequest, signedJwt, idToken, tenantId, collector); + } else { + //Access token expired, we should attempt to refresh it + Optional refreshToken = oidcConfig.refreshTokenCookieHandler() + .findCookie(providerRequest.env().headers()); + //If we have no refresh token to use. Continue with evaluation and reuse failure mechanism. + return refreshToken.map(refreshTokenValue -> refreshAccessToken(providerRequest, + refreshTokenValue, + idToken, + tenantId)) + .orElseGet(() -> processValidationResult(providerRequest, signedJwt, idToken, tenantId, collector)); + } } catch (Exception e) { if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { LOGGER.log(System.Logger.Level.DEBUG, "Failed to validate request", e); @@ -434,10 +546,109 @@ private AuthenticationResponse validateToken(String tenantId, ProviderRequest pr } } + private AuthenticationResponse refreshAccessToken(ProviderRequest providerRequest, + String refreshTokenString, + Jwt idToken, + String tenantId) { + try { + WebClient webClient = tenant.appWebClient(); + Parameters.Builder form = Parameters.builder("oidc-form-params") + .add("grant_type", "refresh_token") + .add("refresh_token", refreshTokenString) + .add("client_id", tenantConfig.clientId()) + .add("client_secret", tenantConfig.clientSecret()); + + HttpClientRequest post = webClient.post() + .uri(tenant.tokenEndpointUri()) + .header(HeaderValues.ACCEPT_JSON); + + try (HttpClientResponse response = post.submit(form.build())) { + if (response.status().family() == Status.Family.SUCCESSFUL) { + try { + JsonObject jsonObject = response.as(JsonObject.class); + String accessToken = jsonObject.getString("access_token"); + String refreshToken = jsonObject.getString("refresh_token", null); + + SignedJwt signedAccessToken; + try { + signedAccessToken = SignedJwt.parseToken(accessToken); + } catch (Exception e) { + //invalid token + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "Could not parse refreshed access token", e); + } + return AuthenticationResponse.failed("Invalid access token", e); + } + Errors.Collector newAccessTokenCollector = jwtValidator.apply(signedAccessToken, Errors.collector()); + + List setCookieParts = new ArrayList<>(); + setCookieParts.add(oidcConfig.tokenCookieHandler() + .createCookie(accessToken) + .build() + .toString()); + if (refreshToken != null) { + setCookieParts.add(oidcConfig.refreshTokenCookieHandler() + .createCookie(refreshToken) + .build() + .toString()); + } + return processValidationResult(providerRequest, + signedAccessToken, + idToken, + tenantId, + newAccessTokenCollector, + setCookieParts); + } catch (Exception e) { + return errorResponse(providerRequest, + Status.UNAUTHORIZED_401, + "refresh_access_token_failure", + "Failed to refresh access token", + tenantId); + } + } else { + String message; + try { + message = response.as(String.class); + return errorResponse(providerRequest, + Status.UNAUTHORIZED_401, + "access_token_refresh_failed", + "Failed to refresh access token. Response status was: " + + response.status() + " " + + "with message: " + message, + tenantId); + } catch (Exception e) { + return AuthenticationResponse.failed( + "Failed to refresh access token, request failed: Failed to process error entity", + e); + } + } + } catch (Exception e) { + return AuthenticationResponse.failed( + "Failed to refresh access token, request failed: Failed to invoke request", + e); + } + } catch (Exception e) { + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "Failed to validate refresh token", e); + } + return AuthenticationResponse.failed("Failed to validate refresh token", e); + } + } + private AuthenticationResponse processValidationResult(ProviderRequest providerRequest, SignedJwt signedJwt, + Jwt idToken, String tenantId, Errors.Collector collector) { + return processValidationResult(providerRequest, signedJwt, idToken, tenantId, collector, List.of()); + } + + private AuthenticationResponse processValidationResult(ProviderRequest providerRequest, + SignedJwt signedJwt, + Jwt idToken, + String tenantId, + Errors.Collector collector, + List cookies) { Jwt jwt = signedJwt.getJwt(); Errors errors = collector.collect(); Errors validationErrors = jwt.validate(tenant.issuer(), @@ -447,7 +658,7 @@ private AuthenticationResponse processValidationResult(ProviderRequest providerR if (errors.isValid() && validationErrors.isValid()) { errors.log(LOGGER); - Subject subject = buildSubject(jwt, signedJwt); + Subject subject = buildSubject(jwt, signedJwt, idToken); Set scopes = subject.grantsByType("scope") .stream() @@ -464,7 +675,11 @@ private AuthenticationResponse processValidationResult(ProviderRequest providerR } if (missingScopes.isEmpty()) { - return AuthenticationResponse.success(subject); + return AuthenticationResponse.builder() + .status(SecurityResponse.SecurityStatus.SUCCESS) + .user(subject) + .responseHeader(HeaderNames.SET_COOKIE.defaultCase(), cookies) + .build(); } else { return errorResponse(providerRequest, Status.FORBIDDEN_403, @@ -486,8 +701,8 @@ private AuthenticationResponse processValidationResult(ProviderRequest providerR } } - private Subject buildSubject(Jwt jwt, SignedJwt signedJwt) { - Principal principal = buildPrincipal(jwt); + private Subject buildSubject(Jwt jwt, SignedJwt signedJwt, Jwt idToken) { + Principal principal = buildPrincipal(jwt, idToken); TokenCredential.Builder builder = TokenCredential.builder(); jwt.issueTime().ifPresent(builder::issueTime); @@ -516,11 +731,16 @@ private Subject buildSubject(Jwt jwt, SignedJwt signedJwt) { } - private Principal buildPrincipal(Jwt jwt) { - String subject = jwt.subject() + private Principal buildPrincipal(Jwt accessToken, Jwt idToken) { + Jwt tokenToUse = idToken; + if (idToken == null) { + tokenToUse = accessToken; + } + + String subject = tokenToUse.subject() .orElseThrow(() -> new JwtException("JWT does not contain subject claim, cannot create principal.")); - String name = jwt.preferredUsername() + String name = tokenToUse.preferredUsername() .orElse(subject); Principal.Builder builder = Principal.builder(); @@ -528,15 +748,15 @@ private Principal buildPrincipal(Jwt jwt) { builder.name(name) .id(subject); - jwt.payloadClaims() + tokenToUse.payloadClaims() .forEach((key, jsonValue) -> builder.addAttribute(key, JwtUtil.toObject(jsonValue))); - jwt.email().ifPresent(value -> builder.addAttribute("email", value)); - jwt.emailVerified().ifPresent(value -> builder.addAttribute("email_verified", value)); - jwt.locale().ifPresent(value -> builder.addAttribute("locale", value)); - jwt.familyName().ifPresent(value -> builder.addAttribute("family_name", value)); - jwt.givenName().ifPresent(value -> builder.addAttribute("given_name", value)); - jwt.fullName().ifPresent(value -> builder.addAttribute("full_name", value)); + tokenToUse.email().ifPresent(value -> builder.addAttribute("email", value)); + tokenToUse.emailVerified().ifPresent(value -> builder.addAttribute("email_verified", value)); + tokenToUse.locale().ifPresent(value -> builder.addAttribute("locale", value)); + tokenToUse.familyName().ifPresent(value -> builder.addAttribute("family_name", value)); + tokenToUse.givenName().ifPresent(value -> builder.addAttribute("given_name", value)); + tokenToUse.fullName().ifPresent(value -> builder.addAttribute("full_name", value)); return builder.build(); } diff --git a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/CommonLoginBase.java b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/CommonLoginBase.java index f987d27dc94..9ca4b479cd0 100644 --- a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/CommonLoginBase.java +++ b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/CommonLoginBase.java @@ -27,6 +27,7 @@ import jakarta.ws.rs.client.ClientBuilder; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.client.ClientProperties; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.testcontainers.junit.jupiter.Container; @@ -65,4 +66,9 @@ public void beforeEach() { client = ClientBuilder.newClient(CONFIG); } + @AfterEach + public void afterEach() { + client.close(); + } + } diff --git a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/IdTokenIT.java b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/IdTokenIT.java new file mode 100644 index 00000000000..6e4c42bdfe8 --- /dev/null +++ b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/IdTokenIT.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.tests.integration.oidc; + +import java.time.Instant; +import java.util.List; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.jersey.connector.HelidonConnectorProvider; +import io.helidon.jersey.connector.HelidonProperties; +import io.helidon.microprofile.testing.junit5.AddConfig; +import io.helidon.security.jwt.Jwt; +import io.helidon.security.jwt.SignedJwt; +import io.helidon.security.jwt.jwk.Jwk; +import io.helidon.security.providers.oidc.common.OidcConfig; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.helidon.security.providers.oidc.common.OidcConfig.DEFAULT_ID_COOKIE_NAME; +import static io.helidon.tests.integration.oidc.TestResource.EXPECTED_TEST_MESSAGE; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; + +class IdTokenIT extends CommonLoginBase { + + private static final ClientConfig CONFIG_CLIENT_2 = new ClientConfig() + .connectorProvider(new HelidonConnectorProvider()) + .property(ClientProperties.CONNECT_TIMEOUT, 10000000) + .property(ClientProperties.READ_TIMEOUT, 10000000) + .property(ClientProperties.FOLLOW_REDIRECTS, true) + .property(HelidonProperties.CONFIG, Config.builder() + .addSource(ConfigSources.classpath("application-no-cookie.yaml")) + .build() + .get("client")); + + private Client client2; + + @BeforeEach + public void beforeEach2() { + client2 = ClientBuilder.newClient(CONFIG_CLIENT_2); + } + + @AfterEach + public void afterEach2() { + client2.close(); + } + + @Test + public void testAuthenticationWithoutIdToken(WebTarget webTarget) { + List setCookies = obtainCookies(webTarget); + + //Ignore ID token cookie + Invocation.Builder request = client2.target(webTarget.getUri()).path("/test").request(); + for (String setCookie : setCookies) { + if (!setCookie.startsWith(DEFAULT_ID_COOKIE_NAME + "=")) { + request.header(HttpHeaders.COOKIE, setCookie); + } + } + + try (Response response = request.get()) { + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + assertThat(response.readEntity(String.class), is(EXPECTED_TEST_MESSAGE)); + assertThat(response.getHeaderString(HttpHeaders.SET_COOKIE), nullValue()); + } + } + + @Test + @AddConfig(key = "security.providers.1.oidc.id-token-signature-validation", value = "false") + @AddConfig(key = "security.providers.1.oidc.cookie-encryption-id-enabled", value = "false") + public void testAuthenticationWithExpiredIdToken(WebTarget webTarget) { + List setCookies = obtainCookies(webTarget); + + //Since id token validation is disabled, it is enough to just create some invalid one in terms of date. + Jwt jwt = Jwt.builder() + .issueTime(Instant.ofEpochMilli(1)) + .expirationTime(Instant.ofEpochMilli(1)) + .notBefore(Instant.ofEpochMilli(1)) + .build(); + SignedJwt signedJwt = SignedJwt.sign(jwt, Jwk.NONE_JWK); + + //Ignore ID token cookie + Invocation.Builder request = client2.target(webTarget.getUri()) + .path("/test") + .request() + .property(ClientProperties.FOLLOW_REDIRECTS, false); + for (String setCookie : setCookies) { + if (!setCookie.startsWith(DEFAULT_ID_COOKIE_NAME + "=")) { + request.header(HttpHeaders.COOKIE, setCookie); + } else { + request.header(HttpHeaders.COOKIE, DEFAULT_ID_COOKIE_NAME + "=" + signedJwt.tokenContent()); + } + } + + try (Response response = request.get()) { + assertThat(response.getStatus(), is(Response.Status.TEMPORARY_REDIRECT.getStatusCode())); + } + + } + + private List obtainCookies(WebTarget webTarget) { + String formUri; + + //greet endpoint is protected, and we need to get JWT token out of the Keycloak. We will get redirected to the Keycloak. + try (Response response = client.target(webTarget.getUri()) + .path("/test") + .request() + .get()) { + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + //We need to get form URI out of the HTML + formUri = getRequestUri(response.readEntity(String.class)); + System.out.println(formUri); + } + + String redirectHelidonUrl; + //Sending authentication to the Keycloak and getting redirected back to the running Helidon app. + //Redirection needs to be disabled, so we can get Set-Cookie header from Helidon redirect endpoint + Entity
form = Entity.form(new Form().param("username", "userone") + .param("password", "12345") + .param("credentialId", "")); + try (Response response = client.target(formUri) + .request() + .property(ClientProperties.FOLLOW_REDIRECTS, false) + .header("Connection", "close") + .post(form)) { + assertThat(response.getStatus(), is(Response.Status.FOUND.getStatusCode())); + redirectHelidonUrl = response.getStringHeaders().getFirst(HttpHeaders.LOCATION); + } + + List setCookies; + //Helidon OIDC redirect endpoint -> Sends back Set-Cookie header + try (Response response = client.target(redirectHelidonUrl) + .request() + .property(ClientProperties.FOLLOW_REDIRECTS, false) + .get()) { + assertThat(response.getStatus(), is(Response.Status.TEMPORARY_REDIRECT.getStatusCode())); + //Since invalid access token has been provided, this means that the new one has been obtained + setCookies = response.getStringHeaders().get(HttpHeaders.SET_COOKIE); + assertThat(setCookies, not(empty())); + assertThat(setCookies, hasItem(startsWith(DEFAULT_ID_COOKIE_NAME))); + } + + return setCookies; + } + + private String getRequestUri(String html) { + Document document = Jsoup.parse(html); + return document.getElementById("kc-form-login").attr("action"); + } + +} diff --git a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/QueryBasedLoginIT.java b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/QueryBasedLoginIT.java index e3cab7e4db3..f7e29b47518 100644 --- a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/QueryBasedLoginIT.java +++ b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/QueryBasedLoginIT.java @@ -35,6 +35,7 @@ @AddConfig(key = "security.providers.1.oidc.cookie-use", value = "false") @AddConfig(key = "security.providers.1.oidc.query-param-use", value = "true") +@AddConfig(key = "server.protocols.http_1_1.max-prologue-length", value = "4096") class QueryBasedLoginIT extends CommonLoginBase { @Test diff --git a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/RefreshTokenIT.java b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/RefreshTokenIT.java new file mode 100644 index 00000000000..aaccacae628 --- /dev/null +++ b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/RefreshTokenIT.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.tests.integration.oidc; + +import java.time.Instant; +import java.util.List; + +import io.helidon.microprofile.testing.junit5.AddConfig; +import io.helidon.security.jwt.Jwt; +import io.helidon.security.jwt.SignedJwt; +import io.helidon.security.jwt.jwk.Jwk; +import io.helidon.security.providers.oidc.common.OidcConfig; + +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.junit.jupiter.api.Test; + +import static io.helidon.tests.integration.oidc.TestResource.EXPECTED_POST_LOGOUT_TEST_MESSAGE; +import static io.helidon.tests.integration.oidc.TestResource.EXPECTED_TEST_MESSAGE; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; + +@AddConfig(key = "security.providers.1.oidc.token-signature-validation", value = "false") +class RefreshTokenIT extends CommonLoginBase { + + @Test + public void testRefreshToken(WebTarget webTarget) { + String formUri; + + //greet endpoint is protected, and we need to get JWT token out of the Keycloak. We will get redirected to the Keycloak. + try (Response response = client.target(webTarget.getUri()).path("/test") + .request() + .get()) { + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + //We need to get form URI out of the HTML + formUri = getRequestUri(response.readEntity(String.class)); + } + + //Sending authentication to the Keycloak and getting redirected back to the running Helidon app. + Entity form = Entity.form(new Form().param("username", "userone") + .param("password", "12345") + .param("credentialId", "")); + try (Response response = client.target(formUri).request().post(form)) { + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + assertThat(response.readEntity(String.class), is(EXPECTED_TEST_MESSAGE)); + } + + //next request should have cookie set, and we do not need to authenticate again + try (Response response = client.target(webTarget.getUri()).path("/test").request().get()) { + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + assertThat(response.readEntity(String.class), is(EXPECTED_TEST_MESSAGE)); + assertThat(response.getHeaderString(HttpHeaders.SET_COOKIE), nullValue()); + } + + //Since access token validation is disabled, it is enough to just create some invalid one in terms of date. + Jwt jwt = Jwt.builder() + .issueTime(Instant.ofEpochMilli(1)) + .expirationTime(Instant.ofEpochMilli(1)) + .notBefore(Instant.ofEpochMilli(1)) + .build(); + SignedJwt signedJwt = SignedJwt.sign(jwt, Jwk.NONE_JWK); + + try (Response response = client + .target(webTarget.getUri()) + .path("/test") + .request() + .header(HttpHeaders.COOKIE, OidcConfig.DEFAULT_COOKIE_NAME + "=" + signedJwt.tokenContent()) + .get()) { + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + assertThat(response.readEntity(String.class), is(EXPECTED_TEST_MESSAGE)); + //Since invalid access token has been provided, this means that the new one has been obtained + List cookies = response.getStringHeaders().get(HttpHeaders.SET_COOKIE); + assertThat(cookies, not(empty())); + assertThat(cookies, hasItem(startsWith(OidcConfig.DEFAULT_COOKIE_NAME))); + } + + //next request should have cookie set, and we do not need to authenticate again + try (Response response = client.target(webTarget.getUri()).path("/test").request().get()) { + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + assertThat(response.readEntity(String.class), is(EXPECTED_TEST_MESSAGE)); + assertThat(response.getHeaderString(HttpHeaders.SET_COOKIE), nullValue()); + } + + } + + private String getRequestUri(String html) { + Document document = Jsoup.parse(html); + return document.getElementById("kc-form-login").attr("action"); + } + +} diff --git a/tests/integration/oidc/src/test/resources/application-no-cookie.yaml b/tests/integration/oidc/src/test/resources/application-no-cookie.yaml new file mode 100644 index 00000000000..3b51b69793d --- /dev/null +++ b/tests/integration/oidc/src/test/resources/application-no-cookie.yaml @@ -0,0 +1,19 @@ +# +# Copyright (c) 2023 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +client: + cookie-manager: + automatic-store-enabled: false diff --git a/webserver/security/src/main/java/io/helidon/webserver/security/SecurityHttpFeature.java b/webserver/security/src/main/java/io/helidon/webserver/security/SecurityHttpFeature.java index 47029297e19..d44dec0a587 100644 --- a/webserver/security/src/main/java/io/helidon/webserver/security/SecurityHttpFeature.java +++ b/webserver/security/src/main/java/io/helidon/webserver/security/SecurityHttpFeature.java @@ -93,6 +93,7 @@ public final class SecurityHttpFeature implements HttpSecurity, HttpFeature, Wei * to the integration). */ public static final String CONTEXT_ADD_HEADERS = "security.addHeaders"; + public static final String CONTEXT_RESPONSE_HEADERS = "security.responseHeaders"; private static final Logger LOGGER = Logger.getLogger(SecurityHttpFeature.class.getName()); From 5f1313d07d182dfffe5a9fc062f508f6e65e8932 Mon Sep 17 00:00:00 2001 From: David Kral Date: Fri, 15 Dec 2023 12:09:14 +0100 Subject: [PATCH 2/5] Checkstyle Signed-off-by: David Kral --- .../security/providers/oidc/TenantAuthenticationHandler.java | 4 +++- .../io/helidon/webserver/security/SecurityHttpFeature.java | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/TenantAuthenticationHandler.java b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/TenantAuthenticationHandler.java index 41a662caa38..e1299a0f9d6 100644 --- a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/TenantAuthenticationHandler.java +++ b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/TenantAuthenticationHandler.java @@ -180,7 +180,9 @@ AuthenticationResponse authenticate(String tenantId, ProviderRequest providerReq if (oidcConfig.useParam()) { idToken = idToken.or(() -> PARAM_ID_HEADER_HANDLER.extractToken(providerRequest.env().headers())); if (idToken.isEmpty()) { - idToken = idToken.or(() -> providerRequest.env().queryParams().first(oidcConfig.idTokenParamName()).asOptional()); + idToken = idToken.or(() -> providerRequest.env() + .queryParams() + .first(oidcConfig.idTokenParamName()).asOptional()); } } if (oidcConfig.useCookie() && idToken.isEmpty()) { diff --git a/webserver/security/src/main/java/io/helidon/webserver/security/SecurityHttpFeature.java b/webserver/security/src/main/java/io/helidon/webserver/security/SecurityHttpFeature.java index d44dec0a587..7f95d0e7de9 100644 --- a/webserver/security/src/main/java/io/helidon/webserver/security/SecurityHttpFeature.java +++ b/webserver/security/src/main/java/io/helidon/webserver/security/SecurityHttpFeature.java @@ -93,6 +93,10 @@ public final class SecurityHttpFeature implements HttpSecurity, HttpFeature, Wei * to the integration). */ public static final String CONTEXT_ADD_HEADERS = "security.addHeaders"; + /** + * Security can accept additional headers to be added to security request. + * This will be used to propagate additional headers from successful security response to the final server response. + */ public static final String CONTEXT_RESPONSE_HEADERS = "security.responseHeaders"; private static final Logger LOGGER = Logger.getLogger(SecurityHttpFeature.class.getName()); From 274f24d6681680aec8e7b8d71ef96bce7b30f598 Mon Sep 17 00:00:00 2001 From: David Kral Date: Tue, 2 Jan 2024 10:38:58 +0100 Subject: [PATCH 3/5] Copyright changed due to year change Signed-off-by: David Kral --- .../config/io_helidon_security_providers_oidc_OidcProvider.adoc | 2 +- .../io_helidon_security_providers_oidc_common_OidcConfig.adoc | 2 +- .../java/io/helidon/microprofile/security/SecurityFilter.java | 2 +- .../io/helidon/microprofile/security/SecurityFilterCommon.java | 2 +- .../io/helidon/security/providers/oidc/common/OidcConfig.java | 2 +- .../java/io/helidon/security/providers/oidc/OidcFeature.java | 2 +- .../security/providers/oidc/TenantAuthenticationHandler.java | 2 +- .../java/io/helidon/tests/integration/oidc/CommonLoginBase.java | 2 +- .../test/java/io/helidon/tests/integration/oidc/IdTokenIT.java | 2 +- .../io/helidon/tests/integration/oidc/QueryBasedLoginIT.java | 2 +- .../java/io/helidon/tests/integration/oidc/RefreshTokenIT.java | 2 +- .../oidc/src/test/resources/application-no-cookie.yaml | 2 +- .../java/io/helidon/webserver/security/SecurityHttpFeature.java | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/src/main/asciidoc/config/io_helidon_security_providers_oidc_OidcProvider.adoc b/docs/src/main/asciidoc/config/io_helidon_security_providers_oidc_OidcProvider.adoc index a2b6a09436c..c48756d7273 100644 --- a/docs/src/main/asciidoc/config/io_helidon_security_providers_oidc_OidcProvider.adoc +++ b/docs/src/main/asciidoc/config/io_helidon_security_providers_oidc_OidcProvider.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2023 Oracle and/or its affiliates. + Copyright (c) 2023, 2024 Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/docs/src/main/asciidoc/config/io_helidon_security_providers_oidc_common_OidcConfig.adoc b/docs/src/main/asciidoc/config/io_helidon_security_providers_oidc_common_OidcConfig.adoc index d813d29811e..39f177902af 100644 --- a/docs/src/main/asciidoc/config/io_helidon_security_providers_oidc_common_OidcConfig.adoc +++ b/docs/src/main/asciidoc/config/io_helidon_security_providers_oidc_common_OidcConfig.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2023 Oracle and/or its affiliates. + Copyright (c) 2023, 2024 Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java index 7439a9583df..aecebfe0304 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java index 2bbc346fea1..b4142c14ef6 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java index 1cc2d076fd7..55b8395aa7a 100644 --- a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java +++ b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcFeature.java b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcFeature.java index a870c943382..2c2b0187e38 100644 --- a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcFeature.java +++ b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcFeature.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/TenantAuthenticationHandler.java b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/TenantAuthenticationHandler.java index e1299a0f9d6..101e4a59378 100644 --- a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/TenantAuthenticationHandler.java +++ b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/TenantAuthenticationHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/CommonLoginBase.java b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/CommonLoginBase.java index 9ca4b479cd0..61f1494eea5 100644 --- a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/CommonLoginBase.java +++ b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/CommonLoginBase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/IdTokenIT.java b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/IdTokenIT.java index 6e4c42bdfe8..94eedd3ce04 100644 --- a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/IdTokenIT.java +++ b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/IdTokenIT.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/QueryBasedLoginIT.java b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/QueryBasedLoginIT.java index f7e29b47518..db43adcba97 100644 --- a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/QueryBasedLoginIT.java +++ b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/QueryBasedLoginIT.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/RefreshTokenIT.java b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/RefreshTokenIT.java index aaccacae628..d6170329b3f 100644 --- a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/RefreshTokenIT.java +++ b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/RefreshTokenIT.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tests/integration/oidc/src/test/resources/application-no-cookie.yaml b/tests/integration/oidc/src/test/resources/application-no-cookie.yaml index 3b51b69793d..b458f60b94c 100644 --- a/tests/integration/oidc/src/test/resources/application-no-cookie.yaml +++ b/tests/integration/oidc/src/test/resources/application-no-cookie.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 Oracle and/or its affiliates. +# Copyright (c) 2023, 2024 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/webserver/security/src/main/java/io/helidon/webserver/security/SecurityHttpFeature.java b/webserver/security/src/main/java/io/helidon/webserver/security/SecurityHttpFeature.java index 7f95d0e7de9..054702197b4 100644 --- a/webserver/security/src/main/java/io/helidon/webserver/security/SecurityHttpFeature.java +++ b/webserver/security/src/main/java/io/helidon/webserver/security/SecurityHttpFeature.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From b3adde45f7507a358bdd7fbbeb89f9243cd91951 Mon Sep 17 00:00:00 2001 From: David Kral Date: Tue, 2 Jan 2024 13:31:58 +0100 Subject: [PATCH 4/5] Condition flipped Signed-off-by: David Kral --- .../java/io/helidon/tests/integration/oidc/IdTokenIT.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/IdTokenIT.java b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/IdTokenIT.java index 94eedd3ce04..b8efaae7004 100644 --- a/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/IdTokenIT.java +++ b/tests/integration/oidc/src/test/java/io/helidon/tests/integration/oidc/IdTokenIT.java @@ -118,10 +118,10 @@ public void testAuthenticationWithExpiredIdToken(WebTarget webTarget) { .request() .property(ClientProperties.FOLLOW_REDIRECTS, false); for (String setCookie : setCookies) { - if (!setCookie.startsWith(DEFAULT_ID_COOKIE_NAME + "=")) { - request.header(HttpHeaders.COOKIE, setCookie); - } else { + if (setCookie.startsWith(DEFAULT_ID_COOKIE_NAME + "=")) { request.header(HttpHeaders.COOKIE, DEFAULT_ID_COOKIE_NAME + "=" + signedJwt.tokenContent()); + } else { + request.header(HttpHeaders.COOKIE, setCookie); } } From adee4c0fb87228b682b05336f92068cf0d3e5985 Mon Sep 17 00:00:00 2001 From: David Kral Date: Mon, 8 Jan 2024 10:52:57 +0100 Subject: [PATCH 5/5] review comments addressed Signed-off-by: David Kral --- .../security/providers/oidc/common/OidcConfig.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java index 55b8395aa7a..b16a6009d4b 100644 --- a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java +++ b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java @@ -1015,7 +1015,7 @@ public Builder config(Config config) { config.get("cookie-name").asString().ifPresent(this::cookieName); config.get("cookie-name-id-token").asString().ifPresent(this::cookieNameIdToken); config.get("cookie-name-tenant").asString().ifPresent(this::cookieTenantName); - config.get("cookie-name-refresh-token").asString().ifPresent(this::cookieRefreshTokenName); + config.get("cookie-name-refresh-token").asString().ifPresent(this::cookieNameRefreshToken); config.get("cookie-domain").asString().ifPresent(this::cookieDomain); config.get("cookie-path").asString().ifPresent(this::cookiePath); config.get("cookie-max-age-seconds").asLong().ifPresent(this::cookieMaxAgeSeconds); @@ -1580,7 +1580,7 @@ public Builder cookieName(String cookieName) { * @param cookieName name of a cookie * @return updated builder instance */ - @ConfiguredOption(key = "cookie-name-id-token", value = DEFAULT_ID_COOKIE_NAME) + @ConfiguredOption(DEFAULT_ID_COOKIE_NAME) public Builder cookieNameIdToken(String cookieName) { this.idTokenCookieBuilder.cookieName(cookieName); return this; @@ -1606,8 +1606,8 @@ public Builder cookieTenantName(String cookieName) { * @param cookieName name of a cookie * @return updated builder instance */ - @ConfiguredOption(key = "cookie-name-refresh-token", value = DEFAULT_REFRESH_COOKIE_NAME) - public Builder cookieRefreshTokenName(String cookieName) { + @ConfiguredOption(DEFAULT_REFRESH_COOKIE_NAME) + public Builder cookieNameRefreshToken(String cookieName) { this.refreshTokenCookieBuilder.cookieName(cookieName); return this; }