Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Outbound with OIDC provider no longer causes an UnsupportedOperationE… #1195

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
Expand Down Expand Up @@ -50,9 +51,11 @@
import io.helidon.security.AuthenticationResponse;
import io.helidon.security.EndpointConfig;
import io.helidon.security.Grant;
import io.helidon.security.OutboundSecurityResponse;
import io.helidon.security.Principal;
import io.helidon.security.ProviderRequest;
import io.helidon.security.Security;
import io.helidon.security.SecurityEnvironment;
import io.helidon.security.SecurityLevel;
import io.helidon.security.SecurityResponse;
import io.helidon.security.Subject;
Expand All @@ -62,6 +65,8 @@
import io.helidon.security.jwt.JwtUtil;
import io.helidon.security.jwt.SignedJwt;
import io.helidon.security.jwt.jwk.JwkKeys;
import io.helidon.security.providers.common.OutboundConfig;
import io.helidon.security.providers.common.OutboundTarget;
import io.helidon.security.providers.common.TokenCredential;
import io.helidon.security.providers.oidc.common.OidcConfig;
import io.helidon.security.spi.AuthenticationProvider;
Expand All @@ -87,11 +92,16 @@ public final class OidcProvider extends SynchronousProvider implements Authentic

private final OidcConfig oidcConfig;
private final TokenHandler paramHeaderHandler;

private final BiConsumer<SignedJwt, Errors.Collector> jwtValidator;
private final Pattern attemptPattern;
private final boolean propagate;
private final OidcOutboundConfig outboundConfig;

private OidcProvider(OidcConfig oidcConfig) {
this.oidcConfig = oidcConfig;
private OidcProvider(Builder builder, OidcOutboundConfig oidcOutboundConfig) {
this.oidcConfig = builder.oidcConfig;
this.propagate = builder.propagate;
this.outboundConfig = oidcOutboundConfig;

attemptPattern = Pattern.compile(".*?" + oidcConfig.redirectAttemptParam() + "=(\\d+).*");

Expand Down Expand Up @@ -156,7 +166,7 @@ private OidcProvider(OidcConfig oidcConfig) {
* @return a new provider configured for OIDC
*/
public static OidcProvider create(Config config) {
return new OidcProvider(OidcConfig.create(config));
return builder().config(config).build();
}

/**
Expand All @@ -166,7 +176,16 @@ public static OidcProvider create(Config config) {
* @return a new provider configured for OIDC
*/
public static OidcProvider create(OidcConfig config) {
return new OidcProvider(config);
return builder().oidcConfig(config).build();
}

/**
* A fluent API builder to created instances of this provider.
*
* @return a new builder instance
*/
public static Builder builder() {
return new Builder();
}

@Override
Expand Down Expand Up @@ -430,6 +449,41 @@ private AuthenticationResponse validateToken(ProviderRequest providerRequest, St
}
}

@Override
public boolean isOutboundSupported(ProviderRequest providerRequest,
SecurityEnvironment outboundEnv,
EndpointConfig outboundConfig) {
return propagate;
}

@Override
protected OutboundSecurityResponse syncOutbound(ProviderRequest providerRequest,
SecurityEnvironment outboundEnv,
EndpointConfig outboundEndpointConfig) {
Optional<Subject> user = providerRequest.securityContext().user();

if (user.isPresent()) {
// we do have a user, let's see if we can propagate
Subject subject = user.get();
Optional<TokenCredential> tokenCredential = subject.publicCredential(TokenCredential.class);
if (tokenCredential.isPresent()) {
String tokenContent = tokenCredential.get()
.token();

OidcOutboundTarget target = outboundConfig.findTarget(outboundEnv);
boolean enabled = target.propagate;

if (enabled) {
Map<String, List<String>> headers = new HashMap<>();
target.tokenHandler.header(headers, tokenContent);
return OutboundSecurityResponse.withHeaders(headers);
}
}
}

return OutboundSecurityResponse.empty();
}

private Subject buildSubject(Jwt jwt, SignedJwt signedJwt) {
Principal principal = buildPrincipal(jwt);

Expand Down Expand Up @@ -480,4 +534,155 @@ private Principal buildPrincipal(Jwt jwt) {

return builder.build();
}

/**
* Builder for {@link io.helidon.security.providers.oidc.OidcProvider}.
*/
public static final class Builder implements io.helidon.common.Builder<OidcProvider> {
private OidcConfig oidcConfig;
// identity propagation is disabled by default. In general we should not reuse the same token
// for outbound calls, unless it is the same audience
private boolean propagate;
private OutboundConfig outboundConfig;
private TokenHandler defaultOutboundHandler = TokenHandler.builder()
.tokenHeader("Authorization")
.tokenPrefix("bearer ")
.build();

@Override
public OidcProvider build() {
if (null == oidcConfig) {
throw new IllegalArgumentException("OidcConfig must be configured");
}
if (null == outboundConfig) {
outboundConfig = OutboundConfig.builder()
.build();
}
return new OidcProvider(this, new OidcOutboundConfig(outboundConfig, defaultOutboundHandler));
}

/**
* Update this builder with configuration.
* Only updates information that was not explicitly set.
*
* The following configuration options are used:
*
* <table class="config">
* <caption>Optional configuration parameters</caption>
* <tr>
* <th>key</th>
* <th>default value</th>
* <th>description</th>
* </tr>
* <tr>
* <td>&nbsp;</td>
* <td>&nbsp;</td>
* <td>The current config node is used to construct {@link io.helidon.security.providers.oidc.common.OidcConfig}.</td>
* </tr>
* <tr>
* <td>propagate</td>
* <td>false</td>
* <td>Whether to propagate token (overall configuration). If set to false, propagation will
* not be done at all.</td>
* </tr>
* <tr>
* <td>outbound</td>
* <td>&nbsp;</td>
* <td>Configuration of {@link io.helidon.security.providers.common.OutboundConfig}.
* In addition you can use {@code propagate} to disable propagation for an outbound target,
* and {@code token} to configure outbound {@link io.helidon.security.util.TokenHandler} for an
* outbound target. Default token handler uses {@code Authorization} header with a {@code bearer } prefix</td>
* </tr>
* </table>
*
* @param config OIDC provider configuration
* @return updated builder instance
*/
public Builder config(Config config) {
if (null == oidcConfig) {
if (config.get("identity-uri").exists()) {
oidcConfig = OidcConfig.create(config);
}
}
config.get("propagate").as(Boolean.class).ifPresent(this::propagate);
if (null == outboundConfig) {
config.get("outbound").ifExists(outbound -> outboundConfig(OutboundConfig.create(outbound)));
}

return this;
}

/**
* Whether to propagate identity.
*
* @param propagate whether to propagate identity (true) or not (false)
* @return updated builder instance
*/
public Builder propagate(boolean propagate) {
this.propagate = propagate;
return this;
}

/**
* Configuration of outbound rules.
*
* @param config outbound configuration
*
* @return updated builder instance
*/
public Builder outboundConfig(OutboundConfig config) {
this.outboundConfig = config;
return this;
}

/**
* Configuration of OIDC (Open ID Connect).
*
* @param config OIDC configuration for this provider
*
* @return updated builder instance
*/
public Builder oidcConfig(OidcConfig config) {
this.oidcConfig = config;
return this;
}
}

private static final class OidcOutboundConfig {
private final Map<OutboundTarget, OidcOutboundTarget> targetCache = new HashMap<>();
private final OutboundConfig outboundConfig;
private final TokenHandler defaultTokenHandler;
private final OidcOutboundTarget defaultTarget;

private OidcOutboundConfig(OutboundConfig outboundConfig, TokenHandler defaultTokenHandler) {
this.outboundConfig = outboundConfig;
this.defaultTokenHandler = defaultTokenHandler;

this.defaultTarget = new OidcOutboundTarget(true, defaultTokenHandler);
}

private OidcOutboundTarget findTarget(SecurityEnvironment env) {
return outboundConfig.findTarget(env)
.map(value -> targetCache.computeIfAbsent(value, outboundTarget -> {
boolean propagate = outboundTarget.getConfig()
.flatMap(cfg -> cfg.get("propagate").asBoolean().asOptional())
.orElse(true);
TokenHandler handler = outboundTarget.getConfig()
.flatMap(cfg -> cfg.get("token").as(TokenHandler::create).asOptional())
.orElse(defaultTokenHandler);
return new OidcOutboundTarget(propagate, handler);
})).orElse(defaultTarget);
}
}

private static final class OidcOutboundTarget {
private final boolean propagate;
private final TokenHandler tokenHandler;

private OidcOutboundTarget(boolean propagate, TokenHandler handler) {
this.propagate = propagate;
tokenHandler = handler;
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,33 @@
package io.helidon.security.providers.oidc;

import java.net.URI;

import java.util.List;
import java.util.Optional;

import io.helidon.common.CollectionsHelper;
import io.helidon.config.Config;
import io.helidon.config.ConfigSources;
import io.helidon.security.EndpointConfig;
import io.helidon.security.OutboundSecurityResponse;
import io.helidon.security.ProviderRequest;
import io.helidon.security.SecurityContext;
import io.helidon.security.SecurityEnvironment;
import io.helidon.security.Subject;
import io.helidon.security.jwt.jwk.JwkKeys;
import io.helidon.security.providers.common.OutboundConfig;
import io.helidon.security.providers.common.OutboundTarget;
import io.helidon.security.providers.common.TokenCredential;
import io.helidon.security.providers.oidc.common.OidcConfig;

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.hamcrest.CoreMatchers.endsWith;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsNot.not;
import static org.mockito.Mockito.when;

/**
* Unit test for {@link OidcSupport}.
Expand Down Expand Up @@ -55,6 +73,17 @@ class OidcSupportTest {

private final OidcSupport oidcSupport = OidcSupport.create(oidcConfig);
private final OidcSupport oidcSupportCustomParam = OidcSupport.create(oidcConfigCustomParam);
private final OidcProvider provider = OidcProvider.builder()
.oidcConfig(oidcConfig)
.outboundConfig(OutboundConfig.builder()
.addTarget(OutboundTarget.builder("disabled")
.addHost("www.example.com")
.config(Config.create(ConfigSources.create(CollectionsHelper.mapOf(
"propagate",
"false"))))
.build())
.build())
.build();

@Test
void testRedirectAttemptNoParams() {
Expand Down Expand Up @@ -127,4 +156,64 @@ void testRedirectAttemptParamsInMiddleCustomName() {
assertThat(state, not(newState));
assertThat(newState, endsWith(PARAM_NAME + "=12&b=second"));
}

@Test
void testOutbound() {
String tokenContent = "huhahihohyhe";
TokenCredential tokenCredential = TokenCredential.builder()
.token(tokenContent)
.build();

Subject subject = Subject.builder()
.addPublicCredential(TokenCredential.class, tokenCredential)
.build();

ProviderRequest providerRequest = Mockito.mock(ProviderRequest.class);
SecurityContext ctx = Mockito.mock(SecurityContext.class);

when(ctx.user()).thenReturn(Optional.of(subject));
when(providerRequest.securityContext()).thenReturn(ctx);

SecurityEnvironment outboundEnv = SecurityEnvironment.builder()
.targetUri(URI.create("http://localhost:7777"))
.path("/test")
.build();
EndpointConfig endpointConfig = EndpointConfig.builder().build();

OutboundSecurityResponse response = provider.syncOutbound(providerRequest, outboundEnv, endpointConfig);

List<String> authorization = response.requestHeaders().get("Authorization");
assertThat("Authorization header", authorization, hasItem("bearer " + tokenContent));
}

@Test
void testOutboundFull() {
String tokenContent = "huhahihohyhe";
TokenCredential tokenCredential = TokenCredential.builder()
.token(tokenContent)
.build();

Subject subject = Subject.builder()
.addPublicCredential(TokenCredential.class, tokenCredential)
.build();

ProviderRequest providerRequest = Mockito.mock(ProviderRequest.class);
SecurityContext ctx = Mockito.mock(SecurityContext.class);

when(ctx.user()).thenReturn(Optional.of(subject));
when(providerRequest.securityContext()).thenReturn(ctx);

SecurityEnvironment outboundEnv = SecurityEnvironment.builder()
.targetUri(URI.create("http://www.example.com:7777"))
.path("/test")
.build();
EndpointConfig endpointConfig = EndpointConfig.builder().build();

boolean outboundSupported = provider.isOutboundSupported(providerRequest, outboundEnv, endpointConfig);
assertThat("Outbound should not be supported by default", outboundSupported, is(false));

OutboundSecurityResponse response = provider.syncOutbound(providerRequest, outboundEnv, endpointConfig);

assertThat("Disabled target should have empty headers", response.requestHeaders().size(), is(0));
}
}