| /* |
| * Copyright (c) 2016 Manumitting Technologies Inc and others. |
| * 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: |
| * Manumitting Technologies Inc - initial API and implementation |
| */ |
| package org.eclipse.userstorage.oauth; |
| |
| import org.eclipse.userstorage.IStorageService; |
| import org.eclipse.userstorage.internal.Activator; |
| import org.eclipse.userstorage.internal.Session; |
| import org.eclipse.userstorage.internal.oauth.AuthToken; |
| import org.eclipse.userstorage.internal.oauth.OAuthCredentialsPersistence; |
| import org.eclipse.userstorage.internal.oauth.UIFacade; |
| import org.eclipse.userstorage.internal.oauth.ui.SWTInternalBrowserFacade; |
| import org.eclipse.userstorage.internal.util.AES; |
| import org.eclipse.userstorage.internal.util.IOUtil; |
| import org.eclipse.userstorage.internal.util.JSONUtil; |
| import org.eclipse.userstorage.internal.util.ProxyUtil; |
| import org.eclipse.userstorage.internal.util.StringUtil; |
| import org.eclipse.userstorage.spi.Credentials; |
| |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.jface.window.IShellProvider; |
| |
| import org.apache.http.HttpResponse; |
| import org.apache.http.HttpStatus; |
| import org.apache.http.NameValuePair; |
| import org.apache.http.client.ClientProtocolException; |
| import org.apache.http.client.fluent.Request; |
| import org.apache.http.client.fluent.Response; |
| import org.apache.http.client.utils.DateUtils; |
| import org.apache.http.client.utils.URIBuilder; |
| import org.apache.http.entity.ContentType; |
| import org.apache.http.message.BasicNameValuePair; |
| import org.apache.http.util.EntityUtils; |
| |
| import java.io.IOException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.security.GeneralSecurityException; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * A crendentials provider that authenticates and authorizes using the |
| * Eclipse OAuth provider. |
| * |
| * <p>Normal flow:</p> |
| * <ul> |
| * <li> REQUEST AUTHORIZATION: GET <tt>https://accounts.eclipse.org/oauth2/authorize?response_type=code&client_id=_XXX_&client_secret=_XXX_&scope=_XXX_%20_XXX_%20_XXX_&redirect_uri=_XXX_&state=_XXX_</tt> </li> |
| * <li> This may redirect a bit: <tt>302 Found</tt> <tt>https://accounts.eclipse.org/user/login?destination=oauth2/authorize</tt> </li> |
| * <li> LOGIN SUCCESS <tt><em>http://localhost/</em>?code=a74a9f4ec60fa71121ddeb0219d8d9bcee9bf139&state=_XXX_</tt> </li> |
| * <li> REQUEST AUTHORIZATION: POST <tt>https://accounts.eclipse.org/oauth2/token</tt> with body <tt>grant_type=authorization_code&client_id=_XXX_&client_secret=_XXX_&redirect_uri=_XXX_&code=_XXX_</tt></li> |
| * <li> Should respond with <tt>200 OK</tt> and body is a JSON auth token. Or may respond with a 4XX if the code is invalid.</li> |
| * </ul> |
| * <p>Error flow:</p> |
| * <ul> |
| * <li> <tt>https://accounts.eclipse.org/oauth2/authorize?response_type=code&client_id=_XXX_&client_secret=_XXX_&scope=_XXX_%20_XXX_%20_XXX_&redirect_uri=_XXX_&state=_XXX_</tt> |
| * <li> <tt><em>http://localhost/</em>?error=access_denied&error_description=The+user+denied+access+to+your+application&state=_XXX_</tt></li> |
| * </ul> |
| * <p>Refresh flow:</p> |
| * <ul> |
| * <li> REQUEST AUTHORIZATION: POST <tt>https://accounts.eclipse.org/oauth2/token</tt> with body <tt>grant_type=refresh_token&client_id=_XXX_&client_secret=_XXX_&redirect_uri=_XXX_&refresh_token=_XXX_</tt></li> |
| * <li> Should respond with 200 OK and body is a JSON auth token. Or may respond with a 4XX if the refresh token has expired or has been repudiated.</li> |
| * </ul> |
| */ |
| public final class EclipseOAuthCredentialsProvider extends OAuthCredentialsProvider |
| { |
| private static final String AUTHORIZATION = "Authorization"; |
| |
| private static final boolean DEBUG = Boolean.getBoolean("org.eclipse.userstorage.session.debug"); |
| |
| private static final String PLUGIN_ID = "org.eclipse.userstorage.oauth"; |
| |
| private static void debug(String message) |
| { |
| if (DEBUG) |
| { |
| System.out.println("<EclipseOAuthCredentialsProvider> " + message); |
| } |
| } |
| |
| private static void log(int statusCode, String message, Throwable exception) |
| { |
| Activator.log(new Status(statusCode, PLUGIN_ID, message, exception)); |
| } |
| |
| private OAuthCredentialsPersistence persister; |
| |
| UIFacade uiFacade; // package protected for testing purposes |
| |
| /** |
| * @param clientId the OAuth identifier assigned to the application |
| * @param clientSecret the OAuth secret assigned to the application |
| * @param scopes the OAuth scopes requested by this application |
| * @param expectedCallback the expected callback URL to indicate success/failure |
| */ |
| public EclipseOAuthCredentialsProvider(String clientId, String clientSecret, String[] scopes, URI expectedCallback) throws URISyntaxException |
| { |
| this(null, clientId, clientSecret, scopes, expectedCallback); |
| } |
| |
| /** |
| * @param authService if null then use the service's serviceURI |
| * @param clientId the OAuth identifier assigned to the application |
| * @param clientSecret the OAuth secret assigned to the application |
| * @param scopes the OAuth scopes requested by this application |
| * @param expectedCallback the expected callback URL to indicate success/failure |
| */ |
| public EclipseOAuthCredentialsProvider(URI authService, String clientId, String clientSecret, String[] scopes, URI expectedCallback) |
| { |
| super(authService, clientId, clientSecret, scopes, expectedCallback); |
| persister = OAuthCredentialsPersistence.standard(); |
| uiFacade = new SWTInternalBrowserFacade(); |
| } |
| |
| public void setShell(IShellProvider provider) |
| { |
| if (uiFacade instanceof SWTInternalBrowserFacade) |
| { |
| ((SWTInternalBrowserFacade)uiFacade).setShell(provider); |
| } |
| } |
| |
| @Override |
| public Credentials provideCredentials(IStorageService service, boolean reauthentication) |
| { |
| try |
| { |
| if (!reauthentication) |
| { |
| // Try the refresh token |
| AuthToken authToken = refreshAuthToken(service); |
| if (authToken != null) |
| { |
| debug("Returning cached credentials"); |
| return asCredentials(service, authToken); |
| } |
| } |
| |
| /* Start the full process */ |
| debug("Starting OAuth authorization process"); |
| String providerName = service.getServiceLabel(); |
| URI authURI = getAuthorizationURI(service); |
| URI callbackURI = getRegisteredCallback(); |
| URI authCodeURI = uiFacade.obtainAuthCode(providerName, authURI, callbackURI); |
| |
| if (authCodeURI == null) |
| { |
| debug("User Cancelled login process"); |
| return null; |
| } |
| List<NameValuePair> params = new URIBuilder(authCodeURI).getQueryParams(); |
| if (findValue(params, "error") != null) |
| { |
| debug("Remote reported OAuth Error: " + authCodeURI.getRawQuery()); |
| uiFacade.showError("OAuth Error", "An error occurred retrieving during the authorization process.", |
| new Status(IStatus.ERROR, PLUGIN_ID, "Error returned: " + authCodeURI.getRawQuery())); |
| return null; |
| } |
| debug("Login success: retrieving access token..."); |
| AuthToken authToken = requestAuthToken(service, authCodeURI); |
| if (authToken == null) |
| { |
| debug("Failed to turn Access Code -> Auth Token"); |
| uiFacade.showError("OAuth Error", "Unable to retrieve Authorization Code", |
| new Status(IStatus.ERROR, PLUGIN_ID, "Unable to turn authorization code to an authoriation token")); |
| return null; |
| } |
| return asCredentials(service, authToken); |
| } |
| catch (IOException ex) |
| { |
| log(IStatus.WARNING, "Unable to fetch credential", ex); |
| return null; |
| } |
| catch (URISyntaxException ex) |
| { |
| log(IStatus.WARNING, "Invalid parameters", ex); |
| return null; |
| } |
| } |
| |
| private String findValue(List<NameValuePair> queryParameters, String key) |
| { |
| for (NameValuePair pair : queryParameters) |
| { |
| if (key.equals(pair.getName())) |
| { |
| return pair.getValue(); |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public Request configureRequest(Request request, URI uri, Credentials credentials) |
| { |
| AuthToken token = AuthToken.deserialize(credentials.getPassword()); |
| return request.addHeader(AUTHORIZATION, token.getTokenType() + " " + token.getAccessToken()); |
| } |
| |
| @Override |
| public boolean isValid(Credentials credentials) |
| { |
| if (!super.isValid(credentials)) |
| { |
| return false; |
| } |
| try |
| { |
| AuthToken token = AuthToken.deserialize(credentials.getPassword()); |
| return token != null && !token.isExpired(); |
| } |
| catch (IllegalArgumentException e) |
| { |
| return false; |
| } |
| } |
| |
| @Override |
| public Credentials getCredentials(IStorageService service) |
| { |
| AuthToken authToken = retrieveStoredAuthToken(service); |
| if (authToken == null || authToken.isExpired()) |
| { |
| return null; |
| } |
| return asCredentials(service, authToken); |
| } |
| |
| @Override |
| public boolean hasCredentials(IStorageService service) |
| { |
| AuthToken authToken = retrieveStoredAuthToken(service); |
| return authToken != null && !authToken.isExpired(); |
| } |
| |
| private AuthToken requestAuthToken(IStorageService service, URI uri) throws URISyntaxException, IOException |
| { |
| Pattern codePattern = Pattern.compile(".*[?&]code=([^&]+).*"); |
| Matcher m = codePattern.matcher(uri.toASCIIString()); |
| if (!m.matches()) |
| { |
| debug("OAuth Access Code URI doesn't have an access code!"); |
| return null; |
| } |
| String accessCode = m.group(1); |
| |
| //@formatter:off |
| URI authorizationURI = new URIBuilder(getAuthorizationServiceBaseURI(service)) |
| .setPath("/oauth2/token") |
| .build(); |
| Request request = Request.Post(authorizationURI) |
| .bodyForm( |
| new BasicNameValuePair("grant_type", "authorization_code"), |
| new BasicNameValuePair("client_id", getClientId()), |
| new BasicNameValuePair("client_secret", getClientSecret()), |
| new BasicNameValuePair("redirect_uri", getRegisteredCallback().toASCIIString()), |
| new BasicNameValuePair("code", accessCode)); |
| //@formatter:on |
| |
| Response result = ProxyUtil.proxyAuthentication(executor, authorizationURI).execute(request); |
| HttpResponse response = result.returnResponse(); |
| if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) |
| { |
| log(IStatus.ERROR, "Authorization/authentication failed: " + response, null); |
| return null; |
| } |
| String jsonRep = EntityUtils.toString(response.getEntity()); |
| Date date = DateUtils.parseDate(response.getFirstHeader("Date").getValue()); |
| |
| AuthToken authToken = new AuthToken(jsonRep, date); |
| String email = retrieveUserDetails(service, authToken); |
| |
| persistAuthToken(service, authToken, email); |
| return authToken; |
| } |
| |
| private String retrieveUserDetails(IStorageService service, AuthToken token) |
| { |
| URI refreshURI; |
| if (!contains(getScopes(), "profile")) |
| { |
| return null; |
| } |
| try |
| { |
| refreshURI = new URIBuilder(service.getServiceURI()).setPath("/account/profile").build(); |
| //@formatter:off |
| Request request = Request.Get(refreshURI) |
| .addHeader(Session.ACCEPT, ContentType.APPLICATION_JSON.getMimeType()) |
| .addHeader(Session.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType()) |
| .addHeader(Session.USER_AGENT, Session.USER_AGENT_ID) |
| .addHeader(AUTHORIZATION, token.getTokenType() + " " + token.getAccessToken()); |
| //@formatter:on |
| Response result = ProxyUtil.proxyAuthentication(executor, refreshURI).execute(request); |
| HttpResponse response = result.returnResponse(); |
| if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) |
| { |
| return null; |
| } |
| String jsonRep = EntityUtils.toString(response.getEntity()); |
| Object o = JSONUtil.parse(IOUtil.streamUTF(jsonRep), null); |
| if (!(o instanceof List<?> && ((List<?>)o).get(0) instanceof Map<?, ?>)) |
| { |
| throw new IllegalArgumentException("not a valid profile object"); |
| } |
| return (String)((Map<?, ?>)((List<?>)o).get(0)).get("mail"); |
| } |
| catch (URISyntaxException ex) |
| { |
| log(IStatus.WARNING, "Unable to construct remote URI", ex); |
| } |
| catch (ClientProtocolException ex) |
| { |
| log(IStatus.WARNING, "Unable to request user profile information", ex); |
| } |
| catch (IOException ex) |
| { |
| log(IStatus.WARNING, "Unable to parse user profile information", ex); |
| } |
| return null; |
| } |
| |
| private boolean contains(String[] scopes, String scope) |
| { |
| for (String s : scopes) |
| { |
| if (s.equals(scope)) |
| { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private AuthToken refreshAuthToken(IStorageService service) |
| { |
| // check to see if we have previously-saved credentials. |
| AuthToken authToken = retrieveStoredAuthToken(service); |
| if (authToken == null) |
| { |
| return null; |
| } |
| if (authToken.getRefreshToken() == null) |
| { |
| discardStoredAuthToken(service); |
| return null; |
| } |
| try |
| { |
| //@formatter:off |
| URI refreshURI = new URIBuilder(getAuthorizationServiceBaseURI(service)) |
| .setPath("/oauth2/token") |
| .build(); |
| Request request = Request.Post(refreshURI) |
| //.addHeader("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType()) |
| .bodyForm( |
| new BasicNameValuePair("grant_type", "refresh_token"), |
| new BasicNameValuePair("refresh_token", authToken.getRefreshToken()), |
| new BasicNameValuePair("client_id", getClientId()), |
| new BasicNameValuePair("client_secret", getClientSecret())); |
| //@formatter:on |
| |
| Response result = ProxyUtil.proxyAuthentication(executor, refreshURI).execute(request); |
| HttpResponse response = result.returnResponse(); |
| if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) |
| { |
| log(IStatus.WARNING, "Authorization/authentication failed when refreshing auth token: " + response, null); |
| discardStoredAuthToken(service); |
| return null; |
| } |
| String jsonRep = EntityUtils.toString(response.getEntity()); |
| Date date = DateUtils.parseDate(response.getFirstHeader("Date").getValue()); |
| |
| authToken = new AuthToken(jsonRep, date); |
| String email = retrieveUserDetails(service, authToken); |
| persistAuthToken(service, authToken, email); |
| return authToken; |
| } |
| catch (IOException ex) |
| { |
| log(IStatus.ERROR, "Unparseable response", ex); |
| return null; |
| } |
| catch (URISyntaxException ex) |
| { |
| log(IStatus.ERROR, "Invalid URI", ex); |
| return null; |
| } |
| } |
| |
| private AuthToken retrieveStoredAuthToken(IStorageService service) |
| { |
| try |
| { |
| String encryptedForm = persister.getAccountToken(getClientId(), getAuthorizationServiceBaseURI(service).toString()); |
| if (encryptedForm == null) |
| { |
| return null; |
| } |
| |
| String serializedForm = AES.decrypt(encryptedForm, getClientSecretAsChars()); |
| return AuthToken.deserialize(serializedForm); |
| } |
| catch (IllegalArgumentException e) |
| { |
| log(IStatus.WARNING, "Unable to deserialize stored token", e); |
| return null; |
| } |
| catch (GeneralSecurityException e) |
| { |
| log(IStatus.WARNING, "Unable to decrypt stored token", e); |
| return null; |
| } |
| } |
| |
| private void discardStoredAuthToken(IStorageService service) |
| { |
| persister.removeAccountToken(getClientId(), getAuthorizationServiceBaseURI(service).toString()); |
| } |
| |
| private void persistAuthToken(IStorageService service, AuthToken authToken, String email) |
| { |
| try |
| { |
| String serializedForm = authToken.serialize(); |
| String encryptedForm = AES.encrypt(serializedForm, getClientSecretAsChars()); |
| persister.putAccountToken(getClientId(), getAuthorizationServiceBaseURI(service).toString(), encryptedForm, email); |
| } |
| catch (GeneralSecurityException e) |
| { |
| log(IStatus.ERROR, "Unable to encrypt auth token for storage", e); |
| } |
| catch (IOException e) |
| { |
| log(IStatus.ERROR, "Unable to serialize auth token for storage", e); |
| } |
| } |
| |
| private Credentials asCredentials(IStorageService service, AuthToken authToken) |
| { |
| try |
| { |
| return new Credentials(service.getServiceURI() + "|" + getClientId(), authToken.serialize()); |
| } |
| catch (IOException e) |
| { |
| log(IStatus.WARNING, "Unable to serialize auth token", e); |
| return null; |
| } |
| } |
| |
| /** |
| * Generate the URI that starts the User Authorization flow |
| */ |
| protected URI getAuthorizationURI(IStorageService service) throws URISyntaxException |
| { |
| //@formatter:off |
| return new URIBuilder(getAuthorizationServiceBaseURI(service)) |
| .setPath("/oauth2/authorize") |
| .addParameter("response_type", "code") |
| .addParameter("client_id", getClientId()) |
| .addParameter("client_secret", getClientSecret()) |
| .addParameter("scope", StringUtil.join(" ", getScopes())) |
| .addParameter("redirect_uri", getRegisteredCallback().toASCIIString()) |
| .addParameter("state", stateCode) |
| .build(); |
| //@formatter:on |
| } |
| } |