Skip to content

Commit fe875b6

Browse files
author
Adnane Miliari
committed
🔑implement inter-service API key authentication with circuit breaker
1 parent 290a209 commit fe875b6

File tree

32 files changed

+340
-62
lines changed

32 files changed

+340
-62
lines changed

apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyEntity.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717
public class ApiKeyEntity {
1818
@Id
1919
@SequenceGenerator(
20-
name = "customer_sequence",
21-
sequenceName = "customer_sequence",
20+
name = "api_key_sequence",
21+
sequenceName = "api_key_sequence",
2222
allocationSize = 1
2323
)
2424
@GeneratedValue(
2525
strategy = GenerationType.SEQUENCE,
26-
generator = "customer_sequence"
26+
generator = "api_key_sequence"
2727
)
2828
@Column(
2929
name = "id",

apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyRepository.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import dev.nano.application.ApplicationName;
44
import org.springframework.data.jpa.repository.JpaRepository;
55
import org.springframework.data.jpa.repository.Query;
6+
import org.springframework.data.repository.query.Param;
67

78
import java.util.Optional;
89

@@ -12,9 +13,9 @@ public interface ApiKeyRepository extends JpaRepository<ApiKeyEntity, Long> {
1213
INNER JOIN ApplicationEntity ap
1314
ON ak.id = ap.apiKey.id
1415
WHERE ak.key = :key
15-
AND ap.applicationName = :appName
16+
AND ap.applicationName = :applicationName
1617
""")
17-
Optional<ApiKeyEntity> findByKeyAndApplicationName(String key, ApplicationName applicationName);
18+
Optional<ApiKeyEntity> findByKeyAndApplicationName(@Param("key") String key, @Param("applicationName") ApplicationName applicationName);
1819

1920
@Query("""
2021
SELECT
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package dev.nano.apikey;
22

33
import dev.nano.application.ApplicationName;
4+
import org.springframework.transaction.annotation.Transactional;
45

56
public interface ApiKeyService {
67
String save(ApiKeyRequest apiKeyRequest);
78
void revokeApi(String key);
9+
@Transactional(readOnly = true)
810
boolean isAuthorized(String apiKey, ApplicationName applicationName);
911
}

apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyServiceImpl.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import dev.nano.application.ApplicationRepository;
66
import dev.nano.exceptionhandler.core.ResourceNotFoundException;
77
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
89
import org.springframework.stereotype.Service;
10+
import org.springframework.transaction.annotation.Transactional;
911

1012
import java.time.LocalDateTime;
1113
import java.util.List;
@@ -18,6 +20,7 @@
1820

1921
@Service
2022
@RequiredArgsConstructor
23+
@Slf4j
2124
public class ApiKeyServiceImpl implements ApiKeyService {
2225
private static final Integer EXPIRATION_DAYS = 30;
2326
private final ApiKeyRepository apiKeyRepository;
@@ -77,10 +80,13 @@ public void revokeApi(String key) {
7780
}
7881

7982
@Override
83+
@Transactional(readOnly = true)
8084
public boolean isAuthorized(String apiKey, ApplicationName applicationName) {
85+
log.info("Attempting to authorize API Key: {} for Application: {}", apiKey, applicationName);
8186
Optional<ApiKeyEntity> optionalApiKey = apiKeyRepository.findByKeyAndApplicationName(apiKey, applicationName);
8287

8388
if(optionalApiKey.isEmpty()) {
89+
log.warn("API Key {} not found for application {}. Returning false.", apiKey, applicationName);
8490
return false;
8591
}
8692

apiKey-manager/src/main/java/dev/nano/application/ApplicationEntity.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
public class ApplicationEntity {
1616
@Id
1717
@SequenceGenerator(
18-
name = "customer_sequence",
19-
sequenceName = "customer_sequence",
18+
name = "application_sequence",
19+
sequenceName = "application_sequence",
2020
allocationSize = 1
2121
)
2222
@GeneratedValue(
2323
strategy = GenerationType.SEQUENCE,
24-
generator = "customer_sequence"
24+
generator = "application_sequence"
2525
)
2626
@Column(
2727
name = "id",
Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,41 @@
11
-- First insert API Keys and store their IDs
22
INSERT INTO api_keys (id, key, client, description, created_date, expiration_date, enabled, never_expires, approved,
33
revoked)
4-
SELECT nextval('api_key_sequence'), key, client, description, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + INTERVAL '365 days', enabled, never_expires, approved, revoked
4+
SELECT nextval('api_key_sequence'), t.key_value, t.client, t.description, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + INTERVAL '365 days', t.enabled, t.never_expires, t.approved, t.revoked
55
FROM (
6-
VALUES
7-
('ecom-frontend-key-2024', 'E-commerce Frontend', 'API Key for frontend application', true, false, true, false),
8-
('mobile-app-key-2024', 'Mobile Application', 'API Key for mobile app', true, false, true, false),
9-
('admin-dashboard-key-2024', 'Admin Dashboard', 'API Key for admin dashboard', true, true, true, false)
10-
) AS t(key, client, description, enabled, never_expires, approved, revoked);
6+
VALUES
7+
('ecom-frontend-key-2024', 'E-commerce Frontend', 'API Key for frontend application', true, false, true, false),
8+
('mobile-app-key-2024', 'Mobile Application', 'API Key for mobile app', true, false, true, false),
9+
('admin-dashboard-key-2024', 'Admin Dashboard', 'API Key for admin dashboard', true, true, true, false),
10+
-- Internal traffic key (never exposed publicly)
11+
('internal-service-key', 'Internal Service', 'API Key for inter-service calls', true, true, true, false)
12+
) AS t(key_value, client, description, enabled, never_expires, approved, revoked);
1113

12-
-- Then insert Applications using the actual API key IDs
14+
-- Then insert Applications using the actual API key IDs by looking up the key value
1315
INSERT INTO applications (id, application_name, enabled, approved, revoked, api_key_id)
1416
SELECT nextval('application_sequence'),
15-
app_name,
17+
t.app_name,
1618
true,
1719
true,
1820
false,
19-
ak.id
20-
FROM (VALUES ('CUSTOMER', 1), ('CUSTOMER', 2), ('CUSTOMER', 3),
21-
('PRODUCT', 1), ('PRODUCT', 2), ('PRODUCT', 3),
22-
('ORDER', 1), ('ORDER', 2), ('ORDER', 3),
23-
('PAYMENT', 1), ('PAYMENT', 3),
24-
('NOTIFICATION', 3)
25-
) AS t(app_name, key_order)
26-
JOIN api_keys ak ON ak.id = (SELECT id FROM api_keys ORDER BY id LIMIT 1 OFFSET (t.key_order - 1));
21+
(SELECT ak.id FROM api_keys ak WHERE ak.key = t.api_key_name)
22+
FROM (VALUES ('CUSTOMER', 'ecom-frontend-key-2024'),
23+
('CUSTOMER', 'mobile-app-key-2024'),
24+
('CUSTOMER', 'admin-dashboard-key-2024'),
25+
('PRODUCT', 'ecom-frontend-key-2024'),
26+
('PRODUCT', 'mobile-app-key-2024'),
27+
('PRODUCT', 'admin-dashboard-key-2024'),
28+
('ORDER', 'ecom-frontend-key-2024'),
29+
('ORDER', 'mobile-app-key-2024'),
30+
('ORDER', 'admin-dashboard-key-2024'),
31+
('PAYMENT', 'ecom-frontend-key-2024'),
32+
('PAYMENT', 'admin-dashboard-key-2024'),
33+
('NOTIFICATION', 'admin-dashboard-key-2024'),
34+
-- Grant the internal-service-key to every microservice that consumes internal APIs
35+
('CUSTOMER', 'internal-service-key'),
36+
('PRODUCT', 'internal-service-key'),
37+
('ORDER', 'internal-service-key'), -- This line specifically authorizes 'internal-service-key' for the 'ORDER' application
38+
('PAYMENT', 'internal-service-key'),
39+
('NOTIFICATION', 'internal-service-key'),
40+
('APIKEY_MANAGER', 'internal-service-key')
41+
) AS t(app_name, api_key_name);

common/src/main/resources/shared-application-default.yml

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ management:
1919

2020
spring:
2121
datasource:
22-
url: jdbc:postgresql://localhost:5432/${spring.application.name}
22+
url: jdbc:postgresql://localhost:5433/${spring.application.name}
2323
username: postgres
2424
password: password
2525
jpa:
@@ -61,3 +61,28 @@ jwt:
6161
auth:
6262
converter:
6363
principle-attribute: preferred_username
64+
65+
resilience4j:
66+
circuitbreaker:
67+
instances:
68+
productService:
69+
sliding-window-type: count_based
70+
sliding-window-size: 5
71+
minimum-number-of-calls: 5
72+
failure-rate-threshold: 50
73+
wait-duration-in-open-state: 30s
74+
orderService:
75+
sliding-window-type: count_based
76+
sliding-window-size: 5
77+
minimum-number-of-calls: 5
78+
failure-rate-threshold: 50
79+
wait-duration-in-open-state: 30s
80+
retry:
81+
instances:
82+
orderService:
83+
max-attempts: 3
84+
wait-duration: 1000ms
85+
timelimiter:
86+
instances:
87+
orderService:
88+
timeout-duration: 3s

feign-clients/src/main/java/dev/nano/clients/apiKeyManager/ApplicationName.java

Lines changed: 0 additions & 10 deletions
This file was deleted.

feign-clients/src/main/java/dev/nano/clients/apiKeyManager/apiKey/ApiKeyManagerClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
public interface ApiKeyManagerClient {
1010

1111
@GetMapping("{apiKey}/applications/{applicationName}/authorization")
12-
ApiKeyManagerResponse isKeyAuthorizedForApplication(
12+
boolean isKeyAuthorizedForApplication(
1313
@PathVariable("apiKey") String apiKey,
1414
@PathVariable("applicationName") String applicationName);
1515

feign-clients/src/main/java/dev/nano/clients/apiKeyManager/apiKey/ApiKeyManagerResponse.java

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)