Merge pull request #187 from Unicon/Frontchannel-slo
Handle front-channel SLO
This commit is contained in:
commit
f5a0ee6987
|
|
@ -77,6 +77,5 @@ public interface ConfigurationKeys {
|
|||
ConfigurationKey<String> ALLOWED_PROXY_CHAINS = new ConfigurationKey<String>("allowedProxyChains", null);
|
||||
ConfigurationKey<Class<? extends Cas20ServiceTicketValidator>> TICKET_VALIDATOR_CLASS = new ConfigurationKey<Class<? extends Cas20ServiceTicketValidator>>("ticketValidatorClass", null);
|
||||
ConfigurationKey<String> PROXY_CALLBACK_URL = new ConfigurationKey<String>("proxyCallbackUrl", null);
|
||||
ConfigurationKey<String> FRONT_LOGOUT_PARAMETER_NAME = new ConfigurationKey<String>("frontLogoutParameterName", "SAMLRequest");
|
||||
ConfigurationKey<String> RELAY_STATE_PARAMETER_NAME = new ConfigurationKey<String>("relayStateParameterName", "RelayState");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ public final class SingleSignOutFilter extends AbstractConfigurationFilter {
|
|||
if (!isIgnoreInitConfiguration()) {
|
||||
setArtifactParameterName(getString(ConfigurationKeys.ARTIFACT_PARAMETER_NAME));
|
||||
setLogoutParameterName(getString(ConfigurationKeys.LOGOUT_PARAMETER_NAME));
|
||||
setFrontLogoutParameterName(getString(ConfigurationKeys.FRONT_LOGOUT_PARAMETER_NAME));
|
||||
setRelayStateParameterName(getString(ConfigurationKeys.RELAY_STATE_PARAMETER_NAME));
|
||||
setCasServerUrlPrefix(getString(ConfigurationKeys.CAS_SERVER_URL_PREFIX));
|
||||
HANDLER.setArtifactParameterOverPost(getBoolean(ConfigurationKeys.ARTIFACT_PARAMETER_OVER_POST));
|
||||
|
|
@ -63,11 +62,7 @@ public final class SingleSignOutFilter extends AbstractConfigurationFilter {
|
|||
public void setLogoutParameterName(final String name) {
|
||||
HANDLER.setLogoutParameterName(name);
|
||||
}
|
||||
|
||||
public void setFrontLogoutParameterName(final String name) {
|
||||
HANDLER.setFrontLogoutParameterName(name);
|
||||
}
|
||||
|
||||
|
||||
public void setRelayStateParameterName(final String name) {
|
||||
HANDLER.setRelayStateParameterName(name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,12 +57,9 @@ public final class SingleSignOutHandler {
|
|||
/** The name of the artifact parameter. This is used to capture the session identifier. */
|
||||
private String artifactParameterName = Protocol.CAS2.getArtifactParameterName();
|
||||
|
||||
/** Parameter name that stores logout request for back channel SLO */
|
||||
/** Parameter name that stores logout request for SLO */
|
||||
private String logoutParameterName = ConfigurationKeys.LOGOUT_PARAMETER_NAME.getDefaultValue();
|
||||
|
||||
/** Parameter name that stores logout request for front channel SLO */
|
||||
private String frontLogoutParameterName = ConfigurationKeys.FRONT_LOGOUT_PARAMETER_NAME.getDefaultValue();
|
||||
|
||||
|
||||
/** Parameter name that stores the state of the CAS server webflow for the callback */
|
||||
private String relayStateParameterName = ConfigurationKeys.RELAY_STATE_PARAMETER_NAME.getDefaultValue();
|
||||
|
||||
|
|
@ -75,7 +72,7 @@ public final class SingleSignOutHandler {
|
|||
|
||||
private List<String> safeParameters;
|
||||
|
||||
private LogoutStrategy logoutStrategy = isServlet30() ? new Servlet30LogoutStrategy() : new Servlet25LogoutStrategy();
|
||||
private final LogoutStrategy logoutStrategy = isServlet30() ? new Servlet30LogoutStrategy() : new Servlet25LogoutStrategy();
|
||||
|
||||
public void setSessionMappingStorage(final SessionMappingStorage storage) {
|
||||
this.sessionMappingStorage = storage;
|
||||
|
|
@ -97,7 +94,7 @@ public final class SingleSignOutHandler {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param name Name of parameter containing CAS logout request message for back channel SLO.
|
||||
* @param name Name of parameter containing CAS logout request message for SLO.
|
||||
*/
|
||||
public void setLogoutParameterName(final String name) {
|
||||
this.logoutParameterName = name;
|
||||
|
|
@ -109,14 +106,7 @@ public final class SingleSignOutHandler {
|
|||
public void setCasServerUrlPrefix(final String casServerUrlPrefix) {
|
||||
this.casServerUrlPrefix = casServerUrlPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name Name of parameter containing CAS logout request message for front channel SLO.
|
||||
*/
|
||||
public void setFrontLogoutParameterName(final String name) {
|
||||
this.frontLogoutParameterName = name;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param name Name of parameter containing the state of the CAS server webflow.
|
||||
*/
|
||||
|
|
@ -135,7 +125,6 @@ public final class SingleSignOutHandler {
|
|||
if (this.safeParameters == null) {
|
||||
CommonUtils.assertNotNull(this.artifactParameterName, "artifactParameterName cannot be null.");
|
||||
CommonUtils.assertNotNull(this.logoutParameterName, "logoutParameterName cannot be null.");
|
||||
CommonUtils.assertNotNull(this.frontLogoutParameterName, "frontLogoutParameterName cannot be null.");
|
||||
CommonUtils.assertNotNull(this.sessionMappingStorage, "sessionMappingStorage cannot be null.");
|
||||
CommonUtils.assertNotNull(this.relayStateParameterName, "relayStateParameterName cannot be null.");
|
||||
CommonUtils.assertNotNull(this.casServerUrlPrefix, "casServerUrlPrefix cannot be null.");
|
||||
|
|
@ -165,32 +154,25 @@ public final class SingleSignOutHandler {
|
|||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given request is a CAS back channel logout request.
|
||||
* Determines whether the given request is a CAS logout request.
|
||||
*
|
||||
* @param request HTTP request.
|
||||
*
|
||||
* @return True if request is logout request, false otherwise.
|
||||
*/
|
||||
private boolean isBackChannelLogoutRequest(final HttpServletRequest request) {
|
||||
return "POST".equals(request.getMethod())
|
||||
&& !isMultipartRequest(request)
|
||||
&& CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName,
|
||||
this.safeParameters));
|
||||
private boolean isLogoutRequest(final HttpServletRequest request) {
|
||||
if ("POST".equalsIgnoreCase(request.getMethod())) {
|
||||
return !isMultipartRequest(request)
|
||||
&& CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName,
|
||||
this.safeParameters));
|
||||
}
|
||||
|
||||
if ("GET".equalsIgnoreCase(request.getMethod())) {
|
||||
return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given request is a CAS front channel logout request. Front Channel log out requests are only supported
|
||||
* when the 'casServerUrlPrefix' value is set.
|
||||
*
|
||||
* @param request HTTP request.
|
||||
*
|
||||
* @return True if request is logout request, false otherwise.
|
||||
*/
|
||||
private boolean isFrontChannelLogoutRequest(final HttpServletRequest request) {
|
||||
return "GET".equals(request.getMethod()) && CommonUtils.isNotBlank(this.casServerUrlPrefix)
|
||||
&& CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.frontLogoutParameterName));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process a request regarding the SLO process: record the session or destroy it.
|
||||
*
|
||||
|
|
@ -203,26 +185,15 @@ public final class SingleSignOutHandler {
|
|||
logger.trace("Received a token request");
|
||||
recordSession(request);
|
||||
return true;
|
||||
|
||||
} else if (isBackChannelLogoutRequest(request)) {
|
||||
logger.trace("Received a back channel logout request");
|
||||
}
|
||||
|
||||
if (isLogoutRequest(request)) {
|
||||
logger.trace("Received a logout request");
|
||||
destroySession(request);
|
||||
return false;
|
||||
|
||||
} else if (isFrontChannelLogoutRequest(request)) {
|
||||
logger.trace("Received a front channel logout request");
|
||||
destroySession(request);
|
||||
// redirection url to the CAS server
|
||||
final String redirectionUrl = computeRedirectionToServer(request);
|
||||
if (redirectionUrl != null) {
|
||||
CommonUtils.sendRedirect(response, redirectionUrl);
|
||||
}
|
||||
return false;
|
||||
|
||||
} else {
|
||||
logger.trace("Ignoring URI for logout: {}", request.getRequestURI());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
logger.trace("Ignoring URI for logout: {}", request.getRequestURI());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -245,7 +216,7 @@ public final class SingleSignOutHandler {
|
|||
try {
|
||||
this.sessionMappingStorage.removeBySessionById(session.getId());
|
||||
} catch (final Exception e) {
|
||||
// ignore if the session is already marked as invalid. Nothing we can do!
|
||||
// ignore if the session is already marked as invalid. Nothing we can do!
|
||||
}
|
||||
sessionMappingStorage.addSessionById(token, session);
|
||||
}
|
||||
|
|
@ -286,16 +257,17 @@ public final class SingleSignOutHandler {
|
|||
* @param request HTTP request containing a CAS logout message.
|
||||
*/
|
||||
private void destroySession(final HttpServletRequest request) {
|
||||
final String logoutMessage;
|
||||
// front channel logout -> the message needs to be base64 decoded + decompressed
|
||||
if (isFrontChannelLogoutRequest(request)) {
|
||||
logoutMessage = uncompressLogoutMessage(CommonUtils.safeGetParameter(request,
|
||||
this.frontLogoutParameterName));
|
||||
} else {
|
||||
logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters);
|
||||
String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters);
|
||||
if (CommonUtils.isBlank(logoutMessage)) {
|
||||
logger.error("Could not locate logout message of the request from {}", this.logoutParameterName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!logoutMessage.contains("SessionIndex")) {
|
||||
logoutMessage = uncompressLogoutMessage(logoutMessage);
|
||||
}
|
||||
|
||||
logger.trace("Logout request:\n{}", logoutMessage);
|
||||
|
||||
final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
|
||||
if (CommonUtils.isNotBlank(token)) {
|
||||
final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);
|
||||
|
|
@ -314,33 +286,6 @@ public final class SingleSignOutHandler {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the redirection url to the CAS server when it's a front channel SLO
|
||||
* (depending on the relay state parameter).
|
||||
*
|
||||
* @param request The HTTP request.
|
||||
* @return the redirection url to the CAS server.
|
||||
*/
|
||||
private String computeRedirectionToServer(final HttpServletRequest request) {
|
||||
final String relayStateValue = CommonUtils.safeGetParameter(request, this.relayStateParameterName);
|
||||
// if we have a state value -> redirect to the CAS server to continue the logout process
|
||||
if (CommonUtils.isNotBlank(relayStateValue)) {
|
||||
final StringBuilder buffer = new StringBuilder();
|
||||
buffer.append(casServerUrlPrefix);
|
||||
if (!this.casServerUrlPrefix.endsWith("/")) {
|
||||
buffer.append("/");
|
||||
}
|
||||
buffer.append("logout?_eventId=next&");
|
||||
buffer.append(this.relayStateParameterName);
|
||||
buffer.append("=");
|
||||
buffer.append(CommonUtils.urlEncode(relayStateValue));
|
||||
final String redirectUrl = buffer.toString();
|
||||
logger.debug("Redirection url to the CAS server: {}", redirectUrl);
|
||||
return redirectUrl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isMultipartRequest(final HttpServletRequest request) {
|
||||
return request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ public class Cas20ProxyReceivingTicketValidationFilter extends AbstractTicketVal
|
|||
TOLERANCE.getName(), IGNORE_PATTERN.getName(), IGNORE_URL_PATTERN_TYPE.getName(), HOSTNAME_VERIFIER.getName(), HOSTNAME_VERIFIER_CONFIG.getName(),
|
||||
EXCEPTION_ON_VALIDATION_FAILURE.getName(), REDIRECT_AFTER_VALIDATION.getName(), USE_SESSION.getName(), SECRET_KEY.getName(), CIPHER_ALGORITHM.getName(), PROXY_RECEPTOR_URL.getName(),
|
||||
PROXY_GRANTING_TICKET_STORAGE_CLASS.getName(), MILLIS_BETWEEN_CLEAN_UPS.getName(), ACCEPT_ANY_PROXY.getName(), ALLOWED_PROXY_CHAINS.getName(), TICKET_VALIDATOR_CLASS.getName(),
|
||||
PROXY_CALLBACK_URL.getName(), FRONT_LOGOUT_PARAMETER_NAME.getName(), RELAY_STATE_PARAMETER_NAME.getName()
|
||||
PROXY_CALLBACK_URL.getName(), RELAY_STATE_PARAMETER_NAME.getName()
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -87,8 +87,8 @@ public class SingleSignOutFilterTests {
|
|||
@Test
|
||||
public void frontChannelRequest() throws IOException, ServletException {
|
||||
final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(TICKET);
|
||||
request.setParameter(ConfigurationKeys.FRONT_LOGOUT_PARAMETER_NAME.getDefaultValue(), logoutMessage);
|
||||
request.setQueryString(ConfigurationKeys.FRONT_LOGOUT_PARAMETER_NAME.getDefaultValue() + "=" + logoutMessage);
|
||||
request.setParameter(ConfigurationKeys.LOGOUT_PARAMETER_NAME.getDefaultValue(), logoutMessage);
|
||||
request.setQueryString(ConfigurationKeys.LOGOUT_PARAMETER_NAME.getDefaultValue() + "=" + logoutMessage);
|
||||
request.setMethod("GET");
|
||||
final MockHttpSession session = new MockHttpSession();
|
||||
SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage().addSessionById(TICKET, session);
|
||||
|
|
@ -100,16 +100,14 @@ public class SingleSignOutFilterTests {
|
|||
@Test
|
||||
public void frontChannelRequestRelayState() throws IOException, ServletException {
|
||||
final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(TICKET);
|
||||
request.setParameter(ConfigurationKeys.FRONT_LOGOUT_PARAMETER_NAME.getDefaultValue(), logoutMessage);
|
||||
request.setParameter(ConfigurationKeys.LOGOUT_PARAMETER_NAME.getDefaultValue(), logoutMessage);
|
||||
request.setParameter(ConfigurationKeys.RELAY_STATE_PARAMETER_NAME.getDefaultValue(), RELAY_STATE);
|
||||
request.setQueryString(ConfigurationKeys.FRONT_LOGOUT_PARAMETER_NAME.getDefaultValue() + "=" + logoutMessage + "&" +
|
||||
request.setQueryString(ConfigurationKeys.LOGOUT_PARAMETER_NAME.getDefaultValue() + "=" + logoutMessage + "&" +
|
||||
ConfigurationKeys.RELAY_STATE_PARAMETER_NAME.getDefaultValue() + "=" + RELAY_STATE);
|
||||
request.setMethod("GET");
|
||||
final MockHttpSession session = new MockHttpSession();
|
||||
SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage().addSessionById(TICKET, session);
|
||||
filter.doFilter(request, response, filterChain);
|
||||
assertNull(SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage().removeSessionByMappingId(TICKET));
|
||||
assertEquals(CAS_SERVER_URL_PREFIX + "/logout?_eventId=next&" +
|
||||
ConfigurationKeys.RELAY_STATE_PARAMETER_NAME.getDefaultValue() + "=" + RELAY_STATE, response.getRedirectedUrl());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ import org.springframework.mock.web.MockHttpSession;
|
|||
|
||||
/**
|
||||
* @author Matt Brown <matt.brown@citrix.com>
|
||||
* @version $Revision$ $Date$
|
||||
* @since 3.2.1
|
||||
*/
|
||||
public final class SingleSignOutHandlerTests {
|
||||
|
|
@ -39,9 +38,8 @@ public final class SingleSignOutHandlerTests {
|
|||
private final static String ANOTHER_PARAMETER = "anotherParameter";
|
||||
private final static String TICKET = "ST-xxxxxxxx";
|
||||
private final static String URL = "http://mycasserver";
|
||||
private final static String LOGOUT_PARAMETER_NAME = "logoutRequest2";
|
||||
private final static String FRONT_LOGOUT_PARAMETER_NAME = "SAMLRequest2";
|
||||
private final static String RELAY_STATE_PARAMETER_NAME = "RelayState2";
|
||||
private final static String LOGOUT_PARAMETER_NAME = "logoutRequest";
|
||||
private final static String RELAY_STATE_PARAMETER_NAME = "RelayState";
|
||||
private final static String ARTIFACT_PARAMETER_NAME = "ticket2";
|
||||
|
||||
private SingleSignOutHandler handler;
|
||||
|
|
@ -52,7 +50,6 @@ public final class SingleSignOutHandlerTests {
|
|||
public void setUp() throws Exception {
|
||||
handler = new SingleSignOutHandler();
|
||||
handler.setLogoutParameterName(LOGOUT_PARAMETER_NAME);
|
||||
handler.setFrontLogoutParameterName(FRONT_LOGOUT_PARAMETER_NAME);
|
||||
handler.setRelayStateParameterName(RELAY_STATE_PARAMETER_NAME);
|
||||
handler.setArtifactParameterName(ARTIFACT_PARAMETER_NAME);
|
||||
handler.setCasServerUrlPrefix(URL);
|
||||
|
|
@ -143,8 +140,8 @@ public final class SingleSignOutHandlerTests {
|
|||
@Test
|
||||
public void frontChannelLogoutFailsIfNoSessionIndex() {
|
||||
final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage("");
|
||||
request.setParameter(FRONT_LOGOUT_PARAMETER_NAME, logoutMessage);
|
||||
request.setQueryString(FRONT_LOGOUT_PARAMETER_NAME + "=" + logoutMessage);
|
||||
request.setParameter(LOGOUT_PARAMETER_NAME, logoutMessage);
|
||||
request.setQueryString(LOGOUT_PARAMETER_NAME + "=" + logoutMessage);
|
||||
request.setMethod("GET");
|
||||
final MockHttpSession session = new MockHttpSession();
|
||||
handler.getSessionMappingStorage().addSessionById(TICKET, session);
|
||||
|
|
@ -155,8 +152,8 @@ public final class SingleSignOutHandlerTests {
|
|||
@Test
|
||||
public void frontChannelLogoutOK() {
|
||||
final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(TICKET);
|
||||
request.setParameter(FRONT_LOGOUT_PARAMETER_NAME, logoutMessage);
|
||||
request.setQueryString(FRONT_LOGOUT_PARAMETER_NAME + "=" + logoutMessage);
|
||||
request.setParameter(LOGOUT_PARAMETER_NAME, logoutMessage);
|
||||
request.setQueryString(LOGOUT_PARAMETER_NAME + "=" + logoutMessage);
|
||||
request.setMethod("GET");
|
||||
final MockHttpSession session = new MockHttpSession();
|
||||
handler.getSessionMappingStorage().addSessionById(TICKET, session);
|
||||
|
|
@ -168,15 +165,13 @@ public final class SingleSignOutHandlerTests {
|
|||
@Test
|
||||
public void frontChannelLogoutRelayStateOK() {
|
||||
final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(TICKET);
|
||||
request.setParameter(FRONT_LOGOUT_PARAMETER_NAME, logoutMessage);
|
||||
request.setParameter(LOGOUT_PARAMETER_NAME, logoutMessage);
|
||||
request.setParameter(RELAY_STATE_PARAMETER_NAME, TICKET);
|
||||
request.setQueryString(FRONT_LOGOUT_PARAMETER_NAME + "=" + logoutMessage + "&" + RELAY_STATE_PARAMETER_NAME + "=" + TICKET);
|
||||
request.setQueryString(LOGOUT_PARAMETER_NAME + "=" + logoutMessage + "&" + RELAY_STATE_PARAMETER_NAME + "=" + TICKET);
|
||||
request.setMethod("GET");
|
||||
final MockHttpSession session = new MockHttpSession();
|
||||
handler.getSessionMappingStorage().addSessionById(TICKET, session);
|
||||
assertFalse(handler.process(request, response));
|
||||
assertTrue(session.isInvalid());
|
||||
assertEquals(URL + "/logout?_eventId=next&" + RELAY_STATE_PARAMETER_NAME + "=" + TICKET,
|
||||
response.getRedirectedUrl());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,11 +51,7 @@ public class SingleSignOutValve extends AbstractLifecycleValve implements Sessio
|
|||
public void setLogoutParameterName(final String name) {
|
||||
this.handler.setLogoutParameterName(name);
|
||||
}
|
||||
|
||||
public void setFrontLogoutParameterName(final String name) {
|
||||
this.handler.setFrontLogoutParameterName(name);
|
||||
}
|
||||
|
||||
|
||||
public void setRelayStateParameterName(final String name) {
|
||||
this.handler.setRelayStateParameterName(name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,11 +55,7 @@ public class SingleSignOutValve extends ValveBase implements SessionListener {
|
|||
public void setLogoutParameterName(final String name) {
|
||||
this.handler.setLogoutParameterName(name);
|
||||
}
|
||||
|
||||
public void setFrontLogoutParameterName(final String name) {
|
||||
this.handler.setFrontLogoutParameterName(name);
|
||||
}
|
||||
|
||||
|
||||
public void setRelayStateParameterName(final String name) {
|
||||
this.handler.setRelayStateParameterName(name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,11 +55,7 @@ public class SingleSignOutValve extends ValveBase implements SessionListener {
|
|||
public void setLogoutParameterName(final String name) {
|
||||
this.handler.setLogoutParameterName(name);
|
||||
}
|
||||
|
||||
public void setFrontLogoutParameterName(final String name) {
|
||||
this.handler.setFrontLogoutParameterName(name);
|
||||
}
|
||||
|
||||
|
||||
public void setRelayStateParameterName(final String name) {
|
||||
this.handler.setRelayStateParameterName(name);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue