001/* 002 * Copyright 2016 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.extrausermgt.authentication.kerberos; 017 018import java.util.Map; 019import java.util.concurrent.Callable; 020import java.util.regex.Pattern; 021 022import javax.security.auth.Subject; 023import javax.security.auth.callback.Callback; 024import javax.security.auth.callback.CallbackHandler; 025import javax.security.auth.callback.NameCallback; 026import javax.security.auth.callback.PasswordCallback; 027import javax.security.auth.login.AppConfigurationEntry; 028import javax.security.auth.login.Configuration; 029import javax.security.auth.login.LoginContext; 030import javax.security.auth.login.LoginException; 031 032import org.apache.avalon.framework.activity.Disposable; 033import org.apache.avalon.framework.context.Context; 034import org.apache.avalon.framework.context.ContextException; 035import org.apache.avalon.framework.context.Contextualizable; 036import org.apache.cocoon.components.ContextHelper; 037import org.apache.cocoon.environment.Redirector; 038import org.apache.cocoon.environment.Request; 039import org.apache.cocoon.environment.Response; 040import org.apache.cocoon.environment.Session; 041import org.apache.commons.codec.binary.Base64; 042import org.apache.commons.lang3.StringUtils; 043import org.ietf.jgss.GSSContext; 044import org.ietf.jgss.GSSCredential; 045import org.ietf.jgss.GSSException; 046import org.ietf.jgss.GSSManager; 047import org.ietf.jgss.GSSName; 048import org.ietf.jgss.Oid; 049 050import org.ametys.core.authentication.AbstractCredentialProvider; 051import org.ametys.core.authentication.NonBlockingCredentialProvider; 052import org.ametys.core.user.UserIdentity; 053import org.ametys.runtime.workspace.WorkspaceMatcher; 054 055/** 056 * Kerberos http authentication. 057 */ 058public class KerberosCredentialProvider extends AbstractCredentialProvider implements NonBlockingCredentialProvider, Contextualizable, Disposable 059{ 060 /** Name of the parameter holding the authentication server kdc adress */ 061 protected static final String __PARAM_KDC = "authentication.kerberos.kdc"; 062 /** Name of the parameter holding the authentication server realm */ 063 protected static final String __PARAM_REALM = "authentication.kerberos.realm"; 064 /** Name of the parameter holding the ametys login */ 065 protected static final String __PARAM_LOGIN = "authentication.kerberos.login"; 066 /** Name of the parameter holding the ametys password */ 067 protected static final String __PARAM_PASSWORD = "authentication.kerberos.password"; 068 /** Name of the parameter holding the regexp to match ip adresses */ 069 protected static final String __PARAM_IPRESTRICTION = "authentication.kerberos.ip-limitation-regexp"; 070 071 /** Name of the login config file */ 072 protected static final String __LOGIN_CONF_FILE = "jaas.conf"; 073 074 /** The url to redirect to skip kerberos current authentication */ 075 protected static final String __SKIP_KERBEROS_URL = "cocoon://_plugins/extra-user-management/userpopulations/credentialproviders/kerberos"; 076 077 /** Kerberos context */ 078 protected static final String __SESSION_ATTRIBUTE_GSSCONTEXT = "GSSContext"; 079 080 private Context _context; 081 private GSSCredential _gssCredential; 082 private Pattern _ipRestriction; 083 084 @Override 085 public void contextualize(Context context) throws ContextException 086 { 087 _context = context; 088 } 089 090 /** 091 * Create a logged in LoginContext for Kerberos 092 * @param realm The realm 093 * @param login The identifier of a user to the kdc 094 * @param password The associated password 095 * @return A non null LoginContext (to be logged out) 096 * @throws LoginException If the login process failed 097 */ 098 public static LoginContext createLoginContext(String realm, String login, String password) throws LoginException 099 { 100 Configuration loginConfig = null; 101 if (System.getProperty("java.security.auth.login.config") == null) 102 { 103 loginConfig = new Configuration() 104 { 105 @Override 106 public AppConfigurationEntry[] getAppConfigurationEntry(String name) 107 { 108 Map<String, String> options = Map.of("storeKey", "true", "isInitiator", "false"); 109 return new AppConfigurationEntry[] {new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options)}; 110 } 111 }; 112 } 113 114 LoginContext loginContext = new LoginContext("kerberos", null, new CallbackHandler() 115 { 116 public void handle(final Callback[] callbacks) 117 { 118 for (Callback callback : callbacks) 119 { 120 if (callback instanceof NameCallback) 121 { 122 ((NameCallback) callback).setName(login + "@" + realm.toUpperCase()); 123 } 124 else if (callback instanceof PasswordCallback) 125 { 126 ((PasswordCallback) callback).setPassword(password.toCharArray()); 127 } 128 else 129 { 130 throw new RuntimeException("Invalid callback received during KerberosCredentialProvider initialization"); 131 } 132 } 133 } 134 }, loginConfig); 135 136 loginContext.login(); 137 138 return loginContext; 139 } 140 141 @Override 142 public void init(String id, String cpModelId, Map<String, Object> paramValues, String label) throws Exception 143 { 144 super.init(id, cpModelId, paramValues, label); 145 146 String ipRegexp = (String) paramValues.get(__PARAM_IPRESTRICTION); 147 if (StringUtils.isNotBlank(ipRegexp)) 148 { 149 _ipRestriction = Pattern.compile(ipRegexp); 150 } 151 else 152 { 153 _ipRestriction = null; 154 } 155 156 String realm = (String) paramValues.get(__PARAM_REALM); 157 String login = (String) paramValues.get(__PARAM_LOGIN); 158 String password = (String) paramValues.get(__PARAM_PASSWORD); 159 160 try 161 { 162 LoginContext loginContext = createLoginContext(realm, login, password); 163 164 GSSManager manager = GSSManager.getInstance(); 165 166 Callable<GSSCredential> action = new Callable<>() 167 { 168 public GSSCredential call() throws GSSException 169 { 170 return manager.createCredential(null, GSSCredential.INDEFINITE_LIFETIME, new Oid("1.3.6.1.5.5.2"), GSSCredential.ACCEPT_ONLY); 171 } 172 }; 173 174 _gssCredential = Subject.callAs(loginContext.getSubject(), action); 175 } 176 catch (LoginException e) 177 { 178 throw new RuntimeException("Unable to initialize the KerberosCredentialProvider", e); 179 } 180 } 181 182 @Override 183 public boolean nonBlockingIsStillConnected(UserIdentity userIdentity, Redirector redirector) throws Exception 184 { 185 // this manager is always valid 186 return true; 187 } 188 189 @Override 190 public boolean nonBlockingGrantAnonymousRequest() 191 { 192 Request request = ContextHelper.getRequest(_context); 193 194 // URL without server context and leading slash. 195 String url = (String) request.getAttribute(WorkspaceMatcher.IN_WORKSPACE_URL); 196 197 return "plugins/extra-user-management/userpopulations/credentialproviders/kerberos/skip".equals(url); 198 } 199 200 @Override 201 public UserIdentity nonBlockingGetUserIdentity(Redirector redirector) throws Exception 202 { 203 Request request = ContextHelper.getRequest(_context); 204 Response response = ContextHelper.getResponse(_context); 205 206 if (!_isIPAuthorized(request)) 207 { 208 return null; 209 } 210 211 String authorization = request.getHeader("Authorization"); 212 if (authorization != null && authorization.startsWith("Negotiate ")) 213 { 214 String negotiateToken = authorization.substring("Negotiate ".length()); 215 216 if (negotiateToken.startsWith("TlRMTVNT")) 217 { 218 // Oups, this is a NTLM token => the user is not on the domain or missing SPN => give up 219 getLogger().debug("A user tried an NTLM token. Let's ignore it."); 220 return null; 221 } 222 223 getLogger().debug("Received token {}", negotiateToken); 224 225 byte[] token = Base64.decodeBase64(negotiateToken); 226 227 Session session = request.getSession(true); 228 GSSContext gssContext = (GSSContext) session.getAttribute(__SESSION_ATTRIBUTE_GSSCONTEXT); 229 if (gssContext == null) 230 { 231 getLogger().debug("Creating new GSSContext"); 232 233 gssContext = GSSManager.getInstance().createContext(_gssCredential); 234 session.setAttribute(__SESSION_ATTRIBUTE_GSSCONTEXT, gssContext); 235 } 236 else 237 { 238 getLogger().debug("Using existing GSSContext"); 239 } 240 241 byte[] kdcTokenAnswer = null; 242 try 243 { 244 kdcTokenAnswer = gssContext.acceptSecContext(token, 0, token.length); 245 } 246 catch (GSSException e) 247 { 248 _disposeContext(gssContext, session); 249 throw e; 250 } 251 252 String tokenAnswer = kdcTokenAnswer != null ? Base64.encodeBase64String(kdcTokenAnswer) : null; 253 if (!gssContext.isEstablished()) 254 { 255 // Handshake is not over, send new token 256 response.setHeader("WWW-Authenticate", "Negotiate " + tokenAnswer); 257 redirector.redirect(false, __SKIP_KERBEROS_URL); 258 getLogger().debug("Need additionnal token. Sending answer token {}", tokenAnswer); 259 return null; 260 } 261 262 GSSName gssSrcName = gssContext.getSrcName(); 263 264 if (gssSrcName == null) 265 { 266 _disposeContext(gssContext, session); 267 268 getLogger().debug("Reseting communication with client"); 269 response.setHeader("WWW-Authenticate", "Negotiate"); 270 redirector.redirect(false, __SKIP_KERBEROS_URL); 271 return null; 272 } 273 274 if (tokenAnswer != null) 275 { 276 getLogger().debug("Sending answer token {}", tokenAnswer); 277 response.setHeader("WWW-Authenticate", "Negotiate " + tokenAnswer); 278 } 279 280 String login = gssSrcName.toString(); 281 // gssSrcName should be <login>@<realm> 282 login = StringUtils.substringBefore(login, "@"); 283 284 _disposeContext(gssContext, session); 285 286 getLogger().debug("User successfully identified '{}'", login); 287 return new UserIdentity(login, null); 288 } 289 else 290 { 291 response.setHeader("WWW-Authenticate", "Negotiate"); 292 redirector.redirect(false, __SKIP_KERBEROS_URL); 293 return null; 294 } 295 } 296 297 private void _disposeContext(GSSContext gssContext, Session session) throws GSSException 298 { 299 gssContext.dispose(); 300 session.removeAttribute(__SESSION_ATTRIBUTE_GSSCONTEXT); 301 } 302 303 private boolean _isIPAuthorized(Request request) 304 { 305 if (_ipRestriction == null) 306 { 307 // There is no restriction 308 getLogger().debug("There is no IP restriction for Kerberos"); 309 return true; 310 } 311 312 // The real client IP may have been put in the non-standard "X-Forwarded-For" request header, in case of reverse proxy 313 String xff = request.getHeader("X-Forwarded-For"); 314 String ip = null; 315 316 if (xff != null) 317 { 318 ip = xff.split(",")[0]; 319 } 320 else 321 { 322 ip = request.getRemoteAddr(); 323 } 324 325 if (!_ipRestriction.matcher(ip).matches()) 326 { 327 getLogger().info("Ip '" + ip + "' was not authorized to use Kerberos authentication with filter " + _ipRestriction.pattern()); 328 return false; 329 } 330 331 return true; 332 } 333 334 @Override 335 public void nonBlockingUserNotAllowed(Redirector redirector) 336 { 337 Request request = ContextHelper.getRequest(_context); 338 Session session = request.getSession(false); 339 340 if (session != null) 341 { 342 session.removeAttribute(__SESSION_ATTRIBUTE_GSSCONTEXT); 343 } 344 } 345 346 @Override 347 public void nonBlockingUserAllowed(UserIdentity userIdentity, Redirector redirector) 348 { 349 Request request = ContextHelper.getRequest(_context); 350 Session session = request.getSession(false); 351 352 if (session != null) 353 { 354 session.removeAttribute(__SESSION_ATTRIBUTE_GSSCONTEXT); 355 } 356 } 357 358 public void dispose() 359 { 360 try 361 { 362 _gssCredential.dispose(); 363 } 364 catch (GSSException e) 365 { 366 throw new RuntimeException("Unable to dispose the GSSCredential during KerberosCredentialProvider disposal", e); 367 } 368 } 369}