Skip to content

Commit 266cfc6

Browse files
committed
Merge pull request signalapp#18 from fxkr/require-http-basic-auth
Require HTTP basic auth for github webhook
2 parents b252c0f + fae0083 commit 266cfc6

File tree

8 files changed

+140
-4
lines changed

8 files changed

+140
-4
lines changed

config/sample.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ github:
33
token: # Your BitHub instance's GitHub auth token.
44
repositories:
55
- # A list of repository URLs to support payouts for.
6+
webhook:
7+
password: # HTTP basic auth. The username defaults to "bithub".
68

79
coinbase:
810
apiKey: # Your Coinbase API key.

src/main/java/org/whispersystems/bithub/BithubServerConfiguration.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.whispersystems.bithub.config.BithubConfiguration;
2323
import org.whispersystems.bithub.config.CoinbaseConfiguration;
2424
import org.whispersystems.bithub.config.GithubConfiguration;
25+
import org.whispersystems.bithub.config.WebhookConfiguration;
2526

2627
import javax.validation.Valid;
2728
import javax.validation.constraints.NotNull;

src/main/java/org/whispersystems/bithub/BithubService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
package org.whispersystems.bithub;
1919

2020
import com.yammer.dropwizard.Service;
21+
import com.yammer.dropwizard.auth.basic.BasicAuthProvider;
2122
import com.yammer.dropwizard.config.Bootstrap;
2223
import com.yammer.dropwizard.config.Environment;
2324
import com.yammer.dropwizard.views.ViewBundle;
25+
import org.whispersystems.bithub.auth.GithubWebhookAuthenticator;
2426
import org.whispersystems.bithub.client.CoinbaseClient;
2527
import org.whispersystems.bithub.client.GithubClient;
2628
import org.whispersystems.bithub.controllers.GithubController;
@@ -51,6 +53,8 @@ public void run(BithubServerConfiguration config, Environment environment)
5153
{
5254
String githubUser = config.getGithubConfiguration().getUser();
5355
String githubToken = config.getGithubConfiguration().getToken();
56+
String githubWebhookUser = config.getGithubConfiguration().getWebhookConfiguration().getUsername();
57+
String githubWebhookPwd = config.getGithubConfiguration().getWebhookConfiguration().getPassword();
5458
List<String> githubRepositories = config.getGithubConfiguration().getRepositories();
5559
BigDecimal payoutRate = config.getBithubConfiguration().getPayoutRate();
5660
GithubClient githubClient = new GithubClient(githubUser, githubToken);
@@ -61,6 +65,8 @@ public void run(BithubServerConfiguration config, Environment environment)
6165
environment.addResource(new StatusController(coinbaseClient, payoutRate));
6266
environment.addProvider(new IOExceptionMapper());
6367
environment.addProvider(new UnauthorizedHookExceptionMapper());
68+
environment.addProvider(new BasicAuthProvider<>(
69+
new GithubWebhookAuthenticator(githubWebhookUser, githubWebhookPwd), GithubWebhookAuthenticator.REALM));
6470
}
6571

6672
public static void main(String[] args) throws Exception {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.whispersystems.bithub.auth;
2+
3+
import com.google.common.base.Optional;
4+
import com.yammer.dropwizard.auth.Authenticator;
5+
import com.yammer.dropwizard.auth.basic.BasicCredentials;
6+
7+
/**
8+
* Accepts only one fixed username/password combination.
9+
*/
10+
public class GithubWebhookAuthenticator implements Authenticator<BasicCredentials, GithubWebhookAuthenticator.Authentication> {
11+
12+
/**
13+
* Represents a successful basic HTTP authentication.
14+
*/
15+
public static class Authentication {
16+
}
17+
18+
public static final String REALM = "bithub";
19+
20+
private final BasicCredentials correctCredentials;
21+
22+
public GithubWebhookAuthenticator(String username, String password) {
23+
this.correctCredentials = new BasicCredentials(username, password);
24+
}
25+
26+
@Override
27+
public Optional<Authentication> authenticate(BasicCredentials clientCredentials) {
28+
if (correctCredentials.equals(clientCredentials)) {
29+
return Optional.of(new Authentication());
30+
} else {
31+
return Optional.absent();
32+
}
33+
}
34+
}

src/main/java/org/whispersystems/bithub/config/GithubConfiguration.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import org.slf4j.Logger;
2525
import org.slf4j.LoggerFactory;
2626

27+
import javax.validation.Valid;
28+
import javax.validation.constraints.NotNull;
2729
import java.io.IOException;
2830
import java.util.LinkedList;
2931
import java.util.List;
@@ -46,6 +48,11 @@ public class GithubConfiguration {
4648
@JsonProperty
4749
private String repositories_heroku;
4850

51+
@Valid
52+
@NotNull
53+
@JsonProperty
54+
private WebhookConfiguration webhook;
55+
4956
public String getUser() {
5057
return user;
5158
}
@@ -70,4 +77,8 @@ public List<String> getRepositories() {
7077

7178
return new LinkedList<>();
7279
}
80+
81+
public WebhookConfiguration getWebhookConfiguration() {
82+
return webhook;
83+
}
7384
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.whispersystems.bithub.config;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import org.hibernate.validator.constraints.NotEmpty;
5+
6+
public class WebhookConfiguration {
7+
8+
@JsonProperty
9+
@NotEmpty
10+
private String username = "bithub";
11+
12+
@JsonProperty
13+
@NotEmpty
14+
private String password;
15+
16+
public String getUsername() { return username; }
17+
18+
public String getPassword() { return password; }
19+
}

src/main/java/org/whispersystems/bithub/controllers/GithubController.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@
1818
package org.whispersystems.bithub.controllers;
1919

2020
import com.fasterxml.jackson.databind.ObjectMapper;
21+
import com.yammer.dropwizard.auth.Auth;
2122
import com.yammer.metrics.annotation.Timed;
2223
import org.apache.commons.net.util.SubnetUtils;
2324
import org.apache.commons.net.util.SubnetUtils.SubnetInfo;
2425
import org.slf4j.Logger;
2526
import org.slf4j.LoggerFactory;
27+
import org.whispersystems.bithub.auth.GithubWebhookAuthenticator.Authentication;
2628
import org.whispersystems.bithub.client.CoinbaseClient;
2729
import org.whispersystems.bithub.client.GithubClient;
2830
import org.whispersystems.bithub.client.TransferFailedException;
@@ -85,8 +87,9 @@ public GithubController(List<String> repositories,
8587
@POST
8688
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
8789
@Path("/commits/")
88-
public void handleCommits(@HeaderParam("X-Forwarded-For") String clientIp,
89-
@FormParam("payload") String eventString)
90+
public void handleCommits(@Auth Authentication auth,
91+
@HeaderParam("X-Forwarded-For") String clientIp,
92+
@FormParam("payload") String eventString)
9093
throws IOException, UnauthorizedHookException
9194
{
9295
authenticate(clientIp);

src/test/java/org/whispersystems/bithub/tests/controllers/GithubControllerTest.java

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@
1919

2020
import com.sun.jersey.api.client.ClientResponse;
2121
import com.sun.jersey.core.util.MultivaluedMapImpl;
22+
import com.yammer.dropwizard.auth.basic.BasicAuthProvider;
2223
import com.yammer.dropwizard.testing.ResourceTest;
24+
import org.apache.commons.codec.binary.Base64;
2325
import org.junit.Test;
26+
import org.whispersystems.bithub.auth.GithubWebhookAuthenticator;
2427
import org.whispersystems.bithub.client.CoinbaseClient;
2528
import org.whispersystems.bithub.client.GithubClient;
2629
import org.whispersystems.bithub.client.TransferFailedException;
@@ -47,6 +50,14 @@ public class GithubControllerTest extends ResourceTest {
4750
private final CoinbaseClient coinbaseClient = mock(CoinbaseClient.class);
4851
private final GithubClient githubClient = mock(GithubClient.class);
4952

53+
// HTTP Basic Authentication data
54+
private final String authUsername = "TestUser";
55+
private final String authPassword = "TestPassword";
56+
private final String authRealm = GithubWebhookAuthenticator.REALM;
57+
private final String authString = "Basic " + Base64.encodeBase64String((authUsername + ":" + authPassword).getBytes());
58+
private final String invalidUserAuthString = "Basic " + Base64.encodeBase64(("wrong:" + authPassword).getBytes());
59+
private final String invalidPasswordAuthString = "Basic " + Base64.encodeBase64((authUsername + ":wrong").getBytes());
60+
5061
private final List<String> repositories = new LinkedList<String>() {{
5162
add("https://github.com/moxie0/test");
5263
}};
@@ -57,6 +68,7 @@ protected void setUpResources() throws Exception {
5768
when(coinbaseClient.getExchangeRate()).thenReturn(EXCHANGE_RATE);
5869
addResource(new GithubController(repositories, githubClient, coinbaseClient, new BigDecimal(0.02)));
5970
addProvider(new UnauthorizedHookExceptionMapper());
71+
addProvider(new BasicAuthProvider<>(new GithubWebhookAuthenticator(authUsername, authPassword), authRealm));
6072
}
6173

6274
protected String payload(String path) {
@@ -72,6 +84,7 @@ public void testInvalidRepository() throws Exception {
7284
post.add("payload", payloadValue);
7385
ClientResponse response = client().resource("/v1/github/commits/")
7486
.header("X-Forwarded-For", "192.30.252.1")
87+
.header("Authorization", authString)
7588
.type(MediaType.APPLICATION_FORM_URLENCODED_TYPE)
7689
.post(ClientResponse.class, post);
7790

@@ -85,19 +98,62 @@ public void testInvalidOrigin() throws Exception {
8598
post.add("payload", payloadValue);
8699
ClientResponse response = client().resource("/v1/github/commits/")
87100
.header("X-Forwarded-For", "192.30.242.1")
101+
.header("Authorization", authString)
88102
.type(MediaType.APPLICATION_FORM_URLENCODED_TYPE)
89103
.post(ClientResponse.class, post);
90104

91105
assertThat(response.getStatus()).isEqualTo(401);
92106
}
93107

108+
@Test
109+
public void testMissingAuth() throws Exception, TransferFailedException {
110+
String payloadValue = payload("/payloads/valid_commit.json");
111+
MultivaluedMapImpl post = new MultivaluedMapImpl();
112+
post.add("payload", payloadValue);
113+
ClientResponse response = client().resource("/v1/github/commits/")
114+
.header("X-Forwarded-For", "192.30.252.1")
115+
.type(MediaType.APPLICATION_FORM_URLENCODED_TYPE)
116+
.post(ClientResponse.class, post);
117+
118+
assertThat(response.getStatus()).isEqualTo(401);
119+
}
120+
121+
@Test
122+
public void testInvalidAuthUser() throws Exception, TransferFailedException {
123+
String payloadValue = payload("/payloads/valid_commit.json");
124+
MultivaluedMapImpl post = new MultivaluedMapImpl();
125+
post.add("payload", payloadValue);
126+
ClientResponse response = client().resource("/v1/github/commits/")
127+
.header("X-Forwarded-For", "192.30.252.1")
128+
.header("Authorization", invalidUserAuthString)
129+
.type(MediaType.APPLICATION_FORM_URLENCODED_TYPE)
130+
.post(ClientResponse.class, post);
131+
132+
assertThat(response.getStatus()).isEqualTo(401);
133+
}
134+
135+
@Test
136+
public void testInvalidAuthPassword() throws Exception, TransferFailedException {
137+
String payloadValue = payload("/payloads/valid_commit.json");
138+
MultivaluedMapImpl post = new MultivaluedMapImpl();
139+
post.add("payload", payloadValue);
140+
ClientResponse response = client().resource("/v1/github/commits/")
141+
.header("X-Forwarded-For", "192.30.252.1")
142+
.header("Authorization", invalidPasswordAuthString)
143+
.type(MediaType.APPLICATION_FORM_URLENCODED_TYPE)
144+
.post(ClientResponse.class, post);
145+
146+
assertThat(response.getStatus()).isEqualTo(401);
147+
}
148+
94149
@Test
95150
public void testOptOutCommit() throws Exception, TransferFailedException {
96151
String payloadValue = payload("/payloads/opt_out_commit.json");
97152
MultivaluedMapImpl post = new MultivaluedMapImpl();
98153
post.add("payload", payloadValue);
99154
ClientResponse response = client().resource("/v1/github/commits/")
100155
.header("X-Forwarded-For", "192.30.252.1")
156+
.header("Authorization", authString)
101157
.type(MediaType.APPLICATION_FORM_URLENCODED_TYPE)
102158
.post(ClientResponse.class, post);
103159

@@ -113,6 +169,7 @@ public void testValidCommit() throws Exception, TransferFailedException {
113169
post.add("payload", payloadValue);
114170
ClientResponse response = client().resource("/v1/github/commits/")
115171
.header("X-Forwarded-For", "192.30.252.1")
172+
.header("Authorization", authString)
116173
.type(MediaType.APPLICATION_FORM_URLENCODED_TYPE)
117174
.post(ClientResponse.class, post);
118175

@@ -126,8 +183,11 @@ public void testValidMultipleCommitsMultipleAuthors() throws Exception, Transfer
126183
String payloadValue = payload("/payloads/multiple_commits_authors.json");
127184
MultivaluedMapImpl post = new MultivaluedMapImpl();
128185
post.add("payload", payloadValue);
129-
ClientResponse response = client().resource("/v1/github/commits/").header("X-Forwarded-For", "192.30.252.1")
130-
.type(MediaType.APPLICATION_FORM_URLENCODED_TYPE).post(ClientResponse.class, post);
186+
ClientResponse response = client().resource("/v1/github/commits/")
187+
.header("X-Forwarded-For", "192.30.252.1")
188+
.header("Authorization", authString)
189+
.type(MediaType.APPLICATION_FORM_URLENCODED_TYPE)
190+
.post(ClientResponse.class, post);
131191

132192
verify(coinbaseClient, times(1)).sendPayment(any(Author.class), eq(BALANCE.multiply(new BigDecimal(0.02))),
133193
anyString());

0 commit comments

Comments
 (0)