/**
* 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.util.Arrays;
import java.util.HashMap;
import java.util.List;
import org.restlet.data.CacheDirective;
import org.restlet.data.MediaType;
import org.restlet.data.Reference;
import org.restlet.ext.freemarker.ContextTemplateLoader;
import org.restlet.ext.freemarker.TemplateRepresentation;
import org.restlet.ext.oauth.internal.AuthSession;
import org.restlet.ext.oauth.internal.Client;
import org.restlet.ext.oauth.internal.Scopes;
import org.restlet.ext.oauth.internal.Token;
import org.restlet.representation.EmptyRepresentation;
import org.restlet.representation.Representation;
import org.restlet.resource.Get;
import freemarker.template.Configuration;
/**
* Helper class to the AuhorizationResource Handles Authorization requests. By
* default it will accept all scopes requested.
*
* To intercept and allow a user to control authorization you should set the
* OAuthHelper.setAuthPageTemplate parameter. It should contain a static HTML
* page or a FreeMarker page that will be loaded with the CLAP protocol straight
* from root.
*
* Example. Add an AuthPageResource to your inbound root.
*
* <pre>
* {
* @code
* public Restlet createInboundRoot(){
* ...
* root.attach(OAuthHelper.getAuthPage(getContext()), AuthPageServerResource.class);
* //Set Template for AuthPage:
* OAuthHelper.setAuthPageTemplate("authorize.html", getContext());
* //Dont ask for approval if previously approved
* OAuthHelper.setAuthSkipApproved(true, getContext());
* ...
* }
*
* }
* </pre>
*
* The FreeMarker data model looks like the following
*
* <pre>
* {
* @code
* HashMap<String, Object> data = new HashMap<String, Object>();
* data.put("target", "/oauth/auth_page");
* data.put("clientId", clientId);
* data.put("clientDescription", client.toString());
* data.put("clientCallback", client.getRedirectUri());
* data.put("clientName", client.getApplicationName());
* data.put("requestingScopes", scopes);
* data.put("grantedScopes", previousScopes);
* }
* </pre>
*
* Below is an example of a simple FreeMarker page for authorization
*
* <pre>
* {@code
* <html>
* <head>
* <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
* <link rel="stylesheet" href="resources/style.css" type="text/css" media="screen"
* charset="utf-8">
* <title>OAuth2 Authorization Server</title>
* </head>
* <body>
* <div id="container">
* <div id="header">
* <h2>OAuth authorization page</h2>
* <section id="intro">
* <h2>Application requesting scope</h2>
* <p>Client ClientId = ${clientId} CB = ${clientDescription} wants to get access to your information.</p>
* </section>
* </div>
* <aside>
* <form action="${target}" method="get">
* <h4>The following private info is requested</h4>
*
* <#list requestingScopes as r> <input type="checkbox" name="scope" value="${r}" checked />
* <b>${r}</b><br/>
* </#list>
* <#if grantedScopes?has_content>
* <hr />
* <h4>Previously approved scopes</h4>
* <#list grantedScopes as g> <input type="checkbox" name="scope" value="${g}" checked />
* <b>${g}</b><br/>
* </#list>
* </#if>
* <br/>
* <input type="submit" name="action" value="Reject"/>
* <input type="submit" name="action" value="Accept" />
* </form>
* </aside>
* <footer>
* <p class="copyright">Copyright © 2010 Ericsson Inc. All rights reserved.</p>
* </footer>
* </div>
* </body>
* </html>
* }
* </pre>
*
*
* should be set in the attributes. It should contain a static HTML page or a
* FreeMarker page that will be loaded with the CLAP protocol straight from
* root.
*
* @author Kristoffer Gronowski
* @author Shotaro Uchida <fantom@xmaker.mx>
*/
public class AuthPageServerResource extends AuthorizationBaseServerResource {
private static final String ACTION_ACCEPT = "Accept";
private static final String ACTION_REJECT = "Reject";
/**
* Helper method if a auth page was present in a context attribute.
*
* The Freemarker Data model looks the following :
*
* HashMap<String,Object> data = new HashMap<String,Object>();
* data.put("target", "/oauth/auth_page"); data.put("clientId", clientId);
* data.put("clientDescription", client.toString());
* data.put("clientCallback", client.getRedirectUri());
* data.put("clientName", client.getApplicationName());
* data.put("requestingScopes", scopes); data.put("grantedScopes",
* previousScopes);
*
* @param authPage
* name of the page in class loader context
* @return html page representation
*/
protected Representation getPage(String authPage) {
String clientId = getQuery().getFirstValue("client");
Client client = clients.findById(clientId);
String[] scopes = getQuery().getValuesArray("scope");
String[] previousScopes = getQuery().getValuesArray("grantedScope");
Configuration config = new Configuration();
ContextTemplateLoader ctl = new ContextTemplateLoader(getContext(),
"clap:///");
config.setTemplateLoader(ctl);
getLogger().fine("loading: " + authPage);
TemplateRepresentation result = new TemplateRepresentation(authPage,
config, MediaType.TEXT_HTML);
// Build the model
HashMap<String, Object> data = new HashMap<String, Object>();
data.put("target",
getRootRef() + HttpOAuthHelper.getAuthPage(getContext()));
// TODO check with Restlet lead
data.put("clientId", clientId);
data.put("clientDescription", client.toString());
data.put("clientCallback", client.getRedirectURIs());
data.put("clientProperties", client.getProperties());
// scopes
data.put("requestingScopes", scopes);
data.put("grantedScopes", previousScopes);
result.setDataModel(data);
return result;
}
/**
*
* Helper method to handle a FORM response. Returns with setting a 307 with
* the location header. Token if the token flow was requested or code is
* included.
*
* @param action
* as interacted by the user.
* @param grantedScope
* the scopes that was approved.
*/
protected void handleAction(String action, String[] grantedScope)
throws OAuthException {
// TODO: SessionId should maybe be removed
AuthSession session = getAuthSession();
session.setGrantedScope(grantedScope);
if (action.equals(ACTION_REJECT)) {
getLogger().fine("Rejected.");
throw new OAuthException(OAuthError.access_denied, "Rejected.",
null);
}
getLogger().fine("Accepting scopes - in handleAction");
Client client = clients.findById(session.getClientId());
String scopeOwner = session.getScopeOwner();
// Create redirection
final Reference location = new Reference(session.getRedirectionURI()
.getURI());
String state = session.getState();
if (state != null && !state.isEmpty()) {
// Setting state information back.
location.addQueryParameter(STATE, state);
}
// Add query parameters for each flow.
ResponseType flow = session.getAuthFlow();
if (flow.equals(ResponseType.token)) {
Token token = tokens
.generateToken(client, scopeOwner, grantedScope);
location.addQueryParameter(TOKEN_TYPE, token.getTokenType());
location.addQueryParameter(ACCESS_TOKEN, token.getAccessToken());
location.addQueryParameter(EXPIRES_IN,
Integer.toString(token.getExpirePeriod()));
String[] scope = token.getScope();
if (!Scopes.isIdentical(scope, session.getRequestedScope())) {
// OPTIONAL, if identical to the scope requested by the client,
// otherwise REQUIRED. (4.2.2. Access Token Response)
location.addQueryParameter(SCOPE, Scopes.toString(scope));
}
} else if (flow.equals(ResponseType.code)) {
String code = tokens.storeSession(session);
location.addQueryParameter(CODE, code);
}
// Reset the state
session.setState(null);
/*
* We might don't need to do this. // Sets the no-store Cache-Control
* header addCacheDirective(getResponse(), CacheDirective.noStore()); //
* TODO: Set Pragma: no-cache
*/
if (flow.equals(ResponseType.token)) {
// Use fragment for Implicit Grant
location.setFragment(location.getQuery());
location.setQuery("");
}
getLogger().fine("Redirecting to -> " + location);
redirectTemporary(location);
}
/**
* Entry point to the AuthPageResource. The AuthorizationResource dispatches
* the call to this method. Should also be invoked by an eventual HTML page
* FORM. In the from HTTP GET should be used and a result parameter: action
* = Accept results in approving requested scope while action = Reject
* results in a rejection error back to the requestor.
*
* @return HTML page with the graphical policy page
*/
@Get("html")
public Representation showPage() throws OAuthException {
String action = getQuery().getFirstValue("action");
// Came back after user interacted with the page
if (action != null) {
String[] scopes = getQuery().getValuesArray("scope");
handleAction(action, scopes);
return new EmptyRepresentation();
}
// Check if an auth page is set in the Context
String authPage = HttpOAuthHelper.getAuthPageTemplate(getContext());
getLogger().fine("this is auth page: " + authPage);
if (authPage != null && authPage.length() > 0) {
getLogger().fine("loading authPage: " + authPage);
// Check if we should skip the page if already approved scopes
boolean sameScope = HttpOAuthHelper
.getAuthSkipApproved(getContext());
if (sameScope) {
String[] scopesArray = getQuery().getValuesArray("scope");
List<String> scopes = Arrays.asList(scopesArray);
List<String> previousScopes = Arrays.asList(getQuery()
.getValuesArray("grantedScope"));
if (previousScopes.containsAll(scopes)) {
// we already have approved the current scopes being
// requested...
getLogger().fine(
"All scopes already approved. - skip auth page.");
handleAction(ACTION_ACCEPT, scopesArray);
return new EmptyRepresentation(); // Will redirect
}
}
addCacheDirective(getResponse(), CacheDirective.noCache());
return getPage(authPage);
}
getLogger().fine("accepting scopes since no authPage: " + authPage);
// No page automatically accept all the scopes requested
handleAction(ACTION_ACCEPT, getQuery().getValuesArray("scope"));
getLogger().fine("action handled");
return new EmptyRepresentation(); // Will redirect
}
}