diff --git a/core/src/main/csharp/ch/cyberduck/core/CredentialManagerPasswordStore.cs b/core/src/main/csharp/ch/cyberduck/core/CredentialManagerPasswordStore.cs index f6da6922965..ed08cface49 100644 --- a/core/src/main/csharp/ch/cyberduck/core/CredentialManagerPasswordStore.cs +++ b/core/src/main/csharp/ch/cyberduck/core/CredentialManagerPasswordStore.cs @@ -60,11 +60,12 @@ public override void delete(Host bookmark) { logger.info(string.Format("Delete password for bookmark {0}", bookmark)); } - var target = ToUri(bookmark); - if (!WinCredentialManager.RemoveCredentials(target.AbsoluteUri)) + var targets = ToUri(bookmark); + foreach (Uri descriptor in targets) { - base.delete(bookmark); + WinCredentialManager.RemoveCredentials(descriptor.AbsoluteUri); } + base.delete(bookmark); } public override void deletePassword(string serviceName, string user) @@ -86,7 +87,7 @@ public override void deletePassword(Scheme scheme, int port, string hostName, st public override string findLoginPassword(Host bookmark) { - var target = ToUri(bookmark); + var target = ToUri(bookmark)[0]; var cred = WinCredentialManager.GetCredentials(target.AbsoluteUri); if (!string.IsNullOrWhiteSpace(cred.Password)) { @@ -101,7 +102,7 @@ public override string findLoginToken(Host bookmark) { logger.info(string.Format("Fetching login token from keychain for {0}", bookmark)); } - var target = ToUri(bookmark); + var target = ToUri(bookmark)[0]; var cred = WinCredentialManager.GetCredentials(target.AbsoluteUri); if (cred.Attributes is Dictionary attrs && attrs.TryGetValue("Token", out var token) @@ -118,21 +119,28 @@ public override OAuthTokens findOAuthTokens(Host bookmark) { logger.info(string.Format("Fetching OAuth tokens from keychain for {0}", bookmark)); } - var target = ToUri(bookmark); - var cred = WinCredentialManager.GetCredentials(target.AbsoluteUri); - if (cred.Attributes is Dictionary attrs - && attrs.TryGetValue("OAuth Access Token", out var accessToken)) + var targets = ToUri(bookmark); + foreach(Uri descriptor in targets) { - attrs.TryGetValue("OAuth Refresh Token", out var refreshToken); - attrs.TryGetValue("OIDC Id Token", out var idToken); - long expiry = default; - if (attrs.TryGetValue("OAuth Expiry", out var expiryValue)) + var cred = WinCredentialManager.GetCredentials(descriptor.AbsoluteUri); + if (cred.Attributes is Dictionary attrs) { - long.TryParse(expiryValue, out expiry); + attrs.TryGetValue("OAuth Access Token", out var accessToken); + attrs.TryGetValue("OAuth Refresh Token", out var refreshToken); + attrs.TryGetValue("OIDC Id Token", out var idToken); + long expiry = default; + if (attrs.TryGetValue("OAuth Expiry", out var expiryValue)) + { + long.TryParse(expiryValue, out expiry); + } + OAuthTokens tokens = new(accessToken, refreshToken, new(expiry), idToken); + if(tokens.validate()) + { + return tokens; + } + // Continue with deprecated descriptors } - return new(accessToken, refreshToken, new(expiry), idToken); } - return base.findOAuthTokens(bookmark); } @@ -142,7 +150,7 @@ public override string findPrivateKeyPassphrase(Host bookmark) { logger.info(string.Format("Fetching private key passphrase from keychain for {0}", bookmark)); } - var target = ToUri(bookmark); + var target = ToUri(bookmark)[0]; var cred = WinCredentialManager.GetCredentials(target.AbsoluteUri); if (cred.Attributes is Dictionary attrs && attrs.TryGetValue("Private Key Passphrase", out var passphrase) @@ -172,7 +180,7 @@ public override void save(Host bookmark) { logger.info(string.Format("Add password for bookmark {0}", bookmark)); } - var target = ToUri(bookmark); + var target = ToUri(bookmark)[0]; var credential = bookmark.getCredentials(); var winCred = new WindowsCredentialManagerCredential( @@ -209,29 +217,42 @@ public override void save(Host bookmark) } } - private static Uri ToUri(Host bookmark) + private static List ToUri(Host bookmark) { - var protocol = bookmark.getProtocol(); - var credentials = bookmark.getCredentials(); - - var targetBuilder = new UriBuilder(PreferencesFactory.get().getProperty("application.container.name"), string.Empty); - var pathBuilder = new StringBuilder(); - pathBuilder.Append(protocol.getIdentifier()); - if (protocol.isHostnameConfigurable() || !(protocol.isTokenConfigurable() || protocol.isOAuthConfigurable())) + List descriptors = new(); + foreach(string descriptor in ToDescriptor(bookmark)) { - pathBuilder.Append(":" + bookmark.getHostname()); - if (protocol.isPortConfigurable() && !Equals(protocol.getDefaultPort(), bookmark.getPort())) + var protocol = bookmark.getProtocol(); + var credentials = bookmark.getCredentials(); + + var targetBuilder = new UriBuilder(PreferencesFactory.get().getProperty("application.container.name"), string.Empty); + var pathBuilder = new StringBuilder(); + pathBuilder.Append(descriptor); + if (protocol.isHostnameConfigurable() || !(protocol.isTokenConfigurable() || protocol.isOAuthConfigurable())) { - pathBuilder.Append(":" + bookmark.getPort()); + pathBuilder.Append(":" + bookmark.getHostname()); + if (protocol.isPortConfigurable() && !Equals(protocol.getDefaultPort(), bookmark.getPort())) + { + pathBuilder.Append(":" + bookmark.getPort()); + } } + targetBuilder.Path = pathBuilder.ToString(); + if (!string.IsNullOrWhiteSpace(credentials.getUsername())) + { + targetBuilder.Query = "user=" + credentials.getUsername(); + } + descriptors.Add(targetBuilder.Uri); } - targetBuilder.Path = pathBuilder.ToString(); - if (!string.IsNullOrWhiteSpace(credentials.getUsername())) + return descriptors; + } + + private static string[] ToDescriptor(Host bookmark) + { + if(bookmark.getProtocol().isOAuthConfigurable()) { - targetBuilder.Query = "user=" + credentials.getUsername(); + return new string[] { bookmark.getProtocol().getOAuthClientId(), bookmark.getProtocol().getIdentifier() }; } - - return targetBuilder.Uri; + return new string[] { bookmark.getProtocol().getIdentifier() }; } } } diff --git a/core/src/main/java/ch/cyberduck/core/DefaultHostPasswordStore.java b/core/src/main/java/ch/cyberduck/core/DefaultHostPasswordStore.java index d53258e390c..43ba07a8364 100644 --- a/core/src/main/java/ch/cyberduck/core/DefaultHostPasswordStore.java +++ b/core/src/main/java/ch/cyberduck/core/DefaultHostPasswordStore.java @@ -148,37 +148,73 @@ public OAuthTokens findOAuthTokens(final Host bookmark) { if(log.isInfoEnabled()) { log.info(String.format("Fetching OAuth tokens from keychain for %s", bookmark)); } - final String prefix = this.getOAuthPrefix(bookmark); - final String hostname = this.getOAuthHostname(bookmark); - try { - final String expiry = this.getPassword(this.getOAuthHostname(bookmark), String.format("%s OAuth2 Token Expiry", prefix)); - return (new OAuthTokens( - this.getPassword(bookmark.getProtocol().getScheme(), bookmark.getPort(), hostname, - String.format("%s OAuth2 Access Token", prefix)), - this.getPassword(bookmark.getProtocol().getScheme(), bookmark.getPort(), hostname, - String.format("%s OAuth2 Refresh Token", prefix)), - expiry != null ? Long.parseLong(expiry) : -1L, - this.getPassword(bookmark.getProtocol().getScheme(), bookmark.getPort(), hostname, - String.format("%s OIDC Id Token", prefix)))); + final String[] descriptors = getOAuthPrefix(bookmark); + for(String prefix : descriptors) { + if(log.isDebugEnabled()) { + log.debug(String.format("Search with prefix %s", prefix)); + } + final String hostname = getOAuthHostname(bookmark); + if(log.isDebugEnabled()) { + log.debug(String.format("Search with hostname %s", hostname)); + } + try { + final String expiry = this.getPassword(getOAuthHostname(bookmark), String.format("%s OAuth2 Token Expiry", prefix)); + final OAuthTokens tokens = new OAuthTokens( + this.getPassword(getOAuthScheme(bookmark), getOAuthPort(bookmark), hostname, + String.format("%s OAuth2 Access Token", prefix)), + this.getPassword(getOAuthScheme(bookmark), getOAuthPort(bookmark), hostname, + String.format("%s OAuth2 Refresh Token", prefix)), + expiry != null ? Long.parseLong(expiry) : -1L, + this.getPassword(getOAuthScheme(bookmark), getOAuthPort(bookmark), hostname, + String.format("%s OIDC Id Token", prefix))); + if(tokens.validate()) { + return tokens; + } + // Continue with deprecated descriptors + } + catch(LocalAccessDeniedException e) { + log.warn(String.format("Failure %s searching in keychain", e)); + return OAuthTokens.EMPTY; + } } - catch(LocalAccessDeniedException e) { - log.warn(String.format("Failure %s searching in keychain", e)); - return OAuthTokens.EMPTY; + return OAuthTokens.EMPTY; + } + + protected static Scheme getOAuthScheme(final Host bookmark) { + final URI uri = URI.create(bookmark.getProtocol().getOAuthTokenUrl()); + if(null == uri.getScheme()) { + return bookmark.getProtocol().getScheme(); } + return Scheme.valueOf(uri.getScheme()); } - protected String getOAuthHostname(final Host bookmark) { - if(StringUtils.isNotBlank(URI.create(bookmark.getProtocol().getOAuthTokenUrl()).getHost())) { - return URI.create(bookmark.getProtocol().getOAuthTokenUrl()).getHost(); + protected static String getOAuthHostname(final Host bookmark) { + final URI uri = URI.create(bookmark.getProtocol().getOAuthTokenUrl()); + if(StringUtils.isNotBlank(uri.getHost())) { + return uri.getHost(); } return bookmark.getHostname(); } - private String getOAuthPrefix(final Host bookmark) { - if(StringUtils.isNotBlank(bookmark.getCredentials().getUsername())) { - return String.format("%s (%s)", bookmark.getProtocol().getDescription(), bookmark.getCredentials().getUsername()); + protected static int getOAuthPort(final Host bookmark) { + final URI uri = URI.create(bookmark.getProtocol().getOAuthTokenUrl()); + if(-1 != uri.getPort()) { + return uri.getPort(); } - return bookmark.getProtocol().getDescription(); + return getOAuthScheme(bookmark).getPort(); + } + + protected static String[] getOAuthPrefix(final Host bookmark) { + if(StringUtils.isNotBlank(bookmark.getCredentials().getUsername())) { + return new String[]{ + String.format("%s (%s)", bookmark.getProtocol().getOAuthClientId(), bookmark.getCredentials().getUsername()), + String.format("%s (%s)", bookmark.getProtocol().getDescription(), bookmark.getCredentials().getUsername()) + }; + } + return new String[]{ + bookmark.getProtocol().getOAuthClientId(), + bookmark.getProtocol().getDescription() + }; } @Override @@ -188,6 +224,7 @@ public void save(final Host bookmark) throws LocalAccessDeniedException { return; } final Credentials credentials = bookmark.getCredentials(); + final Protocol protocol = bookmark.getProtocol(); if(log.isInfoEnabled()) { log.info(String.format("Save credentials %s for bookmark %s", credentials, bookmark)); } @@ -204,36 +241,39 @@ public void save(final Host bookmark) throws LocalAccessDeniedException { log.warn(String.format("No password in credentials for bookmark %s", bookmark.getHostname())); return; } - this.addPassword(bookmark.getProtocol().getScheme(), bookmark.getPort(), + this.addPassword(protocol.getScheme(), bookmark.getPort(), bookmark.getHostname(), credentials.getUsername(), credentials.getPassword()); } if(credentials.isTokenAuthentication()) { - this.addPassword(bookmark.getProtocol().getScheme(), bookmark.getPort(), + this.addPassword(protocol.getScheme(), bookmark.getPort(), bookmark.getHostname(), StringUtils.isEmpty(credentials.getUsername()) ? - bookmark.getProtocol().getTokenPlaceholder() : String.format("%s (%s)", bookmark.getProtocol().getTokenPlaceholder(), credentials.getUsername()), + protocol.getTokenPlaceholder() : String.format("%s (%s)", protocol.getTokenPlaceholder(), credentials.getUsername()), credentials.getToken()); } if(credentials.isOAuthAuthentication()) { - final String prefix = this.getOAuthPrefix(bookmark); - if(StringUtils.isNotBlank(credentials.getOauth().getAccessToken())) { - this.addPassword(bookmark.getProtocol().getScheme(), - bookmark.getPort(), this.getOAuthHostname(bookmark), - String.format("%s OAuth2 Access Token", prefix), credentials.getOauth().getAccessToken()); - } - if(StringUtils.isNotBlank(credentials.getOauth().getRefreshToken())) { - this.addPassword(bookmark.getProtocol().getScheme(), - bookmark.getPort(), this.getOAuthHostname(bookmark), - String.format("%s OAuth2 Refresh Token", prefix), credentials.getOauth().getRefreshToken()); - } - // Save expiry - if(credentials.getOauth().getExpiryInMilliseconds() != null) { - this.addPassword(this.getOAuthHostname(bookmark), String.format("%s OAuth2 Token Expiry", prefix), - String.valueOf(credentials.getOauth().getExpiryInMilliseconds())); - } - if(StringUtils.isNotBlank(credentials.getOauth().getIdToken())) { - this.addPassword(bookmark.getProtocol().getScheme(), - bookmark.getPort(), this.getOAuthHostname(bookmark), - String.format("%s OIDC Id Token", prefix), credentials.getOauth().getIdToken()); + final String[] descriptors = getOAuthPrefix(bookmark); + for(String prefix : descriptors) { + if(StringUtils.isNotBlank(credentials.getOauth().getAccessToken())) { + this.addPassword(getOAuthScheme(bookmark), + getOAuthPort(bookmark), getOAuthHostname(bookmark), + String.format("%s OAuth2 Access Token", prefix), credentials.getOauth().getAccessToken()); + } + if(StringUtils.isNotBlank(credentials.getOauth().getRefreshToken())) { + this.addPassword(getOAuthScheme(bookmark), + getOAuthPort(bookmark), getOAuthHostname(bookmark), + String.format("%s OAuth2 Refresh Token", prefix), credentials.getOauth().getRefreshToken()); + } + // Save expiry + if(credentials.getOauth().getExpiryInMilliseconds() != null) { + this.addPassword(getOAuthHostname(bookmark), String.format("%s OAuth2 Token Expiry", prefix), + String.valueOf(credentials.getOauth().getExpiryInMilliseconds())); + } + if(StringUtils.isNotBlank(credentials.getOauth().getIdToken())) { + this.addPassword(getOAuthScheme(bookmark), + getOAuthPort(bookmark), getOAuthHostname(bookmark), + String.format("%s OIDC Id Token", prefix), credentials.getOauth().getIdToken()); + } + break; } } } @@ -262,22 +302,24 @@ public void delete(final Host bookmark) throws LocalAccessDeniedException { protocol.getTokenPlaceholder() : String.format("%s (%s)", protocol.getTokenPlaceholder(), credentials.getUsername())); } if(protocol.isOAuthConfigurable()) { - final String prefix = this.getOAuthPrefix(bookmark); - if(StringUtils.isNotBlank(credentials.getOauth().getAccessToken())) { - this.deletePassword(protocol.getScheme(), bookmark.getPort(), this.getOAuthHostname(bookmark), - String.format("%s OAuth2 Access Token", prefix)); - } - if(StringUtils.isNotBlank(credentials.getOauth().getRefreshToken())) { - this.deletePassword(protocol.getScheme(), bookmark.getPort(), this.getOAuthHostname(bookmark), - String.format("%s OAuth2 Refresh Token", prefix)); - } - // Save expiry - if(credentials.getOauth().getExpiryInMilliseconds() != null) { - this.deletePassword(this.getOAuthHostname(bookmark), String.format("%s OAuth2 Token Expiry", prefix)); - } - if(StringUtils.isNotBlank(credentials.getOauth().getIdToken())) { - this.deletePassword(protocol.getScheme(), bookmark.getPort(), this.getOAuthHostname(bookmark), - String.format("%s OIDC Id Token", prefix)); + final String[] descriptors = getOAuthPrefix(bookmark); + for(String prefix : descriptors) { + if(StringUtils.isNotBlank(credentials.getOauth().getAccessToken())) { + this.deletePassword(getOAuthScheme(bookmark), getOAuthPort(bookmark), getOAuthHostname(bookmark), + String.format("%s OAuth2 Access Token", prefix)); + } + if(StringUtils.isNotBlank(credentials.getOauth().getRefreshToken())) { + this.deletePassword(getOAuthScheme(bookmark), getOAuthPort(bookmark), getOAuthHostname(bookmark), + String.format("%s OAuth2 Refresh Token", prefix)); + } + // Save expiry + if(credentials.getOauth().getExpiryInMilliseconds() != null) { + this.deletePassword(getOAuthHostname(bookmark), String.format("%s OAuth2 Token Expiry", prefix)); + } + if(StringUtils.isNotBlank(credentials.getOauth().getIdToken())) { + this.deletePassword(getOAuthScheme(bookmark), getOAuthPort(bookmark), getOAuthHostname(bookmark), + String.format("%s OIDC Id Token", prefix)); + } } } } diff --git a/core/src/test/java/ch/cyberduck/core/DefaultHostPasswordStoreTest.java b/core/src/test/java/ch/cyberduck/core/DefaultHostPasswordStoreTest.java new file mode 100644 index 00000000000..d4eeb09608a --- /dev/null +++ b/core/src/test/java/ch/cyberduck/core/DefaultHostPasswordStoreTest.java @@ -0,0 +1,67 @@ +package ch.cyberduck.core; + +/* + * Copyright (c) 2002-2023 iterate GmbH. All rights reserved. + * https://cyberduck.io/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class DefaultHostPasswordStoreTest { + + @Test + public void testGetOAuthPrefix() { + final String[] prefix = DefaultHostPasswordStore.getOAuthPrefix(new Host(new TestProtocol(Scheme.https) { + @Override + public String getOAuthClientId() { + return "clientid"; + } + + @Override + public String getOAuthClientSecret() { + return "clientsecret"; + } + + @Override + public String getOAuthRedirectUrl() { + return "x-cyberduck-action:oauth"; + } + })); + assertEquals("clientid", prefix[0]); + assertEquals("Test", prefix[1]); + } + + @Test + public void testGetOAuthPrefixWithUsername() { + final String[] prefix = DefaultHostPasswordStore.getOAuthPrefix(new Host(new TestProtocol(Scheme.https) { + @Override + public String getOAuthClientId() { + return "clientid"; + } + + @Override + public String getOAuthClientSecret() { + return "clientsecret"; + } + + @Override + public String getOAuthRedirectUrl() { + return "x-cyberduck-action:oauth"; + } + }).withCredentials(new Credentials("user"))); + assertEquals("clientid (user)", prefix[0]); + assertEquals("Test (user)", prefix[1]); + } +} \ No newline at end of file