diff options
author | Danny van Heumen <danny@dannyvanheumen.nl> | 2015-06-10 00:19:09 +0200 |
---|---|---|
committer | Danny van Heumen <danny@dannyvanheumen.nl> | 2015-07-20 22:29:45 +0200 |
commit | 218b69991e594ea56e29f809803900473a47b487 (patch) | |
tree | a74d5893850d97cedfcaf49727adfddf11a72556 /src/net/java/sip/communicator/impl | |
parent | dcbeba9b6a262ce0ab99b96e673a7ef1ec8b5acd (diff) | |
download | jitsi-218b69991e594ea56e29f809803900473a47b487.zip jitsi-218b69991e594ea56e29f809803900473a47b487.tar.gz jitsi-218b69991e594ea56e29f809803900473a47b487.tar.bz2 |
Working on simple UI to facilitate user approval process.
Diffstat (limited to 'src/net/java/sip/communicator/impl')
-rw-r--r-- | src/net/java/sip/communicator/impl/googlecontacts/GoogleContactsConnectionImpl.java | 136 | ||||
-rw-r--r-- | src/net/java/sip/communicator/impl/googlecontacts/OAuth2TokenStore.java | 340 |
2 files changed, 354 insertions, 122 deletions
diff --git a/src/net/java/sip/communicator/impl/googlecontacts/GoogleContactsConnectionImpl.java b/src/net/java/sip/communicator/impl/googlecontacts/GoogleContactsConnectionImpl.java index 46849aa..7c0bafc 100644 --- a/src/net/java/sip/communicator/impl/googlecontacts/GoogleContactsConnectionImpl.java +++ b/src/net/java/sip/communicator/impl/googlecontacts/GoogleContactsConnectionImpl.java @@ -7,15 +7,12 @@ package net.java.sip.communicator.impl.googlecontacts; import java.io.*; -import java.util.concurrent.atomic.*; +import net.java.sip.communicator.impl.googlecontacts.OAuth2TokenStore.FailedAcquireCredentialException; +import net.java.sip.communicator.impl.googlecontacts.OAuth2TokenStore.FailedTokenRefreshException; import net.java.sip.communicator.service.googlecontacts.*; import net.java.sip.communicator.util.*; -import com.google.api.client.auth.oauth2.*; -import com.google.api.client.http.*; -import com.google.api.client.http.javanet.*; -import com.google.api.client.json.jackson2.*; import com.google.gdata.client.contacts.*; import com.google.gdata.data.contacts.*; import com.google.gdata.util.*; @@ -36,37 +33,9 @@ public class GoogleContactsConnectionImpl Logger.getLogger(GoogleContactsConnectionImpl.class); /** - * Google OAuth 2 token server. - */ - private static final GenericUrl GOOGLE_OAUTH2_TOKEN_SERVER = - new GenericUrl("https://accounts.google.com/o/oauth2/token"); - - /** - * Client ID for OAuth 2 based authentication. - */ - private static final String GOOGLE_API_CLIENT_ID = null; - - /** - * Client secret for OAuth 2 based authentication. - */ - private static final String GOOGLE_API_CLIENT_SECRET = null; - - // FIXME Actually use scopes! - private static final String[] GOOGLE_API_OAUTH2_SCOPES = new String[] - { "profile", "email", "https://www.google.com/m8/feeds" }; - - // FIXME Actually use the redirect URL! - private static final String GOOGLE_API_OAUTH2_REDIRECT_URI = - "urn:ietf:wg:oauth:2.0:oob"; - - // FIXME (Danny) Temporary stored refresh token during development... - private static final String TEMP_REFRESH_TOKEN = - "1/mjFeVE86qVmm-O2B6SSLweW6hBcj4qxuYSb0fGvJvH0"; - - /** * The credential store to pass around. */ - private final AtomicReference<Credential> credential = new AtomicReference<Credential>(null); + private final OAuth2TokenStore store = new OAuth2TokenStore(); /** * Login. @@ -164,9 +133,16 @@ public class GoogleContactsConnectionImpl */ public synchronized ConnectionStatus connect() { - createCredential(GOOGLE_OAUTH2_TOKEN_SERVER); - googleService.setOAuth2Credentials(this.credential.get()); - return ConnectionStatus.SUCCESS; + try + { + googleService.setOAuth2Credentials(this.store.get()); + return ConnectionStatus.SUCCESS; + } + catch (FailedAcquireCredentialException e) + { + logger.error("Failed to acquire credentials.", e); + return ConnectionStatus.ERROR_UNKNOWN; + } } /** @@ -199,7 +175,7 @@ public class GoogleContactsConnectionImpl // refresh token logger.info("Failed to execute query. Going to refresh token" + " and try again.", e); - refreshToken(); + this.store.refresh(); } try { @@ -212,80 +188,6 @@ public class GoogleContactsConnectionImpl } /** - * Refresh OAuth2 authentication token. - * - * @throws IOException - */ - private void refreshToken() throws IOException, FailedTokenRefreshException - { - final Credential credential = this.credential.get(); - if (!credential.refreshToken()) - { - logger.warn("Refresh of OAuth2 authentication token failed."); - throw new FailedTokenRefreshException(); - } - } - - /** - * Create credential instance suitable for use in Google Contacts API. - * @param tokenServer the token server URL - * @return Returns a Credential instance. - */ - private void createCredential(final GenericUrl tokenServerURL) - { - final Credential.Builder builder = - new Credential.Builder( - BearerToken.authorizationHeaderAccessMethod()); - builder.setTokenServerUrl(tokenServerURL); - builder.setTransport(new NetHttpTransport()); - builder.setJsonFactory(new JacksonFactory()); - builder.setClientAuthentication(new HttpExecuteInterceptor() - { - - @Override - public void intercept(HttpRequest request) throws IOException - { - final RefreshTokenRequest content = - (RefreshTokenRequest) ((UrlEncodedContent) request - .getContent()).getData(); - content.put("client_id", GOOGLE_API_CLIENT_ID); - content.put("client_secret", GOOGLE_API_CLIENT_SECRET); - logger.warn("Refresh token request: " + content.toString()); - } - }); - builder.addRefreshListener(new CredentialRefreshListener() - { - final AtomicReference<Credential> store = - GoogleContactsConnectionImpl.this.credential; - - @Override - public void onTokenResponse(Credential credential, - TokenResponse tokenResponse) throws IOException - { - logger.debug("Successful token refresh response: " - + tokenResponse.toPrettyString()); - store.set(credential); - } - - @Override - public void onTokenErrorResponse(Credential credential, - TokenErrorResponse tokenErrorResponse) throws IOException - { - logger.debug("Failed token refresh response: " - + tokenErrorResponse.toPrettyString()); - logger.error("Failed to refresh OAuth2 token: " - + tokenErrorResponse.getError() + ": " - + tokenErrorResponse.getErrorDescription()); - } - }); - final Credential credential = builder.build(); - credential.setAccessToken("ya29.iwG37LYEgB4FCwfPLq8vV6Q-CX1vQ5sJrb_2AGydhLAiUT4wmz4iW4FlVkZE57s1B6NgA3BJAspLIw"); - credential.setRefreshToken(TEMP_REFRESH_TOKEN); - credential.setExpiresInSeconds(3600L); - this.credential.set(credential); - } - - /** * Returns if the connection is enabled. * * @return true if connection is enabled, false otherwise @@ -334,14 +236,4 @@ public class GoogleContactsConnectionImpl super("Failed to query Google Contacts API.", cause); } } - - public static class FailedTokenRefreshException - extends Exception - { - - private FailedTokenRefreshException() - { - super("Failed to refresh OAuth2 token."); - } - } } diff --git a/src/net/java/sip/communicator/impl/googlecontacts/OAuth2TokenStore.java b/src/net/java/sip/communicator/impl/googlecontacts/OAuth2TokenStore.java new file mode 100644 index 0000000..18a909d --- /dev/null +++ b/src/net/java/sip/communicator/impl/googlecontacts/OAuth2TokenStore.java @@ -0,0 +1,340 @@ +/* + * Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed 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 net.java.sip.communicator.impl.googlecontacts; + +import java.awt.*; +import java.awt.event.*; +import java.io.*; +import java.net.*; +import java.util.concurrent.atomic.*; + +import javax.swing.*; + +import net.java.sip.communicator.plugin.desktoputil.*; +import net.java.sip.communicator.util.*; + +import com.google.api.client.auth.oauth2.*; +import com.google.api.client.http.*; +import com.google.api.client.http.javanet.*; +import com.google.api.client.json.jackson2.*; + +/** + * OAuth 2 token store. + * + * @author Danny van Heumen + */ +public class OAuth2TokenStore +{ + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(OAuth2TokenStore.class); + + /** + * Google OAuth 2 token server. + */ + private static final GenericUrl GOOGLE_OAUTH2_TOKEN_SERVER = + new GenericUrl("https://accounts.google.com/o/oauth2/token"); + + /** + * Client ID for OAuth 2 based authentication. + */ + private static final String GOOGLE_API_CLIENT_ID = null; + + /** + * Client secret for OAuth 2 based authentication. + */ + private static final String GOOGLE_API_CLIENT_SECRET = null; + + /** + * Required OAuth 2 authentication scopes. + */ + private static final String GOOGLE_API_OAUTH2_SCOPES = + "profile%20email%20https://www.google.com/m8/feeds"; + + /** + * OAuth 2 redirect URL. + */ + private static final String GOOGLE_API_OAUTH2_REDIRECT_URI = + "urn:ietf:wg:oauth:2.0:oob"; + + /** + * Approval URL. + */ + private static final String APPROVAL_URL = String.format( + "https://accounts.google.com/o/oauth2/auth?scope=%s&redirect_uri=%s&response_type=code&client_id=%s", + GOOGLE_API_OAUTH2_SCOPES, + GOOGLE_API_OAUTH2_REDIRECT_URI, + GOOGLE_API_CLIENT_ID); + + /** + * The credential store. + * + * Note: The AtomicReference container is used as a shared container that is + * also passed on to some of the registered listeners for updating the + * credential data. + */ + private final AtomicReference<Credential> store = + new AtomicReference<Credential>(null); + + /** + * Get the credential from the store. In case a credential does not (yet) + * exist, acquire one preferrably from the password store. Optionally, + * involve the user if a credential is not yet stored. + * + * @return Returns the credential. + * @throws FailedAcquireCredentialException + * @throws MalformedURLException In case requesting authn token failed. + */ + public Credential get() throws FailedAcquireCredentialException + { + if (this.store.get() == null) + { + try + { + acquireCredential(this.store); + } + catch (Exception e) + { + throw new FailedAcquireCredentialException(e); + } + } + // should make sure that only succeeded requests reach up to here + return this.store.get(); + } + + /** + * Acquire a new credential instance. + * + * @param store credential store to update upon refreshing and other + * operations + * @return Acquires and returns the credential instance. + * @throws MalformedURLException In case of bad redirect URL. + * @throws URISyntaxException In case of bad redirect URI. + */ + private static void acquireCredential( + final AtomicReference<Credential> store) + throws MalformedURLException, + URISyntaxException + { + LOGGER.info("No credentials available yet. Requesting user to " + + "approve access to Contacts API using URL: " + APPROVAL_URL); + final OAuthApprovalDialog dialog = new OAuthApprovalDialog(); + dialog.setVisible(true); + final String approvalCode = dialog.getApprovalCode(); + LOGGER.debug("Approval code from user: " + approvalCode); + final TokenData data = requestAuthenticationToken(approvalCode); + store.set(createCredential(store, data)); + } + + /** + * Refresh OAuth2 authentication token. + * + * @throws IOException + * @throws FailedTokenRefreshException In case of failed token refresh + * operation. + */ + public void refresh() throws IOException, FailedTokenRefreshException + { + final Credential credential = this.store.get(); + if (credential == null) + { + throw new IllegalStateException("A credential instance should " + + "exist, but it does not. This is likely due to a bug."); + } + if (!credential.refreshToken()) + { + LOGGER.warn("Refresh of OAuth2 authentication token failed."); + throw new FailedTokenRefreshException(); + } + } + + /** + * Create credential instance suitable for use in Google Contacts API. + * + * @param store reference to the credential store for updating credential + * data upon refreshing and other cases + * @param approvalCode the approval code received from Google by the user + * accepting the authorization request + * @return Returns a Credential instance. + * @throws URISyntaxException In case of bad OAuth 2 redirect URI. + */ + private static Credential createCredential( + final AtomicReference<Credential> store, final TokenData data) throws URISyntaxException + { + final Credential.Builder builder = + new Credential.Builder( + BearerToken.authorizationHeaderAccessMethod()); + builder.setTokenServerUrl(GOOGLE_OAUTH2_TOKEN_SERVER); + builder.setTransport(new NetHttpTransport()); + builder.setJsonFactory(new JacksonFactory()); + builder.setClientAuthentication(new HttpExecuteInterceptor() + { + + @Override + public void intercept(HttpRequest request) throws IOException + { + final Object data = + ((UrlEncodedContent) request.getContent()).getData(); + if (data instanceof RefreshTokenRequest) + { + // Insert client authentication credentials in requests. + final RefreshTokenRequest content = + (RefreshTokenRequest) data; + content.put("client_id", GOOGLE_API_CLIENT_ID); + content.put("client_secret", GOOGLE_API_CLIENT_SECRET); + LOGGER.info("Inserting client authentication data into " + + " refresh token request."); + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug("Request: " + content.toString()); + } + } + else + { + LOGGER.info("Unexpected type of request found."); + } + } + }); + builder.addRefreshListener(new CredentialRefreshListener() + { + + @Override + public void onTokenResponse(Credential credential, + TokenResponse tokenResponse) throws IOException + { + LOGGER.debug("Successful token refresh response: " + + tokenResponse.toPrettyString()); + store.set(credential); + } + + @Override + public void onTokenErrorResponse(Credential credential, + TokenErrorResponse tokenErrorResponse) throws IOException + { + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug("Failed token refresh response: " + + tokenErrorResponse.toPrettyString()); + } + LOGGER.error("Failed to refresh OAuth2 token: " + + tokenErrorResponse.getError() + ": " + + tokenErrorResponse.getErrorDescription()); + } + }); + final Credential credential = builder.build(); + credential.setAccessToken(data.accessToken); + credential.setRefreshToken(data.refreshToken); + credential.setExpiresInSeconds(data.expiration); + return credential; + } + + private static TokenData requestAuthenticationToken(final String approvalCode) + { + // FIXME actually acquire credential + return new TokenData("", "", 3600L); + } + + private static class OAuthApprovalDialog extends SIPCommDialog { + private static final long serialVersionUID = 6792589736608633346L; + + private final SIPCommLinkButton label; + + private final SIPCommTextField code = new SIPCommTextField(""); + + public OAuthApprovalDialog() throws MalformedURLException + { + this.setModal(true); + this.label = new SIPCommLinkButton("Click here to approve."); + this.label.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + LOGGER.info("Request user for approval via web page: " + APPROVAL_URL); + // FIXME open browser + } + }); + this.setLayout(new BorderLayout()); + this.add(this.label, BorderLayout.NORTH); + this.add(new JLabel("Code"), BorderLayout.WEST); + this.add(this.code, BorderLayout.CENTER); + final JButton button = new JButton("Done"); + button.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) + { + OAuthApprovalDialog.this.dispose(); + } + }); + this.add(button, BorderLayout.SOUTH); + this.pack(); + } + + public String getApprovalCode() { + return this.code.getText(); + } + } + + private static class TokenData + { + private final String accessToken; + + private final String refreshToken; + + private final long expiration; + + private TokenData(final String accessToken, final String refreshToken, final long expirationTime) + { + if (accessToken == null) + { + throw new NullPointerException("access token cannot be null"); + } + this.accessToken = accessToken; + if (refreshToken == null) + { + throw new NullPointerException("refresh token cannot be null"); + } + this.refreshToken = refreshToken; + this.expiration = expirationTime; + } + } + + public static class FailedAcquireCredentialException + extends Exception + { + private static final long serialVersionUID = 5810534617383420431L; + + private FailedAcquireCredentialException(final Throwable cause) + { + super(cause); + } + } + + public static class FailedTokenRefreshException + extends Exception + { + private static final long serialVersionUID = 3166027054735734199L; + + private FailedTokenRefreshException() + { + super("Failed to refresh OAuth2 token."); + } + } +} |