package org.pac4j.saml.sso.impl; import com.google.common.annotations.VisibleForTesting; import net.shibboleth.utilities.java.support.net.BasicURLComparator; import net.shibboleth.utilities.java.support.net.URIComparator; import net.shibboleth.utilities.java.support.resolver.CriteriaSet; import org.joda.time.DateTime; import org.opensaml.core.criterion.EntityIdCriterion; import org.opensaml.core.xml.XMLObject; import org.opensaml.saml.common.SAMLObject; import org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext; import org.opensaml.saml.common.xml.SAMLConstants; import org.opensaml.saml.criterion.EntityRoleCriterion; import org.opensaml.saml.criterion.ProtocolCriterion; import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.saml.saml2.core.Attribute; import org.opensaml.saml.saml2.core.AttributeStatement; import org.opensaml.saml.saml2.core.Audience; import org.opensaml.saml.saml2.core.AudienceRestriction; import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.core.AuthnStatement; import org.opensaml.saml.saml2.core.BaseID; import org.opensaml.saml.saml2.core.Conditions; import org.opensaml.saml.saml2.core.EncryptedAssertion; import org.opensaml.saml.saml2.core.EncryptedAttribute; import org.opensaml.saml.saml2.core.EncryptedID; import org.opensaml.saml.saml2.core.Issuer; import org.opensaml.saml.saml2.core.NameID; import org.opensaml.saml.saml2.core.NameIDType; import org.opensaml.saml.saml2.core.Response; import org.opensaml.saml.saml2.core.StatusCode; import org.opensaml.saml.saml2.core.Subject; import org.opensaml.saml.saml2.core.SubjectConfirmation; import org.opensaml.saml.saml2.core.SubjectConfirmationData; import org.opensaml.saml.saml2.encryption.Decrypter; import org.opensaml.saml.saml2.metadata.AssertionConsumerService; import org.opensaml.saml.saml2.metadata.Endpoint; import org.opensaml.saml.saml2.metadata.IDPSSODescriptor; import org.opensaml.saml.saml2.metadata.SPSSODescriptor; import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; import org.opensaml.security.SecurityException; import org.opensaml.security.credential.UsageType; import org.opensaml.security.criteria.UsageCriterion; import org.opensaml.xmlsec.encryption.support.DecryptionException; import org.opensaml.xmlsec.signature.Signature; import org.opensaml.xmlsec.signature.support.SignatureException; import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; import org.pac4j.core.credentials.Credentials; import org.pac4j.saml.client.SAML2Client; import org.pac4j.saml.context.SAML2MessageContext; import org.pac4j.saml.credentials.SAML2Credentials; import org.pac4j.saml.crypto.SAML2SignatureTrustEngineProvider; import org.pac4j.saml.exceptions.SAMLException; import org.pac4j.saml.sso.SAML2ResponseValidator; import org.pac4j.saml.storage.SAMLMessageStorage; import org.pac4j.saml.util.UriUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Class responsible for executing every required checks for validating a SAML response. * The method validate populates the given {@link SAML2MessageContext} * with the correct SAML assertion and the corresponding nameID's Bearer subject if every checks succeeds. * * @author Michael Remond * @since 1.5.0 * */ public class SAML2DefaultResponseValidator implements SAML2ResponseValidator { private final static Logger logger = LoggerFactory.getLogger(SAML2DefaultResponseValidator.class); /** The default maximum authentication lifetime, in seconds. Used for {@link #maximumAuthenticationLifetime} if a meaningless (<=0) value is passed to the constructor. */ private static final int DEFAULT_MAXIMUM_AUTHENTICATION_LIFETIME = 3600; /* maximum skew in seconds between SP and IDP clocks */ private int acceptedSkew = 120; /* maximum lifetime after a successful authentication on an IDP */ private int maximumAuthenticationLifetime; private final boolean wantsAssertionsSigned; private final SAML2SignatureTrustEngineProvider signatureTrustEngineProvider; private final Decrypter decrypter; private final URIComparator uriComparator; public SAML2DefaultResponseValidator(final SAML2SignatureTrustEngineProvider engine, final Decrypter decrypter, final int maximumAuthenticationLifetime, final boolean wantsAssertionsSigned) { this(engine, decrypter, maximumAuthenticationLifetime, wantsAssertionsSigned, new BasicURLComparator()); } public SAML2DefaultResponseValidator(final SAML2SignatureTrustEngineProvider engine, final Decrypter decrypter, final int maximumAuthenticationLifetime, final boolean wantsAssertionsSigned, final URIComparator uriComparator) { this.signatureTrustEngineProvider = engine; this.decrypter = decrypter; this.maximumAuthenticationLifetime = (maximumAuthenticationLifetime > 0 ? maximumAuthenticationLifetime : DEFAULT_MAXIMUM_AUTHENTICATION_LIFETIME); this.uriComparator = uriComparator; this.wantsAssertionsSigned = wantsAssertionsSigned; } /** * Validates the SAML protocol response and the SAML SSO response. * The method decrypt encrypted assertions if any. * * @param context the context */ @Override public Credentials validate(final SAML2MessageContext context) { final SAMLObject message = context.getMessage(); if (!(message instanceof Response)) { throw new SAMLException("Response instance is an unsupported type"); } final Response response = (Response) message; final SignatureTrustEngine engine = this.signatureTrustEngineProvider.build(); validateSamlProtocolResponse(response, context, engine); if (decrypter != null) { decryptEncryptedAssertions(response, decrypter); } validateSamlSSOResponse(response, context, engine, decrypter); return buildSAML2Credentials(context); } protected final SAML2Credentials buildSAML2Credentials(final SAML2MessageContext context) { final NameID nameId = context.getSAMLSubjectNameIdentifierContext().getSAML2SubjectNameID(); final Assertion subjectAssertion = context.getSubjectAssertion(); final String sessionIndex = getSessionIndex(subjectAssertion); final List<Attribute> attributes = new ArrayList<Attribute>(); for (final AttributeStatement attributeStatement : subjectAssertion.getAttributeStatements()) { for (final Attribute attribute : attributeStatement.getAttributes()) { attributes.add(attribute); } if (!attributeStatement.getEncryptedAttributes().isEmpty()) { if (decrypter == null) { logger.warn("Encrypted attributes returned, but no keystore was provided."); } else { for (final EncryptedAttribute encryptedAttribute : attributeStatement.getEncryptedAttributes()) { try { attributes.add(decrypter.decrypt(encryptedAttribute)); } catch (final DecryptionException e) { logger.warn("Decryption of attribute failed, continue with the next one", e); } } } } } return new SAML2Credentials(nameId, attributes, subjectAssertion.getConditions(), SAML2Client.class.getSimpleName(), sessionIndex); } /** * Searches the sessionIndex in the assertion * * @param subjectAssertion assertion from the response * @return the sessionIndex if found in the assertion */ private final String getSessionIndex(Assertion subjectAssertion) { List<AuthnStatement> authnStatements = subjectAssertion.getAuthnStatements(); if(authnStatements != null && authnStatements.size() > 0) { AuthnStatement statement = authnStatements.get(0); if(statement != null) { return statement.getSessionIndex(); } } return null; } /** * Validates the SAML protocol response: * - IssueInstant * - Issuer * - StatusCode * - Signature * * @param response the response * @param context the context * @param engine the engine */ protected final void validateSamlProtocolResponse(final Response response, final SAML2MessageContext context, final SignatureTrustEngine engine) { if (!StatusCode.SUCCESS.equals(response.getStatus().getStatusCode().getValue())) { String status = response.getStatus().getStatusCode().getValue(); if (response.getStatus().getStatusMessage() != null) { status += " / " + response.getStatus().getStatusMessage().getMessage(); } throw new SAMLException("Authentication response is not success ; actual " + status); } if (response.getSignature() != null) { final String entityId = context.getSAMLPeerEntityContext().getEntityId(); validateSignature(response.getSignature(), entityId, engine); context.getSAMLPeerEntityContext().setAuthenticated(true); } if (!isIssueInstantValid(response.getIssueInstant())) { throw new SAMLException("Response issue instant is too old or in the future"); } AuthnRequest request = null; final SAMLMessageStorage messageStorage = context.getSAMLMessageStorage(); if (messageStorage != null && response.getInResponseTo() != null) { final XMLObject xmlObject = messageStorage.retrieveMessage(response.getInResponseTo()); if (xmlObject == null) { throw new SAMLException("InResponseToField of the Response doesn't correspond to sent message " + response.getInResponseTo()); } else if (xmlObject instanceof AuthnRequest) { request = (AuthnRequest) xmlObject; } else { throw new SAMLException("Sent request was of different type than the expected AuthnRequest " + response.getInResponseTo()); } } verifyEndpoint(context.getSAMLEndpointContext().getEndpoint(), response.getDestination()); if (request != null) { verifyRequest(request, context); } if (response.getIssuer() != null) { validateIssuer(response.getIssuer(), context); } } protected void verifyRequest(final AuthnRequest request, final SAML2MessageContext context) { // Verify endpoint requested in the original request final AssertionConsumerService assertionConsumerService = (AssertionConsumerService) context.getSAMLEndpointContext().getEndpoint(); if (request.getAssertionConsumerServiceIndex() != null) { if (!request.getAssertionConsumerServiceIndex().equals(assertionConsumerService.getIndex())) { logger.warn("Response was received at a different endpoint index than was requested"); } } else { final String requestedResponseURL = request.getAssertionConsumerServiceURL(); final String requestedBinding = request.getProtocolBinding(); if (requestedResponseURL != null) { final String responseLocation; if (assertionConsumerService.getResponseLocation() != null) { responseLocation = assertionConsumerService.getResponseLocation(); } else { responseLocation = assertionConsumerService.getLocation(); } if (!requestedResponseURL.equals(responseLocation)) { logger.warn("Response was received at a different endpoint URL {} than was requested {}", responseLocation, requestedResponseURL); } } if (requestedBinding != null && !requestedBinding.equals(context.getSAMLBindingContext().getBindingUri())) { logger.warn("Response was received using a different binding {} than was requested {}", context.getSAMLBindingContext().getBindingUri(), requestedBinding); } } } protected final void verifyEndpoint(final Endpoint endpoint, final String destination) { try { if (destination != null && !uriComparator.compare(destination, endpoint.getLocation()) && !uriComparator.compare(destination, endpoint.getResponseLocation())) { throw new SAMLException("Intended destination " + destination + " doesn't match any of the endpoint URLs on endpoint " + endpoint.getLocation()); } } catch (final Exception e) { throw new SAMLException(e); } } /** * Validates the SAML SSO response by finding a valid assertion with authn statements. * Populates the {@link SAML2MessageContext} with a subjectAssertion and a subjectNameIdentifier. * * @param response the response * @param context the context * @param engine the engine * @param decrypter the decrypter */ protected final void validateSamlSSOResponse(final Response response, final SAML2MessageContext context, final SignatureTrustEngine engine, final Decrypter decrypter) { for (final Assertion assertion : response.getAssertions()) { if (!assertion.getAuthnStatements().isEmpty()) { try { validateAssertion(assertion, context, engine, decrypter); } catch (final SAMLException e) { logger.error("Current assertion validation failed, continue with the next one", e); continue; } context.setSubjectAssertion(assertion); break; } } if (context.getSubjectAssertion() == null) { throw new SAMLException("No valid subject assertion found in response"); } // We do not check EncryptedID here because it has been already decrypted and stored into NameID final List<SubjectConfirmation> subjectConfirmations = context.getSubjectConfirmations(); final NameID nameIdentifier = (NameID) context.getSAMLSubjectNameIdentifierContext().getSubjectNameIdentifier(); if ((nameIdentifier == null || nameIdentifier.getValue() == null) && context.getBaseID() == null && (subjectConfirmations == null || subjectConfirmations.isEmpty())) { throw new SAMLException( "Subject NameID, BaseID and EncryptedID cannot be all null at the same time if there are no Subject Confirmations."); } } /** * Decrypt encrypted assertions and add them to the assertions list of the response. * * @param response the response * @param decrypter the decrypter */ protected final void decryptEncryptedAssertions(final Response response, final Decrypter decrypter) { for (final EncryptedAssertion encryptedAssertion : response.getEncryptedAssertions()) { try { final Assertion decryptedAssertion = decrypter.decrypt(encryptedAssertion); response.getAssertions().add(decryptedAssertion); } catch (final DecryptionException e) { logger.error("Decryption of assertion failed, continue with the next one", e); } } } /** * Validate issuer format and value. * * @param issuer the issuer * @param context the context */ protected final void validateIssuer(final Issuer issuer, final SAML2MessageContext context) { if (issuer.getFormat() != null && !issuer.getFormat().equals(NameIDType.ENTITY)) { throw new SAMLException("Issuer type is not entity but " + issuer.getFormat()); } final String entityId = context.getSAMLPeerEntityContext().getEntityId(); if (entityId == null || !entityId.equals(issuer.getValue())) { throw new SAMLException("Issuer " + issuer.getValue() + " does not match idp entityId " + entityId); } } /** * Validate the given assertion: * - issueInstant * - issuer * - subject * - conditions * - authnStatements * - signature * * @param assertion the assertion * @param context the context * @param engine the engine * @param decrypter the decrypter */ protected final void validateAssertion(final Assertion assertion, final SAML2MessageContext context, final SignatureTrustEngine engine, final Decrypter decrypter) { if (!isIssueInstantValid(assertion.getIssueInstant())) { throw new SAMLException("Assertion issue instant is too old or in the future"); } validateIssuer(assertion.getIssuer(), context); if (assertion.getSubject() != null) { validateSubject(assertion.getSubject(), context, decrypter); } else { throw new SAMLException("Assertion subject cannot be null"); } validateAssertionConditions(assertion.getConditions(), context); validateAuthenticationStatements(assertion.getAuthnStatements(), context); validateAssertionSignature(assertion.getSignature(), context, engine); } /** * Validate the given subject by finding a valid Bearer confirmation. If the subject is valid, put its nameID in the context. * * NameID / BaseID / EncryptedID is first looked up directly in the Subject. If not present there, then all relevant * SubjectConfirmations are parsed and the IDs are taken from them. * * @param subject * The Subject from an assertion. * @param context * SAML message context. * @param decrypter * Decrypter used to decrypt some encrypted IDs, if they are present. May be {@code null}, no decryption will be possible * then. */ @SuppressWarnings("unchecked") protected final void validateSubject(final Subject subject, final SAML2MessageContext context, final Decrypter decrypter) { boolean samlIDFound = false; // Read NameID/BaseID/EncryptedID from the subject. If not present directly in the subject, try to find it in subject confirmations. NameID nameIdFromSubject = subject.getNameID(); final BaseID baseIdFromSubject = subject.getBaseID(); final EncryptedID encryptedIdFromSubject = subject.getEncryptedID(); // Encrypted ID can overwrite the non-encrypted one, if present final NameID decryptedNameIdFromSubject = decryptEncryptedId(encryptedIdFromSubject, decrypter); if (decryptedNameIdFromSubject != null) { nameIdFromSubject = decryptedNameIdFromSubject; } // If we have a Name ID or a Base ID, we are fine // If we don't have anything, let's go through all subject confirmations and get the IDs from them. // At least one should be present but we don't care at this point. if (nameIdFromSubject != null || baseIdFromSubject != null) { context.getSAMLSubjectNameIdentifierContext().setSubjectNameIdentifier(nameIdFromSubject); context.setBaseID(baseIdFromSubject); samlIDFound = true; } for (final SubjectConfirmation confirmation : subject.getSubjectConfirmations()) { if (SubjectConfirmation.METHOD_BEARER.equals(confirmation.getMethod()) && isValidBearerSubjectConfirmationData(confirmation.getSubjectConfirmationData(), context)) { NameID nameIDFromConfirmation = confirmation.getNameID(); final BaseID baseIDFromConfirmation = confirmation.getBaseID(); final EncryptedID encryptedIDFromConfirmation = confirmation.getEncryptedID(); // Encrypted ID can overwrite the non-encrypted one, if present final NameID decryptedNameIdFromConfirmation = decryptEncryptedId(encryptedIDFromConfirmation, decrypter); if (decryptedNameIdFromConfirmation != null) { nameIDFromConfirmation = decryptedNameIdFromConfirmation; } if (!samlIDFound && (nameIDFromConfirmation != null || baseIDFromConfirmation != null)) { context.getSAMLSubjectNameIdentifierContext().setSubjectNameIdentifier(nameIDFromConfirmation); context.setBaseID(baseIDFromConfirmation); context.getSubjectConfirmations().add(confirmation); samlIDFound = true; } if (!samlIDFound) { logger.warn("Could not find any Subject NameID/BaseID/EncryptedID, neither directly in the Subject nor in any Subject Confirmation."); } return; } } throw new SAMLException("Subject confirmation validation failed"); } /** * Decrypts an EncryptedID, using a decrypter. * * @param encryptedId The EncryptedID to be decrypted. * @param decrypter The decrypter to use. * * @return Decrypted ID or {@code null} if any input is {@code null}. * * @throws SAMLException If the input ID cannot be decrypted. */ protected final NameID decryptEncryptedId(final EncryptedID encryptedId, final Decrypter decrypter) throws SAMLException { if (encryptedId == null) { return null; } if (decrypter == null) { logger.warn("Encrypted attributes returned, but no keystore was provided."); return null; } try { final NameID decryptedId = (NameID) decrypter.decrypt(encryptedId); return decryptedId; } catch (final DecryptionException e) { throw new SAMLException("Decryption of an EncryptedID failed.", e); } } /** * Validate Bearer subject confirmation data * - notBefore * - NotOnOrAfter * - recipient * * @param data the data * @param context the context * @return true if all Bearer subject checks are passing */ protected final boolean isValidBearerSubjectConfirmationData(final SubjectConfirmationData data, final SAML2MessageContext context) { if (data == null) { logger.debug("SubjectConfirmationData cannot be null for Bearer confirmation"); return false; } // TODO Validate inResponseTo if (data.getNotBefore() != null) { logger.debug("SubjectConfirmationData notBefore must be null for Bearer confirmation"); return false; } if (data.getNotOnOrAfter() == null) { logger.debug("SubjectConfirmationData notOnOrAfter cannot be null for Bearer confirmation"); return false; } if (data.getNotOnOrAfter().plusSeconds(acceptedSkew).isBeforeNow()) { logger.debug("SubjectConfirmationData notOnOrAfter is too old"); return false; } try { if (data.getRecipient() == null) { logger.debug("SubjectConfirmationData recipient cannot be null for Bearer confirmation"); return false; } else { final Endpoint endpoint = context.getSAMLEndpointContext().getEndpoint(); if (endpoint == null) { logger.warn("No endpoint was found in the SAML endpoint context"); return false; } final URI recipientUri = new URI(data.getRecipient()); final URI appEndpointUri = new URI(endpoint.getLocation()); if (!UriUtils.urisEqualAfterPortNormalization(recipientUri, appEndpointUri)) { logger.debug("SubjectConfirmationData recipient {} does not match SP assertion consumer URL, found. SP ACS URL from context: {}", recipientUri, appEndpointUri); return false; } } } catch (URISyntaxException use) { logger.error("Unable to check SubjectConfirmationData recipient, a URI has invalid syntax.", use); return false; } return true; } /** * Validate assertionConditions * - notBefore * - notOnOrAfter * * @param conditions the conditions * @param context the context */ protected final void validateAssertionConditions(final Conditions conditions, final SAML2MessageContext context) { if (conditions == null) { throw new SAMLException("Assertion conditions cannot be null"); } if (conditions.getNotBefore() != null && conditions.getNotBefore().minusSeconds(acceptedSkew).isAfterNow()) { throw new SAMLException("Assertion condition notBefore is not valid"); } if (conditions.getNotOnOrAfter() != null && conditions.getNotOnOrAfter().plusSeconds(acceptedSkew).isBeforeNow()) { throw new SAMLException("Assertion condition notOnOrAfter is not valid"); } final String entityId = context.getSAMLSelfEntityContext().getEntityId(); validateAudienceRestrictions(conditions.getAudienceRestrictions(), entityId); } /** * Validate audience by matching the SP entityId. * * @param audienceRestrictions the audience restrictions * @param spEntityId the sp entity id */ protected final void validateAudienceRestrictions(final List<AudienceRestriction> audienceRestrictions, final String spEntityId) { if (audienceRestrictions == null || audienceRestrictions.isEmpty()) { throw new SAMLException("Audience restrictions cannot be null or empty"); } final Set<String> audienceUris = new HashSet<String>(); for (final AudienceRestriction audienceRestriction : audienceRestrictions) { if (audienceRestriction.getAudiences() != null) { for (final Audience audience : audienceRestriction.getAudiences()) { audienceUris.add(audience.getAudienceURI()); } } } if (!audienceUris.contains(spEntityId)) { throw new SAMLException("Assertion audience " + audienceUris + " does not match SP configuration " + spEntityId); } } /** * Validate the given authnStatements: * - authnInstant * - sessionNotOnOrAfter * * @param authnStatements the authn statements * @param context the context */ protected final void validateAuthenticationStatements(final List<AuthnStatement> authnStatements, final SAML2MessageContext context) { for (final AuthnStatement statement : authnStatements) { if (!isAuthnInstantValid(statement.getAuthnInstant())) { throw new SAMLException("Authentication issue instant is too old or in the future"); } if (statement.getSessionNotOnOrAfter() != null && statement.getSessionNotOnOrAfter().isBeforeNow()) { throw new SAMLException("Authentication session between IDP and subject has ended"); } // TODO implement authnContext validation } } /** * Validate assertion signature. If none is found and the SAML response did not have one and the SP requires * the assertions to be signed, the validation fails. * * @param signature the signature * @param context the context * @param engine the engine */ protected final void validateAssertionSignature(final Signature signature, final SAML2MessageContext context, final SignatureTrustEngine engine) { final SAMLPeerEntityContext peerContext = context.getSAMLPeerEntityContext(); if (signature != null) { final String entityId = peerContext.getEntityId(); validateSignature(signature, entityId, engine); } else { if (wantsAssertionsSigned(context) && !peerContext.isAuthenticated()) { throw new SAMLException("Assertion or response must be signed"); } } } @VisibleForTesting Boolean wantsAssertionsSigned(SAML2MessageContext context) { if (context == null) return wantsAssertionsSigned; SPSSODescriptor spDescriptor = context.getSPSSODescriptor(); if (spDescriptor == null) return wantsAssertionsSigned; return spDescriptor.getWantAssertionsSigned(); } /** * Validate the given digital signature by checking its profile and value. * * @param signature the signature * @param idpEntityId the idp entity id * @param trustEngine the trust engine */ protected final void validateSignature(final Signature signature, final String idpEntityId, final SignatureTrustEngine trustEngine) { final SAMLSignatureProfileValidator validator = new SAMLSignatureProfileValidator(); try { validator.validate(signature); } catch (final SignatureException e) { throw new SAMLException("SAMLSignatureProfileValidator failed to validate signature", e); } final CriteriaSet criteriaSet = new CriteriaSet(); criteriaSet.add(new UsageCriterion(UsageType.SIGNING)); criteriaSet.add(new EntityRoleCriterion(IDPSSODescriptor.DEFAULT_ELEMENT_NAME)); criteriaSet.add(new ProtocolCriterion(SAMLConstants.SAML20P_NS)); criteriaSet.add(new EntityIdCriterion(idpEntityId)); final boolean valid; try { valid = trustEngine.validate(signature, criteriaSet); } catch (final SecurityException e) { throw new SAMLException("An error occurred during signature validation", e); } if (!valid) { throw new SAMLException("Signature is not trusted"); } } private boolean isDateValid(final DateTime issueInstant, final int interval) { final DateTime before = DateTime.now().plusSeconds(acceptedSkew); final DateTime after = DateTime.now().minusSeconds(acceptedSkew + interval); boolean isDateValid = issueInstant.isBefore(before) && issueInstant.isAfter(after); if (!isDateValid) { logger.trace("interval={},before={},after={},issueInstant={}", interval, before.toDateTime(issueInstant.getZone()), after.toDateTime(issueInstant.getZone()), issueInstant); } return isDateValid; } private boolean isIssueInstantValid(final DateTime issueInstant) { return isDateValid(issueInstant, 0); } private boolean isAuthnInstantValid(final DateTime authnInstant) { return isDateValid(authnInstant, this.maximumAuthenticationLifetime); } @Override public final void setAcceptedSkew(final int acceptedSkew) { this.acceptedSkew = acceptedSkew; } @Override public final void setMaximumAuthenticationLifetime(final int maximumAuthenticationLifetime) { this.maximumAuthenticationLifetime = maximumAuthenticationLifetime; } }