package org.mitre.rhex.security; import edu.umd.cs.findbugs.annotations.NonNull; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.StringUtils; import org.apache.http.*; import org.apache.http.client.CookieStore; import org.apache.http.client.HttpClient; import org.apache.http.client.RedirectStrategy; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.params.ClientPNames; import org.apache.http.client.protocol.ClientContext; import org.apache.http.impl.client.AbstractHttpClient; import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.message.BasicNameValuePair; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.HttpContext; import org.apache.http.util.EntityUtils; import org.mitre.test.ClientHelper; import org.mitre.test.Context; import org.mitre.test.HttpRequestChecker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * HTTP request security handler for authentication via modified OpenID Connect * RubyGem implementation. * * @author Jason Mathews, MITRE Corp. * Date: 3/30/12 12:51 PM */ public class RhexOmniAuthOIDCSecurityChecker implements HttpRequestChecker { private static final Logger log = LoggerFactory.getLogger(RhexOmniAuthOIDCSecurityChecker.class); private HttpContext localContext; private String currentUser; private final Map<String, HttpContext> contexts = new HashMap<String,HttpContext>(); /** * Setups and initializes the HttpRequestChecker * * @param context Application context * @throws IllegalArgumentException if setup/configuration fails */ @Override public void setup(Context context) { log.debug("XXX: enable OpenID Connect authentication for defaultUser"); String loginEmail = context.getUserProperty("defaultUser", "email"); String loginPassword = context.getUserProperty("defaultUser", "password"); if (StringUtils.isBlank(loginEmail) || StringUtils.isBlank(loginPassword)) { throw new IllegalArgumentException("email and password properties are empty or missing for defaultUser"); } setUser(context, Context.DEFAULT_USER, loginEmail, loginPassword); } @Override public String getCurrentUser(Context context) { return currentUser; } /** * * @param context * @param userId * @param userEmail * @param userPassword * * @throws IllegalArgumentException if setup/configuration fails */ @Override public void setUser(Context context, String userId, String userEmail, String userPassword) { if (currentUser != null && currentUser.equals(userEmail)) { log.debug("same user context: no change"); // XXX return; // user is already active and context is set } HttpContext httpContext = contexts.get(userEmail); if (httpContext != null) { //localContext = contexts.get(userEmail); //if (localContext != null) { log.debug("switch user context: {}", userEmail); currentUser = userEmail; localContext = httpContext; // log.info("local user context: " + localContext.getAttribute("user")); return; } log.info("set user context: " + userEmail); final URI uri = context.getPropertyAsURI("loginURL"); if (uri == null) { throw new IllegalArgumentException("loginURL property not defined"); } /* Step 1: GET auth URL which redirects to authentication endpoint GET -> http://rhex.mitre.org:3000/auth/openid_connect Host: rhex.mitre.org:3000 Connection: Keep-Alive User-Agent: Apache-HttpClient/4.1.3 (java 1.5) redirects to http://rhex.mitre.org:3001/accounts/sign_in */ //HttpContext httpContext = localContext; //if (httpContext == null) { // Create a local instance of cookie store CookieStore cookieStore = new BasicCookieStore(); // Create local HTTP context httpContext = new BasicHttpContext(); // Bind custom cookie store to the local context httpContext.setAttribute(ClientContext.COOKIE_STORE, cookieStore); //} HttpClient client = null; URI redirect = null; boolean debug = log.isDebugEnabled(); try { client = context.getHttpClient(); if (client instanceof AbstractHttpClient) { log.debug("*** set setRedirectStrategy ***"); AbstractHttpClient c = (AbstractHttpClient)client; c.setRedirectStrategy(new RedirectStrategy() { @Override public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException { return false; } @Override public HttpUriRequest getRedirect(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException { return null; } }); } log.debug("1.GET auth URL: {}", uri); HttpGet req = new HttpGet(uri); req.setHeader("Cache-Control", "no-cache"); // req.setHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); HttpResponse response = client.execute(req, httpContext); // expect 302 response // should redirect to something like: // http://rhex.mitre.org:3001/authorizations/new?client_id=xx&nonce=xx&redirect_uri=http://rhex.mitre.org:3000/auth/openid_connect/callback&request=xxx&response_type=code&scope=openid redirect = ClientHelper.getRedirectURI(response); // getRedirectURI(response); if (debug || redirect == null) { ClientHelper.dumpResponse(req, response, true); } if (redirect == null) { log.error("failed to get redirect URL"); return; } } catch (IOException e) { log.error("", e); return; } finally { if (client != null) client.getConnectionManager().shutdown(); } /* * Step 2: * Invoke GET on redirected URL at identity provider endpoint * GET -> http://rhex.mitre.org:3001/authorizations/new?client_id=xx&nonce=xx&redirect_uri=http://rhex.mitre.org:3000/auth/openid_connect/callback&request=xxx&response_type=code&scope=openid */ String token = null; String formAction = "/accounts/sign_in"; // default FORM action for POST log.debug("*** 2.GET redirect URL: {}", redirect); try { client = context.getHttpClient(); //client = wrapClient(context.getHttpClient()); client.getParams().setParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, true); HttpGet req = new HttpGet(redirect); req.setHeader("Cache-Control", "no-cache"); HttpResponse response = client.execute(req, httpContext); if (debug) { ClientHelper.dumpResponse(req, response, false); } HttpEntity entity = response.getEntity(); if (entity != null) try { String text = EntityUtils.toString(entity); // <form>...<input name="authenticity_token" type="hidden" value="LHrOI+YLyGQ9Y2vR9QS5wDaGze8j6/krFT6uHIz6f0o=" /> Pattern p = Pattern.compile("<input name=\"authenticity_token\".*?value=\"([^\"]+)"); Matcher m = p.matcher(text); if (m.find()) { token = m.group(1); //System.out.println("\nXXX: ** MATCH ***" + token); log.trace("Response body:\n{}", text); } else { // no match found log.debug("Response body:\n{}", text); } // <form accept-charset="UTF-8" action="/accounts/sign_in" ... p = Pattern.compile("<form\\s.*?action=\"([^\"]+)"); m = p.matcher(text); if (m.find()) { formAction = m.group(1); //if (formAction.startsWith("/")) //formAction = formAction.substring(1); log.trace("form action={}", formAction); } } catch(IOException e) { log.warn("", e); } else log.warn("XXX: No body"); } catch (ParseException e) { log.error("", e); return; } catch (IOException e) { log.error("", e); return; } finally { if (client != null) client.getConnectionManager().shutdown(); } if (token == null) { log.error("failed to get auth token"); return; } /* Step 3: POST to form; e.g., http://rhex.mitre.org:3001/accounts/sign_in action="/accounts/sign_in" construct POST URL from auth endpoint and submit credentials form parameters: account[email] test@test.com account[password] password account[remember_me] 0 authenticity_token XsV2cW8kHyia6endlijMV37SeeiApdbmPXUi8UvsVJY= commit Sign in utf8 ? utf8=%E2%9C%93&authenticity_token=XsV2cW8kHyia6endlijMV37SeeiApdbmPXUi8UvsVJY%3D&account%5Bemail%5D=test%40test.com&account%5Bpassword%5D=testtest&account%5Bremember_me%5D=0&commit=Sign+in */ final URI signInUrl = redirect.resolve(formAction); /* try { final int port = redirect.getPort(); signInUrl = port == -1 ? new URI(String.format("%s://%s/%s", redirect.getScheme(), redirect.getHost(), formAction)) : new URI(String.format("%s://%s:%d/%s", redirect.getScheme(), redirect.getHost(), port, formAction)); } catch (URISyntaxException e) { log.error("", e); return; } */ log.debug("*** 3.POST auth URL: {}", signInUrl); HttpPost httppost = new HttpPost(signInUrl); httppost.setHeader("Cache-Control", "no-cache"); List<NameValuePair> formParams = new ArrayList<NameValuePair>(5); formParams.add(new BasicNameValuePair("account[email]", userEmail)); formParams.add(new BasicNameValuePair("account[password]", userPassword)); formParams.add(new BasicNameValuePair("authenticity_token", token)); formParams.add(new BasicNameValuePair("account[remember_me]", "0")); formParams.add(new BasicNameValuePair("commit", "Sign in")); // formParams.add(new BasicNameValuePair("utf8", "✓")); // %E2%9C%93 URI postRedirect = null; try { httppost.setEntity(new UrlEncodedFormEntity(formParams)); client = context.getHttpClient(); /* if (client instanceof AbstractHttpClient) { log.debug("*** setCredentials ***"); ((AbstractHttpClient)client).getCredentialsProvider().setCredentials( new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM), new UsernamePasswordCredentials("user", "pass")); } */ HttpResponse response = client.execute(httppost, httpContext); postRedirect = ClientHelper.getRedirectURI(response); if (debug || postRedirect == null) { ClientHelper.dumpResponse(httppost, response, true); } if (postRedirect == null) { log.error("failed to get redirect URL"); return; } } catch (IOException e) { throw new IllegalArgumentException(e); } finally { if (client != null) client.getConnectionManager().shutdown(); } /* Step 4: * * Expected POST Response: HTTP/1.1 302 Found * Location: http://rhex.mitre.org:3001/authorizations/new?client_id=xxx&nonce=xxx&redirect_uri=http://rhex.mitre.org:3000/auth/openid_connect/callback&request=xxx&response_type=code&scope=openid * GET redirect URL to verify authentication */ log.debug("*** 4.GET redirect URL: {}", postRedirect); URI getRedirect = null; try { client = context.getHttpClient(); HttpGet req = new HttpGet(postRedirect); req.setHeader("Cache-Control", "no-cache"); client.getParams().setParameter(ClientPNames.HANDLE_REDIRECTS, false); HttpResponse response = client.execute(req, httpContext); boolean success = response.getStatusLine().getStatusCode() == 302; getRedirect = ClientHelper.getRedirectURI(response); if (debug || !success) { ClientHelper.dumpResponse(req, response, !success); } if (getRedirect == null) { log.error("failed to get redirect URL"); return; } // boolean success = response.getStatusLine().getStatusCode() == 200; // if HANDLE_REDIRECTS=false then // Location: http://rhex-simple.mitre.org:3000/auth/openid_connect/callback?code=6415c21d14d45bf69054b9efca8b36591214c17490f643521cc7b0331f4386ec } catch (IOException e) { throw new IllegalArgumentException(e); } finally { if (client != null) client.getConnectionManager().shutdown(); } log.debug("*** 5.GET redirect URL: {}", getRedirect); try { client = context.getHttpClient(); HttpGet req = new HttpGet(getRedirect); HttpResponse response = client.execute(req, httpContext); boolean success = response.getStatusLine().getStatusCode() == 200; if (debug || !success) { System.out.println("XXX: 123-1"); ClientHelper.dumpResponse(req, response, !success); System.out.println("XXX: 123-2"); } if (success) { log.info("XXX: authentication successful: {}", userEmail); saveUriParameters(context, postRedirect, userId); saveUriParameters(context, getRedirect, userId); currentUser = userEmail; this.localContext = httpContext; // httpContext.setAttribute("user", userEmail); contexts.put(userEmail, httpContext); // save context } } catch (IOException e) { throw new IllegalArgumentException(e); } finally { if (client != null) client.getConnectionManager().shutdown(); } } private void saveUriParameters(Context context, URI uri, String userId) { String query = uri.getQuery(); if (userId != null && query != null) { // if (debug) System.out.println("XXX: params"); for(String s : query.split("&")) { int ind = s.indexOf('='); if (ind == -1) continue; String name = s.substring(0,ind); String value = s.substring(ind + 1); if ("client_id".equals(name) || "nonce".equals(name) || "code".equals(name)) { // save client id + nonce in configuration properties context.setProperty(userId + "." + name, value); } /* else if ("request".equals(name)) { byte[] bytes = Base64.decodeBase64(value); if (bytes != null) { value = new String(bytes); System.out.println("\t** XXX: request=" + value); } } */ /* try { value = new String(bytes);//, "UTF-8"); System.out.println("\t** value=" + value); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } */ // if (debug) System.out.printf("\t%s=%s%n", name, value); //if ("nonce".equals(name)) { //context.setProperty(userEmail.replaceAll("[^a-zA-Z0-9]+","_") + ".nonce", value); //} } } /* if (log.isDebugEnabled()) { // log.debug("XXX: params\n\t" + query.replace("&", "\n\t")); } } */ } /** * Wrap <tt>HttpClient.execute()</tt> to pre/post-test HTTP requests for * any server specific implementation handling such as authentication. * * @param context Application context * @param client the HttpClient, must never be null * @param req the request to execute, must never be null * * @return the response to the request. * @throws java.io.IOException in case of a problem or the connection was aborted * @throws org.apache.http.client.ClientProtocolException in case of an http protocol error */ @NonNull public HttpResponse executeRequest(Context context, HttpClient client, HttpUriRequest req) throws IOException { return client.execute(req, localContext); } }