/*
* Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores
* CA 94065 USA or visit www.oracle.com if you need additional information or
* have any questions.
*/
package com.codename1.io;
import com.codename1.components.InfiniteProgress;
import com.codename1.components.WebBrowser;
import com.codename1.ui.Command;
import com.codename1.ui.Component;
import com.codename1.ui.Dialog;
import com.codename1.ui.Display;
import com.codename1.ui.Form;
import com.codename1.ui.animations.CommonTransitions;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.events.ActionListener;
import com.codename1.ui.html.DocumentInfo;
import com.codename1.ui.layouts.BorderLayout;
import com.codename1.util.regex.StringReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Map;
/**
* This is a utility class that allows Oauth2 authentication This utility uses
* the Codename One XHTML Component to display the authentication pages.
* http://tools.ietf.org/pdf/draft-ietf-oauth-v2-12.pdf
*
* @author Chen Fishbein
*/
public class Oauth2 {
public static final String TOKEN = "access_token";
/**
* Enables going back to the parent form after login is completed
*
* @return the backToParent
*/
public static boolean isBackToParent() {
return backToParent;
}
/**
* Enables going back to the parent form after login is completed
*
* @param aBackToParent the backToParent to set
*/
public static void setBackToParent(boolean aBackToParent) {
backToParent = aBackToParent;
}
private String token;
private static String expires;
private String clientId;
private String redirectURI;
private String scope;
private String clientSecret;
private String oauth2URL;
private String tokenRequestURL;
private Hashtable additionalParams;
private Dialog login;
private static boolean backToParent = true;
/**
* Simple constructor
*
* @param oauth2URL the authentication url of the service
* @param clientId the client id that would like to use the service
* @param redirectURI the redirect uri
*/
public Oauth2(String oauth2URL, String clientId, String redirectURI) {
this(oauth2URL, clientId, redirectURI, null, null, null);
}
/**
* Simple constructor
*
* @param oauth2URL the authentication url of the service
* @param clientId the client id that would like to use the service
* @param redirectURI the redirect uri
* @param scope the authentication scope
*/
public Oauth2(String oauth2URL, String clientId, String redirectURI, String scope) {
this(oauth2URL, clientId, redirectURI, scope, null, null);
}
/**
* Simple constructor
*
* @param oauth2URL the authentication url of the service
* @param clientId the client id that would like to use the service
* @param redirectURI the redirect uri
* @param scope the authentication scope
* @param clientSecret the client secret
*/
public Oauth2(String oauth2URL, String clientId, String redirectURI, String scope,
String tokenRequestURL, String clientSecret) {
this(oauth2URL, clientId, redirectURI, scope, tokenRequestURL, clientSecret, null);
}
/**
* Returns the expiry for the token received via oauth
*
* @return the expires argument for the token
*/
public static String getExpires() {
return expires;
}
/**
* Simple constructor
*
* @param oauth2URL the authentication url of the service
* @param clientId the client id that would like to use the service
* @param redirectURI the redirect uri
* @param scope the authentication scope
* @param clientSecret the client secret
* @param additionalParams hashtable of additional parameters to the
* authentication request
*/
public Oauth2(String oauth2URL, String clientId, String redirectURI, String scope, String tokenRequestURL, String clientSecret, Hashtable additionalParams) {
this.oauth2URL = oauth2URL;
this.redirectURI = redirectURI;
this.clientId = clientId;
this.scope = scope;
this.clientSecret = clientSecret;
this.tokenRequestURL = tokenRequestURL;
this.additionalParams = additionalParams;
}
/**
* This method preforms the actual authentication, this method is a blocking
* method that will display the user the html authentication pages.
*
* @return the method if passes authentication will return the access token
* or null if authentication failed.
*
* @throws IOException the method will throw an IOException if something
* went wrong in the communication.
* @deprecated use createAuthComponent or showAuthentication which work
* asynchronously and adapt better to different platforms
*/
public String authenticate() {
if (token == null) {
login = new Dialog();
boolean i = Dialog.isAutoAdjustDialogSize();
Dialog.setAutoAdjustDialogSize(false);
login.setLayout(new BorderLayout());
login.setScrollable(false);
Component html = createLoginComponent(null, null, null, null);
login.addComponent(BorderLayout.CENTER, html);
login.setScrollable(false);
login.setDialogUIID("Container");
login.setTransitionInAnimator(CommonTransitions.createSlide(CommonTransitions.SLIDE_VERTICAL, true, 300));
login.setTransitionOutAnimator(CommonTransitions.createSlide(CommonTransitions.SLIDE_VERTICAL, false, 300));
login.show(0, 0, 0, 0, false, true);
Dialog.setAutoAdjustDialogSize(i);
}
return token;
}
/**
* This method creates a component which can authenticate. You will receive
* either the authentication key or an Exception object within the
* ActionListener callback method.
*
* @param al a listener that will receive at its source either a token for
* the service or an exception in case of a failure
* @return a component that should be displayed to the user in order to
* perform the authentication
*/
public Component createAuthComponent(ActionListener al) {
return createLoginComponent(al, null, null, null);
}
/**
* This method shows an authentication for login form
*
* @param al a listener that will receive at its source either a token for
* the service or an exception in case of a failure
* @return a component that should be displayed to the user in order to
* perform the authentication
*/
public void showAuthentication(ActionListener al) {
final Form old = Display.getInstance().getCurrent();
InfiniteProgress inf = new InfiniteProgress();
final Dialog progress = inf.showInifiniteBlocking();
Form authenticationForm = new Form("Login");
authenticationForm.setScrollable(false);
if (old != null) {
Command cancel = new Command("Cancel") {
public void actionPerformed(ActionEvent ev) {
if (Display.getInstance().getCurrent() == progress) {
progress.dispose();
}
old.showBack();
}
};
if (authenticationForm.getToolbar() != null){
authenticationForm.getToolbar().addCommandToLeftBar(cancel);
} else {
authenticationForm.addCommand(cancel);
}
authenticationForm.setBackCommand(cancel);
}
authenticationForm.setLayout(new BorderLayout());
authenticationForm.addComponent(BorderLayout.CENTER, createLoginComponent(al, authenticationForm, old, progress));
}
private Component createLoginComponent(final ActionListener al, final Form frm, final Form backToForm, final Dialog progress) {
String URL = oauth2URL + "?client_id=" + clientId
+ "&redirect_uri=" + Util.encodeUrl(redirectURI);
if (scope != null) {
URL += "&scope=" + scope;
}
if (clientSecret != null) {
URL += "&response_type=code";
} else {
URL += "&response_type=token";
}
if (additionalParams != null) {
Enumeration e = additionalParams.keys();
while (e.hasMoreElements()) {
String key = (String) e.nextElement();
String val = additionalParams.get(key).toString();
URL += "&" + key + "=" + val;
}
}
DocumentInfo.setDefaultEncoding(DocumentInfo.ENCODING_UTF8);
final WebBrowser[] web = new WebBrowser[1];
web[0] = new WebBrowser() {
@Override
public void onLoad(String url) {
handleURL(url, this, al, frm, backToForm, progress);
}
public void onStart(String url) {
}
};
web[0].setURL(URL);
return web[0];
}
private void handleURL(String url, WebBrowser web, final ActionListener al, final Form frm, final Form backToForm, final Dialog progress) {
if ((url.startsWith(redirectURI))) {
if (Display.getInstance().getCurrent() == progress) {
progress.dispose();
}
web.stop();
//remove the browser component.
if (login != null) {
login.removeAll();
login.revalidate();
}
if (url.indexOf("code=") > -1) {
Hashtable params = getParamsFromURL(url);
ConnectionRequest req = new ConnectionRequest() {
protected void readResponse(InputStream input) throws IOException {
byte[] tok = Util.readInputStream(input);
String t = new String(tok);
if(t.startsWith("{")){
JSONParser p = new JSONParser();
Map map = p.parseJSON(new StringReader(t));
token = (String) map.get("access_token");
Object ex = map.get("expires_in");
if(ex == null){
ex = map.get("expires");
}
if(ex != null){
expires = ex.toString();
}
}else{
token = t.substring(t.indexOf("=") + 1, t.indexOf("&"));
int off = t.indexOf("expires=");
int start = 8;
if(off == -1){
off = t.indexOf("expires_in=");
start = 11;
}
if (off > -1) {
int end = t.indexOf('&', off);
if (end < 0 || end < off) {
end = t.length();
}
expires = t.substring(off + start, end);
}
}
if (login != null) {
login.dispose();
}
}
protected void handleException(Exception err) {
if (backToForm != null) {
backToForm.showBack();
}
if (al != null) {
al.actionPerformed(new ActionEvent(err,ActionEvent.Type.Exception));
}
}
protected void postResponse() {
if (backToParent && backToForm != null) {
backToForm.showBack();
}
if (al != null) {
al.actionPerformed(new ActionEvent(new AccessToken(token, expires),ActionEvent.Type.Response));
}
}
};
req.setUrl(tokenRequestURL);
req.setPost(true);
req.addRequestHeader("Content-Type", "application/x-www-form-urlencoded");
req.addArgument("client_id", clientId);
req.addArgument("redirect_uri", redirectURI);
req.addArgument("client_secret", clientSecret);
req.addArgument("code", (String) params.get("code"));
req.addArgument("grant_type", "authorization_code");
NetworkManager.getInstance().addToQueue(req);
} else if (url.indexOf("error_reason=") > -1) {
Hashtable table = getParamsFromURL(url);
String error = (String) table.get("error_reason");
if (login != null) {
login.dispose();
}
if (backToForm != null) {
backToForm.showBack();
}
if (al != null) {
al.actionPerformed(new ActionEvent(new IOException(error),ActionEvent.Type.Exception));
}
} else {
boolean success = url.indexOf("#") > -1;
if (success) {
String accessToken = url.substring(url.indexOf("#") + 1);
if (accessToken.indexOf("&") > 0) {
token = accessToken.substring(accessToken.indexOf("=") + 1, accessToken.indexOf("&"));
} else {
token = accessToken.substring(accessToken.indexOf("=") + 1);
}
if (login != null) {
login.dispose();
}
if (backToParent && backToForm != null) {
backToForm.showBack();
}
if (al != null) {
al.actionPerformed(new ActionEvent(new AccessToken(token, expires),ActionEvent.Type.Response));
}
}
}
} else {
if (frm != null && Display.getInstance().getCurrent() != frm) {
progress.dispose();
frm.show();
}
}
}
private Hashtable getParamsFromURL(String url) {
int paramsStarts = url.indexOf('?');
if (paramsStarts > -1) {
url = url.substring(paramsStarts + 1);
}
Hashtable retVal = new Hashtable();
String[] params = Util.split(url, "&");
int plen = params.length;
for (int i = 0; i < plen; i++) {
if (params[i].indexOf("=") > 0) {
String[] keyVal = Util.split(params[i], "=");
retVal.put(keyVal[0], keyVal[1]);
}
}
return retVal;
}
}