Skip to content

Commit

Permalink
🔐implement API key authorization with caching and circuit breaker in …
Browse files Browse the repository at this point in the history
…gateway
  • Loading branch information
Adnane Miliari committed Nov 29, 2024
1 parent b6bd0fa commit 9072765
Show file tree
Hide file tree
Showing 43 changed files with 896 additions and 107 deletions.
67 changes: 67 additions & 0 deletions apiKey-manager/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.nano</groupId>
<artifactId>demo-microservices</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>apiKey-manager</artifactId>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>dev.nano</groupId>
<artifactId>common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.version}</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package dev.nano;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ApiKeyManagerApplication {
public static void main(String[] args) {
SpringApplication.run(ApiKeyManagerApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dev.nano.apikey;

public class ApiKeyConstant {
public static final String API_KEY_URI_REST_API = "/api/v1/apiKey-manager/api-keys";
public static final String API_KEY_NOT_FOUND = "Api key not found";
}
56 changes: 56 additions & 0 deletions apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package dev.nano.apikey;

import dev.nano.application.ApplicationEntity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.List;

@Entity
@Table(name = "api_keys")
@Data @SuperBuilder
@NoArgsConstructor @AllArgsConstructor
public class ApiKeyEntity {
@Id
@SequenceGenerator(
name = "customer_sequence",
sequenceName = "customer_sequence",
allocationSize = 1
)
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "customer_sequence"
)
@Column(
name = "id",
updatable = false
)
private Long id;

@Column(unique = true, nullable = false)
private String key;

@Column(nullable = false, unique = true)
private String client;

private String description;

private LocalDateTime createdDate;

private LocalDateTime expirationDate;

private boolean enabled;

private boolean neverExpires;

private boolean approved;

private boolean revoked;

@OneToMany(mappedBy = "apiKey")
private List<ApplicationEntity> applications;
}
32 changes: 32 additions & 0 deletions apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package dev.nano.apikey;

import dev.nano.application.ApplicationName;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.Optional;

public interface ApiKeyRepository extends JpaRepository<ApiKeyEntity, Long> {
@Query("""
SELECT ak FROM ApiKeyEntity ak
INNER JOIN ApplicationEntity ap
ON ak.id = ap.apiKey.id
WHERE ak.key = :key
AND ap.applicationName = :appName
""")
Optional<ApiKeyEntity> findByKeyAndApplicationName(String key, ApplicationName applicationName);

@Query("""
SELECT
CASE WHEN COUNT(ak) > 0
THEN TRUE
ELSE FALSE
END
FROM ApiKeyEntity ak
WHERE ak.key = :key
""")
boolean doesKeyExists(String key);

@Query("SELECT ak FROM ApiKeyEntity ak WHERE ak.key = :key")
Optional<ApiKeyEntity> findByKey(String key);
}
12 changes: 12 additions & 0 deletions apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dev.nano.apikey;

import dev.nano.application.ApplicationName;

import java.util.List;

public record ApiKeyRequest(
String client,
String description,
List<ApplicationName> applications
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dev.nano.apikey;

import dev.nano.application.ApplicationName;

public interface ApiKeyService {
String save(ApiKeyRequest apiKeyRequest);
void revokeApi(String key);
boolean isAuthorized(String apiKey, ApplicationName applicationName);
}
104 changes: 104 additions & 0 deletions apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyServiceImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package dev.nano.apikey;

import dev.nano.application.ApplicationEntity;
import dev.nano.application.ApplicationName;
import dev.nano.application.ApplicationRepository;
import exceptionhandler.core.ResourceNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static dev.nano.apikey.ApiKeyConstant.API_KEY_NOT_FOUND;


@Service
@RequiredArgsConstructor
public class ApiKeyServiceImpl implements ApiKeyService {
private static final Integer EXPIRATION_DAYS = 30;
private final ApiKeyRepository apiKeyRepository;
private final ApplicationRepository applicationRepository;
private final KeyGenerator keyGenerator;

@Override
public String save(ApiKeyRequest apiKeyRequest) {
ApiKeyEntity apiKey = new ApiKeyEntity();

apiKey.setClient(apiKeyRequest.client());
apiKey.setDescription(apiKeyRequest.description());

String apiKeyValue = keyGenerator.generateKey();
apiKey.setKey(apiKeyValue);

apiKey.setApproved(true);
apiKey.setEnabled(true);
apiKey.setNeverExpires(false);
apiKey.setCreatedDate(LocalDateTime.now());
apiKey.setExpirationDate(LocalDateTime.now().plusDays(EXPIRATION_DAYS));

ApiKeyEntity savedApiKeyEntity = apiKeyRepository.save(apiKey);

Set<ApplicationEntity> applications = Optional.ofNullable(apiKeyRequest.applications())
.orElse(List.of())
.stream().map(app -> ApplicationEntity.builder()
.applicationName(app)
.apiKey(savedApiKeyEntity)
.revoked(false)
.enabled(true)
.build())
.collect(Collectors.toUnmodifiableSet());

applicationRepository.saveAll(applications);

return apiKeyValue;
}

@Override
public void revokeApi(String key) {
ApiKeyEntity apiKey = apiKeyRepository.findByKey(key).orElseThrow(
() -> new ResourceNotFoundException(API_KEY_NOT_FOUND));

apiKey.setRevoked(true);
apiKey.setEnabled(false);
apiKey.setApproved(false);
apiKeyRepository.save(apiKey);

// revoke all applications associated with this api key
apiKey.getApplications().forEach(app -> {
app.setRevoked(true);
app.setEnabled(false);
app.setApproved(false);
applicationRepository.save(app);
});
}

@Override
public boolean isAuthorized(String apiKey, ApplicationName applicationName) {
Optional<ApiKeyEntity> optionalApiKey = apiKeyRepository.findByKeyAndApplicationName(apiKey, applicationName);

if(optionalApiKey.isEmpty()) {
return false;
}

ApiKeyEntity apiKeyEntity = optionalApiKey.get();

return apiKeyEntity.getApplications()
.stream()
.filter(app -> app.getApplicationName().equals(applicationName))
.findFirst()
.map(app -> app.isEnabled() &&
app.isApproved() &&
!app.isRevoked() &&
apiKeyEntity.isEnabled() &&
apiKeyEntity.isApproved() &&
!apiKeyEntity.isRevoked() &&
(apiKeyEntity.isNeverExpires() || LocalDateTime.now().isBefore(apiKeyEntity.getExpirationDate())) // isAfter used to check if the expiration date is in the future
)
.orElse(false);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package dev.nano.apikey;

public interface KeyGenerator {
String generateKey();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dev.nano.apikey;

import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
public class UUIDKeyGeneratorImpl implements KeyGenerator {
@Override
public String generateKey() {
return UUID.randomUUID().toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dev.nano.application;

public class ApplicationConstant {
public static final String APPLICATION_URI_REST_API = "/api/v1/apiKey-manager/applications";
public static final String APPLICATION_NOT_FOUND = "Application not found";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package dev.nano.application;

import dev.nano.apikey.ApiKeyEntity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

import jakarta.persistence.*;

@Entity
@Table(name = "applications")
@Data @SuperBuilder
@NoArgsConstructor @AllArgsConstructor
public class ApplicationEntity {
@Id
@SequenceGenerator(
name = "customer_sequence",
sequenceName = "customer_sequence",
allocationSize = 1
)
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "customer_sequence"
)
@Column(
name = "id",
updatable = false
)
private Integer id;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ApplicationName applicationName;
private boolean enabled;
private boolean approved;
private boolean revoked;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(
name = "api_key_id",
referencedColumnName = "id"
)
private ApiKeyEntity apiKey;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dev.nano.application;

public enum ApplicationName {
CUSTOMER,
PRODUCT,
ORDER,
PAYMENT,
NOTIFICATION,
APIKEY_MANAGER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dev.nano.application;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface ApplicationRepository extends JpaRepository<ApplicationEntity, Integer> {
Optional<ApplicationEntity> findByApplicationName(String applicationName);
}

Loading

0 comments on commit 9072765

Please sign in to comment.