aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
authorGravatar Vitaly Takmazov2023-01-04 03:37:05 +0300
committerGravatar Vitaly Takmazov2023-01-04 03:37:05 +0300
commit086d9a7625bfc5a386f5b1028d364fb546c2fa9d (patch)
tree54db8116fa0eaa40e5617d17545e62148b8c608f /src/main
parentaa9240e5431c5ee81f3d25d6481c66c445d11711 (diff)
JWT authentication for API
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/com/juick/KeystoreManager.java14
-rw-r--r--src/main/java/com/juick/config/SecurityConfig.java18
-rw-r--r--src/main/java/com/juick/service/security/BaseAuthenticationFilter.java33
-rw-r--r--src/main/java/com/juick/service/security/BearerTokenAuthenticationFilter.java82
-rw-r--r--src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java23
-rw-r--r--src/main/java/com/juick/service/security/HashParamAuthenticationFilter.java23
6 files changed, 154 insertions, 39 deletions
diff --git a/src/main/java/com/juick/KeystoreManager.java b/src/main/java/com/juick/KeystoreManager.java
index d8355941..952cb72c 100644
--- a/src/main/java/com/juick/KeystoreManager.java
+++ b/src/main/java/com/juick/KeystoreManager.java
@@ -17,7 +17,9 @@
package com.juick;
+import com.juick.model.User;
import com.juick.www.api.activity.model.objects.Actor;
+import io.jsonwebtoken.Jwts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
@@ -30,6 +32,9 @@ import java.io.InputStream;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
+import java.time.ZonedDateTime;
+import java.util.Arrays;
+import java.util.Date;
public class KeystoreManager {
private static final Logger logger = LoggerFactory.getLogger("ActivityPub");
@@ -51,7 +56,7 @@ public class KeystoreManager {
}
}
- private KeyPair getKeyPair() {
+ public KeyPair getKeyPair() {
java.security.Key privateKey;
try {
privateKey = ks.getKey("1", keystorePassword.toCharArray());
@@ -77,4 +82,11 @@ public class KeystoreManager {
String pubkeyPem = person.getPublicKey().getPublicKeyPem();
return Keys.decode(pubkeyPem.getBytes()).getKey();
}
+ public String generateToken(User user) {
+ return Jwts.builder()
+ .setSubject(user.getName())
+ .setIssuedAt(Date.from(ZonedDateTime.now().toInstant()))
+ .signWith(getPrivateKey())
+ .compact();
+ }
}
diff --git a/src/main/java/com/juick/config/SecurityConfig.java b/src/main/java/com/juick/config/SecurityConfig.java
index ad189052..d2030a62 100644
--- a/src/main/java/com/juick/config/SecurityConfig.java
+++ b/src/main/java/com/juick/config/SecurityConfig.java
@@ -17,8 +17,10 @@
package com.juick.config;
+import com.juick.KeystoreManager;
import com.juick.SignatureManager;
import com.juick.service.UserService;
+import com.juick.service.security.BearerTokenAuthenticationFilter;
import com.juick.service.security.HTTPSignatureAuthenticationFilter;
import com.juick.service.security.HashParamAuthenticationFilter;
import com.juick.service.security.JuickUserDetailsService;
@@ -57,9 +59,10 @@ import java.util.Collections;
public class SecurityConfig {
@Inject
private UserService userService;
+ @Inject
+ private KeystoreManager keystoreManager;
private static final String COOKIE_NAME = "juick-remember-me";
-
@Bean
UserDetailsService userDetailsService() {
return new JuickUserDetailsService(userService);
@@ -89,7 +92,7 @@ public class SecurityConfig {
}
@Bean
- AuthenticationEntryPoint juickAuthenticationEntryPoint() {
+ AuthenticationEntryPoint apiAuthenticationEntryPoint() {
var entryPoint = new BasicAuthenticationEntryPoint();
entryPoint.setRealmName("Juick");
return entryPoint;
@@ -104,6 +107,10 @@ public class SecurityConfig {
HashParamAuthenticationFilter wwwAuthenticationFilter() {
return new HashParamAuthenticationFilter(userService, hashCookieServices());
}
+ @Bean
+ BearerTokenAuthenticationFilter bearerTokenAuthenticationFilter() {
+ return new BearerTokenAuthenticationFilter(userService, keystoreManager.getKeyPair());
+ }
@Bean
RememberMeServices hashCookieServices() {
@@ -124,6 +131,7 @@ public class SecurityConfig {
.addFilterBefore(apiAuthenticationFilter(), BasicAuthenticationFilter.class)
.addFilterBefore(new HTTPSignatureAuthenticationFilter(signatureManager, userService),
BasicAuthenticationFilter.class)
+ .addFilterBefore(bearerTokenAuthenticationFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests(requests -> requests
.requestMatchers(HttpMethod.OPTIONS).permitAll()
.requestMatchers("/api/", "/api/messages", "/api/avatar",
@@ -141,12 +149,12 @@ public class SecurityConfig {
.anonymous(anonymous -> anonymous.principal(JuickUser.ANONYMOUS_USER)
.authorities(JuickUser.ANONYMOUS_AUTHORITY))
.httpBasic(httpBasic -> httpBasic
- .authenticationEntryPoint(juickAuthenticationEntryPoint()))
+ .authenticationEntryPoint(apiAuthenticationEntryPoint()))
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(sessionManagement -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exceptionHandling -> exceptionHandling
- .authenticationEntryPoint(juickAuthenticationEntryPoint()))
+ .authenticationEntryPoint(apiAuthenticationEntryPoint()))
.csrf().disable()
.headers().defaultsDisabled().cacheControl();
return http.build();
@@ -170,7 +178,7 @@ public class SecurityConfig {
.sessionManagement(sessionManagement -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exceptionHandling -> exceptionHandling
- .authenticationEntryPoint(juickAuthenticationEntryPoint()))
+ .authenticationEntryPoint(apiAuthenticationEntryPoint()))
.headers().defaultsDisabled().cacheControl();
return http.build();
}
diff --git a/src/main/java/com/juick/service/security/BaseAuthenticationFilter.java b/src/main/java/com/juick/service/security/BaseAuthenticationFilter.java
new file mode 100644
index 00000000..7bfc39b2
--- /dev/null
+++ b/src/main/java/com/juick/service/security/BaseAuthenticationFilter.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2008-2023, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.service.security;
+
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+public abstract class BaseAuthenticationFilter extends OncePerRequestFilter {
+ boolean authenticationIsRequired() {
+ Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
+
+ return existingAuth == null ||
+ !existingAuth.isAuthenticated() ||
+ existingAuth instanceof AnonymousAuthenticationToken;
+ }
+}
diff --git a/src/main/java/com/juick/service/security/BearerTokenAuthenticationFilter.java b/src/main/java/com/juick/service/security/BearerTokenAuthenticationFilter.java
new file mode 100644
index 00000000..2e96a594
--- /dev/null
+++ b/src/main/java/com/juick/service/security/BearerTokenAuthenticationFilter.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2008-2023, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.service.security;
+
+import com.juick.service.UserService;
+import com.juick.service.security.entities.JuickUser;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.JwtParser;
+import io.jsonwebtoken.Jwts;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.security.KeyPair;
+import java.util.Collections;
+import java.util.stream.Collectors;
+
+public class BearerTokenAuthenticationFilter extends BaseAuthenticationFilter {
+ private static final Logger logger = LoggerFactory.getLogger("Auth");
+ private final JwtParser jwtParser;
+ private final UserService userService;
+
+ public BearerTokenAuthenticationFilter(UserService userService, KeyPair keys) {
+ this.userService = userService;
+ this.jwtParser = Jwts.parserBuilder()
+ .setSigningKey(keys.getPrivate())
+ .build();
+ }
+
+ @Override
+ protected void doFilterInternal(@Nonnull HttpServletRequest request,
+ @Nonnull HttpServletResponse response,
+ @Nonnull FilterChain filterChain) throws ServletException, IOException {
+ if (authenticationIsRequired()) {
+ var headers = Collections.list(request.getHeaderNames())
+ .stream()
+ .collect(Collectors.toMap(String::toLowerCase, request::getHeader));
+ var authorizationHeaderValue = headers.get("authorization");
+ if (StringUtils.isNotEmpty(authorizationHeaderValue) && authorizationHeaderValue.startsWith("Bearer")) {
+ String token = authorizationHeaderValue.substring(7);
+ try {
+ var claims = jwtParser.parseClaimsJws(token).getBody();
+ var user = userService.getUserByName(claims.getSubject());
+ if (!user.isAnonymous()) {
+ Authentication authentication = new UsernamePasswordAuthenticationToken(
+ new JuickUser(user),
+ user.getCredentials(),
+ JuickUser.USER_AUTHORITY);
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ }
+ } catch (Exception e) {
+ logger.warn("Invalid Bearer token: {}", e.getMessage());
+ }
+ }
+ }
+ filterChain.doFilter(request, response);
+ }
+}
diff --git a/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java b/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java
index 92e26406..5f6a730e 100644
--- a/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java
+++ b/src/main/java/com/juick/service/security/HTTPSignatureAuthenticationFilter.java
@@ -17,21 +17,19 @@
package com.juick.service.security;
-import com.juick.model.User;
import com.juick.SignatureManager;
+import com.juick.model.User;
import com.juick.service.UserService;
import com.juick.service.security.entities.JuickUser;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.web.filter.OncePerRequestFilter;
-
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
import javax.annotation.Nonnull;
import java.io.IOException;
@@ -39,7 +37,7 @@ import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
-public class HTTPSignatureAuthenticationFilter extends OncePerRequestFilter {
+public class HTTPSignatureAuthenticationFilter extends BaseAuthenticationFilter {
private final SignatureManager signatureManager;
private final UserService userService;
@@ -69,6 +67,7 @@ public class HTTPSignatureAuthenticationFilter extends OncePerRequestFilter {
new JuickUser(user), userWithPassword.getCredentials(), JuickUser.USER_AUTHORITY);
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
+ // anonymous must have with uri
Authentication authentication = new AnonymousAuthenticationToken(userUri,
new JuickUser(user), JuickUser.ANONYMOUS_AUTHORITY);
SecurityContextHolder.getContext().setAuthentication(authentication);
@@ -79,12 +78,4 @@ public class HTTPSignatureAuthenticationFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response);
}
-
- private boolean authenticationIsRequired() {
- Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
-
- return existingAuth == null ||
- !existingAuth.isAuthenticated() ||
- existingAuth instanceof AnonymousAuthenticationToken;
- }
}
diff --git a/src/main/java/com/juick/service/security/HashParamAuthenticationFilter.java b/src/main/java/com/juick/service/security/HashParamAuthenticationFilter.java
index 0f4ac66f..06f5edf4 100644
--- a/src/main/java/com/juick/service/security/HashParamAuthenticationFilter.java
+++ b/src/main/java/com/juick/service/security/HashParamAuthenticationFilter.java
@@ -20,10 +20,14 @@ package com.juick.service.security;
import com.juick.model.User;
import com.juick.service.UserService;
import com.juick.service.security.entities.JuickUser;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
-import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.RememberMeAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
@@ -31,20 +35,14 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import org.springframework.util.Assert;
-import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.Cookie;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Created by aalexeev on 4/5/17.
*/
-public class HashParamAuthenticationFilter extends OncePerRequestFilter {
+public class HashParamAuthenticationFilter extends BaseAuthenticationFilter {
public static final String PARAM_NAME = "hash";
@@ -85,7 +83,6 @@ public class HashParamAuthenticationFilter extends OncePerRequestFilter {
userWithPassword.getCredentials(),
JuickUser.USER_AUTHORITY);
SecurityContextHolder.getContext().setAuthentication(authentication);
-
}
}
}
@@ -93,14 +90,6 @@ public class HashParamAuthenticationFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response);
}
- private boolean authenticationIsRequired() {
- Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
-
- return existingAuth == null ||
- !existingAuth.isAuthenticated() ||
- existingAuth instanceof AnonymousAuthenticationToken;
- }
-
private String hashFromAuthorizationHeader(HttpServletRequest request) {
String authorizationHeader = request.getHeader("Authorization");
if (StringUtils.isNotEmpty(authorizationHeader)) {