Initial import.
diff --git a/.gitignore b/.gitignore
index 32858aa..900bd4d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,6 @@
 
 # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
 hs_err_pid*
+target
+project/target
+.idea
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5e9061d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,130 @@
+# Gerrit SAML Plugin
+
+This plugin allows you to authenticate to Gerrit using a SAML identity
+provider.
+
+## Installation
+
+Gerrit looks for 3 attributes (which are configurable) in the AttributeStatement:
+
+*DisplayName:* the full name of the user.
+*EmailAddress:* email address of the user.
+*UserName:* username (used for ssh).
+
+If any of these attributes is not found in the assertion, their value is
+taken from the NameId field of the SAML assertion.
+
+### Setting Gerrit in your IdP (Okta, Onelogin, ...)
+
+- Create a new SAML 2.0 application.
+- Set the following parameters:
+  - Single sign on URL: http://gerrit.site.com/plugins/gerrit-saml-plugin/saml
+  - Check "Use this for Recipient URL and Destination URL".
+  - Audience URI (SP Entity Id): http://gerrit.site.com/plugins/gerrit-saml-plugin/saml
+  - We need to set up the attributes in the assertion to send the right
+    information. Here is how to do it with Okta:
+    - Application username: "Okta username prefix"
+    - Add attribute statement: Name: "DisplayName" with Value
+      "user.displayName"
+    - Add attribute statement: Name: "EmailAddress" with Value
+      "user.email"
+    - *IMPORTANT*: If you are not using Okta, you need to set up an attribute
+      "UserName" with the value of the username (not email, without @). If you
+      do not do so, the name will be taken from the NameId provided by
+      the assertion.  This is why in Okta we set the application username to
+      "Okta username prefix".
+- Obtain your IdP metadata (either URL or a local XML file)
+
+### Download the plugin
+
+Download [gerrit-saml-plugin](https://bintray.com/artifact/download/thesamet/maven/gerrit-saml-plugin-2.11.3-1.jar) and put it in $gerrit_site/lib/.
+
+### Configure Gerrit to use the SAML filter:
+In `$gerritSite/etc/gerrit.config` file, the `[httpd]` section should contain
+
+```
+[httpd]
+    filterClass = com.thesamet.gerrit.plugins.saml.SamlWebFilter
+```
+
+### Configure HTTP authentication for Gerrit:
+
+In `$gerritSite/etc/gerrit.config` file, the `[auth]` section should include
+the following lines:
+
+```
+[auth]
+	type = HTTP
+    logoutUrl = https://mysso.example.com/logout
+    httpHeader = X-SAML-UserName
+    httpDisplaynameHeader = X-SAML-DisplayName
+    httpEmailHeader = X-SAML-EmailHeader
+    httpExternalIdHeader = X-SAML-ExternalId
+```
+
+The header names are used internally between the SAML plugin and Gerrit to
+communicate the user's identity.  You can use other names (as long as it will
+not conflict with any other HTTP header Gerrit might expect).
+
+### Create a local keystore
+
+In `$gerrit_site/etc` create a local keystore:
+
+```
+keytool -genkeypair -alias pac4j -keypass pac4j-demo-password \
+  -keystore samlKeystore.jks \
+  -storepass pac4j-demo-password -keyalg RSA -keysize 2048 -validity 3650
+```
+
+### Configure SAML
+
+Add a new `[saml]` section to `$site_path/etc/gerrit.config`:
+
+```
+[saml]
+    keystorePath = /path/to/samlKeystore.jks
+    keystorePassword = pac4j-demo-passwd
+    privateKeyPassword = pac4j-demo-passwd
+    metadataPath = https://mycompany.okta.com/app/hashash/sso/saml/metadata
+```
+
+**saml.metadataPath**: Location of IdP Metadata from your SAML identity provider.
+The value can be a URL, or a local file (prefix with `file://`)
+
+**saml.keystorePath**: Path to the keystore created above. If not absolute,
+the path is resolved relative to `$site_path`.
+
+**saml.privateKeyPassword**: Password protecting the private key of the generated
+key pair (needs to be the same as the password provided throguh the `keypass`
+flag above.)
+
+**saml.keystorePassword**: Password that is used to protect the integrity of the
+keystore (needs to be the same as the password provided throguh the `keystore`
+flag above.)
+
+**saml.displayNameAttr**: Gerrit will look for an attribute with this name in
+the assertion to find a display name for the user. If the attribute is not
+found, the NameId from the SAML assertion is used instead.
+
+Default is `DisplayName`
+
+**saml.emailAddressAttr**: Gerrit will look for an attribute with this name in
+the assertion to find a the email address of the user. If the attribute is not
+found, the NameId from the SAML assertion is used instead.
+
+Default is `EmailAddress`
+
+**saml.userNameAttr**: Gerrit will look for an attribute with this name in the
+assertion to find a the email address of the user. If the attribute is not
+found, the NameId from the SAML assertion is used instead.
+
+Default is `UserName`
+
+## Development
+
+- Clone this repository.
+- Install sbt.
+- Edit the code.
+- Run 'sbt assembly' to build the jar.
+- Copy target/out/gerrit-saml-plugin-$VERSION.jar into $site_path/lib/
+
diff --git a/build.sbt b/build.sbt
new file mode 100644
index 0000000..1d1f8b7
--- /dev/null
+++ b/build.sbt
@@ -0,0 +1,50 @@
+name := "gerrit-saml-plugin"
+
+val GerritVersion = "2.11.3"
+
+version := GerritVersion + "-1"
+
+javacOptions ++= Seq("-source", "1.7", "-target", "1.7")
+
+libraryDependencies += ("com.google.gerrit" % "gerrit-plugin-api" % GerritVersion % "provided")
+
+libraryDependencies += "org.pac4j" % "pac4j-saml" % "1.8.0-RC1"
+
+libraryDependencies ~= { _ map {
+  case m => m
+    .exclude("ch.qos.logback", "logback-classic")
+    .exclude("ch.qos.logback", "logback-core")
+    .exclude("com.google.guava", "guava")
+    .exclude("commons-codec", "commons-codec")
+    .exclude("commons-collections", "commons-collections")
+    .exclude("commons-httpclient", "commons-httpclient")
+    .exclude("commons-lang", "commons-lang")
+    .exclude("commons-logging", "commons-logging")
+    .exclude("commons-ssl", "commons-ssl")
+    .exclude("javax.servlet", "servlet-api")
+    .exclude("javax.xml", "*")
+    .exclude("junit", "*")
+    .exclude("org.apache.httpcomponents", "*")
+    .exclude("org.apache.velocity", "*")
+    .exclude("org.bouncycastle", "*")
+    .exclude("org.slf4j", "*")
+    .exclude("xalan", "xalan")
+}}
+
+assemblyMergeStrategy in assembly := {
+  case PathList("META-INF", "INDEX.LIST") => MergeStrategy.discard
+  case PathList("META-INF", "MANIFEST.MF") => MergeStrategy.discard
+  case PathList("META-INF", "NOTICE") => MergeStrategy.discard
+  case PathList("META-INF", "LICENSE") => MergeStrategy.concat
+  // Trick is here: get all the initializers concatenated...
+  case PathList("META-INF", "services", "org.opensaml.core.config.Initializer") => MergeStrategy.concat
+  case PathList("schema", v) if v.endsWith(".xsd") => MergeStrategy.first
+  case PathList("credential-criteria-registry.properties") => MergeStrategy.first
+  case PathList(xml) if xml.endsWith(".xml") => MergeStrategy.first
+  case _ => MergeStrategy.deduplicate
+}
+
+assemblyOption in assembly := (assemblyOption in assembly).value.copy(includeScala = false)
+
+assemblyOutputPath in assembly := target.value / "out" / (s"${name.value}-${version.value}.jar")
+
diff --git a/project/assembly.sbt b/project/assembly.sbt
new file mode 100644
index 0000000..a815d58
--- /dev/null
+++ b/project/assembly.sbt
@@ -0,0 +1 @@
+addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.0")
diff --git a/src/main/java/com/thesamet/gerrit/plugins/saml/AuthenticatedUser.java b/src/main/java/com/thesamet/gerrit/plugins/saml/AuthenticatedUser.java
new file mode 100644
index 0000000..432e36f
--- /dev/null
+++ b/src/main/java/com/thesamet/gerrit/plugins/saml/AuthenticatedUser.java
@@ -0,0 +1,41 @@
+package com.thesamet.gerrit.plugins.saml;
+
+public class AuthenticatedUser implements java.io.Serializable {
+    private String username;
+    private String displayName;
+    private String email;
+    private String externalId;
+
+    public AuthenticatedUser(String username, String displayName, String email, String extenalId) {
+        this.username = username;
+        this.displayName = displayName;
+        this.email = email;
+        this.externalId = extenalId;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public String getExternalId() {
+        return externalId;
+    }
+
+    @Override
+    public String toString() {
+        return "AuthenticatedUser{" +
+                "username='" + username + '\'' +
+                ", displayName='" + displayName + '\'' +
+                ", email='" + email + '\'' +
+                ", externalId='" + externalId + '\'' +
+                '}';
+    }
+}
diff --git a/src/main/java/com/thesamet/gerrit/plugins/saml/SamlConfig.java b/src/main/java/com/thesamet/gerrit/plugins/saml/SamlConfig.java
new file mode 100644
index 0000000..ffca75f
--- /dev/null
+++ b/src/main/java/com/thesamet/gerrit/plugins/saml/SamlConfig.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.thesamet.gerrit.plugins.saml;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * SAML 2.0 related settings from {@code gerrit.config}.
+ */
+@Singleton
+public class SamlConfig {
+  private final String metadataPath;
+  private final String keystorePath;
+  private final String privateKeyPassword;
+  private final String keystorePassword;
+  private final String displayNameAttr;
+  private final String userNameAttr;
+  private final String emailAddressAttr;
+
+  @Inject
+  SamlConfig(@GerritServerConfig final Config cfg) {
+    metadataPath = getString(cfg, "metadataPath");
+    keystorePath = getString(cfg, "keystorePath");
+    privateKeyPassword = getString(cfg, "privateKeyPassword");
+    keystorePassword = getString(cfg, "keystorePassword");
+    displayNameAttr =
+        getGetStringWithDefault(cfg, "displayNameAttr", "DisplayName");
+    userNameAttr = getGetStringWithDefault(cfg, "userNameAttr", "UserName");
+    emailAddressAttr =
+        getGetStringWithDefault(cfg, "emailAddressAttr", "EmailAddress");
+  }
+
+  public String getMetadataPath() {
+    return metadataPath;
+  }
+
+  public String getKeystorePath() {
+    return keystorePath;
+  }
+
+  public String getPrivateKeyPassword() {
+    return privateKeyPassword;
+  }
+
+  public String getKeystorePassword() {
+    return keystorePassword;
+  }
+
+  public String getDisplayNameAttr() {
+    return displayNameAttr;
+  }
+
+  public String getUserNameAttr() {
+    return userNameAttr;
+  }
+
+  public String getEmailAddressAttr() {
+    return emailAddressAttr;
+  }
+
+  private static String getString(Config cfg, String name) {
+    return cfg.getString("saml", null, name);
+  }
+
+  private static String getGetStringWithDefault(Config cfg, String name,
+      String defaultValue) {
+    String result = getString(cfg, name);
+    if (result != null) {
+      return result;
+    }
+    return defaultValue;
+  }
+}
diff --git a/src/main/java/com/thesamet/gerrit/plugins/saml/SamlWebFilter.java b/src/main/java/com/thesamet/gerrit/plugins/saml/SamlWebFilter.java
new file mode 100644
index 0000000..b093e06
--- /dev/null
+++ b/src/main/java/com/thesamet/gerrit/plugins/saml/SamlWebFilter.java
@@ -0,0 +1,278 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.thesamet.gerrit.plugins.saml;
+
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Singleton;
+import org.eclipse.jgit.lib.Config;
+import org.pac4j.core.context.J2EContext;
+import org.pac4j.core.exception.RequiresHttpAction;
+import org.pac4j.core.exception.TechnicalException;
+import org.pac4j.saml.client.SAML2Client;
+import org.pac4j.saml.client.SAML2ClientConfiguration;
+import org.pac4j.saml.credentials.SAML2Credentials;
+import org.pac4j.saml.profile.SAML2Profile;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+
+@Singleton
+class SamlWebFilter implements Filter {
+    static final String GERRIT_LOGOUT = "/logout";
+    static final String SAML_POSTBACK = "/plugins/gerrit-saml-plugin/saml";
+
+    private static final String SESSION_ATTR_USER = "Gerrit-Saml-User";
+
+    static final Logger log = LoggerFactory.getLogger(SamlWebFilter.class);
+    private final Injector injector;
+    private final SAML2Client saml2Client;
+    private final SamlConfig samlConfig;
+    private final String httpUserNameHeader;
+    private final String httpDisplaynameHeader;
+    private final String httpEmailHeader;
+    private final String httpExternalIdHeader;
+    private final HashSet<String> authHeaders;
+    private final String logoutUrl;
+
+    private String getHeaderFromConfig(Config gerritConfig, String name) {
+        String s = gerritConfig.getString("auth", null, name);
+        return s == null ? "" : s.toUpperCase();
+    }
+
+    @Inject
+    SamlWebFilter(Injector injector, @GerritServerConfig Config gerritConfig, SamlConfig samlConfig) {
+        this.injector = injector;
+        this.samlConfig = samlConfig;
+        saml2Client =
+                new SAML2Client(new SAML2ClientConfiguration(
+                        samlConfig.getKeystorePath(), samlConfig.getKeystorePassword(),
+                        samlConfig.getPrivateKeyPassword(), samlConfig.getMetadataPath()));
+        String callbackUrl = gerritConfig.getString("gerrit", null, "canonicalWebUrl") + "plugins/gerrit-saml-plugin/saml";
+        httpUserNameHeader = getHeaderFromConfig(gerritConfig, "httpHeader");
+        httpDisplaynameHeader = getHeaderFromConfig(gerritConfig, "httpDisplaynameHeader");
+        httpEmailHeader = getHeaderFromConfig(gerritConfig, "httpEmailHeader");
+        httpExternalIdHeader = getHeaderFromConfig(gerritConfig, "httpExternalIdHeader");
+        authHeaders = Sets.newHashSet(
+                httpUserNameHeader,
+                httpDisplaynameHeader,
+                httpEmailHeader,
+                httpExternalIdHeader);
+        if (authHeaders.contains("") || authHeaders.contains(null)) {
+            throw new RuntimeException("All authentication headers must be set.");
+        }
+        if (authHeaders.size() != 4) {
+            throw new RuntimeException("Unique values for httpUserNameHeader, " +
+                    "httpDisplaynameHeader, httpEmailHeader and httpExternalIdHeader " +
+                    "are required.");
+        }
+        logoutUrl = gerritConfig.getString("auth", null, "logoutUrl");
+
+        saml2Client.setCallbackUrl(callbackUrl);
+    }
+
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+    private AuthenticatedUser userFromRequest(HttpServletRequest request) {
+        HttpSession s = request.getSession();
+        AuthenticatedUser user = (AuthenticatedUser) s.getAttribute(SESSION_ATTR_USER);
+        if (user == null || user.getUsername() == null)
+            return null;
+        else return user;
+    }
+
+    private void signin(J2EContext context) throws RequiresHttpAction, IOException {
+        SAML2Credentials credentials = saml2Client.getCredentials(context);
+        SAML2Profile user = saml2Client.getUserProfile(credentials, context);
+        if (user != null) {
+            log.debug("Received SAML callback for userId={} with attributes: {}",
+                    getUserName(user), user.getAttributes());
+            HttpSession s = context.getRequest().getSession();
+            s.setAttribute(SESSION_ATTR_USER, new AuthenticatedUser(
+                    getUserName(user),
+                    getDisplayName(user),
+                    getEmailAddress(user),
+                    "saml/" + user.getId()));
+
+            String redirectUri = context.getRequest().getParameter("RelayState");
+            log.debug("Got {}", redirectUri);
+            if (null == redirectUri) {
+                redirectUri = "/";
+            }
+            context.getResponse().sendRedirect(redirectUri);
+        } else {
+            signout(context.getRequest(), context.getResponse());
+        }
+    }
+
+    private void signout(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        HttpSession s = request.getSession();
+        s.removeAttribute(SESSION_ATTR_USER);
+        response.sendRedirect(logoutUrl);
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response,
+                         FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+        AuthenticatedUser user = userFromRequest(httpRequest);
+
+        try {
+            if (isSamlPostback(httpRequest)) {
+                J2EContext context = new J2EContext(httpRequest, httpResponse);
+                signin(context);
+            } else if (isGerritLogout(httpRequest)) {
+                signout(httpRequest, httpResponse);
+            } else if (isAllowedWithoutAuth(httpRequest)) {
+                // We allow URLs to continue without a user (and hence without the authentication
+                // headers.  It is up for Gerrit to approve or deny these requests.  We do it
+                // specifically for favicon.ico:  it could be that during a normal authentication
+                // redirect, the browser will try to fetch /favicon.ico which will start a
+                // parallel authentication process, but it will override the redirectUri and the
+                // user will be redirected to /favicon.ico.  This would have been eliminated
+                // if pac4j would allow obtaining RelayState...
+                chain.doFilter(httpRequest, response);
+            } else if (user == null) {
+                J2EContext context = new J2EContext(httpRequest, httpResponse);
+                redirectToIdentityProvider(context);
+            } else {
+                HttpServletRequest req = new AuthenticatedHttpRequest(httpRequest, user);
+                chain.doFilter(req, response);
+            }
+        } catch (final RequiresHttpAction requiresHttpAction) {
+            throw new TechnicalException("Unexpected HTTP action", requiresHttpAction);
+        }
+    }
+
+    private boolean isAllowedWithoutAuth(HttpServletRequest httpRequest) {
+        return (httpRequest.getRequestURI().equals("/favicon.ico"));
+    }
+
+    private void redirectToIdentityProvider(J2EContext context)
+            throws RequiresHttpAction {
+        String redirectUri = Url.decode(context
+                .getRequest()
+                .getRequestURI()
+                .substring(
+                        context.getRequest().getContextPath().length()));
+        context.setSessionAttribute(SAML2Client.SAML_RELAY_STATE_ATTRIBUTE, redirectUri);
+        log.debug("Setting redirectUri: {}", redirectUri);
+        saml2Client.redirect(context, true);
+    }
+
+    private static boolean isGerritLogout(HttpServletRequest request) {
+        return request.getRequestURI().indexOf(GERRIT_LOGOUT) >= 0;
+    }
+
+    private static boolean isSamlPostback(HttpServletRequest request) {
+        return "POST".equals(request.getMethod())
+                && request.getRequestURI().indexOf(SAML_POSTBACK) >= 0;
+    }
+
+    private String getAttribute(SAML2Profile user, String attrName) {
+        List<?> names = (List<?>) user.getAttribute(attrName);
+        if (names != null && !names.isEmpty()) {
+            return (String) names.get(0);
+        }
+        return null;
+    }
+
+    private String getAttributeOrElseId(SAML2Profile user, String attrName) {
+        String value = getAttribute(user, attrName);
+        if (value != null) {
+            return value;
+        }
+        return user.getId();
+    }
+
+    private String getDisplayName(SAML2Profile user) {
+        return getAttributeOrElseId(user, samlConfig.getDisplayNameAttr());
+    }
+
+    private String getEmailAddress(SAML2Profile user) {
+        String emailAddress = getAttribute(user, samlConfig.getEmailAddressAttr());
+        if (emailAddress != null) {
+            return emailAddress;
+        }
+        String nameId = user.getId();
+        if (!nameId.contains("@")) {
+            log.debug(
+                    "Email address attribute not found, NameId {} does not look like an email.",
+                    nameId);
+            return null;
+        }
+        return emailAddress;
+    }
+
+    private String getUserName(SAML2Profile user) {
+        return getAttributeOrElseId(user, samlConfig.getUserNameAttr());
+    }
+
+    private class AuthenticatedHttpRequest extends HttpServletRequestWrapper {
+        private AuthenticatedUser user;
+
+        public AuthenticatedHttpRequest(HttpServletRequest request,
+                                        AuthenticatedUser user) {
+            super(request);
+            this.user = user;
+        }
+
+        @Override
+        public Enumeration<String> getHeaderNames() {
+            final Enumeration<String> wrappedHeaderNames = super.getHeaderNames();
+            HashSet<String> headerNames = new HashSet<>(authHeaders);
+            while (wrappedHeaderNames.hasMoreElements()) {
+                headerNames.add(wrappedHeaderNames.nextElement());
+            }
+            return Iterators.asEnumeration(headerNames.iterator());
+        }
+
+        @Override
+        public String getHeader(String name) {
+            String nameUpperCase = name.toUpperCase();
+            if (httpUserNameHeader.equals(nameUpperCase)) {
+                return user.getUsername();
+            } else if (httpDisplaynameHeader.equals(nameUpperCase)) {
+                return user.getDisplayName();
+            } else if (httpEmailHeader.equals(nameUpperCase)) {
+                return user.getEmail();
+            } else if (httpExternalIdHeader.equals(nameUpperCase)) {
+                return user.getExternalId();
+            } else {
+                return super.getHeader(name);
+            }
+        }
+    }
+}