Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,77 +34,97 @@
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

public class LoopbackOAuth2AuthorizationCodeProvider extends BrowserOAuth2AuthorizationCodeProvider {
private static final Logger log = LogManager.getLogger(LoopbackOAuth2AuthorizationCodeProvider.class);

@Override
public String prompt(final Host bookmark, final LoginCallback prompt, final String authorizationCodeUrl, final String redirectUri, final String state) throws BackgroundException {
return this.prompt(bookmark, prompt, ignored -> authorizationCodeUrl, redirectUri, state);
}

public String prompt(final Host bookmark, final LoginCallback prompt,
final Function<String, String> authorizationCodeUrl, final String state) throws BackgroundException {
return this.prompt(bookmark, prompt, authorizationCodeUrl, null, state);
}

private String prompt(final Host bookmark, final LoginCallback prompt,
final Function<String, String> authorizationCodeUrl,
final String requestedRedirectUri, final String expectedState) throws BackgroundException {
final CountDownLatch signal = new CountDownLatch(1);
final OAuth2TokenListenerRegistry registry = OAuth2TokenListenerRegistry.get();
final AtomicReference<String> authenticationCode = new AtomicReference<>();
registry.register(state, new OAuth2TokenListener() {
@Override
public void callback(final String code) {
log.info("Callback with code {}", code);
if(!StringUtils.isBlank(code)) {
authenticationCode.set(code);
}
OAuth2TokenListenerRegistry.get().register(expectedState, code -> {
if(StringUtils.isBlank(code)) {
signal.countDown();
}
else {
authenticationCode.set(code);
}
});
try {
final HttpServer server = HttpServer.create(new InetSocketAddress(
URI.create(redirectUri).getHost(), -1 == URI.create(redirectUri).getPort() ? 0 : URI.create(redirectUri).getPort()), 0);
final URI requested = null == requestedRedirectUri ? null : URI.create(requestedRedirectUri);
final HttpServer server = HttpServer.create(null == requested ?
new InetSocketAddress(InetAddress.getByAddress(new byte[]{127, 0, 0, 1}), 0) :
new InetSocketAddress(requested.getHost(), -1 == requested.getPort() ? 0 : requested.getPort()), 0);
final String redirectUri = null == requested ? String.format("http://127.0.0.1:%d/oauth/callback", server.getAddress().getPort()) : requestedRedirectUri;
final ExecutorService executor = Executors.newSingleThreadExecutor(new NamedThreadFactory("oauth"));
// Create handler for OAuth callback
server.createContext(StringUtils.isBlank(URI.create(redirectUri).getRawPath()) ?
String.valueOf(Path.DELIMITER) : URI.create(redirectUri).getRawPath(), new HttpHandler() {
@Override
public void handle(final HttpExchange exchange) throws IOException {
log.debug("Received callback with query {}", exchange.getRequestURI().getQuery());
final List<NameValuePair> pairs = URLEncodedUtils.parse(exchange.getRequestURI(), Charset.defaultCharset());
String.valueOf(Path.DELIMITER) : URI.create(redirectUri).getRawPath(), exchange -> {
final List<NameValuePair> pairs = URLEncodedUtils.parse(exchange.getRequestURI(), StandardCharsets.UTF_8);
String state = StringUtils.EMPTY;
String code = StringUtils.EMPTY;
for(NameValuePair pair : pairs) {
if(StringUtils.equals(pair.getName(), "state")) {
state = StringUtils.equals(pair.getName(), "state") ? pair.getValue() : StringUtils.EMPTY;
state = pair.getValue();
}
if(StringUtils.equals(pair.getName(), "code")) {
code = StringUtils.equals(pair.getName(), "code") ? pair.getValue() : StringUtils.EMPTY;
code = pair.getValue();
}
}
final OAuth2TokenListenerRegistry oauth = OAuth2TokenListenerRegistry.get();
if(oauth.notify(state, code)) {
exchange.getResponseHeaders().add(HttpHeaders.LOCATION, OAuth2AuthorizationService.CYBERDUCK_REDIRECT_URI);
exchange.sendResponseHeaders(302, 0L);
final boolean accepted = StringUtils.equals(expectedState, state) && OAuth2TokenListenerRegistry.get().notify(state, code);
try {
if(!accepted) {
exchange.sendResponseHeaders(400, 0);
}
else if(null == requested) {
final byte[] response = LocaleFactory.localizedString("Login successful", "Credentials").getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add(HttpHeaders.CONTENT_TYPE, "text/plain; charset=utf-8");
exchange.sendResponseHeaders(200, response.length);
exchange.getResponseBody().write(response);
}
else {
exchange.getResponseHeaders().add(HttpHeaders.LOCATION, OAuth2AuthorizationService.CYBERDUCK_REDIRECT_URI);
exchange.sendResponseHeaders(302, 0L);
}
}
else {
exchange.sendResponseHeaders(400, 0);
finally {
IOUtils.close(exchange.getResponseBody());
if(accepted) {
signal.countDown();
}
}
IOUtils.close(exchange.getResponseBody());
}
});
server.setExecutor(executor);
server.start();
log.info("Started OAuth callback server {}", server);
try {
// Open browser with authorization URL
this.open(authorizationCodeUrl);
this.open(authorizationCodeUrl.apply(redirectUri));
// Wait for callback
log.info("Await callback from custom scheme {} and state {}", redirectUri, state);
log.info("Await callback from custom scheme {} and state {}", redirectUri, expectedState);
prompt.await(signal, bookmark, String.format("%s %s", LocaleFactory.localizedString("Login", "Login"), BookmarkNameProvider.toString(bookmark, true)),
LocaleFactory.localizedString("Open web browser to authenticate and obtain an authorization code", "Credentials"));
bookmark.getCredentials().setSaved(new LoginOptions().save);
Expand All @@ -119,4 +139,4 @@ public void handle(final HttpExchange exchange) throws IOException {
throw new DefaultIOExceptionMappingService().map(e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public boolean notify(final String state, final String token) {
return false;
}
listeners.remove(state);
log.debug("Notify listener for state {} with token {}", state, token);
log.debug("Notify listener for state {}", state);
listener.callback(token);
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2002-2026 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.
-->

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Protocol</key>
<string>s3-login</string>
<key>Vendor</key>
<string>iterate GmbH</string>
<key>Bundled</key>
<true/>
<key>Description</key>
<string>Amazon S3 (AWS Console Sign-In)</string>
<key>Default Nickname</key>
<string>Amazon S3 (AWS Console Sign-In)</string>
<key>Hostname Configurable</key>
<false/>
<key>Port Configurable</key>
<false/>
<key>Password Configurable</key>
<false/>
<key>Username Configurable</key>
<false/>
<key>Region</key>
<string>us-east-1</string>
<key>Regions</key>
<array>
<string>af-south-1</string>
<string>ap-east-1</string>
<string>ap-east-2</string>
<string>ap-south-1</string>
<string>ap-south-2</string>
<string>ap-northeast-1</string>
<string>ap-northeast-2</string>
<string>ap-northeast-3</string>
<string>ap-southeast-1</string>
<string>ap-southeast-2</string>
<string>ap-southeast-3</string>
<string>ap-southeast-4</string>
<string>ap-southeast-5</string>
<string>ap-southeast-7</string>
<string>ca-central-1</string>
<string>ca-west-1</string>
<string>eu-west-1</string>
<string>eu-west-2</string>
<string>eu-west-3</string>
<string>eu-north-1</string>
<string>eu-south-1</string>
<string>eu-south-2</string>
<string>eu-central-1</string>
<string>eu-central-2</string>
<string>il-central-1</string>
<string>me-central-1</string>
<string>me-south-1</string>
<string>mx-central-1</string>
<string>sa-east-1</string>
<string>us-east-1</string>
<string>us-east-2</string>
<string>us-west-1</string>
<string>us-west-2</string>
</array>
<key>Properties</key>
<array>
<string>s3.login.enable=true</string>
<string>s3.storage.class.options=STANDARD INTELLIGENT_TIERING STANDARD_IA ONEZONE_IA REDUCED_REDUNDANCY
GLACIER GLACIER_IR DEEP_ARCHIVE
</string>
</array>
</dict>
</plist>
49 changes: 49 additions & 0 deletions s3/src/main/java/ch/cyberduck/core/s3/S3LoginProtocol.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package ch.cyberduck.core.s3;

/*
* Copyright (c) 2002-2026 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 ch.cyberduck.core.CredentialsConfigurator;
import ch.cyberduck.core.Protocol;

import com.google.auto.service.AutoService;

@AutoService(Protocol.class)
public class S3LoginProtocol extends S3Protocol {

@Override
public String getIdentifier() {
return "s3-login";
}

@Override
public Type getType() {
return Type.s3;
}

@Override
public String disk() {
return String.format("%s.tiff", "s3");
}

@Override
@SuppressWarnings("unchecked")
public <T> T getFeature(final Class<T> type) {
if(type == CredentialsConfigurator.class) {
return (T) CredentialsConfigurator.DISABLED;
}
return super.getFeature(type);
}
}
5 changes: 5 additions & 0 deletions s3/src/main/java/ch/cyberduck/core/s3/S3Session.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import ch.cyberduck.core.sso.IdentityCenterAuthorizationService;
import ch.cyberduck.core.sso.IdentityCenterCredentialsStrategy;
import ch.cyberduck.core.sso.RegisterClientOAuth2RequestInterceptor;
import ch.cyberduck.core.signin.AWSConsoleLoginCredentialsStrategy;
import ch.cyberduck.core.sts.STSAssumeRoleCredentialsStrategy;
import ch.cyberduck.core.sts.STSAssumeRoleWithWebIdentityCredentialsStrategy;
import ch.cyberduck.core.sts.STSAuthorizationService;
Expand Down Expand Up @@ -266,6 +267,10 @@ public void process(final HttpRequest request, final HttpContext context) {

protected S3CredentialsStrategy configureCredentialsStrategy(final HttpClientBuilder configuration,
final LoginCallback prompt) throws BackgroundException {
if(preferences.getBoolean("s3.login.enable")) {
log.debug("Configure AWS Console Sign-In");
return new AWSConsoleLoginCredentialsStrategy(configuration.build(), host, prompt);
}
if(host.getProtocol().isOAuthConfigurable()) {
if(host.getProtocol().getOAuthScopes().contains(IdentityCenterCredentialsStrategy.SSO_ACCOUNT_ACCESS_SCOPE)) {
log.debug("Configure SSO");
Expand Down
Loading