diff --git a/docs/shared/security/providers/oidc.adoc b/docs/shared/security/providers/oidc.adoc index 1931bc3a483..7d68a75afb6 100644 --- a/docs/shared/security/providers/oidc.adoc +++ b/docs/shared/security/providers/oidc.adoc @@ -82,6 +82,7 @@ an important distinction when more than one provider is used |`proxy-protocol` |`http` |Proxy protocol to use when proxy is used |`proxy-host` |`null` |Proxy host to use. When defined, triggers usage of proxy for HTTP requests |`proxy-port` |`80` |Port of the proxy server to use +|`relative-uris`|`false`|Can be set to `true` to force the use of relative URIs in all requests, regardless of the presence or absence of proxies or no-proxy lists. By default, requests that use the Proxy will have absolute URIs. Set this flag to `true` if the receiving host is unable to accept absolute URIs. |`redirect-uri` |`/oidc/redirect` |URI to register web server component on, used by the OIDC server to redirect authorization requests to after a user logs in or approves scopes. Note that usually the redirect URI configured here must be the same one as configured on OIDC server. |`scope-audience` |empty string |Audience of the scope required by this application. This is prefixed to the scope name when requesting scopes from the identity server. |`cookie-use` |`true` |Whether to use cookie to store JWT. If used, redirects happen only in case the user is not authenticated or has insufficient scopes 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 b9697e105cc..2deff823f06 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 @@ -117,6 +117,13 @@ * Port of the proxy server to use * * + * relative-uris + * false + * Flag to force the use of relative URIs in all requests. By default, + * requests that use the Proxy will have absolute URIs. Set this flag to + * true if the host is unable to accept absolute URIs. + * + * * redirect-uri * /oidc/redirect * URI to register web server component on, used by the OIDC server to @@ -332,6 +339,7 @@ public final class OidcConfig { static final boolean DEFAULT_PARAM_USE = false; static final boolean DEFAULT_HEADER_USE = false; static final String DEFAULT_PROXY_PROTOCOL = "http"; + static final boolean DEFAULT_RELATIVE_URIS = false; static final String DEFAULT_BASE_SCOPES = "openid"; static final boolean DEFAULT_JWT_VALIDATE_JWK = true; static final boolean DEFAULT_REDIRECT = true; @@ -385,6 +393,7 @@ public final class OidcConfig { private final CrossOriginConfig crossOriginConfig; private final boolean forceHttpsRedirects; private final Duration tokenRefreshSkew; + private final boolean relativeUris; private OidcConfig(Builder builder) { this.clientId = builder.clientId; @@ -418,6 +427,7 @@ private OidcConfig(Builder builder) { this.tokenEndpointAuthentication = builder.tokenEndpointAuthentication; this.clientTimeout = builder.clientTimeout; this.forceHttpsRedirects = builder.forceHttpsRedirects; + this.relativeUris = builder.relativeUris; if (tokenEndpointAuthentication == ClientAuthentication.CLIENT_SECRET_POST) { // we should only store this if required @@ -986,6 +996,16 @@ public Duration tokenRefreshSkew() { return tokenRefreshSkew; } + /** + * Determines whether to force the use of relative URIs in all requests, + * regardless of the presence or absence of proxies or no-proxy lists. + * + * @return {@code true} if we should use relative URIs + */ + public boolean relativeUris() { + return relativeUris; + } + /** * Client Authentication methods that are used by Clients to authenticate to the Authorization * Server when using the Token Endpoint. @@ -1088,6 +1108,7 @@ public static class Builder implements io.helidon.common.Builder { private String proxyProtocol = DEFAULT_PROXY_PROTOCOL; private String proxyHost; private int proxyPort = DEFAULT_PROXY_PORT; + private boolean relativeUris = DEFAULT_RELATIVE_URIS; private String scopeAudience; private OidcMetadata.Builder oidcMetadata = OidcMetadata.builder(); private String frontendUri; @@ -1140,6 +1161,7 @@ public OidcConfig build() { WebClient.Builder webClientBuilder = OidcUtil.webClientBaseBuilder(proxyHost, proxyPort, + relativeUris, clientTimeout); ClientBuilder clientBuilder = OidcUtil.clientBaseBuilder(proxyProtocol, proxyHost, proxyPort); @@ -1275,6 +1297,7 @@ public Builder config(Config config) { .ifPresent(this::proxyProtocol); config.get("proxy-host").asString().ifPresent(this::proxyHost); config.get("proxy-port").asInt().ifPresent(this::proxyPort); + config.get("relative-uris").asBoolean().ifPresent(this::relativeUris); // our application config.get("redirect-uri").asString().ifPresent(this::redirectUri); @@ -1951,6 +1974,22 @@ public Builder proxyPort(int proxyPort) { return this; } + /** + * Can be set to {@code true} to force the use of relative URIs in all requests, + * regardless of the presence or absence of proxies or no-proxy lists. By default, + * requests that use the Proxy will have absolute URIs. Set this flag to {@code true} + * if the host is unable to accept absolute URIs. + * Defaults to {@value #DEFAULT_RELATIVE_URIS}. + * + * @param relativeUris relative URIs flag + * @return updated builder instance + */ + @ConfiguredOption("false") + public Builder relativeUris(boolean relativeUris) { + this.relativeUris = relativeUris; + return this; + } + /** * Client ID as generated by OIDC server. * diff --git a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcUtil.java b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcUtil.java index ae07d5b20ea..b221118d255 100644 --- a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcUtil.java +++ b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2022 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. @@ -69,12 +69,14 @@ static ClientBuilder clientBaseBuilder(String proxyProtocol, String proxyHost, i static WebClient.Builder webClientBaseBuilder(String proxyHost, int proxyPort, + boolean relativeUris, Duration clientTimeout) { WebClient.Builder webClientBuilder = WebClient.builder() .addService(WebClientTracing.create()) .addMediaSupport(JsonpSupport.create()) .connectTimeout(clientTimeout.toMillis(), TimeUnit.MILLISECONDS) - .readTimeout(clientTimeout.toMillis(), TimeUnit.MILLISECONDS); + .readTimeout(clientTimeout.toMillis(), TimeUnit.MILLISECONDS) + .relativeUris(relativeUris); if (proxyHost != null) { webClientBuilder.proxy(Proxy.builder() diff --git a/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcConfigAbstractTest.java b/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcConfigAbstractTest.java index 1347b45f488..a7afac2ab42 100644 --- a/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcConfigAbstractTest.java +++ b/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcConfigAbstractTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2022 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. @@ -48,7 +48,10 @@ void testExplicitValues() { is("http://identity.oracle.com/authorization")), () -> assertThat("Introspect endpoint", config.introspectEndpoint().getUri(), - is(URI.create("http://identity.oracle.com/introspect"))) + is(URI.create("http://identity.oracle.com/introspect"))), + () -> assertThat("Validate relativeUris flag", + config.relativeUris(), + is(true)) ); } diff --git a/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcConfigFromBuilderTest.java b/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcConfigFromBuilderTest.java index c0fbea8f1fc..5cd0700fbc6 100644 --- a/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcConfigFromBuilderTest.java +++ b/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcConfigFromBuilderTest.java @@ -18,12 +18,18 @@ import io.helidon.config.Config; import io.helidon.config.ConfigSources; +import io.helidon.webserver.Routing; +import io.helidon.webserver.WebServer; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import java.net.URI; +import java.time.Duration; import java.util.Arrays; import java.util.Map; @@ -32,6 +38,9 @@ */ class OidcConfigFromBuilderTest extends OidcConfigAbstractTest { private OidcConfig oidcConfig; + private boolean isCommunicationWithProxy = true; + private String httpHostPort; + private boolean relativeUris; private String cookieEncryptionPasswordValue; OidcConfigFromBuilderTest() { @@ -46,6 +55,7 @@ class OidcConfigFromBuilderTest extends OidcConfigAbstractTest { .tokenEndpointUri(URI.create("http://identity.oracle.com/tokens")) .authorizationEndpointUri(URI.create("http://identity.oracle.com/authorization")) .introspectEndpointUri(URI.create("http://identity.oracle.com/introspect")) + .relativeUris(true) .build(); } @@ -54,6 +64,105 @@ OidcConfig getConfig() { return oidcConfig; } + @Test + void testDefaultValues() { + OidcConfig config = OidcConfig.builder() + // The next 3 parameters need to be set or config builder will fail + .identityUri(URI.create("https://identity.oracle.com")) + .clientId("client-id-value") + .clientSecret("client-secret-value") + // Set to false so it will not load metadata + .oidcMetadataWellKnown(false) + .build(); + assertAll("All values using defaults", + () -> assertThat("Redirect URI", config.redirectUri(), is(OidcConfig.DEFAULT_REDIRECT_URI)), + () -> assertThat("Should Redirect", config.shouldRedirect(), is(OidcConfig.DEFAULT_REDIRECT)), + () -> assertThat("Logout URI", config.logoutUri(), is(OidcConfig.DEFAULT_LOGOUT_URI)), + () -> assertThat("Use Parameter", config.useParam(), is(OidcConfig.DEFAULT_PARAM_USE)), + () -> assertThat("Parameter Name", config.paramName(), is(OidcConfig.DEFAULT_PARAM_NAME)), + () -> assertThat("Relative URIs", config.relativeUris(), is(OidcConfig.DEFAULT_RELATIVE_URIS)), + () -> assertThat("Use Cookie", config.useCookie(), is(OidcConfig.DEFAULT_COOKIE_USE)), + () -> assertThat("Use Header", config.useHeader(), is(OidcConfig.DEFAULT_HEADER_USE)), + () -> assertThat("Base scopes to use", config.baseScopes(), is(OidcConfig.DEFAULT_BASE_SCOPES)), + () -> assertThat("Cookie value prefix", config.cookieValuePrefix(), is(OidcConfig.DEFAULT_COOKIE_NAME + "=")), + () -> assertThat("Cookie name", config.cookieName(), is(OidcConfig.DEFAULT_COOKIE_NAME)), + () -> assertThat("Realm", config.realm(), is(OidcConfig.DEFAULT_REALM)), + () -> assertThat("Redirect Attempt Parameter", config.redirectAttemptParam(), is(OidcConfig.DEFAULT_ATTEMPT_PARAM)), + () -> assertThat("Max Redirects", config.maxRedirects(), is(OidcConfig.DEFAULT_MAX_REDIRECTS)), + () -> assertThat("Client Timeout", config.clientTimeout(), is(Duration.ofSeconds(OidcConfig.DEFAULT_TIMEOUT_SECONDS))), + () -> assertThat("Force HTTPS Redirects", config.forceHttpsRedirects(), is(OidcConfig.DEFAULT_FORCE_HTTPS_REDIRECTS)), + () -> assertThat("Token Refresh Skew", config.tokenRefreshSkew(), is(OidcConfig.DEFAULT_TOKEN_REFRESH_SKEW)), + // cookie options should be separated by space as defined by the specification + () -> assertThat("Cookie options", config.cookieOptions(), is("; Path=/; HttpOnly; SameSite=Lax")), + () -> assertThat("Audience", config.audience(), is("https://identity.oracle.com")), + () -> assertThat("Parameter name", config.paramName(), is("accessToken")), + () -> assertThat("Issuer", config.issuer(), nullValue()), + () -> assertThat("Client without authentication", config.generalClient(), notNullValue()), + () -> assertThat("Client with authentication", config.appClient(), notNullValue()), + () -> assertThat("JWK Keys", config.signJwk(), notNullValue()) + ); + } + + @Test + void testRequestUrisWithProxy() { + httpHostPort = ""; // This will be set once the server is up + isCommunicationWithProxy = true; // initial request is with a proxy + // This server will simulate a Proxy on the 1st request and Identity Server on the 2nd request + WebServer proxyAndIdentityServer = WebServer.builder() + .host("localhost") + .routing(Routing.builder() + .any((req, res) -> { + // Simulate a successful Proxy response + if (isCommunicationWithProxy) { + // Flip to false so next request will simulate Identity Server interaction + isCommunicationWithProxy = false; + res.send(); + } + // Simulate a failed Identity response if relativeURIs=false but the request URI is relative + else if (!relativeUris && !req.uri().toASCIIString().startsWith(httpHostPort)) { + res.status(500); + res.send("URI must be absolute"); + } + // Simulate a failed Identity response if relativeURIs=true but the request URI is absolute + else if (relativeUris && req.uri().toASCIIString().startsWith(httpHostPort)) { + res.status(500); + res.send("URI must be relative"); + } + // Simulate a successful Identity response + else { + res.send("{}"); + } + })) + .build(); + proxyAndIdentityServer.start().await(Duration.ofSeconds(10)); + httpHostPort = "http://localhost:" + proxyAndIdentityServer.port(); + + // 1st test will simulate relativeUris=false and will fail if URI is relative + OidcConfig.builder() + // The next 3 parameters need to be set or config builder will fail + .identityUri(URI.create(httpHostPort + "/identity")) + .clientId("client-id-value") + .clientSecret("client-secret-value") + .proxyProtocol("http") + .proxyHost("localhost") + .proxyPort(proxyAndIdentityServer.port()) + .build(); + + // 2nd test will simulate relativeUris=true and will fail if URI is absolute + relativeUris = true; + OidcConfig.builder() + // The next 3 parameters need to be set or config builder will fail + .identityUri(URI.create(httpHostPort + "/identity")) + .clientId("client-id-value") + .clientSecret("client-secret-value") + .proxyProtocol("http") + .proxyHost("localhost") + .proxyPort(proxyAndIdentityServer.port()) + .relativeUris(relativeUris) + .build(); + proxyAndIdentityServer.shutdown(); + } + @Test void testCookieEncryptionPasswordFromBuilderConfig() { OidcConfig.Builder builder = new TestOidcConfigBuilder(); diff --git a/security/providers/oidc-common/src/test/resources/application.yaml b/security/providers/oidc-common/src/test/resources/application.yaml index 33b86fa0ca8..fcd5dd056c9 100644 --- a/security/providers/oidc-common/src/test/resources/application.yaml +++ b/security/providers/oidc-common/src/test/resources/application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2022 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. @@ -28,3 +28,4 @@ security: token-endpoint-uri: "http://identity.oracle.com/tokens" authorization-endpoint-uri: "http://identity.oracle.com/authorization" introspect-endpoint-uri: "http://identity.oracle.com/introspect" + relative-uris: true diff --git a/webclient/webclient/src/main/java/io/helidon/webclient/WebClient.java b/webclient/webclient/src/main/java/io/helidon/webclient/WebClient.java index ecd6b94c880..8a93e05786f 100644 --- a/webclient/webclient/src/main/java/io/helidon/webclient/WebClient.java +++ b/webclient/webclient/src/main/java/io/helidon/webclient/WebClient.java @@ -204,6 +204,18 @@ public Builder proxy(Proxy proxy) { return this; } + /** + * Can be set to {@code true} to force the use of relative URIs in all requests, + * regardless of the presence or absence of proxies or no-proxy lists. + * + * @param relativeUris relative URIs flag + * @return updated builder instance + */ + public Builder relativeUris(boolean relativeUris) { + this.configuration.relativeUris(relativeUris); + return this; + } + @Override public Builder mediaContext(MediaContext mediaContext) { configuration.mediaContext(mediaContext); diff --git a/webclient/webclient/src/test/java/io/helidon/webclient/ProxyTest.java b/webclient/webclient/src/test/java/io/helidon/webclient/ProxyTest.java index f7f6f05e734..0ef61269c38 100644 --- a/webclient/webclient/src/test/java/io/helidon/webclient/ProxyTest.java +++ b/webclient/webclient/src/test/java/io/helidon/webclient/ProxyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2022 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. @@ -80,16 +80,27 @@ void testNoProxyHandlingPredicate() { } @Test - void testForceRelativeUris() { + void testForceRelativeUrisViaWebClientConfiguration() { Config config = Config.create(); Proxy proxy = Proxy.create(config.get("proxy")); WebClientConfiguration webConfig = WebClientConfiguration.builder() - .config(config.get("force-relative-uris")).build(); - boolean relativeUris = webConfig.relativeUris(); - assertThat(relativizeNoProxy(URI.create("http://www.localhost/foo"), proxy, relativeUris).toString(), - is("/foo")); - assertThat(relativizeNoProxy(URI.create("http://identity.oci.com/foo/bar"), proxy, relativeUris).toString(), - is("/foo/bar")); + .config(config.get("force-relative-uris")) + .proxy(proxy) + .build(); + validateRelativizeNoProxy(webConfig); + } + + @Test + void testForceRelativeUrisViaWebClient() { + Proxy proxy = Proxy.builder() + .host("localhost") + .port(8080) + .build(); + WebClientConfiguration webConfig = WebClient.builder() + .relativeUris(true) + .proxy(proxy) + .configuration(); + validateRelativizeNoProxy(webConfig); } @Test @@ -99,7 +110,16 @@ void testDefaultProxyType() { assertThat(proxy.type(), is(Proxy.ProxyType.HTTP)); } + private void validateRelativizeNoProxy(WebClientConfiguration webConfig) { + boolean relativeUris = webConfig.relativeUris(); + Proxy proxy = webConfig.proxy().get(); + assertThat(relativizeNoProxy(URI.create("http://www.localhost/foo"), proxy, relativeUris).toString(), + is("/foo")); + assertThat(relativizeNoProxy(URI.create("http://identity.oci.com/foo/bar"), proxy, relativeUris).toString(), + is("/foo/bar")); + } + private URI address(String host, int port) { return URI.create("http://" + host + ":" + port); } -} \ No newline at end of file +}