1 package org.codehaus.redback.integration.filter.authentication.digest;
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
12 * http://www.apache.org/licenses/LICENSE-2.0
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
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.commons.codec.binary.Base64;
26 import org.apache.archiva.redback.authentication.AuthenticationResult;
27 import org.apache.archiva.redback.authentication.TokenBasedAuthenticationDataSource;
28 import org.apache.archiva.redback.policy.AccountLockedException;
29 import org.apache.archiva.redback.users.UserManager;
30 import org.apache.archiva.redback.users.UserNotFoundException;
31 import org.codehaus.plexus.util.StringUtils;
32 import org.codehaus.redback.integration.filter.authentication.HttpAuthenticationException;
33 import org.codehaus.redback.integration.filter.authentication.HttpAuthenticator;
34 import org.springframework.stereotype.Service;
36 import javax.inject.Inject;
37 import javax.inject.Named;
38 import javax.servlet.http.HttpServletRequest;
39 import javax.servlet.http.HttpServletResponse;
40 import javax.servlet.http.HttpSession;
41 import java.io.IOException;
44 * HttpDigestAuthentication methods for working with <a href="http://www.faqs.org/rfcs/rfc2617.html">RFC 2617 HTTP Authentication</a>.
46 * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a>
49 @Service("httpAuthenticator#digest")
50 public class HttpDigestAuthentication
51 extends HttpAuthenticator
54 @Named(value="userManager#configurable")
55 private UserManager userManager;
60 private int nonceLifetimeSeconds = 300;
63 * NOTE: Must be alphanumeric.
67 private String digestKey ="OrycteropusAfer";
73 return HttpDigestAuthentication.class.getName();
76 public AuthenticationResult getAuthenticationResult( HttpServletRequest request, HttpServletResponse response )
77 throws AuthenticationException, AccountLockedException, MustChangePasswordException
79 HttpSession httpSession = request.getSession( true );
80 if ( isAlreadyAuthenticated( httpSession ) )
82 return getSecuritySession( httpSession ).getAuthenticationResult();
85 TokenBasedAuthenticationDataSource authDataSource = new TokenBasedAuthenticationDataSource();
86 String authHeader = request.getHeader( "Authorization" );
88 // in tomcat this is : authorization=Basic YWRtaW46TWFuYWdlMDc=
89 if ( authHeader == null )
91 authHeader = request.getHeader( "authorization" );
94 if ( ( authHeader != null ) && authHeader.startsWith( "Digest " ) )
96 String rawDigestHeader = authHeader.substring( 7 );
98 HttpDigestHeader digestHeader = new HttpDigestHeader();
99 digestHeader.parseClientHeader( rawDigestHeader, getRealm(), digestKey );
101 // Lookup password for presented username
102 User user = findUser( digestHeader.username );
103 authDataSource.setPrincipal( user.getPrincipal().toString() );
105 String serverSideHash = generateDigestHash( digestHeader, user.getPassword(), request.getMethod() );
107 if ( !StringUtils.equals( serverSideHash, digestHeader.response ) )
109 throw new HttpAuthenticationException( "Digest response was invalid." );
113 return super.authenticate( authDataSource, httpSession );
116 public User findUser( String username )
117 throws HttpAuthenticationException
121 return userManager.findUser( username );
123 catch ( UserNotFoundException e )
125 String msg = "Unable to find primary user '" + username + "'.";
127 throw new HttpAuthenticationException( msg, e );
132 * Issue HTTP Digest Authentication Challenge
134 * @param request the request to use.
135 * @param response the response to use.
136 * @param realmName the realm name to state.
137 * @param exception the exception to base the message off of.
138 * @throws IOException if there was a problem with the {@link HttpServletResponse#sendError(int,String)} call.
140 public void challenge( HttpServletRequest request, HttpServletResponse response, String realmName,
141 AuthenticationException exception )
144 // The Challenge Header
145 StringBuilder authHeader = new StringBuilder();
146 authHeader.append( "Digest " );
147 // [REQUIRED] The name to appear in the dialog box to the user.
148 authHeader.append( "realm=\"" ).append( realmName ).append( "\"" );
149 // [OPTIONAL] We do not use the optional 'domain' header.
150 // authHeader.append( "domain=\"" ).append( domain ).append( "\"" );
151 // [REQUIRED] Nonce specification.
152 authHeader.append( ", nonce=\"" );
153 long timestamp = System.currentTimeMillis() + ( nonceLifetimeSeconds * 1000 );
154 // Not using ETag from RFC 2617 intentionally.
155 String hraw = String.valueOf( timestamp ) + ":" + digestKey;
156 String rawnonce = String.valueOf( timestamp ) + ":" + Digest.md5Hex( hraw );
157 authHeader.append( Base64.encodeBase64( rawnonce.getBytes() ) );
158 authHeader.append( "\"" );
159 // [REQUIRED] The RFC 2617 Quality of Protection.
160 // MSIE Appears to only support 'auth'
161 // Do not use 'opaque' here. (Your MSIE users will have issues)
162 authHeader.append( ", qop=\"auth\"" );
163 // [BROKEN] since we force the 'auth' qop we cannot use the opaque option.
164 // authHeader.append( ", opaque=\"").append(opaqueString).append("\"");
166 // [OPTIONAL] Use of the stale option is reserved for expired nonce strings.
167 if ( exception instanceof NonceExpirationException )
169 authHeader.append( ", stale=\"true\"" );
172 // [OPTIONAL] We do not use the optional Algorithm header.
173 // authHeader.append( ", algorithm=\"MD5\"");
175 response.addHeader( "WWW-Authenticate", authHeader.toString() );
176 response.sendError( HttpServletResponse.SC_UNAUTHORIZED, exception.getMessage() );
179 private String generateDigestHash( HttpDigestHeader digestHeader, String password, String httpMethod )
181 String a1 = Digest.md5Hex( digestHeader.username + ":" + realm + ":" + password );
182 String a2 = Digest.md5Hex( httpMethod + ":" + digestHeader.uri );
186 if ( StringUtils.isEmpty( digestHeader.qop ) )
188 digest = a1 + ":" + digestHeader.nonce + ":" + a2;
190 else if ( StringUtils.equals( "auth", digestHeader.qop ) )
192 digest = a1 + ":" + digestHeader.nonce + ":" + digestHeader.nc + ":" + digestHeader.cnonce + ":"
193 + digestHeader.qop + ":" + a2;
197 throw new IllegalStateException( "Http Digest Parameter [qop] with value of [" + digestHeader.qop
198 + "] is unsupported." );
201 return Digest.md5Hex( digest );
204 public String getRealm()
209 public void setRealm( String realm )