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}