Skip to content

Commit

Permalink
Security framework for the API
Browse files Browse the repository at this point in the history
  • Loading branch information
Bernard Labno committed Feb 25, 2019
1 parent 208cad5 commit d40e1d2
Show file tree
Hide file tree
Showing 18 changed files with 1,495 additions and 12 deletions.
33 changes: 30 additions & 3 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@ You can run it either as the desktop application or as a headless application.
On that branch we start to implement feature by feature starting with the most simple one - `version`.


_**Known issues**: Wallet password protection is not supported at the moment for the headless version. So if you have set
**Known issues**: Wallet password protection is not supported at the moment for the headless version. So if you have set
a wallet password when running the Desktop version and afterwards run the headless version it will get stuck at startup
expecting the wallet password. This feature will be implemented soon._

_**Note**:
If you have a Bisq application with BTC already set up it is recommended to use the optional `appName` argument to
**Note**: If you have a Bisq application with BTC already set up it is recommended to use the optional `appName` argument to
provide a different name and data directory so that the default Bisq application is not exposed via the API. That way
your data and wallet from your default Bisq application are completely isolated from the API application. In the below
commands we use the argument `--appName=bisq-API` to ensure you are not mixing up your default Bisq setup when
experimenting with the API branch. You cannot run the desktop and the headless version in parallel as they would access
the same data.

**Security**: Api uses HTTP transport which is not encrypted. Use the API only locally and do not expose it over
public network interfaces.

## Run the API as Desktop application

cd desktop
Expand All @@ -39,6 +41,31 @@ Documentation is available at http://localhost:8080/docs/
Sample call:

curl http://localhost:8080/api/v1/version

#### Authentication

By default there is no password required for the API. We recommend that you set password as soon as possible:

curl -X POST "http://localhost:8080/api/v1/user/password" -H "Content-Type: application/json" -d "{\"newPassword\":\"string\"}"

Password digest and salt are stored in a `apipasswd` in Bisq data directory.
If you forget your password, just delete that file and restart Bisq.

Once you set the password you need to trade it for access token.

curl -X POST "http://localhost:8080/api/v1/user/authenticate" -H "Content-Type: application/json" -d "{\"password\":\"string\"}"

You should get the token in response looking like this:

{
"token": "5130bcb6-bee5-47ae-ac78-ee31d6557ed5"
}

Now you can access other endpoints by adding `Authorization` header to the request:

curl -X GET "http://localhost:8080/api/v1/version" -H "authorization: 5130bcb6-bee5-47ae-ac78-ee31d6557ed5"

**NOTE**: Tokens expire after 30 minutes.

## Run the API as headless application

Expand Down
4 changes: 4 additions & 0 deletions api/src/main/java/bisq/api/http/HttpApiModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
package bisq.api.http;

import bisq.api.http.service.HttpApiServer;
import bisq.api.http.service.auth.ApiPasswordManager;
import bisq.api.http.service.auth.TokenRegistry;
import bisq.api.http.service.endpoint.VersionEndpoint;

import bisq.core.app.AppOptionKeys;
Expand All @@ -38,6 +40,8 @@ public HttpApiModule(Environment environment) {
@Override
protected void configure() {
bind(HttpApiServer.class).in(Singleton.class);
bind(TokenRegistry.class).in(Singleton.class);
bind(ApiPasswordManager.class).in(Singleton.class);
bind(VersionEndpoint.class).in(Singleton.class);

String httpApiHost = environment.getProperty(AppOptionKeys.HTTP_API_HOST, String.class, "127.0.0.1");
Expand Down
46 changes: 46 additions & 0 deletions api/src/main/java/bisq/api/http/exceptions/ExceptionMappers.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package bisq.api.http.exceptions;

import com.fasterxml.jackson.core.JsonParseException;

import lombok.extern.slf4j.Slf4j;



import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import org.eclipse.jetty.io.EofException;
import org.glassfish.jersey.server.ResourceConfig;

@Slf4j
public final class ExceptionMappers {

private ExceptionMappers() {
}

public static void register(ResourceConfig environment) {
environment.register(new ExceptionMappers.EofExceptionMapper(), 1);
environment.register(new ExceptionMappers.JsonParseExceptionMapper(), 1);
environment.register(new ExceptionMappers.UnauthorizedExceptionMapper());
}

public static class EofExceptionMapper implements ExceptionMapper<EofException> {
@Override
public Response toResponse(EofException e) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
}

public static class JsonParseExceptionMapper implements ExceptionMapper<JsonParseException> {
@Override
public Response toResponse(JsonParseException e) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
}

public static class UnauthorizedExceptionMapper implements ExceptionMapper<UnauthorizedException> {
@Override
public Response toResponse(UnauthorizedException exception) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package bisq.api.http.exceptions;

public class UnauthorizedException extends RuntimeException {
}
17 changes: 17 additions & 0 deletions api/src/main/java/bisq/api/http/model/AuthForm.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package bisq.api.http.model;

import org.hibernate.validator.constraints.NotEmpty;

public class AuthForm {

@NotEmpty
public String password;

@SuppressWarnings("unused")
public AuthForm() {
}

public AuthForm(String password) {
this.password = password;
}
}
14 changes: 14 additions & 0 deletions api/src/main/java/bisq/api/http/model/AuthResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package bisq.api.http.model;

public class AuthResult {

public String token;

@SuppressWarnings("unused")
public AuthResult() {
}

public AuthResult(String token) {
this.token = token;
}
}
16 changes: 16 additions & 0 deletions api/src/main/java/bisq/api/http/model/ChangePassword.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package bisq.api.http.model;

public class ChangePassword {

public String newPassword;
public String oldPassword;

@SuppressWarnings("unused")
public ChangePassword() {
}

public ChangePassword(String newPassword, String oldPassword) {
this.newPassword = newPassword;
this.oldPassword = oldPassword;
}
}
22 changes: 21 additions & 1 deletion api/src/main/java/bisq/api/http/service/HttpApiInterfaceV1.java
Original file line number Diff line number Diff line change
@@ -1,31 +1,51 @@
package bisq.api.http.service;

import bisq.api.http.service.endpoint.UserEndpoint;
import bisq.api.http.service.endpoint.VersionEndpoint;

import javax.inject.Inject;



import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.ws.rs.Path;

@SecurityScheme(
type = SecuritySchemeType.APIKEY,
in = SecuritySchemeIn.HEADER,
name = "authorization",
paramName = "authorization"
)
@OpenAPIDefinition(
info = @Info(version = "0.0.1", title = "Bisq HTTP API"),
security = @SecurityRequirement(name = "authorization"),
tags = {
@Tag(name = "user"),
@Tag(name = "version")
}
)
@Path("/api/v1")
public class HttpApiInterfaceV1 {
private final UserEndpoint userEndpoint;
private final VersionEndpoint versionEndpoint;

@Inject
public HttpApiInterfaceV1(VersionEndpoint versionEndpoint) {
public HttpApiInterfaceV1(UserEndpoint userEndpoint, VersionEndpoint versionEndpoint) {
this.userEndpoint = userEndpoint;
this.versionEndpoint = versionEndpoint;
}

@Path("user")
public UserEndpoint getUserEndpoint() {
return userEndpoint;
}

@Path("version")
public VersionEndpoint getVersionEndpoint() {
return versionEndpoint;
Expand Down
27 changes: 24 additions & 3 deletions api/src/main/java/bisq/api/http/service/HttpApiServer.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
package bisq.api.http.service;

import bisq.api.http.exceptions.ExceptionMappers;
import bisq.api.http.service.auth.ApiPasswordManager;
import bisq.api.http.service.auth.AuthFilter;
import bisq.api.http.service.auth.TokenRegistry;

import bisq.core.app.BisqEnvironment;

import javax.servlet.DispatcherType;

import javax.inject.Inject;

import java.net.InetSocketAddress;

import java.util.EnumSet;

import lombok.extern.slf4j.Slf4j;


Expand All @@ -16,6 +25,7 @@
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.glassfish.jersey.server.ResourceConfig;
Expand All @@ -26,12 +36,16 @@
public class HttpApiServer {
private final HttpApiInterfaceV1 httpApiInterfaceV1;
private final BisqEnvironment bisqEnvironment;
private final TokenRegistry tokenRegistry;
private final ApiPasswordManager apiPasswordManager;

@Inject
public HttpApiServer(HttpApiInterfaceV1 httpApiInterfaceV1,
BisqEnvironment bisqEnvironment) {
this.httpApiInterfaceV1 = httpApiInterfaceV1;
public HttpApiServer(ApiPasswordManager apiPasswordManager, BisqEnvironment bisqEnvironment, HttpApiInterfaceV1 httpApiInterfaceV1,
TokenRegistry tokenRegistry) {
this.apiPasswordManager = apiPasswordManager;
this.bisqEnvironment = bisqEnvironment;
this.httpApiInterfaceV1 = httpApiInterfaceV1;
this.tokenRegistry = tokenRegistry;
}

public void startServer() {
Expand All @@ -52,11 +66,13 @@ public void startServer() {

private ContextHandler buildAPIHandler() {
ResourceConfig resourceConfig = new ResourceConfig();
ExceptionMappers.register(resourceConfig);
resourceConfig.register(httpApiInterfaceV1);
resourceConfig.packages("io.swagger.v3.jaxrs2.integration.resources");
ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS | ServletContextHandler.NO_SECURITY);
servletContextHandler.setContextPath("/");
servletContextHandler.addServlet(new ServletHolder(new ServletContainer(resourceConfig)), "/*");
setupAuth(servletContextHandler);
return servletContextHandler;
}

Expand All @@ -77,4 +93,9 @@ private ContextHandler buildSwaggerUIHandler() throws Exception {
swaggerUIContext.setHandler(swaggerUIResourceHandler);
return swaggerUIContext;
}

private void setupAuth(ServletContextHandler appContextHandler) {
AuthFilter authFilter = new AuthFilter(apiPasswordManager, tokenRegistry);
appContextHandler.addFilter(new FilterHolder(authFilter), "/*", EnumSet.allOf(DispatcherType.class));
}
}
Loading

0 comments on commit d40e1d2

Please sign in to comment.