This project has retired. For details please refer to its Attic page.
HttpDigestAuthentication xref
View Javadoc

1   package org.apache.archiva.redback.integration.filter.authentication.digest;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   * http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.archiva.redback.authentication.AuthenticationException;
23  import org.apache.archiva.redback.policy.MustChangePasswordException;
24  import org.apache.archiva.redback.users.User;
25  import org.apache.archiva.redback.users.UserManagerException;
26  import org.apache.commons.codec.binary.Base64;
27  import org.apache.archiva.redback.authentication.AuthenticationResult;
28  import org.apache.archiva.redback.authentication.TokenBasedAuthenticationDataSource;
29  import org.apache.archiva.redback.policy.AccountLockedException;
30  import org.apache.archiva.redback.users.UserManager;
31  import org.apache.archiva.redback.users.UserNotFoundException;
32  import org.apache.archiva.redback.integration.filter.authentication.HttpAuthenticationException;
33  import org.apache.archiva.redback.integration.filter.authentication.HttpAuthenticator;
34  import org.apache.commons.lang.StringUtils;
35  import org.springframework.stereotype.Service;
36  
37  import javax.inject.Inject;
38  import javax.inject.Named;
39  import javax.servlet.http.HttpServletRequest;
40  import javax.servlet.http.HttpServletResponse;
41  import javax.servlet.http.HttpSession;
42  import java.io.IOException;
43  
44  /**
45   * HttpDigestAuthentication methods for working with <a href="http://www.faqs.org/rfcs/rfc2617.html">RFC 2617 HTTP Authentication</a>.
46   *
47   * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a>
48   */
49  @Service("httpAuthenticator#digest")
50  public class HttpDigestAuthentication
51      extends HttpAuthenticator
52  {
53      @Inject
54      @Named(value = "userManager#default")
55      private UserManager userManager;
56  
57      /**
58       *
59       */
60      private int nonceLifetimeSeconds = 300;
61  
62      /**
63       * NOTE: Must be alphanumeric.
64       */
65      private String digestKey = "OrycteropusAfer";
66  
67      private String realm;
68  
69      public String getId()
70      {
71          return HttpDigestAuthentication.class.getName();
72      }
73  
74      public AuthenticationResult getAuthenticationResult( HttpServletRequest request, HttpServletResponse response )
75          throws AuthenticationException, AccountLockedException, MustChangePasswordException
76      {
77          HttpSession httpSession = request.getSession( true );
78          if ( isAlreadyAuthenticated( httpSession ) )
79          {
80              return getSecuritySession( httpSession ).getAuthenticationResult();
81          }
82  
83          TokenBasedAuthenticationDataSource authDataSource = new TokenBasedAuthenticationDataSource();
84          String authHeader = request.getHeader( "Authorization" );
85  
86          // in tomcat this is : authorization=Basic YWRtaW46TWFuYWdlMDc=
87          if ( authHeader == null )
88          {
89              authHeader = request.getHeader( "authorization" );
90          }
91  
92          if ( ( authHeader != null ) && authHeader.startsWith( "Digest " ) )
93          {
94              String rawDigestHeader = authHeader.substring( 7 );
95  
96              HttpDigestHeader digestHeader = new HttpDigestHeader();
97              digestHeader.parseClientHeader( rawDigestHeader, getRealm(), digestKey );
98  
99              // Lookup password for presented username
100             User user = findUser( digestHeader.username );
101             authDataSource.setPrincipal( user.getUsername() );
102 
103             String serverSideHash = generateDigestHash( digestHeader, user.getPassword(), request.getMethod() );
104 
105             if ( !StringUtils.equals( serverSideHash, digestHeader.response ) )
106             {
107                 throw new HttpAuthenticationException( "Digest response was invalid." );
108             }
109         }
110 
111         return super.authenticate( authDataSource, httpSession );
112     }
113 
114     public User findUser( String username )
115         throws HttpAuthenticationException
116     {
117         try
118         {
119             return userManager.findUser( username );
120         }
121         catch ( UserNotFoundException e )
122         {
123             String msg = "Unable to find primary user '" + username + "'.";
124             log.error( msg, e );
125             throw new HttpAuthenticationException( msg, e );
126         }
127         catch ( UserManagerException e )
128         {
129             log.error( "issue find user {}, message: {}", username, e.getMessage(), e );
130             throw new HttpAuthenticationException( "issue find user " + username + ", message: " + e.getMessage(), e );
131         }
132     }
133 
134     /**
135      * Issue HTTP Digest Authentication Challenge
136      *
137      * @param request   the request to use.
138      * @param response  the response to use.
139      * @param realmName the realm name to state.
140      * @param exception the exception to base the message off of.
141      * @throws IOException if there was a problem with the {@link HttpServletResponse#sendError(int, String)} call.
142      */
143     public void challenge( HttpServletRequest request, HttpServletResponse response, String realmName,
144                            AuthenticationException exception )
145         throws IOException
146     {
147         // The Challenge Header
148         StringBuilder authHeader = new StringBuilder();
149         authHeader.append( "Digest " );
150         // [REQUIRED] The name to appear in the dialog box to the user.
151         authHeader.append( "realm=\"" ).append( realmName ).append( "\"" );
152         // [OPTIONAL] We do not use the optional 'domain' header.
153         // authHeader.append( "domain=\"" ).append( domain ).append( "\"" );
154         // [REQUIRED] Nonce specification.
155         authHeader.append( ", nonce=\"" );
156         long timestamp = System.currentTimeMillis() + ( nonceLifetimeSeconds * 1000 );
157         // Not using ETag from RFC 2617 intentionally.
158         String hraw = String.valueOf( timestamp ) + ":" + digestKey;
159         String rawnonce = String.valueOf( timestamp ) + ":" + Digest.md5Hex( hraw );
160         authHeader.append( Base64.encodeBase64( rawnonce.getBytes() ) );
161         authHeader.append( "\"" );
162         // [REQUIRED] The RFC 2617 Quality of Protection.
163         // MSIE Appears to only support 'auth'
164         // Do not use 'opaque' here. (Your MSIE users will have issues)
165         authHeader.append( ", qop=\"auth\"" );
166         // [BROKEN] since we force the 'auth' qop we cannot use the opaque option.
167         // authHeader.append( ", opaque=\"").append(opaqueString).append("\"");
168 
169         // [OPTIONAL] Use of the stale option is reserved for expired nonce strings.
170         if ( exception instanceof NonceExpirationException )
171         {
172             authHeader.append( ", stale=\"true\"" );
173         }
174 
175         // [OPTIONAL] We do not use the optional Algorithm header.
176         // authHeader.append( ", algorithm=\"MD5\"");
177 
178         response.addHeader( "WWW-Authenticate", authHeader.toString() );
179         response.sendError( HttpServletResponse.SC_UNAUTHORIZED, exception.getMessage() );
180     }
181 
182     private String generateDigestHash( HttpDigestHeader digestHeader, String password, String httpMethod )
183     {
184         String a1 = Digest.md5Hex( digestHeader.username + ":" + realm + ":" + password );
185         String a2 = Digest.md5Hex( httpMethod + ":" + digestHeader.uri );
186 
187         String digest;
188 
189         if ( StringUtils.isEmpty( digestHeader.qop ) )
190         {
191             digest = a1 + ":" + digestHeader.nonce + ":" + a2;
192         }
193         else if ( StringUtils.equals( "auth", digestHeader.qop ) )
194         {
195             digest = a1 + ":" + digestHeader.nonce + ":" + digestHeader.nc + ":" + digestHeader.cnonce + ":"
196                 + digestHeader.qop + ":" + a2;
197         }
198         else
199         {
200             throw new IllegalStateException(
201                 "Http Digest Parameter [qop] with value of [" + digestHeader.qop + "] is unsupported." );
202         }
203 
204         return Digest.md5Hex( digest );
205     }
206 
207     public String getRealm()
208     {
209         return realm;
210     }
211 
212     public void setRealm( String realm )
213     {
214         this.realm = realm;
215     }
216 
217 }