aboutsummaryrefslogtreecommitdiffstats
path: root/src/net/java/sip/communicator/impl/googlecontacts/OAuth2TokenStore.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/net/java/sip/communicator/impl/googlecontacts/OAuth2TokenStore.java')
-rw-r--r--src/net/java/sip/communicator/impl/googlecontacts/OAuth2TokenStore.java340
1 files changed, 340 insertions, 0 deletions
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.");
+ }
+ }
+}