From 15e8e245c3bd43afeeabe6b65c4a5f55dc677b2e Mon Sep 17 00:00:00 2001 From: Abdul Qadir Date: Sat, 21 Oct 2023 20:28:38 +0500 Subject: [PATCH 1/2] Add API key management APIs --- .../lowcoder/domain/user/model/APIKey.java | 33 ++++++++++ .../org/lowcoder/domain/user/model/User.java | 37 ++++++++++-- .../plugin/sql/SqlBasedConnector.java | 6 +- .../lowcoder/sdk/config/AuthProperties.java | 7 +++ server/api-service/lowcoder-server/pom.xml | 18 ++++++ .../AuthenticationController.java | 23 +++++++ .../api/authentication/dto/APIKeyRequest.java | 27 +++++++++ .../service/AuthenticationApiService.java | 9 +++ .../service/AuthenticationApiServiceImpl.java | 57 ++++++++++++++++-- .../api/authentication/util/JWTUtils.java | 60 +++++++++++++++++++ .../api/usermanagement/view/APIKeyVO.java | 18 ++++++ .../main/resources/application-lowcoder.yml | 2 + 12 files changed, 285 insertions(+), 12 deletions(-) create mode 100644 server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/APIKey.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/dto/APIKeyRequest.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/util/JWTUtils.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/view/APIKeyVO.java diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/APIKey.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/APIKey.java new file mode 100644 index 000000000..dffbdb8c8 --- /dev/null +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/APIKey.java @@ -0,0 +1,33 @@ +package org.lowcoder.domain.user.model; + +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nullable; +import java.util.function.Function; + +@Getter +@Setter +public class APIKey { + + private String id; + private String name; + private String description; + private String token; + + public APIKey(@Nullable String id, String name, String description, String token) { + this.id = id; + this.name = name; + this.description = description; + this.token = token; + } + + public void doEncrypt(Function encryptFunc) { + this.token = encryptFunc.apply(token); + } + + public void doDecrypt(Function decryptFunc) { + this.token = decryptFunc.apply(token); + } + +} diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java index 484b4353b..507fa05e2 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java @@ -3,15 +3,18 @@ import static com.google.common.base.Suppliers.memoize; import static org.lowcoder.infra.util.AssetUtils.toAssetPath; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.function.Supplier; +import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.collections4.SetUtils; import org.apache.commons.lang3.StringUtils; +import org.lowcoder.domain.mongodb.AfterMongodbRead; +import org.lowcoder.domain.mongodb.BeforeMongodbWrite; +import org.lowcoder.domain.mongodb.MongodbInterceptorContext; +import org.lowcoder.sdk.config.SerializeConfig; import org.lowcoder.sdk.models.HasIdAndAuditing; +import org.lowcoder.sdk.util.JsonUtils; import org.springframework.data.annotation.Transient; import org.springframework.data.mongodb.core.mapping.Document; @@ -29,7 +32,7 @@ @ToString @Document @JsonIgnoreProperties(ignoreUnknown = true) -public class User extends HasIdAndAuditing { +public class User extends HasIdAndAuditing implements BeforeMongodbWrite, AfterMongodbRead { private static final OrgTransformedUserInfo EMPTY_TRANSFORMED_USER_INFO = new OrgTransformedUserInfo(); @@ -52,6 +55,16 @@ public class User extends HasIdAndAuditing { private Set connections; + @Setter + @Getter + @Transient + private List apiKeysList = new ArrayList<>(); + + /** + * Only used for mongodb (de)serialization + */ + private List apiKeys = new ArrayList<>(); + @Transient @JsonIgnore private Supplier avatarUrl = memoize(() -> StringUtils.isNotBlank(avatar) ? toAssetPath(avatar) : tpAvatarLink); @@ -109,4 +122,18 @@ public void markAsDeleted() { .forEach(connection -> connection.setSource( connection.getSource() + "(User deleted at " + System.currentTimeMillis() / 1000 + ")")); } + + @Override + public void beforeMongodbWrite(MongodbInterceptorContext context) { + this.apiKeysList.forEach(apiKey -> apiKey.doEncrypt(s -> context.encryptionService().encryptString(s))); + apiKeys = JsonUtils.fromJsonSafely(JsonUtils.toJsonSafely(apiKeysList, SerializeConfig.JsonViews.Internal.class), new TypeReference<>() { + }, new ArrayList<>()); + } + + @Override + public void afterMongodbRead(MongodbInterceptorContext context) { + this.apiKeysList = JsonUtils.fromJsonSafely(JsonUtils.toJson(apiKeys), new TypeReference<>() { + }, new ArrayList<>()); + this.apiKeysList.forEach(authConfig -> authConfig.doDecrypt(s -> context.encryptionService().decryptString(s))); + } } diff --git a/server/api-service/lowcoder-plugins/sqlBasedPlugin/src/main/java/org/lowcoder/plugin/sql/SqlBasedConnector.java b/server/api-service/lowcoder-plugins/sqlBasedPlugin/src/main/java/org/lowcoder/plugin/sql/SqlBasedConnector.java index 54e444ff4..3bd3c79cc 100644 --- a/server/api-service/lowcoder-plugins/sqlBasedPlugin/src/main/java/org/lowcoder/plugin/sql/SqlBasedConnector.java +++ b/server/api-service/lowcoder-plugins/sqlBasedPlugin/src/main/java/org/lowcoder/plugin/sql/SqlBasedConnector.java @@ -107,9 +107,9 @@ public Set validateConfig(T connectionConfig) { invalids.add("HOST_WITH_COLON"); } - if (StringUtils.equalsIgnoreCase(host, "localhost") || StringUtils.equals(host, "127.0.0.1")) { - invalids.add("INVALID_HOST"); - } +// if (StringUtils.equalsIgnoreCase(host, "localhost") || StringUtils.equals(host, "127.0.0.1")) { +// invalids.add("INVALID_HOST"); +// } if (StringUtils.isBlank(connectionConfig.getDatabase())) { invalids.add("DATABASE_NAME_EMPTY"); diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/AuthProperties.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/AuthProperties.java index 234a965a1..178571bea 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/AuthProperties.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/AuthProperties.java @@ -27,6 +27,7 @@ public class AuthProperties { private Email email = new Email(); private Oauth2Simple google = new Oauth2Simple(); private Oauth2Simple github = new Oauth2Simple(); + private ApiKey apiKey = new ApiKey(); @Getter @Setter @@ -53,6 +54,12 @@ public static class Oauth2Simple extends AuthWay { private String clientSecret; } + @Setter + @Getter + public static class ApiKey { + private String secret; + } + /** * For saas mode, such as app.lowcoder.cloud */ diff --git a/server/api-service/lowcoder-server/pom.xml b/server/api-service/lowcoder-server/pom.xml index e43766de4..cd2d2ed86 100644 --- a/server/api-service/lowcoder-server/pom.xml +++ b/server/api-service/lowcoder-server/pom.xml @@ -184,6 +184,24 @@ 5.9.3 test + + io.jsonwebtoken + jjwt-api + 0.11.5 + compile + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + compile + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/AuthenticationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/AuthenticationController.java index 8f6a2cfcd..d4f36164f 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/AuthenticationController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/AuthenticationController.java @@ -2,14 +2,17 @@ import java.util.List; +import org.lowcoder.api.authentication.dto.APIKeyRequest; import org.lowcoder.api.authentication.dto.AuthConfigRequest; import org.lowcoder.api.authentication.service.AuthenticationApiService; import org.lowcoder.api.framework.view.ResponseView; import org.lowcoder.api.home.SessionUserService; import org.lowcoder.api.usermanagement.UserController; import org.lowcoder.api.usermanagement.UserController.UpdatePasswordRequest; +import org.lowcoder.api.usermanagement.view.APIKeyVO; import org.lowcoder.api.util.BusinessEventPublisher; import org.lowcoder.domain.authentication.FindAuthConfig; +import org.lowcoder.domain.user.model.APIKey; import org.lowcoder.infra.constant.NewUrl; import org.lowcoder.sdk.auth.AbstractAuthConfig; import org.lowcoder.sdk.config.SerializeConfig.JsonViews; @@ -104,6 +107,26 @@ public Mono>> getAllConfigs() { .map(ResponseView::success); } + // ----------- API Key Management ---------------- + @PostMapping("/api-key") + public Mono> createAPIKey(@RequestBody APIKeyRequest apiKeyRequest) { + return authenticationApiService.createAPIKey(apiKeyRequest) + .map(ResponseView::success); + } + + @DeleteMapping("/api-key/{id}") + public Mono> deleteAPIKey(@PathVariable("id") String id) { + return authenticationApiService.deleteAPIKey(id) + .thenReturn(ResponseView.success(null)); + } + + @GetMapping("/api-keys") + public Mono>> getAllAPIKeys() { + return authenticationApiService.findAPIKeys() + .collectList() + .map(ResponseView::success); + } + /** * @param loginId phone number or email for now. * @param register register or login diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/dto/APIKeyRequest.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/dto/APIKeyRequest.java new file mode 100644 index 000000000..fd65c0d9c --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/dto/APIKeyRequest.java @@ -0,0 +1,27 @@ +package org.lowcoder.api.authentication.dto; + +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.ObjectUtils; + +import java.util.HashMap; + +import static org.lowcoder.sdk.util.IDUtils.generate; + +public class APIKeyRequest extends HashMap { + + public String getId() { + return ObjectUtils.firstNonNull(getString("id"), generate()); + } + + public String getName() { + return getString("name"); + } + + public String getDescription() { + return getString("description"); + } + + public String getString(String key) { + return MapUtils.getString(this, key); + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiService.java index 68ac9f691..e6ae37bd9 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiService.java @@ -1,7 +1,10 @@ package org.lowcoder.api.authentication.service; +import org.lowcoder.api.authentication.dto.APIKeyRequest; import org.lowcoder.api.authentication.dto.AuthConfigRequest; +import org.lowcoder.api.usermanagement.view.APIKeyVO; import org.lowcoder.domain.authentication.FindAuthConfig; +import org.lowcoder.domain.user.model.APIKey; import org.lowcoder.domain.user.model.AuthUser; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; @@ -20,4 +23,10 @@ public interface AuthenticationApiService { Mono disableAuthConfig(String authId, boolean delete); Flux findAuthConfigs(boolean enableOnly); + + Mono createAPIKey(APIKeyRequest apiKeyRequest); + + Mono deleteAPIKey(String authId); + + Flux findAPIKeys(); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java index 2851e1070..28b593126 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java @@ -3,15 +3,19 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.lowcoder.api.authentication.dto.APIKeyRequest; import org.lowcoder.api.authentication.dto.AuthConfigRequest; import org.lowcoder.api.authentication.request.AuthRequestFactory; import org.lowcoder.api.authentication.request.oauth2.OAuth2RequestContext; import org.lowcoder.api.authentication.service.factory.AuthConfigFactory; import org.lowcoder.api.authentication.util.AuthenticationUtils; +import org.lowcoder.api.authentication.util.JWTUtils; import org.lowcoder.api.home.SessionUserService; import org.lowcoder.api.usermanagement.InvitationApiService; import org.lowcoder.api.usermanagement.OrgApiService; import org.lowcoder.api.usermanagement.UserApiService; +import org.lowcoder.api.usermanagement.view.APIKeyVO; import org.lowcoder.api.util.BusinessEventPublisher; import org.lowcoder.domain.authentication.AuthenticationService; import org.lowcoder.domain.authentication.FindAuthConfig; @@ -22,10 +26,7 @@ import org.lowcoder.domain.organization.model.OrganizationDomain; import org.lowcoder.domain.organization.service.OrgMemberService; import org.lowcoder.domain.organization.service.OrganizationService; -import org.lowcoder.domain.user.model.AuthUser; -import org.lowcoder.domain.user.model.Connection; -import org.lowcoder.domain.user.model.ConnectionAuthToken; -import org.lowcoder.domain.user.model.User; +import org.lowcoder.domain.user.model.*; import org.lowcoder.domain.user.service.UserService; import org.lowcoder.sdk.auth.AbstractAuthConfig; import org.lowcoder.sdk.exception.BizError; @@ -81,6 +82,9 @@ public class AuthenticationApiServiceImpl implements AuthenticationApiService { @Autowired private OrgMemberService orgMemberService; + @Autowired + private JWTUtils jwtUtils; + @Override public Mono authenticateByForm(String loginId, String password, String source, boolean register, String authId) { return authenticate(authId, source, new FormAuthRequestContext(loginId, password, register)); @@ -262,6 +266,51 @@ public Flux findAuthConfigs(boolean enableOnly) { .flatMapMany(orgMember -> authenticationService.findAllAuthConfigs(orgMember.getOrgId(),false)); } + @Override + public Mono createAPIKey(APIKeyRequest apiKeyRequest) { + return sessionUserService.getVisitor() + .map(user -> { + String token = jwtUtils.createToken(user); + APIKey apiKey = new APIKey(apiKeyRequest.getId(), apiKeyRequest.getName(), apiKeyRequest.getDescription(), token); + addAPIKey(user, apiKey); + return Pair.of(token, user); + }) + .flatMap(pair -> userService.update(pair.getRight().getId(), pair.getRight()).thenReturn(pair.getKey())) + .map(APIKeyVO::from); + } + + private void addAPIKey(User user, APIKey newApiKey) { + Map apiKeyMap = user.getApiKeysList() + .stream() + .collect(Collectors.toMap(APIKey::getId, Function.identity())); + apiKeyMap.put(newApiKey.getId(), newApiKey); + user.setApiKeysList(new ArrayList<>(apiKeyMap.values())); + } + + @Override + public Mono deleteAPIKey(String apiKeyId) { + return sessionUserService.getVisitor() + .doOnNext(user -> deleteAPIKey(user, apiKeyId)) + .flatMap(user -> userService.update(user.getId(), user)) + .then(); + } + + private void deleteAPIKey(User user, String apiKeyId) { + List apiKeys = Optional.of(user) + .map(User::getApiKeysList) + .orElse(Collections.emptyList()); + apiKeys.removeIf(apiKey -> Objects.equals(apiKey.getId(), apiKeyId)); + user.setApiKeysList(apiKeys); + } + + @Override + public Flux findAPIKeys() { + return sessionUserService.getVisitor() + .flatMapIterable(user -> + new ArrayList<>(user.getApiKeysList()) + ); + } + private Mono removeTokensByAuthId(String authId) { return sessionUserService.getVisitorOrgMemberCache() diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/util/JWTUtils.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/util/JWTUtils.java new file mode 100644 index 000000000..3b9d0026f --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/util/JWTUtils.java @@ -0,0 +1,60 @@ +package org.lowcoder.api.authentication.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import org.lowcoder.domain.user.model.User; +import org.lowcoder.sdk.config.AuthProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import java.util.Random; + +import java.util.Date; + +@Component +public class JWTUtils { + + @Autowired + private AuthProperties authProperties; + + private JwtParser jwtParser; + + private final String TOKEN_HEADER = "Authorization"; + private final String TOKEN_PREFIX = "Bearer "; + + @PostConstruct + public void setup(){ + this.jwtParser = Jwts.parser().setSigningKey(authProperties.getApiKey().getSecret()); + } + + public String createToken(User user) { + Claims claims = Jwts.claims() + .setSubject(user.getId()) + .setIssuedAt(new Date()); + claims.put("userId", user.getId() ); + claims.put("createdBy", user.getName()); + String randomFactor = String.valueOf(new Random().nextLong(100000000L)); + return Jwts.builder() + .setClaims(claims) + .signWith(SignatureAlgorithm.HS256, authProperties.getApiKey().getSecret() + randomFactor) + .compact(); + } + + private Claims parseJwtClaims(String token) { + return jwtParser.parseClaimsJws(token).getBody(); + } + + public String resolveToken(HttpServletRequest request) { + + String bearerToken = request.getHeader(TOKEN_HEADER); + if (bearerToken != null && bearerToken.startsWith(TOKEN_PREFIX)) { + return bearerToken.substring(TOKEN_PREFIX.length()); + } + return null; + } + + +} \ No newline at end of file diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/view/APIKeyVO.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/view/APIKeyVO.java new file mode 100644 index 000000000..40bb1c3e4 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/view/APIKeyVO.java @@ -0,0 +1,18 @@ +package org.lowcoder.api.usermanagement.view; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class APIKeyVO { + + private final String token; + + public static APIKeyVO from(String token) { + return APIKeyVO.builder() + .token(token) + .build(); + } + +} diff --git a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml index 3488bd1c0..223e127ce 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml +++ b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml @@ -56,6 +56,8 @@ springdoc: paths-to-exclude: /api/v1/** auth: + api-key: + secret: 123456789101112131415123456789101112131415123456789101112131415123456789101112131415 email: enable: true enable-register: false \ No newline at end of file From 71d21b29d82016484b91535bc850ba153f892510 Mon Sep 17 00:00:00 2001 From: Abdul Qadir Date: Sun, 22 Oct 2023 16:13:59 +0500 Subject: [PATCH 2/2] Add auth layer handling for API Key --- .../org/lowcoder/sdk/util/CookieHelper.java | 5 -- .../api/authentication/util/JWTUtils.java | 18 +++++-- .../framework/filter/APIKeyAuthFilter.java | 54 +++++++++++++++++++ .../framework/security/SecurityConfig.java | 6 +++ .../lowcoder/api/home/SessionUserService.java | 3 ++ .../api/home/SessionUserServiceImpl.java | 12 +++++ 6 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/APIKeyAuthFilter.java diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/util/CookieHelper.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/util/CookieHelper.java index 374c66d12..595a2c193 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/util/CookieHelper.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/util/CookieHelper.java @@ -53,11 +53,6 @@ public String getCookieToken(ServerWebExchange exchange) { return getCookieValue(exchange, getCookieName(), ""); } - @Nullable - public String getJWT(ServerWebExchange exchange) { - return getCookieValue(exchange, "JWT", null); - } - public String getCookieValue(ServerWebExchange exchange, String cookieName, String defaultValue) { MultiValueMap cookies = exchange.getRequest().getCookies(); return ofNullable(cookies.getFirst(cookieName)) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/util/JWTUtils.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/util/JWTUtils.java index 3b9d0026f..c5c746b79 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/util/JWTUtils.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/util/JWTUtils.java @@ -5,16 +5,19 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import jakarta.annotation.PostConstruct; -import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; import org.lowcoder.domain.user.model.User; import org.lowcoder.sdk.config.AuthProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; + import java.util.Random; import java.util.Date; @Component +@Slf4j(topic = "JWTUtils") public class JWTUtils { @Autowired @@ -43,13 +46,18 @@ public String createToken(User user) { .compact(); } - private Claims parseJwtClaims(String token) { - return jwtParser.parseClaimsJws(token).getBody(); + public Claims parseJwtClaims(String token) { + try { + return jwtParser.parseClaimsJws(token).getBody(); + } catch (Exception e) { + log.warn("Failed to validate token. Exception: ", e); + return null; + } } - public String resolveToken(HttpServletRequest request) { + public String resolveToken(ServerWebExchange exchange) { - String bearerToken = request.getHeader(TOKEN_HEADER); + String bearerToken = exchange.getRequest().getHeaders().getFirst(TOKEN_HEADER); if (bearerToken != null && bearerToken.startsWith(TOKEN_PREFIX)) { return bearerToken.substring(TOKEN_PREFIX.length()); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/APIKeyAuthFilter.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/APIKeyAuthFilter.java new file mode 100644 index 000000000..79a0ea663 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/APIKeyAuthFilter.java @@ -0,0 +1,54 @@ +package org.lowcoder.api.framework.filter; + +import io.jsonwebtoken.Claims; +import lombok.extern.slf4j.Slf4j; +import org.lowcoder.api.authentication.util.JWTUtils; +import org.lowcoder.api.home.SessionUserService; +import org.lowcoder.sdk.util.CookieHelper; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +import javax.annotation.Nonnull; + +import static org.lowcoder.api.authentication.util.AuthenticationUtils.toAuthentication; +import static org.springframework.security.core.context.ReactiveSecurityContextHolder.withAuthentication; + +@Slf4j +public class APIKeyAuthFilter implements WebFilter { + + private final SessionUserService service; + + private final CookieHelper cookieHelper; + private final JWTUtils jwtUtils; + + public APIKeyAuthFilter(SessionUserService service, CookieHelper cookieHelper, JWTUtils jwtUtils) { + this.service = service; + this.cookieHelper = cookieHelper; + this.jwtUtils = jwtUtils; + } + + @Nonnull + @Override + public Mono filter(@Nonnull ServerWebExchange exchange, WebFilterChain chain) { + String cookieToken = cookieHelper.getCookieToken(exchange); + if(cookieToken.isEmpty()) { + String jwtToken = jwtUtils.resolveToken(exchange); + if(jwtToken == null || jwtToken.isEmpty()) { + return chain.filter(exchange); + } else { + Claims claims = jwtUtils.parseJwtClaims(jwtToken); + if(claims == null) { + return chain.filter(exchange); + } else { + return service.resolveSessionUserForJWT(claims, jwtToken) + .flatMap(user -> chain.filter(exchange).contextWrite(withAuthentication(toAuthentication(user)))); + } + + } + } else { + return chain.filter(exchange); + } + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java index 6f3f2f211..c57c3fabc 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java @@ -3,6 +3,8 @@ import org.lowcoder.api.authentication.request.AuthRequestFactory; import org.lowcoder.api.authentication.service.AuthenticationApiServiceImpl; +import org.lowcoder.api.authentication.util.JWTUtils; +import org.lowcoder.api.framework.filter.APIKeyAuthFilter; import org.lowcoder.api.framework.filter.UserSessionPersistenceFilter; import org.lowcoder.api.home.SessionUserService; import org.lowcoder.domain.authentication.AuthenticationService; @@ -66,6 +68,9 @@ public class SecurityConfig { @Autowired AuthRequestFactory authRequestFactory; + @Autowired + JWTUtils jwtUtils; + @Bean SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { @@ -149,6 +154,7 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { ); http.addFilterBefore(new UserSessionPersistenceFilter(sessionUserService, cookieHelper, authenticationService, authenticationApiService, authRequestFactory), SecurityWebFiltersOrder.AUTHENTICATION); + http.addFilterBefore(new APIKeyAuthFilter(sessionUserService, cookieHelper, jwtUtils), SecurityWebFiltersOrder.AUTHENTICATION); return http.build(); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java index ef2156617..9104839d9 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java @@ -1,5 +1,6 @@ package org.lowcoder.api.home; +import io.jsonwebtoken.Claims; import org.lowcoder.domain.organization.model.OrgMember; import org.lowcoder.domain.user.model.User; import org.lowcoder.infra.annotation.NonEmptyMono; @@ -29,5 +30,7 @@ public interface SessionUserService { Mono resolveSessionUserFromCookie(String token); + Mono resolveSessionUserForJWT(Claims claims, String token); + Mono tokenExist(String token); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java index 8101ad491..5c0b5e1fe 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java @@ -8,6 +8,7 @@ import java.time.Duration; import java.util.Objects; +import io.jsonwebtoken.Claims; import org.apache.commons.lang3.StringUtils; import org.lowcoder.api.usermanagement.UserApiService; import org.lowcoder.domain.organization.model.OrgMember; @@ -139,6 +140,17 @@ public Mono resolveSessionUserFromCookie(String token) { .filter(user -> user.getState() != UserState.DELETED); } + @Override + public Mono resolveSessionUserForJWT(Claims claims, String token) { + String userId = claims.get("userId").toString(); + return userService.findById(userId) + .filter(user -> user.getState() != UserState.DELETED) + .filter(user -> { + long apiKeyFound = user.getApiKeysList().stream().filter(apiKey -> apiKey.getToken().equals(token)).count(); + return apiKeyFound > 0; + }); + } + @Override public Mono tokenExist(String token) { return reactiveTemplate.hasKey(token);