Use a single auth token vs username:password

There is actually no use case for both username and password forming the
authentication token. This change simplifies the arrangement such that a
single token is passed.

--rpcUser and --rpcPassword are no longer used and are replaced by
--apiToken on both cli and daemon sides.
This commit is contained in:
Chris Beams 2020-04-23 11:32:19 +02:00
parent ca0658229b
commit 1a133f4b67
No known key found for this signature in database
GPG Key ID: 3D214F8F5BC5ED73
7 changed files with 34 additions and 78 deletions

View File

@ -4,11 +4,8 @@ import io.grpc.CallCredentials;
import io.grpc.Metadata;
import io.grpc.Metadata.Key;
import java.util.Map;
import java.util.concurrent.Executor;
import static bisq.cli.app.CliConfig.RPC_PASSWORD;
import static bisq.cli.app.CliConfig.RPC_USER;
import static io.grpc.Metadata.ASCII_STRING_MARSHALLER;
import static io.grpc.Status.UNAUTHENTICATED;
@ -17,10 +14,10 @@ import static io.grpc.Status.UNAUTHENTICATED;
*/
public class BisqCallCredentials extends CallCredentials {
private final Map<String, String> credentials;
private final String apiToken;
public BisqCallCredentials(Map<String, String> credentials) {
this.credentials = credentials;
public BisqCallCredentials(String apiToken) {
this.apiToken = apiToken;
}
@Override
@ -28,8 +25,8 @@ public class BisqCallCredentials extends CallCredentials {
appExecutor.execute(() -> {
try {
Metadata headers = new Metadata();
Key<String> creds = Key.of("bisq-api-token", ASCII_STRING_MARSHALLER);
headers.put(creds, encodeCredentials());
Key<String> apiTokenKey = Key.of("bisq-api-token", ASCII_STRING_MARSHALLER);
headers.put(apiTokenKey, apiToken);
metadataApplier.apply(headers);
} catch (Throwable ex) {
metadataApplier.fail(UNAUTHENTICATED.withCause(ex));
@ -37,13 +34,6 @@ public class BisqCallCredentials extends CallCredentials {
});
}
private String encodeCredentials() {
if (!credentials.containsKey(RPC_USER) || !credentials.containsKey(RPC_PASSWORD))
throw new ConfigException("Cannot call rpc service without username:password credentials");
return credentials.get(RPC_USER) + ":" + credentials.get(RPC_PASSWORD);
}
@Override
public void thisUsesUnstableApi() {
}

View File

@ -16,12 +16,10 @@ final class CliConfig {
static final String STOPSERVER = "stopserver";
// Argument accepting name constants
static final String RPC_USER = "rpcuser";
static final String RPC_PASSWORD = "rpcpassword";
static final String API_TOKEN = "apiToken";
// Argument accepting cmd options
final String rpcUser;
final String rpcPassword;
final String apiToken;
// The parser that will be used to parse both cmd line and config file options
private final OptionParser optionParser = new OptionParser();
@ -30,22 +28,17 @@ final class CliConfig {
CliConfig(String[] params) {
this.params = params;
ArgumentAcceptingOptionSpec<String> rpcUserOpt =
optionParser.accepts(RPC_USER, "Bisq daemon username")
.withRequiredArg()
.defaultsTo("");
ArgumentAcceptingOptionSpec<String> rpcPasswordOpt =
optionParser.accepts(RPC_PASSWORD, "Bisq daemon password")
.withRequiredArg()
.defaultsTo("");
ArgumentAcceptingOptionSpec<String> apiTokenOpt =
optionParser.accepts(API_TOKEN, "Bisq API token")
.withRequiredArg();
try {
CompositeOptionSet options = new CompositeOptionSet();
// Parse command line options
OptionSet cliOpts = optionParser.parse(params);
options.addOptionSet(cliOpts);
this.rpcUser = options.valueOf(rpcUserOpt);
this.rpcPassword = options.valueOf(rpcPasswordOpt);
this.apiToken = options.valueOf(apiTokenOpt);
optionParser.allowsUnrecognizedOptions();
optionParser.nonOptions(GETBALANCE).ofType(String.class).describedAs("Get btc balance");
@ -69,6 +62,6 @@ final class CliConfig {
}
static void printHelp() {
out.println("Usage: bisq-cli --rpcpassword=user --rpcpassword=password getbalance | getversion");
out.println("Usage: bisq-cli --apiToken=token getbalance | getversion");
}
}

View File

@ -4,19 +4,13 @@ import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import static java.lang.System.out;
final class CommandParser {
private Optional<String> cmdToken;
private final Map<String, String> creds = new HashMap<>();
private String apiToken;
private final CliConfig config;
public CommandParser(CliConfig config) {
@ -25,37 +19,19 @@ final class CommandParser {
}
public Optional<String> getCmdToken() {
return this.cmdToken;
return cmdToken;
}
public Map<String, String> getCreds() {
return this.creds;
public String getApiToken() {
return apiToken;
}
private void init() {
OptionParser parser = config.getOptionParser();
OptionSpec<String> nonOptions = parser.nonOptions().ofType(String.class);
OptionSet options = parser.parse(config.getParams());
creds.putAll(rpcCredentials.apply(options));
// debugOptionsSet(options, nonOptions);
apiToken = (String) options.valueOf("apiToken");
List<String> detectedOptions = nonOptions.values(options);
cmdToken = detectedOptions.isEmpty() ? Optional.empty() : Optional.of(detectedOptions.get(0));
}
final Function<OptionSet, Map<String, String>> rpcCredentials = (opts) ->
opts.asMap().entrySet().stream()
.filter(e -> e.getKey().options().size() == 1 && e.getKey().options().get(0).startsWith("rpc"))
.collect(Collectors.toUnmodifiableMap(m -> m.getKey().options().get(0), m -> (String) m.getValue().get(0)));
private void debugOptionsSet(OptionSet options, OptionSpec<String> nonOptions) {
// https://programtalk.com/java-api-usage-examples/joptsimple.OptionParser
out.println("*** BEGIN Debug OptionSet ***");
out.println("[argument acceptors]");
options.asMap().entrySet().forEach(out::println);
out.println("[rpc credentials map]");
out.println(rpcCredentials.apply(options));
out.println("[non options]");
nonOptions.values(options).forEach(out::println);
out.println("*** END Debug OptionSet ***");
}
}

View File

@ -32,7 +32,7 @@ final class RpcCommand {
final Function<Long, String> prettyBalance = (sats) -> btcFormat.format(BigDecimal.valueOf(sats).divide(satoshiDivisor));
RpcCommand(ManagedChannel channel, CommandParser parser) {
this.callCredentials = new BisqCallCredentials(parser.getCreds());
this.callCredentials = new BisqCallCredentials(parser.getApiToken());
this.getBalanceStub = GetBalanceGrpc.newBlockingStub(channel).withCallCredentials(callCredentials);
this.getVersionStub = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(callCredentials);
this.stopServerStub = StopServerGrpc.newBlockingStub(channel).withCallCredentials(callCredentials);

View File

@ -116,6 +116,7 @@ public class Config {
public static final String DAO_ACTIVATED = "daoActivated";
public static final String DUMP_DELAYED_PAYOUT_TXS = "dumpDelayedPayoutTxs";
public static final String ALLOW_FAULTY_DELAYED_TXS = "allowFaultyDelayedTxs";
public static final String API_TOKEN = "apiToken";
// Default values for certain options
public static final int UNSPECIFIED_PORT = -1;
@ -199,6 +200,7 @@ public class Config {
public final long genesisTotalSupply;
public final boolean dumpDelayedPayoutTxs;
public final boolean allowFaultyDelayedTxs;
public final String apiToken;
// Properties derived from options but not exposed as options themselves
public final File torDir;
@ -615,6 +617,11 @@ public class Config {
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<String> apiTokenOpt =
parser.accepts(API_TOKEN, "Bisq gRPC API authentication token")
.withRequiredArg()
.defaultsTo("");
try {
CompositeOptionSet options = new CompositeOptionSet();
@ -727,6 +734,7 @@ public class Config {
this.daoActivated = options.valueOf(daoActivatedOpt);
this.dumpDelayedPayoutTxs = options.valueOf(dumpDelayedPayoutTxsOpt);
this.allowFaultyDelayedTxs = options.valueOf(allowFaultyDelayedTxsOpt);
this.apiToken = options.valueOf(apiTokenOpt);
} catch (OptionException ex) {
throw new ConfigException("problem parsing option '%s': %s",
ex.options().get(0),

View File

@ -205,8 +205,6 @@ public class BisqGrpcServer {
private void start() throws IOException {
// TODO add to options
int port = 9998;
String rpcUser = config.rpcUser;
String rpcPassword = config.rpcPassword;
// Config services
server = ServerBuilder.forPort(port)
@ -217,7 +215,7 @@ public class BisqGrpcServer {
.addService(new GetPaymentAccountsImpl())
.addService(new PlaceOfferImpl())
.addService(new StopServerImpl())
.intercept(new TokenAuthInterceptor(rpcUser, rpcPassword))
.intercept(new TokenAuthInterceptor(config.apiToken))
.build()
.start();

View File

@ -18,12 +18,10 @@ import static io.grpc.Status.UNAUTHENTICATED;
@Slf4j
public class TokenAuthInterceptor implements ServerInterceptor {
private final String rpcUser;
private final String rpcPassword;
private final String apiToken;
public TokenAuthInterceptor(String rpcUser, String rpcPassword) {
this.rpcUser = rpcUser;
this.rpcPassword = rpcPassword;
public TokenAuthInterceptor(String apiToken) {
this.apiToken = apiToken;
}
@Override
@ -35,16 +33,9 @@ public class TokenAuthInterceptor implements ServerInterceptor {
private void authenticate(String authToken) {
if (authToken == null)
throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("Authentication token is missing"));
throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("API token is missing"));
if (!isValidToken(authToken))
throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("Invalid username or password"));
log.info("Authenticated user {} with token {}", rpcUser, authToken);
}
private boolean isValidToken(String token) {
String[] pair = token.split(":");
return pair[0].equals(rpcUser) && pair[1].equals(rpcPassword);
if (!authToken.equals(apiToken))
throw new StatusRuntimeException(UNAUTHENTICATED.withDescription("Invalid API token"));
}
}