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

IS-176 New signature specifications #181

Merged
merged 2 commits into from
Dec 19, 2024
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 @@ -15,16 +15,11 @@
*/
package se.swedenconnect.signservice.authn.saml;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import net.shibboleth.shared.resolver.ResolverException;
import net.shibboleth.shared.xml.SerializeSupport;
import org.apache.commons.lang3.StringUtils;
import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.core.xml.io.Unmarshaller;
Expand All @@ -42,12 +37,6 @@
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.xmlsec.signature.support.SignatureException;
import org.w3c.dom.Element;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import net.shibboleth.shared.resolver.ResolverException;
import net.shibboleth.shared.xml.SerializeSupport;
import se.idsec.signservice.xml.DOMUtils;
import se.swedenconnect.opensaml.saml2.core.build.RequestedAuthnContextBuilder;
import se.swedenconnect.opensaml.saml2.metadata.EntityDescriptorContainer;
Expand Down Expand Up @@ -88,6 +77,17 @@
import se.swedenconnect.signservice.protocol.msg.AuthnRequirements;
import se.swedenconnect.signservice.protocol.msg.SignMessage;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serial;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

/**
* Abstract base class for SAML authentication handlers.
*/
Expand Down Expand Up @@ -272,7 +272,7 @@ public AuthenticationResultChoice resumeAuthentication(@Nonnull final HttpUserRe
}
final String sentRelayState = context.get(RELAY_STATE_KEY, String.class);

// Setup the response processing input object ...
// Set up the response processing input object ...
//
final ResponseProcessingInput input =
this.createResponseProcessingInput(authnRequest, sentRelayState, httpRequest, context);
Expand Down Expand Up @@ -314,6 +314,7 @@ public AuthenticationResultChoice resumeAuthentication(@Nonnull final HttpUserRe
return new AuthenticationResultChoice(
new AuthenticationResult() {

@Serial
private static final long serialVersionUID = 3481951951577173265L;

@Override
Expand Down Expand Up @@ -345,6 +346,11 @@ else if (StatusCode.NO_SUPPORTED_IDP.equals(status.getMinorStatusCode())
throw new UserAuthenticationException(AuthenticationErrorCode.UNKNOWN_AUTHENTICATION_SERVICE,
status.getStatusMessage("Requested IdP is not available"), e);
}
else if (SamlStatus.FRAUD_STATUS_CODE.equals(status.getMinorStatusCode())
|| SamlStatus.POSSIBLE_FRAUD_STATUS_CODE.equals(status.getMinorStatusCode())) {
throw new UserAuthenticationException(AuthenticationErrorCode.SECURITY_VIOLATION,
status.getStatusMessage("Possible fraud detected"), e);
}
else {
throw new UserAuthenticationException(AuthenticationErrorCode.FAILED_AUTHN,
String.format("Authentication failure: %s (%s)", status.getStatusMessage("Unknown authentication error"),
Expand Down Expand Up @@ -384,9 +390,9 @@ public boolean canProcess(@Nonnull final HttpUserRequest httpRequest, @Nullable
final String requestPath = httpRequest.getServerServletPath();
if (!(requestPath.equalsIgnoreCase(this.urlConfiguration.getAssertionConsumerPath())
|| (this.urlConfiguration.getAdditionalAssertionConsumerPath() != null
&& requestPath.equalsIgnoreCase(this.urlConfiguration.getAdditionalAssertionConsumerPath())))) {
&& requestPath.equalsIgnoreCase(this.urlConfiguration.getAdditionalAssertionConsumerPath())))) {
log.info("{}: Path {} is not supported by handler '{}'",
Optional.ofNullable(context).map(SignServiceContext::getId).orElseGet(() -> ""), requestPath, this.getName());
Optional.ofNullable(context).map(SignServiceContext::getId).orElse(""), requestPath, this.getName());
return false;
}

Expand Down Expand Up @@ -525,8 +531,8 @@ protected AuthnRequestGeneratorContext createAuthnRequestContext(
&& !authnRequirements.getAuthnContextIdentifiers().isEmpty()) {
final List<String> supportedUris = EntityDescriptorUtils.getAssuranceCertificationUris(idpMetadata);
final boolean match = authnRequirements.getAuthnContextIdentifiers().stream()
.map(AuthnContextIdentifier::getIdentifier)
.anyMatch(a -> supportedUris.contains(a));
.map(AuthnContextIdentifier::getIdentifier)
.anyMatch(supportedUris::contains);
if (!match) {
final String msg = "None of the requested authn context URIs are supported by the IdP";
log.info("{}: {}", context.getId(), msg);
Expand All @@ -539,7 +545,7 @@ protected AuthnRequestGeneratorContext createAuthnRequestContext(
@Override
@Nonnull
public String getPreferredBinding() {
return getPreferredBindingUri();
return AbstractSamlAuthenticationHandler.this.getPreferredBindingUri();
}

@Override
Expand All @@ -558,7 +564,7 @@ public RequestedAuthnContextBuilderFunction getRequestedAuthnContextBuilderFunct
//
final List<String> uris = authnRequirements.getAuthnContextIdentifiers().stream()
.map(AuthnContextIdentifier::getIdentifier)
.filter(i -> list.contains(i))
.filter(list::contains)
.collect(Collectors.toList());

if (uris.isEmpty()) {
Expand All @@ -585,7 +591,7 @@ public RequestedAuthnContextBuilderFunction getRequestedAuthnContextBuilderFunct
*/
@Nonnull
protected ResponseProcessingInput createResponseProcessingInput(
@Nonnull AuthnRequest authnRequest, @Nullable String sentRelayState,
@Nonnull final AuthnRequest authnRequest, @Nullable final String sentRelayState,
@Nonnull final HttpUserRequest httpRequest, @Nonnull final SignServiceContext context) {

final Instant received = Instant.now();
Expand Down Expand Up @@ -617,7 +623,7 @@ public Instant getReceiveInstant() {

@Override
public String getClientIpAddress() {
// We don't check IP addresses - it's too error prone.
// We don't check IP addresses - it's too error-prone.
return null;
}

Expand Down Expand Up @@ -731,12 +737,12 @@ protected void assertAttributes(@Nonnull final AuthnRequirements authnRequiremen
throw new UserAuthenticationException(AuthenticationErrorCode.MISMATCHING_IDENTITY_ATTRIBUTES, msg);
}
// OK, the attribute is provided. Let's check its value.
// Since we support multi-valued attributes, we have a match if at least one of the values
// Since we support multivalued attributes, we have a match if at least one of the values
// from the requested attribute is found in the issued attribute.
//
boolean match = false;
for (final Object value : requestedAttribute.getValues()) {
if (issuedAttribute.getValues().stream().filter(v -> Objects.equals(v, value)).findAny().isPresent()) {
if (issuedAttribute.getValues().stream().anyMatch(v -> Objects.equals(v, value))) {
match = true;
break;
}
Expand Down Expand Up @@ -771,7 +777,7 @@ protected void assertAuthnContext(@Nonnull final AuthnRequest authnRequest,
final List<String> requestedContexts = authnRequest.getRequestedAuthnContext().getAuthnContextClassRefs()
.stream()
.map(AuthnContextClassRef::getURI)
.collect(Collectors.toList());
.toList();

if (requestedContexts.isEmpty()) {
return;
Expand All @@ -798,7 +804,7 @@ protected void assertAuthnContext(@Nonnull final AuthnRequest authnRequest,
* @param signMessage the sign message that was requsted (may be null)
* @param attributes the received attributes
* @param result the processing result
* @param authnRequest the sent authentication request
* @param authnRequest the authentication request
* @param context the SignService context
* @throws UserAuthenticationException for errors asserting the sign message
*/
Expand All @@ -810,7 +816,7 @@ protected void assertSignMessage(@Nullable final SignMessage signMessage,
}

/**
* A method that enables sub-classes to extend the verification of the received assertion. The default implementation
* A method that enables subclasses to extend the verification of the received assertion. The default implementation
* does nothing.
*
* @param authnRequirements the authentication requirements
Expand All @@ -820,7 +826,7 @@ protected void assertSignMessage(@Nullable final SignMessage signMessage,
* @throws UserAuthenticationException for verification errors
*/
protected void extendedAssertionVerification(@Nonnull final AuthnRequirements authnRequirements,
@Nonnull final AuthnRequest authnRequest, @Nonnull ResponseProcessingResult result,
@Nonnull final AuthnRequest authnRequest, @Nonnull final ResponseProcessingResult result,
@Nonnull final SignServiceContext context) throws UserAuthenticationException {
}

Expand Down Expand Up @@ -868,10 +874,11 @@ protected IdentityAssertion buildIdentityAssertion(
* @param authnRequest the authentication request
* @param context the SignService context
* @return a flag indicating whether the sign message was displayed or not
* @throws UserAuthenticationException for processing errors, i.e., the proof for a displayed sign message is illegal
* @throws UserAuthenticationException for processing errors, i.e., the proof for a displayed sign message is
* illegal
*/
protected boolean wasSignMessageDisplayed(@Nonnull final ResponseProcessingResult result,
@Nonnull List<IdentityAttribute<?>> attributes, @Nonnull final AuthnRequest authnRequest,
@Nonnull final List<IdentityAttribute<?>> attributes, @Nonnull final AuthnRequest authnRequest,
@Nonnull final SignServiceContext context) throws UserAuthenticationException {
return false;
}
Expand Down Expand Up @@ -915,7 +922,7 @@ protected AuthnRequest getAuthnRequest(@Nonnull final SignServiceContext context
final Element xml = DOMUtils.bytesToDocument(encodedAuthnRequest).getDocumentElement();
final Unmarshaller unmarshaller = Optional.ofNullable(XMLObjectSupport.getUnmarshaller(xml))
.orElseThrow(() -> new UnmarshallingException("No unmarshaller for AuthnRequest available"));
return AuthnRequest.class.cast(unmarshaller.unmarshall(xml));
return (AuthnRequest) unmarshaller.unmarshall(xml);
}
catch (final Exception e) {
final String msg = "Failed to unmarshall AuthnRequest object";
Expand All @@ -931,7 +938,7 @@ protected AuthnRequest getAuthnRequest(@Nonnull final SignServiceContext context
*/
@Nonnull
protected String getPreferredBindingUri() {
return Optional.ofNullable(this.preferredBindingUri).orElseGet(() -> SAMLConstants.SAML2_REDIRECT_BINDING_URI);
return Optional.ofNullable(this.preferredBindingUri).orElse(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@
*/
package se.swedenconnect.signservice.authn.saml;

import java.util.Objects;
import java.util.Optional;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.opensaml.saml.saml2.core.Status;
import org.opensaml.saml.saml2.core.StatusCode;
import org.opensaml.saml.saml2.core.StatusMessage;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import java.util.Objects;
import java.util.Optional;

/**
* A utility class for working with SAML {@link Status} objects.
Expand All @@ -33,8 +32,13 @@ public class SamlStatus {
/** The status code for cancel (defined by the Swedish eID framework). */
public static final String CANCEL_STATUS_CODE = "http://id.elegnamnden.se/status/1.0/cancel";

/** Status code for fraud detection. */
public static final String FRAUD_STATUS_CODE = "http://id.elegnamnden.se/status/1.0/fraud";

/** Status code for possible fraud detection. */
public static final String POSSIBLE_FRAUD_STATUS_CODE = "http://id.elegnamnden.se/status/1.0/possibleFraud";

/** The status object. */
@Nonnull
private final Status status;

/**
Expand Down Expand Up @@ -108,7 +112,7 @@ public boolean isCancel() {
/** {@inheritDoc} */
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("code='");
final StringBuilder sb = new StringBuilder("code='");
sb.append(this.getMinorStatusCode()).append("'");
final String minor = this.getMinorStatusCode();
if (minor != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public enum AuthenticationErrorCode {
/** The user failed to authenticate - general authentication error. */
FAILED_AUTHN,

/** Possible security violation. */
SECURITY_VIOLATION,

/** General error for bad authentication setup. For example, the IdP does not recognize the SP. */
INTERNAL_AUTHN_ERROR;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public enum SignServiceErrorCode {
/** General authentication error. */
AUTHN_FAILURE("The user failed to authenticate"),

/** Security violation. */
SECURITY_VIOLATION("A security violation was detected"),

/** Error generating the signing key. */
KEY_GENERATION_FAILED("The generation of the signature key failed"),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
*/
package se.swedenconnect.signservice.protocol.msg;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;

import java.io.Serializable;
import java.time.Instant;

Expand All @@ -28,13 +31,15 @@ public interface MessageConditions extends Serializable {
*
* @return not before instant
*/
@Nullable
Instant getNotBefore();

/**
* Tells that the message must not be regarded as valid after this instant.
*
* @return not after instant
*/
@Nullable
Instant getNotAfter();

/**
Expand All @@ -46,6 +51,6 @@ public interface MessageConditions extends Serializable {
* @param instant the instant to test
* @return true if the supplied instant meets the criteria and false otherwise
*/
boolean isWithinRange(final Instant instant);
boolean isWithinRange(@Nonnull final Instant instant);

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
*/
package se.swedenconnect.signservice.protocol.msg.impl;

import java.io.Serial;
import java.time.Instant;
import java.util.Objects;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import se.swedenconnect.signservice.core.annotations.GeneratedMethod;
import se.swedenconnect.signservice.protocol.msg.MessageConditions;

Expand All @@ -27,6 +30,7 @@
public class DefaultMessageConditions implements MessageConditions {

/** For serializing. */
@Serial
private static final long serialVersionUID = -1228444313739138273L;

/** Not before condition. */
Expand All @@ -38,10 +42,10 @@ public class DefaultMessageConditions implements MessageConditions {
/**
* Constructor.
*
* @param notBefore the not before condition
* @param notAfter the not after condition
* @param notBefore the not-before condition
* @param notAfter the not-after condition
*/
public DefaultMessageConditions(final Instant notBefore, final Instant notAfter) {
public DefaultMessageConditions(@Nullable final Instant notBefore, @Nullable final Instant notAfter) {
this.notBefore = notBefore;
this.notAfter = notAfter;

Expand All @@ -52,19 +56,21 @@ public DefaultMessageConditions(final Instant notBefore, final Instant notAfter)

/** {@inheritDoc} */
@Override
@Nullable
public Instant getNotBefore() {
return this.notBefore;
}

/** {@inheritDoc} */
@Override
@Nullable
public Instant getNotAfter() {
return this.notAfter;
}

/** {@inheritDoc} */
@Override
public boolean isWithinRange(final Instant instant) {
public boolean isWithinRange(@Nonnull final Instant instant) {
if (instant == null) {
return false;
}
Expand All @@ -91,10 +97,9 @@ public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof DefaultMessageConditions)) {
if (!(obj instanceof final DefaultMessageConditions other)) {
return false;
}
final DefaultMessageConditions other = (DefaultMessageConditions) obj;
return Objects.equals(this.notAfter, other.notAfter) && Objects.equals(this.notBefore, other.notBefore);
}

Expand Down
Loading
Loading