View Javadoc
1   /*
2    * Copyright 2012-2021 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.codelibs.fess.sso.spnego;
17  
18  import java.io.File;
19  import java.util.Arrays;
20  import java.util.Enumeration;
21  
22  import javax.annotation.PostConstruct;
23  import javax.servlet.FilterConfig;
24  import javax.servlet.ServletContext;
25  import javax.servlet.http.HttpServletResponse;
26  
27  import org.apache.logging.log4j.LogManager;
28  import org.apache.logging.log4j.Logger;
29  import org.codelibs.core.io.ResourceUtil;
30  import org.codelibs.core.lang.StringUtil;
31  import org.codelibs.fess.app.web.base.login.ActionResponseCredential;
32  import org.codelibs.fess.app.web.base.login.FessLoginAssist.LoginCredentialResolver;
33  import org.codelibs.fess.app.web.base.login.SpnegoCredential;
34  import org.codelibs.fess.exception.SsoLoginException;
35  import org.codelibs.fess.mylasta.action.FessUserBean;
36  import org.codelibs.fess.mylasta.direction.FessConfig;
37  import org.codelibs.fess.sso.SsoAuthenticator;
38  import org.codelibs.fess.sso.SsoResponseType;
39  import org.codelibs.fess.util.ComponentUtil;
40  import org.codelibs.spnego.SpnegoFilterConfig;
41  import org.codelibs.spnego.SpnegoHttpFilter;
42  import org.codelibs.spnego.SpnegoHttpFilter.Constants;
43  import org.codelibs.spnego.SpnegoHttpServletResponse;
44  import org.codelibs.spnego.SpnegoPrincipal;
45  import org.dbflute.optional.OptionalEntity;
46  import org.lastaflute.web.login.credential.LoginCredential;
47  import org.lastaflute.web.response.ActionResponse;
48  import org.lastaflute.web.servlet.filter.RequestLoggingFilter;
49  import org.lastaflute.web.util.LaRequestUtil;
50  import org.lastaflute.web.util.LaResponseUtil;
51  
52  public class SpnegoAuthenticator implements SsoAuthenticator {
53  
54      private static final Logger logger = LogManager.getLogger(SpnegoAuthenticator.class);
55  
56      protected static final String SPNEGO_INITIALIZED = "spnego.initialized";
57      protected static final String SPNEGO_EXCLUDE_DIRS = "spnego.exclude.dirs";
58      protected static final String SPNEGO_ALLOW_DELEGATION = "spnego.allow.delegation";
59      protected static final String SPNEGO_ALLOW_LOCALHOST = "spnego.allow.localhost";
60      protected static final String SPNEGO_PROMPT_NTLM = "spnego.prompt.ntlm";
61      protected static final String SPNEGO_ALLOW_UNSECURE_BASIC = "spnego.allow.unsecure.basic";
62      protected static final String SPNEGO_ALLOW_BASIC = "spnego.allow.basic";
63      protected static final String SPNEGO_PREAUTH_PASSWORD = "spnego.preauth.password";
64      protected static final String SPNEGO_PREAUTH_USERNAME = "spnego.preauth.username";
65      protected static final String SPNEGO_LOGIN_SERVER_MODULE = "spnego.login.server.module";
66      protected static final String SPNEGO_LOGIN_CLIENT_MODULE = "spnego.login.client.module";
67      protected static final String SPNEGO_KRB5_CONF = "spnego.krb5.conf";
68      protected static final String SPNEGO_LOGIN_CONF = "spnego.login.conf";
69      protected static final String SPNEGO_LOGGER_LEVEL = "spnego.logger.level";
70  
71      protected org.codelibs.spnego.SpnegoAuthenticator authenticator = null;
72  
73      @PostConstruct
74      public void init() {
75          if (logger.isDebugEnabled()) {
76              logger.debug("Initialize {}", this.getClass().getSimpleName());
77          }
78          ComponentUtil.getSsoManager().register(this);
79      }
80  
81      protected synchronized org.codelibs.spnego.SpnegoAuthenticator getAuthenticator() {
82          final FessConfig fessConfig = ComponentUtil.getFessConfig();
83          if (authenticator != null && fessConfig.getSystemPropertyAsBoolean(SPNEGO_INITIALIZED, false)) {
84              return authenticator;
85          }
86          try {
87              // set some System properties
88              final SpnegoFilterConfig config = SpnegoFilterConfig.getInstance(new SpengoConfig());
89  
90              // pre-authenticate
91              authenticator = new org.codelibs.spnego.SpnegoAuthenticator(config);
92  
93              fessConfig.setSystemPropertyAsBoolean(SPNEGO_INITIALIZED, true);
94              fessConfig.storeSystemProperties();
95              return authenticator;
96          } catch (final Exception e) {
97              throw new SsoLoginException("Failed to initialize SPNEGO.", e);
98          }
99      }
100 
101     /* (non-Javadoc)
102      * @see org.codelibs.fess.sso.spnego.SsoAuthenticator#getLoginCredential()
103      */
104     @Override
105     public LoginCredential getLoginCredential() {
106         return LaRequestUtil.getOptionalRequest().map(request -> {
107             if (logger.isDebugEnabled()) {
108                 logger.debug("Logging in with SPNEGO Authenticator");
109             }
110             final HttpServletResponse response = LaResponseUtil.getResponse();
111             final SpnegoHttpServletResponse spnegoResponse = new SpnegoHttpServletResponse(response);
112 
113             // client/caller principal
114             final SpnegoPrincipal principal;
115             try {
116                 principal = getAuthenticator().authenticate(request, spnegoResponse);
117                 if (logger.isDebugEnabled()) {
118                     logger.debug("principal: {}", principal);
119                 }
120             } catch (final Exception e) {
121                 final String msg = "HTTP Authorization Header=" + request.getHeader(Constants.AUTHZ_HEADER);
122                 if (logger.isDebugEnabled()) {
123                     logger.debug(msg);
124                 }
125                 throw new SsoLoginException(msg, e);
126             }
127 
128             // context/auth loop not yet complete
129             final boolean status = spnegoResponse.isStatusSet();
130             if (logger.isDebugEnabled()) {
131                 logger.debug("isStatusSet: {}", status);
132             }
133             if (status) {
134                 return new ActionResponseCredential(() -> {
135                     throw new RequestLoggingFilter.RequestClientErrorException("Your request is not authorized.", "401 Unauthorized",
136                             HttpServletResponse.SC_UNAUTHORIZED);
137                 });
138             }
139 
140             // assert
141             if (null == principal) {
142                 final String msg = "Principal was null.";
143                 if (logger.isDebugEnabled()) {
144                     logger.debug(msg);
145                 }
146                 throw new SsoLoginException(msg);
147             }
148 
149             if (logger.isDebugEnabled()) {
150                 logger.debug("principal={}", principal);
151             }
152 
153             final String[] username = principal.getName().split("@", 2);
154             if (logger.isDebugEnabled()) {
155                 logger.debug("username: {}", Arrays.toString(username));
156             }
157             return new SpnegoCredential(username[0]);
158         }).orElseGet(() -> null);
159 
160     }
161 
162     protected class SpengoConfig implements FilterConfig {
163 
164         @Override
165         public String getFilterName() {
166             return SpnegoAuthenticator.class.getName();
167         }
168 
169         @Override
170         public ServletContext getServletContext() {
171             throw new UnsupportedOperationException();
172         }
173 
174         @Override
175         public String getInitParameter(final String name) {
176             if (SpnegoHttpFilter.Constants.LOGGER_LEVEL.equals(name)) {
177                 final String logLevel = getProperty(SPNEGO_LOGGER_LEVEL, StringUtil.EMPTY);
178                 if (StringUtil.isNotBlank(logLevel)) {
179                     return logLevel;
180                 }
181                 if (logger.isDebugEnabled()) {
182                     return "3";
183                 } else if (logger.isInfoEnabled()) {
184                     return "5";
185                 } else if (logger.isWarnEnabled()) {
186                     return "6";
187                 } else if (logger.isErrorEnabled()) {
188                     return "7";
189                 } else {
190                     return "0";
191                 }
192             }
193             if (SpnegoHttpFilter.Constants.LOGIN_CONF.equals(name)) {
194                 return getResourcePath(getProperty(SPNEGO_LOGIN_CONF, "auth_login.conf"));
195             }
196             if (SpnegoHttpFilter.Constants.KRB5_CONF.equals(name)) {
197                 return getResourcePath(getProperty(SPNEGO_KRB5_CONF, "krb5.conf"));
198             } else if (SpnegoHttpFilter.Constants.CLIENT_MODULE.equals(name)) {
199                 return getProperty(SPNEGO_LOGIN_CLIENT_MODULE, "spnego-client");
200             } else if (SpnegoHttpFilter.Constants.SERVER_MODULE.equals(name)) {
201                 return getProperty(SPNEGO_LOGIN_SERVER_MODULE, "spnego-server");
202             } else if (SpnegoHttpFilter.Constants.PREAUTH_USERNAME.equals(name)) {
203                 return getProperty(SPNEGO_PREAUTH_USERNAME, "username");
204             } else if (SpnegoHttpFilter.Constants.PREAUTH_PASSWORD.equals(name)) {
205                 return getProperty(SPNEGO_PREAUTH_PASSWORD, "password");
206             } else if (SpnegoHttpFilter.Constants.ALLOW_BASIC.equals(name)) {
207                 return getProperty(SPNEGO_ALLOW_BASIC, "true");
208             } else if (SpnegoHttpFilter.Constants.ALLOW_UNSEC_BASIC.equals(name)) {
209                 return getProperty(SPNEGO_ALLOW_UNSECURE_BASIC, "true");
210             } else if (SpnegoHttpFilter.Constants.PROMPT_NTLM.equals(name)) {
211                 return getProperty(SPNEGO_PROMPT_NTLM, "true");
212             } else if (SpnegoHttpFilter.Constants.ALLOW_LOCALHOST.equals(name)) {
213                 return getProperty(SPNEGO_ALLOW_LOCALHOST, "true");
214             } else if (SpnegoHttpFilter.Constants.ALLOW_DELEGATION.equals(name)) {
215                 return getProperty(SPNEGO_ALLOW_DELEGATION, "false");
216             } else if (SpnegoHttpFilter.Constants.EXCLUDE_DIRS.equals(name)) {
217                 return getProperty(SPNEGO_EXCLUDE_DIRS, StringUtil.EMPTY);
218             }
219             return null;
220         }
221 
222         protected String getProperty(final String key, final String defaultValue) {
223             return ComponentUtil.getSystemProperties().getProperty(key, defaultValue);
224         }
225 
226         protected String getResourcePath(final String path) {
227             final File file = ResourceUtil.getResourceAsFileNoException(path);
228             if (file != null) {
229                 return file.getAbsolutePath();
230             }
231             return null;
232         }
233 
234         @Override
235         public Enumeration<String> getInitParameterNames() {
236             throw new UnsupportedOperationException();
237         }
238 
239     }
240 
241     @Override
242     public void resolveCredential(final LoginCredentialResolver resolver) {
243         resolver.resolve(SpnegoCredential.class, credential -> {
244             final String username = credential.getUserId();
245             if (!ComponentUtil.getFessConfig().isAdminUser(username)) {
246                 return ComponentUtil.getLdapManager().login(username);
247             }
248             return OptionalEntity.empty();
249         });
250     }
251 
252     @Override
253     public ActionResponse getResponse(final SsoResponseType responseType) {
254         return null;
255     }
256 
257     @Override
258     public String logout(final FessUserBean user) {
259         return null;
260     }
261 
262 }