/* *Copyright (c) 2005-2013, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * *WSO2 Inc. licenses this file to you under the Apache License, *Version 2.0 (the "License"); you may not use this file except *in compliance with the License. *You may obtain a copy of the License at * *http://www.apache.org/licenses/LICENSE-2.0 * *Unless required by applicable law or agreed to in writing, *software distributed under the License is distributed on an *"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *KIND, either express or implied. See the License for the *specific language governing permissions and limitations *under the License. */ package org.wso2.carbon.identity.oauth; import com.google.gdata.client.authn.oauth.GoogleOAuthParameters; import com.google.gdata.client.authn.oauth.OAuthException; import com.google.gdata.client.authn.oauth.OAuthHmacSha1Signer; import com.google.gdata.client.authn.oauth.OAuthUtil; import org.apache.axis2.context.MessageContext; import org.apache.axis2.context.ServiceContext; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.wso2.carbon.core.common.AuthenticationException; import org.wso2.carbon.identity.base.IdentityException; import org.wso2.carbon.identity.oauth.dao.OAuthAppDO; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.oauth.dao.OAuthAppDAO; import org.wso2.carbon.identity.oauth.dao.OAuthConsumerDAO; import org.wso2.carbon.identity.oauth.dto.OAuthConsumerDTO; import org.wso2.carbon.registry.core.exceptions.RegistryException; import org.wso2.carbon.user.api.UserStoreException; import org.wso2.carbon.utils.multitenancy.MultitenantUtils; import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; public class OAuthService { private static final String OAUTH_LATEST_TIMESTAMP = "OAUTH_LATEST_TIMESTAMP"; private static final String OAUTH_NONCE_STORE = "OAUTH_NONCE_STORE"; private static Log log = LogFactory.getLog(OAuthService.class); /** * Checks whether the given consumer is valid or not. This is done by validating the signature, * signed by this particular consumer. * * @param oauthConsumer Parameter related to the OAuth authorization header. * @return * @throws Exception */ public boolean isOAuthConsumerValid(OAuthConsumerDTO oauthConsumer) throws IdentityException { String oAuthSecretKey = getOAuthSecretKey(oauthConsumer.getOauthConsumerKey()); if (oAuthSecretKey == null) { log.debug("Invalid Consumer Key."); throw IdentityException.error("Invalid Consumer Key"); } try { return validateOauthSignature(oauthConsumer, oAuthSecretKey); } catch (AuthenticationException e) { throw IdentityException.error(e.getMessage(), e); } } /** * Returns the OAuth request token after verifying the consumer. The Service Provider verifies * the signature and Consumer Key. If successful, it generates a Request Token and Token Secret * and returns them to the Consumer in the HTTP response body as defined in Service Provider * Response Parameters. The Service Provider MUST ensure the Request Token cannot be exchanged * for an Access Token until the User successfully grants access in Obtaining User * Authorization. * * @param params A container for the following attributes. * @param params:oauth_consumer_key (required) : Domain identifying the third-party web * application. This is the domain used when registering the application with WSO2 * Identity Server. * @param params:oauth_nonce (required) : Random 64-bit, unsigned number encoded as an ASCII * string in decimal format. The nonce/time-stamp pair should always be unique to * prevent replay attacks. * @param params:oauth_signature_method (required) : Signature algorithm. The legal values for * this parameter "RSA-SHA1" or "HMAC-SHA1". WSO2 does not support "PLAINTEXT" and * "RSA-SHA1". * @param params:oauth_signature (required) String generated using the referenced signature * method. * @param params:oauth_timestamp (required) : Integer representing the time the request is sent. * The time-stamp should be expressed in number of seconds after January 1, 1970 * 00:00:00 GMT. scope (required) URL identifying the service(s) to be accessed. The * resulting token enables access to the specified service(s) only. * @param params:scope : The resource the third party web application should be authorized to * access. To specify more than one scope, list each one separated with a space. This * parameter is not defined in the OAuth standards, it is a WSO2-specific parameter. * @param params:oauth_callback (required) : URL the user should be redirected to after access * to a service is granted (in response to a call to OAuthAuthorizeToken). The * response to this getOauthRequestToken call verifies that WSO2 handles a call-back * URL. * @param params:oauth_version (optional) : The OAuth version used by the requesting web * application. This value must be "1.0"; if not provided, WSO2 assumes version 1.0 * is in use. * @return oauth_token, oauth_token_secret, oauth_callback_confirmed * @throws Exception */ public Parameters getOauthRequestToken(Parameters params) throws AuthenticationException, IdentityOAuthAdminException { boolean isValidSignature = false; String secretkey = null; validateTimestampAndNonce(params.getOauthTimeStamp(), params.getOauthNonce()); OAuthConsumerDAO dao = new OAuthConsumerDAO(); secretkey = dao.getOAuthConsumerSecret(params.getOauthConsumerKey()); if (secretkey == null) { log.debug("Invalid Credentials."); throw new AuthenticationException("Invalid Credentials."); } isValidSignature = validateOauthSignature(params, secretkey, null); if (!isValidSignature) { throw new AuthenticationException("Invalid Signature"); } return generateOauthToken(params); } /** * Authorizes the OAuth request token for the given scope. In order for the Consumer to be able * to exchange the Request Token for an Access Token, the Consumer MUST obtain approval from the * User by directing the User to the Service Provider. The Consumer constructs an HTTP GET * request to the Service Provider's User Authorization URL with the following parameters. * * @param params A container for the following attributes. * @param params:oauth_token (required) : Request token obtained from WSO2. * @param params:userName : User who authorizes the token. * @param params:password : Password of the user who authorizes the token. * @return oauth_token, oauth_verifier * @throws Exception */ public Parameters authorizeOauthRequestToken(Parameters params) throws IdentityException, AuthenticationException { String tenantUser = MultitenantUtils.getTenantAwareUsername(params.getAuthorizedbyUserName()); String domainName = MultitenantUtils.getTenantDomain(params.getAuthorizedbyUserName()); boolean isAuthenticated = false; try { isAuthenticated = IdentityTenantUtil .getRealm(domainName, params.getAuthorizedbyUserName()).getUserStoreManager() .authenticate(tenantUser, params.getAuthorizedbyUserPassword()); } catch (UserStoreException e) { log.error("Error while authenticating the user", e); throw IdentityException.error("Error while authenticating the user"); } if (isAuthenticated) { OAuthConsumerDAO dao = new OAuthConsumerDAO(); String oauthVerifier = org.wso2.carbon.identity.oauth.OAuthUtil.getRandomNumber(); Parameters token = dao.authorizeOAuthToken(params.getOauthToken(), tenantUser, oauthVerifier); token.setOauthToken(params.getOauthToken()); token.setOauthTokenVerifier(oauthVerifier); return token; } else { throw new AuthenticationException("User Authentication Failed"); } } /** * Exchanges the authorized OAuth token to an access token. To request an Access Token, the * Consumer makes an HTTP request to the Service Provider's Access Token URL. The Service * Provider documentation specifies the HTTP method for this request, and HTTP POST is * RECOMMENDED. The request MUST be signed per Signing Requests. The Service Provider MUST * ensure that: The request signature has been successfully verified. The Request Token has * never been exchanged for an Access Token. The Request Token matches the Consumer Key. The * verification code received from the Consumer has been successfully verified. * * @param params A container for the following attributes. * @param params:oauth_consumer_key (required) : Domain identifying the third-party web * application. This is the domain used when registering the application with WSO2 * Identity Server. * @param params:oauth_nonce (required) : Random 64-bit, unsigned number encoded as an ASCII * string in decimal format. The nonce/time-stamp pair should always be unique to * prevent replay attacks. * @param params:oauth_signature_method (required) : Signature algorithm. The legal values for * this parameter "RSA-SHA1" or "HMAC-SHA1". WSO2 does not support "PLAINTEXT" and * "RSA-SHA1". * @param params:oauth_signature (required) String generated using the referenced signature * method. * @param params:oauth_timestamp (required) : Integer representing the time the request is sent. * The time-stamp should be expressed in number of seconds after January 1, 1970 * 00:00:00 GMT. scope (required) URL identifying the service(s) to be accessed. The * resulting token enables access to the specified service(s) only. * @param params:oauth_version (optional) : The OAuth version used by the requesting web * application. This value must be "1.0"; if not provided, WSO2 assumes version 1.0 * is in use. * @return oauth_token, oauth_token_secret * @throws Exception */ public Parameters getAccessToken(Parameters params) throws IdentityOAuthAdminException, AuthenticationException, IdentityException { boolean isValidSignature = false; String secretKey = null; OAuthConsumerDAO dao = new OAuthConsumerDAO(); secretKey = dao.getOAuthConsumerSecret(params.getOauthConsumerKey()); if (secretKey == null) { log.debug("Invalid Credentials."); throw new AuthenticationException("Invalid Credentials."); } String tokenSecret = dao.getOAuthTokenSecret(params.getOauthToken(), false); isValidSignature = validateOauthSignature(params, secretKey, tokenSecret); if (!isValidSignature) { throw new AuthenticationException("Invalid Signature"); } // The request signature has been successfully verified Parameters resp = dao.getRequestToken(params.getOauthToken()); if (resp.getOauthTokenVerifier() == null || !resp.getOauthTokenVerifier().equals(params.getOauthTokenVerifier()) || resp.getAuthorizedbyUserName() == null) { throw new AuthenticationException("Invalid request for OAuth access token"); } // The Request Token has never been exchanged for an Access Token resp.isAccessTokenIssued() // = false // The verification code received from the Consumer has been successfully verified - // resp.getOauthTokenVerifier() String oauthToken = org.wso2.carbon.identity.oauth.OAuthUtil.getRandomNumber(); String oauthSecret = org.wso2.carbon.identity.oauth.OAuthUtil.getRandomNumber(); dao.issueAccessToken(params.getOauthConsumerKey(), oauthToken, oauthSecret, params.getOauthToken(), resp.getAuthorizedbyUserName(), resp.getScope()); resp.setOauthToken(oauthToken); resp.setOauthTokenSecret(oauthSecret); return resp; } /** * Returns the scope and the web application this particular token been issued to. * * @param oauthToken OAuth request token. * @return * @throws Exception */ public Parameters getScopeAndAppName(String oauthToken) throws Exception { OAuthConsumerDAO consumerDAO = new OAuthConsumerDAO(); Parameters params = consumerDAO.getRequestToken(oauthToken); OAuthAppDAO appDAO = new OAuthAppDAO(); OAuthAppDO oauthAppDO = appDAO.getAppInformation(params.getOauthConsumerKey()); Parameters resp = new Parameters(); resp.setScope(params.getScope()); resp.setAppName(oauthAppDO.getApplicationName()); return resp; } /** * Validates the request to a resource protected with OAuth. After successfully receiving the * Access Token and Token Secret, the Consumer is able to access the Protected Resources on * behalf of the User. * * @param params A container for the following attributes. * @param params:oauth_consumer_key (required) : Domain identifying the third-party web * application. This is the domain used when registering the application with WSO2 * Identity Server. * @param params:oauth_token (required) : OAuth access token. * @param params:oauth_nonce (required) : Random 64-bit, unsigned number encoded as an ASCII * string in decimal format. The nonce/time-stamp pair should always be unique to * prevent replay attacks. * @param params:oauth_signature_method (required) : Signature algorithm. The legal values for * this parameter "RSA-SHA1" or "HMAC-SHA1". WSO2 does not support "PLAINTEXT" and * "RSA-SHA1". * @param params:oauth_signature (required) String generated using the referenced signature * method. * @param params:oauth_timestamp (required) : Integer representing the time the request is sent. * The time-stamp should be expressed in number of seconds after January 1, 1970 * 00:00:00 GMT. scope (required) URL identifying the service(s) to be accessed. The * resulting token enables access to the specified service(s) only. * @param params:oauth_version (optional) : The OAuth version used by the requesting web * application. This value must be "1.0"; if not provided, WSO2 assumes version 1.0 * is in use. * @return Parameters : scope : the authorized scope * @throws Exception Error when validating the access token request. */ public Parameters validateAuthenticationRequest(Parameters params) throws AuthenticationException, IdentityException { boolean isAuthenticated = false; String secretKey = null; validateTimestampAndNonce(params.getOauthTimeStamp(), params.getOauthNonce()); OAuthConsumerDAO dao = new OAuthConsumerDAO(); secretKey = dao.getOAuthConsumerSecret(params.getOauthConsumerKey()); if (secretKey == null) { log.debug("Invalid Credentials."); throw new AuthenticationException("Invalid Credentials."); } String tokenSecret = dao.getOAuthTokenSecret(params.getOauthToken(), true); isAuthenticated = validateOauthSignature(params, secretKey, tokenSecret); if (isAuthenticated) { // Signature is verified - so this is a valid OAuth consumer. String subject = dao.validateAccessToken(params.getOauthConsumerKey(), params.getOauthToken(), params.getScope()); Parameters returnParams = new Parameters(); returnParams.setAuthorizedbyUserName(subject); returnParams.setScope(params.getScope()); return returnParams; } else { throw new AuthenticationException("Invalid Signature."); } } /** * @param oauthParams * @return * @throws RegistryException * @throws IdentityException */ private Parameters generateOauthToken(Parameters oauthParams) throws IdentityOAuthAdminException { OAuthConsumerDAO dao = new OAuthConsumerDAO(); String oauthToken = org.wso2.carbon.identity.oauth.OAuthUtil.getRandomNumber(); String oauthSecret = org.wso2.carbon.identity.oauth.OAuthUtil.getRandomNumber(); dao.createOAuthRequestToken(oauthParams.getOauthConsumerKey(), oauthToken, oauthSecret, oauthParams.getOauthCallback(), oauthParams.getScope()); Parameters params = new Parameters(); params.setOauthConsumerKey(oauthParams.getOauthConsumerKey()); params.setOauthToken(oauthToken); params.setOauthTokenSecret(oauthSecret); return params; } /** * @param oauthParams * @param secretKey * @return * @throws Exception */ private boolean validateOauthSignature(OAuthConsumerDTO oauthParams, String secretKey) throws AuthenticationException { GoogleOAuthParameters oauthParameters = new GoogleOAuthParameters(); oauthParameters.setOAuthConsumerKey(oauthParams.getOauthConsumerKey()); oauthParameters.setOAuthConsumerSecret(secretKey); oauthParameters.setOAuthNonce(oauthParams.getOauthNonce()); oauthParameters.setOAuthTimestamp(oauthParams.getOauthTimeStamp()); oauthParameters.setOAuthSignatureMethod(oauthParams.getOauthSignatureMethod()); validateTimestampAndNonce(oauthParams.getOauthTimeStamp(), oauthParams.getOauthNonce()); OAuthHmacSha1Signer signer = new OAuthHmacSha1Signer(); String signature; try { String baseString = OAuthUtil.getSignatureBaseString(oauthParams.getBaseString(), oauthParams.getHttpMethod(), oauthParameters.getBaseParameters()); signature = signer.getSignature(baseString, oauthParameters); } catch (OAuthException e) { throw new AuthenticationException(e.getMessage(), e); } if (signature != null && URLEncoder.encode(signature).equals(oauthParams.getOauthSignature())) { return true; } else if (signature != null && signature.equals(oauthParams.getOauthSignature())) { return true; } return false; } /** * @param oauthParams * @param secretKey * @return * @throws Exception */ private boolean validateOauthSignature(Parameters oauthParams, String secretKey, String tokenSecret) throws AuthenticationException { GoogleOAuthParameters oauthParameters = new GoogleOAuthParameters(); oauthParameters.setOAuthConsumerKey(oauthParams.getOauthConsumerKey()); oauthParameters.setOAuthConsumerSecret(secretKey); oauthParameters.setOAuthNonce(oauthParams.getOauthNonce()); oauthParameters.setOAuthTimestamp(oauthParams.getOauthTimeStamp()); oauthParameters.setOAuthSignatureMethod(oauthParams.getOauthSignatureMethod()); if (oauthParams.getOauthToken() != null) { oauthParameters.setOAuthToken(oauthParams.getOauthToken()); } if (oauthParams.getOauthTokenVerifier() != null) { oauthParameters.setOAuthVerifier((oauthParams.getOauthTokenVerifier())); } if (tokenSecret != null) { oauthParameters.setOAuthTokenSecret(tokenSecret); } OAuthHmacSha1Signer signer = new OAuthHmacSha1Signer(); String signature; try { String baseString = OAuthUtil.getSignatureBaseString(oauthParams.getBaseString(), oauthParams.getHttpMethod(), oauthParameters.getBaseParameters()); signature = signer.getSignature(baseString, oauthParameters); } catch (OAuthException e) { throw new AuthenticationException("Error while validating signature"); } if (signature != null && URLEncoder.encode(signature).equals(oauthParams.getOauthSignature())) { return true; } else if (signature != null && signature.equals(oauthParams.getOauthSignature())) { return true; } return false; } /** * Unless otherwise specified by the Service Provider, the time-stamp is expressed in the number * of seconds since January 1, 1970 00:00:00 GMT. The time-stamp value MUST be a positive * integer and MUST be equal or greater than the time-stamp used in previous requests. The * Consumer SHALL then generate a Nonce value that is unique for all requests with that * timestamp. A nonce is a random string, uniquely generated for each request. The nonce allows * the Service Provider to verify that a request has never been made before and helps prevent * replay attacks when requests are made over a non-secure channel (such as HTTP). * * @param timestamp * @param nonce * @throws Exception */ private void validateTimestampAndNonce(String timestamp, String nonce) throws AuthenticationException { if (timestamp == null || nonce == null || nonce.trim().length() == 0) { // We are not going to give out the exact error why the request failed. throw new AuthenticationException("Invalid request for OAuth access token"); } long time = Long.parseLong(timestamp); synchronized (this) { long latestTimeStamp = 0; String strTimestamp; ServiceContext context = MessageContext.getCurrentMessageContext().getServiceContext(); if ((strTimestamp = (String) context.getProperty(OAUTH_LATEST_TIMESTAMP)) != null) { latestTimeStamp = Long.parseLong(strTimestamp); } if (time < 0 || time < latestTimeStamp) { // The time-stamp value MUST be a positive integer and MUST be equal or greater than // the time-stamp used in previous requests throw new AuthenticationException("Invalid timestamp"); } context.setProperty(OAUTH_LATEST_TIMESTAMP, String.valueOf(time)); List<String> nonceStore = null; if ((nonceStore = (List<String>) context.getProperty(OAUTH_NONCE_STORE)) != null) { if (nonceStore.contains(nonce)) { // We are not going to give out the exact error why the request failed. throw new AuthenticationException("Invalid request for OAuth access token"); } else { nonceStore.add(nonce); } } else { nonceStore = new ArrayList<String>(); nonceStore.add(nonce); context.setProperty(OAUTH_NONCE_STORE, nonceStore); } } } /** * @param consumerKey Consumer Key provided by the user * @return consumer secret * @throws Exception Error when reading the consumer secret from the persistence store. */ private String getOAuthSecretKey(String consumerKey) throws IdentityOAuthAdminException { OAuthConsumerDAO dao = new OAuthConsumerDAO(); return dao.getOAuthConsumerSecret(consumerKey); } }