View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2012  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  package davmail.http;
20  
21  import org.apache.commons.codec.binary.Base64;
22  import org.apache.commons.httpclient.Credentials;
23  import org.apache.commons.httpclient.Header;
24  import org.apache.commons.httpclient.HttpMethod;
25  import org.apache.commons.httpclient.URIException;
26  import org.apache.commons.httpclient.auth.*;
27  import org.apache.commons.httpclient.util.EncodingUtil;
28  import org.ietf.jgss.GSSException;
29  
30  import javax.security.auth.login.LoginException;
31  
32  /**
33   * Implement spnego (Negotiate) authentication scheme.
34   */
35  public class SpNegoScheme implements AuthScheme {
36      private static final int UNINITIATED = 0;
37      private static final int INITIATED = 1;
38      private static final int TYPE1_MSG_GENERATED = 2;
39      private static final int TYPE2_MSG_RECEIVED = 3;
40      private static final int TYPE3_MSG_GENERATED = 4;
41      private static final int FAILED = Integer.MAX_VALUE;
42  
43      private byte[] serverToken;
44      /**
45       * Authentication process state
46       */
47      private int state;
48  
49      /**
50       * Processes the Negotiate challenge.
51       *
52       * @param challenge the challenge string
53       * @throws MalformedChallengeException is thrown if the authentication challenge is malformed
54       */
55      public void processChallenge(final String challenge) throws MalformedChallengeException {
56          String authScheme = AuthChallengeParser.extractScheme(challenge);
57          if (!authScheme.equalsIgnoreCase(getSchemeName())) {
58              throw new MalformedChallengeException("Invalid Negotiate challenge: " + challenge);
59          }
60          int spaceIndex = challenge.indexOf(' ');
61          if (spaceIndex != -1) {
62              // step 2: received server challenge
63              serverToken = Base64.decodeBase64(EncodingUtil.getBytes(
64                      challenge.substring(spaceIndex, challenge.length()).trim(), "ASCII"));
65              this.state = TYPE2_MSG_RECEIVED;
66          } else {
67              this.serverToken = null;
68              if (this.state == UNINITIATED) {
69                  this.state = INITIATED;
70              } else {
71                  this.state = FAILED;
72              }
73          }
74      }
75  
76  
77      /**
78       * Returns textual designation of the Negotiate authentication scheme.
79       *
80       * @return <code>Negotiate</code>
81       */
82      public String getSchemeName() {
83          return "Negotiate";
84      }
85  
86      /**
87       * Not used with Negotiate.
88       *
89       * @return null
90       */
91      public String getParameter(String s) {
92          return null;
93      }
94  
95      /**
96       * Not used with Negotiate.
97       *
98       * @return null
99       */
100     public String getRealm() {
101         return null;
102     }
103 
104     /**
105      * Deprecated.
106      */
107     @Deprecated
108     public String getID() {
109         throw new UnsupportedOperationException();
110     }
111 
112     /**
113      * Negotiate is connection based.
114      *
115      * @return true
116      */
117     public boolean isConnectionBased() {
118         return true;
119     }
120 
121     /**
122      * Tests if the Negotiate authentication process has been completed.
123      *
124      * @return <tt>true</tt> if authorization has been processed
125      */
126     public boolean isComplete() {
127         return state == TYPE3_MSG_GENERATED || state == FAILED;
128     }
129 
130     /**
131      * Not implemented.
132      *
133      * @param credentials user credentials
134      * @param method      method name
135      * @param uri         URI
136      * @return an Negotiate authorization string
137      * @throws org.apache.commons.httpclient.auth.InvalidCredentialsException
138      *          if authentication credentials
139      *          are not valid or not applicable for this authentication scheme
140      * @throws org.apache.commons.httpclient.auth.AuthenticationException
141      *          if authorization string cannot
142      *          be generated due to an authentication failure
143      */
144     @Deprecated
145     public String authenticate(final Credentials credentials, String method, String uri) throws AuthenticationException {
146         throw new UnsupportedOperationException();
147     }
148 
149     /**
150      * Produces Negotiate authorization string for the given set of
151      * {@link Credentials}.
152      *
153      * @param credentials The set of credentials to be used for authentication
154      * @param httpMethod  The method being authenticated
155      * @return an Negotiate authorization string
156      * @throws org.apache.commons.httpclient.auth.InvalidCredentialsException
157      *                                 if authentication credentials
158      *                                 are not valid or not applicable for this authentication scheme
159      * @throws AuthenticationException if authorization string cannot
160      *                                 be generated due to an authentication failure
161      */
162     public String authenticate(Credentials credentials, HttpMethod httpMethod) throws AuthenticationException {
163         if (this.state == UNINITIATED) {
164             throw new IllegalStateException("Negotiate authentication process has not been initiated");
165         }
166         String host = null;
167         try {
168             host = httpMethod.getURI().getHost();
169         } catch (URIException e) {
170             // ignore
171         }
172         if (host == null) {
173             Header header = httpMethod.getRequestHeader("Host");
174             if (header != null) {
175                 host = header.getValue();
176                 if (host.indexOf(':') >= 0) {
177                     host = host.substring(0, host.indexOf(':'));
178                 }
179             }
180         }
181         if (host == null) {
182             throw new IllegalStateException("Negotiate authentication failed: empty host");
183         }
184 
185         // no credentials needed
186         String response;
187         try {
188             if (this.state == INITIATED || this.state == FAILED) {
189                 // send initial token to server
190                 response = EncodingUtil.getAsciiString(Base64.encodeBase64(KerberosHelper.initSecurityContext("HTTP", host, new byte[0])));
191                 this.state = TYPE1_MSG_GENERATED;
192             } else {
193                 // send challenge response
194                 response = EncodingUtil.getAsciiString(Base64.encodeBase64(KerberosHelper.initSecurityContext("HTTP", host, serverToken)));
195                 this.state = TYPE3_MSG_GENERATED;
196             }
197         } catch (GSSException gsse) {
198             state = FAILED;
199             if (gsse.getMajor() == GSSException.DEFECTIVE_CREDENTIAL
200                     || gsse.getMajor() == GSSException.CREDENTIALS_EXPIRED)
201                 throw new InvalidCredentialsException(gsse.getMessage(), gsse);
202             if (gsse.getMajor() == GSSException.NO_CRED)
203                 throw new CredentialsNotAvailableException(gsse.getMessage(), gsse);
204             if (gsse.getMajor() == GSSException.DEFECTIVE_TOKEN
205                     || gsse.getMajor() == GSSException.DUPLICATE_TOKEN
206                     || gsse.getMajor() == GSSException.OLD_TOKEN)
207                 throw new AuthChallengeException(gsse.getMessage(), gsse);
208             // other error
209             throw new AuthenticationException(gsse.getMessage(), gsse);
210         } catch (LoginException e) {
211             state = FAILED;
212             throw new InvalidCredentialsException(e.getMessage(), e);
213         }
214         return "Negotiate " + response;
215     }
216 
217 }