package com.ibm.sbt.opensocial.domino.oauth;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang3.StringUtils;
import org.apache.shindig.auth.SecurityToken;
import org.apache.shindig.common.crypto.BlobCrypter;
import org.apache.shindig.common.servlet.Authority;
import org.apache.shindig.common.uri.Uri;
import org.apache.shindig.gadgets.GadgetContext;
import org.apache.shindig.gadgets.GadgetException;
import org.apache.shindig.gadgets.GadgetException.Code;
import org.apache.shindig.gadgets.GadgetSpecFactory;
import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
import org.apache.shindig.gadgets.oauth2.OAuth2Arguments;
import org.apache.shindig.gadgets.oauth2.OAuth2Error;
import org.apache.shindig.gadgets.oauth2.OAuth2GadgetContext;
import org.apache.shindig.gadgets.oauth2.OAuth2RequestException;
import org.apache.shindig.gadgets.oauth2.OAuth2Token;
import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Encrypter;
import org.apache.shindig.gadgets.oauth2.persistence.OAuth2TokenPersistence;
import org.apache.shindig.gadgets.spec.BaseOAuthService.EndPoint;
import org.apache.shindig.gadgets.spec.GadgetSpec;
import org.apache.shindig.gadgets.spec.OAuth2Service;
import org.apache.shindig.gadgets.spec.OAuth2Spec;
import com.google.common.base.Joiner;
import com.ibm.sbt.opensocial.domino.container.ContainerExtPoint;
import com.ibm.sbt.opensocial.domino.container.ContainerExtPointException;
import com.ibm.sbt.opensocial.domino.container.ContainerExtPointManager;
/**
* Stores OAuth2 information.
*
*/
//TODO Right now every container is forced to let every gadget share OAuth tokens. Some containers may not want that. We should allow
//containers to specify that in their configuration and handle that here.
public class DominoOAuth2TokenStore {
private static final String CLASS = DominoOAuth2TokenStore.class.getName();
private Map<String, DominoOAuth2Accessor> accessorStore = Collections.synchronizedMap(new HashMap<String, DominoOAuth2Accessor>());
private Map<String, OAuth2Token> accessTokenStore = Collections.synchronizedMap(new HashMap<String, OAuth2Token>());
private Map<String, OAuth2Token> refreshTokenStore = Collections.synchronizedMap(new HashMap<String, OAuth2Token>());
private static class OAuth2SpecInfo {
private final String authorizationUrl;
private final String scope;
private final String tokenUrl;
public OAuth2SpecInfo(final String authorizationUrl, final String tokenUrl, final String scope) {
this.authorizationUrl = authorizationUrl;
this.tokenUrl = tokenUrl;
this.scope = scope;
}
public String getAuthorizationUrl() {
return this.authorizationUrl;
}
public String getScope() {
return this.scope;
}
public String getTokenUrl() {
return this.tokenUrl;
}
}
private ContainerExtPointManager manager;
private Logger log;
private GadgetSpecFactory specFactory;
private OAuth2Encrypter encrypter;
private Authority authority;
private String contextRoot;
private BlobCrypter stateCrypter;
private String globalRedirectUri;
/**
* Creates a new DominoOAuth2TokenStore.
* @param specFactory The spec factory containing the gadget specs.
* @param manager The container extension point manager.
* @param log The logger.
* @param encrypter The OAuth 2.0 encyrpter.
* @param authority The authority information.
* @param contextRoot The servers context root.
* @param stateCrypter The encrypter to use for state information.
* @param globalRedirectUri The OAuth 2 callback URI.
*/
public DominoOAuth2TokenStore(GadgetSpecFactory specFactory, ContainerExtPointManager manager,
Logger log, OAuth2Encrypter encrypter,
Authority authority, String contextRoot,
BlobCrypter stateCrypter,
String globalRedirectUri) {
this.specFactory = specFactory;
this.manager = manager;
this.encrypter = encrypter;
this.authority = authority;
this.contextRoot = contextRoot;
this.stateCrypter = stateCrypter;
this.globalRedirectUri = globalRedirectUri;
this.log = log;
}
/**
* Gets an OAuth 2 accessor. This method will actually merge OAuth 2 information from the gadget spec
* if it is present and the OAuth 2 client allows it.
* @param securityToken The security token containing information to lookup the accessor by.
* @param arguments The OAuth 2 arguments.
* @param gadgetUri The gadget URI.
* @return An OAuth 2 accessor if one exists in the store.
*/
public DominoOAuth2Accessor getOAuth2Accessor(SecurityToken securityToken, OAuth2Arguments arguments, Uri gadgetUri) {
final String method = "getOAuth2Accessor";
log.entering(CLASS, method,
new Object[] { securityToken, arguments, gadgetUri });
DominoOAuth2Accessor ret = null;
if ((gadgetUri == null) || (securityToken == null)) {
ret = new BasicDominoOAuth2Accessor();
ret.setErrorResponse(null, OAuth2Error.GET_OAUTH2_ACCESSOR_PROBLEM,
"OAuth2Accessor missing a param --- gadgetUri = "
+ gadgetUri + " , securityToken = " + securityToken, "");
} else {
final String serviceName = StringUtils.defaultString(arguments.getServiceName());
try {
ret = getOAuth2Accessor(gadgetUri, serviceName, securityToken, arguments);
storeOAuth2Accessor(ret);
} catch (Exception e) {
log.logp(Level.WARNING, CLASS, method, "Error while getting OAuth 2 accessor.", e);
ret = new BasicDominoOAuth2Accessor();
ret.setErrorResponse(e, OAuth2Error.GET_OAUTH2_ACCESSOR_PROBLEM, "Error while getting OAuth 2 accessor", "");
}
}
log.exiting(CLASS, method, ret);
return ret;
}
/**
* Gets the scope the OAuth2Arguments. If the OAuth2Arguments does not have scope information than we will try and get it
* from the gadget spec. If the gadget spec does not have scope information than return the empty string.
* @param arguments OAuth 2 arguments.
* @param specInfo Gadget spec.
* @return The OAuth 2 scope.
*/
private String getScope(OAuth2Arguments arguments, OAuth2SpecInfo specInfo) {
return StringUtils.isBlank(arguments.getScope()) ? StringUtils.isBlank(specInfo.getScope()) ? "" : specInfo.getScope() : arguments.getScope();
}
private DominoOAuth2Accessor getOAuth2Accessor(Uri gadgetUri, String serviceName, SecurityToken securityToken, OAuth2Arguments arguments) throws GadgetException, OAuth2RequestException {
OAuth2SpecInfo specInfo = this.lookupSpecInfo(securityToken, arguments, gadgetUri);
String scope = getScope(arguments, specInfo);
String container = securityToken.getContainer();
DominoOAuth2Accessor persistedAccessor = getOAuth2Accessor(gadgetUri.toString(), serviceName,
securityToken.getViewerId(), scope, container);
return addModuleOverrides(persistedAccessor, securityToken, arguments, gadgetUri, specInfo);
}
private DominoOAuth2Accessor addModuleOverrides(DominoOAuth2Accessor accessor, SecurityToken securityToken, OAuth2Arguments arguments, Uri gadgetUri, OAuth2SpecInfo specInfo)
throws OAuth2RequestException {
DominoOAuth2Accessor mergedAccessor = new BasicDominoOAuth2Accessor(accessor);
mergedAccessor.setContainer(securityToken.getContainer());
if (accessor.isAllowModuleOverrides()) {
final String specAuthorizationUrl = specInfo.getAuthorizationUrl();
final String specTokenUrl = specInfo.getTokenUrl();
if (!StringUtils.isBlank(specAuthorizationUrl)) {
mergedAccessor.setAuthorizationUrl(specAuthorizationUrl);
}
if (!StringUtils.isBlank(specTokenUrl)) {
mergedAccessor.setTokenUrl(specTokenUrl);
}
}
return mergedAccessor;
}
private OAuth2SpecInfo lookupSpecInfo(final SecurityToken securityToken,
final OAuth2Arguments arguments, final Uri gadgetUri) throws OAuth2RequestException {
log.entering(CLASS, "lookupSpecInfo", new Object[] { securityToken, arguments, gadgetUri });
final GadgetSpec spec = this.findSpec(securityToken, arguments, gadgetUri);
final OAuth2Spec oauthSpec = spec.getModulePrefs().getOAuth2Spec();
if (oauthSpec == null) {
throw new OAuth2RequestException(OAuth2Error.LOOKUP_SPEC_PROBLEM,
"Failed to retrieve OAuth URLs, spec for gadget " + securityToken.getAppUrl()
+ " does not contain OAuth element.", null);
}
final OAuth2Service service = oauthSpec.getServices().get(arguments.getServiceName());
if (service == null) {
throw new OAuth2RequestException(OAuth2Error.LOOKUP_SPEC_PROBLEM,
"Failed to retrieve OAuth URLs, spec for gadget does not contain OAuth service "
+ arguments.getServiceName() + ". Known services: "
+ Joiner.on(',').join(oauthSpec.getServices().keySet()) + '.', null);
}
String authorizationUrl = null;
final EndPoint authorizationUrlEndpoint = service.getAuthorizationUrl();
if (authorizationUrlEndpoint != null) {
authorizationUrl = authorizationUrlEndpoint.url.toString();
}
String tokenUrl = null;
final EndPoint tokenUrlEndpoint = service.getTokenUrl();
if (tokenUrlEndpoint != null) {
tokenUrl = tokenUrlEndpoint.url.toString();
}
final OAuth2SpecInfo ret = new OAuth2SpecInfo(authorizationUrl, tokenUrl, service.getScope());
log.exiting(CLASS, "lookupSpecInfo", ret);
return ret;
}
private GadgetSpec findSpec(final SecurityToken securityToken, final OAuth2Arguments arguments,
final Uri gadgetUri) throws OAuth2RequestException {
final String method = "findSpec";
log.entering(CLASS, method, new Object[] { arguments, gadgetUri });
GadgetSpec ret;
try {
final GadgetContext context = new OAuth2GadgetContext(securityToken, arguments, gadgetUri);
ret = this.specFactory.getGadgetSpec(context);
} catch (final GadgetException e) {
log.logp(Level.WARNING, CLASS, method, "Error finding GadgetContext " + gadgetUri.toString(), e);
throw new OAuth2RequestException(OAuth2Error.GADGET_SPEC_PROBLEM, gadgetUri.toString(), e);
}
// this is cumbersome in the logs, just return whether or not it's null
if (ret == null) {
log.exiting(CLASS, method, null);
} else {
log.exiting(CLASS, method, "non-null spec omitted from logs");
}
return ret;
}
private DominoOAuth2Store getOAuth2Store(String container) throws GadgetException {
final String method = "getOAuth2Store";
ContainerExtPoint extPoint = manager.getExtPoint(container);
if(extPoint != null) {
try {
return extPoint.getContainerOAuth2Store();
} catch (ContainerExtPointException e) {
log.logp(Level.WARNING, CLASS, method, "There was an error getting the OAuth2Store for container " + container, e);
throw new GadgetException(GadgetException.Code.OAUTH_STORAGE_ERROR,
"There was an error getting the OAuth2Store for container " + container, e);
}
} else {
log.logp(Level.WARNING, CLASS, method, "There was no ContainerExtPoint for container {0}.", new Object[] {container});
throw new GadgetException(GadgetException.Code.OAUTH_STORAGE_ERROR, "No ContainerExtPoint for container " + container);
}
}
/**
* Removes an OAuth 2 accessor from the store.
* @param accessor The accessor to remove.
* @throws GadgetException Thrown if there is an issue removing the accessor.
*/
public void removeOAuth2Accessor(DominoOAuth2Accessor accessor) throws GadgetException {
synchronized (accessorStore) {
accessorStore.remove(generateKey(accessor));
}
}
/**
* Stores an OAuth 2 accessor in the store.
* @param accessor The accessor to store.
* @throws GadgetException Thrown if there is an issue storing the accessor.
*/
public void storeOAuth2Accessor(DominoOAuth2Accessor accessor) throws GadgetException {
synchronized (accessorStore) {
accessorStore.put(generateKey(accessor), accessor);
}
}
/**
* Removes an OAuth 2 accessor from the store.
* @param accessor The accessor to remove.
* @throws GadgetException Thrown if there is an issue removing the accessor.
*/
public void removeAccessToken(DominoOAuth2Accessor accessor) throws GadgetException {
synchronized (accessTokenStore) {
accessTokenStore.remove(generateKey(accessor));
}
}
/**
* Removes an OAuth 2 refresh token from the store.
* @param accessor The accessor containing the refresh token to remove.
* @throws GadgetException Thrown is there is an issue removing the refresh token.
*/
public void removeRefreshToken(DominoOAuth2Accessor accessor) throws GadgetException {
synchronized (refreshTokenStore) {
refreshTokenStore.remove(generateKey(accessor));
}
}
private String generateKey(DominoOAuth2Accessor accessor) throws GadgetException {
return generateKey(accessor.getContainer(), accessor.getServiceName(), accessor.getScope(), accessor.getUser());
}
private String generateKey(DominoOAuth2CallbackState state) throws GadgetException {
return generateKey(state.getContainer(), state.getServiceName(), state.getScope(), state.getUser());
}
private String generateKey(String container, OAuth2Token token) throws GadgetException {
return generateKey(container, token.getServiceName(), token.getScope(), token.getUser());
}
private String generateKey(String container, String serviceName, String scope, String user) throws GadgetException {
if(container == null || serviceName == null || user == null) {
throw new GadgetException(GadgetException.Code.OAUTH_STORAGE_ERROR, "Invalid key parameters, container: " + container + " user: " + user + " service name: " + serviceName);
}
return container + ":" + serviceName + ":" + scope + ":" + user;
}
/**
* Creates a new OAuth 2 token that can be used as a refresh or access token.
* @return A new OAuth 2 token.
*/
public OAuth2Token createToken() {
return new OAuth2TokenPersistence(this.encrypter);
}
/**
* Stores an OAuth 2 refresh token in the store.
* @param container The container the refresh token belongs to.
* @param token The token to store.
* @throws GadgetException Thrown if there is an issue storing the refresh token.
*/
public void storeRefreshToken(String container, OAuth2Token token) throws GadgetException {
synchronized (refreshTokenStore) {
refreshTokenStore.put(generateKey(container, token), token);
}
}
/**
* Stores an OAuth 2 access token in the store.
* @param container The container the access token belongs to.
* @param token The token to store.
* @throws GadgetException Thrown if there is an issue storing the access token.
*/
public void storeAccessToken(String container, OAuth2Token token) throws GadgetException {
synchronized (accessTokenStore) {
accessTokenStore.put(generateKey(container, token), token);
}
}
/**
* Gets an OAuth 2 accessor. This method should only be called if you are sure the token is already in the store.
* It will not consider any OAuth2 information from within the gadget.
* @param state The OAuth 2 callback state information.
* @return An OAuth 2 accessor.
* @throws GadgetException Thrown if there is a problem retrieving the accessor.
*/
public DominoOAuth2Accessor getOAuth2Accessor(DominoOAuth2CallbackState state) throws GadgetException {
DominoOAuth2Accessor ret = accessorStore.get(generateKey(state));
if (ret == null || !ret.isValid()) {
final DominoOAuth2Client client = getClient(state);
if (client == null) {
throw new GadgetException(Code.OAUTH_STORAGE_ERROR,
"Could not find OAuth2 client information where container = " + state.getContainer() +
", user = " + state.getUser() + ", serviceName = " + state.getServiceName() + ", and scope = " + state.getScope() + ".");
} else {
ret = createAccessor(state, client);
this.storeOAuth2Accessor(ret);
}
}
return ret;
}
private DominoOAuth2Client getClient(DominoOAuth2CallbackState state) throws GadgetException {
return getClient(state.getUser(), state.getServiceName(), state.getContainer(),
state.getScope(), state.getGadgetUri());
}
private DominoOAuth2Client getClient(String user, String serviceName, String container, String scope,
String gadgetUri) throws GadgetException {
final String method = "getClient";
DominoOAuth2Store store = getOAuth2Store(container);
if(store == null) {
log.logp(Level.WARNING, CLASS, method,
"Could not find an OAuth2 store for the container {0}, returning an error accessor.",
new Object[]{container});
return null;
}
final DominoOAuth2Client client = store.getClient(user, serviceName, container, scope, gadgetUri);
if (client == null) {
log.logp(Level.WARNING, CLASS, method,
"Could not find OAuth2 client information where container = {0}, user = {1}, serviceName = {2}, and scope = {3}.",
new Object[]{container, user, serviceName, scope});
return null;
}
return client;
}
private DominoOAuth2Accessor createAccessor(DominoOAuth2CallbackState state, DominoOAuth2Client client)
throws GadgetException {
return createAccessor(state.getGadgetUri(), state.getServiceName(), state.getUser(), state.getScope(), state.getContainer(),
client);
}
private DominoOAuth2Accessor createAccessor(String gadgetUri, String serviceName, String user, String scope, String container,
DominoOAuth2Client client) throws GadgetException {
final OAuth2Token accessToken = this.getAccessToken(gadgetUri, serviceName, user, scope, container);
final OAuth2Token refreshToken = this.getRefreshToken(gadgetUri, serviceName, user, scope, container);
String authType = client.getClientAuthenticationType() == null ? null : client.getClientAuthenticationType().toString();
final BasicDominoOAuth2Accessor newAccessor = new BasicDominoOAuth2Accessor(gadgetUri, serviceName,
user, scope, client.isAllowModuleOverride(), this.stateCrypter, this.globalRedirectUri,
this.authority, this.contextRoot, container);
newAccessor.setAccessToken(accessToken);
newAccessor.setAuthorizationUrl(client.getAuthorizationUrl());
newAccessor.setClientAuthenticationType(authType);
newAccessor.setAuthorizationHeader(client.useAuthorizationHeader());
newAccessor.setUrlParameter(client.useUrlParameter());
newAccessor.setClientId(client.getClientId());
newAccessor.setClientSecret(client.getClientSecret().getBytes());
newAccessor.setGrantType(client.getGrantType().toString());
newAccessor.setRefreshToken(refreshToken);
newAccessor.setTokenUrl(client.getTokenUrl());
//TODO do we want to support other types?
newAccessor.setType(OAuth2Accessor.Type.CONFIDENTIAL);
newAccessor.setAllowedDomains(new String[0]);
return newAccessor;
}
/**
* Gets an OAuth 2 access token.
* @param gadgetUri The gadget URI.
* @param serviceName The OAuth 2 service name.
* @param user The user.
* @param scope The OAuth 2 scope for the service.
* @param container The container.
* @return An OAuth 2 access token.
* @throws GadgetException Thrown if there is a problem retrieving the access token.
*/
public OAuth2Token getAccessToken(String gadgetUri, String serviceName, String user, String scope, String container) throws GadgetException {
return accessTokenStore.get(generateKey(container, serviceName, scope, user));
}
/**
* Gets an OAuth 2 refresh token.
* @param gadgetUri The gadget URI.
* @param serviceName The OAuth 2 service name.
* @param user The user.
* @param scope The OAuth 2 scope for the service.
* @param container The container.
* @return An OAuth 2 refresh token.
* @throws GadgetException Thrown if there is a problem retrieving the refresh token.
*/
public OAuth2Token getRefreshToken(String gadgetUri, String serviceName, String user, String scope, String container) throws GadgetException {
return refreshTokenStore.get(generateKey(container, serviceName, scope, user));
}
private DominoOAuth2Accessor getOAuth2Accessor(String gadgetUri, String serviceName,
String user, String scope, String container) throws GadgetException {
final DominoOAuth2CallbackState state = new DominoOAuth2CallbackState(this.stateCrypter);
state.setGadgetUri(gadgetUri);
state.setServiceName(serviceName);
state.setUser(user);
state.setScope(scope);
state.setContainer(container);
return getOAuth2Accessor(state);
}
}