/* * Copyright (c) 2005 Aetrion LLC. */ package com.flickr4java.flickr.auth; import com.flickr4java.flickr.Flickr; import com.flickr4java.flickr.FlickrException; import com.flickr4java.flickr.Response; import com.flickr4java.flickr.Transport; import com.flickr4java.flickr.people.User; import com.flickr4java.flickr.util.ByteUtilities; import com.flickr4java.flickr.util.XMLUtilities; import org.apache.log4j.Logger; import org.scribe.builder.ServiceBuilder; import org.scribe.builder.api.FlickrApi; import org.scribe.exceptions.OAuthException; import org.scribe.model.Token; import org.scribe.model.Verifier; import org.scribe.oauth.OAuthService; import org.w3c.dom.Element; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.TreeMap; /** * Authentication interface. * * @author Anthony Eden */ public class AuthInterface { /** * The "callback url" passed to Flickr if not specified by caller. */ private static final String OUT_OF_BOUND_AUTH_METHOD = "oob"; private static final String METHOD_CHECK_TOKEN = "flickr.auth.oauth.checkToken"; private static final String METHOD_EXCHANGE_TOKEN = "flickr.auth.oauth.getAccessToken"; private final String apiKey; private final String sharedSecret; private final Transport transportAPI; private final static Logger logger = Logger.getLogger(AuthInterface.class); private int maxGetTokenRetries = 3; /** * Construct the AuthInterface. * * @param apiKey * The API key * @param transport * The Transport interface */ public AuthInterface(String apiKey, String sharedSecret, Transport transport) { this.apiKey = apiKey; this.sharedSecret = sharedSecret; this.transportAPI = transport; } /** * Get the OAuth request token - this is step one of authorization. * * @return the {@link Token}, store this for when the user returns from the Flickr website. */ public Token getRequestToken() { return getRequestToken(null); } /** * Get the OAuth request token - this is step one of authorization. * * @param callbackUrl * optional callback URL - required for web auth flow, will be set to "oob" if not specified. * @return the {@link Token}, store this for when the user returns from the Flickr website. */ public Token getRequestToken(String callbackUrl) { String callback = (callbackUrl != null) ? callbackUrl : OUT_OF_BOUND_AUTH_METHOD; OAuthService service = new ServiceBuilder().provider(FlickrApi.class).apiKey(apiKey).apiSecret(sharedSecret).callback(callback).build(); return service.getRequestToken(); } /** * Get the auth URL, this is step two of authorization. * * @param oAuthRequestToken * the token from a {@link AuthInterface#getRequestToken} call. */ public String getAuthorizationUrl(Token oAuthRequestToken, Permission permission) { OAuthService service = new ServiceBuilder().provider(FlickrApi.class).apiKey(apiKey).apiSecret(sharedSecret).build(); String authorizationUrl = service.getAuthorizationUrl(oAuthRequestToken); return String.format("%s&perms=%s", authorizationUrl, permission.toString()); } /** * Trade the request token for an access token, this is step three of authorization. * * @param oAuthRequestToken * this is the token returned by the {@link AuthInterface#getRequestToken} call. * @param verifier * the Verifier created from the code entered by a user or passed back to a callback URL. */ @SuppressWarnings("boxing") public Token getAccessToken(Token oAuthRequestToken, Verifier verifier) { OAuthService service = new ServiceBuilder().provider(FlickrApi.class).apiKey(apiKey).apiSecret(sharedSecret).build(); // Flickr seems to return invalid token sometimes so retry a few times. // See http://www.flickr.com/groups/api/discuss/72157628028927244/ Token accessToken = null; boolean success = false; for (int i = 0; i < maxGetTokenRetries && !success; i++) { try { accessToken = service.getAccessToken(oAuthRequestToken, verifier); success = true; } catch (OAuthException e) { if (i == maxGetTokenRetries - 1) { logger.error(String.format("OAuthService.getAccessToken failing after %d tries, re-throwing exception", i), e); throw e; } else { logger.warn(String.format("OAuthService.getAccessToken failed, try number %d: %s", i, e.getMessage())); try { Thread.sleep(500); } catch (InterruptedException ie) { // Do nothing } } } } assert accessToken != null; assert success; return accessToken; } /** * Returns the credentials attached to an OAuth authentication token. * * @param accessToken * The authentication token * @return The Auth object * @throws FlickrException */ public Auth checkToken(Token accessToken) throws FlickrException { return checkToken(accessToken.getToken(), accessToken.getSecret()); } /** * * @param authToken * The authentication token * @param tokenSecret * @return The Auth object * @throws FlickrException * @see "http://www.flickr.com/services/api/flickr.auth.oauth.checkToken.html" */ public Auth checkToken(String authToken, String tokenSecret) throws FlickrException { // Use TreeMap so keys are automatically sorted alphabetically Map<String, String> parameters = new TreeMap<String, String>(); parameters.put("method", METHOD_CHECK_TOKEN); parameters.put("oauth_token", authToken); parameters.put(Flickr.API_KEY, apiKey); // This method call must be signed using Flickr (not OAuth) style signing parameters.put("api_sig", getSignature(sharedSecret, parameters)); Response response = transportAPI.getNonOAuth(transportAPI.getPath(), parameters); if (response.isError()) { throw new FlickrException(response.getErrorCode(), response.getErrorMessage()); } Auth auth = constructAuth(response, authToken, tokenSecret); return auth; } /** * Exchange an auth token from the old Authentication API, to an OAuth access token. * * Calling this method will delete the auth token used to make the request. * * @param authToken * @throws FlickrException * @see "http://www.flickr.com/services/api/flickr.auth.oauth.getAccessToken.html" */ public Token exchangeAuthToken(String authToken) throws FlickrException { // Use TreeMap so keys are automatically sorted alphabetically Map<String, String> parameters = new TreeMap<String, String>(); parameters.put("method", METHOD_EXCHANGE_TOKEN); parameters.put(Flickr.API_KEY, apiKey); // This method call must be signed using Flickr (not OAuth) style signing parameters.put("api_sig", getSignature(sharedSecret, parameters)); Response response = transportAPI.getNonOAuth(transportAPI.getPath(), parameters); if (response.isError()) { throw new FlickrException(response.getErrorCode(), response.getErrorMessage()); } Token accessToken = constructToken(response); return accessToken; } /** * * @param response * @param tokenSecret * @param authToken */ private Auth constructAuth(Response response, String authToken, String tokenSecret) { Auth auth = new Auth(); Element authElement = response.getPayload(); auth.setToken(authToken); auth.setTokenSecret(tokenSecret); auth.setPermission(Permission.fromString(XMLUtilities.getChildValue(authElement, "perms"))); Element userElement = XMLUtilities.getChild(authElement, "user"); User user = new User(); user.setId(userElement.getAttribute("nsid")); user.setUsername(userElement.getAttribute("username")); user.setRealName(userElement.getAttribute("fullname")); auth.setUser(user); return auth; } /** * Construct a Access Token from a Flickr Response. * * @param response */ private Token constructToken(Response response) { Element authElement = response.getPayload(); String oauthToken = XMLUtilities.getChildValue(authElement, "oauth_token"); String oauthTokenSecret = XMLUtilities.getChildValue(authElement, "oauth_token_secret"); Token token = new Token(oauthToken, oauthTokenSecret); return token; } /** * Get a signature for a list of parameters using the given shared secret. * * @param sharedSecret * The shared secret * @param params * The parameters * @return The signature String */ private String getSignature(String sharedSecret, Map<String, String> params) { StringBuffer buffer = new StringBuffer(); buffer.append(sharedSecret); for (Map.Entry<String, String> entry : params.entrySet()) { buffer.append(entry.getKey()); buffer.append(entry.getValue()); } try { MessageDigest md = MessageDigest.getInstance("MD5"); return ByteUtilities.toHexString(md.digest(buffer.toString().getBytes("UTF-8"))); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } catch (UnsupportedEncodingException u) { throw new RuntimeException(u); } } public void setMaxGetTokenRetries(int maxGetTokenRetries) { this.maxGetTokenRetries = maxGetTokenRetries; } }