Skip to content

Commit

Permalink
Outbound with OIDC provider no longer causes an UnsupportedOperationE…
Browse files Browse the repository at this point in the history
…xception
  • Loading branch information
tomas-langer committed Dec 16, 2019
1 parent 31365ec commit 58c64f1
Show file tree
Hide file tree
Showing 2 changed files with 299 additions and 5 deletions.
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));
}
}

0 comments on commit 58c64f1

Please sign in to comment.