package org.atricore.idbus.capabilities.openidconnect.main.binding; import com.google.api.client.auth.oauth.OAuthCallbackUrl; import com.google.api.client.auth.oauth2.AuthorizationCodeResponseUrl; import com.google.api.client.auth.openidconnect.IdTokenResponse; import com.google.api.client.http.HttpResponse; import org.apache.camel.Exchange; import org.apache.camel.Message; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.atricore.idbus.kernel.main.federation.metadata.EndpointDescriptor; import org.atricore.idbus.kernel.main.mediation.*; import org.atricore.idbus.kernel.main.mediation.camel.component.binding.AbstractMediationHttpBinding; import org.atricore.idbus.kernel.main.mediation.camel.component.binding.CamelMediationMessage; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; /** * OpenID Connect (OAuth2 Authorization) messages binding using Google OpenID connect toolkit. */ public class OpenIDConnectHttpAuthzBinding extends AbstractMediationHttpBinding { private static final Log logger = LogFactory.getLog(OpenIDConnectHttpAuthzBinding.class); private static final int MAX_NUM_OF_AUTHORIZATION_RETRIES = 1; public OpenIDConnectHttpAuthzBinding(Channel channel) { super(OpenIDConnectBinding.OPENIDCONNECT_AUTHZ.getValue(), channel); } @Override public MediationMessage createMessage(CamelMediationMessage message) { // Create mediation message based on HTTP request // The nested exchange contains HTTP information Exchange exchange = message.getExchange().getExchange(); logger.debug("Create Message Body from exchange " + exchange.getClass().getName()); Message httpMsg = exchange.getIn(); if (httpMsg.getHeader("http.requestMethod") == null || !httpMsg.getHeader("http.requestMethod").equals("GET")) { throw new IllegalArgumentException("Unknown message, no valid HTTP Method header found!"); } String requestUrl = (String) httpMsg.getHeaders().get("org.atricore.idbus.http.RequestURL"); String queryString = (String) httpMsg.getHeaders().get("org.atricore.idbus.http.QueryString"); // Check the type of response we're building // HTTP Request Parameters from HTTP Request body MediationState state = createMediationState(exchange); if (state.getTransientVariable("code") != null) { // Looks like the code to retrieve an access token, this must be an authorization code response StringBuffer buf = new StringBuffer(requestUrl); if (queryString != null) { buf.append('?').append(queryString); } AuthorizationCodeResponseUrl responseUrl = new AuthorizationCodeResponseUrl(buf.toString()); return new MediationMessageImpl<AuthorizationCodeResponseUrl>(httpMsg.getMessageId(), responseUrl, responseUrl.build(), null, responseUrl.getState(), null, state); } else if (state.getTransientVariable("access_token") != null) { // Looks like an access token } else if (state.getTransientVariable("oauth_token") != null && state.getTransientVariable("oauth_verifier") != null) { // Looks like a OAuth 1.0a (Twitter) response StringBuffer buf = new StringBuffer(requestUrl); if (queryString != null) { buf.append('?').append(queryString); } OAuthCallbackUrl callbackUrl = new OAuthCallbackUrl(buf.toString()); return new MediationMessageImpl<OAuthCallbackUrl>(httpMsg.getMessageId(), callbackUrl, callbackUrl.build(), null, state.getTransientVariable("state"), null, state); } else if (state.getTransientVariable("error_code") != null || state.getTransientVariable("error") != null) { // This is an error from the Idp, withotut StringBuffer buf = new StringBuffer(requestUrl); if (queryString != null) { buf.append('?').append(queryString); } // ------------------------------------------------ // Some Facebook handling, adapt some parameters // to play nice with Google OpenIDConnect stack // ------------------------------------------------ // Facebook only sends error_code, not error or code, so. if ( state.getTransientVariable("error") == null) { if (state.getTransientVariable("error_code") != null) buf.append("&error=" + state.getTransientVariable("error_code")); } // Facebook sends error_message instead of error_description if (state.getTransientVariable("error_description") == null) { if (state.getTransientVariable("error_message") != null) { try { buf.append("&error_description=" + URLEncoder.encode(state.getTransientVariable("error_message"), "UTF-8")); } catch (UnsupportedEncodingException e) { logger.error("Cannot encode OpenID error message : " + state.getTransientVariable("error_message")); } } } AuthorizationCodeResponseUrl responseUrl = new AuthorizationCodeResponseUrl(buf.toString()); return new MediationMessageImpl<AuthorizationCodeResponseUrl>(httpMsg.getMessageId(), responseUrl, responseUrl.build(), null, responseUrl.getState(), null, state); } throw new IllegalStateException("Unrecognized HTTP redirect mesage [" + requestUrl + "?" + queryString + "]"); } @Override public void copyMessageToExchange(CamelMediationMessage openIdConnectOut, Exchange exchange) { // Transfer message information to HTTP layer // Content is OPTIONAL MediationMessage out = openIdConnectOut.getMessage(); EndpointDescriptor ed = out.getDestination(); // ------------------------------------------------------------ // Validate received message // ------------------------------------------------------------ assert ed != null : "Mediation Response MUST Provide a destination"; // ------------------------------------------------------------ // Create HTML Form for response body // ------------------------------------------------------------ if (logger.isDebugEnabled()) logger.debug("Creating HTML Redirect to " + ed.getLocation()); Message httpOut = exchange.getOut(); Message httpIn = exchange.getIn(); String openIdConnectRedirLocation = this.buildHttpTargetLocation(httpIn, ed); if (logger.isDebugEnabled()) logger.debug("Redirecting to " + openIdConnectRedirLocation); try { // ------------------------------------------------------------ // Prepare HTTP Resposne // ------------------------------------------------------------ copyBackState(out.getState(), exchange); httpOut.getHeaders().put("Cache-Control", "no-cache, no-store"); httpOut.getHeaders().put("Pragma", "no-cache"); httpOut.getHeaders().put("http.responseCode", 302); httpOut.getHeaders().put("Content-Type", "text/html"); httpOut.getHeaders().put("Location", openIdConnectRedirLocation); handleCrossOriginResourceSharing(exchange); } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } } @Override public Object sendMessage(MediationMessage message) throws IdentityMediationException { int retry = 0; while (retry <= MAX_NUM_OF_AUTHORIZATION_RETRIES) { try { AuthorizationCodeTokenIdRequest tokenRequest = (AuthorizationCodeTokenIdRequest) message.getContent(); HttpResponse httpResponse = tokenRequest.executeUnparsed(); IdTokenResponse idTokenResponse = httpResponse.parseAs(IdTokenResponse.class); return idTokenResponse; } catch (IOException e) { retry++; logger.error(e.getMessage(), e); if (retry <= MAX_NUM_OF_AUTHORIZATION_RETRIES) { logger.debug("OpenID Connect authorization retry: " + retry); } else { throw new IdentityMediationException(e); } } } throw new IdentityMediationException("OpenID Connect authorization failed!"); } }