diff --git a/core/src/main/java/google/registry/tools/ForwardingServerReceiver.java b/core/src/main/java/google/registry/tools/ForwardingServerReceiver.java
new file mode 100644
index 000000000..ac8df9640
--- /dev/null
+++ b/core/src/main/java/google/registry/tools/ForwardingServerReceiver.java
@@ -0,0 +1,72 @@
+// Copyright 2022 The Nomulus Authors. All Rights Reserved.
+//
+// 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 google.registry.tools;
+
+import com.google.api.client.extensions.java6.auth.oauth2.VerificationCodeReceiver;
+import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
+import java.io.IOException;
+
+/**
+ * A thin wrapper around {@link LocalServerReceiver} which points the redirect URI to a different
+ * port (the forwarding port) while still listening on the random unused port (the remote port)
+ * nomulus itself picks. This allows us to run the nomulus tool on a remote host (which one can SSH
+ * into) while performing the OAuth 3-legged login flow in a local browser (from the lost host where
+ * the SSH client resides).
+ *
+ *
When performing the login flow, an HTTP server will be listening on the remote port and have a
+ * redirect_uri of http://localhost:remote_port
, which is only accessible from the
+ * remote host. By changing the redirect_uri to http://localhost:forwarding_port
, it
+ * becomes accessible from the local host, if local_host:forwarding_port
is forwarded
+ * to remote_host:remote_port
.
+ *
+ *
Note that port forwarding is required. We cannot use the remote host's IP or reverse
+ * DNS address in the redirect URI, even if they are directly accessible from the local host,
+ * because the only allowed redirect URI scheme for desktops apps when sending a request to the
+ * Google OAuth server is the loopback address with a port.
+ *
+ * @see
+ * redirect_uri values
+ */
+final class ForwardingServerReceiver implements VerificationCodeReceiver {
+
+ private final int forwarding_port;
+ private final LocalServerReceiver localServerReceiver = new LocalServerReceiver();
+
+ ForwardingServerReceiver(int forwarding_port) {
+ this.forwarding_port = forwarding_port;
+ }
+
+ @Override
+ public String getRedirectUri() throws IOException {
+ String redirect_uri = localServerReceiver.getRedirectUri();
+ return redirect_uri.replace("localhost:" + getRemotePort(), "localhost:" + forwarding_port);
+ }
+
+ @Override
+ public String waitForCode() throws IOException {
+ return localServerReceiver.waitForCode();
+ }
+
+ @Override
+ public void stop() throws IOException {
+ localServerReceiver.stop();
+ System.out.println("You can now exit from the SSH session created for port forwarding.");
+ }
+
+ int getRemotePort() {
+ return localServerReceiver.getPort();
+ }
+}
diff --git a/core/src/main/java/google/registry/tools/LoginCommand.java b/core/src/main/java/google/registry/tools/LoginCommand.java
index 3b4fce14f..10c9905b6 100644
--- a/core/src/main/java/google/registry/tools/LoginCommand.java
+++ b/core/src/main/java/google/registry/tools/LoginCommand.java
@@ -19,7 +19,7 @@ import com.beust.jcommander.Parameters;
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
-import com.google.api.client.googleapis.extensions.java6.auth.oauth2.GooglePromptReceiver;
+import java.net.InetAddress;
import javax.inject.Inject;
/** Authorizes the nomulus tool for OAuth 2.0 access to remote resources. */
@@ -30,24 +30,35 @@ final class LoginCommand implements Command {
@Inject @AuthModule.ClientScopeQualifier String clientScopeQualifier;
@Parameter(
- names = "--remote",
+ names = "--port",
description =
- "Whether the command is run on a remote host where access to a browser is not available. "
- + "If set to true, a URL will be given and a code is expected to be entered after "
- + "the user completes authorization by visiting that URL.")
- private boolean remote = false;
+ "A free port on the local host. When set, it is assumed that the nomulus tool runs on a"
+ + " remote host whose browser is not accessible locally. i. e. if you SSH to a"
+ + " machine and run `nomulus` there, the ssh client is on the local host and nomulus"
+ + " runs on a remote host. You will need to forward the local port specified here to"
+ + " a remote port that nomulus randomly picks. Follow the instruction when prompted.")
+ private int port = 0;
@Override
public void run() throws Exception {
AuthorizationCodeInstalledApp app;
- if (remote) {
+ if (port != 0) {
+ String remote_host = InetAddress.getLocalHost().getHostName();
+ ForwardingServerReceiver forwardingServerReceiver = new ForwardingServerReceiver(port);
app =
new AuthorizationCodeInstalledApp(
flow,
- new GooglePromptReceiver(),
+ forwardingServerReceiver,
url -> {
- System.out.println("Please open the following address in your browser:");
- System.out.println(" " + url);
+ int remote_port = forwardingServerReceiver.getRemotePort();
+ System.out.printf(
+ "Please first run the following command in a separate terminal on your local "
+ + "host:\n\n ssh -L %s:localhost:%s %s\n\n",
+ port, remote_port, remote_host);
+ System.out.printf(
+ "Please then open the following URL in your local browser and follow the"
+ + " instructions:\n\n %s\n\n",
+ url);
});
} else {
app = new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver());