I am wanting to implement a SSO with Azure to an application that is built with Struts 2, I have already implemented the filter to make me the login from the microsoft page.
however the redirection issue when everything is correct is not quite clear to me as I am not very good with Struts as when you originally log into the app it redirects to a URL like this localhost:8080/Portal/login/loginAction_validateUser.action.
this is the original login class of the application
public class ActionLogin extends ActionSupport implements ServletRequestAware {
private static Logger logger = LogManager.getLogger(ActionLogin.class);
private HttpServletRequest request;
private UtilHttp uhttp = new UtilHttp();
private UtilFiles ufiles = new UtilFiles();
private User user;
private Person person;
private int invalidateSession;
public void setServletRequest(HttpServletRequest request) {
this.request = request;
}
public String validateUser() {
try {
HttpSession session = request.getSession();
uhttp.cleanSession(request);
LoginBusiness business = new LoginBusiness();
String page = "menu";
boolean loadMenu = true;
if (invalidateSession == 1) {
new LoginBusiness().createDifferentSession(session, user.getLogin());
invalidateSession = 0;
}
user = business.validateUser(user.getLogin(), user.getPassword(), user.getIdProfile());
if (business.isUserLocked(user.getIdUser())) {
addActionError(PortalAdministracionErrorCode.E0400.getMessage());
return "blockedAccount";
}
if (user.isValidAuth()) {
logger.info("user.isValidAuth()");
if (user.isChangePassword()) {
loadMenu = false;
page = "change";
}
if (business.isPasswordExpired(user.getIdUser())) {
addActionError(getText(PortalAdministracionErrorCode.E0401.getMessage()));
loadMenu = false;
return "passwordexpired";
}
session = request.getSession(true);
session.setAttribute(Constants.SESSION_USER, user);
} else {
addActionError(getText("label.error.login.password"));
user.setAuthType("1");
page = "login";
}
return page;
} catch (Exception e) {
addActionError(e.getMessage());
return "login";
}
}
public String logOut() {
HttpSession session = request.getSession(false);
if (session != null) {
try {
logger.info("Realiza un logOut el usuario ");
uhttp.cleanSession(request);
return "logout";
//session.invalidate();
} catch (Exception ex) {
logger.error(ex);
return "logout";
}
}
return "logout";
}
public String invalidateSession() {
invalidateSession = 0;
return "login";
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
public int getInvalidateSession() {
return invalidateSession;
}
public void setInvalidateSession(int invalidateSession) {
this.invalidateSession = invalidateSession;
}
}
this is the XML that is configured with struts, when the login is ok it redirects to the JSP called entrada.jsp but in the url it does not change when it does this if it does not put the action of the login that was made, any idea how to adapt it?
my login.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
"http://struts.apache.org/dtds/struts-2.5.dtd">
<struts>
<package name="login" namespace="/login" extends="struts-default,json-default">
<interceptors>
<interceptor-stack name="defaultSecurityStack">
<interceptor-ref name="tokenSession">
<param name="excludeMethods">*</param>
</interceptor-ref>
<interceptor-ref name="defaultStack" />
</interceptor-stack>
</interceptors>
<default-interceptor-ref name="defaultSecurityStack" />
<global-results>
<result name="error">/jsp/common/errorDetail.jsp</result>
<result name="invalid.token">/jsp/Login.jsp</result>
</global-results>
<action name="loginAction_*" method="{1}" class="action.ActionLogin">
<result name="login">/jsp/Login.jsp</result>
<result name="menu">/jsp/menu/Entrada.jsp</result>
<result name="platformAdmon">/jsp/profile/AddPlatformAdmon.jsp</result>
<result name="change">/jsp/menu/ChangePassword.jsp</result>
<result name="passwordexpired">/jsp/login/account/password-expired.jsp</result>
<result name="success">/jsp/menu/ResultOperation.jsp</result>
<result name="blockedAccount">/jsp/login/account/blocked.jsp</result>
<result name="logout">/jsp/logout.jsp</result>
<interceptor-ref name="defaultSecurityStack">
<param name="tokenSession.excludeMethods">validatePlatformAdmin,logOut,editPassword</param>
</interceptor-ref>
<allowed-methods>toPageLogin, addPlatformAdmon, authType, changePassword, editPassword, logOut, showMenu, validatePlatformAdmin, validateUser</allowed-methods>
</action>
<action name="account_*" method="{1}" class="com.seguridata.rne.administracion.portal.action.LoginAccountAction">
<result name="login">/jsp/Login.jsp</result>
<result name="blocked">/jsp/login/account/blocked.jsp</result>
<result name="resultblocked">/jsp/login/account/blocked-response.jsp</result>
<result name="unlock">/jsp/login/account/unlock.jsp</result>
<result name="resultunlock">/jsp/login/account/unlock-response.jsp</result>
<result name="passwordexpired">/jsp/login/account/password-expired.jsp</result>
<result name="passwordexpired_success">/jsp/login/account/password-expired-success.jsp</result>
<interceptor-ref name="defaultSecurityStack">
<param name="tokenSession.excludeMethods">*</param>
</interceptor-ref>
<allowed-methods>unlock, unlockRequest, updatePasswordExpired, validateUnlockRequest</allowed-methods>
</action>
</package>
</struts>
this is my filter code for the SSO redirection I don't really see the issue here but in the configuration in Azure
public class BasicFilter implements Filter {
public static final String STATES = "states";
public static final String STATE = "state";
public static final Integer STATE_TTL = 3600;
public static final String FAILED_TO_VALIDATE_MESSAGE = "Failed to validate data received from Authorization service - ";
private String clientId = "";
private String clientSecret = "";
private String tenant = "";
private String authority;
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
if (request instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
try {
String currentUri = httpRequest.getRequestURL().toString();
String queryStr = httpRequest.getQueryString();
String fullUrl = currentUri + (queryStr != null ? "?" + queryStr : "");
// check if user has a AuthData in the session
if (!AuthHelper.isAuthenticated(httpRequest)) {
if (AuthHelper.containsAuthenticationData(httpRequest)) {
processAuthenticationData(httpRequest, currentUri, fullUrl);
} else {
// not authenticated
sendAuthRedirect(httpRequest, httpResponse);
return;
}
}
if (isAuthDataExpired(httpRequest)) {
updateAuthDataUsingRefreshToken(httpRequest);
}
} catch (AuthenticationException authException) {
// something went wrong (like expiration or revocation of token)
// we should invalidate AuthData stored in session and redirect to Authorization server
removePrincipalFromSession(httpRequest);
sendAuthRedirect(httpRequest, httpResponse);
return;
} catch (Throwable exc) {
httpResponse.setStatus(500);
request.setAttribute("error", exc.getMessage());
request.getRequestDispatcher("/error.jsp").forward(request, response);
}
}
chain.doFilter(request, response);
}
private boolean isAuthDataExpired(HttpServletRequest httpRequest) {
AuthenticationResult authData = AuthHelper.getAuthSessionObject(httpRequest);
return authData.getExpiresOnDate().before(new Date()) ? true : false;
}
private void updateAuthDataUsingRefreshToken(HttpServletRequest httpRequest) throws Throwable {
AuthenticationResult authData =
getAccessTokenFromRefreshToken(AuthHelper.getAuthSessionObject(httpRequest).getRefreshToken());
setSessionPrincipal(httpRequest, authData);
}
private void processAuthenticationData(HttpServletRequest httpRequest, String currentUri, String fullUrl)
throws Throwable {
HttpSession session = httpRequest.getSession(false);
HashMap<String, String> params = new HashMap<>();
for (String key : httpRequest.getParameterMap().keySet()) {
params.put(key, httpRequest.getParameterMap().get(key)[0]);
}
// validate that state in response equals to state in request
StateData stateData = validateState(httpRequest.getSession(), params.get(STATE));
AuthenticationResponse authResponse = AuthenticationResponseParser.parse(new URI(fullUrl), params);
if (AuthHelper.isAuthenticationSuccessful(authResponse)) {
AuthenticationSuccessResponse oidcResponse = (AuthenticationSuccessResponse) authResponse;
// validate that OIDC Auth Response matches Code Flow (contains only requested artifacts)
validateAuthRespMatchesCodeFlow(oidcResponse);
AuthenticationResult authData =
getAccessToken(oidcResponse.getAuthorizationCode(), currentUri);
// validate nonce to prevent reply attacks (code maybe substituted to one with broader access)
validateNonce(stateData, getClaimValueFromIdToken(authData.getIdToken(), "nonce"));
setSessionPrincipal(httpRequest, authData);
} else {
AuthenticationErrorResponse oidcResponse = (AuthenticationErrorResponse) authResponse;
throw new Exception(String.format("Request for auth code failed: %s - %s",
oidcResponse.getErrorObject().getCode(),
oidcResponse.getErrorObject().getDescription()));
}
}
private void validateNonce(StateData stateData, String nonce) throws Exception {
if (StringUtils.isEmpty(nonce) || !nonce.equals(stateData.getNonce())) {
throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate nonce");
}
}
private String getClaimValueFromIdToken(String idToken, String claimKey) throws ParseException {
return (String) JWTParser.parse(idToken).getJWTClaimsSet().getClaim(claimKey);
}
private void sendAuthRedirect(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException {
httpResponse.setStatus(302);
// use state parameter to validate response from Authorization server
String state = UUID.randomUUID().toString();
// use nonce parameter to validate idToken
String nonce = UUID.randomUUID().toString();
storeStateInSession(httpRequest.getSession(), state, nonce);
String currentUri = httpRequest.getRequestURL().toString();
httpResponse.sendRedirect(getRedirectUrl(currentUri, state, nonce));
}
/**
* make sure that state is stored in the session,
* delete it from session - should be used only once
*
* @param session
* @param state
* @throws Exception
*/
private StateData validateState(HttpSession session, String state) throws Exception {
if (StringUtils.isNotEmpty(state)) {
StateData stateDataInSession = removeStateFromSession(session, state);
if (stateDataInSession != null) {
return stateDataInSession;
}
}
throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate state");
}
private void validateAuthRespMatchesCodeFlow(AuthenticationSuccessResponse oidcResponse) throws Exception {
if (oidcResponse.getIDToken() != null || oidcResponse.getAccessToken() != null ||
oidcResponse.getAuthorizationCode() == null) {
throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "unexpected set of artifacts received");
}
}
@SuppressWarnings("unchecked")
private StateData removeStateFromSession(HttpSession session, String state) {
Map<String, StateData> states = (Map<String, StateData>) session.getAttribute(STATES);
if (states != null) {
eliminateExpiredStates(states);
StateData stateData = states.get(state);
if (stateData != null) {
states.remove(state);
return stateData;
}
}
return null;
}
@SuppressWarnings("unchecked")
private void storeStateInSession(HttpSession session, String state, String nonce) {
if (session.getAttribute(STATES) == null) {
session.setAttribute(STATES, new HashMap<String, StateData>());
}
((Map<String, StateData>) session.getAttribute(STATES)).put(state, new StateData(nonce, new Date()));
}
private void eliminateExpiredStates(Map<String, StateData> map) {
Iterator<Map.Entry<String, StateData>> it = map.entrySet().iterator();
Date currTime = new Date();
while (it.hasNext()) {
Map.Entry<String, StateData> entry = it.next();
long diffInSeconds = TimeUnit.MILLISECONDS.
toSeconds(currTime.getTime() - entry.getValue().getExpirationDate().getTime());
if (diffInSeconds > STATE_TTL) {
it.remove();
}
}
}
private AuthenticationResult getAccessTokenFromRefreshToken(
String refreshToken) throws Throwable {
AuthenticationContext context;
AuthenticationResult result = null;
ExecutorService service = null;
try {
service = Executors.newFixedThreadPool(1);
context = new AuthenticationContext(authority + tenant + "/", true,
service);
Future<AuthenticationResult> future = context
.acquireTokenByRefreshToken(refreshToken, new ClientCredential(clientId, clientSecret), null, null);
result = future.get();
} catch (ExecutionException e) {
throw e.getCause();
} finally {
service.shutdown();
}
if (result == null) {
throw new ServiceUnavailableException("authentication result was null");
}
return result;
}
private AuthenticationResult getAccessToken(
AuthorizationCode authorizationCode, String currentUri)
throws Throwable {
String authCode = authorizationCode.getValue();
ClientCredential credential = new ClientCredential(clientId,
clientSecret);
AuthenticationContext context;
AuthenticationResult result = null;
ExecutorService service = null;
try {
service = Executors.newFixedThreadPool(1);
context = new AuthenticationContext(authority + tenant + "/", true,
service);
Future<AuthenticationResult> future = context
.acquireTokenByAuthorizationCode(authCode, new URI(
currentUri), credential, null);
result = future.get();
} catch (ExecutionException e) {
throw e.getCause();
} finally {
service.shutdown();
}
if (result == null) {
throw new ServiceUnavailableException("authentication result was null");
}
return result;
}
private void setSessionPrincipal(HttpServletRequest httpRequest,
AuthenticationResult result) {
httpRequest.getSession().setAttribute(AuthHelper.PRINCIPAL_SESSION_NAME, result);
}
private void removePrincipalFromSession(HttpServletRequest httpRequest) {
httpRequest.getSession().removeAttribute(AuthHelper.PRINCIPAL_SESSION_NAME);
}
private String getRedirectUrl(String currentUri, String state, String nonce)
throws UnsupportedEncodingException {
String redirectUrl = authority
+ this.tenant
+ "/oauth2/authorize?response_type=code&scope=directory.read.all&response_mode=form_post&redirect_uri="
+ URLEncoder.encode(currentUri, "UTF-8") + "&client_id="
+ clientId + "&resource=https%3a%2f%2fgraph.microsoft.com"
+ "&state=" + state
+ "&nonce=" + nonce;
return redirectUrl;
}
public void init(FilterConfig config) throws ServletException {
clientId = config.getInitParameter("client_id");
authority = config.getServletContext().getInitParameter("authority");
tenant = config.getServletContext().getInitParameter("tenant");
clientSecret = config.getInitParameter("secret_key");
}
private class StateData {
private String nonce;
private Date expirationDate;
public StateData(String nonce, Date expirationDate) {
this.nonce = nonce;
this.expirationDate = expirationDate;
}
public String getNonce() {
return nonce;
}
public Date getExpirationDate() {
return expirationDate;
}
}
}
my web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<filter>
<filter-name>struts-prepare</filter-name>
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareFilter</filter-class>
</filter>
<filter>
<filter-name>sitemesh</filter-name>
<filter-class>com.opensymphony.module.sitemesh.filter.PageFilter</filter-class>
</filter>
<filter>
<filter-name>struts-execute</filter-name>
<filter-class>org.apache.struts2.dispatcher.filter.StrutsExecuteFilter</filter-class>
</filter>
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>struts-prepare</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>sitemesh</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>struts-execute</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<jsp-config>
<taglib>
<taglib-uri>/struts-tags</taglib-uri>
<taglib-location>/WEB-INF/struts-tags.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>sitemesh-page</taglib-uri>
<taglib-location>/WEB-INF/sitemesh-page.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>sitemesh-decorator</taglib-uri>
<taglib-location>/WEB-INF/sitemesh-decorator.tld</taglib-location>
</taglib>
</jsp-config>
<welcome-file-list>
<welcome-file>/jsp/Login.jsp</welcome-file>
</welcome-file-list>
<context-param>
<param-name>authority</param-name>
<param-value>https://login.windows.net/</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/mvc-dispatcher-servlet.xml</param-value>
</context-param>
<context-param>
<param-name>tenant</param-name>
<param-value>mytenat</param-value>
</context-param>
<filter-mapping>
<filter-name>BasicFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>BasicFilter</filter-name>
<filter-class>mx.prototype.adaj4jAzure.BasicFilter</filter-class>
<init-param>
<param-name>client_id</param-name>
<param-value>myclientid</param-value>
</init-param>
<init-param>
<param-name>secret_key</param-name>
<param-value>secretkeyy</param-value>
</init-param>
</filter>
<servlet>
<servlet-name>mvc-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mvc-dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
my struts.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
"http://struts.apache.org/dtds/struts-2.5.dtd">
<struts>
<!--
<constant name="struts.enable.DynamicMethodInvocation" value="true"/>
<constant name="struts.devMode" value="false" />
<constant name="struts.configuration.xml.reload" value="false" />
<constant name="struts.i18n.reload" value="false" />
<constant name="struts.custom.i18n.resources" value="ApplicationResources" />
<constant name="struts.multipart.maxSize" value="5242880"/>
-->
<constant name="struts.enable.DynamicMethodInvocation" value="true"/>
<constant name="struts.custom.i18n.resources" value="ApplicationResources" />
<constant name="struts.multipart.maxSize" value="524288000" />
<constant name="struts.devMode" value="false" />
<constant name="struts.i18n.reload" value="false"/>
<constant name="struts.configuration.xml.reload" value="false"/>
<constant name="struts.freemarker.templatesCache" value="true"/>
<constant name="struts.freemarker.templatesCache.updateDelay" value="120"/>
<constant name="struts.freemarker.mru.max.strong.size" value="120"/>
<constant name="struts.action.excludePattern" value="/struts/webconsole.html" />
<constant name="struts.action.excludePattern" value="/struts/webconsole.css"/>
<constant name="struts.action.excludePattern" value="/struts/webconsole.js"/>
<include file="strutsDomain.xml"/>
<include file="strutsLogin.xml"/>
<include file="strutsPasswordParameters.xml"/>
<include file="strutsPersonalize.xml"/>
<include file="strutsProfile.xml"/>
<include file="strutsTemplate.xml"/>
<include file="strutsRoute.xml"/>
<include file="strutsPlantillas.xml"/>
<package name="serveAll" namespace="" extends="struts-default">
<action name="*">
<result>/jsp/common/error.jsp</result>
</action>
</package>
</struts>
