View Javadoc
1   package org.argeo.cms.auth;
2   
3   import java.io.IOException;
4   import java.security.cert.X509Certificate;
5   import java.util.Base64;
6   import java.util.Locale;
7   import java.util.Map;
8   import java.util.StringTokenizer;
9   
10  import javax.security.auth.Subject;
11  import javax.security.auth.callback.Callback;
12  import javax.security.auth.callback.CallbackHandler;
13  import javax.security.auth.callback.UnsupportedCallbackException;
14  import javax.security.auth.login.LoginException;
15  import javax.security.auth.spi.LoginModule;
16  import javax.servlet.http.HttpServletRequest;
17  import javax.servlet.http.HttpServletResponse;
18  import javax.servlet.http.HttpSession;
19  
20  import org.apache.commons.logging.Log;
21  import org.apache.commons.logging.LogFactory;
22  import org.argeo.cms.CmsException;
23  import org.argeo.cms.internal.kernel.Activator;
24  import org.osgi.framework.BundleContext;
25  import org.osgi.framework.FrameworkUtil;
26  import org.osgi.service.http.HttpContext;
27  import org.osgi.service.useradmin.Authorization;
28  
29  /** Use the HTTP session as the basis for authentication. */
30  public class HttpSessionLoginModule implements LoginModule {
31  	private final static Log log = LogFactory.getLog(HttpSessionLoginModule.class);
32  
33  	private Subject subject = null;
34  	private CallbackHandler callbackHandler = null;
35  	private Map<String, Object> sharedState = null;
36  
37  	private HttpServletRequest request = null;
38  	private HttpServletResponse response = null;
39  
40  	private BundleContext bc;
41  
42  	private Authorization authorization;
43  	private Locale locale;
44  
45  	@SuppressWarnings("unchecked")
46  	@Override
47  	public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
48  			Map<String, ?> options) {
49  		bc = FrameworkUtil.getBundle(HttpSessionLoginModule.class).getBundleContext();
50  		assert bc != null;
51  		this.subject = subject;
52  		this.callbackHandler = callbackHandler;
53  		this.sharedState = (Map<String, Object>) sharedState;
54  	}
55  
56  	@Override
57  	public boolean login() throws LoginException {
58  		if (callbackHandler == null)
59  			return false;
60  		HttpRequestCallback httpCallback = new HttpRequestCallback();
61  		try {
62  			callbackHandler.handle(new Callback[] { httpCallback });
63  		} catch (IOException e) {
64  			throw new LoginException("Cannot handle http callback: " + e.getMessage());
65  		} catch (UnsupportedCallbackException e) {
66  			return false;
67  		}
68  		request = httpCallback.getRequest();
69  		if (request == null) {
70  			HttpSession httpSession = httpCallback.getHttpSession();
71  			if (httpSession == null)
72  				return false;
73  			// TODO factorize with below
74  			String httpSessionId = httpSession.getId();
75  			if (log.isTraceEnabled())
76  				log.trace("HTTP login: " + request.getPathInfo() + " #" + httpSessionId);
77  			CmsSession cmsSession = CmsAuthUtils.cmsSessionFromHttpSession(bc, httpSessionId);
78  			if (cmsSession != null) {
79  				authorization = cmsSession.getAuthorization();
80  				locale = cmsSession.getLocale();
81  				if (log.isTraceEnabled())
82  					log.trace("Retrieved authorization from " + cmsSession);
83  			}
84  		} else {
85  			authorization = (Authorization) request.getAttribute(HttpContext.AUTHORIZATION);
86  			if (authorization == null) {// search by session ID
87  				HttpSession httpSession = request.getSession(false);
88  				if (httpSession == null) {
89  					// TODO make sure this is always safe
90  					if (log.isTraceEnabled())
91  						log.trace("Create http session");
92  					httpSession = request.getSession(true);
93  				}
94  				String httpSessionId = httpSession.getId();
95  				if (log.isTraceEnabled())
96  					log.trace("HTTP login: " + request.getPathInfo() + " #" + httpSessionId);
97  				CmsSession cmsSession = CmsAuthUtils.cmsSessionFromHttpSession(bc, httpSessionId);
98  				if (cmsSession != null) {
99  					authorization = cmsSession.getAuthorization();
100 					locale = cmsSession.getLocale();
101 					if (log.isTraceEnabled())
102 						log.trace("Retrieved authorization from " + cmsSession);
103 				}
104 			}
105 			sharedState.put(CmsAuthUtils.SHARED_STATE_HTTP_REQUEST, request);
106 			extractHttpAuth(request);
107 			extractClientCertificate(request);
108 		}
109 		if (authorization == null) {
110 			if (log.isTraceEnabled())
111 				log.trace("HTTP login: " + false);
112 			return false;
113 		} else {
114 			if (log.isTraceEnabled())
115 				log.trace("HTTP login: " + true);
116 			return true;
117 		}
118 	}
119 
120 	@Override
121 	public boolean commit() throws LoginException {
122 		byte[] outToken = (byte[]) sharedState.get(CmsAuthUtils.SHARED_STATE_SPNEGO_OUT_TOKEN);
123 		if (outToken != null) {
124 			response.setHeader(CmsAuthUtils.HEADER_WWW_AUTHENTICATE,
125 					"Negotiate " + java.util.Base64.getEncoder().encodeToString(outToken));
126 		}
127 
128 		if (authorization != null) {
129 			// Locale locale = request.getLocale();
130 			if (locale == null && request != null)
131 				locale = request.getLocale();
132 			if (locale != null)
133 				subject.getPublicCredentials().add(locale);
134 			CmsAuthUtils.addAuthorization(subject, authorization);
135 			CmsAuthUtils.registerSessionAuthorization(request, subject, authorization, locale);
136 			cleanUp();
137 			return true;
138 		} else {
139 			cleanUp();
140 			return false;
141 		}
142 	}
143 
144 	@Override
145 	public boolean abort() throws LoginException {
146 		cleanUp();
147 		return false;
148 	}
149 
150 	private void cleanUp() {
151 		authorization = null;
152 		request = null;
153 	}
154 
155 	@Override
156 	public boolean logout() throws LoginException {
157 		cleanUp();
158 		return true;
159 	}
160 
161 	private void extractHttpAuth(final HttpServletRequest httpRequest) {
162 		String authHeader = httpRequest.getHeader(CmsAuthUtils.HEADER_AUTHORIZATION);
163 		extractHttpAuth(authHeader);
164 	}
165 
166 	private void extractHttpAuth(String authHeader) {
167 		if (authHeader != null) {
168 			StringTokenizer st = new StringTokenizer(authHeader);
169 			if (st.hasMoreTokens()) {
170 				String basic = st.nextToken();
171 				if (basic.equalsIgnoreCase("Basic")) {
172 					try {
173 						// TODO manipulate char[]
174 						Base64.Decoder decoder = Base64.getDecoder();
175 						String credentials = new String(decoder.decode(st.nextToken()), "UTF-8");
176 						// log.debug("Credentials: " + credentials);
177 						int p = credentials.indexOf(":");
178 						if (p != -1) {
179 							final String login = credentials.substring(0, p).trim();
180 							final char[] password = credentials.substring(p + 1).trim().toCharArray();
181 							sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, login);
182 							sharedState.put(CmsAuthUtils.SHARED_STATE_PWD, password);
183 						} else {
184 							throw new CmsException("Invalid authentication token");
185 						}
186 					} catch (Exception e) {
187 						throw new CmsException("Couldn't retrieve authentication", e);
188 					}
189 				} else if (basic.equalsIgnoreCase("Negotiate")) {
190 					String spnegoToken = st.nextToken();
191 					Base64.Decoder decoder = Base64.getDecoder();
192 					byte[] authToken = decoder.decode(spnegoToken);
193 					sharedState.put(CmsAuthUtils.SHARED_STATE_SPNEGO_TOKEN, authToken);
194 				}
195 			}
196 		}
197 
198 		// auth token
199 		// String mail = request.getParameter(LdapAttrs.mail.name());
200 		// String authPassword = request.getParameter(LdapAttrs.authPassword.name());
201 		// if (authPassword != null) {
202 		// sharedState.put(CmsAuthUtils.SHARED_STATE_PWD, authPassword);
203 		// if (mail != null)
204 		// sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, mail);
205 		// }
206 	}
207 
208 	private void extractClientCertificate(HttpServletRequest req) {
209 		X509Certificate[] certs = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
210 		if (null != certs && certs.length > 0) {// Servlet container verified the client certificate
211 			String certDn = certs[0].getSubjectX500Principal().getName();
212 			sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, certDn);
213 			sharedState.put(CmsAuthUtils.SHARED_STATE_CERTIFICATE_CHAIN, certs);
214 			if (log.isDebugEnabled())
215 				log.debug("Client certificate " + certDn + " verified by servlet container");
216 		} // Reverse proxy verified the client certificate
217 		String clientDnHttpHeader = Activator.getHttpProxySslHeader();
218 		if (clientDnHttpHeader != null) {
219 			String certDn = req.getHeader(clientDnHttpHeader);
220 			// TODO retrieve more cf. https://httpd.apache.org/docs/current/mod/mod_ssl.html
221 			// String issuerDn = req.getHeader("SSL_CLIENT_I_DN");
222 			if (certDn != null && !certDn.trim().equals("(null)")) {
223 				sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, certDn);
224 				sharedState.put(CmsAuthUtils.SHARED_STATE_CERTIFICATE_CHAIN, "");
225 				if (log.isDebugEnabled())
226 					log.debug("Client certificate " + certDn + " verified by reverse proxy");
227 			}
228 		}
229 	}
230 
231 }