1 package gov.nist.javax.sip.clientauthutils; 2 3 /* 4 * 5 * This code has been contributed with permission from: 6 * 7 * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client but has been significantly changed. 8 * It is donated to the JAIN-SIP project as it is common code that many sip clients 9 * need to perform class and others will consitute a set of utility functions 10 * that will implement common operations that ease the life of the developer. 11 * 12 * Acknowledgements: 13 * ---------------- 14 * 15 * Fredrik Wickstrom reported that dialog cseq counters are not incremented 16 * when resending requests. He later uncovered additional problems and 17 * proposed a way to fix them (his proposition was taken into account). 18 */ 19 20 import gov.nist.javax.sip.SipStackImpl; 21 import gov.nist.javax.sip.address.SipUri; 22 import gov.nist.javax.sip.message.SIPRequest; 23 import gov.nist.javax.sip.stack.SIPClientTransaction; 24 import gov.nist.javax.sip.stack.SIPTransactionStack; 25 26 import java.text.ParseException; 27 import java.util.Collection; 28 import java.util.Iterator; 29 import java.util.ListIterator; 30 import java.util.Timer; 31 32 import javax.sip.ClientTransaction; 33 import javax.sip.DialogState; 34 import javax.sip.InvalidArgumentException; 35 import javax.sip.SipException; 36 import javax.sip.SipProvider; 37 import javax.sip.address.Hop; 38 import javax.sip.address.SipURI; 39 import javax.sip.address.URI; 40 import javax.sip.header.AuthorizationHeader; 41 import javax.sip.header.CSeqHeader; 42 import javax.sip.header.Header; 43 import javax.sip.header.HeaderFactory; 44 import javax.sip.header.ProxyAuthenticateHeader; 45 import javax.sip.header.ProxyAuthorizationHeader; 46 import javax.sip.header.ViaHeader; 47 import javax.sip.header.WWWAuthenticateHeader; 48 import javax.sip.message.Request; 49 import javax.sip.message.Response; 50 51 /** 52 * The class handles authentication challenges, caches user credentials and takes care (through 53 * the SecurityAuthority interface) about retrieving passwords. 54 * 55 * 56 * @author Emil Ivov 57 * @author Jeroen van Bemmel 58 * @author M. Ranganathan 59 * 60 * @since 2.0 61 */ 62 63 public class AuthenticationHelperImpl implements AuthenticationHelper { 64 65 /** 66 * Credentials cached so far. 67 */ 68 private CredentialsCache cachedCredentials; 69 70 /** 71 * The account manager for the system. Stores user credentials. 72 */ 73 private Object accountManager = null; 74 75 /* 76 * Header factory for this security manager. 77 */ 78 private HeaderFactory headerFactory; 79 80 private SipStackImpl sipStack; 81 82 Timer timer; 83 84 /** 85 * Default constructor for the security manager. There is one Account manager. There is one 86 * SipSecurity manager for every user name, 87 * 88 * @param sipStack -- our stack. 89 * @param accountManager -- an implementation of the AccountManager interface. 90 * @param headerFactory -- header factory. 91 */ 92 public AuthenticationHelperImpl(SipStackImpl sipStack, AccountManager accountManager, 93 HeaderFactory headerFactory) { 94 this.accountManager = accountManager; 95 this.headerFactory = headerFactory; 96 this.sipStack = sipStack; 97 98 this.cachedCredentials = new CredentialsCache(((SIPTransactionStack) sipStack).getTimer()); 99 } 100 101 /** 102 * Default constructor for the security manager. There is one Account manager. There is one 103 * SipSecurity manager for every user name, 104 * 105 * @param sipStack -- our stack. 106 * @param accountManager -- an implementation of the AccountManager interface. 107 * @param headerFactory -- header factory. 108 */ 109 public AuthenticationHelperImpl(SipStackImpl sipStack, SecureAccountManager accountManager, 110 HeaderFactory headerFactory) { 111 this.accountManager = accountManager; 112 this.headerFactory = headerFactory; 113 this.sipStack = sipStack; 114 115 this.cachedCredentials = new CredentialsCache(((SIPTransactionStack) sipStack).getTimer()); 116 } 117 118 119 /* 120 * (non-Javadoc) 121 * 122 * @see gov.nist.javax.sip.clientauthutils.AuthenticationHelper#handleChallenge(javax.sip.message.Response, 123 * javax.sip.ClientTransaction, javax.sip.SipProvider) 124 */ 125 public ClientTransaction handleChallenge(Response challenge, 126 ClientTransaction challengedTransaction, SipProvider transactionCreator, int cacheTime) 127 throws SipException, NullPointerException { 128 try { 129 if (sipStack.isLoggingEnabled()) { 130 sipStack.getStackLogger().logDebug("handleChallenge: " + challenge); 131 } 132 133 SIPRequest challengedRequest = ((SIPRequest) challengedTransaction.getRequest()); 134 135 Request reoriginatedRequest = null; 136 /* 137 * If the challenged request is part of a Dialog and the 138 * Dialog is confirmed the re-originated request should be 139 * generated as an in-Dialog request. 140 */ 141 if ( challengedRequest.getToTag() != null || 142 challengedTransaction.getDialog() == null || 143 challengedTransaction.getDialog().getState() != DialogState.CONFIRMED) { 144 reoriginatedRequest = (Request) challengedRequest.clone(); 145 } else { 146 /* 147 * Re-originate the request by consulting the dialog. In particular 148 * the route set could change between the original request and the 149 * in-dialog challenge. 150 */ 151 reoriginatedRequest = 152 challengedTransaction.getDialog().createRequest(challengedRequest.getMethod()); 153 Iterator<String> headerNames = challengedRequest.getHeaderNames(); 154 while (headerNames.hasNext()) { 155 String headerName = headerNames.next(); 156 if ( reoriginatedRequest.getHeader(headerName) != null) { 157 ListIterator<Header> iterator = reoriginatedRequest.getHeaders(headerName); 158 while (iterator.hasNext()) { 159 reoriginatedRequest.addHeader(iterator.next()); 160 } 161 } 162 } 163 } 164 165 166 167 // remove the branch id so that we could use the request in a new 168 // transaction 169 removeBranchID(reoriginatedRequest); 170 171 if (challenge == null || reoriginatedRequest == null) { 172 throw new NullPointerException("A null argument was passed to handle challenge."); 173 } 174 175 ListIterator authHeaders = null; 176 177 if (challenge.getStatusCode() == Response.UNAUTHORIZED) { 178 authHeaders = challenge.getHeaders(WWWAuthenticateHeader.NAME); 179 } else if (challenge.getStatusCode() == Response.PROXY_AUTHENTICATION_REQUIRED) { 180 authHeaders = challenge.getHeaders(ProxyAuthenticateHeader.NAME); 181 } else { 182 throw new IllegalArgumentException("Unexpected status code "); 183 } 184 185 if (authHeaders == null) { 186 throw new IllegalArgumentException( 187 "Could not find WWWAuthenticate or ProxyAuthenticate headers"); 188 } 189 190 // Remove all authorization headers from the request (we'll re-add them 191 // from cache) 192 reoriginatedRequest.removeHeader(AuthorizationHeader.NAME); 193 reoriginatedRequest.removeHeader(ProxyAuthorizationHeader.NAME); 194 195 // rfc 3261 says that the cseq header should be augmented for the new 196 // request. do it here so that the new dialog (created together with 197 // the new client transaction) takes it into account. 198 // Bug report - Fredrik Wickstrom 199 CSeqHeader cSeq = (CSeqHeader) reoriginatedRequest.getHeader((CSeqHeader.NAME)); 200 try { 201 cSeq.setSeqNumber(cSeq.getSeqNumber() + 1l); 202 } catch (InvalidArgumentException ex) { 203 throw new SipException("Invalid CSeq -- could not increment : " 204 + cSeq.getSeqNumber()); 205 } 206 207 208 /* Resolve this to the next hop based on the previous lookup. If we are not using 209 * lose routing (RFC2543) then just attach hop as a maddr param. 210 */ 211 if ( challengedRequest.getRouteHeaders() == null ) { 212 Hop hop = ((SIPClientTransaction) challengedTransaction).getNextHop(); 213 SipURI sipUri = (SipURI) reoriginatedRequest.getRequestURI(); 214 sipUri.setMAddrParam(hop.getHost()); 215 if ( hop.getPort() != -1 ) sipUri.setPort(hop.getPort()); 216 } 217 ClientTransaction retryTran = transactionCreator 218 .getNewClientTransaction(reoriginatedRequest); 219 220 WWWAuthenticateHeader authHeader = null; 221 SipURI requestUri = (SipURI) challengedTransaction.getRequest().getRequestURI(); 222 while (authHeaders.hasNext()) { 223 authHeader = (WWWAuthenticateHeader) authHeaders.next(); 224 String realm = authHeader.getRealm(); 225 AuthorizationHeader authorization = null; 226 String sipDomain; 227 if ( this.accountManager instanceof SecureAccountManager ) { 228 UserCredentialHash credHash = 229 ((SecureAccountManager)this.accountManager).getCredentialHash(challengedTransaction,realm); 230 URI uri = reoriginatedRequest.getRequestURI(); 231 sipDomain = credHash.getSipDomain(); 232 authorization = this.getAuthorization(reoriginatedRequest 233 .getMethod(), uri.toString(), 234 (reoriginatedRequest.getContent() == null) ? "" : new String( 235 reoriginatedRequest.getRawContent()), authHeader, credHash); 236 } else { 237 UserCredentials userCreds = ((AccountManager) this.accountManager).getCredentials(challengedTransaction, realm); 238 sipDomain = userCreds.getSipDomain(); 239 if (userCreds == null) 240 throw new SipException( 241 "Cannot find user creds for the given user name and realm"); 242 243 // we haven't yet authenticated this realm since we were 244 // started. 245 246 authorization = this.getAuthorization(reoriginatedRequest 247 .getMethod(), reoriginatedRequest.getRequestURI().toString(), 248 (reoriginatedRequest.getContent() == null) ? "" : new String( 249 reoriginatedRequest.getRawContent()), authHeader, userCreds); 250 } 251 if (sipStack.isLoggingEnabled()) 252 sipStack.getStackLogger().logDebug( 253 "Created authorization header: " + authorization.toString()); 254 255 if (cacheTime != 0) 256 cachedCredentials.cacheAuthorizationHeader(sipDomain, 257 authorization, cacheTime); 258 259 reoriginatedRequest.addHeader(authorization); 260 } 261 262 if (sipStack.isLoggingEnabled()) { 263 sipStack.getStackLogger().logDebug( 264 "Returning authorization transaction." + retryTran); 265 } 266 return retryTran; 267 } catch (SipException ex) { 268 throw ex; 269 } catch (Exception ex) { 270 sipStack.getStackLogger().logError("Unexpected exception ", ex); 271 throw new SipException("Unexpected exception ", ex); 272 } 273 } 274 275 276 277 278 /** 279 * Generates an authorisation header in response to wwwAuthHeader. 280 * 281 * @param method method of the request being authenticated 282 * @param uri digest-uri 283 * @param requestBody the body of the request. 284 * @param authHeader the challenge that we should respond to 285 * @param userCredentials username and pass 286 * 287 * @return an authorisation header in response to authHeader. 288 * 289 * @throws OperationFailedException if auth header was malformated. 290 */ 291 private AuthorizationHeader getAuthorization(String method, String uri, String requestBody, 292 WWWAuthenticateHeader authHeader, UserCredentials userCredentials) { 293 String response = null; 294 295 // JvB: authHeader.getQop() is a quoted _list_ of qop values 296 // (e.g. "auth,auth-int") Client is supposed to pick one 297 String qopList = authHeader.getQop(); 298 String qop = (qopList != null) ? "auth" : null; 299 String nc_value = "00000001"; 300 String cnonce = "xyz"; 301 302 response = MessageDigestAlgorithm.calculateResponse(authHeader.getAlgorithm(), 303 userCredentials.getUserName(), authHeader.getRealm(), userCredentials 304 .getPassword(), authHeader.getNonce(), nc_value, // JvB added 305 cnonce, // JvB added 306 method, uri, requestBody, qop,sipStack.getStackLogger());// jvb changed 307 308 AuthorizationHeader authorization = null; 309 try { 310 if (authHeader instanceof ProxyAuthenticateHeader) { 311 authorization = headerFactory.createProxyAuthorizationHeader(authHeader 312 .getScheme()); 313 } else { 314 authorization = headerFactory.createAuthorizationHeader(authHeader.getScheme()); 315 } 316 317 authorization.setUsername(userCredentials.getUserName()); 318 authorization.setRealm(authHeader.getRealm()); 319 authorization.setNonce(authHeader.getNonce()); 320 authorization.setParameter("uri", uri); 321 authorization.setResponse(response); 322 if (authHeader.getAlgorithm() != null) { 323 authorization.setAlgorithm(authHeader.getAlgorithm()); 324 } 325 326 if (authHeader.getOpaque() != null) { 327 authorization.setOpaque(authHeader.getOpaque()); 328 } 329 330 // jvb added 331 if (qop != null) { 332 authorization.setQop(qop); 333 authorization.setCNonce(cnonce); 334 authorization.setNonceCount(Integer.parseInt(nc_value)); 335 } 336 337 authorization.setResponse(response); 338 339 } catch (ParseException ex) { 340 throw new RuntimeException("Failed to create an authorization header!"); 341 } 342 343 return authorization; 344 } 345 /** 346 * Generates an authorisation header in response to wwwAuthHeader. 347 * 348 * @param method method of the request being authenticated 349 * @param uri digest-uri 350 * @param requestBody the body of the request. 351 * @param authHeader the challenge that we should respond to 352 * @param userCredentials username and pass 353 * 354 * @return an authorisation header in response to authHeader. 355 * 356 * @throws OperationFailedException if auth header was malformated. 357 */ 358 private AuthorizationHeader getAuthorization(String method, String uri, String requestBody, 359 WWWAuthenticateHeader authHeader, UserCredentialHash userCredentials) { 360 String response = null; 361 362 // JvB: authHeader.getQop() is a quoted _list_ of qop values 363 // (e.g. "auth,auth-int") Client is supposed to pick one 364 String qopList = authHeader.getQop(); 365 String qop = (qopList != null) ? "auth" : null; 366 String nc_value = "00000001"; 367 String cnonce = "xyz"; 368 369 response = MessageDigestAlgorithm.calculateResponse(authHeader.getAlgorithm(), 370 userCredentials.getHashUserDomainPassword(), authHeader.getNonce(), nc_value, // JvB added 371 cnonce, // JvB added 372 method, uri, requestBody, qop,sipStack.getStackLogger());// jvb changed 373 374 AuthorizationHeader authorization = null; 375 try { 376 if (authHeader instanceof ProxyAuthenticateHeader) { 377 authorization = headerFactory.createProxyAuthorizationHeader(authHeader 378 .getScheme()); 379 } else { 380 authorization = headerFactory.createAuthorizationHeader(authHeader.getScheme()); 381 } 382 383 authorization.setUsername(userCredentials.getUserName()); 384 authorization.setRealm(authHeader.getRealm()); 385 authorization.setNonce(authHeader.getNonce()); 386 authorization.setParameter("uri", uri); 387 authorization.setResponse(response); 388 if (authHeader.getAlgorithm() != null) { 389 authorization.setAlgorithm(authHeader.getAlgorithm()); 390 } 391 392 if (authHeader.getOpaque() != null) { 393 authorization.setOpaque(authHeader.getOpaque()); 394 } 395 396 // jvb added 397 if (qop != null) { 398 authorization.setQop(qop); 399 authorization.setCNonce(cnonce); 400 authorization.setNonceCount(Integer.parseInt(nc_value)); 401 } 402 403 authorization.setResponse(response); 404 405 } catch (ParseException ex) { 406 throw new RuntimeException("Failed to create an authorization header!"); 407 } 408 409 return authorization; 410 } 411 /** 412 * Removes all via headers from <tt>request</tt> and replaces them with a new one, equal to 413 * the one that was top most. 414 * 415 * @param request the Request whose branchID we'd like to remove. 416 * 417 */ 418 private void removeBranchID(Request request) { 419 420 ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME); 421 422 viaHeader.removeParameter("branch"); 423 424 } 425 426 /* 427 * (non-Javadoc) 428 * 429 * @see gov.nist.javax.sip.clientauthutils.AuthenticationHelper#attachAuthenticationHeaders(javax.sip.message.Request) 430 */ 431 public void setAuthenticationHeaders(Request request) { 432 SIPRequest sipRequest = (SIPRequest) request; 433 434 String callId = sipRequest.getCallId().getCallId(); 435 436 request.removeHeader(AuthorizationHeader.NAME); 437 Collection<AuthorizationHeader> authHeaders = this.cachedCredentials 438 .getCachedAuthorizationHeaders(callId); 439 if (authHeaders == null) { 440 if (sipStack.isLoggingEnabled()) 441 sipStack.getStackLogger().logDebug( 442 "Could not find authentication headers for " + callId); 443 return; 444 } 445 446 for (AuthorizationHeader authHeader : authHeaders) { 447 request.addHeader(authHeader); 448 } 449 450 } 451 452 /* 453 * (non-Javadoc) 454 * 455 * @see gov.nist.javax.sip.clientauthutils.AuthenticationHelper#removeCachedAuthenticationHeaders(java.lang.String) 456 */ 457 public void removeCachedAuthenticationHeaders(String callId) { 458 if (callId == null) 459 throw new NullPointerException("Null callId argument "); 460 this.cachedCredentials.removeAuthenticationHeader(callId); 461 462 } 463 464 } 465