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    
019    package net.sourceforge.spnego;
020    
021    import java.io.IOException;
022    import java.net.URL;
023    import java.security.PrivilegedActionException;
024    import java.security.PrivilegedExceptionAction;
025    import java.util.logging.Level;
026    import java.util.logging.Logger;
027    
028    import javax.security.auth.Subject;
029    import javax.security.auth.callback.Callback;
030    import javax.security.auth.callback.CallbackHandler;
031    import javax.security.auth.callback.NameCallback;
032    import javax.security.auth.callback.PasswordCallback;
033    import javax.servlet.http.HttpServletRequest;
034    import javax.servlet.http.HttpServletResponse;
035    
036    import net.sourceforge.spnego.SpnegoHttpFilter.Constants;
037    
038    import org.ietf.jgss.GSSContext;
039    import org.ietf.jgss.GSSCredential;
040    import org.ietf.jgss.GSSException;
041    import org.ietf.jgss.GSSManager;
042    import org.ietf.jgss.GSSName;
043    import 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     */
067    public final class SpnegoProvider {
068    
069        /** Default LOGGER. */
070        static final Logger LOGGER = Logger.getLogger(Constants.LOGGER_NAME); //NOPMD
071    
072        /** Factory for GSS-API mechanism. */
073        static final GSSManager MANAGER = GSSManager.getInstance(); // NOPMD
074    
075        /** GSS-API mechanism "1.3.6.1.5.5.2". */
076        static final Oid SPNEGO_OID = SpnegoProvider.getOid(); // NOPMD
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 SpnegoConstants.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        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    }