Skip to content

Commit

Permalink
AUTH-33
Browse files Browse the repository at this point in the history
  • Loading branch information
madness-inc committed Jan 27, 2023
1 parent 19dbd8c commit b784f1c
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 43 deletions.
5 changes: 5 additions & 0 deletions application-home/application.xml
Expand Up @@ -36,6 +36,11 @@
<property id="mailFrom" description="the sender address for emails send during passwort retrieval">support@example.com</property>
<property id="digestMaxValidity" description="the maximum validity of a login digest in minutes">3</property>
<property id="loginForwardStatus" description="The HTTP Status to use when forwarding the user after login">302</property>
<property id="samlEnabled" description="Enable SAML 2 authentication" type="boolean">false</property>
<property id="samlClientId" description="The client ID/application name for SAML"></property>
<property id="samlForwardTarget" description="Target to forward to after successful SAML authentication">/manager</property>
<property id="samlDescriptor" description="The metadata describing the SAML Identity Provider" clob="true" type="multiline"></property>
<property id="samlCreateNewUserWithGroups" description="if not empty, a new user with this groups is created in case the user does not exist" type="text"></property>
</properties>

</application>
Expand Up @@ -4,6 +4,7 @@
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -13,12 +14,19 @@
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.appng.api.BusinessException;
import org.appng.api.Environment;
import org.appng.api.model.Application;
import org.appng.api.model.AuthSubject.PasswordChangePolicy;
import org.appng.api.model.Site;
import org.appng.api.model.Subject;
import org.appng.api.model.UserType;
import org.appng.api.support.ElementHelper;
import org.appng.application.authentication.AuthenticationSettings;
import org.appng.application.authentication.MessageConstants;
import org.appng.core.domain.SubjectImpl;
import org.appng.core.service.CoreService;
import org.appng.tools.ui.StringNormalizer;
import org.appng.xml.platform.Message;
import org.appng.xml.platform.MessageType;
import org.appng.xml.platform.Messages;
Expand All @@ -28,6 +36,7 @@
import org.opensaml.saml.saml2.core.AttributeValue;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
Expand Down Expand Up @@ -66,24 +75,28 @@ public class SamlController implements InitializingBean {
private final Site site;
private final Application application;
private final CoreService coreService;
private final MessageSource messageSource;

private @Value("${samlEnabled:false}") boolean samlEnabled;
private @Value("${samlClientId:}") String clientId;
// private @Value("${samlAssertionConsumerUrl:}") String assertionConsumerUrl;
private @Value("${" + AuthenticationSettings.SAML_ENABLED + "}") boolean samlEnabled;
private @Value("${" + AuthenticationSettings.SAML_CLIENT_ID + "}") String clientId;
private @Value("${" + AuthenticationSettings.SAML_FORWARD_TARGET + "}") String forwardTarget;
private List<String> userGroups;
private SamlClient samlClient;

public static String CLAIM = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/";

@Override
public void afterPropertiesSet() throws Exception {
if (samlEnabled) {
byte[] samlDescriptor = application.getProperties().getClob("samlDescriptor")
byte[] samlDescriptor = application.getProperties().getClob(AuthenticationSettings.SAML_DESCRIPTOR)
.getBytes(StandardCharsets.UTF_8);
userGroups = application.getProperties().getList(AuthenticationSettings.SAML_CREATE_NEW_USER_WITH_GROUPS,
",");
String assertionConsumerUrl = String.format("%s/service/%s/%s/rest/saml", site.getDomain(), site.getName(),
application.getName());
samlClient = SamlClient.fromMetadata(clientId, assertionConsumerUrl,
new InputStreamReader(new ByteArrayInputStream(samlDescriptor)), SamlClient.SamlIdpBinding.POST);
LOGGER.debug("Created SAML client for '' with endpoint {}", clientId, assertionConsumerUrl);
LOGGER.info("Created SAML client '{}' with endpoint {}", clientId, samlClient.getIdentityProviderUrl());
} else {
LOGGER.debug("SAML is disabled");
}
Expand All @@ -98,88 +111,110 @@ public void login(HttpServletResponse response) throws IOException, SamlExceptio
samlClient.redirectToIdentityProvider(response, null);
}

@PostMapping(path = "/saml/sign-on", produces = { MediaType.TEXT_PLAIN_VALUE }, consumes = {
MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_XML_VALUE })
public ResponseEntity<String> signOn(@RequestBody String payload) {
if (!samlEnabled) {
return NOT_IMPLEMENTED;
}
return new ResponseEntity<>(payload, HttpStatus.OK);
}

@PostMapping(path = "/saml/logout", produces = { MediaType.TEXT_PLAIN_VALUE }, consumes = {
MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_XML_VALUE })
public ResponseEntity<String> logout(@RequestBody String payload) {
if (!samlEnabled) {
return NOT_IMPLEMENTED;
}
return new ResponseEntity<>(payload, HttpStatus.OK);
}

@PostMapping(path = "/saml", produces = MediaType.TEXT_PLAIN_VALUE, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ResponseEntity<Void> reply(HttpServletRequest request, Environment environment) {
if (!samlEnabled) {
return NOT_IMPLEMENTED;
}
String messageText = "Login failed!";
String messageText = MessageConstants.USER_LOGIN_FAIL;
MessageType level = MessageType.ERROR;
try {
String parameter = request.getParameter("SAMLResponse");
LOGGER.debug("Received SAMLResponse: {}", parameter);
SamlResponse samlResp = samlClient.decodeAndValidateSamlResponse(parameter, request.getMethod());
LOGGER.debug("Received SAMLResponse for {}", samlResp.getNameID());

Assertion assertion = samlResp.getAssertion();
Map<String, List<String>> stringAttributes = new HashMap<>();
Map<String, List<String>> attributes = new HashMap<>();

for (AttributeStatement as : assertion.getAttributeStatements()) {
for (Attribute attr : as.getAttributes()) {
String name = attr.getName();
List<String> values = attr.getAttributeValues().stream().filter(v -> (v instanceof AttributeValue))
.map(AttributeValue.class::cast).map(AttributeValue::getTextContent)
.collect(Collectors.toList());
stringAttributes.put(name, values);
attributes.put(name, values);
LOGGER.debug("Attribute {} with values {}", name, StringUtils.join(values, ", "));
}
}

// https://learn.microsoft.com/en-us/azure/active-directory/develop/reference-saml-tokens
String email = attributes.get(CLAIM + "name").get(0);
Subject subject = coreService.getSubjectByEmail(email);
if (null == subject && !userGroups.isEmpty()) {
subject = createUser(environment, email, attributes);
}

String emailAttributeName = "name";
String givenname = "givenname";
String surname = "surname";
List<String> emails = stringAttributes.get(CLAIM + emailAttributeName);
if (!emails.isEmpty()) {
String email = emails.get(0);
Subject subject = coreService.getSubjectByEmail(email);
if (null == subject) {
messageText = "Unknown user";
level = MessageType.INVALID;
} else if (subject.isLocked()) {
messageText = "User is locked";
if (null != subject) {
if (subject.isLocked()) {
messageText = MessageConstants.USER_IS_LOCKED;
} else {
boolean success = coreService.loginByUserName(environment, subject.getAuthName());
LOGGER.info("Logged in {} : {}", subject.getAuthName(), success);
if (success) {
messageText = "Login successfull";
messageText = MessageConstants.USER_AUTHENTICATED;
level = MessageType.OK;
}
}
} else {
messageText = MessageConstants.USER_UNKNOWN;
level = MessageType.INVALID;
}

} catch (SamlException e) {
LOGGER.error("Error processing SAML Response #" + e.hashCode() + "", e);
LOGGER.error("Error processing SAML Response", e);
messageText = "Error processing login request (#" + e.hashCode() + ")";
}
Messages messages = new Messages();
Message message = new Message();
message.setClazz(level);
message.setContent(messageText);
message.setContent(messageSource.getMessage(messageText, new Object[0], environment.getLocale()));
messages.getMessageList().add(message);
ElementHelper.addMessages(environment, messages);

HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.LOCATION, "/manager");
headers.set(HttpHeaders.LOCATION, forwardTarget);
return new ResponseEntity<>(headers, HttpStatus.FOUND);
}

private Subject createUser(Environment environment, String email, Map<String, List<String>> attributes) {
String givenname = attributes.get(CLAIM + "givenName").get(0);
String surname = attributes.get(CLAIM + "surname").get(0);
String userName = StringUtils.lowerCase(StringNormalizer.normalize(givenname + "." + surname));
try {
SubjectImpl user = new SubjectImpl();
user.setEmail(email);
user.setLanguage(environment.getLocale().getLanguage());
user.setName(userName);
user.setTimeZone(environment.getTimeZone().getID());
user.setPasswordChangePolicy(PasswordChangePolicy.MUST_NOT);
user.setRealname(givenname + " " + surname);
user.setDescription("automatically created from SAML login at " + new Date());
user.setUserType(UserType.LOCAL_USER);
Subject newUser = coreService.createSubject(user);
coreService.addGroupsToSubject(user.getName(), userGroups, true);
return newUser;
} catch (BusinessException e) {
LOGGER.error("Error creating new user " + userName, e);
}
return null;
}

@PostMapping(path = "/saml/sign-on", produces = { MediaType.TEXT_PLAIN_VALUE }, consumes = {
MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_XML_VALUE })
public ResponseEntity<String> signOn(@RequestBody String payload) {
if (!samlEnabled) {
return NOT_IMPLEMENTED;
}
return new ResponseEntity<>(payload, HttpStatus.OK);
}

@PostMapping(path = "/saml/logout", produces = { MediaType.TEXT_PLAIN_VALUE }, consumes = {
MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_XML_VALUE })
public ResponseEntity<String> logout(@RequestBody String payload) {
if (!samlEnabled) {
return NOT_IMPLEMENTED;
}
return new ResponseEntity<>(payload, HttpStatus.OK);
}

}

0 comments on commit b784f1c

Please sign in to comment.