From c84c751991fa7bbaa722a3fb7b2fd3715dcdb064 Mon Sep 17 00:00:00 2001 From: "U-MITRE\\mjwalsh" Date: Mon, 26 Mar 2012 14:18:54 -0400 Subject: [PATCH] client refactoring, and javadocing --- .../OpenIdConnectAuthenticationFilter.java | 706 +++++++++--------- 1 file changed, 373 insertions(+), 333 deletions(-) diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OpenIdConnectAuthenticationFilter.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OpenIdConnectAuthenticationFilter.java index ca63716b6..745ba8ee8 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OpenIdConnectAuthenticationFilter.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OpenIdConnectAuthenticationFilter.java @@ -9,13 +9,13 @@ import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; +import java.security.SecureRandom; import java.security.Signature; import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Random; import javax.servlet.ServletException; import javax.servlet.http.Cookie; @@ -23,8 +23,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.codec.binary.Base64; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.DefaultHttpClient; import org.mitre.openid.connect.model.IdToken; @@ -33,6 +31,7 @@ import org.springframework.security.authentication.AuthenticationServiceExceptio import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.HttpClientErrorException; @@ -93,21 +92,78 @@ public class OpenIdConnectAuthenticationFilter extends private final static String NONCE_SIGNATURE_COOKIE_NAME = "nonce"; private final static String FILTER_PROCESSES_URL = "/openid_connect_login"; + /** + * Builds the redirect_uri that will be sent to the Authorization Endpoint. + * By default returns the URL of the current request minus zero or more + * fields of the URL's query string. + * + * @param request + * the current request which is being processed by this filter + * @param ignoreFields + * an array of field names to ignore. + * @return a URL built from the messaged parameters. + */ + public static String buildRedirectURI(HttpServletRequest request, + String[] ignoreFields) { + + List ignore = (ignoreFields != null) ? Arrays + .asList(ignoreFields) : null; + + boolean isFirst = true; + + StringBuffer sb = request.getRequestURL(); + + for (Enumeration e = request.getParameterNames(); e + .hasMoreElements();) { + + String name = (String) e.nextElement(); + + if ((ignore == null) || (!ignore.contains(name))) { + // Assume for simplicity that there is only one value + String value = request.getParameter(name); + + if (value == null) { + continue; + } + + if (isFirst) { + sb.append("?"); + isFirst = false; + } + + sb.append(name).append("=").append(value); + + if (e.hasMoreElements()) { + sb.append("&"); + } + } + } + + return sb.toString(); + } + /** * Return the URL w/ GET parameters * * @param baseURI - * @param params - * @return + * A String containing the protocol, server address, path, and + * program as per "http://server/path/program" + * @param queryStringFields + * A map where each key is the field name and the associated + * key's value is the field value used to populate the URL's + * query string + * @return A String representing the URL in form of + * http://server/path/program?query_string from the messaged + * parameters. */ public static String buildURL(String baseURI, - Map urlVariables) { + Map queryStringFields) { StringBuilder URLBuilder = new StringBuilder(baseURI); char appendChar = '?'; - for (Map.Entry param : urlVariables.entrySet()) { + for (Map.Entry param : queryStringFields.entrySet()) { try { URLBuilder.append(appendChar).append(param.getKey()) .append('=') @@ -124,7 +180,13 @@ public class OpenIdConnectAuthenticationFilter extends /** * Returns the signature text for the byte array of data * - * @return + * @param signer + * The algorithm to sign with + * @param privateKey + * The private key to sign with + * @param data + * The data to be signed + * @return The signature text */ public static String sign(Signature signer, PrivateKey privateKey, byte[] data) { @@ -151,8 +213,10 @@ public class OpenIdConnectAuthenticationFilter extends * Verifies the signature text against the data * * @param data + * The data * @param sigText - * @return + * The signature text + * @return True if valid, false if not */ public static boolean verify(Signature signer, PublicKey publicKey, String data, String sigText) { @@ -197,7 +261,7 @@ public class OpenIdConnectAuthenticationFilter extends private Signature signer; /** - * + * OpenIdConnectAuthenticationFilter constructor */ protected OpenIdConnectAuthenticationFilter() { super(FILTER_PROCESSES_URL); @@ -213,34 +277,21 @@ public class OpenIdConnectAuthenticationFilter extends public void afterPropertiesSet() { super.afterPropertiesSet(); - if (errorRedirectURI == null) { - throw new IllegalArgumentException( - "An Error Redirect URI must be supplied"); - } + Assert.notNull(errorRedirectURI, + "An Error Redirect URI must be supplied"); - if (authorizationEndpointURI == null) { - throw new IllegalArgumentException( - "An Authorization Endpoint URI must be supplied"); - } + Assert.notNull(authorizationEndpointURI, + "An Authorization Endpoint URI must be supplied"); - if (tokenEndpointURI == null) { - throw new IllegalArgumentException( - "A Token ID Endpoint URI must be supplied"); - } + Assert.notNull(tokenEndpointURI, + "A Token ID Endpoint URI must be supplied"); - if (checkIDEndpointURI == null) { - throw new IllegalArgumentException( - "A Check ID Endpoint URI must be supplied"); - } + Assert.notNull(checkIDEndpointURI, + "A Check ID Endpoint URI must be supplied"); - if (clientId == null) { - throw new IllegalArgumentException("A Client ID must be supplied"); - } + Assert.notNull(clientId, "A Client ID must be supplied"); - if (clientSecret == null) { - throw new IllegalArgumentException( - "A Client Secret must be supplied"); - } + Assert.notNull(clientSecret, "A Client Secret must be supplied"); KeyPairGenerator keyPairGenerator; try { @@ -256,19 +307,11 @@ public class OpenIdConnectAuthenticationFilter extends throw new IllegalStateException(generalSecurityException); } - // prepend the spec necessary scope + // prepend the spec necessary SCOPE setScope((scope != null && !scope.isEmpty()) ? SCOPE + " " + scope : SCOPE); } - /* - * (non-Javadoc) - * - * @see org.springframework.security.web.authentication. - * AbstractAuthenticationProcessingFilter - * #attemptAuthentication(javax.servlet.http.HttpServletRequest, - * javax.servlet.http.HttpServletResponse) - */ /* * (non-Javadoc) * @@ -282,278 +325,22 @@ public class OpenIdConnectAuthenticationFilter extends HttpServletResponse response) throws AuthenticationException, IOException, ServletException { - final boolean debug = logger.isDebugEnabled(); - if (request.getParameter("error") != null) { - // Handle Authorization Endpoint error - - String error = request.getParameter("error"); - String errorDescription = request.getParameter("error_description"); - String errorURI = request.getParameter("error_uri"); - - Map requestParams = new HashMap(); - - requestParams.put("error", error); - - if (errorDescription != null) { - requestParams.put("error_description", errorDescription); - } - - if (errorURI != null) { - requestParams.put("error_uri", errorURI); - } - - response.sendRedirect(buildURL(errorRedirectURI, requestParams)); + handleError(request, response); } else { // Determine if the Authorization Endpoint issued an // authorization grant - String authorizationGrant = request.getParameter("code"); + if (request.getParameter("code") != null) { - if (authorizationGrant != null) { - - // Handle Token Endpoint interaction - - HttpClient httpClient = new DefaultHttpClient(); - - httpClient.getParams().setParameter("http.socket.timeout", - new Integer(httpSocketTimeout)); - -// -// TODO: basic auth is untested (it wasn't working last I tested) -// UsernamePasswordCredentials credentials = new UsernamePasswordCredentials( -// clientId, clientSecret); -// ((DefaultHttpClient) httpClient).getCredentialsProvider() -// .setCredentials(AuthScope.ANY, credentials); - - HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory( - httpClient); - - RestTemplate restTemplate = new RestTemplate(factory); - - MultiValueMap form = new LinkedMultiValueMap(); - form.add("grant_type", "authorization_code"); - form.add("code", authorizationGrant); - form.add("redirect_uri", - buildRedirectURI(request, new String[] { "code" })); - -// pass clientId and clientSecret in post of request - form.add("client_id", clientId); - form.add("client_secret", clientSecret); - - - if (debug) { - logger.debug("tokenEndpointURI = " + tokenEndpointURI); - logger.debug("form = " + form); - } - - String jsonString = null; - - try { - jsonString = restTemplate.postForObject(tokenEndpointURI, - form, String.class); - } catch (HttpClientErrorException httpClientErrorException) { - - // Handle error - - logger.error("Token Endpoint error response: " - + httpClientErrorException.getStatusText() + " : " - + httpClientErrorException.getMessage()); - - throw new AuthenticationServiceException( - "Unable to obtain Access Token."); - } - - JsonElement jsonRoot = new JsonParser().parse(jsonString); - - if (jsonRoot.getAsJsonObject().get("error") != null) { - - // Handle error - - String error = jsonRoot.getAsJsonObject().get("error") - .getAsString(); - - logger.error("Token Endpoint returned: " + error); - - throw new AuthenticationServiceException( - "Unable to obtain Access Token. Token Endpoint returned: " - + error); - - } else { - - // Extract the id_token to insert into the - // OpenIdConnectAuthenticationToken - - IdToken idToken = null; - - if (jsonRoot.getAsJsonObject().get("id_token") != null) { - - try { - idToken = IdToken.parse(jsonRoot.getAsJsonObject() - .get("id_token").getAsString()); - } catch (Exception e) { - - // I suspect this could happen - - logger.error("Problem parsing id_token: " + e); - // e.printStackTrace(); - - throw new AuthenticationServiceException( - "Problem parsing id_token return from Token endpoint: " + e); - } - - } else { - - // An error is unlikely, but it good security to check - - logger.error("Token Endpoint did not return a token_id"); - - throw new AuthenticationServiceException( - "Token Endpoint did not return a token_id"); - } - - // Handle Check ID Endpoint interaction - - httpClient = new DefaultHttpClient(); - - httpClient.getParams().setParameter("http.socket.timeout", - new Integer(httpSocketTimeout)); - - factory = new HttpComponentsClientHttpRequestFactory( - httpClient); - restTemplate = new RestTemplate(factory); - - form = new LinkedMultiValueMap(); - - form.add("access_token", - jsonRoot.getAsJsonObject().get("id_token") - .getAsString()); - - jsonString = null; - - try { - jsonString = restTemplate.postForObject( - checkIDEndpointURI, form, String.class); - } catch (HttpClientErrorException httpClientErrorException) { - - // Handle error - - logger.error("Check ID Endpoint error response: " - + httpClientErrorException.getStatusText() - + " : " + httpClientErrorException.getMessage()); - - throw new AuthenticationServiceException( - "Unable check token."); - } - - jsonRoot = new JsonParser().parse(jsonString); - - // String iss = jsonRoot.getAsJsonObject().get("iss") - // .getAsString(); - String userId = jsonRoot.getAsJsonObject().get("user_id") - .getAsString(); - // String aud = jsonRoot.getAsJsonObject().get("aud") - // .getAsString(); - String nonce = jsonRoot.getAsJsonObject().get("nonce") - .getAsString(); - // String exp = jsonRoot.getAsJsonObject().get("exp") - // .getAsString(); - - // Compare returned ID Token to signed session cookie - // to detect ID Token replay by third parties. - - Cookie nonceSignatureCookie = WebUtils.getCookie(request, - NONCE_SIGNATURE_COOKIE_NAME); - - if (nonceSignatureCookie != null) { - - String sigText = nonceSignatureCookie.getValue(); - - if (sigText != null && !sigText.isEmpty()) { - - if (!verify(signer, publicKey, nonce, sigText)) { - logger.error("Possible replay attack detected! " - + "The comparison of the nonce in the returned " - + "ID Token to the signed session " - + NONCE_SIGNATURE_COOKIE_NAME - + " failed."); - - throw new AuthenticationServiceException( - "Possible replay attack detected! " - + "The comparison of the nonce in the returned " - + "ID Token to the signed session " - + NONCE_SIGNATURE_COOKIE_NAME - + " failed."); - } - - } else { - logger.error(NONCE_SIGNATURE_COOKIE_NAME - + " was found, but was null or empty."); - - throw new AuthenticationServiceException(NONCE_SIGNATURE_COOKIE_NAME - + " was found, but was null or empty."); - } - - } else { - - logger.error(NONCE_SIGNATURE_COOKIE_NAME - + " cookie was not found."); - - throw new AuthenticationServiceException(NONCE_SIGNATURE_COOKIE_NAME - + " cookie was not found."); - } - - // Create an Authentication object for the token, and - // return. - - OpenIdConnectAuthenticationToken token = new OpenIdConnectAuthenticationToken( - userId, idToken); - - Authentication authentication = this - .getAuthenticationManager().authenticate(token); - - return authentication; - - } + return handleAuthorizationGrantResponse(request); } else { - // Initiate an Authorization request - - Map urlVariables = new HashMap(); - - // Required parameters: - - urlVariables.put("response_type", "code"); - urlVariables.put("client_id", clientId); - urlVariables.put("scope", scope); - urlVariables.put("redirect_uri", - buildRedirectURI(request, null)); - - // Create a string value used to associate a user agent session - // with an ID Token to mitigate replay attacks. The value is - // passed through unmodified to the ID Token. One method is to - // store a random value as a signed session cookie, and pass the - // value in the nonce parameter. - - String nonce = new BigInteger(50, new Random()).toString(16); - - Cookie nonceCookie = new Cookie(NONCE_SIGNATURE_COOKIE_NAME, - sign(signer, privateKey, nonce.getBytes())); - - response.addCookie(nonceCookie); - - urlVariables.put("nonce", nonce); - - // Optional parameters: - - // TODO: display, prompt, request, request_uri - - response.sendRedirect(buildURL(authorizationEndpointURI, - urlVariables)); + handleAuthorizationRequest(request, response); } } @@ -561,52 +348,305 @@ public class OpenIdConnectAuthenticationFilter extends } /** - * Builds the redirect_uri that will be sent to the Authorization Endpoint. - * By default returns the URL of the current request. + * Handles the authorization grant response * * @param request - * the current request which is being processed by this filter - * @param ingoreParameters - * an array of parameter names to ignore. - * @return + * The request from which to extract parameters and perform the + * authentication + * @return The authenticated user token, or null if authentication is + * incomplete. */ - private String buildRedirectURI(HttpServletRequest request, - String[] ingoreParameters) { + private Authentication handleAuthorizationGrantResponse( + HttpServletRequest request) { - List ignore = (ingoreParameters != null) ? Arrays - .asList(ingoreParameters) : null; + final boolean debug = logger.isDebugEnabled(); - boolean isFirst = true; + String authorizationGrant = request.getParameter("code"); - StringBuffer sb = request.getRequestURL(); + // Handle Token Endpoint interaction + HttpClient httpClient = new DefaultHttpClient(); - for (Enumeration e = request.getParameterNames(); e - .hasMoreElements();) { + httpClient.getParams().setParameter("http.socket.timeout", + new Integer(httpSocketTimeout)); - String name = (String) e.nextElement(); + // + // TODO: basic auth is untested (it wasn't working last I + // tested) + // UsernamePasswordCredentials credentials = new + // UsernamePasswordCredentials( + // clientId, clientSecret); + // ((DefaultHttpClient) httpClient).getCredentialsProvider() + // .setCredentials(AuthScope.ANY, credentials); + // - if ((ignore == null) || (!ignore.contains(name))) { - // Assume for simplicity that there is only one value - String value = request.getParameter(name); + HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory( + httpClient); - if (value == null) { - continue; - } + RestTemplate restTemplate = new RestTemplate(factory); - if (isFirst) { - sb.append("?"); - isFirst = false; - } + MultiValueMap form = new LinkedMultiValueMap(); + form.add("grant_type", "authorization_code"); + form.add("code", authorizationGrant); + form.add("redirect_uri", OpenIdConnectAuthenticationFilter + .buildRedirectURI(request, new String[] { "code" })); - sb.append(name).append("=").append(value); + // pass clientId and clientSecret in post of request + form.add("client_id", clientId); + form.add("client_secret", clientSecret); - if (e.hasMoreElements()) { - sb.append("&"); - } - } + if (debug) { + logger.debug("tokenEndpointURI = " + tokenEndpointURI); + logger.debug("form = " + form); } - return sb.toString(); + String jsonString = null; + + try { + jsonString = restTemplate.postForObject(tokenEndpointURI, form, + String.class); + } catch (HttpClientErrorException httpClientErrorException) { + + // Handle error + + logger.error("Token Endpoint error response: " + + httpClientErrorException.getStatusText() + " : " + + httpClientErrorException.getMessage()); + + throw new AuthenticationServiceException( + "Unable to obtain Access Token."); + } + + JsonElement jsonRoot = new JsonParser().parse(jsonString); + + if (jsonRoot.getAsJsonObject().get("error") != null) { + + // Handle error + + String error = jsonRoot.getAsJsonObject().get("error") + .getAsString(); + + logger.error("Token Endpoint returned: " + error); + + throw new AuthenticationServiceException( + "Unable to obtain Access Token. Token Endpoint returned: " + + error); + + } else { + + // Extract the id_token to insert into the + // OpenIdConnectAuthenticationToken + + IdToken idToken = null; + + if (jsonRoot.getAsJsonObject().get("id_token") != null) { + + try { + idToken = IdToken.parse(jsonRoot.getAsJsonObject() + .get("id_token").getAsString()); + } catch (Exception e) { + + // I suspect this could happen + + logger.error("Problem parsing id_token: " + e); + // e.printStackTrace(); + + throw new AuthenticationServiceException( + "Problem parsing id_token return from Token endpoint: " + + e); + } + + } else { + + // An error is unlikely, but it good security to check + + logger.error("Token Endpoint did not return a token_id"); + + throw new AuthenticationServiceException( + "Token Endpoint did not return a token_id"); + } + + // Handle Check ID Endpoint interaction + + httpClient = new DefaultHttpClient(); + + httpClient.getParams().setParameter("http.socket.timeout", + new Integer(httpSocketTimeout)); + + factory = new HttpComponentsClientHttpRequestFactory(httpClient); + restTemplate = new RestTemplate(factory); + + form = new LinkedMultiValueMap(); + + form.add("access_token", jsonRoot.getAsJsonObject().get("id_token") + .getAsString()); + + jsonString = null; + + try { + jsonString = restTemplate.postForObject(checkIDEndpointURI, + form, String.class); + } catch (HttpClientErrorException httpClientErrorException) { + + // Handle error + + logger.error("Check ID Endpoint error response: " + + httpClientErrorException.getStatusText() + " : " + + httpClientErrorException.getMessage()); + + throw new AuthenticationServiceException("Unable check token."); + } + + jsonRoot = new JsonParser().parse(jsonString); + + // String iss = jsonRoot.getAsJsonObject().get("iss") + // .getAsString(); + String userId = jsonRoot.getAsJsonObject().get("user_id") + .getAsString(); + // String aud = jsonRoot.getAsJsonObject().get("aud") + // .getAsString(); + String nonce = jsonRoot.getAsJsonObject().get("nonce") + .getAsString(); + // String exp = jsonRoot.getAsJsonObject().get("exp") + // .getAsString(); + + // Compare returned ID Token to signed session cookie + // to detect ID Token replay by third parties. + + Cookie nonceSignatureCookie = WebUtils.getCookie(request, + NONCE_SIGNATURE_COOKIE_NAME); + + if (nonceSignatureCookie != null) { + + String sigText = nonceSignatureCookie.getValue(); + + if (sigText != null && !sigText.isEmpty()) { + + if (!verify(signer, publicKey, nonce, sigText)) { + logger.error("Possible replay attack detected! " + + "The comparison of the nonce in the returned " + + "ID Token to the signed session " + + NONCE_SIGNATURE_COOKIE_NAME + " failed."); + + throw new AuthenticationServiceException( + "Possible replay attack detected! " + + "The comparison of the nonce in the returned " + + "ID Token to the signed session " + + NONCE_SIGNATURE_COOKIE_NAME + + " failed."); + } + + } else { + logger.error(NONCE_SIGNATURE_COOKIE_NAME + + " was found, but was null or empty."); + + throw new AuthenticationServiceException( + NONCE_SIGNATURE_COOKIE_NAME + + " was found, but was null or empty."); + } + + } else { + + logger.error(NONCE_SIGNATURE_COOKIE_NAME + + " cookie was not found."); + + throw new AuthenticationServiceException( + NONCE_SIGNATURE_COOKIE_NAME + " cookie was not found."); + } + + // Create an Authentication object for the token, and + // return. + + OpenIdConnectAuthenticationToken token = new OpenIdConnectAuthenticationToken( + userId, idToken); + + Authentication authentication = this.getAuthenticationManager() + .authenticate(token); + + return authentication; + + } + } + + /** + * Initiate an Authorization request + * + * @param request + * The request from which to extract parameters and perform the + * authentication + * @param response + * The response, needed to set a cookie and do a redirect as part + * of a multi-stage authentication process + * @throws IOException + * If an input or output exception occurs + */ + private void handleAuthorizationRequest(HttpServletRequest request, + HttpServletResponse response) throws IOException { + + Map urlVariables = new HashMap(); + + // Required parameters: + + urlVariables.put("response_type", "code"); + urlVariables.put("client_id", clientId); + urlVariables.put("scope", scope); + urlVariables.put("redirect_uri", OpenIdConnectAuthenticationFilter + .buildRedirectURI(request, null)); + + // Create a string value used to associate a user agent session + // with an ID Token to mitigate replay attacks. The value is + // passed through unmodified to the ID Token. One method is to + // store a random value as a signed session cookie, and pass the + // value in the nonce parameter. + + String nonce = new BigInteger(50, new SecureRandom()).toString(16); + + Cookie nonceCookie = new Cookie(NONCE_SIGNATURE_COOKIE_NAME, sign( + signer, privateKey, nonce.getBytes())); + + response.addCookie(nonceCookie); + + urlVariables.put("nonce", nonce); + + // Optional parameters: + + // TODO: display, prompt, request, request_uri + + response.sendRedirect(OpenIdConnectAuthenticationFilter.buildURL( + authorizationEndpointURI, urlVariables)); + } + + /** + * Handle Authorization Endpoint error + * + * @param request + * The request from which to extract parameters and handle the + * error + * @param response + * The response, needed to do a redirect to display the error + * @throws IOException + * If an input or output exception occurs + */ + private void handleError(HttpServletRequest request, + HttpServletResponse response) throws IOException { + + String error = request.getParameter("error"); + String errorDescription = request.getParameter("error_description"); + String errorURI = request.getParameter("error_uri"); + + Map requestParams = new HashMap(); + + requestParams.put("error", error); + + if (errorDescription != null) { + requestParams.put("error_description", errorDescription); + } + + if (errorURI != null) { + requestParams.put("error_uri", errorURI); + } + + response.sendRedirect(OpenIdConnectAuthenticationFilter.buildURL( + errorRedirectURI, requestParams)); } public void setAuthorizationEndpointURI(String authorizationEndpointURI) {