Skip to content

Commit 9072765

Browse files
author
Adnane Miliari
committed
🔐implement API key authorization with caching and circuit breaker in gateway
1 parent b6bd0fa commit 9072765

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+896
-107
lines changed

apiKey-manager/pom.xml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
<parent>
5+
<groupId>dev.nano</groupId>
6+
<artifactId>demo-microservices</artifactId>
7+
<version>1.0-SNAPSHOT</version>
8+
</parent>
9+
10+
<artifactId>apiKey-manager</artifactId>
11+
12+
<properties>
13+
<maven.compiler.source>17</maven.compiler.source>
14+
<maven.compiler.target>17</maven.compiler.target>
15+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
16+
</properties>
17+
18+
<dependencies>
19+
<dependency>
20+
<groupId>org.springframework.boot</groupId>
21+
<artifactId>spring-boot-starter-web</artifactId>
22+
</dependency>
23+
<dependency>
24+
<groupId>org.springframework.boot</groupId>
25+
<artifactId>spring-boot-starter-data-jpa</artifactId>
26+
</dependency>
27+
<dependency>
28+
<groupId>org.springframework.boot</groupId>
29+
<artifactId>spring-boot-starter-validation</artifactId>
30+
</dependency>
31+
<dependency>
32+
<groupId>org.postgresql</groupId>
33+
<artifactId>postgresql</artifactId>
34+
<scope>runtime</scope>
35+
</dependency>
36+
<dependency>
37+
<groupId>dev.nano</groupId>
38+
<artifactId>common</artifactId>
39+
<version>1.0-SNAPSHOT</version>
40+
</dependency>
41+
</dependencies>
42+
43+
<build>
44+
<plugins>
45+
<plugin>
46+
<groupId>org.springframework.boot</groupId>
47+
<artifactId>spring-boot-maven-plugin</artifactId>
48+
</plugin>
49+
<plugin>
50+
<groupId>org.apache.maven.plugins</groupId>
51+
<artifactId>maven-compiler-plugin</artifactId>
52+
<version>${maven.compiler.version}</version>
53+
<configuration>
54+
<source>${maven.compiler.source}</source>
55+
<target>${maven.compiler.target}</target>
56+
<annotationProcessorPaths>
57+
<path>
58+
<groupId>org.projectlombok</groupId>
59+
<artifactId>lombok</artifactId>
60+
<version>${lombok.version}</version>
61+
</path>
62+
</annotationProcessorPaths>
63+
</configuration>
64+
</plugin>
65+
</plugins>
66+
</build>
67+
</project>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package dev.nano;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class ApiKeyManagerApplication {
8+
public static void main(String[] args) {
9+
SpringApplication.run(ApiKeyManagerApplication.class, args);
10+
}
11+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package dev.nano.apikey;
2+
3+
public class ApiKeyConstant {
4+
public static final String API_KEY_URI_REST_API = "/api/v1/apiKey-manager/api-keys";
5+
public static final String API_KEY_NOT_FOUND = "Api key not found";
6+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package dev.nano.apikey;
2+
3+
import dev.nano.application.ApplicationEntity;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
import lombok.experimental.SuperBuilder;
8+
9+
import jakarta.persistence.*;
10+
import java.time.LocalDateTime;
11+
import java.util.List;
12+
13+
@Entity
14+
@Table(name = "api_keys")
15+
@Data @SuperBuilder
16+
@NoArgsConstructor @AllArgsConstructor
17+
public class ApiKeyEntity {
18+
@Id
19+
@SequenceGenerator(
20+
name = "customer_sequence",
21+
sequenceName = "customer_sequence",
22+
allocationSize = 1
23+
)
24+
@GeneratedValue(
25+
strategy = GenerationType.SEQUENCE,
26+
generator = "customer_sequence"
27+
)
28+
@Column(
29+
name = "id",
30+
updatable = false
31+
)
32+
private Long id;
33+
34+
@Column(unique = true, nullable = false)
35+
private String key;
36+
37+
@Column(nullable = false, unique = true)
38+
private String client;
39+
40+
private String description;
41+
42+
private LocalDateTime createdDate;
43+
44+
private LocalDateTime expirationDate;
45+
46+
private boolean enabled;
47+
48+
private boolean neverExpires;
49+
50+
private boolean approved;
51+
52+
private boolean revoked;
53+
54+
@OneToMany(mappedBy = "apiKey")
55+
private List<ApplicationEntity> applications;
56+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package dev.nano.apikey;
2+
3+
import dev.nano.application.ApplicationName;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Query;
6+
7+
import java.util.Optional;
8+
9+
public interface ApiKeyRepository extends JpaRepository<ApiKeyEntity, Long> {
10+
@Query("""
11+
SELECT ak FROM ApiKeyEntity ak
12+
INNER JOIN ApplicationEntity ap
13+
ON ak.id = ap.apiKey.id
14+
WHERE ak.key = :key
15+
AND ap.applicationName = :appName
16+
""")
17+
Optional<ApiKeyEntity> findByKeyAndApplicationName(String key, ApplicationName applicationName);
18+
19+
@Query("""
20+
SELECT
21+
CASE WHEN COUNT(ak) > 0
22+
THEN TRUE
23+
ELSE FALSE
24+
END
25+
FROM ApiKeyEntity ak
26+
WHERE ak.key = :key
27+
""")
28+
boolean doesKeyExists(String key);
29+
30+
@Query("SELECT ak FROM ApiKeyEntity ak WHERE ak.key = :key")
31+
Optional<ApiKeyEntity> findByKey(String key);
32+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package dev.nano.apikey;
2+
3+
import dev.nano.application.ApplicationName;
4+
5+
import java.util.List;
6+
7+
public record ApiKeyRequest(
8+
String client,
9+
String description,
10+
List<ApplicationName> applications
11+
) {
12+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package dev.nano.apikey;
2+
3+
import dev.nano.application.ApplicationName;
4+
5+
public interface ApiKeyService {
6+
String save(ApiKeyRequest apiKeyRequest);
7+
void revokeApi(String key);
8+
boolean isAuthorized(String apiKey, ApplicationName applicationName);
9+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package dev.nano.apikey;
2+
3+
import dev.nano.application.ApplicationEntity;
4+
import dev.nano.application.ApplicationName;
5+
import dev.nano.application.ApplicationRepository;
6+
import exceptionhandler.core.ResourceNotFoundException;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.stereotype.Service;
9+
10+
import java.time.LocalDateTime;
11+
import java.util.List;
12+
import java.util.Optional;
13+
import java.util.Set;
14+
import java.util.stream.Collectors;
15+
16+
import static dev.nano.apikey.ApiKeyConstant.API_KEY_NOT_FOUND;
17+
18+
19+
@Service
20+
@RequiredArgsConstructor
21+
public class ApiKeyServiceImpl implements ApiKeyService {
22+
private static final Integer EXPIRATION_DAYS = 30;
23+
private final ApiKeyRepository apiKeyRepository;
24+
private final ApplicationRepository applicationRepository;
25+
private final KeyGenerator keyGenerator;
26+
27+
@Override
28+
public String save(ApiKeyRequest apiKeyRequest) {
29+
ApiKeyEntity apiKey = new ApiKeyEntity();
30+
31+
apiKey.setClient(apiKeyRequest.client());
32+
apiKey.setDescription(apiKeyRequest.description());
33+
34+
String apiKeyValue = keyGenerator.generateKey();
35+
apiKey.setKey(apiKeyValue);
36+
37+
apiKey.setApproved(true);
38+
apiKey.setEnabled(true);
39+
apiKey.setNeverExpires(false);
40+
apiKey.setCreatedDate(LocalDateTime.now());
41+
apiKey.setExpirationDate(LocalDateTime.now().plusDays(EXPIRATION_DAYS));
42+
43+
ApiKeyEntity savedApiKeyEntity = apiKeyRepository.save(apiKey);
44+
45+
Set<ApplicationEntity> applications = Optional.ofNullable(apiKeyRequest.applications())
46+
.orElse(List.of())
47+
.stream().map(app -> ApplicationEntity.builder()
48+
.applicationName(app)
49+
.apiKey(savedApiKeyEntity)
50+
.revoked(false)
51+
.enabled(true)
52+
.build())
53+
.collect(Collectors.toUnmodifiableSet());
54+
55+
applicationRepository.saveAll(applications);
56+
57+
return apiKeyValue;
58+
}
59+
60+
@Override
61+
public void revokeApi(String key) {
62+
ApiKeyEntity apiKey = apiKeyRepository.findByKey(key).orElseThrow(
63+
() -> new ResourceNotFoundException(API_KEY_NOT_FOUND));
64+
65+
apiKey.setRevoked(true);
66+
apiKey.setEnabled(false);
67+
apiKey.setApproved(false);
68+
apiKeyRepository.save(apiKey);
69+
70+
// revoke all applications associated with this api key
71+
apiKey.getApplications().forEach(app -> {
72+
app.setRevoked(true);
73+
app.setEnabled(false);
74+
app.setApproved(false);
75+
applicationRepository.save(app);
76+
});
77+
}
78+
79+
@Override
80+
public boolean isAuthorized(String apiKey, ApplicationName applicationName) {
81+
Optional<ApiKeyEntity> optionalApiKey = apiKeyRepository.findByKeyAndApplicationName(apiKey, applicationName);
82+
83+
if(optionalApiKey.isEmpty()) {
84+
return false;
85+
}
86+
87+
ApiKeyEntity apiKeyEntity = optionalApiKey.get();
88+
89+
return apiKeyEntity.getApplications()
90+
.stream()
91+
.filter(app -> app.getApplicationName().equals(applicationName))
92+
.findFirst()
93+
.map(app -> app.isEnabled() &&
94+
app.isApproved() &&
95+
!app.isRevoked() &&
96+
apiKeyEntity.isEnabled() &&
97+
apiKeyEntity.isApproved() &&
98+
!apiKeyEntity.isRevoked() &&
99+
(apiKeyEntity.isNeverExpires() || LocalDateTime.now().isBefore(apiKeyEntity.getExpirationDate())) // isAfter used to check if the expiration date is in the future
100+
)
101+
.orElse(false);
102+
103+
}
104+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package dev.nano.apikey;
2+
3+
public interface KeyGenerator {
4+
String generateKey();
5+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package dev.nano.apikey;
2+
3+
import org.springframework.stereotype.Component;
4+
5+
import java.util.UUID;
6+
7+
@Component
8+
public class UUIDKeyGeneratorImpl implements KeyGenerator {
9+
@Override
10+
public String generateKey() {
11+
return UUID.randomUUID().toString();
12+
}
13+
}

0 commit comments

Comments
 (0)