|
16 | 16 | package com.linecorp.armeria.server.saml; |
17 | 17 |
|
18 | 18 | import static com.google.common.base.MoreObjects.firstNonNull; |
| 19 | +import static com.google.common.base.Preconditions.checkArgument; |
| 20 | +import static com.google.common.base.Preconditions.checkState; |
19 | 21 | import static com.google.common.collect.ImmutableList.toImmutableList; |
20 | 22 | import static com.google.common.collect.ImmutableSet.toImmutableSet; |
21 | 23 | import static com.linecorp.armeria.server.saml.HttpRedirectBindingUtil.responseWithLocation; |
|
68 | 70 | * A builder which builds a {@link SamlServiceProvider}. |
69 | 71 | */ |
70 | 72 | public final class SamlServiceProviderBuilder { |
| 73 | + |
71 | 74 | private static final Logger logger = LoggerFactory.getLogger(SamlServiceProviderBuilder.class); |
72 | 75 |
|
| 76 | + private static final int DEFAULT_MIN_RELAY_STATE_MAX_LENGTH = 80; |
| 77 | + |
73 | 78 | private final List<SamlIdentityProviderConfigBuilder> idpConfigBuilders = new ArrayList<>(); |
74 | 79 | private final List<SamlAssertionConsumerConfigBuilder> acsConfigBuilders = new ArrayList<>(); |
75 | 80 |
|
@@ -101,35 +106,11 @@ public final class SamlServiceProviderBuilder { |
101 | 106 |
|
102 | 107 | private boolean signatureRequired = true; |
103 | 108 |
|
104 | | - private SamlSingleSignOnHandler ssoHandler = new SamlSingleSignOnHandler() { |
105 | | - @Override |
106 | | - public CompletionStage<Void> beforeInitiatingSso(ServiceRequestContext ctx, HttpRequest req, |
107 | | - MessageContext<AuthnRequest> message, |
108 | | - SamlIdentityProviderConfig idpConfig) { |
109 | | - final String requestedPath = req.path(); |
110 | | - if (requestedPath.length() <= 80) { |
111 | | - // Relay the requested path by default. |
112 | | - final SAMLBindingContext sub = message.getSubcontext(SAMLBindingContext.class, true); |
113 | | - assert sub != null : "SAMLBindingContext"; |
114 | | - sub.setRelayState(requestedPath); |
115 | | - } |
116 | | - return UnmodifiableFuture.completedFuture(null); |
117 | | - } |
118 | | - |
119 | | - @Override |
120 | | - public HttpResponse loginSucceeded(ServiceRequestContext ctx, AggregatedHttpRequest req, |
121 | | - MessageContext<Response> message, @Nullable String sessionIndex, |
122 | | - @Nullable String relayState) { |
123 | | - return responseWithLocation(firstNonNull(relayState, "/")); |
124 | | - } |
| 109 | + @Nullable |
| 110 | + private Integer relayStateMaxLength; |
125 | 111 |
|
126 | | - @Override |
127 | | - public HttpResponse loginFailed(ServiceRequestContext ctx, AggregatedHttpRequest req, |
128 | | - @Nullable MessageContext<Response> message, Throwable cause) { |
129 | | - logger.warn("{} SAML SSO failed", ctx, cause); |
130 | | - return responseWithLocation("/error"); |
131 | | - } |
132 | | - }; |
| 112 | + @Nullable |
| 113 | + private SamlSingleSignOnHandler ssoHandler; |
133 | 114 |
|
134 | 115 | private SamlSingleLogoutHandler sloHandler = new SamlSingleLogoutHandler() { |
135 | 116 | @Override |
@@ -263,13 +244,33 @@ public SamlServiceProviderBuilder requestIdManager(SamlRequestIdManager requestI |
263 | 244 | } |
264 | 245 |
|
265 | 246 | /** |
266 | | - * Sets a {@link SamlSingleSignOnHandler} which handles SAML messages for a single sign-on. |
| 247 | + * Sets a {@link SamlSingleSignOnHandler} which handles SAML messages for a single sign-on. If this is set, |
| 248 | + * {@link #relayStateMaxLength(int)} will be ignored so that the |
| 249 | + * {@link SamlSingleSignOnHandler#beforeInitiatingSso(ServiceRequestContext, HttpRequest, |
| 250 | + * MessageContext, SamlIdentityProviderConfig)} |
| 251 | + * is responsible for handling the {@code RelayState} parameter. |
267 | 252 | */ |
268 | 253 | public SamlServiceProviderBuilder ssoHandler(SamlSingleSignOnHandler ssoHandler) { |
| 254 | + checkState(relayStateMaxLength == null, |
| 255 | + "relayStateMaxLength() and ssoHandler() are mutually exclusive."); |
269 | 256 | this.ssoHandler = requireNonNull(ssoHandler, "ssoHandler"); |
270 | 257 | return this; |
271 | 258 | } |
272 | 259 |
|
| 260 | + /** |
| 261 | + * Sets the maximum length of the {@code RelayState} parameter which is sent to an identity provider |
| 262 | + * and is returned with the {@code SAMLResponse} parameter. If the length of the {@code RelayState} |
| 263 | + * exceeds the specified value, the {@code RelayState} parameter will be ignored. |
| 264 | + * The value must be equal to or greater than {@value #DEFAULT_MIN_RELAY_STATE_MAX_LENGTH}. |
| 265 | + */ |
| 266 | + public SamlServiceProviderBuilder relayStateMaxLength(int maxLength) { |
| 267 | + checkState(ssoHandler == null, "relayStateMaxLength() and ssoHandler() are mutually exclusive."); |
| 268 | + checkArgument(maxLength >= DEFAULT_MIN_RELAY_STATE_MAX_LENGTH, |
| 269 | + "maxLength: %s (expected: >= %s)", maxLength, DEFAULT_MIN_RELAY_STATE_MAX_LENGTH); |
| 270 | + relayStateMaxLength = maxLength; |
| 271 | + return this; |
| 272 | + } |
| 273 | + |
273 | 274 | /** |
274 | 275 | * Sets a {@link SamlSingleLogoutHandler} which handles SAML messages for a single sign-on. |
275 | 276 | */ |
@@ -447,6 +448,15 @@ public SamlServiceProvider build() { |
447 | 448 | e); |
448 | 449 | } |
449 | 450 |
|
| 451 | + final SamlSingleSignOnHandler ssoHandler; |
| 452 | + if (this.ssoHandler != null) { |
| 453 | + ssoHandler = this.ssoHandler; |
| 454 | + } else { |
| 455 | + final int relayStateMaxLength = firstNonNull(this.relayStateMaxLength, |
| 456 | + DEFAULT_MIN_RELAY_STATE_MAX_LENGTH); |
| 457 | + ssoHandler = newDefaultSsoHandler(relayStateMaxLength); |
| 458 | + } |
| 459 | + |
450 | 460 | return new SamlServiceProvider(authorizer, |
451 | 461 | entityId, |
452 | 462 | hostname, |
@@ -487,6 +497,41 @@ private static void validateSignatureAlgorithm(String signatureAlgorithm, Creden |
487 | 497 | } |
488 | 498 | } |
489 | 499 |
|
| 500 | + private static SamlSingleSignOnHandler newDefaultSsoHandler(int relayStateMaxLength) { |
| 501 | + return new SamlSingleSignOnHandler() { |
| 502 | + @Override |
| 503 | + public CompletionStage<Void> beforeInitiatingSso(ServiceRequestContext ctx, HttpRequest req, |
| 504 | + MessageContext<AuthnRequest> message, |
| 505 | + SamlIdentityProviderConfig idpConfig) { |
| 506 | + final String requestedPath = req.path(); |
| 507 | + if (requestedPath.length() <= relayStateMaxLength) { |
| 508 | + // Relay the requested path by default. |
| 509 | + final SAMLBindingContext sub = message.getSubcontext(SAMLBindingContext.class, true); |
| 510 | + assert sub != null : "SAMLBindingContext"; |
| 511 | + sub.setRelayState(requestedPath); |
| 512 | + } else { |
| 513 | + logger.debug("requested path length ({}) exceeds the configured maximum length ({}).", |
| 514 | + requestedPath.length(), relayStateMaxLength); |
| 515 | + } |
| 516 | + return UnmodifiableFuture.completedFuture(null); |
| 517 | + } |
| 518 | + |
| 519 | + @Override |
| 520 | + public HttpResponse loginSucceeded(ServiceRequestContext ctx, AggregatedHttpRequest req, |
| 521 | + MessageContext<Response> message, @Nullable String sessionIndex, |
| 522 | + @Nullable String relayState) { |
| 523 | + return responseWithLocation(firstNonNull(relayState, "/")); |
| 524 | + } |
| 525 | + |
| 526 | + @Override |
| 527 | + public HttpResponse loginFailed(ServiceRequestContext ctx, AggregatedHttpRequest req, |
| 528 | + @Nullable MessageContext<Response> message, Throwable cause) { |
| 529 | + logger.warn("{} SAML SSO failed", ctx, cause); |
| 530 | + return responseWithLocation("/error"); |
| 531 | + } |
| 532 | + }; |
| 533 | + } |
| 534 | + |
490 | 535 | /** |
491 | 536 | * An adapter for {@link CredentialResolver} which helps to resolve a {@link Credential} from |
492 | 537 | * the specified {@code keyName}. |
|
0 commit comments