/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.security.oauth1;
import com.google.api.client.auth.oauth.OAuthAuthorizeTemporaryTokenUrl;
import com.google.api.client.auth.oauth.OAuthCredentialsResponse;
import com.google.api.client.auth.oauth.OAuthGetAccessToken;
import com.google.api.client.auth.oauth.OAuthGetTemporaryToken;
import com.google.api.client.auth.oauth.OAuthHmacSigner;
import com.google.api.client.auth.oauth.OAuthParameters;
import com.google.api.client.auth.oauth.OAuthRsaSigner;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.util.Base64;
import org.eclipse.che.api.auth.shared.dto.OAuthToken;
import org.eclipse.che.commons.annotation.Nullable;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.net.URLDecoder.decode;
import static org.eclipse.che.dto.server.DtoFactory.newDto;
/**
* Authentication service which allows get access token from OAuth provider site.
*
* @author Kevin Pollet
* @author Igor Vinokur
*/
public abstract class OAuthAuthenticator {
private static final String USER_ID_PARAM_KEY = "userId";
private static final String REQUEST_METHOD_PARAM_KEY = "request_method";
private static final String SIGNATURE_METHOD_PARAM_KEY = "signature_method";
private static final String STATE_PARAM_KEY = "state";
private static final String OAUTH_TOKEN_PARAM_KEY = "oauth_token";
private static final String OAUTH_VERIFIER_PARAM_KEY = "oauth_verifier";
private final String clientId;
private final String clientSecret;
private final String privateKey;
private final String requestTokenUri;
private final String accessTokenUri;
private final String authorizeTokenUri;
private final String redirectUri;
private final HttpTransport httpTransport;
private final Map<String, OAuthCredentialsResponse> credentialsStore;
private final ReentrantLock credentialsStoreLock;
private final Map<String, String> sharedTokenSecrets;
protected OAuthAuthenticator(String clientId,
String requestTokenUri,
String accessTokenUri,
String authorizeTokenUri,
String redirectUri,
@Nullable String clientSecret,
@Nullable String privateKey) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.privateKey = privateKey;
this.requestTokenUri = requestTokenUri;
this.accessTokenUri = accessTokenUri;
this.authorizeTokenUri = authorizeTokenUri;
this.redirectUri = redirectUri;
this.httpTransport = new NetHttpTransport();
this.credentialsStore = new HashMap<>();
this.credentialsStoreLock = new ReentrantLock();
this.sharedTokenSecrets = new HashMap<>();
}
/**
* Create authentication URL.
*
* @param requestUrl
* URL of current HTTP request. This parameter required to be able determine URL for redirection after
* authentication. If URL contains query parameters they will be copied to 'state' parameter and returned to
* callback method.
* @param requestMethod
* HTTP request method that will be used to request temporary token
* @param signatureMethod
* OAuth signature algorithm
* @return URL for authentication.
* @throws OAuthAuthenticationException
* if authentication failed.
*/
String getAuthenticateUrl(final URL requestUrl,
@Nullable final String requestMethod,
@Nullable final String signatureMethod) throws OAuthAuthenticationException {
try {
final GenericUrl callbackUrl = new GenericUrl(redirectUri);
callbackUrl.put(STATE_PARAM_KEY, requestUrl.getQuery());
OAuthGetTemporaryToken temporaryToken;
if (requestMethod != null && "post".equals(requestMethod.toLowerCase())) {
temporaryToken = new OAuthPostTemporaryToken(requestTokenUri);
} else {
temporaryToken = new OAuthGetTemporaryToken(requestTokenUri);
}
if (signatureMethod != null && "rsa".equals(signatureMethod.toLowerCase())) {
temporaryToken.signer = getOAuthRsaSigner();
} else {
temporaryToken.signer = getOAuthHmacSigner(null, null);
}
temporaryToken.consumerKey = clientId;
temporaryToken.callback = callbackUrl.build();
temporaryToken.transport = httpTransport;
final OAuthCredentialsResponse credentialsResponse = temporaryToken.execute();
final OAuthAuthorizeTemporaryTokenUrl authorizeTemporaryTokenUrl = new OAuthAuthorizeTemporaryTokenUrl(authorizeTokenUri);
authorizeTemporaryTokenUrl.temporaryToken = credentialsResponse.token;
sharedTokenSecrets.put(credentialsResponse.token, credentialsResponse.tokenSecret);
return authorizeTemporaryTokenUrl.build();
} catch (Exception e) {
throw new OAuthAuthenticationException(e.getMessage());
}
}
/**
* Process callback request.
*
* @param requestUrl
* request URI. URI should contain OAuth token and OAuth verifier.
* @return id of authenticated user
* @throws OAuthAuthenticationException
* if authentication failed or {@code requestUrl} does not contain required parameters.
*/
String callback(final URL requestUrl) throws OAuthAuthenticationException {
try {
final GenericUrl callbackUrl = new GenericUrl(requestUrl.toString());
if (callbackUrl.getFirst(OAUTH_TOKEN_PARAM_KEY) == null) {
throw new OAuthAuthenticationException("Missing oauth_token parameter");
}
if (callbackUrl.getFirst(OAUTH_VERIFIER_PARAM_KEY) == null) {
throw new OAuthAuthenticationException("Missing oauth_verifier parameter");
}
final String state = (String)callbackUrl.getFirst(STATE_PARAM_KEY);
String requestMethod = getParameterFromState(state, REQUEST_METHOD_PARAM_KEY);
String signatureMethod = getParameterFromState(state, SIGNATURE_METHOD_PARAM_KEY);
final String oauthTemporaryToken = (String)callbackUrl.getFirst(OAUTH_TOKEN_PARAM_KEY);
OAuthGetAccessToken getAccessToken;
if (requestMethod != null && "post".equals(requestMethod.toLowerCase())) {
getAccessToken = new OAuthPostAccessToken(accessTokenUri);
} else {
getAccessToken = new OAuthGetAccessToken(accessTokenUri);
}
getAccessToken.consumerKey = clientId;
getAccessToken.temporaryToken = oauthTemporaryToken;
getAccessToken.verifier = (String)callbackUrl.getFirst(OAUTH_VERIFIER_PARAM_KEY);
getAccessToken.transport = httpTransport;
if (signatureMethod != null && "rsa".equals(signatureMethod.toLowerCase())) {
getAccessToken.signer = getOAuthRsaSigner();
} else {
getAccessToken.signer = getOAuthHmacSigner(clientSecret, sharedTokenSecrets.remove(oauthTemporaryToken));
}
final OAuthCredentialsResponse credentials = getAccessToken.execute();
String userId = getParameterFromState(state, USER_ID_PARAM_KEY);
credentialsStoreLock.lock();
try {
final OAuthCredentialsResponse userId2Credential = credentialsStore.get(userId);
if (userId2Credential == null) {
credentialsStore.put(userId, credentials);
} else {
userId2Credential.token = credentials.token;
userId2Credential.tokenSecret = credentials.tokenSecret;
}
} finally {
credentialsStoreLock.unlock();
}
return userId;
} catch (Exception e) {
throw new OAuthAuthenticationException(e.getMessage());
}
}
/**
* Get name of OAuth provider supported by current implementation.
*
* @return the oauth provider name.
*/
abstract String getOAuthProvider();
/**
* Compute the Authorization header to sign the OAuth 1 request.
*
* @param userId
* the user id.
* @param requestMethod
* the HTTP request method.
* @param requestUrl
* the HTTP request url with encoded query parameters.
* @return the authorization header value, or {@code null} if token was not found for given user id.
* @throws OAuthAuthenticationException
* if authentication failed.
*/
String computeAuthorizationHeader(final String userId,
final String requestMethod,
final String requestUrl) throws OAuthAuthenticationException {
final OAuthCredentialsResponse credentials = new OAuthCredentialsResponse();
OAuthToken oauthToken = getToken(userId);
credentials.token = oauthToken != null ? oauthToken.getToken() : null;
if (credentials.token != null) {
return computeAuthorizationHeader(requestMethod, requestUrl, credentials.token, credentials.tokenSecret);
}
return null;
}
private OAuthToken getToken(final String userId) {
OAuthCredentialsResponse credentials;
credentialsStoreLock.lock();
try {
credentials = credentialsStore.get(userId);
} finally {
credentialsStoreLock.unlock();
}
if (credentials != null) {
return newDto(OAuthToken.class).withToken(credentials.token).withScope(credentials.tokenSecret);
}
return null;
}
/**
* Compute the Authorization header to sign the OAuth 1 request.
*
* @param requestMethod
* the HTTP request method.
* @param requestUrl
* the HTTP request url with encoded query parameters.
* @param token
* the token.
* @param tokenSecret
* the secret token.
* @return the authorization header value, or {@code null}.
*/
private String computeAuthorizationHeader(final String requestMethod,
final String requestUrl,
final String token,
final String tokenSecret) throws OAuthAuthenticationException {
OAuthParameters oauthParameters;
try {
oauthParameters = new OAuthParameters();
oauthParameters.consumerKey = clientId;
oauthParameters.signer = clientSecret == null ? getOAuthRsaSigner() : getOAuthHmacSigner(clientSecret, tokenSecret);
oauthParameters.token = token;
oauthParameters.version = "1.0";
oauthParameters.computeNonce();
oauthParameters.computeTimestamp();
oauthParameters.computeSignature(requestMethod, new GenericUrl(requestUrl));
} catch (GeneralSecurityException e) {
throw new OAuthAuthenticationException(e);
}
return oauthParameters.getAuthorizationHeader();
}
private String getParameterFromState(String state, String parameterName) {
if (isNullOrEmpty(state)) {
return null;
}
for (final String param : extractStateParams(state)) {
if (param.startsWith(parameterName + "=")) {
return param.substring(parameterName.length() + 1);
}
}
return null;
}
private String[] extractStateParams(String state) {
try {
String decodedState = decode(state, "UTF-8");
return decodedState.split("&");
} catch (UnsupportedEncodingException ignored) {
// should never happen, UTF-8 supported.
}
return null;
}
private OAuthRsaSigner getOAuthRsaSigner() throws NoSuchAlgorithmException, InvalidKeySpecException {
OAuthRsaSigner oAuthRsaSigner = new OAuthRsaSigner();
oAuthRsaSigner.privateKey = getPrivateKey(privateKey);
return oAuthRsaSigner;
}
private OAuthHmacSigner getOAuthHmacSigner(@Nullable String clientSecret, @Nullable String oauthTemporaryToken)
throws NoSuchAlgorithmException, InvalidKeySpecException {
final OAuthHmacSigner signer = new OAuthHmacSigner();
signer.clientSharedSecret = clientSecret;
signer.tokenSharedSecret = sharedTokenSecrets.remove(oauthTemporaryToken);
return signer;
}
private PrivateKey getPrivateKey(String privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] privateKeyBytes = Base64.decodeBase64(privateKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
private static class OAuthPostTemporaryToken extends OAuthGetTemporaryToken {
OAuthPostTemporaryToken(String authorizationServerUrl) {
super(authorizationServerUrl);
super.usePost = true;
}
}
private static class OAuthPostAccessToken extends OAuthGetAccessToken {
OAuthPostAccessToken(String authorizationServerUrl) {
super(authorizationServerUrl);
super.usePost = true;
}
}
}