001/** 002 * Copyright (C) 2009 "Darwin V. Felix" <darwinfelix@users.sourceforge.net> 003 * 004 * This library is free software; you can redistribute it and/or 005 * modify it under the terms of the GNU Lesser General Public 006 * License as published by the Free Software Foundation; either 007 * version 2.1 of the License, or (at your option) any later version. 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * You should have received a copy of the GNU Lesser General Public 015 * License along with this library; if not, write to the Free Software 016 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 017 */ 018 019package net.sourceforge.spnego; 020 021import java.io.IOException; 022import java.net.URL; 023import java.security.PrivilegedActionException; 024import java.security.PrivilegedExceptionAction; 025import java.util.logging.Level; 026import java.util.logging.Logger; 027 028import javax.security.auth.Subject; 029import javax.security.auth.callback.Callback; 030import javax.security.auth.callback.CallbackHandler; 031import javax.security.auth.callback.NameCallback; 032import javax.security.auth.callback.PasswordCallback; 033import javax.servlet.http.HttpServletRequest; 034import javax.servlet.http.HttpServletResponse; 035 036import net.sourceforge.spnego.SpnegoHttpFilter.Constants; 037 038import org.ietf.jgss.GSSContext; 039import org.ietf.jgss.GSSCredential; 040import org.ietf.jgss.GSSException; 041import org.ietf.jgss.GSSManager; 042import org.ietf.jgss.GSSName; 043import org.ietf.jgss.Oid; 044 045/** 046 * This is a Utility Class that can be used for finer grained control 047 * over message integrity, confidentiality and mutual authentication. 048 * 049 * <p> 050 * This Class is exposed for developers who want to implement a custom 051 * HTTP client. 052 * </p> 053 * 054 * <p> 055 * Take a look at the {@link SpnegoHttpURLConnection} class and the 056 * {@link SpnegoHttpFilter} class before attempting to implement your 057 * own HTTP client. 058 * </p> 059 * 060 * <p>For more example usage, see the documentation at 061 * <a href="http://spnego.sourceforge.net" target="_blank">http://spnego.sourceforge.net</a> 062 * </p> 063 * 064 * @author Darwin V. Felix 065 * 066 */ 067public final class SpnegoProvider { 068 069 /** Default LOGGER. */ 070 private static final Logger LOGGER = Logger.getLogger(SpnegoProvider.class.getName()); 071 072 /** Factory for GSS-API mechanism. */ 073 private static final GSSManager MANAGER = GSSManager.getInstance(); 074 075 /** GSS-API mechanism "1.3.6.1.5.5.2". */ 076 private static final Oid SPNEGO_OID = SpnegoProvider.getOid(); 077 078 /* 079 * This is a utility class (not a Singleton). 080 */ 081 private SpnegoProvider() { 082 // default private 083 } 084 085 /** 086 * Returns the {@link SpnegoAuthScheme} mechanism used to authenticate 087 * the request. 088 * 089 * <p> 090 * This method may return null in which case you must check the HTTP 091 * Status Code to determine if additional processing is required. 092 * <br /> 093 * For example, if req. did not contain the Constants.AUTHZ_HEADER, 094 * the HTTP Status Code SC_UNAUTHORIZED will be set and the client must 095 * send authentication information on the next request. 096 * </p> 097 * 098 * @param req servlet request 099 * @param resp servlet response 100 * @param basicSupported pass true to offer/allow BASIC Authentication 101 * @param promptIfNtlm pass true ntlm request should be downgraded 102 * @param realm should be the realm the server used to pre-authenticate 103 * @return null if negotiation needs to continue or failed 104 * @throws IOException 105 */ 106 static SpnegoAuthScheme negotiate( 107 final HttpServletRequest req, final SpnegoHttpServletResponse resp 108 , final boolean basicSupported, final boolean promptIfNtlm 109 , final String realm) throws IOException { 110 111 final SpnegoAuthScheme scheme = SpnegoProvider.getAuthScheme( 112 req.getHeader(Constants.AUTHZ_HEADER)); 113 114 if (null == scheme || scheme.getToken().length == 0) { 115 LOGGER.finer("Header Token was NULL"); 116 resp.setHeader(Constants.AUTHN_HEADER, Constants.NEGOTIATE_HEADER); 117 118 if (basicSupported) { 119 resp.addHeader(Constants.AUTHN_HEADER, 120 Constants.BASIC_HEADER + " realm=\"" + realm + '\"'); 121 } else { 122 LOGGER.finer("Basic NOT offered: Not Enabled or SSL Required."); 123 } 124 125 resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED, true); 126 127 return null; 128 129 } 130 131 // assert 132 if (scheme.isNtlmToken()) { 133 LOGGER.warning("Downgrade NTLM request to Basic Auth."); 134 135 if (resp.isStatusSet()) { 136 throw new IllegalStateException("HTTP Status already set."); 137 } 138 139 if (basicSupported && promptIfNtlm) { 140 resp.setHeader(Constants.AUTHN_HEADER, 141 Constants.BASIC_HEADER + " realm=\"" + realm + '\"'); 142 } else { 143 // TODO : decode/decrypt NTLM token and return a new SpnegoAuthScheme 144 // of type "Basic" where the token value is a base64 encoded 145 // username + ":" + password string 146 throw new UnsupportedOperationException("NTLM specified. Downgraded to " 147 + "Basic Auth (and/or SSL) but downgrade not supported."); 148 } 149 150 resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED, true); 151 152 return null; 153 } 154 155 return scheme; 156 } 157 158 /** 159 * Returns the GSS-API interface for creating a security context. 160 * 161 * @param subject the person to be authenticated 162 * @return GSSCredential to be used for creating a security context. 163 * @throws PrivilegedActionException 164 */ 165 public static GSSCredential getClientCredential(final Subject subject) 166 throws PrivilegedActionException { 167 168 final PrivilegedExceptionAction<GSSCredential> action = 169 new PrivilegedExceptionAction<GSSCredential>() { 170 public GSSCredential run() throws GSSException { 171 return MANAGER.createCredential( 172 null 173 , GSSCredential.DEFAULT_LIFETIME 174 , SpnegoProvider.SPNEGO_OID 175 , GSSCredential.INITIATE_ONLY); 176 } 177 }; 178 179 return Subject.doAs(subject, action); 180 } 181 182 /** 183 * Returns a GSSContext to be used by custom clients to set 184 * data integrity requirements, confidentiality and if mutual 185 * authentication is required. 186 * 187 * @param creds credentials of the person to be authenticated 188 * @param url HTTP address of server (used for constructing a {@link GSSName}). 189 * @return GSSContext 190 * @throws GSSException 191 * @throws PrivilegedActionException 192 */ 193 public static GSSContext getGSSContext(final GSSCredential creds, final URL url) 194 throws GSSException { 195 196 return MANAGER.createContext(SpnegoProvider.getServerName(url) 197 , SpnegoProvider.SPNEGO_OID 198 , creds 199 , GSSContext.DEFAULT_LIFETIME); 200 } 201 202 /** 203 * Returns the {@link SpnegoAuthScheme} or null if header is missing. 204 * 205 * <p> 206 * Throws UnsupportedOperationException if header is NOT Negotiate 207 * or Basic. 208 * </p> 209 * 210 * @param header ex. Negotiate or Basic 211 * @return null if header missing/null else the auth scheme 212 */ 213 public static SpnegoAuthScheme getAuthScheme(final String header) { 214 215 if (null == header || header.isEmpty()) { 216 LOGGER.finer("authorization header was missing/null"); 217 return null; 218 219 } else if (header.startsWith(Constants.NEGOTIATE_HEADER)) { 220 final String token = header.substring(Constants.NEGOTIATE_HEADER.length() + 1); 221 return new SpnegoAuthScheme(Constants.NEGOTIATE_HEADER, token); 222 223 } else if (header.startsWith(Constants.BASIC_HEADER)) { 224 final String token = header.substring(Constants.BASIC_HEADER.length() + 1); 225 return new SpnegoAuthScheme(Constants.BASIC_HEADER, token); 226 227 } else { 228 throw new UnsupportedOperationException("Negotiate or Basic Only:" + header); 229 } 230 } 231 232 /** 233 * Returns the Universal Object Identifier representation of 234 * the SPNEGO mechanism. 235 * 236 * @return Object Identifier of the GSS-API mechanism 237 */ 238 private static Oid getOid() { 239 Oid oid = null; 240 try { 241 oid = new Oid("1.3.6.1.5.5.2"); 242 } catch (GSSException gsse) { 243 LOGGER.log(Level.SEVERE, "Unable to create OID 1.3.6.1.5.5.2 !", gsse); 244 } 245 return oid; 246 } 247 248 /** 249 * Returns the {@link GSSCredential} the server uses for pre-authentication. 250 * 251 * @param subject account server uses for pre-authentication 252 * @return credential that allows server to authenticate clients 253 * @throws PrivilegedActionException 254 */ 255 static GSSCredential getServerCredential(final Subject subject) 256 throws PrivilegedActionException { 257 258 final PrivilegedExceptionAction<GSSCredential> action = 259 new PrivilegedExceptionAction<GSSCredential>() { 260 public GSSCredential run() throws GSSException { 261 return MANAGER.createCredential( 262 null 263 , GSSCredential.INDEFINITE_LIFETIME 264 , SpnegoProvider.SPNEGO_OID 265 , GSSCredential.ACCEPT_ONLY); 266 } 267 }; 268 return Subject.doAs(subject, action); 269 } 270 271 /** 272 * Returns the {@link GSSName} constructed out of the passed-in 273 * URL object. 274 * 275 * @param url HTTP address of server 276 * @return GSSName of URL. 277 * @throws GSSException 278 */ 279 private static GSSName getServerName(final URL url) throws GSSException { 280 return MANAGER.createName("HTTP@" + url.getHost(), 281 GSSName.NT_HOSTBASED_SERVICE, SpnegoProvider.SPNEGO_OID); 282 } 283 284 /** 285 * Used by the BASIC Auth mechanism for establishing a LoginContext 286 * to authenticate a client/caller/request. 287 * 288 * @param username client username 289 * @param password client password 290 * @return CallbackHandler to be used for establishing a LoginContext 291 */ 292 public static CallbackHandler getUsernamePasswordHandler( 293 final String username, final String password) { 294 295 LOGGER.fine("username=" + username + "; password=" + password.hashCode()); 296 297 final CallbackHandler handler = new CallbackHandler() { 298 public void handle(final Callback[] callback) { 299 for (int i=0; i<callback.length; i++) { 300 if (callback[i] instanceof NameCallback) { 301 final NameCallback nameCallback = (NameCallback) callback[i]; 302 nameCallback.setName(username); 303 } else if (callback[i] instanceof PasswordCallback) { 304 final PasswordCallback passCallback = (PasswordCallback) callback[i]; 305 passCallback.setPassword(password.toCharArray()); 306 } else { 307 LOGGER.warning("Unsupported Callback i=" + i + "; class=" 308 + callback[i].getClass().getName()); 309 } 310 } 311 } 312 }; 313 314 return handler; 315 } 316}