/** * Copyright 2005-2014 Restlet * * The contents of this file are subject to the terms of one of the following * open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can * select the license that you prefer but you may not use this file except in * compliance with one of these Licenses. * * You can obtain a copy of the Apache 2.0 license at * http://www.opensource.org/licenses/apache-2.0 * * You can obtain a copy of the EPL 1.0 license at * http://www.opensource.org/licenses/eclipse-1.0 * * See the Licenses for the specific language governing permissions and * limitations under the Licenses. * * Alternatively, you can obtain a royalty free commercial license with less * limitations, transferable or non-transferable, directly at * http://restlet.com/products/restlet-framework * * Restlet is a registered trademark of Restlet S.A.S. */ package org.restlet.ext.oauth; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.logging.Level; import org.json.JSONException; import org.restlet.Context; import org.restlet.Request; import org.restlet.Response; import org.restlet.data.CacheDirective; import org.restlet.data.ChallengeScheme; import org.restlet.data.CookieSetting; import org.restlet.data.Form; import org.restlet.data.MediaType; import org.restlet.data.Reference; import org.restlet.data.Status; import org.restlet.engine.util.Base64; import org.restlet.ext.oauth.internal.Token; import org.restlet.representation.Representation; import org.restlet.representation.StringRepresentation; import org.restlet.routing.Filter; /** * A restlet filter for initiating a web server flow or comparable to OAuth 2.0 * 3-legged authorization. * * On successful execution a working OAuth token will be maintained. It is * recommended to put a ServerResource after this filter to display to the end * user on successful service setup. * * The following example shows how to gain an access token that will be * available for "DummyResource" to use to access some remote protected resource * * <pre> * { * @code * OAuthProxy proxy = new OauthProxy(getContext(), true); * proxy.setClientId("clientId"); * proxy.setClientSecret("clientSecret"); * proxy.setRedirectURI("callbackURI"); * proxy.setAuthorizationURI("authURI"); * proxy.setTokenURI("tokenURI"); * proxy.setNext(DummyResource.class); * router.attach("/write", write); * } * </pre> * * @author Kristoffer Gronowski * @author Shotaro Uchida <fantom@xmaker.mx> * @see org.restlet.ext.oauth.OAuthParameters */ public class OAuthProxy extends Filter implements OAuthResourceDefs { private final static List<CacheDirective> no = new ArrayList<CacheDirective>(); private final static String VERSION = "RFC6749"; // Final spec. /** * Returns the current proxy's version. * * @return The current proxy's version. */ public static String getVersion() { return VERSION; } private String authorizationURI; private final boolean basicSecret; private final org.restlet.Client cc; private String clientId; private String clientSecret; private final SecureRandom random; private String redirectURI; private String[] scope; private String tokenURI; /** * Sets up an OauthProxy. Defaults to form based authentication and not http * basic. * * @param ctx * The Restlet context. */ public OAuthProxy(Context ctx) { this(ctx, true); // Use BASIC method as default. } /** * Sets up an OAuthProxy. * * @param useBasicSecret * If true use http basic authentication otherwise use form * based. * @param ctx * The Restlet context. */ public OAuthProxy(Context ctx, boolean useBasicSecret) { this(ctx, useBasicSecret, null); } /** * Sets up an OAuthProxy. * * @param useBasicSecret * If true use http basic authentication otherwise use form * based. * @param ctx * The Restlet context. * @param requestClient * A predefined client that will be used for remote client * request. Useful when you need to set e.g. SSL initialization * parameters */ public OAuthProxy(Context ctx, boolean useBasicSecret, org.restlet.Client requestClient) { this.basicSecret = useBasicSecret; setContext(ctx); no.add(CacheDirective.noStore()); this.cc = requestClient; try { random = SecureRandom.getInstance("SHA1PRNG"); } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException(ex); } } @Override protected int beforeHandle(Request request, Response response) { // Sets the no-store Cache-Control header request.setCacheDirectives(no); Form params = new Form(request.getOriginalRef().getQuery()); getLogger().fine("Incomming request query = " + params); try { // Check if error is available. String error = params.getFirstValue(ERROR); if (error != null && !error.isEmpty()) { validateState(request, params); // CSRF protection return sendErrorPage(response, OAuthException.toOAuthException(params)); } // Check if code is available. String code = params.getFirstValue(CODE); if (code != null && !code.isEmpty()) { // Execute authorization_code grant validateState(request, params); // CSRF protection Token token = requestToken(code); request.getAttributes().put(Token.class.getName(), token); return CONTINUE; } } catch (Exception ex) { if (!(ex instanceof OAuthException)) { getLogger().log(Level.SEVERE, "OAuthProxy error", ex); } return sendErrorPage(response, ex); } // Redirect to authorization uri OAuthParameters authRequest = createAuthorizationRequest(); authRequest.state(setupState(response)); // CSRF protection Reference redirRef = authRequest.toReference(getAuthorizationURI()); getLogger().fine("Redirecting to : " + redirRef.toUri()); response.setCacheDirectives(no); response.redirectTemporary(redirRef); getLogger().fine("After Redirecting to : " + redirRef.toUri()); return STOP; } protected OAuthParameters createAuthorizationRequest() { OAuthParameters parameters = new OAuthParameters().responseType( ResponseType.code).add(CLIENT_ID, getClientId()); if (redirectURI != null) { parameters.redirectURI(redirectURI); } if (scope != null) { parameters.scope(scope); } return parameters; } protected OAuthParameters createTokenRequest(String code) { OAuthParameters parameters = new OAuthParameters().grantType( GrantType.authorization_code).code(code); if (redirectURI != null) { parameters.redirectURI(redirectURI); } return parameters; } /** * @return the authorizationURI */ public String getAuthorizationURI() { return authorizationURI; } /** * @return the clientId */ public String getClientId() { return clientId; } /** * @return the clientSecret */ public String getClientSecret() { return clientSecret; } protected Representation getErrorPage(Exception ex) { // Failed in initial auth resource request StringBuilder sb = new StringBuilder(); sb.append("<html><body><pre>"); if (ex instanceof OAuthException) { OAuthException oex = (OAuthException) ex; sb.append("OAuth2 error detected.\n"); sb.append("Error : ").append(oex.getError()); if (oex.getErrorDescription() != null) { sb.append("Error description : ").append( oex.getErrorDescription()); } if (oex.getErrorURI() != null) { sb.append("<a href=\""); sb.append(oex.getErrorURI()); sb.append("\">Error Description</a>"); } } else { sb.append("General error detected.\n"); sb.append("Error : ").append(ex.getMessage()); } sb.append("</pre></body></html>"); return new StringRepresentation(sb.toString(), MediaType.TEXT_HTML); } /** * @return the redirectURI */ public String getRedirectURI() { return redirectURI; } /** * @return the scope */ public String[] getScope() { return scope; } /** * @return the tokenURI */ public String getTokenURI() { return tokenURI; } private Token requestToken(String code) throws OAuthException, IOException, JSONException { getLogger().fine("Came back after authorization code = " + code); final AccessTokenClientResource tokenResource; String endpoint = getTokenURI(); if (endpoint.contains("graph.facebook.com")) { // We should use Facebook implementation. (Old draft spec.) tokenResource = new FacebookAccessTokenClientResource( new Reference(endpoint)); } else { tokenResource = new AccessTokenClientResource(new Reference( endpoint)); tokenResource .setAuthenticationMethod(basicSecret ? ChallengeScheme.HTTP_BASIC : null); } tokenResource.setClientCredentials(getClientId(), getClientSecret()); if (cc != null) { tokenResource.setNext(cc); } OAuthParameters tokenRequest = createTokenRequest(code); try { getLogger().fine("Sending access form : " + tokenRequest); return tokenResource.requestToken(tokenRequest); } finally { tokenResource.release(); } } private int sendErrorPage(Response response, Exception ex) { response.setStatus(Status.CLIENT_ERROR_BAD_REQUEST, ex.getMessage()); response.setEntity(getErrorPage(ex)); return STOP; } /** * @param authorizationURI * the authorizationURI to set */ public void setAuthorizationURI(String authorizationURI) { this.authorizationURI = authorizationURI; } /** * @param clientId * the clientId to set */ public void setClientId(String clientId) { this.clientId = clientId; } /** * @param clientSecret * the clientSecret to set */ public void setClientSecret(String clientSecret) { this.clientSecret = clientSecret; } /** * @param redirectURI * the redirectURI to set */ public void setRedirectURI(String redirectURI) { this.redirectURI = redirectURI; } /** * @param scope * the scope to set */ public void setScope(String[] scope) { this.scope = scope; } /** * @param tokenURI * the tokenURI to set */ public void setTokenURI(String tokenURI) { this.tokenURI = tokenURI; } private String setupState(Response response) { String sessionId = UUID.randomUUID().toString(); byte[] secret = new byte[20]; random.nextBytes(secret); String state = Base64.encode(secret, false); CookieSetting cs = new CookieSetting("_state", sessionId); response.getCookieSettings().add(cs); getContext().getAttributes().put(sessionId, state); return state; } private void validateState(Request request, Form params) throws Exception { String sessionId = request.getCookies().getFirstValue("_state"); String state = (String) getContext().getAttributes().get(sessionId); if (state != null && state.equals(params.getFirstValue(STATE))) { return; } // CSRF detected throw new Exception("The state does not match."); } }