aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
authorGravatar Vitaly Takmazov2018-12-20 09:41:32 +0300
committerGravatar Vitaly Takmazov2019-01-16 16:39:21 +0300
commit809ef60e18bb8ab7c95db93b7777f3c0ffb30872 (patch)
treee3d1529ff1c1a2026118a2b856c6366b6b6ea2ef /src/main
parent1aea2345966f5026d064a44baaa82bb2d958eb8f (diff)
HTTPSignatureAuthenticationFilter
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/com/juick/server/KeystoreManager.java9
-rw-r--r--src/main/java/com/juick/server/SignatureManager.java60
-rw-r--r--src/main/java/com/juick/server/api/activity/Profile.java26
-rw-r--r--src/main/java/com/juick/server/configuration/BaseWebConfiguration.java10
-rw-r--r--src/main/java/com/juick/server/configuration/SecurityConfig.java5
-rw-r--r--src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java68
6 files changed, 131 insertions, 47 deletions
diff --git a/src/main/java/com/juick/server/KeystoreManager.java b/src/main/java/com/juick/server/KeystoreManager.java
index 67a24f11..3ae7b866 100644
--- a/src/main/java/com/juick/server/KeystoreManager.java
+++ b/src/main/java/com/juick/server/KeystoreManager.java
@@ -19,20 +19,17 @@ import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.stream.Collectors;
-@Component
public class KeystoreManager {
private static final Logger logger = LoggerFactory.getLogger("com.juick.server");
- @Value("${keystore:juick.p12}")
- private String keystore;
- @Value("${keystore_password:secret}")
+
private String keystorePassword;
private KeyStore ks;
private KeyManagerFactory kmf;
- @PostConstruct
- public void init() {
+ public KeystoreManager(String keystore, String keystorePassword) {
+ this.keystorePassword = keystorePassword;
try (InputStream ksIs = new FileInputStream(keystore)) {
ks = KeyStore.getInstance("PKCS12");
ks.load(ksIs, keystorePassword.toCharArray());
diff --git a/src/main/java/com/juick/server/SignatureManager.java b/src/main/java/com/juick/server/SignatureManager.java
index 9ecdaad5..23f5c37a 100644
--- a/src/main/java/com/juick/server/SignatureManager.java
+++ b/src/main/java/com/juick/server/SignatureManager.java
@@ -2,6 +2,7 @@ package com.juick.server;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.juick.User;
+import com.juick.model.AnonymousUser;
import com.juick.server.api.activity.model.Context;
import com.juick.server.api.activity.model.objects.Person;
import com.juick.server.api.webfinger.model.Account;
@@ -11,7 +12,6 @@ import com.juick.util.DateFormattersHolder;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
@@ -53,28 +53,43 @@ public class SignatureManager {
URI inbox = uriComponentsBuilder.build().toUri();
Instant now = Instant.now();
String requestDate = DateFormattersHolder.getHttpDateFormatter().format(now);
- Signature templateSignature = new Signature(from.getPublicKey().getId(), "rsa-sha256", null,
- "(request-target)", "host", "date");
- Signer signer = new Signer(keystoreManager.getPrivateKey(), templateSignature);
- Map<String, String> headers = new HashMap<>();
- headers.put("host", inbox.getHost());
- headers.put("date", requestDate);
- Signature signature = signer.sign("POST", inbox.getPath(), headers);
+ String host = inbox.getPort() > 0 ? String.format("%s:%d", inbox.getHost(), inbox.getPort()) : inbox.getHost();
+ String signatureString = addSignature(from, host, "POST", inbox.getPath(), requestDate);
+
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.add("Content-Type", Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE);
requestHeaders.add("Date", requestDate);
- requestHeaders.add("Signature", signature.toString().substring(10));
+ requestHeaders.add("Host", host);
+ requestHeaders.add("Signature", signatureString);
HttpEntity<Context> request = new HttpEntity<>(Context.build(data), requestHeaders);
- //boolean valid = verifySignature(Signature.fromString(requestHeaders.getFirst("Signature")),
- // keystoreManager.getPublicKey(), "POST", inbox.getPath(), headers);
logger.info("Sending context: {}", jsonMapper.writeValueAsString(data));
logger.info("Request date: {}", requestDate);
ResponseEntity<Void> response = apClient.postForEntity(inbox, request, Void.class);
logger.info("accepted follower: {}", response.getStatusCodeValue());
+ }
+
+ public String addSignature(Person from, String host, String method, String path, String dateString) throws IOException {
+ return addSignature(from, host, method, path, dateString, keystoreManager);
+ }
+ public String addSignature(Person from, String host, String method, String path, String dateString, KeystoreManager keystoreManager) throws IOException {
+ Signature templateSignature = new Signature(from.getPublicKey().getId(), "rsa-sha256", null,
+ "(request-target)", "host", "date");
+ Map<String, String> headers = new HashMap<>();
+ headers.put("host", host);
+ headers.put("date", dateString);
+ Signer signer = new Signer(keystoreManager.getPrivateKey(), templateSignature);
+ Signature signature = signer.sign(method, path, headers);
+ // remove "Signature: " from result
+ return signature.toString().substring(10);
}
+
public User verifySignature(String method, String path, Map<String, String> headers) throws IOException {
- Signature signature = Signature.fromString(headers.get("signature"));
+ String signatureString = headers.get("signature");
+ if (StringUtils.isEmpty(signatureString)) {
+ return AnonymousUser.INSTANCE;
+ }
+ Signature signature = Signature.fromString(signatureString);
Optional<Context> context = getContext(URI.create(signature.getKeyId()));
if (context.isPresent() && context.get() instanceof Person) {
Person person = (Person) context.get();
@@ -84,12 +99,16 @@ public class SignatureManager {
try {
boolean result = verifier.verify(method, path, headers);
logger.info("signature is valid: {}", result);
- User user = new User();
- user.setUri(URI.create(person.getId()));
- if (key.equals(keystoreManager.getPublicKey())) {
- return userService.getUserByName(person.getName());
+ if (result) {
+ User user = new User();
+ user.setUri(URI.create(person.getId()));
+ if (key.equals(keystoreManager.getPublicKey())) {
+ return userService.getUserByName(person.getName());
+ }
+ return user;
+ } else {
+ return AnonymousUser.INSTANCE;
}
- return user;
} catch (NoSuchAlgorithmException | SignatureException | IOException e) {
throw new IOException("Invalid signature");
}
@@ -110,9 +129,12 @@ public class SignatureManager {
return Optional.empty();
}
public Optional<Context> discoverPerson(String acct) {
- Jid acctId = Jid.of(acct);
+ String[] accountParts = acct.split(":", 2);
+ String account = accountParts[0];
+ int port = accountParts.length > 1 ? Integer.valueOf(accountParts[1]) : 80;
+ Jid acctId = Jid.of(account);
URI resourceUri = UriComponentsBuilder.fromUriString(
- String.format("https://%s/.well-known/webfinger?resource=acct:%s", acctId.getDomain(), acct)).build().toUri();
+ String.format("http://%s:%d/.well-known/webfinger?resource=acct:%s", acctId.getDomain(), port, account)).build().toUri();
Account acctData = apClient.getForEntity(resourceUri, Account.class).getBody();
if (acctData != null) {
for (Link l : acctData.getLinks()) {
diff --git a/src/main/java/com/juick/server/api/activity/Profile.java b/src/main/java/com/juick/server/api/activity/Profile.java
index 2614cded..404f0f84 100644
--- a/src/main/java/com/juick/server/api/activity/Profile.java
+++ b/src/main/java/com/juick/server/api/activity/Profile.java
@@ -44,6 +44,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
@@ -252,30 +253,11 @@ public class Profile {
}
@PostMapping(value = "/api/inbox", consumes = {Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE})
- public ResponseEntity<Void> processInbox(@RequestBody Activity activity,
- @RequestHeader(name = "Host") String host,
- @RequestHeader(name = "Date") String date,
- @RequestHeader(name = "Digest", required = false) String digest,
- @RequestHeader(name = "Content-Type") String contentType,
- @RequestHeader(name = "User-Agent", required = false) String userAgent,
- @RequestHeader(name = "Accept-Encoding", required = false) String acceptEncoding,
- @RequestHeader(name = "Signature", required = false) String signature) throws Exception {
- UriComponents componentsBuilder = ServletUriComponentsBuilder.fromCurrentRequestUri().build();
- Map<String, String> headers = new HashMap<>();
- headers.put("host", host.split(":", 2)[0]);
- headers.put("date", date);
- headers.put("digest", digest);
- headers.put("content-type", contentType);
- headers.put("user-agent", userAgent);
- headers.put("accept-encoding", acceptEncoding);
- headers.put("signature", signature);
- User signedUser = signatureManager.verifySignature( "POST",
- componentsBuilder.getPath(), headers);
- if ((StringUtils.isNotEmpty(signedUser.getUri().toString()) && signedUser.getUri().equals(URI.create(activity.getActor()))) || !signedUser.isAnonymous()) {
+ public ResponseEntity<Void> processInbox(@RequestBody Activity activity) throws Exception {
+ User visitor = UserUtils.getCurrentUser();
+ if ((StringUtils.isNotEmpty(visitor.getUri().toString()) && visitor.getUri().equals(URI.create(activity.getActor()))) || !visitor.isAnonymous()) {
if (activity instanceof Follow) {
Follow followRequest = (Follow) activity;
- String actor = followRequest.getActor();
- Person follower = (Person) signatureManager.getContext(URI.create(actor)).orElseThrow(HttpBadRequestException::new);
applicationEventPublisher.publishEvent(
new FollowEvent(this, followRequest));
return new ResponseEntity<>(HttpStatus.ACCEPTED);
diff --git a/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java b/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java
index 6a2a8142..16693995 100644
--- a/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java
+++ b/src/main/java/com/juick/server/configuration/BaseWebConfiguration.java
@@ -17,6 +17,8 @@
package com.juick.server.configuration;
+import com.juick.server.KeystoreManager;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
@@ -36,6 +38,10 @@ import java.util.concurrent.Executors;
@Configuration
public class BaseWebConfiguration implements WebMvcConfigurer, SchedulingConfigurer {
+ @Value("${keystore:juick.p12}")
+ private String keystore;
+ @Value("${keystore_password:secret}")
+ private String keystorePassword;
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
@@ -61,4 +67,8 @@ public class BaseWebConfiguration implements WebMvcConfigurer, SchedulingConfigu
public ExecutorService executorService() {
return Executors.newCachedThreadPool();
}
+ @Bean
+ public KeystoreManager keystoreManager() {
+ return new KeystoreManager(keystore, keystorePassword);
+ }
}
diff --git a/src/main/java/com/juick/server/configuration/SecurityConfig.java b/src/main/java/com/juick/server/configuration/SecurityConfig.java
index 7145e9d5..d2d3ab13 100644
--- a/src/main/java/com/juick/server/configuration/SecurityConfig.java
+++ b/src/main/java/com/juick/server/configuration/SecurityConfig.java
@@ -17,7 +17,9 @@
package com.juick.server.configuration;
+import com.juick.server.SignatureManager;
import com.juick.service.UserService;
+import com.juick.service.security.HTTPSignatureAuthenticationFilter;
import com.juick.service.security.HashParamAuthenticationFilter;
import com.juick.service.security.JuickUserDetailsService;
import com.juick.service.security.deprecated.RequestParamHashRememberMeServices;
@@ -93,6 +95,8 @@ public class SecurityConfig {
private String webDomain;
@Resource
private UserService userService;
+ @Resource
+ private SignatureManager signatureManager;
ApiConfig() {
super(true);
}
@@ -109,6 +113,7 @@ public class SecurityConfig {
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/api/**")
.addFilterBefore(apiAuthenticationFilter(), BasicAuthenticationFilter.class)
+ .addFilterBefore(new HTTPSignatureAuthenticationFilter(signatureManager, userService), BasicAuthenticationFilter.class)
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.antMatchers("/api/", "/api/messages", "/api/messages/discussions", "/api/users", "/api/thread", "/api/tags", "/api/tlgmbtwbhk", "/api/fbwbhk",
diff --git a/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java b/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java
new file mode 100644
index 00000000..8332fc8c
--- /dev/null
+++ b/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java
@@ -0,0 +1,68 @@
+package com.juick.service.security;
+
+import com.juick.User;
+import com.juick.server.SignatureManager;
+import com.juick.service.UserService;
+import com.juick.service.security.entities.JuickUser;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.annotation.Nonnull;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collections;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class HTTPSignatureAuthenticationFilter extends OncePerRequestFilter {
+
+ private final SignatureManager signatureManager;
+ private final UserService userService;
+
+
+ public HTTPSignatureAuthenticationFilter(
+ final SignatureManager signatureManager,
+ final UserService userService) {
+ this.signatureManager = signatureManager;
+ this.userService = userService;
+ }
+ @Override
+ protected void doFilterInternal(@Nonnull HttpServletRequest request, @Nonnull HttpServletResponse response, @Nonnull FilterChain filterChain) throws IOException, ServletException {
+ if (authenticationIsRequired()) {
+ Map<String, String> headers = Collections.list(request.getHeaderNames())
+ .stream()
+ .collect(Collectors.toMap(String::toLowerCase, request::getHeader));
+ User user = signatureManager.verifySignature(request.getMethod(), request.getRequestURI(), headers);
+ if (!user.isAnonymous()) {
+ String userUri = user.getUri().toString();
+ if (userUri.length() == 0) {
+ User userWithPassword = userService.getUserByName(user.getName());
+ userWithPassword.setAuthHash(userService.getHashByUID(userWithPassword.getUid()));
+ Authentication authentication = new UsernamePasswordAuthenticationToken(userWithPassword.getName(), userWithPassword.getCredentials());
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ } else {
+ Authentication authentication = new AnonymousAuthenticationToken(userUri, user, Collections.singletonList(new SimpleGrantedAuthority("ROLE_ANONYMOUS")));
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ }
+ }
+ }
+
+ filterChain.doFilter(request, response);
+ }
+
+ private boolean authenticationIsRequired() {
+ Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
+
+ return existingAuth == null ||
+ !existingAuth.isAuthenticated() ||
+ existingAuth instanceof AnonymousAuthenticationToken;
+ }
+}