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

Added logout support #68

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions mujina-idp/src/main/java/mujina/api/IdpConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class IdpConfiguration extends SharedConfiguration {
private Map<String, List<String>> attributes = new TreeMap<>();
private List<FederatedUserAuthenticationToken> users = new ArrayList<>();
private String acsEndpoint;
private String slsEndpoint;
private AuthenticationMethod authenticationMethod;
private AuthenticationMethod defaultAuthenticationMethod;
private final String idpPrivateKey;
Expand Down
6 changes: 6 additions & 0 deletions mujina-idp/src/main/java/mujina/api/IdpController.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ public void setAcsEndpoint(@RequestBody String acsEndpoint) {
configuration().setAcsEndpoint(acsEndpoint);
}

@PutMapping("/slsendpoint")
public void setSlsEndpoint(@RequestBody String slsEndpoint) {
LOG.info("Request to set Single Logout Service Endpoint to {}", slsEndpoint);
configuration().setSlsEndpoint(slsEndpoint);
}

private IdpConfiguration configuration() {
return IdpConfiguration.class.cast(super.configuration);
}
Expand Down
7 changes: 7 additions & 0 deletions mujina-idp/src/main/java/mujina/idp/MetadataController.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.opensaml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml2.metadata.NameIDFormat;
import org.opensaml.saml2.metadata.SingleLogoutService;
import org.opensaml.saml2.metadata.SingleSignOnService;
import org.opensaml.xml.io.Marshaller;
import org.opensaml.xml.io.MarshallingException;
Expand Down Expand Up @@ -84,6 +85,12 @@ public String metadata(@Value("${idp.base_url}") String idpBaseUrl) throws Secur

idpssoDescriptor.getSingleSignOnServices().add(singleSignOnService);

SingleLogoutService singleLogoutService = buildSAMLObject(SingleLogoutService.class, SingleLogoutService.DEFAULT_ELEMENT_NAME);
singleLogoutService.setLocation(idpBaseUrl + "/SingleLogoutService");
singleLogoutService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);

idpssoDescriptor.getSingleLogoutServices().add(singleLogoutService);

X509KeyInfoGeneratorFactory keyInfoGeneratorFactory = new X509KeyInfoGeneratorFactory();
keyInfoGeneratorFactory.setEmitEntityCertificate(true);
KeyInfoGenerator keyInfoGenerator = keyInfoGeneratorFactory.newInstance();
Expand Down
58 changes: 41 additions & 17 deletions mujina-idp/src/main/java/mujina/idp/SAMLMessageHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
import org.opensaml.common.binding.encoding.SAMLMessageEncoder;
import org.opensaml.common.xml.SAMLConstants;
import org.opensaml.saml2.binding.encoding.HTTPRedirectDeflateEncoder;
import org.opensaml.saml2.core.Assertion;
import org.opensaml.saml2.core.AuthnRequest;
import org.opensaml.saml2.core.Issuer;
import org.opensaml.saml2.core.LogoutResponse;
import org.opensaml.saml2.core.Response;
import org.opensaml.saml2.core.Status;
import org.opensaml.saml2.core.StatusCode;
import org.opensaml.saml2.core.StatusResponseType;
import org.opensaml.saml2.metadata.Endpoint;
import org.opensaml.saml2.metadata.SingleSignOnService;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
Expand Down Expand Up @@ -54,6 +56,7 @@ public class SAMLMessageHandler {
private final KeyManager keyManager;
private final Collection<SAMLMessageDecoder> decoders;
private final SAMLMessageEncoder encoder;
private final SAMLMessageEncoder logoutEncoder;
private final SecurityPolicyResolver resolver;
private final IdpConfiguration idpConfiguration;

Expand All @@ -72,6 +75,7 @@ public SAMLMessageHandler(KeyManager keyManager, Collection<SAMLMessageDecoder>
getValidatorSuite("saml2-core-schema-validator"),
getValidatorSuite("saml2-core-spec-validator"));
this.proxiedSAMLContextProviderLB = new ProxiedSAMLContextProviderLB(new URI(idpBaseUrl));
logoutEncoder = new HTTPRedirectDeflateEncoder();
}

public SAMLMessageContext extractSAMLMessageContext(HttpServletRequest request, HttpServletResponse response, boolean postRequest) throws ValidationException, SecurityException, MessageDecodingException, MetadataProviderException {
Expand All @@ -86,10 +90,9 @@ public SAMLMessageContext extractSAMLMessageContext(HttpServletRequest request,

SAMLObject inboundSAMLMessage = messageContext.getInboundSAMLMessage();

AuthnRequest authnRequest = (AuthnRequest) inboundSAMLMessage;
//lambda is poor with Exceptions
for (ValidatorSuite validatorSuite : validatorSuites) {
validatorSuite.validate(authnRequest);
validatorSuite.validate(inboundSAMLMessage);
}
return messageContext;
}
Expand All @@ -105,27 +108,25 @@ private SAMLMessageDecoder samlMessageDecoder(boolean postRequest) {
}

@SuppressWarnings("unchecked")
public void sendAuthnResponse(SAMLPrincipal principal, HttpServletResponse response) throws MarshallingException, SignatureException, MessageEncodingException {
Status status = buildStatus(StatusCode.SUCCESS_URI);
private void sendResponseCommon(SAMLMessageEncoder encoder, StatusResponseType responseObject,
SAMLPrincipal principal, String statusCode,
HttpServletResponse response) throws MessageEncodingException {

Status status = buildStatus(statusCode);

String entityId = idpConfiguration.getEntityId();
Credential signingCredential = resolveCredential(entityId);

Response authResponse = buildSAMLObject(Response.class, Response.DEFAULT_ELEMENT_NAME);
Issuer issuer = buildIssuer(entityId);

authResponse.setIssuer(issuer);
authResponse.setID(SAMLBuilder.randomSAMLId());
authResponse.setIssueInstant(new DateTime());
authResponse.setInResponseTo(principal.getRequestID());
responseObject.setIssuer(issuer);
responseObject.setID(SAMLBuilder.randomSAMLId());
responseObject.setIssueInstant(new DateTime());
responseObject.setInResponseTo(principal.getRequestID());

Assertion assertion = buildAssertion(principal, status, entityId);
signAssertion(assertion, signingCredential);

authResponse.getAssertions().add(assertion);
authResponse.setDestination(principal.getAssertionConsumerServiceURL());
responseObject.setDestination(principal.getAssertionConsumerServiceURL());

authResponse.setStatus(status);
responseObject.setStatus(status);

Endpoint endpoint = buildSAMLObject(Endpoint.class, SingleSignOnService.DEFAULT_ELEMENT_NAME);
endpoint.setLocation(principal.getAssertionConsumerServiceURL());
Expand All @@ -136,7 +137,7 @@ public void sendAuthnResponse(SAMLPrincipal principal, HttpServletResponse respo

messageContext.setOutboundMessageTransport(outTransport);
messageContext.setPeerEntityEndpoint(endpoint);
messageContext.setOutboundSAMLMessage(authResponse);
messageContext.setOutboundSAMLMessage(responseObject);
messageContext.setOutboundSAMLMessageSigningCredential(signingCredential);

messageContext.setOutboundMessageIssuer(entityId);
Expand All @@ -146,6 +147,29 @@ public void sendAuthnResponse(SAMLPrincipal principal, HttpServletResponse respo

}

public void sendLogoutResponse(SAMLPrincipal principal, HttpServletResponse response, String statusCode) throws MessageEncodingException {

LogoutResponse logoutResponse = buildSAMLObject(LogoutResponse.class, LogoutResponse.DEFAULT_ELEMENT_NAME);
sendResponseCommon(logoutEncoder, logoutResponse, principal, statusCode, response);

}

public void sendAuthnResponse(SAMLPrincipal principal, HttpServletResponse response) throws MarshallingException, SignatureException, MessageEncodingException {

String entityId = idpConfiguration.getEntityId();
Credential signingCredential = resolveCredential(entityId);

Response authResponse = buildSAMLObject(Response.class, Response.DEFAULT_ELEMENT_NAME);

Assertion assertion = buildAssertion(principal, buildStatus(StatusCode.SUCCESS_URI), entityId);
signAssertion(assertion, signingCredential);

authResponse.getAssertions().add(assertion);

sendResponseCommon(encoder, authResponse, principal, StatusCode.SUCCESS_URI, response);

}

private Credential resolveCredential(String entityId) {
try {
return keyManager.resolveSingle(new CriteriaSet(new EntityIDCriteria(entityId)));
Expand Down
84 changes: 72 additions & 12 deletions mujina-idp/src/main/java/mujina/idp/SsoController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,36 @@
import mujina.saml.SAMLPrincipal;
import org.opensaml.common.binding.SAMLMessageContext;
import org.opensaml.saml2.core.AuthnRequest;
import org.opensaml.saml2.core.LogoutRequest;
import org.opensaml.saml2.core.NameIDType;
import org.opensaml.saml2.core.RequestAbstractType;
import org.opensaml.saml2.core.StatusCode;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.ws.message.decoder.MessageDecodingException;
import org.opensaml.ws.message.encoder.MessageEncodingException;
import org.opensaml.xml.io.MarshallingException;
import org.opensaml.xml.security.SecurityException;
import org.opensaml.xml.signature.SignatureException;
import org.opensaml.xml.validation.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import javax.servlet.http.HttpSession;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import static java.util.Collections.singletonList;
Expand All @@ -37,6 +43,8 @@
@Controller
public class SsoController {

protected final Logger LOG = LoggerFactory.getLogger(getClass());

@Autowired
private SAMLMessageHandler samlMessageHandler;

Expand All @@ -45,36 +53,88 @@

@GetMapping("/SingleSignOnService")
public void singleSignOnServiceGet(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, MarshallingException, SignatureException, MessageEncodingException, ValidationException, SecurityException, MessageDecodingException, MetadataProviderException, ServletException {
throws MarshallingException, SignatureException, MessageEncodingException, ValidationException, SecurityException, MessageDecodingException, MetadataProviderException {
doSSO(request, response, authentication, false);
}

@PostMapping("/SingleSignOnService")
public void singleSignOnServicePost(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, MarshallingException, SignatureException, MessageEncodingException, ValidationException, SecurityException, MessageDecodingException, MetadataProviderException, ServletException {
throws MarshallingException, SignatureException, MessageEncodingException, ValidationException, SecurityException, MessageDecodingException, MetadataProviderException {
doSSO(request, response, authentication, true);
}

@SuppressWarnings("unchecked")
private void doSSO(HttpServletRequest request, HttpServletResponse response, Authentication authentication, boolean postRequest) throws ValidationException, SecurityException, MessageDecodingException, MarshallingException, SignatureException, MessageEncodingException, MetadataProviderException, IOException, ServletException {
@GetMapping("/SingleLogoutService")
public void singleLogoutServiceGet(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws MessageEncodingException, ValidationException, SecurityException, MessageDecodingException, MetadataProviderException {
doSLO(request, response, authentication, false);
}

@PostMapping("/SingleLogoutService")
public void singleLogoutServicePost(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws MessageEncodingException, ValidationException, SecurityException, MessageDecodingException, MetadataProviderException {
doSLO(request, response, authentication, true);
}

private void doSSO(HttpServletRequest request, HttpServletResponse response, Authentication authentication, boolean postRequest) throws ValidationException, SecurityException, MessageDecodingException, MarshallingException, SignatureException, MessageEncodingException, MetadataProviderException {
SAMLMessageContext messageContext = samlMessageHandler.extractSAMLMessageContext(request, response, postRequest);
AuthnRequest authnRequest = (AuthnRequest) messageContext.getInboundSAMLMessage();

String assertionConsumerServiceURL = idpConfiguration.getAcsEndpoint() != null ? idpConfiguration.getAcsEndpoint() : authnRequest.getAssertionConsumerServiceURL();

samlMessageHandler.sendAuthnResponse(makeSAMLPrincipal(authentication, authnRequest, assertionConsumerServiceURL, messageContext.getRelayState()), response);

}

private SAMLPrincipal makeSAMLPrincipal(Authentication authentication, RequestAbstractType request,
String assertionConsumerServiceURL, String relayState) {

List<SAMLAttribute> attributes = attributes(authentication);

SAMLPrincipal principal = new SAMLPrincipal(
return new SAMLPrincipal(
authentication.getName(),
attributes.stream()
.filter(attr -> "urn:oasis:names:tc:SAML:1.1:nameid-format".equals(attr.getName()))
.findFirst().map(attr -> attr.getValue()).orElse(NameIDType.UNSPECIFIED),
.findFirst().map(SAMLAttribute::getValue).orElse(NameIDType.UNSPECIFIED),
attributes,
authnRequest.getIssuer().getValue(),
authnRequest.getID(),
request.getIssuer().getValue(),
request.getID(),
assertionConsumerServiceURL,
messageContext.getRelayState());
relayState);

}

private void doSLO(HttpServletRequest request, HttpServletResponse response, Authentication authentication, boolean postRequest)
throws ValidationException, SecurityException, MessageDecodingException, MessageEncodingException, MetadataProviderException {

SAMLMessageContext messageContext = samlMessageHandler.extractSAMLMessageContext(request, response, postRequest);
LogoutRequest logoutRequest = (LogoutRequest) messageContext.getInboundSAMLMessage();

// There is no SLS endpoint specified in the logout request, so the only
// thing we can use is the SLS from the IDP configuration.
String destination = idpConfiguration.getSlsEndpoint();

if (!Objects.equals(authentication.getPrincipal(), logoutRequest.getNameID().getValue())) {

LOG.warn("User "+authentication.getPrincipal()+" sent logout request for "+logoutRequest.getNameID());

Check warning on line 118 in mujina-idp/src/main/java/mujina/idp/SsoController.java

View check run for this annotation

Codecov / codecov/patch

mujina-idp/src/main/java/mujina/idp/SsoController.java#L118

Added line #L118 was not covered by tests

samlMessageHandler.sendLogoutResponse(makeSAMLPrincipal(authentication, logoutRequest,
destination, messageContext.getRelayState()),

Check warning on line 121 in mujina-idp/src/main/java/mujina/idp/SsoController.java

View check run for this annotation

Codecov / codecov/patch

mujina-idp/src/main/java/mujina/idp/SsoController.java#L120-L121

Added lines #L120 - L121 were not covered by tests
response, StatusCode.NO_AUTHN_CONTEXT_URI);
return;

Check warning on line 123 in mujina-idp/src/main/java/mujina/idp/SsoController.java

View check run for this annotation

Codecov / codecov/patch

mujina-idp/src/main/java/mujina/idp/SsoController.java#L123

Added line #L123 was not covered by tests
}

LOG.warn("Logging out " + authentication.getPrincipal());

HttpSession session = request.getSession(false);
SecurityContextHolder.clearContext();
if (session != null) {
session.invalidate();
}

samlMessageHandler.sendLogoutResponse(makeSAMLPrincipal(authentication, logoutRequest,
destination, messageContext.getRelayState()),
response, StatusCode.SUCCESS_URI);

samlMessageHandler.sendAuthnResponse(principal, response);
}

@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ public void metadata() throws Exception {
given()
.config(newConfig()
.xmlConfig(xmlConfig().declareNamespace("md", "urn:oasis:names:tc:SAML:2.0:metadata")))
.header("Content-Type", "application/xml")
.get("/metadata")
.then()
.contentType("application/xml")
.statusCode(SC_OK)
.body(
"EntityDescriptor.IDPSSODescriptor.SingleSignOnService.@Location",
equalTo(idpBaseUrl + "/SingleSignOnService"));
equalTo(idpBaseUrl + "/SingleSignOnService"))
.body(
"EntityDescriptor.IDPSSODescriptor.SingleLogoutService.@Location",
equalTo(idpBaseUrl + "/SingleLogoutService"));
}

}
Expand Down
Loading