Skip to content

Commit

Permalink
Merge pull request #14877 from iterate-ch/feature/GH-14876
Browse files Browse the repository at this point in the history
Allow OAuth for ownCloud connections
  • Loading branch information
dkocher authored Aug 11, 2023
2 parents ca61ad4 + cbb5a22 commit 811693d
Show file tree
Hide file tree
Showing 19 changed files with 190 additions and 81 deletions.
6 changes: 4 additions & 2 deletions box/src/main/java/ch/cyberduck/core/box/BoxSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import ch.cyberduck.core.box.io.swagger.client.ApiException;
import ch.cyberduck.core.box.io.swagger.client.api.UsersApi;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.ConnectionCanceledException;
import ch.cyberduck.core.exception.LoginCanceledException;
import ch.cyberduck.core.features.AttributesFinder;
import ch.cyberduck.core.features.Copy;
import ch.cyberduck.core.features.Delete;
Expand Down Expand Up @@ -64,9 +66,9 @@ public BoxSession(final Host host, final X509TrustManager trust, final X509KeyMa
}

@Override
public CloseableHttpClient connect(final Proxy proxy, final HostKeyCallback key, final LoginCallback prompt, final CancelCallback cancel) {
public CloseableHttpClient connect(final Proxy proxy, final HostKeyCallback key, final LoginCallback prompt, final CancelCallback cancel) throws ConnectionCanceledException {
final HttpClientBuilder configuration = builder.build(proxy, this, prompt);
authorizationService = new OAuth2RequestInterceptor(configuration.build(), host)
authorizationService = new OAuth2RequestInterceptor(configuration.build(), host, prompt)
.withRedirectUri(host.getProtocol().getOAuthRedirectUrl());
configuration.addInterceptorLast(authorizationService);
configuration.setServiceUnavailableRetryStrategy(new OAuth2ErrorResponseInterceptor(host, authorizationService, prompt));
Expand Down
3 changes: 2 additions & 1 deletion cli/src/main/java/ch/cyberduck/cli/TerminalLoginService.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.LoginOptions;
import ch.cyberduck.core.PasswordStoreFactory;
import ch.cyberduck.core.exception.ConnectionCanceledException;
import ch.cyberduck.core.exception.LoginCanceledException;
import ch.cyberduck.core.exception.LoginFailureException;
import ch.cyberduck.core.preferences.PreferencesFactory;
Expand All @@ -43,7 +44,7 @@ public TerminalLoginService(final CommandLine input) {
}

@Override
public void validate(final Host bookmark, final LoginCallback prompt, final LoginOptions options) throws LoginCanceledException, LoginFailureException {
public void validate(final Host bookmark, final LoginCallback prompt, final LoginOptions options) throws ConnectionCanceledException, LoginFailureException {
final Credentials credentials = bookmark.getCredentials();
if(input.hasOption(TerminalOptionsBuilder.Params.anonymous.name())) {
credentials.setUsername(PreferencesFactory.get().getProperty("connection.login.anon.name"));
Expand Down
13 changes: 7 additions & 6 deletions core/src/main/java/ch/cyberduck/core/KeychainLoginService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.ConnectionCanceledException;
import ch.cyberduck.core.exception.LocalAccessDeniedException;
import ch.cyberduck.core.exception.LoginCanceledException;
import ch.cyberduck.core.exception.LoginFailureException;
Expand All @@ -44,7 +45,7 @@ public KeychainLoginService(final HostPasswordStore keychain) {
}

@Override
public void validate(final Host bookmark, final LoginCallback prompt, final LoginOptions options) throws LoginCanceledException, LoginFailureException {
public void validate(final Host bookmark, final LoginCallback prompt, final LoginOptions options) throws ConnectionCanceledException, LoginFailureException {
if(log.isDebugEnabled()) {
log.debug(String.format("Validate login credentials for %s", bookmark));
}
Expand Down Expand Up @@ -137,7 +138,7 @@ public void validate(final Host bookmark, final LoginCallback prompt, final Logi
* @return True if credentials have been updated
* @throws LoginCanceledException Prompt canceled by user
*/
public boolean prompt(final Host bookmark, final String message, final LoginCallback prompt, final LoginOptions options) throws LoginCanceledException {
public boolean prompt(final Host bookmark, final String message, final LoginCallback prompt, final LoginOptions options) throws ConnectionCanceledException {
final Credentials credentials = bookmark.getCredentials();
if(options.password) {
final Credentials input = prompt.prompt(bookmark, credentials.getUsername(),
Expand All @@ -158,6 +159,9 @@ public boolean prompt(final Host bookmark, final String message, final LoginCall
credentials.setToken(input.getPassword());
}
if(options.oauth) {
prompt.warn(bookmark, LocaleFactory.localizedString("Login failed", "Credentials"), message,
LocaleFactory.localizedString("Continue", "Credentials"),
LocaleFactory.localizedString("Cancel", "Localizable"), null);
log.warn(String.format("Reset OAuth tokens for %s", bookmark));
credentials.setOauth(OAuthTokens.EMPTY);
}
Expand Down Expand Up @@ -204,10 +208,7 @@ else if(credentials.isCertificateAuthentication()) {
listener.message(LocaleFactory.localizedString("Login failed", "Credentials"));
credentials.setPassed(false);
final LoginOptions options = new LoginOptions(bookmark.getProtocol());
final StringAppender details = new StringAppender();
details.append(LocaleFactory.localizedString("Login failed", "Credentials"));
details.append(e.getDetail());
if(this.prompt(bookmark, details.toString(), prompt, options)) {
if(this.prompt(bookmark, e.getDetail(), prompt, options)) {
// Retry
return false;
}
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/java/ch/cyberduck/core/LoginService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.ConnectionCanceledException;
import ch.cyberduck.core.exception.LoginCanceledException;
import ch.cyberduck.core.exception.LoginFailureException;
import ch.cyberduck.core.proxy.Proxy;
Expand All @@ -31,7 +32,7 @@ public interface LoginService {
* @param pompt Login prompt
* @param options Login mechanism features
*/
void validate(Host bookmark, LoginCallback pompt, LoginOptions options) throws LoginCanceledException, LoginFailureException;
void validate(Host bookmark, LoginCallback pompt, LoginOptions options) throws ConnectionCanceledException, LoginFailureException;

/**
* Login and prompt on failure
Expand Down
7 changes: 5 additions & 2 deletions core/src/main/java/ch/cyberduck/core/Profile.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public class Profile implements Protocol {
*/
private final Protocol parent;

public static final String OAUTH_CLIENT_ID_KEY = "OAuth Client ID";
public static final String OAUTH_CLIENT_SECRET_KEY = "OAuth Client Secret";

private Local disk;
private Local icon;

Expand Down Expand Up @@ -512,7 +515,7 @@ public String getOAuthRedirectUrl() {

@Override
public String getOAuthClientId() {
final String v = this.value("OAuth Client ID");
final String v = this.value(OAUTH_CLIENT_ID_KEY);
if(StringUtils.isBlank(v)) {
return parent.getOAuthClientId();
}
Expand All @@ -521,7 +524,7 @@ public String getOAuthClientId() {

@Override
public String getOAuthClientSecret() {
final String v = this.value("OAuth Client Secret");
final String v = this.value(OAUTH_CLIENT_SECRET_KEY);
if(StringUtils.isBlank(v)) {
return parent.getOAuthClientSecret();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public void process(final HttpRequest request, final HttpContext context) {
}
}
}
}).build(), host) {
}).build(), host, prompt) {
@Override
public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
if(request instanceof HttpRequestWrapper) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import ch.cyberduck.core.UrlProvider;
import ch.cyberduck.core.UseragentProvider;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.ConnectionCanceledException;
import ch.cyberduck.core.exception.LoginCanceledException;
import ch.cyberduck.core.features.*;
import ch.cyberduck.core.http.DefaultHttpRateLimiter;
import ch.cyberduck.core.http.HttpSession;
Expand Down Expand Up @@ -72,9 +74,9 @@ public DropboxSession(final Host host, final X509TrustManager trust, final X509K
}

@Override
protected CustomDbxRawClientV2 connect(final Proxy proxy, final HostKeyCallback callback, final LoginCallback prompt, final CancelCallback cancel) {
protected CustomDbxRawClientV2 connect(final Proxy proxy, final HostKeyCallback callback, final LoginCallback prompt, final CancelCallback cancel) throws ConnectionCanceledException {
final HttpClientBuilder configuration = builder.build(proxy, this, prompt);
authorizationService = new OAuth2RequestInterceptor(configuration.build(), host)
authorizationService = new OAuth2RequestInterceptor(configuration.build(), host, prompt)
.withRedirectUri(host.getProtocol().getOAuthRedirectUrl());
configuration.addInterceptorLast(authorizationService);
configuration.setServiceUnavailableRetryStrategy(new OAuth2ErrorResponseInterceptor(host, authorizationService, prompt));
Expand Down
2 changes: 1 addition & 1 deletion eue/src/main/java/ch/cyberduck/core/eue/EueSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public void process(final HttpRequest request, final HttpContext context) {
request.addHeader(HttpHeaders.AUTHORIZATION,
String.format("Basic %s", Base64.encodeToString(String.format("%s:%s", host.getProtocol().getOAuthClientId(), host.getProtocol().getOAuthClientSecret()).getBytes(StandardCharsets.UTF_8), false)));
}
}).build(), host)
}).build(), host, prompt)
.withRedirectUri(host.getProtocol().getOAuthRedirectUrl()
);
configuration.setServiceUnavailableRetryStrategy(new OAuth2ErrorResponseInterceptor(host, authorizationService, prompt));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
import ch.cyberduck.core.UrlProvider;
import ch.cyberduck.core.UseragentProvider;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.ConnectionCanceledException;
import ch.cyberduck.core.exception.HostParserException;
import ch.cyberduck.core.exception.LoginCanceledException;
import ch.cyberduck.core.features.*;
import ch.cyberduck.core.http.DefaultHttpRateLimiter;
import ch.cyberduck.core.http.HttpSession;
Expand Down Expand Up @@ -68,9 +70,9 @@ public DriveSession(final Host host, final X509TrustManager trust, final X509Key
}

@Override
protected Drive connect(final Proxy proxy, final HostKeyCallback callback, final LoginCallback prompt, final CancelCallback cancel) throws HostParserException {
protected Drive connect(final Proxy proxy, final HostKeyCallback callback, final LoginCallback prompt, final CancelCallback cancel) throws HostParserException, ConnectionCanceledException {
final HttpClientBuilder configuration = builder.build(proxy, this, prompt);
authorizationService = new OAuth2RequestInterceptor(builder.build(ProxyFactory.get().find(host.getProtocol().getOAuthAuthorizationUrl()), this, prompt).build(), host)
authorizationService = new OAuth2RequestInterceptor(builder.build(ProxyFactory.get().find(host.getProtocol().getOAuthAuthorizationUrl()), this, prompt).build(), host, prompt)
.withRedirectUri(host.getProtocol().getOAuthRedirectUrl());
configuration.addInterceptorLast(authorizationService);
configuration.setServiceUnavailableRetryStrategy(new OAuth2ErrorResponseInterceptor(host, authorizationService, prompt));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import ch.cyberduck.core.UseragentProvider;
import ch.cyberduck.core.cdn.DistributionConfiguration;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.ConnectionCanceledException;
import ch.cyberduck.core.exception.LoginCanceledException;
import ch.cyberduck.core.features.*;
import ch.cyberduck.core.http.HttpSession;
import ch.cyberduck.core.http.UserAgentHttpRequestInitializer;
Expand Down Expand Up @@ -63,9 +65,9 @@ public GoogleStorageSession(final Host host, final X509TrustManager trust, final
}

@Override
protected Storage connect(final Proxy proxy, final HostKeyCallback key, final LoginCallback prompt, final CancelCallback cancel) {
protected Storage connect(final Proxy proxy, final HostKeyCallback key, final LoginCallback prompt, final CancelCallback cancel) throws ConnectionCanceledException {
final HttpClientBuilder configuration = builder.build(proxy, this, prompt);
authorizationService = new OAuth2RequestInterceptor(builder.build(ProxyFactory.get().find(host.getProtocol().getOAuthAuthorizationUrl()), this, prompt).build(), host)
authorizationService = new OAuth2RequestInterceptor(builder.build(ProxyFactory.get().find(host.getProtocol().getOAuthAuthorizationUrl()), this, prompt).build(), host, prompt)
.withRedirectUri(host.getProtocol().getOAuthRedirectUrl());
configuration.addInterceptorLast(authorizationService);
configuration.setServiceUnavailableRetryStrategy(new OAuth2ErrorResponseInterceptor(host, authorizationService, prompt));
Expand Down
6 changes: 4 additions & 2 deletions hubic/src/main/java/ch/cyberduck/core/hubic/HubicSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import ch.cyberduck.core.OAuthTokens;
import ch.cyberduck.core.cdn.DistributionConfiguration;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.ConnectionCanceledException;
import ch.cyberduck.core.exception.LoginCanceledException;
import ch.cyberduck.core.oauth.OAuth2ErrorResponseInterceptor;
import ch.cyberduck.core.oauth.OAuth2RequestInterceptor;
import ch.cyberduck.core.openstack.SwiftExceptionMappingService;
Expand Down Expand Up @@ -50,9 +52,9 @@ public HubicSession(final Host host, final X509TrustManager trust, final X509Key
}

@Override
protected Client connect(final Proxy proxy, final HostKeyCallback key, final LoginCallback prompt, final CancelCallback cancel) {
protected Client connect(final Proxy proxy, final HostKeyCallback key, final LoginCallback prompt, final CancelCallback cancel) throws ConnectionCanceledException {
final HttpClientBuilder configuration = builder.build(proxy, this, prompt);
authorizationService = new OAuth2RequestInterceptor(configuration.build(), host)
authorizationService = new OAuth2RequestInterceptor(configuration.build(), host, prompt)
.withRedirectUri(host.getProtocol().getOAuthRedirectUrl());
configuration.addInterceptorLast(authorizationService);
configuration.setServiceUnavailableRetryStrategy(new OAuth2ErrorResponseInterceptor(host, authorizationService, prompt));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@
import ch.cyberduck.core.LoginOptions;
import ch.cyberduck.core.OAuthTokens;
import ch.cyberduck.core.PreferencesUseragentProvider;
import ch.cyberduck.core.Profile;
import ch.cyberduck.core.StringAppender;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.InteroperabilityException;
import ch.cyberduck.core.exception.LoginCanceledException;
import ch.cyberduck.core.exception.LoginFailureException;
import ch.cyberduck.core.http.DefaultHttpResponseExceptionMappingService;
import ch.cyberduck.core.http.UserAgentHttpRequestInitializer;
import ch.cyberduck.core.preferences.HostPreferences;
import ch.cyberduck.core.preferences.PreferencesFactory;
import ch.cyberduck.core.threading.CancelCallback;

Expand Down Expand Up @@ -90,21 +92,23 @@ public class OAuth2AuthorizationService {

private final HttpTransport transport;

public OAuth2AuthorizationService(final HttpClient client,
public OAuth2AuthorizationService(final HttpClient client, final Host host,
final String tokenServerUrl, final String authorizationServerUrl,
final String clientid, final String clientsecret, final List<String> scopes, final boolean pkce) {
this(new ApacheHttpTransport(client),
tokenServerUrl, authorizationServerUrl, clientid, clientsecret, scopes, pkce);
final String clientid, final String clientsecret, final List<String> scopes, final boolean pkce, final LoginCallback prompt) throws LoginCanceledException {
this(new ApacheHttpTransport(client), host,
tokenServerUrl, authorizationServerUrl, clientid, clientsecret, scopes, pkce, prompt);
}

public OAuth2AuthorizationService(final HttpTransport transport,
public OAuth2AuthorizationService(final HttpTransport transport, final Host host,
final String tokenServerUrl, final String authorizationServerUrl,
final String clientid, final String clientsecret, final List<String> scopes, final boolean pkce) {
final String clientid, final String clientsecret, final List<String> scopes, final boolean pkce, final LoginCallback prompt) throws LoginCanceledException {
this.transport = transport;
this.tokenServerUrl = tokenServerUrl;
this.authorizationServerUrl = authorizationServerUrl;
this.clientid = clientid;
this.clientsecret = clientsecret;
this.clientid = prompt(host, prompt, Profile.OAUTH_CLIENT_ID_KEY, LocaleFactory.localizedString(
Profile.OAUTH_CLIENT_ID_KEY, "Credentials"), clientid);
this.clientsecret = prompt(host, prompt, Profile.OAUTH_CLIENT_SECRET_KEY, LocaleFactory.localizedString(
Profile.OAUTH_CLIENT_SECRET_KEY, "Credentials"), clientsecret);
this.scopes = scopes;
this.pkce = pkce;
}
Expand Down Expand Up @@ -165,11 +169,32 @@ private TokenResponse authorizeWithCode(final Host bookmark, final LoginCallback
);
}
// Start OAuth2 flow within browser
final ClientParametersAuthentication clientParameters;
if(StringUtils.isBlank(clientid)) {
if(null != new HostPreferences(bookmark).getProperty("oauth.clientid")) {
clientParameters = new ClientParametersAuthentication(new HostPreferences(bookmark).getProperty("oauth.clientid"), StringUtils.EMPTY);
}
else {
final Credentials clientid = prompt.prompt(bookmark,
LocaleFactory.localizedString(Profile.OAUTH_CLIENT_ID_KEY, "Credentials"),
LocaleFactory.localizedString("Provide additional login credentials", "Credentials"),
new LoginOptions(bookmark.getProtocol())
.user(true).password(true)
.passwordPlaceholder(LocaleFactory.localizedString(Profile.OAUTH_CLIENT_ID_KEY, "Credentials")));
if(clientid.isSaved()) {
bookmark.setProperty("oauth.clientid", clientid.getPassword());
}
clientParameters = new ClientParametersAuthentication(clientid.getUsername(), StringUtils.EMPTY);
}
}
else {
clientParameters = new ClientParametersAuthentication(clientid, clientsecret);
}
final AuthorizationCodeFlow.Builder flowBuilder = new AuthorizationCodeFlow.Builder(
method,
transport, json,
new GenericUrl(tokenServerUrl),
new ClientParametersAuthentication(clientid, clientsecret),
clientParameters,
clientid,
authorizationServerUrl)
.setScopes(scopes)
Expand Down Expand Up @@ -355,4 +380,20 @@ public TokenResponse toTokenResponse() {
}
}

/**
* Prompt for value if missing
*/
private static String prompt(final Host bookmark, final LoginCallback prompt,
final String property, final String message, final String value) throws LoginCanceledException {
if(null == value) {
final Credentials input = prompt.prompt(bookmark, message,
LocaleFactory.localizedString("Provide additional login credentials", "Credentials"),
new LoginOptions().icon(bookmark.getProtocol().disk()));
if(input.isSaved()) {
bookmark.setProperty(property, input.getPassword());
}
return input.getPassword();
}
return value;
}
}
Loading

0 comments on commit 811693d

Please sign in to comment.