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