aboutsummaryrefslogtreecommitdiffstats
path: root/odl-aaa-moon/aaa-authn-federation/src
diff options
context:
space:
mode:
Diffstat (limited to 'odl-aaa-moon/aaa-authn-federation/src')
-rw-r--r--odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/Activator.java51
-rw-r--r--odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ClaimAuthFilter.java249
-rw-r--r--odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationConfiguration.java95
-rw-r--r--odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationEndpoint.java149
-rw-r--r--odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ServiceLocator.java83
-rw-r--r--odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/SssdFilter.java151
-rw-r--r--odl-aaa-moon/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.properties11
-rw-r--r--odl-aaa-moon/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.xml19
-rw-r--r--odl-aaa-moon/aaa-authn-federation/src/main/resources/WEB-INF/web.xml34
-rw-r--r--odl-aaa-moon/aaa-authn-federation/src/main/resources/federation.cfg3
-rw-r--r--odl-aaa-moon/aaa-authn-federation/src/test/java/org/opendaylight/aaa/federation/FederationEndpointTest.java121
11 files changed, 966 insertions, 0 deletions
diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/Activator.java b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/Activator.java
new file mode 100644
index 00000000..4ae027c8
--- /dev/null
+++ b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/Activator.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.federation;
+
+import java.util.Dictionary;
+import org.apache.felix.dm.DependencyActivatorBase;
+import org.apache.felix.dm.DependencyManager;
+import org.opendaylight.aaa.api.ClaimAuth;
+import org.opendaylight.aaa.api.IdMService;
+import org.opendaylight.aaa.api.TokenStore;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.service.cm.ManagedService;
+
+/**
+ * An activator for the secure token server to inject in a
+ * <code>CredentialAuth</code> implementation.
+ *
+ * @author liemmn
+ *
+ */
+public class Activator extends DependencyActivatorBase {
+ private static final String FEDERATION_PID = "org.opendaylight.aaa.federation";
+
+ @Override
+ public void init(BundleContext context, DependencyManager manager) throws Exception {
+ manager.add(createComponent()
+ .setImplementation(ServiceLocator.getInstance())
+ .add(createServiceDependency().setService(TokenStore.class).setRequired(true))
+ .add(createServiceDependency().setService(IdMService.class).setRequired(true))
+ .add(createServiceDependency().setService(ClaimAuth.class).setRequired(false)
+ .setCallbacks("claimAuthAdded", "claimAuthRemoved")));
+ context.registerService(ManagedService.class, FederationConfiguration.instance(),
+ addPid(FederationConfiguration.defaults));
+ }
+
+ @Override
+ public void destroy(BundleContext context, DependencyManager manager) throws Exception {
+ }
+
+ private Dictionary<String, ?> addPid(Dictionary<String, String> dict) {
+ dict.put(Constants.SERVICE_PID, FEDERATION_PID);
+ return dict;
+ }
+}
diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ClaimAuthFilter.java b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ClaimAuthFilter.java
new file mode 100644
index 00000000..10a1277d
--- /dev/null
+++ b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ClaimAuthFilter.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.federation;
+
+import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
+import static org.opendaylight.aaa.federation.FederationEndpoint.AUTH_CLAIM;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.opendaylight.aaa.api.Claim;
+import org.opendaylight.aaa.api.ClaimAuth;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A generic {@link Filter} for {@link ClaimAuth} implementations.
+ * <p>
+ * This filter trusts any authentication metadata bound to a request. A request
+ * with fake authentication claims could be forged by an attacker and submitted
+ * to one of the Connector ports the engine is listening on and we would blindly
+ * accept the forged information in this filter. Therefore it is vital we only
+ * accept authentication claims from a trusted proxy. It is incumbent upon the
+ * site administrator to dedicate specific connector ports on which previously
+ * authenticated requests from a trusted proxy will be sent to and to assure
+ * only a trusted proxy can connect to that port. The site administrator must
+ * enumerate those ports in the configuration. We reject any request which did
+ * not originate on one of the configured secure proxy ports.
+ *
+ * @author liemmn
+ *
+ */
+public class ClaimAuthFilter implements Filter {
+ private static final Logger LOG = LoggerFactory.getLogger(ClaimAuthFilter.class);
+
+ private static final String CGI_AUTH_TYPE = "AUTH_TYPE";
+ private static final String CGI_PATH_INFO = "PATH_INFO";
+ private static final String CGI_PATH_TRANSLATED = "PATH_TRANSLATED";
+ private static final String CGI_QUERY_STRING = "QUERY_STRING";
+ private static final String CGI_REMOTE_ADDR = "REMOTE_ADDR";
+ private static final String CGI_REMOTE_HOST = "REMOTE_HOST";
+ private static final String CGI_REMOTE_PORT = "REMOTE_PORT";
+ private static final String CGI_REMOTE_USER = "REMOTE_USER";
+ private static final String CGI_REMOTE_USER_GROUPS = "REMOTE_USER_GROUPS";
+ private static final String CGI_REQUEST_METHOD = "REQUEST_METHOD";
+ private static final String CGI_SCRIPT_NAME = "SCRIPT_NAME";
+ private static final String CGI_SERVER_PROTOCOL = "SERVER_PROTOCOL";
+
+ static final String UNAUTHORIZED_PORT_ERR = "Unauthorized proxy port";
+
+ @Override
+ public void init(FilterConfig fc) throws ServletException {
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ @Override
+ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
+ throws IOException, ServletException {
+ Set<Integer> secureProxyPorts;
+ int localPort;
+
+ // Check to see if we are communicated over an authorized port or not
+ secureProxyPorts = FederationConfiguration.instance().secureProxyPorts();
+ localPort = req.getLocalPort();
+ if (!secureProxyPorts.contains(localPort)) {
+ ((HttpServletResponse) resp).sendError(SC_UNAUTHORIZED, UNAUTHORIZED_PORT_ERR);
+ return;
+ }
+
+ // Let's do some transformation!
+ List<ClaimAuth> claimAuthCollection = ServiceLocator.getInstance().getClaimAuthCollection();
+ for (ClaimAuth ca : claimAuthCollection) {
+ Claim claim = ca.transform(claims((HttpServletRequest) req));
+ if (claim != null) {
+ req.setAttribute(AUTH_CLAIM, claim);
+ // No need to do further transformation since it has been done
+ break;
+ }
+ }
+ chain.doFilter(req, resp);
+ }
+
+ // Extract attributes and headers out of the request
+ private Map<String, Object> claims(HttpServletRequest req) {
+ String name;
+ Object objectValue;
+ String stringValue;
+ Map<String, Object> claims = new HashMap<>();
+
+ /*
+ * Tomcat has a bug/feature, not all attributes are enumerated by
+ * getAttributeNames() therefore getAttributeNames() cannot be used to
+ * obtain the full set of attributes. However if you know the name of
+ * the attribute a priori you can call getAttribute() and obtain the
+ * value. Therefore we maintain a list of attribute names
+ * (httpAttributes) which will be used to call getAttribute() with so we
+ * don't miss essential attributes.
+ *
+ * This is the Tomcat bug, note it is marked WONTFIX. Bug 25363 -
+ * request.getAttributeNames() not working properly Status: RESOLVED
+ * WONTFIX https://issues.apache.org/bugzilla/show_bug.cgi?id=25363
+ *
+ * The solution adopted by Tomcat is to document the behavior in the
+ * "The Apache Tomcat Connector - Reference Guide" under the JkEnvVar
+ * property where is says:
+ *
+ * You can retrieve the variables on Tomcat as request attributes via
+ * request.getAttribute(attributeName). Note that the variables send via
+ * JkEnvVar will not be listed in request.getAttributeNames().
+ */
+
+ // Capture attributes which can be enumerated ...
+ @SuppressWarnings("unchecked")
+ Enumeration<String> attrs = req.getAttributeNames();
+ while (attrs.hasMoreElements()) {
+ name = attrs.nextElement();
+ objectValue = req.getAttribute(name);
+ if (objectValue instanceof String) {
+ // metadata might be i18n, assume UTF8 and decode
+ stringValue = decodeUTF8((String) objectValue);
+ objectValue = stringValue;
+ }
+ claims.put(name, objectValue);
+ }
+
+ // Capture specific attributes which cannot be enumerated ...
+ for (String attr : FederationConfiguration.instance().httpAttributes()) {
+ name = attr;
+ objectValue = req.getAttribute(name);
+ if (objectValue instanceof String) {
+ // metadata might be i18n, assume UTF8 and decode
+ stringValue = decodeUTF8((String) objectValue);
+ objectValue = stringValue;
+ }
+ claims.put(name, objectValue);
+ }
+
+ /*
+ * In general we should not utilize HTTP headers as validated security
+ * assertions because they are too easy to forge. Therefore in general
+ * we don't include HTTP headers, however in certain circumstances
+ * specific headers may be acceptable, thus we permit an admin to
+ * configure the capture of specific headers.
+ */
+ for (String header : FederationConfiguration.instance().httpHeaders()) {
+ claims.put(header, req.getHeader(header));
+ }
+
+ // Capture standard CGI variables...
+ claims.put(CGI_AUTH_TYPE, req.getAuthType());
+ claims.put(CGI_PATH_INFO, req.getPathInfo());
+ claims.put(CGI_PATH_TRANSLATED, req.getPathTranslated());
+ claims.put(CGI_QUERY_STRING, req.getQueryString());
+ claims.put(CGI_REMOTE_ADDR, req.getRemoteAddr());
+ claims.put(CGI_REMOTE_HOST, req.getRemoteHost());
+ claims.put(CGI_REMOTE_PORT, req.getRemotePort());
+ // remote user might be i18n, assume UTF8 and decode
+ claims.put(CGI_REMOTE_USER, decodeUTF8(req.getRemoteUser()));
+ claims.put(CGI_REMOTE_USER_GROUPS, req.getAttribute(CGI_REMOTE_USER_GROUPS));
+ claims.put(CGI_REQUEST_METHOD, req.getMethod());
+ claims.put(CGI_SCRIPT_NAME, req.getServletPath());
+ claims.put(CGI_SERVER_PROTOCOL, req.getProtocol());
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("ClaimAuthFilter claims = {}", claims.toString());
+ }
+
+ return claims;
+ }
+
+ /**
+ * Decode from UTF-8, return Unicode.
+ *
+ * If we're unable to UTF-8 decode the string the fallback is to return the
+ * string unmodified and log a warning.
+ *
+ * Some data, especially metadata attached to a user principal may be
+ * internationalized (i18n). The classic examples are the user's name,
+ * location, organization, etc. We need to be able to read this metadata and
+ * decode it into unicode characters so that we properly handle i18n string
+ * values.
+ *
+ * One of the the prolems is we often don't know the encoding (i.e. charset)
+ * of the string. RFC-5987 is supposed to define how non-ASCII values are
+ * transmitted in HTTP headers, this is a follow on from the work in
+ * RFC-2231. However at the time of this writing these RFC's are not
+ * implemented in the Servlet Request classes. Not only are these RFC's
+ * unimplemented but they are specific to HTTP headers, much of our metadata
+ * arrives via attributes as opposed to being in a header.
+ *
+ * Note: ASCII encoding is a subset of UTF-8 encoding therefore any strings
+ * which are pure ASCII will decode from UTF-8 just fine. However on the
+ * other hand Latin-1 (ISO-8859-1) encoding is not compatible with UTF-8 for
+ * code points in the range 128-255 (i.e. beyond 7-bit ascii). ISO-8859-1 is
+ * the default encoding for HTTP and HTML 4, however the consensus is the
+ * use of ISO-8859-1 was a mistake and Unicode with UTF-8 encoding is now
+ * the norm. If a string value is transmitted encoded in ISO-8859-1
+ * contaiing code points in the range 128-255 and we try to UTF-8 decode it
+ * it will either not be the correct decoded string or it will throw a
+ * decoding exception.
+ *
+ * Conventional practice at the moment is for the sending side to encode
+ * internationalized values in UTF-8 with the receving end decoding the
+ * value back from UTF-8. We do not expect the use of ISO-8859-1 on these
+ * attributes. However due to peculiarities of the Java String
+ * implementation we have to specify the raw bytes are encoded in ISO-8859-1
+ * just to get back the raw bytes to be able to feed into the UTF-8 decoder.
+ * This doesn't seem right but it is because we need the full 8-bit byte and
+ * the only way to say "unmodified 8-bit bytes" in Java is to call it
+ * ISO-8859-1. Ugh!
+ *
+ * @param string
+ * The input string in UTF-8 to be decoded.
+ * @return Unicode string
+ */
+ private String decodeUTF8(String string) {
+ if (string == null) {
+ return null;
+ }
+ try {
+ return new String(string.getBytes("ISO8859-1"), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ LOG.warn("Unable to UTF-8 decode: ", string, e);
+ return string;
+ }
+ }
+
+}
diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationConfiguration.java b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationConfiguration.java
new file mode 100644
index 00000000..a68dc15c
--- /dev/null
+++ b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationConfiguration.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.federation;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedService;
+
+/**
+ * AAA federation configurations in OSGi.
+ *
+ * @author liemmn
+ *
+ */
+public class FederationConfiguration implements ManagedService {
+ private static final String FEDERATION_CONFIG_ERR = "Error saving federation configuration";
+
+ static final String HTTP_HEADERS = "httpHeaders";
+ static final String HTTP_ATTRIBUTES = "httpAttributes";
+ static final String SECURE_PROXY_PORTS = "secureProxyPorts";
+
+ static FederationConfiguration instance = new FederationConfiguration();
+
+ static final Hashtable<String, String> defaults = new Hashtable<>();
+ static {
+ defaults.put(HTTP_HEADERS, "");
+ defaults.put(HTTP_ATTRIBUTES, "");
+ }
+ private static Map<String, String> configs = new ConcurrentHashMap<>();
+
+ // singleton
+ private FederationConfiguration() {
+ }
+
+ public static FederationConfiguration instance() {
+ return instance;
+ }
+
+ @Override
+ public void updated(Dictionary<String, ?> props) throws ConfigurationException {
+ if (props == null) {
+ configs.clear();
+ configs.putAll(defaults);
+ } else {
+ try {
+ Enumeration<String> keys = props.keys();
+ while (keys.hasMoreElements()) {
+ String key = keys.nextElement();
+ configs.put(key, (String) props.get(key));
+ }
+ } catch (Throwable t) {
+ throw new ConfigurationException(null, FEDERATION_CONFIG_ERR, t);
+ }
+ }
+ }
+
+ public List<String> httpHeaders() {
+ String headers = configs.get(HTTP_HEADERS);
+ return (headers == null) ? new ArrayList<String>() : Arrays.asList(headers.split(" "));
+ }
+
+ public List<String> httpAttributes() {
+ String attributes = configs.get(HTTP_ATTRIBUTES);
+ return (attributes == null) ? new ArrayList<String>() : Arrays
+ .asList(attributes.split(" "));
+ }
+
+ public Set<Integer> secureProxyPorts() {
+ String ports = configs.get(SECURE_PROXY_PORTS);
+ Set<Integer> secureProxyPorts = new TreeSet<Integer>();
+
+ if (ports != null && !ports.isEmpty()) {
+ for (String port : ports.split(" ")) {
+ secureProxyPorts.add(Integer.parseInt(port));
+ }
+ }
+ return secureProxyPorts;
+ }
+
+}
diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationEndpoint.java b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationEndpoint.java
new file mode 100644
index 00000000..6ac76c0a
--- /dev/null
+++ b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationEndpoint.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.federation;
+
+import static javax.servlet.http.HttpServletResponse.SC_CREATED;
+import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.oltu.oauth2.as.issuer.OAuthIssuer;
+import org.apache.oltu.oauth2.as.issuer.OAuthIssuerImpl;
+import org.apache.oltu.oauth2.as.issuer.UUIDValueGenerator;
+import org.apache.oltu.oauth2.as.response.OAuthASResponse;
+import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
+import org.apache.oltu.oauth2.common.message.OAuthResponse;
+import org.opendaylight.aaa.AuthenticationBuilder;
+import org.opendaylight.aaa.ClaimBuilder;
+import org.opendaylight.aaa.api.Authentication;
+import org.opendaylight.aaa.api.AuthenticationException;
+import org.opendaylight.aaa.api.Claim;
+
+/**
+ * An endpoint for claim-based authentication federation (in-bound).
+ *
+ * @author liemmn
+ *
+ */
+public class FederationEndpoint extends HttpServlet {
+
+ private static final long serialVersionUID = -5553885846238987245L;
+
+ /** An in-bound authentication claim */
+ static final String AUTH_CLAIM = "AAA-CLAIM";
+
+ private static final String UNAUTHORIZED = "unauthorized";
+
+ private transient OAuthIssuer oi;
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+ oi = new OAuthIssuerImpl(new UUIDValueGenerator());
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException,
+ ServletException {
+ try {
+ createRefreshToken(req, resp);
+ } catch (Exception e) {
+ error(resp, SC_UNAUTHORIZED, e.getMessage());
+ }
+ }
+
+ // Create a refresh token
+ private void createRefreshToken(HttpServletRequest req, HttpServletResponse resp)
+ throws OAuthSystemException, IOException {
+ Claim claim = (Claim) req.getAttribute(AUTH_CLAIM);
+ oauthRefreshTokenResponse(resp, claim);
+ }
+
+ // Build OAuth refresh token response from the given claim mapped and
+ // injected by the external IdP
+ private void oauthRefreshTokenResponse(HttpServletResponse resp, Claim claim)
+ throws OAuthSystemException, IOException {
+ if (claim == null) {
+ throw new AuthenticationException(UNAUTHORIZED);
+ }
+
+ String userName = claim.user();
+ // Need to have at least a mapped username!
+ if (userName == null) {
+ throw new AuthenticationException(UNAUTHORIZED);
+ }
+
+ String domain = claim.domain();
+ // Need to have at least a domain!
+ if (domain == null) {
+ throw new AuthenticationException(UNAUTHORIZED);
+ }
+
+ String userId = userName + "@" + domain;
+
+ // Create an unscoped ODL context from the external claim
+ Authentication auth = new AuthenticationBuilder(new ClaimBuilder(claim).setUserId(userId)
+ .build()).setExpiration(tokenExpiration()).build();
+
+ // Create OAuth response
+ String token = oi.refreshToken();
+ OAuthResponse r = OAuthASResponse
+ .tokenResponse(SC_CREATED)
+ .setRefreshToken(token)
+ .setExpiresIn(Long.toString(auth.expiration()))
+ .setScope(
+ // Use mapped domain if there is one, else list
+ // all the ones that this user has access to
+ (claim.domain().isEmpty()) ? listToString(ServiceLocator.getInstance()
+ .getIdmService().listDomains(userId)) : claim.domain())
+ .buildJSONMessage();
+ // Cache this token...
+ ServiceLocator.getInstance().getTokenStore().put(token, auth);
+ write(resp, r);
+ }
+
+ // Token expiration
+ private long tokenExpiration() {
+ return ServiceLocator.getInstance().getTokenStore().tokenExpiration();
+ }
+
+ // Space-delimited string from a list of strings
+ private String listToString(List<String> list) {
+ StringBuffer sb = new StringBuffer();
+ for (String s : list) {
+ sb.append(s).append(" ");
+ }
+ return sb.toString().trim();
+ }
+
+ // Emit an error OAuthResponse with the given HTTP code
+ private void error(HttpServletResponse resp, int httpCode, String error) {
+ try {
+ OAuthResponse r = OAuthResponse.errorResponse(httpCode).setError(error)
+ .buildJSONMessage();
+ write(resp, r);
+ } catch (Exception e1) {
+ // Nothing to do here
+ }
+ }
+
+ // Write out an OAuthResponse
+ private void write(HttpServletResponse resp, OAuthResponse r) throws IOException {
+ resp.setStatus(r.getResponseStatus());
+ PrintWriter pw = resp.getWriter();
+ pw.print(r.getBody());
+ pw.flush();
+ pw.close();
+ }
+}
diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ServiceLocator.java b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ServiceLocator.java
new file mode 100644
index 00000000..dd861514
--- /dev/null
+++ b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ServiceLocator.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.federation;
+
+import java.util.List;
+import java.util.Vector;
+import org.opendaylight.aaa.api.ClaimAuth;
+import org.opendaylight.aaa.api.IdMService;
+import org.opendaylight.aaa.api.TokenStore;
+
+/**
+ * A service locator to bridge between the web world and OSGi world.
+ *
+ * @author liemmn
+ *
+ */
+public class ServiceLocator {
+
+ private static final ServiceLocator instance = new ServiceLocator();
+
+ protected volatile List<ClaimAuth> claimAuthCollection = new Vector<>();
+
+ protected volatile TokenStore tokenStore;
+
+ protected volatile IdMService idmService;
+
+ private ServiceLocator() {
+ }
+
+ public static ServiceLocator getInstance() {
+ return instance;
+ }
+
+ /**
+ * Called through reflection from the federation Activator
+ *
+ * @see org.opendaylight.aaa.federation.ServiceLocator
+ * @param ca the injected claims implementation
+ */
+ protected void claimAuthAdded(ClaimAuth ca) {
+ this.claimAuthCollection.add(ca);
+ }
+
+ /**
+ * Called through reflection from the federation Activator
+ *
+ * @see org.opendaylight.aaa.federation.Activator
+ * @param ca the claims implementation to remove
+ */
+ protected void claimAuthRemoved(ClaimAuth ca) {
+ this.claimAuthCollection.remove(ca);
+ }
+
+ public List<ClaimAuth> getClaimAuthCollection() {
+ return claimAuthCollection;
+ }
+
+ public void setClaimAuthCollection(List<ClaimAuth> claimAuthCollection) {
+ this.claimAuthCollection = claimAuthCollection;
+ }
+
+ public TokenStore getTokenStore() {
+ return tokenStore;
+ }
+
+ public void setTokenStore(TokenStore tokenStore) {
+ this.tokenStore = tokenStore;
+ }
+
+ public IdMService getIdmService() {
+ return idmService;
+ }
+
+ public void setIdmService(IdMService idmService) {
+ this.idmService = idmService;
+ }
+}
diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/SssdFilter.java b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/SssdFilter.java
new file mode 100644
index 00000000..9223c6dd
--- /dev/null
+++ b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/SssdFilter.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2014, 2015 Red Hat, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.federation;
+
+import java.io.IOException;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+
+class SssdHeadersRequest extends HttpServletRequestWrapper {
+ private static final String headerPrefix = "X-SSSD-";
+
+ public SssdHeadersRequest(HttpServletRequest request) {
+ super(request);
+ }
+
+ public Object getAttribute(String name) {
+ HttpServletRequest request = (HttpServletRequest) getRequest();
+ String headerValue;
+
+ headerValue = request.getHeader(headerPrefix + name);
+ if (headerValue != null) {
+ return headerValue;
+ } else {
+ return request.getAttribute(name);
+ }
+ }
+
+ @Override
+ public String getRemoteUser() {
+ HttpServletRequest request = (HttpServletRequest) getRequest();
+ String headerValue;
+
+ headerValue = request.getHeader(headerPrefix + "REMOTE_USER");
+ if (headerValue != null) {
+ return headerValue;
+ } else {
+ return request.getRemoteUser();
+ }
+ }
+
+ @Override
+ public String getAuthType() {
+ HttpServletRequest request = (HttpServletRequest) getRequest();
+ String headerValue;
+
+ headerValue = request.getHeader(headerPrefix + "AUTH_TYPE");
+ if (headerValue != null) {
+ return headerValue;
+ } else {
+ return request.getAuthType();
+ }
+ }
+
+ @Override
+ public String getRemoteAddr() {
+ HttpServletRequest request = (HttpServletRequest) getRequest();
+ String headerValue;
+
+ headerValue = request.getHeader(headerPrefix + "REMOTE_ADDR");
+ if (headerValue != null) {
+ return headerValue;
+ } else {
+ return request.getRemoteAddr();
+ }
+ }
+
+ @Override
+ public String getRemoteHost() {
+ HttpServletRequest request = (HttpServletRequest) getRequest();
+ String headerValue;
+
+ headerValue = request.getHeader(headerPrefix + "REMOTE_HOST");
+ if (headerValue != null) {
+ return headerValue;
+ } else {
+ return request.getRemoteHost();
+ }
+ }
+
+ @Override
+ public int getRemotePort() {
+ HttpServletRequest request = (HttpServletRequest) getRequest();
+ String headerValue;
+
+ headerValue = request.getHeader(headerPrefix + "REMOTE_PORT");
+ if (headerValue != null) {
+ return Integer.parseInt(headerValue);
+ } else {
+ return request.getRemotePort();
+ }
+ }
+
+}
+
+/**
+ * Populate HttpRequestServlet API data from HTTP extension headers.
+ *
+ * When SSSD is used for authentication and identity lookup those actions occur
+ * in an Apache HTTP server which is fronting the servlet container. After
+ * successful authentication Apache will proxy the request to the container
+ * along with additional authentication and identity metadata.
+ *
+ * The preferred way to transport the metadata and have it appear seamlessly in
+ * the servlet API is via the AJP protocol. However AJP may not be available or
+ * desirable. An alternative method is to transport the metadata in extension
+ * HTTP headers. However we still want the standard servlet request API methods
+ * to work. Another way to say this is we do not want upper layers to be aware
+ * of the transport mechanism. To achieve this we wrap the HttpServletRequest
+ * class and override specific methods which need to extract the data from the
+ * extension HTTP headers. (This is roughly equivalent to what happens when AJP
+ * is implemented natively in the container).
+ *
+ * The extension HTTP headers are identified by the prefix "X-SSSD-". The
+ * overridden methods check for the existence of the appropriate extension
+ * header and if present returns the value found in the extension header,
+ * otherwise it returns the value from the method it's wrapping.
+ *
+ */
+public class SssdFilter implements Filter {
+ @Override
+ public void init(FilterConfig fc) throws ServletException {
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ @Override
+ public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
+ FilterChain filterChain) throws IOException, ServletException {
+ if (servletRequest instanceof HttpServletRequest) {
+ HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
+ SssdHeadersRequest request = new SssdHeadersRequest(httpServletRequest);
+ filterChain.doFilter(request, servletResponse);
+ } else {
+ filterChain.doFilter(servletRequest, servletResponse);
+ }
+ }
+}
diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.properties b/odl-aaa-moon/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.properties
new file mode 100644
index 00000000..4323c04d
--- /dev/null
+++ b/odl-aaa-moon/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.properties
@@ -0,0 +1,11 @@
+org.opendaylight.aaa.federation.name = Opendaylight AAA Federation Configuration
+org.opendaylight.aaa.federation.description = Configuration for AAA federation
+org.opendaylight.aaa.federation.httpHeaders.name = Custom HTTP Headers
+org.opendaylight.aaa.federation.httpHeaders.description = Space-delimited list of \
+specific HTTP headers to capture for authentication federation.
+org.opendaylight.aaa.federation.httpAttributes.name = Custom HTTP Attributes
+org.opendaylight.aaa.federation.httpAttributes.description = Space-delimited list of \
+specific HTTP attributes to capture for authentication federation.
+org.opendaylight.aaa.federation.secureProxyPorts.name = Secure Proxy Ports
+org.opendaylight.aaa.federation.secureProxyPorts.description = Space-delimited list of \
+port numbers on which a trusted HTTP proxy performing authentication forwards pre-authenticated requests.
diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.xml b/odl-aaa-moon/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.xml
new file mode 100644
index 00000000..e2efd3d4
--- /dev/null
+++ b/odl-aaa-moon/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<metatype:MetaData xmlns:metatype="http://www.osgi.org/xmlns/metatype/v1.0.0"
+ localization="OSGI-INF/metatype/metatype">
+ <OCD id="org.opendaylight.aaa.federation" name="%org.opendaylight.aaa.federation.name"
+ description="%org.opendaylight.aaa.federation.description">
+ <AD id="httpHeaders" type="String" default=""
+ name="%org.opendaylight.aaa.federation.httpHeaders.name"
+ description="%org.opendaylight.aaa.federation.httpHeaders.description" />
+ <AD id="httpAttributes" type="String" default=""
+ name="%org.opendaylight.aaa.federation.httpAttributes.name"
+ description="%org.opendaylight.aaa.federation.httpAttributes.description" />
+ <AD id="secureProxyPorts" type="String" default=""
+ name="%org.opendaylight.aaa.federation.secureProxyPorts.name"
+ description="%org.opendaylight.aaa.federation.secureProxyPorts.description" />
+ </OCD>
+ <Designate pid="org.opendaylight.aaa.federation">
+ <Object ocdref="org.opendaylight.aaa.federation" />
+ </Designate>
+</metatype:MetaData>
diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/resources/WEB-INF/web.xml b/odl-aaa-moon/aaa-authn-federation/src/main/resources/WEB-INF/web.xml
new file mode 100644
index 00000000..9fd9751f
--- /dev/null
+++ b/odl-aaa-moon/aaa-authn-federation/src/main/resources/WEB-INF/web.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<web-app 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_3_0.xsd"
+ version="3.0">
+
+ <servlet>
+ <servlet-name>federation</servlet-name>
+ <servlet-class>org.opendaylight.aaa.federation.FederationEndpoint</servlet-class>
+ <load-on-startup>1</load-on-startup>
+ </servlet>
+ <servlet-mapping>
+ <servlet-name>federation</servlet-name>
+ <url-pattern>/*</url-pattern>
+ </servlet-mapping>
+
+ <!-- Federation Auth filter -->
+ <filter>
+ <filter-name>SssdFilter</filter-name>
+ <filter-class>org.opendaylight.aaa.federation.SssdFilter</filter-class>
+ </filter>
+ <filter-mapping>
+ <filter-name>SssdFilter</filter-name>
+ <url-pattern>/*</url-pattern>
+ </filter-mapping>
+ <filter>
+ <filter-name>ClaimAuthFilter</filter-name>
+ <filter-class>org.opendaylight.aaa.federation.ClaimAuthFilter</filter-class>
+ </filter>
+ <filter-mapping>
+ <filter-name>ClaimAuthFilter</filter-name>
+ <url-pattern>/*</url-pattern>
+ </filter-mapping>
+
+</web-app>
diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/resources/federation.cfg b/odl-aaa-moon/aaa-authn-federation/src/main/resources/federation.cfg
new file mode 100644
index 00000000..60ef1c46
--- /dev/null
+++ b/odl-aaa-moon/aaa-authn-federation/src/main/resources/federation.cfg
@@ -0,0 +1,3 @@
+httpHeaders=
+httpAttributes=
+secureProxyPorts=
diff --git a/odl-aaa-moon/aaa-authn-federation/src/test/java/org/opendaylight/aaa/federation/FederationEndpointTest.java b/odl-aaa-moon/aaa-authn-federation/src/test/java/org/opendaylight/aaa/federation/FederationEndpointTest.java
new file mode 100644
index 00000000..ae098652
--- /dev/null
+++ b/odl-aaa-moon/aaa-authn-federation/src/test/java/org/opendaylight/aaa/federation/FederationEndpointTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.federation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyMap;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.TreeSet;
+import org.eclipse.jetty.testing.HttpTester;
+import org.eclipse.jetty.testing.ServletTester;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.opendaylight.aaa.ClaimBuilder;
+import org.opendaylight.aaa.api.Claim;
+import org.opendaylight.aaa.api.ClaimAuth;
+import org.opendaylight.aaa.api.IdMService;
+import org.opendaylight.aaa.api.TokenStore;
+
+/**
+ * A unit test for federation endpoint.
+ *
+ * @author liemmn
+ *
+ */
+public class FederationEndpointTest {
+ private static final long TOKEN_TIMEOUT_SECS = 10;
+ private static final String CONTEXT = "/oauth2/federation";
+
+ private final static ServletTester server = new ServletTester();
+ private static final Claim claim = new ClaimBuilder().setUser("bob").setUserId("1234")
+ .addRole("admin").build();
+
+ @BeforeClass
+ public static void init() throws Exception {
+ // Set up server
+ server.setContextPath(CONTEXT);
+
+ // Add our servlet under test
+ server.addServlet(FederationEndpoint.class, "/*");
+
+ // Add ClaimAuth filter
+ server.addFilter(ClaimAuthFilter.class, "/*", 0);
+
+ // Let's do dis
+ server.start();
+ }
+
+ @AfterClass
+ public static void shutdown() throws Exception {
+ server.stop();
+ }
+
+ @Before
+ public void setup() {
+ mockServiceLocator();
+ when(ServiceLocator.getInstance().getTokenStore().tokenExpiration()).thenReturn(
+ TOKEN_TIMEOUT_SECS);
+ }
+
+ @After
+ public void teardown() {
+ ServiceLocator.getInstance().getClaimAuthCollection().clear();
+ }
+
+ @Test
+ public void testFederationUnconfiguredProxyPort() throws Exception {
+ HttpTester req = new HttpTester();
+ req.setMethod("POST");
+ req.setURI(CONTEXT + "/");
+ req.setVersion("HTTP/1.0");
+
+ HttpTester resp = new HttpTester();
+ resp.parse(server.getResponses(req.generate()));
+ assertEquals(401, resp.getStatus());
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testFederation() throws Exception {
+ when(ServiceLocator.getInstance().getClaimAuthCollection().get(0).transform(anyMap()))
+ .thenReturn(claim);
+ when(ServiceLocator.getInstance().getIdmService().listDomains(anyString())).thenReturn(
+ Arrays.asList("pepsi", "coke"));
+
+ // Configure secure port (of zero)
+ FederationConfiguration.instance = mock(FederationConfiguration.class);
+ when(FederationConfiguration.instance.secureProxyPorts()).thenReturn(
+ new TreeSet<Integer>(Arrays.asList(0)));
+
+ HttpTester req = new HttpTester();
+ req.setMethod("POST");
+ req.setURI(CONTEXT + "/");
+ req.setVersion("HTTP/1.0");
+
+ HttpTester resp = new HttpTester();
+ resp.parse(server.getResponses(req.generate()));
+ assertEquals(201, resp.getStatus());
+ String content = resp.getContent();
+ assertTrue(content.contains("pepsi coke"));
+ }
+
+ private static void mockServiceLocator() {
+ ServiceLocator.getInstance().setIdmService(mock(IdMService.class));
+ ServiceLocator.getInstance().setTokenStore(mock(TokenStore.class));
+ ServiceLocator.getInstance().getClaimAuthCollection().add(mock(ClaimAuth.class));
+ }
+}