Explorar el Código

#582 OpenID Connect support

Shinsuke Sugaya hace 9 años
padre
commit
d44e76d0d1

+ 21 - 0
pom.xml

@@ -1052,6 +1052,27 @@
 		</dependency>
 		</dependency>
 
 
 		<!-- common library -->
 		<!-- common library -->
+		<dependency>
+			<groupId>com.google.guava</groupId>
+			<artifactId>guava</artifactId>
+			<version>19.0</version>
+		</dependency>
+		<dependency>
+			<groupId>com.google.oauth-client</groupId>
+			<artifactId>google-oauth-client</artifactId>
+			<version>1.22.0</version>
+			<exclusions>
+				<exclusion>
+					<groupId>org.apache.httpcomponents</groupId>
+					<artifactId>httpclient</artifactId>
+				</exclusion>
+			</exclusions>
+		</dependency>
+		<dependency>
+			<groupId>com.google.http-client</groupId>
+			<artifactId>google-http-client-jackson2</artifactId>
+			<version>1.22.0</version>
+		</dependency>
 		<dependency>
 		<dependency>
 			<groupId>org.codelibs</groupId>
 			<groupId>org.codelibs</groupId>
 			<artifactId>corelib</artifactId>
 			<artifactId>corelib</artifactId>

+ 15 - 2
src/main/java/org/codelibs/fess/app/web/base/login/EmptyLoginCredential.java → src/main/java/org/codelibs/fess/app/web/base/login/ActionLoginCredential.java

@@ -16,8 +16,18 @@
 package org.codelibs.fess.app.web.base.login;
 package org.codelibs.fess.app.web.base.login;
 
 
 import java.util.Collections;
 import java.util.Collections;
+import java.util.function.Function;
 
 
-public class EmptyLoginCredential implements LoginCredential {
+import org.codelibs.fess.app.web.sso.SsoAction;
+import org.lastaflute.web.response.ActionResponse;
+
+public class ActionLoginCredential implements LoginCredential {
+
+    private final Function<SsoAction, ActionResponse> action;
+
+    public ActionLoginCredential(final Function<SsoAction, ActionResponse> action) {
+        this.action = action;
+    }
 
 
     @Override
     @Override
     public void validate() {
     public void validate() {
@@ -25,7 +35,7 @@ public class EmptyLoginCredential implements LoginCredential {
 
 
     @Override
     @Override
     public String getId() {
     public String getId() {
-        return "empty";
+        return action.toString();
     }
     }
 
 
     @Override
     @Override
@@ -33,4 +43,7 @@ public class EmptyLoginCredential implements LoginCredential {
         return Collections.emptyMap();
         return Collections.emptyMap();
     }
     }
 
 
+    public ActionResponse execute(final SsoAction a) {
+        return action.apply(a);
+    }
 }
 }

+ 4 - 2
src/main/java/org/codelibs/fess/app/web/base/login/FessLoginAssist.java

@@ -175,11 +175,13 @@ public class FessLoginAssist extends TypicalLoginAssist<String, FessUserBean, Fe
                 }
                 }
             }
             }
             return doFindLoginUser(username, encryptPassword(password));
             return doFindLoginUser(username, encryptPassword(password));
-        } else if (credential instanceof SsoLoginCredential) {
-            final String username = ((SsoLoginCredential) credential).getUsername();
+        } else if (credential instanceof SpnegoLoginCredential) {
+            final String username = credential.getId();
             if (!fessConfig.isAdminUser(username)) {
             if (!fessConfig.isAdminUser(username)) {
                 return ComponentUtil.getLdapManager().login(username);
                 return ComponentUtil.getLdapManager().login(username);
             }
             }
+        } else if (credential instanceof OpenIdConnectLoginCredential) {
+            return OptionalEntity.of(((OpenIdConnectLoginCredential) credential).getUser());
         }
         }
         return OptionalEntity.empty();
         return OptionalEntity.empty();
     }
     }

+ 82 - 0
src/main/java/org/codelibs/fess/app/web/base/login/OpenIdConnectLoginCredential.java

@@ -0,0 +1,82 @@
+/*
+ * Copyright 2012-2016 CodeLibs Project and the Others.
+ *
+ * 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 org.codelibs.fess.app.web.base.login;
+
+import java.util.Map;
+
+import org.codelibs.fess.entity.FessUser;
+
+public class OpenIdConnectLoginCredential implements LoginCredential {
+
+    private final Map<String, Object> attributes;
+
+    public OpenIdConnectLoginCredential(final Map<String, Object> attributes) {
+        this.attributes = attributes;
+    }
+
+    @Override
+    public void validate() {
+        assertLoginAccountRequired((String) attributes.get("email"));
+    }
+
+    @Override
+    public String getId() {
+        return (String) attributes.get("email");
+    }
+
+    @Override
+    public Object getResource() {
+        return attributes;
+    }
+
+    public User getUser() {
+        return new User(getId());
+    }
+
+    public static class User implements FessUser {
+
+        private final String name;
+
+        protected User(final String name) {
+            this.name = name;
+            // TODO groups and roles
+        }
+
+        @Override
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String[] getRoleNames() {
+            // TODO Auto-generated method stub
+            return null;
+        }
+
+        @Override
+        public String[] getGroupNames() {
+            // TODO Auto-generated method stub
+            return null;
+        }
+
+        @Override
+        public String[] getPermissions() {
+            // TODO Auto-generated method stub
+            return null;
+        }
+
+    }
+}

+ 2 - 2
src/main/java/org/codelibs/fess/app/web/base/login/SsoLoginCredential.java → src/main/java/org/codelibs/fess/app/web/base/login/SpnegoLoginCredential.java

@@ -17,12 +17,12 @@ package org.codelibs.fess.app.web.base.login;
 
 
 import org.dbflute.util.DfCollectionUtil;
 import org.dbflute.util.DfCollectionUtil;
 
 
-public class SsoLoginCredential implements LoginCredential {
+public class SpnegoLoginCredential implements LoginCredential {
     private final String username;
     private final String username;
 
 
     // private Principal principal;
     // private Principal principal;
 
 
-    public SsoLoginCredential(final String username) {
+    public SpnegoLoginCredential(final String username) {
         this.username = username;
         this.username = username;
     }
     }
 
 

+ 12 - 12
src/main/java/org/codelibs/fess/app/web/sso/SsoAction.java

@@ -15,18 +15,16 @@
  */
  */
 package org.codelibs.fess.app.web.sso;
 package org.codelibs.fess.app.web.sso;
 
 
-import javax.servlet.http.HttpServletResponse;
-
 import org.codelibs.fess.app.web.base.FessLoginAction;
 import org.codelibs.fess.app.web.base.FessLoginAction;
-import org.codelibs.fess.app.web.base.login.EmptyLoginCredential;
+import org.codelibs.fess.app.web.base.login.ActionLoginCredential;
 import org.codelibs.fess.app.web.base.login.LoginCredential;
 import org.codelibs.fess.app.web.base.login.LoginCredential;
 import org.codelibs.fess.app.web.login.LoginAction;
 import org.codelibs.fess.app.web.login.LoginAction;
-import org.codelibs.fess.sso.SsoAuthenticator;
+import org.codelibs.fess.sso.SsoManager;
 import org.codelibs.fess.util.ComponentUtil;
 import org.codelibs.fess.util.ComponentUtil;
 import org.lastaflute.web.Execute;
 import org.lastaflute.web.Execute;
 import org.lastaflute.web.login.exception.LoginFailureException;
 import org.lastaflute.web.login.exception.LoginFailureException;
 import org.lastaflute.web.response.ActionResponse;
 import org.lastaflute.web.response.ActionResponse;
-import org.lastaflute.web.servlet.filter.RequestLoggingFilter;
+import org.lastaflute.web.response.HtmlResponse;
 import org.slf4j.Logger;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
 
 
@@ -42,19 +40,18 @@ public class SsoAction extends FessLoginAction {
 
 
     @Execute
     @Execute
     public ActionResponse index() {
     public ActionResponse index() {
-        final SsoAuthenticator authenticator = ComponentUtil.getSsoAuthenticator();
-        LoginCredential loginCredential = authenticator.getLoginCredential();
+        final SsoManager ssoManager = ComponentUtil.getSsoManager();
+        final LoginCredential loginCredential = ssoManager.getLoginCredential();
         if (loginCredential == null) {
         if (loginCredential == null) {
             if (logger.isDebugEnabled()) {
             if (logger.isDebugEnabled()) {
                 logger.debug("No user in SSO request.");
                 logger.debug("No user in SSO request.");
             }
             }
-            if (fessConfig.isSsoEnabled()) {
+            if (ssoManager.available()) {
                 saveError(messages -> messages.addErrorsSsoLoginError(GLOBAL));
                 saveError(messages -> messages.addErrorsSsoLoginError(GLOBAL));
             }
             }
             return redirect(LoginAction.class);
             return redirect(LoginAction.class);
-        } else if (loginCredential instanceof EmptyLoginCredential) {
-            throw new RequestLoggingFilter.RequestClientErrorException("Your request is not authorized.", "401 Unauthorized",
-                    HttpServletResponse.SC_UNAUTHORIZED);
+        } else if (loginCredential instanceof ActionLoginCredential) {
+            return ((ActionLoginCredential) loginCredential).execute(this);
         }
         }
         try {
         try {
             return fessLoginAssist.loginRedirect(loginCredential, op -> {}, () -> {
             return fessLoginAssist.loginRedirect(loginCredential, op -> {}, () -> {
@@ -65,11 +62,14 @@ public class SsoAction extends FessLoginAction {
             if (logger.isDebugEnabled()) {
             if (logger.isDebugEnabled()) {
                 logger.debug("SSO login failure.", lfe);
                 logger.debug("SSO login failure.", lfe);
             }
             }
-            if (fessConfig.isSsoEnabled()) {
+            if (ssoManager.available()) {
                 saveError(messages -> messages.addErrorsSsoLoginError(GLOBAL));
                 saveError(messages -> messages.addErrorsSsoLoginError(GLOBAL));
             }
             }
             return redirect(LoginAction.class);
             return redirect(LoginAction.class);
         }
         }
     }
     }
 
 
+    public ActionResponse redirect(final String url) {
+        return HtmlResponse.fromRedirectPathAsIs(url);
+    }
 }
 }

+ 2 - 2
src/main/java/org/codelibs/fess/exception/SsoLoginException.java

@@ -19,11 +19,11 @@ public class SsoLoginException extends FessSystemException {
 
 
     private static final long serialVersionUID = 1L;
     private static final long serialVersionUID = 1L;
 
 
-    public SsoLoginException(String message) {
+    public SsoLoginException(final String message) {
         super(message);
         super(message);
     }
     }
 
 
-    public SsoLoginException(String message, Exception e) {
+    public SsoLoginException(final String message, final Exception e) {
         super(message, e);
         super(message, e);
     }
     }
 
 

+ 1 - 1
src/main/java/org/codelibs/fess/helper/RoleQueryHelper.java

@@ -110,7 +110,7 @@ public class RoleQueryHelper {
             requestManager.findUserBean(FessUserBean.class)
             requestManager.findUserBean(FessUserBean.class)
                     .ifPresent(fessUserBean -> stream(fessUserBean.getPermissions()).of(stream -> stream.forEach(roleList::add)))
                     .ifPresent(fessUserBean -> stream(fessUserBean.getPermissions()).of(stream -> stream.forEach(roleList::add)))
                     .orElse(() -> roleList.addAll(fessConfig.getSearchGuestPermissionList()));
                     .orElse(() -> roleList.addAll(fessConfig.getSearchGuestPermissionList()));
-        } catch (RuntimeException e) {
+        } catch (final RuntimeException e) {
             requestManager.findLoginManager(FessUserBean.class).ifPresent(manager -> manager.logout());
             requestManager.findLoginManager(FessUserBean.class).ifPresent(manager -> manager.logout());
             throw e;
             throw e;
         }
         }

+ 0 - 2
src/main/java/org/codelibs/fess/ldap/LdapUser.java

@@ -19,8 +19,6 @@ import static org.codelibs.core.stream.StreamUtil.stream;
 
 
 import java.util.Hashtable;
 import java.util.Hashtable;
 
 
-import javax.naming.Context;
-
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.ArrayUtils;
 import org.codelibs.core.lang.StringUtil;
 import org.codelibs.core.lang.StringUtil;
 import org.codelibs.fess.entity.FessUser;
 import org.codelibs.fess.entity.FessUser;

+ 91 - 19
src/main/java/org/codelibs/fess/mylasta/direction/FessConfig.java

@@ -931,8 +931,8 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction
     /** The key of the configuration. e.g. homeDirectory */
     /** The key of the configuration. e.g. homeDirectory */
     String LDAP_ATTR_HOME_DIRECTORY = "ldap.attr.homeDirectory";
     String LDAP_ATTR_HOME_DIRECTORY = "ldap.attr.homeDirectory";
 
 
-    /** The key of the configuration. e.g. false */
-    String SSO_ENABLED = "sso.enabled";
+    /** The key of the configuration. e.g. none */
+    String SSO_TYPE = "sso.type";
 
 
     /** The key of the configuration. e.g. 0 */
     /** The key of the configuration. e.g. 0 */
     String SPNEGO_LOGGER_LEVEL = "spnego.logger.level";
     String SPNEGO_LOGGER_LEVEL = "spnego.logger.level";
@@ -970,6 +970,24 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction
     /** The key of the configuration. e.g. false */
     /** The key of the configuration. e.g. false */
     String SPNEGO_ALLOW_DELEGATION = "spnego.allow.delegation";
     String SPNEGO_ALLOW_DELEGATION = "spnego.allow.delegation";
 
 
+    /** The key of the configuration. e.g. __CLIENT_ID__ */
+    String OIC_CLIENT_ID = "oic.client.id";
+
+    /** The key of the configuration. e.g. __CLIENT_SECRET__ */
+    String OIC_CLIENT_SECRET = "oic.client.secret";
+
+    /** The key of the configuration. e.g. https://accounts.google.com/o/oauth2/auth */
+    String OIC_AUTH_SERVER_URL = "oic.auth.server.url";
+
+    /** The key of the configuration. e.g. http://localhost:8080/sso/ */
+    String OIC_REDIRECT_URL = "oic.redirect.url";
+
+    /** The key of the configuration. e.g. openid email */
+    String OIC_SCOPE = "oic.scope";
+
+    /** The key of the configuration. e.g. https://accounts.google.com/o/oauth2/token */
+    String OIC_TOKEN_SERVER_URL = "oic.token.server.url";
+
     /**
     /**
      * Get the value of property as {@link String}.
      * Get the value of property as {@link String}.
      * @param propertyKey The key of the property. (NotNull)
      * @param propertyKey The key of the property. (NotNull)
@@ -3779,20 +3797,12 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction
     String getLdapAttrHomeDirectory();
     String getLdapAttrHomeDirectory();
 
 
     /**
     /**
-     * Get the value for the key 'sso.enabled'. <br>
-     * The value is, e.g. false <br>
+     * Get the value for the key 'sso.type'. <br>
+     * The value is, e.g. none <br>
      * comment: ------
      * comment: ------
      * @return The value of found property. (NotNull: if not found, exception but basically no way)
      * @return The value of found property. (NotNull: if not found, exception but basically no way)
      */
      */
-    String getSsoEnabled();
-
-    /**
-     * Is the property for the key 'sso.enabled' true? <br>
-     * The value is, e.g. false <br>
-     * comment: ------
-     * @return The determination, true or false. (if not found, exception but basically no way)
-     */
-    boolean isSsoEnabled();
+    String getSsoType();
 
 
     /**
     /**
      * Get the value for the key 'spnego.logger.level'. <br>
      * Get the value for the key 'spnego.logger.level'. <br>
@@ -3921,6 +3931,48 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction
      */
      */
     boolean isSpnegoAllowDelegation();
     boolean isSpnegoAllowDelegation();
 
 
+    /**
+     * Get the value for the key 'oic.client.id'. <br>
+     * The value is, e.g. __CLIENT_ID__ <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     */
+    String getOicClientId();
+
+    /**
+     * Get the value for the key 'oic.client.secret'. <br>
+     * The value is, e.g. __CLIENT_SECRET__ <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     */
+    String getOicClientSecret();
+
+    /**
+     * Get the value for the key 'oic.auth.server.url'. <br>
+     * The value is, e.g. https://accounts.google.com/o/oauth2/auth <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     */
+    String getOicAuthServerUrl();
+
+    /**
+     * Get the value for the key 'oic.redirect.url'. <br>
+     * The value is, e.g. http://localhost:8080/sso/ <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     */
+    String getOicRedirectUrl();
+
+    /**
+     * Get the value for the key 'oic.scope'. <br>
+     * The value is, e.g. openid email <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     */
+    String getOicScope();
+
+    /**
+     * Get the value for the key 'oic.token.server.url'. <br>
+     * The value is, e.g. https://accounts.google.com/o/oauth2/token <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     */
+    String getOicTokenServerUrl();
+
     /**
     /**
      * The simple implementation for configuration.
      * The simple implementation for configuration.
      * @author FreeGen
      * @author FreeGen
@@ -5410,12 +5462,8 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction
             return get(FessConfig.LDAP_ATTR_HOME_DIRECTORY);
             return get(FessConfig.LDAP_ATTR_HOME_DIRECTORY);
         }
         }
 
 
-        public String getSsoEnabled() {
-            return get(FessConfig.SSO_ENABLED);
-        }
-
-        public boolean isSsoEnabled() {
-            return is(FessConfig.SSO_ENABLED);
+        public String getSsoType() {
+            return get(FessConfig.SSO_TYPE);
         }
         }
 
 
         public String getSpnegoLoggerLevel() {
         public String getSpnegoLoggerLevel() {
@@ -5489,5 +5537,29 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction
         public boolean isSpnegoAllowDelegation() {
         public boolean isSpnegoAllowDelegation() {
             return is(FessConfig.SPNEGO_ALLOW_DELEGATION);
             return is(FessConfig.SPNEGO_ALLOW_DELEGATION);
         }
         }
+
+        public String getOicClientId() {
+            return get(FessConfig.OIC_CLIENT_ID);
+        }
+
+        public String getOicClientSecret() {
+            return get(FessConfig.OIC_CLIENT_SECRET);
+        }
+
+        public String getOicAuthServerUrl() {
+            return get(FessConfig.OIC_AUTH_SERVER_URL);
+        }
+
+        public String getOicRedirectUrl() {
+            return get(FessConfig.OIC_REDIRECT_URL);
+        }
+
+        public String getOicScope() {
+            return get(FessConfig.OIC_SCOPE);
+        }
+
+        public String getOicTokenServerUrl() {
+            return get(FessConfig.OIC_TOKEN_SERVER_URL);
+        }
     }
     }
 }
 }

+ 45 - 0
src/main/java/org/codelibs/fess/sso/SsoManager.java

@@ -0,0 +1,45 @@
+/*
+ * Copyright 2012-2016 CodeLibs Project and the Others.
+ *
+ * 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 org.codelibs.fess.sso;
+
+import javax.annotation.PostConstruct;
+
+import org.codelibs.fess.app.web.base.login.LoginCredential;
+import org.codelibs.fess.util.ComponentUtil;
+
+public class SsoManager {
+
+    private SsoAuthenticator authenticator;
+
+    @PostConstruct
+    public void init() {
+        final String ssoType = ComponentUtil.getFessConfig().getSsoType();
+        if (!"none".equals(ssoType)) {
+            authenticator = ComponentUtil.getComponent(ssoType + "Authenticator");
+        }
+    }
+
+    public boolean available() {
+        return authenticator != null;
+    }
+
+    public LoginCredential getLoginCredential() {
+        if (authenticator != null) {
+            return authenticator.getLoginCredential();
+        }
+        return null;
+    }
+}

+ 178 - 0
src/main/java/org/codelibs/fess/sso/oic/OpenIdConnectAuthenticator.java

@@ -0,0 +1,178 @@
+/*
+ * Copyright 2012-2016 CodeLibs Project and the Others.
+ *
+ * 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 org.codelibs.fess.sso.oic;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+import org.codelibs.core.lang.StringUtil;
+import org.codelibs.core.net.UuidUtil;
+import org.codelibs.fess.app.web.base.login.ActionLoginCredential;
+import org.codelibs.fess.app.web.base.login.LoginCredential;
+import org.codelibs.fess.app.web.base.login.OpenIdConnectLoginCredential;
+import org.codelibs.fess.crawler.Constants;
+import org.codelibs.fess.mylasta.direction.FessConfig;
+import org.codelibs.fess.sso.SsoAuthenticator;
+import org.codelibs.fess.util.ComponentUtil;
+import org.lastaflute.web.util.LaRequestUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl;
+import com.google.api.client.auth.oauth2.AuthorizationCodeTokenRequest;
+import com.google.api.client.auth.oauth2.TokenResponse;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.JsonParser;
+import com.google.api.client.json.JsonToken;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.client.util.Base64;
+
+public class OpenIdConnectAuthenticator implements SsoAuthenticator {
+
+    private static final Logger logger = LoggerFactory.getLogger(OpenIdConnectAuthenticator.class);
+
+    private static final String OIC_STATE = "OIC_STATE";
+
+    private final HttpTransport httpTransport = new NetHttpTransport();
+
+    private final JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
+
+    @Override
+    public LoginCredential getLoginCredential() {
+        return LaRequestUtil.getOptionalRequest().map(request -> {
+            final HttpSession session = request.getSession(false);
+            if (session != null) {
+                final String sesState = (String) session.getAttribute(OIC_STATE);
+                session.removeAttribute(OIC_STATE);
+                if (StringUtil.isNotBlank(sesState)) {
+                    final String code = request.getParameter("code");
+                    final String reqState = request.getParameter("state");
+                    if (sesState.equals(reqState) && StringUtil.isNotBlank(code)) {
+                        return processCallback(request, code);
+                    }
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("code:" + code + " state(request):" + reqState + " state(session):" + sesState);
+                    }
+                    return null;
+                }
+            }
+
+            return new ActionLoginCredential(action -> action.redirect(getAuthUrl(request)));
+        }).orElse(null);
+    }
+
+    protected String getAuthUrl(final HttpServletRequest request) {
+        final FessConfig fessConfig = ComponentUtil.getFessConfig();
+        final String state = UuidUtil.create();
+        request.getSession().setAttribute(OIC_STATE, state);
+        return new AuthorizationCodeRequestUrl(fessConfig.getOicAuthServerUrl(), fessConfig.getOicClientId())//
+                .setScopes(Arrays.asList(fessConfig.getOicScope()))//
+                .setResponseTypes(Arrays.asList("code"))//
+                .setRedirectUri(fessConfig.getOicRedirectUrl())//
+                .setState(state)//
+                .build();
+    }
+
+    protected LoginCredential processCallback(final HttpServletRequest request, final String code) {
+        try {
+            final TokenResponse tr = getTokenUrl(code);
+
+            final String[] jwt = ((String) tr.get("id_token")).split("\\.");
+            final byte[] jwtHeader = Base64.decodeBase64(jwt[0]);
+            final byte[] jwtClaim = Base64.decodeBase64(jwt[1]);
+            final byte[] jwtSigniture = Base64.decodeBase64(jwt[2]);
+
+            // TODO validate signiture
+
+            final Map<String, Object> attributes = new HashMap<>();
+            attributes.put("accesstoken", tr.getAccessToken());
+            attributes.put("refreshtoken", tr.getRefreshToken() == null ? "null" : tr.getRefreshToken());
+            attributes.put("tokentype", tr.getTokenType());
+            attributes.put("expire", tr.getExpiresInSeconds());
+            attributes.put("jwtheader", new String(jwtHeader, Constants.UTF_8_CHARSET));
+            attributes.put("jwtclaim", new String(jwtClaim, Constants.UTF_8_CHARSET));
+            attributes.put("jwtsign", new String(jwtSigniture, Constants.UTF_8_CHARSET));
+
+            parseJwtClaim(new String(jwtClaim, Constants.UTF_8_CHARSET), attributes);
+
+            return new OpenIdConnectLoginCredential(attributes);
+        } catch (final IOException e) {
+            if (logger.isDebugEnabled()) {
+                logger.debug("Failed to process callbacked request.", e);
+            }
+        }
+        return null;
+    }
+
+    protected void parseJwtClaim(final String jwtClaim, final Map<String, Object> attributes) throws IOException {
+        final JsonParser jsonParser = jsonFactory.createJsonParser(jwtClaim);
+        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
+            final String name = jsonParser.getCurrentName();
+            if (name != null) {
+                jsonParser.nextToken();
+
+                // TODO other parameters
+                switch (name) {
+                case "iss":
+                    attributes.put("iss", jsonParser.getText());
+                    break;
+                case "sub":
+                    attributes.put("sub", jsonParser.getText());
+                    break;
+                case "azp":
+                    attributes.put("azp", jsonParser.getText());
+                    break;
+                case "email":
+                    attributes.put("email", jsonParser.getText());
+                    break;
+                case "at_hash":
+                    attributes.put("at_hash", jsonParser.getText());
+                    break;
+                case "email_verified":
+                    attributes.put("email_verified", jsonParser.getText());
+                    break;
+                case "aud":
+                    attributes.put("aud", jsonParser.getText());
+                    break;
+                case "iat":
+                    attributes.put("iat", jsonParser.getText());
+                    break;
+                case "exp":
+                    attributes.put("exp", jsonParser.getText());
+                    break;
+                }
+            }
+        }
+    }
+
+    protected TokenResponse getTokenUrl(final String code) throws IOException {
+        final FessConfig fessConfig = ComponentUtil.getFessConfig();
+        return new AuthorizationCodeTokenRequest(httpTransport, jsonFactory, new GenericUrl(fessConfig.getOicTokenServerUrl()), code)//
+                .setGrantType("authorization_code")//
+                .setRedirectUri(fessConfig.getOicRedirectUrl())//
+                .set("client_id", fessConfig.getOicClientId())//
+                .set("client_secret", fessConfig.getOicClientSecret())//
+                .execute();
+    }
+}

+ 44 - 42
src/main/java/org/codelibs/fess/sso/spnego/SpnegoAuthenticator.java

@@ -24,9 +24,9 @@ import javax.servlet.ServletContext;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpServletResponse;
 
 
 import org.codelibs.core.io.ResourceUtil;
 import org.codelibs.core.io.ResourceUtil;
-import org.codelibs.fess.app.web.base.login.EmptyLoginCredential;
+import org.codelibs.fess.app.web.base.login.ActionLoginCredential;
 import org.codelibs.fess.app.web.base.login.LoginCredential;
 import org.codelibs.fess.app.web.base.login.LoginCredential;
-import org.codelibs.fess.app.web.base.login.SsoLoginCredential;
+import org.codelibs.fess.app.web.base.login.SpnegoLoginCredential;
 import org.codelibs.fess.exception.FessSystemException;
 import org.codelibs.fess.exception.FessSystemException;
 import org.codelibs.fess.exception.SsoLoginException;
 import org.codelibs.fess.exception.SsoLoginException;
 import org.codelibs.fess.mylasta.direction.FessConfig;
 import org.codelibs.fess.mylasta.direction.FessConfig;
@@ -37,6 +37,7 @@ import org.codelibs.spnego.SpnegoHttpFilter;
 import org.codelibs.spnego.SpnegoHttpFilter.Constants;
 import org.codelibs.spnego.SpnegoHttpFilter.Constants;
 import org.codelibs.spnego.SpnegoHttpServletResponse;
 import org.codelibs.spnego.SpnegoHttpServletResponse;
 import org.codelibs.spnego.SpnegoPrincipal;
 import org.codelibs.spnego.SpnegoPrincipal;
+import org.lastaflute.web.servlet.filter.RequestLoggingFilter;
 import org.lastaflute.web.util.LaRequestUtil;
 import org.lastaflute.web.util.LaRequestUtil;
 import org.lastaflute.web.util.LaResponseUtil;
 import org.lastaflute.web.util.LaResponseUtil;
 import org.slf4j.Logger;
 import org.slf4j.Logger;
@@ -49,14 +50,14 @@ public class SpnegoAuthenticator implements SsoAuthenticator {
 
 
     @PostConstruct
     @PostConstruct
     public void init() {
     public void init() {
-        if (ComponentUtil.getFessConfig().isSsoEnabled()) {
+        if ("spnego".equals(ComponentUtil.getFessConfig().getSsoType())) {
             try {
             try {
                 // set some System properties
                 // set some System properties
                 final SpnegoFilterConfig config = SpnegoFilterConfig.getInstance(new SpengoConfig());
                 final SpnegoFilterConfig config = SpnegoFilterConfig.getInstance(new SpengoConfig());
 
 
                 // pre-authenticate
                 // pre-authenticate
                 authenticator = new org.codelibs.spnego.SpnegoAuthenticator(config);
                 authenticator = new org.codelibs.spnego.SpnegoAuthenticator(config);
-            } catch (Exception e) {
+            } catch (final Exception e) {
                 throw new FessSystemException("Failed to initialize SPNEGO.", e);
                 throw new FessSystemException("Failed to initialize SPNEGO.", e);
             }
             }
         }
         }
@@ -67,43 +68,44 @@ public class SpnegoAuthenticator implements SsoAuthenticator {
      */
      */
     @Override
     @Override
     public LoginCredential getLoginCredential() {
     public LoginCredential getLoginCredential() {
-        if (!ComponentUtil.getFessConfig().isSsoEnabled()) {
-            return null;
-        }
-
-        return LaRequestUtil.getOptionalRequest().map(request -> {
-            final HttpServletResponse response = LaResponseUtil.getResponse();
-            final SpnegoHttpServletResponse spnegoResponse = new SpnegoHttpServletResponse(response);
-
-            // client/caller principal
-                final SpnegoPrincipal principal;
-                try {
-                    principal = authenticator.authenticate(request, spnegoResponse);
-                } catch (Exception e) {
-                    final String msg = "HTTP Authorization Header=" + request.getHeader(Constants.AUTHZ_HEADER);
-                    logger.error(msg);
-                    throw new SsoLoginException(msg, e);
-                }
-
-                // context/auth loop not yet complete
-                if (spnegoResponse.isStatusSet()) {
-                    return new EmptyLoginCredential();
-                }
-
-                // assert
-                if (null == principal) {
-                    String msg = "Principal was null.";
-                    logger.error(msg);
-                    throw new SsoLoginException(msg);
-                }
-
-                if (logger.isDebugEnabled()) {
-                    logger.debug("principal=" + principal);
-                }
-
-                final String[] username = principal.getName().split("@", 2);
-                return new SsoLoginCredential(username[0]);
-            }).orElseGet(() -> null);
+        return LaRequestUtil
+                .getOptionalRequest()
+                .map(request -> {
+                    final HttpServletResponse response = LaResponseUtil.getResponse();
+                    final SpnegoHttpServletResponse spnegoResponse = new SpnegoHttpServletResponse(response);
+
+                    // client/caller principal
+                    final SpnegoPrincipal principal;
+                    try {
+                        principal = authenticator.authenticate(request, spnegoResponse);
+                    } catch (final Exception e) {
+                        final String msg = "HTTP Authorization Header=" + request.getHeader(Constants.AUTHZ_HEADER);
+                        logger.error(msg);
+                        throw new SsoLoginException(msg, e);
+                    }
+
+                    // context/auth loop not yet complete
+                    if (spnegoResponse.isStatusSet()) {
+                        return new ActionLoginCredential(action -> {
+                            throw new RequestLoggingFilter.RequestClientErrorException("Your request is not authorized.",
+                                    "401 Unauthorized", HttpServletResponse.SC_UNAUTHORIZED);
+                        });
+                    }
+
+                    // assert
+                    if (null == principal) {
+                        final String msg = "Principal was null.";
+                        logger.error(msg);
+                        throw new SsoLoginException(msg);
+                    }
+
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("principal=" + principal);
+                    }
+
+                    final String[] username = principal.getName().split("@", 2);
+                    return new SpnegoLoginCredential(username[0]);
+                }).orElseGet(() -> null);
 
 
     }
     }
 
 
@@ -122,7 +124,7 @@ public class SpnegoAuthenticator implements SsoAuthenticator {
         }
         }
 
 
         @Override
         @Override
-        public String getInitParameter(String name) {
+        public String getInitParameter(final String name) {
             if (SpnegoHttpFilter.Constants.LOGGER_LEVEL.equals(name)) {
             if (SpnegoHttpFilter.Constants.LOGGER_LEVEL.equals(name)) {
                 return fessConfig.getSpnegoLoggerLevel();
                 return fessConfig.getSpnegoLoggerLevel();
             } else if (SpnegoHttpFilter.Constants.LOGIN_CONF.equals(name)) {
             } else if (SpnegoHttpFilter.Constants.LOGIN_CONF.equals(name)) {

+ 4 - 4
src/main/java/org/codelibs/fess/util/ComponentUtil.java

@@ -59,7 +59,7 @@ import org.codelibs.fess.indexer.IndexUpdater;
 import org.codelibs.fess.job.JobExecutor;
 import org.codelibs.fess.job.JobExecutor;
 import org.codelibs.fess.ldap.LdapManager;
 import org.codelibs.fess.ldap.LdapManager;
 import org.codelibs.fess.mylasta.direction.FessConfig;
 import org.codelibs.fess.mylasta.direction.FessConfig;
-import org.codelibs.fess.sso.SsoAuthenticator;
+import org.codelibs.fess.sso.SsoManager;
 import org.lastaflute.core.message.MessageManager;
 import org.lastaflute.core.message.MessageManager;
 import org.lastaflute.di.core.SingletonLaContainer;
 import org.lastaflute.di.core.SingletonLaContainer;
 import org.lastaflute.di.core.factory.SingletonLaContainerFactory;
 import org.lastaflute.di.core.factory.SingletonLaContainerFactory;
@@ -73,7 +73,7 @@ public final class ComponentUtil {
 
 
     private static final Logger logger = LoggerFactory.getLogger(ComponentUtil.class);
     private static final Logger logger = LoggerFactory.getLogger(ComponentUtil.class);
 
 
-    private static final String SSO_AUTHENTICATOR = "ssoAuthenticator";
+    private static final String SSO_MANAGER = "ssoManager";
 
 
     private static final String PERMISSION_HELPER = "permissionHelper";
     private static final String PERMISSION_HELPER = "permissionHelper";
 
 
@@ -360,8 +360,8 @@ public final class ComponentUtil {
         return getComponent(PERMISSION_HELPER);
         return getComponent(PERMISSION_HELPER);
     }
     }
 
 
-    public static SsoAuthenticator getSsoAuthenticator() {
-        return getComponent(SSO_AUTHENTICATOR);
+    public static SsoManager getSsoManager() {
+        return getComponent(SSO_MANAGER);
     }
     }
 
 
     public static CrawlerClientFactory getCrawlerClientFactory() {
     public static CrawlerClientFactory getCrawlerClientFactory() {

+ 7 - 1
src/main/resources/fess_config.properties

@@ -473,7 +473,7 @@ ldap.attr.homeDirectory=homeDirectory
 # ----------------------------------------------------------
 # ----------------------------------------------------------
 #                                                      SSO
 #                                                      SSO
 #                                                     ------
 #                                                     ------
-sso.enabled=false
+sso.type=none
 spnego.logger.level=0
 spnego.logger.level=0
 spnego.krb5.conf=krb5.conf
 spnego.krb5.conf=krb5.conf
 spnego.login.conf=auth_login.conf
 spnego.login.conf=auth_login.conf
@@ -487,4 +487,10 @@ spnego.prompt.ntlm=true
 spnego.allow.localhost=true
 spnego.allow.localhost=true
 spnego.allow.delegation=false
 spnego.allow.delegation=false
 
 
+oic.client.id=__CLIENT_ID__
+oic.client.secret=__CLIENT_SECRET__
+oic.auth.server.url=https://accounts.google.com/o/oauth2/auth
+oic.redirect.url=http://localhost:8080/sso/
+oic.scope=openid email
+oic.token.server.url=https://accounts.google.com/o/oauth2/token
 
 

+ 4 - 1
src/main/resources/fess_sso.xml

@@ -2,7 +2,10 @@
 <!DOCTYPE components PUBLIC "-//DBFLUTE//DTD LastaDi 1.0//EN"
 <!DOCTYPE components PUBLIC "-//DBFLUTE//DTD LastaDi 1.0//EN"
 	"http://dbflute.org/meta/lastadi10.dtd">
 	"http://dbflute.org/meta/lastadi10.dtd">
 <components>
 <components>
-	<component name="ssoAuthenticator" class="org.codelibs.fess.sso.spnego.SpnegoAuthenticator">
+	<component name="ssoManager" class="org.codelibs.fess.sso.SsoManager">
+	</component>
+
+	<component name="spnegoAuthenticator" class="org.codelibs.fess.sso.spnego.SpnegoAuthenticator">
 	</component>
 	</component>
 
 
 </components>
 </components>