Machine Installation id based Oauth 2
This commit is contained in:
98
cygnus-cloud-service/README.md
Normal file
98
cygnus-cloud-service/README.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Cygnus cloud service
|
||||
|
||||
## Identity login API
|
||||
|
||||
`POST /api/v1/identity/login` requires a valid machine JWT with the
|
||||
`identity.login` scope. The JWT must carry `client_id` and `installation_id`;
|
||||
both must equal the values inside the encrypted payload.
|
||||
|
||||
The request uses a hybrid encrypted envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"keyId": "cygnus-login-2026-01",
|
||||
"encryptedKey": "base64 RSA-OAEP-SHA256 encrypted AES key",
|
||||
"initializationVector": "base64 12-byte AES-GCM IV",
|
||||
"encryptedPayload": "base64 AES-GCM ciphertext and tag"
|
||||
}
|
||||
```
|
||||
|
||||
The AES-GCM additional authenticated data is the UTF-8 `keyId`. The decrypted
|
||||
JSON is:
|
||||
|
||||
```json
|
||||
{
|
||||
"loginId": "user",
|
||||
"password": "password",
|
||||
"clientId": "client-id-from-jwt",
|
||||
"installationId": "installation-id-from-jwt",
|
||||
"nonce": "unique-random-value",
|
||||
"issuedAt": "2026-07-23T06:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
Configure the PKCS#8 RSA private key with
|
||||
`CYGNUS_LOGIN_PRIVATE_KEY=file:/secure/path/login-private.pem`. Keep this key
|
||||
outside the source tree and container image. The corresponding public key is
|
||||
distributed to the on-prem gateway.
|
||||
|
||||
The database bootstrap is
|
||||
`src/main/resources/db/identity/001_identity_login_schema.sql`. It is
|
||||
transactional and idempotent; it copies login/menu data from `matrix.public`
|
||||
to `matrix.identity`. It is intended for initial migration and controlled
|
||||
development refreshes. Do not run it after `identity` becomes the production
|
||||
system of record because its upserts intentionally refresh rows from `public`.
|
||||
|
||||
## Machine token endpoint
|
||||
|
||||
`POST /oauth2/token` implements the client-credentials flow used by the
|
||||
on-premises gateway. The client assertion must be:
|
||||
|
||||
- an inner RS256 JWT signed with the installation private key;
|
||||
- encrypted as RSA-OAEP-256 plus AES-256-GCM using the cloud assertion key;
|
||||
- bound to the configured client ID, installation ID, and token audience;
|
||||
- unexpired and no longer-lived than `CYGNUS_ASSERTION_TTL`.
|
||||
|
||||
The endpoint returns a short-lived RS256 access token carrying `client_id`,
|
||||
`installation_id`, and the approved scope. The identity endpoint requires the
|
||||
`identity.login` scope and verifies the same machine binding in the encrypted
|
||||
login payload.
|
||||
|
||||
Generate separate cloud key pairs:
|
||||
|
||||
```bash
|
||||
mkdir -p config/keys
|
||||
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 \
|
||||
-out config/keys/assertion-decryption-private.pem
|
||||
openssl pkey -in config/keys/assertion-decryption-private.pem -pubout \
|
||||
-out config/keys/assertion-decryption-public.pem
|
||||
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 \
|
||||
-out config/keys/access-token-private.pem
|
||||
openssl pkey -in config/keys/access-token-private.pem -pubout \
|
||||
-out config/keys/access-token-public.pem
|
||||
chmod 600 config/keys/*private.pem
|
||||
```
|
||||
|
||||
Configure clients in an external Spring YAML file rather than the packaged
|
||||
`application.yml`:
|
||||
|
||||
```yaml
|
||||
cygnus:
|
||||
security:
|
||||
enabled: true
|
||||
issuer-uri: https://cloud.example.com
|
||||
audience: cygnus-cloud-api
|
||||
token-audience: https://cloud.example.com/oauth2/token
|
||||
clients:
|
||||
customer-a:
|
||||
enabled: true
|
||||
installation-id: site-01
|
||||
assertion-public-key: file:/secure/clients/customer-a/public.pem
|
||||
scopes:
|
||||
- identity.login
|
||||
```
|
||||
|
||||
Start with that protected file using
|
||||
`--spring.config.additional-location=file:/secure/cygnus/clients.yml`.
|
||||
Never place cloud private keys, customer assertions, or installation private
|
||||
keys in the repository or container image.
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.cygnus.cloud.identity.api;
|
||||
|
||||
import com.cygnus.cloud.identity.model.AuthenticatedIdentity;
|
||||
import com.cygnus.cloud.identity.service.AuthenticationException;
|
||||
import com.cygnus.cloud.identity.service.IdentityAuthenticationService;
|
||||
import com.cygnus.cloud.identity.service.LoginRequestReplayService;
|
||||
import jakarta.validation.Valid;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/identity")
|
||||
public class CloudLoginController {
|
||||
|
||||
private final RsaLoginPayloadDecryptor decryptor;
|
||||
private final IdentityAuthenticationService authenticationService;
|
||||
private final LoginRequestReplayService replayService;
|
||||
private final LoginEncryptionProperties encryptionProperties;
|
||||
private final Clock clock;
|
||||
|
||||
public CloudLoginController(
|
||||
RsaLoginPayloadDecryptor decryptor,
|
||||
IdentityAuthenticationService authenticationService,
|
||||
LoginRequestReplayService replayService,
|
||||
LoginEncryptionProperties encryptionProperties,
|
||||
Clock clock) {
|
||||
this.decryptor = decryptor;
|
||||
this.authenticationService = authenticationService;
|
||||
this.replayService = replayService;
|
||||
this.encryptionProperties = encryptionProperties;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public Mono<AuthenticatedIdentity> login(
|
||||
@AuthenticationPrincipal Jwt machineJwt,
|
||||
@Valid @RequestBody EncryptedLoginRequest request,
|
||||
ServerHttpRequest serverRequest) {
|
||||
if (machineJwt == null) {
|
||||
return Mono.error(new AuthenticationException("Machine authentication required"));
|
||||
}
|
||||
|
||||
LoginPayload payload = decryptor.decrypt(request);
|
||||
validatePayload(payload);
|
||||
validateMachineBinding(machineJwt, payload);
|
||||
validateFreshness(payload);
|
||||
|
||||
return replayService
|
||||
.claim(
|
||||
payload.installationId(),
|
||||
payload.nonce(),
|
||||
encryptionProperties.payloadTtl())
|
||||
.flatMap(claimed -> {
|
||||
if (!claimed) {
|
||||
return Mono.error(new AuthenticationException("Login request replayed"));
|
||||
}
|
||||
return authenticationService.authenticate(
|
||||
payload.loginId(),
|
||||
payload.password(),
|
||||
remoteAddress(serverRequest));
|
||||
});
|
||||
}
|
||||
|
||||
private void validatePayload(LoginPayload payload) {
|
||||
if (payload == null
|
||||
|| !StringUtils.hasText(payload.loginId())
|
||||
|| !StringUtils.hasText(payload.password())
|
||||
|| !StringUtils.hasText(payload.clientId())
|
||||
|| !StringUtils.hasText(payload.installationId())
|
||||
|| !StringUtils.hasText(payload.nonce())
|
||||
|| payload.issuedAt() == null) {
|
||||
throw new AuthenticationException("Invalid login payload");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMachineBinding(Jwt jwt, LoginPayload payload) {
|
||||
String authenticatedClient = jwt.getClaimAsString("client_id");
|
||||
String authenticatedInstallation = jwt.getClaimAsString("installation_id");
|
||||
if (!payload.clientId().equals(authenticatedClient)
|
||||
|| !payload.installationId().equals(authenticatedInstallation)) {
|
||||
throw new AuthenticationException("Machine identity mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateFreshness(LoginPayload payload) {
|
||||
Duration age = Duration.between(payload.issuedAt(), clock.instant()).abs();
|
||||
if (age.compareTo(encryptionProperties.payloadTtl()) > 0) {
|
||||
throw new AuthenticationException("Login request expired");
|
||||
}
|
||||
}
|
||||
|
||||
private String remoteAddress(ServerHttpRequest request) {
|
||||
return request.getRemoteAddress() == null
|
||||
? null
|
||||
: request.getRemoteAddress().getAddress().getHostAddress();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cygnus.cloud.identity.api;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record EncryptedLoginRequest(
|
||||
@NotBlank String keyId,
|
||||
@NotBlank String encryptedKey,
|
||||
@NotBlank String initializationVector,
|
||||
@NotBlank String encryptedPayload) {
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.cygnus.cloud.identity.api;
|
||||
|
||||
import com.cygnus.cloud.identity.service.AuthenticationException;
|
||||
import java.util.Map;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class IdentityErrorHandler {
|
||||
|
||||
@ExceptionHandler(AuthenticationException.class)
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
Map<String, String> authenticationFailure() {
|
||||
return Map.of("code", "AUTHENTICATION_FAILED", "message", "Authentication failed");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cygnus.cloud.identity.api;
|
||||
|
||||
import java.time.Duration;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties("cygnus.login-encryption")
|
||||
public record LoginEncryptionProperties(
|
||||
String keyId,
|
||||
String privateKeyLocation,
|
||||
Duration payloadTtl) {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.cygnus.cloud.identity.api;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.Instant;
|
||||
|
||||
public record LoginPayload(
|
||||
@NotBlank String loginId,
|
||||
@NotBlank String password,
|
||||
@NotBlank String clientId,
|
||||
@NotBlank String installationId,
|
||||
@NotBlank String nonce,
|
||||
@NotNull Instant issuedAt) {
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.cygnus.cloud.identity.api;
|
||||
|
||||
import com.cygnus.cloud.identity.service.AuthenticationException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.spec.MGF1ParameterSpec;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.OAEPParameterSpec;
|
||||
import javax.crypto.spec.PSource;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Component
|
||||
public class RsaLoginPayloadDecryptor {
|
||||
|
||||
private static final OAEPParameterSpec OAEP_SHA_256 = new OAEPParameterSpec(
|
||||
"SHA-256",
|
||||
"MGF1",
|
||||
MGF1ParameterSpec.SHA256,
|
||||
PSource.PSpecified.DEFAULT);
|
||||
|
||||
private final LoginEncryptionProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ResourceLoader resourceLoader;
|
||||
private volatile PrivateKey privateKey;
|
||||
|
||||
public RsaLoginPayloadDecryptor(
|
||||
LoginEncryptionProperties properties,
|
||||
ObjectMapper objectMapper,
|
||||
ResourceLoader resourceLoader) {
|
||||
this.properties = properties;
|
||||
this.objectMapper = objectMapper;
|
||||
this.resourceLoader = resourceLoader;
|
||||
}
|
||||
|
||||
public LoginPayload decrypt(EncryptedLoginRequest request) {
|
||||
if (!properties.keyId().equals(request.keyId())) {
|
||||
throw new AuthenticationException("Unsupported encryption key");
|
||||
}
|
||||
try {
|
||||
Cipher keyCipher = Cipher.getInstance("RSA/ECB/OAEPPadding");
|
||||
keyCipher.init(Cipher.DECRYPT_MODE, privateKey(), OAEP_SHA_256);
|
||||
byte[] aesKey =
|
||||
keyCipher.doFinal(Base64.getDecoder().decode(request.encryptedKey()));
|
||||
|
||||
Cipher payloadCipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
payloadCipher.init(
|
||||
Cipher.DECRYPT_MODE,
|
||||
new SecretKeySpec(aesKey, "AES"),
|
||||
new GCMParameterSpec(
|
||||
128,
|
||||
Base64.getDecoder().decode(request.initializationVector())));
|
||||
payloadCipher.updateAAD(request.keyId().getBytes(StandardCharsets.UTF_8));
|
||||
byte[] plaintext = payloadCipher.doFinal(
|
||||
Base64.getDecoder().decode(request.encryptedPayload()));
|
||||
return objectMapper.readValue(plaintext, LoginPayload.class);
|
||||
} catch (AuthenticationException exception) {
|
||||
throw exception;
|
||||
} catch (Exception exception) {
|
||||
throw new AuthenticationException("Invalid encrypted login request");
|
||||
}
|
||||
}
|
||||
|
||||
private PrivateKey privateKey() throws Exception {
|
||||
PrivateKey loaded = privateKey;
|
||||
if (loaded != null) {
|
||||
return loaded;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (privateKey == null) {
|
||||
Resource resource =
|
||||
resourceLoader.getResource(properties.privateKeyLocation());
|
||||
String pem = resource.getContentAsString(StandardCharsets.US_ASCII);
|
||||
String encoded = pem
|
||||
.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.replace("-----END PRIVATE KEY-----", "")
|
||||
.replaceAll("\\s", "");
|
||||
byte[] keyBytes = Base64.getDecoder().decode(encoded);
|
||||
privateKey = KeyFactory.getInstance("RSA")
|
||||
.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||
}
|
||||
return privateKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.cygnus.cloud.identity.model;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
public record AuthenticatedIdentity(
|
||||
short userId,
|
||||
String loginId,
|
||||
String displayName,
|
||||
short groupId,
|
||||
String groupName,
|
||||
short branchId,
|
||||
String branchName,
|
||||
String branchCode,
|
||||
String branchLocation,
|
||||
short companyId,
|
||||
String companyName,
|
||||
String companyCode,
|
||||
Instant loginTime,
|
||||
List<MenuItem> menu) {
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.cygnus.cloud.identity.model;
|
||||
|
||||
public record IdentityUser(
|
||||
short userId,
|
||||
String loginId,
|
||||
String displayName,
|
||||
String legacyPassword,
|
||||
short groupId,
|
||||
String groupName,
|
||||
short branchId,
|
||||
String branchName,
|
||||
String branchCode,
|
||||
String branchLocation,
|
||||
short companyId,
|
||||
String companyName,
|
||||
String companyCode,
|
||||
boolean active) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cygnus.cloud.identity.model;
|
||||
|
||||
public record MenuItem(
|
||||
short pageId,
|
||||
String label,
|
||||
String targetUrl,
|
||||
short parentPage,
|
||||
short pageOrder,
|
||||
String permission,
|
||||
String targetWindow,
|
||||
String requestValue) {
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.cygnus.cloud.identity.repository;
|
||||
|
||||
import com.cygnus.cloud.database.ReactiveDatabaseClient;
|
||||
import com.cygnus.cloud.identity.model.IdentityUser;
|
||||
import com.cygnus.cloud.identity.model.MenuItem;
|
||||
import io.vertx.sqlclient.Tuple;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Repository
|
||||
public class IdentityRepository {
|
||||
|
||||
private static final String FIND_USER = """
|
||||
SELECT u.user_id, u.loginid, u.displayname, u.loginpassword, u.group_id,
|
||||
g.name AS group_name, u.branch_id, b.branchname, b.branchcode, b.city,
|
||||
u.company_id, c.companyname, c.companycode, u.isactive
|
||||
FROM identity.app_user u
|
||||
JOIN identity.user_group g ON g.group_id = u.group_id
|
||||
JOIN identity.company c ON c.company_id = u.company_id
|
||||
JOIN identity.company_branch b
|
||||
ON b.branch_id = u.branch_id AND b.company_id = u.company_id
|
||||
WHERE upper(u.loginid) = upper($1)
|
||||
""";
|
||||
|
||||
private static final String FIND_MENU = """
|
||||
SELECT p.page_id, p.menulabel, p.targeturl, p.parentpage, p.pageorder,
|
||||
permissions.permission, p.targetwindow, permissions.requestval
|
||||
FROM identity.permission permissions
|
||||
JOIN identity.pages p ON p.page_id = permissions.page_id
|
||||
WHERE permissions.group_id = $1
|
||||
AND p.isvisible = 1
|
||||
AND permissions.permission <> '000'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM identity.denied_pages denied
|
||||
WHERE denied.user_id = $2
|
||||
AND denied.page_id = permissions.page_id
|
||||
AND denied.isdenied = 1
|
||||
)
|
||||
ORDER BY p.parentpage, p.pageorder DESC, p.page_id
|
||||
""";
|
||||
|
||||
private static final String RECORD_LOGIN = """
|
||||
INSERT INTO identity.user_loginhistory
|
||||
(loginid, logintime, ipaddr, user_id)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING uid
|
||||
""";
|
||||
|
||||
private final ReactiveDatabaseClient database;
|
||||
private final IdentityRowMapper mapper;
|
||||
|
||||
public IdentityRepository(ReactiveDatabaseClient database, IdentityRowMapper mapper) {
|
||||
this.database = database;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public Flux<IdentityUser> findUsersByLoginId(String loginId) {
|
||||
return database.preparedQuery(FIND_USER, Tuple.of(loginId))
|
||||
.flatMapMany(rows -> Flux.fromIterable(rows).map(mapper::user));
|
||||
}
|
||||
|
||||
public Mono<List<MenuItem>> findMenu(short groupId, short userId) {
|
||||
return database.preparedQuery(FIND_MENU, Tuple.of(groupId, userId))
|
||||
.flatMapMany(rows -> Flux.fromIterable(rows).map(mapper::menuItem))
|
||||
.collectList();
|
||||
}
|
||||
|
||||
public Mono<Long> recordLogin(
|
||||
String loginId, Instant loginTime, String remoteAddress, short userId) {
|
||||
LocalDateTime databaseTime = LocalDateTime.ofInstant(loginTime, ZoneOffset.UTC);
|
||||
return database.preparedQuery(
|
||||
RECORD_LOGIN,
|
||||
Tuple.of(loginId, databaseTime, remoteAddress, userId))
|
||||
.map(rows -> rows.iterator().next().getLong("uid"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.cygnus.cloud.identity.repository;
|
||||
|
||||
import com.cygnus.cloud.identity.model.IdentityUser;
|
||||
import com.cygnus.cloud.identity.model.MenuItem;
|
||||
import io.vertx.sqlclient.Row;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class IdentityRowMapper {
|
||||
|
||||
IdentityUser user(Row row) {
|
||||
return new IdentityUser(
|
||||
row.getShort("user_id"),
|
||||
row.getString("loginid"),
|
||||
row.getString("displayname"),
|
||||
row.getString("loginpassword"),
|
||||
row.getShort("group_id"),
|
||||
row.getString("group_name"),
|
||||
row.getShort("branch_id"),
|
||||
row.getString("branchname"),
|
||||
row.getString("branchcode"),
|
||||
row.getString("city"),
|
||||
row.getShort("company_id"),
|
||||
row.getString("companyname"),
|
||||
row.getString("companycode"),
|
||||
row.getShort("isactive") == 1);
|
||||
}
|
||||
|
||||
MenuItem menuItem(Row row) {
|
||||
return new MenuItem(
|
||||
row.getShort("page_id"),
|
||||
row.getString("menulabel"),
|
||||
row.getString("targeturl"),
|
||||
row.getShort("parentpage"),
|
||||
row.getShort("pageorder"),
|
||||
row.getString("permission").trim(),
|
||||
row.getString("targetwindow"),
|
||||
row.getString("requestval"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cygnus.cloud.identity.service;
|
||||
|
||||
public class AuthenticationException extends RuntimeException {
|
||||
|
||||
public AuthenticationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.cygnus.cloud.identity.service;
|
||||
|
||||
import com.cygnus.cloud.identity.model.AuthenticatedIdentity;
|
||||
import com.cygnus.cloud.identity.model.IdentityUser;
|
||||
import com.cygnus.cloud.identity.repository.IdentityRepository;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Service
|
||||
public class IdentityAuthenticationService {
|
||||
|
||||
private final IdentityRepository repository;
|
||||
private final LegacyPasswordVerifier passwordVerifier;
|
||||
private final Clock clock;
|
||||
|
||||
public IdentityAuthenticationService(
|
||||
IdentityRepository repository,
|
||||
LegacyPasswordVerifier passwordVerifier,
|
||||
Clock clock) {
|
||||
this.repository = repository;
|
||||
this.passwordVerifier = passwordVerifier;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public Mono<AuthenticatedIdentity> authenticate(
|
||||
String loginId, String password, String remoteAddress) {
|
||||
return repository.findUsersByLoginId(loginId)
|
||||
.collectList()
|
||||
.flatMap(users -> {
|
||||
if (users.isEmpty()) {
|
||||
return Mono.error(new AuthenticationException("Invalid credentials"));
|
||||
}
|
||||
java.util.List<IdentityUser> exactMatches = users.stream()
|
||||
.filter(user -> user.loginId().equals(loginId))
|
||||
.toList();
|
||||
IdentityUser user;
|
||||
if (exactMatches.size() == 1) {
|
||||
user = exactMatches.getFirst();
|
||||
} else if (users.size() == 1) {
|
||||
user = users.getFirst();
|
||||
} else {
|
||||
return Mono.error(
|
||||
new AuthenticationException("Ambiguous login identity"));
|
||||
}
|
||||
if (!user.active()
|
||||
|| !passwordVerifier.matches(password, user.legacyPassword())) {
|
||||
return Mono.error(new AuthenticationException("Invalid credentials"));
|
||||
}
|
||||
Instant loginTime = clock.instant();
|
||||
return repository.findMenu(user.groupId(), user.userId())
|
||||
.flatMap(menu -> repository.recordLogin(
|
||||
user.loginId(), loginTime, remoteAddress, user.userId())
|
||||
.thenReturn(toAuthenticatedIdentity(user, loginTime, menu)));
|
||||
});
|
||||
}
|
||||
|
||||
private AuthenticatedIdentity toAuthenticatedIdentity(
|
||||
IdentityUser user,
|
||||
Instant loginTime,
|
||||
java.util.List<com.cygnus.cloud.identity.model.MenuItem> menu) {
|
||||
return new AuthenticatedIdentity(
|
||||
user.userId(),
|
||||
user.loginId(),
|
||||
user.displayName(),
|
||||
user.groupId(),
|
||||
user.groupName(),
|
||||
user.branchId(),
|
||||
user.branchName(),
|
||||
user.branchCode(),
|
||||
user.branchLocation(),
|
||||
user.companyId(),
|
||||
user.companyName(),
|
||||
user.companyCode(),
|
||||
loginTime,
|
||||
menu);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.cygnus.cloud.identity.service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Temporary compatibility verifier for passwords created by the legacy app.
|
||||
* New passwords must not be written with this algorithm.
|
||||
*/
|
||||
@Component
|
||||
public class LegacyPasswordVerifier {
|
||||
|
||||
private static final byte[] LEGACY_KEY =
|
||||
"ThisIsASecretKey".getBytes(StandardCharsets.US_ASCII);
|
||||
|
||||
public boolean matches(String rawPassword, String storedPassword) {
|
||||
if (rawPassword == null || storedPassword == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(LEGACY_KEY, "AES"));
|
||||
byte[] encrypted = cipher.doFinal(rawPassword.getBytes(StandardCharsets.UTF_8));
|
||||
byte[] expected = Base64.getMimeDecoder().decode(storedPassword);
|
||||
return MessageDigest.isEqual(encrypted, expected);
|
||||
} catch (GeneralSecurityException | IllegalArgumentException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.cygnus.cloud.identity.service;
|
||||
|
||||
import com.cygnus.cloud.cache.ReactiveCacheService;
|
||||
import java.time.Duration;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Service
|
||||
public class LoginRequestReplayService {
|
||||
|
||||
private static final String NAMESPACE = "login-nonce";
|
||||
|
||||
private final ReactiveCacheService cache;
|
||||
|
||||
public LoginRequestReplayService(ReactiveCacheService cache) {
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
public Mono<Boolean> claim(String installationId, String nonce, Duration ttl) {
|
||||
return cache.putIfAbsent(
|
||||
NAMESPACE, installationId + ':' + nonce, "used", ttl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jose.crypto.RSASSASigner;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
class AccessTokenIssuer {
|
||||
|
||||
private final CommunicationSecurityProperties properties;
|
||||
private final Clock clock;
|
||||
|
||||
AccessTokenIssuer(CommunicationSecurityProperties properties, Clock clock) {
|
||||
this.properties = properties;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
TokenResponse issue(MachineClientPrincipal principal, Set<String> scopes) {
|
||||
try {
|
||||
Instant issuedAt = clock.instant();
|
||||
Instant expiresAt = issuedAt.plus(properties.accessTokenTtl());
|
||||
JWTClaimsSet claims = new JWTClaimsSet.Builder()
|
||||
.issuer(properties.issuerUri())
|
||||
.subject(principal.clientId())
|
||||
.audience(properties.audience())
|
||||
.issueTime(Date.from(issuedAt))
|
||||
.expirationTime(Date.from(expiresAt))
|
||||
.jwtID(UUID.randomUUID().toString())
|
||||
.claim("client_id", principal.clientId())
|
||||
.claim("installation_id", principal.installationId())
|
||||
.claim("scope", String.join(" ", scopes))
|
||||
.build();
|
||||
SignedJWT jwt = new SignedJWT(
|
||||
new JWSHeader.Builder(JWSAlgorithm.RS256)
|
||||
.keyID("cygnus-access-token")
|
||||
.build(),
|
||||
claims);
|
||||
jwt.sign(new RSASSASigner(
|
||||
PemKeyLoader.privateKey(properties.accessTokenPrivateKey())));
|
||||
return new TokenResponse(
|
||||
jwt.serialize(),
|
||||
"Bearer",
|
||||
properties.accessTokenTtl().toSeconds(),
|
||||
String.join(" ", scopes));
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("Unable to issue access token", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
import com.nimbusds.jose.EncryptionMethod;
|
||||
import com.nimbusds.jose.JWEAlgorithm;
|
||||
import com.nimbusds.jose.JWEObject;
|
||||
import com.nimbusds.jose.crypto.RSADecrypter;
|
||||
import com.nimbusds.jose.crypto.RSASSAVerifier;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
class ClientAssertionValidator {
|
||||
|
||||
private final CommunicationSecurityProperties properties;
|
||||
private final Clock clock;
|
||||
|
||||
ClientAssertionValidator(CommunicationSecurityProperties properties, Clock clock) {
|
||||
this.properties = properties;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
MachineClientPrincipal validate(String clientId, String encryptedAssertion) {
|
||||
CommunicationSecurityProperties.MachineClient client =
|
||||
properties.clients() == null ? null : properties.clients().get(clientId);
|
||||
if (client == null || !client.enabled()) {
|
||||
throw invalid();
|
||||
}
|
||||
try {
|
||||
JWEObject jwe = JWEObject.parse(encryptedAssertion);
|
||||
if (!JWEAlgorithm.RSA_OAEP_256.equals(jwe.getHeader().getAlgorithm())
|
||||
|| !EncryptionMethod.A256GCM.equals(jwe.getHeader().getEncryptionMethod())) {
|
||||
throw invalid();
|
||||
}
|
||||
jwe.decrypt(new RSADecrypter(
|
||||
PemKeyLoader.privateKey(properties.assertionDecryptionPrivateKey())));
|
||||
|
||||
SignedJWT signedJwt = SignedJWT.parse(jwe.getPayload().toString());
|
||||
if (!signedJwt.verify(new RSASSAVerifier(
|
||||
PemKeyLoader.publicKey(client.assertionPublicKey())))) {
|
||||
throw invalid();
|
||||
}
|
||||
|
||||
JWTClaimsSet claims = signedJwt.getJWTClaimsSet();
|
||||
validateClaims(clientId, client, claims);
|
||||
return new MachineClientPrincipal(
|
||||
clientId, client.installationId(), client.scopes());
|
||||
} catch (MachineAuthenticationException exception) {
|
||||
throw exception;
|
||||
} catch (Exception exception) {
|
||||
throw new MachineAuthenticationException("Invalid client assertion", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateClaims(
|
||||
String clientId,
|
||||
CommunicationSecurityProperties.MachineClient client,
|
||||
JWTClaimsSet claims) throws Exception {
|
||||
Instant now = clock.instant();
|
||||
Date issuedAt = claims.getIssueTime();
|
||||
Date expiresAt = claims.getExpirationTime();
|
||||
if (!clientId.equals(claims.getIssuer())
|
||||
|| !clientId.equals(claims.getSubject())
|
||||
|| !claims.getAudience().contains(properties.tokenAudience())
|
||||
|| !client.installationId().equals(
|
||||
claims.getStringClaim("installation_id"))
|
||||
|| issuedAt == null
|
||||
|| expiresAt == null
|
||||
|| now.isBefore(issuedAt.toInstant().minusSeconds(60))
|
||||
|| !now.isBefore(expiresAt.toInstant())) {
|
||||
throw invalid();
|
||||
}
|
||||
Duration lifetime = Duration.between(
|
||||
issuedAt.toInstant(), expiresAt.toInstant());
|
||||
if (lifetime.isNegative() || lifetime.compareTo(properties.assertionTtl()) > 0) {
|
||||
throw invalid();
|
||||
}
|
||||
}
|
||||
|
||||
private MachineAuthenticationException invalid() {
|
||||
return new MachineAuthenticationException("Invalid client assertion");
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,9 @@ public class CloudSecurityConfiguration {
|
||||
return http
|
||||
.authorizeExchange(exchange -> exchange
|
||||
.pathMatchers("/actuator/health", "/actuator/info").permitAll()
|
||||
.pathMatchers("/oauth2/token").permitAll()
|
||||
.pathMatchers("/api/v1/identity/login")
|
||||
.hasAuthority("SCOPE_identity.login")
|
||||
.anyExchange().authenticated())
|
||||
.oauth2ResourceServer(resourceServer -> resourceServer.jwt(withDefaults()))
|
||||
.build();
|
||||
@@ -38,7 +41,7 @@ public class CloudSecurityConfiguration {
|
||||
@ConditionalOnProperty(name = "cygnus.security.enabled", havingValue = "true")
|
||||
ReactiveJwtDecoder reactiveJwtDecoder(CommunicationSecurityProperties properties) {
|
||||
NimbusReactiveJwtDecoder decoder = NimbusReactiveJwtDecoder
|
||||
.withIssuerLocation(properties.issuerUri())
|
||||
.withPublicKey(PemKeyLoader.publicKey(properties.accessTokenPublicKey()))
|
||||
.build();
|
||||
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<Jwt>(
|
||||
JwtValidators.createDefaultWithIssuer(properties.issuerUri()),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@@ -9,6 +11,18 @@ public record CommunicationSecurityProperties(
|
||||
boolean enabled,
|
||||
String issuerUri,
|
||||
String audience,
|
||||
String tokenAudience,
|
||||
Duration assertionTtl,
|
||||
Duration accessTokenTtl) {
|
||||
Duration accessTokenTtl,
|
||||
String assertionDecryptionPrivateKey,
|
||||
String accessTokenPrivateKey,
|
||||
String accessTokenPublicKey,
|
||||
Map<String, MachineClient> clients) {
|
||||
|
||||
public record MachineClient(
|
||||
boolean enabled,
|
||||
String installationId,
|
||||
String assertionPublicKey,
|
||||
Set<String> scopes) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
final class MachineAuthenticationException extends RuntimeException {
|
||||
|
||||
MachineAuthenticationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
MachineAuthenticationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
record MachineClientPrincipal(
|
||||
String clientId,
|
||||
String installationId,
|
||||
Set<String> allowedScopes) {
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConditionalOnProperty(
|
||||
prefix = "cygnus.security",
|
||||
name = "enabled",
|
||||
havingValue = "true")
|
||||
class MachineSecurityConfigurationValidator implements InitializingBean {
|
||||
|
||||
private final CommunicationSecurityProperties properties;
|
||||
|
||||
MachineSecurityConfigurationValidator(
|
||||
CommunicationSecurityProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
requireUri("issuer-uri", properties.issuerUri());
|
||||
requireText("audience", properties.audience());
|
||||
requireUri("token-audience", properties.tokenAudience());
|
||||
requirePositive("assertion-ttl", properties.assertionTtl());
|
||||
requirePositive("access-token-ttl", properties.accessTokenTtl());
|
||||
requireText(
|
||||
"assertion-decryption-private-key",
|
||||
properties.assertionDecryptionPrivateKey());
|
||||
requireText("access-token-private-key", properties.accessTokenPrivateKey());
|
||||
requireText("access-token-public-key", properties.accessTokenPublicKey());
|
||||
|
||||
Map<String, CommunicationSecurityProperties.MachineClient> clients =
|
||||
properties.clients();
|
||||
if (clients == null || clients.isEmpty()) {
|
||||
throw invalid("at least one machine client is required");
|
||||
}
|
||||
clients.forEach(this::validateClient);
|
||||
}
|
||||
|
||||
private void validateClient(
|
||||
String clientId,
|
||||
CommunicationSecurityProperties.MachineClient client) {
|
||||
requireText("clients.<client-id>", clientId);
|
||||
if (client == null) {
|
||||
throw invalid("client '" + clientId + "' has no configuration");
|
||||
}
|
||||
requireText(
|
||||
"clients." + clientId + ".installation-id",
|
||||
client.installationId());
|
||||
requireText(
|
||||
"clients." + clientId + ".assertion-public-key",
|
||||
client.assertionPublicKey());
|
||||
if (client.scopes() == null
|
||||
|| client.scopes().isEmpty()
|
||||
|| client.scopes().stream().anyMatch(this::isBlank)) {
|
||||
throw invalid(
|
||||
"clients." + clientId + ".scopes must contain valid scopes");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireUri(String name, String value) {
|
||||
requireText(name, value);
|
||||
try {
|
||||
URI uri = URI.create(value);
|
||||
if (!uri.isAbsolute()) {
|
||||
throw invalid(name + " must be an absolute URI");
|
||||
}
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw invalid(name + " must be a valid absolute URI");
|
||||
}
|
||||
}
|
||||
|
||||
private void requirePositive(String name, Duration value) {
|
||||
if (value == null || value.isZero() || value.isNegative()) {
|
||||
throw invalid(name + " must be positive");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireText(String name, String value) {
|
||||
if (isBlank(value)) {
|
||||
throw invalid(name + " is required");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBlank(String value) {
|
||||
return value == null || value.isBlank();
|
||||
}
|
||||
|
||||
private IllegalStateException invalid(String detail) {
|
||||
return new IllegalStateException(
|
||||
"Invalid cygnus.security configuration: " + detail);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
class MachineTokenController {
|
||||
|
||||
private static final String CLIENT_CREDENTIALS = "client_credentials";
|
||||
private static final String ASSERTION_TYPE =
|
||||
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
||||
|
||||
private final ClientAssertionValidator assertionValidator;
|
||||
private final AccessTokenIssuer tokenIssuer;
|
||||
|
||||
MachineTokenController(
|
||||
ClientAssertionValidator assertionValidator,
|
||||
AccessTokenIssuer tokenIssuer) {
|
||||
this.assertionValidator = assertionValidator;
|
||||
this.tokenIssuer = tokenIssuer;
|
||||
}
|
||||
|
||||
@PostMapping(
|
||||
value = "/oauth2/token",
|
||||
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
Mono<Map<String, Object>> token(ServerWebExchange exchange) {
|
||||
return exchange.getFormData().map(this::issueToken);
|
||||
}
|
||||
|
||||
Map<String, Object> issueToken(MultiValueMap<String, String> form) {
|
||||
if (!CLIENT_CREDENTIALS.equals(form.getFirst("grant_type"))
|
||||
|| !ASSERTION_TYPE.equals(form.getFirst("client_assertion_type"))) {
|
||||
throw new MachineAuthenticationException("Unsupported token request");
|
||||
}
|
||||
String clientId = required(form, "client_id");
|
||||
MachineClientPrincipal principal = assertionValidator.validate(
|
||||
clientId, required(form, "client_assertion"));
|
||||
Set<String> requestedScopes = scopes(form.getFirst("scope"));
|
||||
if (requestedScopes.isEmpty()
|
||||
|| !principal.allowedScopes().containsAll(requestedScopes)) {
|
||||
throw new MachineAuthenticationException("Invalid requested scope");
|
||||
}
|
||||
return tokenIssuer.issue(principal, requestedScopes).asOAuthResponse();
|
||||
}
|
||||
|
||||
private String required(MultiValueMap<String, String> form, String name) {
|
||||
String value = form.getFirst(name);
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new MachineAuthenticationException("Invalid token request");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private Set<String> scopes(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return Set.of();
|
||||
}
|
||||
return new LinkedHashSet<>(Arrays.asList(value.trim().split("\\s+")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
import java.util.Map;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@RestControllerAdvice(assignableTypes = MachineTokenController.class)
|
||||
class MachineTokenErrorHandler {
|
||||
|
||||
@ExceptionHandler(MachineAuthenticationException.class)
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
Map<String, String> invalidClient() {
|
||||
return Map.of("error", "invalid_client");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
final class PemKeyLoader {
|
||||
|
||||
private static final ConcurrentHashMap<String, RSAPrivateKey> PRIVATE_KEYS =
|
||||
new ConcurrentHashMap<>();
|
||||
private static final ConcurrentHashMap<String, RSAPublicKey> PUBLIC_KEYS =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
private PemKeyLoader() {
|
||||
}
|
||||
|
||||
static RSAPrivateKey privateKey(String location) {
|
||||
return PRIVATE_KEYS.computeIfAbsent(location, PemKeyLoader::loadPrivateKey);
|
||||
}
|
||||
|
||||
private static RSAPrivateKey loadPrivateKey(String location) {
|
||||
try {
|
||||
String encoded = read(location)
|
||||
.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.replace("-----END PRIVATE KEY-----", "")
|
||||
.replaceAll("\\s", "");
|
||||
PrivateKey key = KeyFactory.getInstance("RSA")
|
||||
.generatePrivate(new PKCS8EncodedKeySpec(
|
||||
Base64.getDecoder().decode(encoded)));
|
||||
return (RSAPrivateKey) key;
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("Unable to load RSA private key", exception);
|
||||
}
|
||||
}
|
||||
|
||||
static RSAPublicKey publicKey(String location) {
|
||||
return PUBLIC_KEYS.computeIfAbsent(location, PemKeyLoader::loadPublicKey);
|
||||
}
|
||||
|
||||
private static RSAPublicKey loadPublicKey(String location) {
|
||||
try {
|
||||
String encoded = read(location)
|
||||
.replace("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replace("-----END PUBLIC KEY-----", "")
|
||||
.replaceAll("\\s", "");
|
||||
PublicKey key = KeyFactory.getInstance("RSA")
|
||||
.generatePublic(new X509EncodedKeySpec(
|
||||
Base64.getDecoder().decode(encoded)));
|
||||
return (RSAPublicKey) key;
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("Unable to load RSA public key", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static String read(String location) throws Exception {
|
||||
if (location == null || location.isBlank()) {
|
||||
throw new IllegalArgumentException("RSA key location is not configured");
|
||||
}
|
||||
if (location.startsWith("classpath:")) {
|
||||
String resource = location.substring("classpath:".length());
|
||||
try (InputStream stream = Thread.currentThread()
|
||||
.getContextClassLoader()
|
||||
.getResourceAsStream(resource)) {
|
||||
if (stream == null) {
|
||||
throw new IllegalArgumentException("Key resource not found");
|
||||
}
|
||||
return new String(stream.readAllBytes(), StandardCharsets.US_ASCII);
|
||||
}
|
||||
}
|
||||
String file = location.startsWith("file:") ? location.substring(5) : location;
|
||||
return Files.readString(Path.of(file), StandardCharsets.US_ASCII);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
record TokenResponse(
|
||||
String accessToken,
|
||||
String tokenType,
|
||||
long expiresIn,
|
||||
String scope) {
|
||||
|
||||
Map<String, Object> asOAuthResponse() {
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("access_token", accessToken);
|
||||
response.put("token_type", tokenType);
|
||||
response.put("expires_in", expiresIn);
|
||||
response.put("scope", scope);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ spring:
|
||||
redis:
|
||||
host: ${REDIS_HOST:192.168.0.111}
|
||||
port: ${REDIS_PORT:7901}
|
||||
password: ${REDIS_PASSWORD:M@triXR3d1s@6202}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
connect-timeout: ${REDIS_CONNECT_TIMEOUT:3s}
|
||||
timeout: ${REDIS_COMMAND_TIMEOUT:3s}
|
||||
lettuce:
|
||||
@@ -21,7 +21,7 @@ cygnus:
|
||||
port: ${DB_PORT:5432}
|
||||
database: ${DB_NAME:matrix}
|
||||
username: ${DB_USER:postgres}
|
||||
password: ${DB_PASSWORD:M@triXPostgr3s@6202}
|
||||
password: ${DB_PASSWORD:}
|
||||
ssl: ${DB_SSL:false}
|
||||
connect-timeout: ${DB_CONNECT_TIMEOUT:3s}
|
||||
pool-size: ${DB_POOL_SIZE:20}
|
||||
@@ -30,8 +30,17 @@ cygnus:
|
||||
enabled: ${CYGNUS_SECURITY_ENABLED:false}
|
||||
issuer-uri: ${CYGNUS_JWT_ISSUER_URI:http://localhost:8090}
|
||||
audience: ${CYGNUS_JWT_AUDIENCE:cygnus-cloud-api}
|
||||
assertion-ttl: ${CYGNUS_ASSERTION_TTL:5m}
|
||||
token-audience: ${CYGNUS_TOKEN_AUDIENCE:http://localhost:8090/oauth2/token}
|
||||
assertion-ttl: ${CYGNUS_ASSERTION_TTL:370d}
|
||||
access-token-ttl: ${CYGNUS_ACCESS_TOKEN_TTL:20m}
|
||||
assertion-decryption-private-key: ${CYGNUS_ASSERTION_DECRYPTION_PRIVATE_KEY:file:./config/keys/assertion-decryption-private.pem}
|
||||
access-token-private-key: ${CYGNUS_ACCESS_TOKEN_PRIVATE_KEY:file:./config/keys/access-token-private.pem}
|
||||
access-token-public-key: ${CYGNUS_ACCESS_TOKEN_PUBLIC_KEY:file:./config/keys/access-token-public.pem}
|
||||
clients: {}
|
||||
login-encryption:
|
||||
key-id: ${CYGNUS_LOGIN_KEY_ID:cygnus-login-2026-01}
|
||||
private-key-location: ${CYGNUS_LOGIN_PRIVATE_KEY:file:./config/keys/login-private.pem}
|
||||
payload-ttl: ${CYGNUS_LOGIN_PAYLOAD_TTL:5m}
|
||||
cache:
|
||||
key-prefix: ${CYGNUS_CACHE_PREFIX:cygnus}
|
||||
default-ttl: ${CYGNUS_CACHE_TTL:10m}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS identity;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identity.company
|
||||
(LIKE public.company INCLUDING ALL);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identity.company_branch
|
||||
(LIKE public.company_branch INCLUDING ALL);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identity.user_group
|
||||
(LIKE public.user_group INCLUDING ALL);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identity.app_user
|
||||
(LIKE public.app_user INCLUDING ALL);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identity.pages
|
||||
(LIKE public.pages INCLUDING ALL);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identity.permission
|
||||
(LIKE public.permission INCLUDING ALL);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identity.denied_pages
|
||||
(LIKE public.denied_pages INCLUDING ALL);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identity.user_loginhistory
|
||||
(LIKE public.user_loginhistory INCLUDING ALL);
|
||||
|
||||
CREATE SEQUENCE IF NOT EXISTS identity.app_user_user_id_seq
|
||||
AS smallint MAXVALUE 32767;
|
||||
CREATE SEQUENCE IF NOT EXISTS identity.company_company_id_seq
|
||||
AS smallint MAXVALUE 32767;
|
||||
CREATE SEQUENCE IF NOT EXISTS identity.company_branch_branch_id_seq
|
||||
AS smallint MAXVALUE 32767;
|
||||
CREATE SEQUENCE IF NOT EXISTS identity.user_group_group_id_seq
|
||||
AS smallint MAXVALUE 32767;
|
||||
CREATE SEQUENCE IF NOT EXISTS identity.pages_page_id_seq
|
||||
AS smallint MAXVALUE 32767;
|
||||
CREATE SEQUENCE IF NOT EXISTS identity.permission_permission_id_seq
|
||||
AS integer;
|
||||
CREATE SEQUENCE IF NOT EXISTS identity.denied_pages_uid_seq
|
||||
AS integer;
|
||||
CREATE SEQUENCE IF NOT EXISTS identity.user_loginhistory_uid_seq
|
||||
AS bigint;
|
||||
|
||||
ALTER TABLE identity.app_user
|
||||
ALTER COLUMN user_id SET DEFAULT nextval('identity.app_user_user_id_seq');
|
||||
ALTER TABLE identity.company
|
||||
ALTER COLUMN company_id SET DEFAULT nextval('identity.company_company_id_seq');
|
||||
ALTER TABLE identity.company_branch
|
||||
ALTER COLUMN branch_id SET DEFAULT nextval('identity.company_branch_branch_id_seq');
|
||||
ALTER TABLE identity.user_group
|
||||
ALTER COLUMN group_id SET DEFAULT nextval('identity.user_group_group_id_seq');
|
||||
ALTER TABLE identity.pages
|
||||
ALTER COLUMN page_id SET DEFAULT nextval('identity.pages_page_id_seq');
|
||||
ALTER TABLE identity.permission
|
||||
ALTER COLUMN permission_id SET DEFAULT nextval('identity.permission_permission_id_seq');
|
||||
ALTER TABLE identity.denied_pages
|
||||
ALTER COLUMN uid SET DEFAULT nextval('identity.denied_pages_uid_seq');
|
||||
ALTER TABLE identity.user_loginhistory
|
||||
ALTER COLUMN uid SET DEFAULT nextval('identity.user_loginhistory_uid_seq');
|
||||
ALTER TABLE identity.user_loginhistory
|
||||
ALTER COLUMN ipaddr TYPE character varying(45);
|
||||
|
||||
INSERT INTO identity.company
|
||||
SELECT * FROM public.company
|
||||
ON CONFLICT (company_id) DO UPDATE SET
|
||||
companyname = EXCLUDED.companyname,
|
||||
companycode = EXCLUDED.companycode,
|
||||
servicetaxno = EXCLUDED.servicetaxno,
|
||||
panno = EXCLUDED.panno,
|
||||
createdon = EXCLUDED.createdon,
|
||||
isactive = EXCLUDED.isactive,
|
||||
cinno = EXCLUDED.cinno;
|
||||
|
||||
INSERT INTO identity.company_branch
|
||||
SELECT * FROM public.company_branch
|
||||
ON CONFLICT (branch_id) DO UPDATE SET
|
||||
company_id = EXCLUDED.company_id,
|
||||
branchname = EXCLUDED.branchname,
|
||||
branchcode = EXCLUDED.branchcode,
|
||||
address1 = EXCLUDED.address1,
|
||||
address2 = EXCLUDED.address2,
|
||||
address3 = EXCLUDED.address3,
|
||||
city = EXCLUDED.city,
|
||||
pincode = EXCLUDED.pincode,
|
||||
landlineno = EXCLUDED.landlineno,
|
||||
faxno = EXCLUDED.faxno,
|
||||
emailid = EXCLUDED.emailid,
|
||||
contactperson1 = EXCLUDED.contactperson1,
|
||||
contactno1 = EXCLUDED.contactno1,
|
||||
emailid1 = EXCLUDED.emailid1,
|
||||
contactperson2 = EXCLUDED.contactperson2,
|
||||
contactno2 = EXCLUDED.contactno2,
|
||||
emailid2 = EXCLUDED.emailid2,
|
||||
isactive = EXCLUDED.isactive,
|
||||
createdon = EXCLUDED.createdon,
|
||||
gstin = EXCLUDED.gstin,
|
||||
cgst = EXCLUDED.cgst,
|
||||
sgst = EXCLUDED.sgst,
|
||||
igst = EXCLUDED.igst;
|
||||
|
||||
INSERT INTO identity.user_group
|
||||
SELECT * FROM public.user_group
|
||||
ON CONFLICT (group_id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
department_id = EXCLUDED.department_id,
|
||||
createdon = EXCLUDED.createdon,
|
||||
createdby = EXCLUDED.createdby,
|
||||
lasteditedon = EXCLUDED.lasteditedon,
|
||||
lasteditedby = EXCLUDED.lasteditedby,
|
||||
isactive = EXCLUDED.isactive;
|
||||
|
||||
INSERT INTO identity.app_user
|
||||
SELECT * FROM public.app_user
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
loginid = EXCLUDED.loginid,
|
||||
displayname = EXCLUDED.displayname,
|
||||
loginpassword = EXCLUDED.loginpassword,
|
||||
group_id = EXCLUDED.group_id,
|
||||
createdon = EXCLUDED.createdon,
|
||||
createdby = EXCLUDED.createdby,
|
||||
activatedon = EXCLUDED.activatedon,
|
||||
activatedby = EXCLUDED.activatedby,
|
||||
lasteditedon = EXCLUDED.lasteditedon,
|
||||
lasteditedby = EXCLUDED.lasteditedby,
|
||||
branch_id = EXCLUDED.branch_id,
|
||||
company_id = EXCLUDED.company_id,
|
||||
isactive = EXCLUDED.isactive,
|
||||
team_id = EXCLUDED.team_id,
|
||||
emailid = EXCLUDED.emailid,
|
||||
entry_time = EXCLUDED.entry_time;
|
||||
|
||||
INSERT INTO identity.pages
|
||||
SELECT * FROM public.pages
|
||||
ON CONFLICT (page_id) DO UPDATE SET
|
||||
menulabel = EXCLUDED.menulabel,
|
||||
targeturl = EXCLUDED.targeturl,
|
||||
parentpage = EXCLUDED.parentpage,
|
||||
targetwindow = EXCLUDED.targetwindow,
|
||||
pageorder = EXCLUDED.pageorder,
|
||||
isvisible = EXCLUDED.isvisible;
|
||||
|
||||
INSERT INTO identity.permission
|
||||
SELECT * FROM public.permission
|
||||
ON CONFLICT (permission_id) DO UPDATE SET
|
||||
page_id = EXCLUDED.page_id,
|
||||
group_id = EXCLUDED.group_id,
|
||||
lasteditedon = EXCLUDED.lasteditedon,
|
||||
lasteditedby = EXCLUDED.lasteditedby,
|
||||
permission = EXCLUDED.permission,
|
||||
requestval = EXCLUDED.requestval;
|
||||
|
||||
INSERT INTO identity.denied_pages
|
||||
SELECT * FROM public.denied_pages
|
||||
ON CONFLICT (uid) DO UPDATE SET
|
||||
page_id = EXCLUDED.page_id,
|
||||
user_id = EXCLUDED.user_id,
|
||||
isdenied = EXCLUDED.isdenied;
|
||||
|
||||
INSERT INTO identity.user_loginhistory
|
||||
SELECT * FROM public.user_loginhistory
|
||||
ON CONFLICT (uid) DO UPDATE SET
|
||||
loginid = EXCLUDED.loginid,
|
||||
logintime = EXCLUDED.logintime,
|
||||
logouttime = EXCLUDED.logouttime,
|
||||
ipaddr = EXCLUDED.ipaddr,
|
||||
user_id = EXCLUDED.user_id;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_identity_app_user_loginid_ci
|
||||
ON identity.app_user (upper(loginid));
|
||||
CREATE INDEX IF NOT EXISTS ix_identity_permission_group_page
|
||||
ON identity.permission (group_id, page_id)
|
||||
WHERE permission <> '000';
|
||||
CREATE INDEX IF NOT EXISTS ix_identity_pages_menu
|
||||
ON identity.pages (parentpage, pageorder DESC)
|
||||
WHERE isvisible = 1;
|
||||
CREATE INDEX IF NOT EXISTS ix_identity_denied_pages_user_page
|
||||
ON identity.denied_pages (user_id, page_id)
|
||||
WHERE isdenied = 1;
|
||||
CREATE INDEX IF NOT EXISTS ix_identity_login_history_user_time
|
||||
ON identity.user_loginhistory (user_id, logintime DESC);
|
||||
|
||||
SELECT setval(
|
||||
'identity.app_user_user_id_seq',
|
||||
GREATEST(COALESCE((SELECT max(user_id) FROM identity.app_user), 1), 1),
|
||||
EXISTS (SELECT 1 FROM identity.app_user));
|
||||
SELECT setval(
|
||||
'identity.company_company_id_seq',
|
||||
GREATEST(COALESCE((SELECT max(company_id) FROM identity.company), 1), 1),
|
||||
EXISTS (SELECT 1 FROM identity.company));
|
||||
SELECT setval(
|
||||
'identity.company_branch_branch_id_seq',
|
||||
GREATEST(COALESCE((SELECT max(branch_id) FROM identity.company_branch), 1), 1),
|
||||
EXISTS (SELECT 1 FROM identity.company_branch));
|
||||
SELECT setval(
|
||||
'identity.user_group_group_id_seq',
|
||||
GREATEST(COALESCE((SELECT max(group_id) FROM identity.user_group), 1), 1),
|
||||
EXISTS (SELECT 1 FROM identity.user_group));
|
||||
SELECT setval(
|
||||
'identity.pages_page_id_seq',
|
||||
GREATEST(COALESCE((SELECT max(page_id) FROM identity.pages), 1), 1),
|
||||
EXISTS (SELECT 1 FROM identity.pages));
|
||||
SELECT setval(
|
||||
'identity.permission_permission_id_seq',
|
||||
GREATEST(COALESCE((SELECT max(permission_id) FROM identity.permission), 1), 1),
|
||||
EXISTS (SELECT 1 FROM identity.permission));
|
||||
SELECT setval(
|
||||
'identity.denied_pages_uid_seq',
|
||||
GREATEST(COALESCE((SELECT max(uid) FROM identity.denied_pages), 1), 1),
|
||||
EXISTS (SELECT 1 FROM identity.denied_pages));
|
||||
SELECT setval(
|
||||
'identity.user_loginhistory_uid_seq',
|
||||
GREATEST(COALESCE((SELECT max(uid) FROM identity.user_loginhistory), 1), 1),
|
||||
EXISTS (SELECT 1 FROM identity.user_loginhistory));
|
||||
|
||||
COMMIT;
|
||||
@@ -36,7 +36,7 @@ class InfrastructureConfigurationTest {
|
||||
assertThat(postgresPool).isNotNull();
|
||||
assertThat(databaseClient).isNotNull();
|
||||
assertThat(cacheService).isNotNull();
|
||||
assertThat(databaseProperties.database()).isEqualTo("cygnus");
|
||||
assertThat(databaseProperties.database()).isEqualTo("matrix");
|
||||
assertThat(securityProperties.audience()).isEqualTo("cygnus-cloud-api");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.cygnus.cloud.identity.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.cygnus.cloud.identity.model.IdentityUser;
|
||||
import com.cygnus.cloud.identity.model.MenuItem;
|
||||
import com.cygnus.cloud.identity.repository.IdentityRepository;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class IdentityAuthenticationServiceTest {
|
||||
|
||||
private static final Instant LOGIN_TIME = Instant.parse("2026-07-23T06:30:00Z");
|
||||
|
||||
@Mock
|
||||
private IdentityRepository repository;
|
||||
|
||||
@Mock
|
||||
private LegacyPasswordVerifier passwordVerifier;
|
||||
|
||||
@Test
|
||||
void authenticatesActiveUserAndReturnsStructuredSessionData() {
|
||||
IdentityUser user = user("maddy", true);
|
||||
MenuItem menuItem = new MenuItem(
|
||||
(short) 10, "Operations", "/ver/operations", (short) 0,
|
||||
(short) 1, "110", "_parent", null);
|
||||
when(repository.findUsersByLoginId("maddy")).thenReturn(Flux.just(user));
|
||||
when(passwordVerifier.matches("secret", "legacy-value")).thenReturn(true);
|
||||
when(repository.findMenu((short) 4, (short) 25))
|
||||
.thenReturn(Mono.just(List.of(menuItem)));
|
||||
when(repository.recordLogin("maddy", LOGIN_TIME, "127.0.0.1", (short) 25))
|
||||
.thenReturn(Mono.just(101L));
|
||||
|
||||
IdentityAuthenticationService service = new IdentityAuthenticationService(
|
||||
repository,
|
||||
passwordVerifier,
|
||||
Clock.fixed(LOGIN_TIME, ZoneOffset.UTC));
|
||||
|
||||
StepVerifier.create(service.authenticate("maddy", "secret", "127.0.0.1"))
|
||||
.assertNext(result -> {
|
||||
assertThat(result.loginId()).isEqualTo("maddy");
|
||||
assertThat(result.companyName()).isEqualTo("Matrix");
|
||||
assertThat(result.menu()).containsExactly(menuItem);
|
||||
assertThat(result.loginTime()).isEqualTo(LOGIN_TIME);
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
verify(repository).recordLogin("maddy", LOGIN_TIME, "127.0.0.1", (short) 25);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInactiveUserWithoutLoadingMenu() {
|
||||
IdentityUser user = user("maddy", false);
|
||||
when(repository.findUsersByLoginId("maddy")).thenReturn(Flux.just(user));
|
||||
|
||||
IdentityAuthenticationService service = new IdentityAuthenticationService(
|
||||
repository,
|
||||
passwordVerifier,
|
||||
Clock.fixed(LOGIN_TIME, ZoneOffset.UTC));
|
||||
|
||||
StepVerifier.create(service.authenticate("maddy", "secret", "127.0.0.1"))
|
||||
.expectError(AuthenticationException.class)
|
||||
.verify();
|
||||
}
|
||||
|
||||
private IdentityUser user(String loginId, boolean active) {
|
||||
return new IdentityUser(
|
||||
(short) 25,
|
||||
loginId,
|
||||
"Maddy",
|
||||
"legacy-value",
|
||||
(short) 4,
|
||||
"Administrator",
|
||||
(short) 2,
|
||||
"Delhi",
|
||||
"DEL",
|
||||
"Delhi",
|
||||
(short) 1,
|
||||
"Matrix",
|
||||
"MCR",
|
||||
active);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.cygnus.cloud.identity.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class LegacyPasswordVerifierTest {
|
||||
|
||||
private final LegacyPasswordVerifier verifier = new LegacyPasswordVerifier();
|
||||
|
||||
@Test
|
||||
void matchesLegacyAesPassword() {
|
||||
assertThat(verifier.matches("password", "sS3vFSMkpzsHrGYlS1Nn6Q==")).isTrue();
|
||||
assertThat(verifier.matches("wrong", "sS3vFSMkpzsHrGYlS1Nn6Q==")).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package com.cygnus.cloud.security;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import com.nimbusds.jose.EncryptionMethod;
|
||||
import com.nimbusds.jose.JWEAlgorithm;
|
||||
import com.nimbusds.jose.JWEHeader;
|
||||
import com.nimbusds.jose.JWEObject;
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jose.Payload;
|
||||
import com.nimbusds.jose.crypto.RSAEncrypter;
|
||||
import com.nimbusds.jose.crypto.RSASSASigner;
|
||||
import com.nimbusds.jose.crypto.RSASSAVerifier;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
|
||||
class MachineTokenFlowTest {
|
||||
|
||||
private static final Instant NOW = Instant.parse("2026-07-23T06:30:00Z");
|
||||
private static final String CLIENT_ID = "customer-a";
|
||||
private static final String INSTALLATION_ID = "site-01";
|
||||
private static final String TOKEN_AUDIENCE =
|
||||
"https://cloud.example.test/oauth2/token";
|
||||
|
||||
@TempDir
|
||||
Path tempDirectory;
|
||||
|
||||
private KeyPair assertionEncryptionKeys;
|
||||
private KeyPair clientSigningKeys;
|
||||
private KeyPair accessTokenKeys;
|
||||
private CommunicationSecurityProperties properties;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
assertionEncryptionKeys = keyPair();
|
||||
clientSigningKeys = keyPair();
|
||||
accessTokenKeys = keyPair();
|
||||
|
||||
properties = new CommunicationSecurityProperties(
|
||||
true,
|
||||
"https://cloud.example.test",
|
||||
"cygnus-cloud-api",
|
||||
TOKEN_AUDIENCE,
|
||||
Duration.ofDays(370),
|
||||
Duration.ofMinutes(20),
|
||||
privatePem("assertion-private.pem", assertionEncryptionKeys),
|
||||
privatePem("access-private.pem", accessTokenKeys),
|
||||
publicPem("access-public.pem", accessTokenKeys),
|
||||
Map.of(
|
||||
CLIENT_ID,
|
||||
new CommunicationSecurityProperties.MachineClient(
|
||||
true,
|
||||
INSTALLATION_ID,
|
||||
publicPem("client-public.pem", clientSigningKeys),
|
||||
Set.of("identity.login"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesNestedAssertionAndIssuesBoundShortLivedAccessToken()
|
||||
throws Exception {
|
||||
Clock clock = Clock.fixed(NOW, ZoneOffset.UTC);
|
||||
ClientAssertionValidator validator =
|
||||
new ClientAssertionValidator(properties, clock);
|
||||
AccessTokenIssuer issuer = new AccessTokenIssuer(properties, clock);
|
||||
MachineTokenController controller =
|
||||
new MachineTokenController(validator, issuer);
|
||||
|
||||
LinkedMultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
||||
form.add("grant_type", "client_credentials");
|
||||
form.add("client_id", CLIENT_ID);
|
||||
form.add(
|
||||
"client_assertion_type",
|
||||
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
|
||||
form.add("client_assertion", encryptedAssertion(NOW, NOW.plus(Duration.ofDays(365))));
|
||||
form.add("scope", "identity.login");
|
||||
|
||||
Map<String, Object> response = controller.issueToken(form);
|
||||
SignedJWT token = SignedJWT.parse((String) response.get("access_token"));
|
||||
|
||||
assertThat(token.verify(new RSASSAVerifier(
|
||||
(RSAPublicKey) accessTokenKeys.getPublic())))
|
||||
.isTrue();
|
||||
assertThat(token.getJWTClaimsSet().getStringClaim("client_id"))
|
||||
.isEqualTo(CLIENT_ID);
|
||||
assertThat(token.getJWTClaimsSet().getStringClaim("installation_id"))
|
||||
.isEqualTo(INSTALLATION_ID);
|
||||
assertThat(token.getJWTClaimsSet().getStringClaim("scope"))
|
||||
.isEqualTo("identity.login");
|
||||
assertThat(response)
|
||||
.containsEntry("token_type", "Bearer")
|
||||
.containsEntry("expires_in", 1200L)
|
||||
.containsEntry("scope", "identity.login");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsExpiredAssertion() throws Exception {
|
||||
ClientAssertionValidator validator = new ClientAssertionValidator(
|
||||
properties, Clock.fixed(NOW, ZoneOffset.UTC));
|
||||
|
||||
assertThatThrownBy(() -> validator.validate(
|
||||
CLIENT_ID,
|
||||
encryptedAssertion(
|
||||
NOW.minus(Duration.ofDays(366)),
|
||||
NOW.minusSeconds(1))))
|
||||
.isInstanceOf(MachineAuthenticationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsOAuthFormEncodedTokenRequestOverHttp() throws Exception {
|
||||
Clock clock = Clock.fixed(NOW, ZoneOffset.UTC);
|
||||
MachineTokenController controller = new MachineTokenController(
|
||||
new ClientAssertionValidator(properties, clock),
|
||||
new AccessTokenIssuer(properties, clock));
|
||||
LinkedMultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
||||
form.add("grant_type", "client_credentials");
|
||||
form.add("client_id", CLIENT_ID);
|
||||
form.add(
|
||||
"client_assertion_type",
|
||||
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
|
||||
form.add(
|
||||
"client_assertion",
|
||||
encryptedAssertion(NOW, NOW.plus(Duration.ofDays(365))));
|
||||
form.add("scope", "identity.login");
|
||||
|
||||
WebTestClient.bindToController(controller)
|
||||
.build()
|
||||
.post()
|
||||
.uri("/oauth2/token")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body(BodyInserters.fromFormData(form))
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)
|
||||
.expectBody()
|
||||
.jsonPath("$.access_token").isNotEmpty()
|
||||
.jsonPath("$.token_type").isEqualTo("Bearer")
|
||||
.jsonPath("$.expires_in").isEqualTo(1200)
|
||||
.jsonPath("$.scope").isEqualTo("identity.login");
|
||||
}
|
||||
|
||||
private String encryptedAssertion(Instant issuedAt, Instant expiresAt)
|
||||
throws Exception {
|
||||
JWTClaimsSet claims = new JWTClaimsSet.Builder()
|
||||
.issuer(CLIENT_ID)
|
||||
.subject(CLIENT_ID)
|
||||
.audience(TOKEN_AUDIENCE)
|
||||
.issueTime(Date.from(issuedAt))
|
||||
.expirationTime(Date.from(expiresAt))
|
||||
.claim("installation_id", INSTALLATION_ID)
|
||||
.build();
|
||||
SignedJWT signed = new SignedJWT(
|
||||
new JWSHeader(JWSAlgorithm.RS256), claims);
|
||||
signed.sign(new RSASSASigner(
|
||||
(RSAPrivateKey) clientSigningKeys.getPrivate()));
|
||||
|
||||
JWEObject encrypted = new JWEObject(
|
||||
new JWEHeader(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM),
|
||||
new Payload(signed.serialize()));
|
||||
encrypted.encrypt(new RSAEncrypter(
|
||||
(RSAPublicKey) assertionEncryptionKeys.getPublic()));
|
||||
return encrypted.serialize();
|
||||
}
|
||||
|
||||
private KeyPair keyPair() throws Exception {
|
||||
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
|
||||
generator.initialize(2048);
|
||||
return generator.generateKeyPair();
|
||||
}
|
||||
|
||||
private String privatePem(String name, KeyPair pair) throws Exception {
|
||||
return writePem(
|
||||
name,
|
||||
"PRIVATE KEY",
|
||||
pair.getPrivate().getEncoded());
|
||||
}
|
||||
|
||||
private String publicPem(String name, KeyPair pair) throws Exception {
|
||||
return writePem(
|
||||
name,
|
||||
"PUBLIC KEY",
|
||||
pair.getPublic().getEncoded());
|
||||
}
|
||||
|
||||
private String writePem(String name, String type, byte[] key) throws Exception {
|
||||
String body = Base64.getMimeEncoder(64, new byte[] {'\n'})
|
||||
.encodeToString(key);
|
||||
Path path = tempDirectory.resolve(name);
|
||||
Files.writeString(
|
||||
path,
|
||||
"-----BEGIN " + type + "-----\n"
|
||||
+ body
|
||||
+ "\n-----END " + type + "-----\n",
|
||||
StandardCharsets.US_ASCII);
|
||||
return "file:" + path;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ spring:
|
||||
redis:
|
||||
host: ${REDIS_HOST:192.168.0.111}
|
||||
port: ${REDIS_PORT:7901}
|
||||
password: ${REDIS_PASSWORD:M@triXR3d1s@6202}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
connect-timeout: ${REDIS_CONNECT_TIMEOUT:3s}
|
||||
timeout: ${REDIS_COMMAND_TIMEOUT:3s}
|
||||
lettuce:
|
||||
@@ -21,7 +21,7 @@ cygnus:
|
||||
port: ${DB_PORT:5432}
|
||||
database: ${DB_NAME:matrix}
|
||||
username: ${DB_USER:postgres}
|
||||
password: ${DB_PASSWORD:M@triXPostgr3s@6202}
|
||||
password: ${DB_PASSWORD:}
|
||||
ssl: ${DB_SSL:false}
|
||||
connect-timeout: ${DB_CONNECT_TIMEOUT:3s}
|
||||
pool-size: ${DB_POOL_SIZE:20}
|
||||
@@ -30,8 +30,17 @@ cygnus:
|
||||
enabled: ${CYGNUS_SECURITY_ENABLED:false}
|
||||
issuer-uri: ${CYGNUS_JWT_ISSUER_URI:http://localhost:8090}
|
||||
audience: ${CYGNUS_JWT_AUDIENCE:cygnus-cloud-api}
|
||||
assertion-ttl: ${CYGNUS_ASSERTION_TTL:5m}
|
||||
token-audience: ${CYGNUS_TOKEN_AUDIENCE:http://localhost:8090/oauth2/token}
|
||||
assertion-ttl: ${CYGNUS_ASSERTION_TTL:370d}
|
||||
access-token-ttl: ${CYGNUS_ACCESS_TOKEN_TTL:20m}
|
||||
assertion-decryption-private-key: ${CYGNUS_ASSERTION_DECRYPTION_PRIVATE_KEY:file:./config/keys/assertion-decryption-private.pem}
|
||||
access-token-private-key: ${CYGNUS_ACCESS_TOKEN_PRIVATE_KEY:file:./config/keys/access-token-private.pem}
|
||||
access-token-public-key: ${CYGNUS_ACCESS_TOKEN_PUBLIC_KEY:file:./config/keys/access-token-public.pem}
|
||||
clients: {}
|
||||
login-encryption:
|
||||
key-id: ${CYGNUS_LOGIN_KEY_ID:cygnus-login-2026-01}
|
||||
private-key-location: ${CYGNUS_LOGIN_PRIVATE_KEY:file:./config/keys/login-private.pem}
|
||||
payload-ttl: ${CYGNUS_LOGIN_PAYLOAD_TTL:5m}
|
||||
cache:
|
||||
key-prefix: ${CYGNUS_CACHE_PREFIX:cygnus}
|
||||
default-ttl: ${CYGNUS_CACHE_TTL:10m}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,217 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS identity;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identity.company
|
||||
(LIKE public.company INCLUDING ALL);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identity.company_branch
|
||||
(LIKE public.company_branch INCLUDING ALL);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identity.user_group
|
||||
(LIKE public.user_group INCLUDING ALL);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identity.app_user
|
||||
(LIKE public.app_user INCLUDING ALL);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identity.pages
|
||||
(LIKE public.pages INCLUDING ALL);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identity.permission
|
||||
(LIKE public.permission INCLUDING ALL);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identity.denied_pages
|
||||
(LIKE public.denied_pages INCLUDING ALL);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identity.user_loginhistory
|
||||
(LIKE public.user_loginhistory INCLUDING ALL);
|
||||
|
||||
CREATE SEQUENCE IF NOT EXISTS identity.app_user_user_id_seq
|
||||
AS smallint MAXVALUE 32767;
|
||||
CREATE SEQUENCE IF NOT EXISTS identity.company_company_id_seq
|
||||
AS smallint MAXVALUE 32767;
|
||||
CREATE SEQUENCE IF NOT EXISTS identity.company_branch_branch_id_seq
|
||||
AS smallint MAXVALUE 32767;
|
||||
CREATE SEQUENCE IF NOT EXISTS identity.user_group_group_id_seq
|
||||
AS smallint MAXVALUE 32767;
|
||||
CREATE SEQUENCE IF NOT EXISTS identity.pages_page_id_seq
|
||||
AS smallint MAXVALUE 32767;
|
||||
CREATE SEQUENCE IF NOT EXISTS identity.permission_permission_id_seq
|
||||
AS integer;
|
||||
CREATE SEQUENCE IF NOT EXISTS identity.denied_pages_uid_seq
|
||||
AS integer;
|
||||
CREATE SEQUENCE IF NOT EXISTS identity.user_loginhistory_uid_seq
|
||||
AS bigint;
|
||||
|
||||
ALTER TABLE identity.app_user
|
||||
ALTER COLUMN user_id SET DEFAULT nextval('identity.app_user_user_id_seq');
|
||||
ALTER TABLE identity.company
|
||||
ALTER COLUMN company_id SET DEFAULT nextval('identity.company_company_id_seq');
|
||||
ALTER TABLE identity.company_branch
|
||||
ALTER COLUMN branch_id SET DEFAULT nextval('identity.company_branch_branch_id_seq');
|
||||
ALTER TABLE identity.user_group
|
||||
ALTER COLUMN group_id SET DEFAULT nextval('identity.user_group_group_id_seq');
|
||||
ALTER TABLE identity.pages
|
||||
ALTER COLUMN page_id SET DEFAULT nextval('identity.pages_page_id_seq');
|
||||
ALTER TABLE identity.permission
|
||||
ALTER COLUMN permission_id SET DEFAULT nextval('identity.permission_permission_id_seq');
|
||||
ALTER TABLE identity.denied_pages
|
||||
ALTER COLUMN uid SET DEFAULT nextval('identity.denied_pages_uid_seq');
|
||||
ALTER TABLE identity.user_loginhistory
|
||||
ALTER COLUMN uid SET DEFAULT nextval('identity.user_loginhistory_uid_seq');
|
||||
ALTER TABLE identity.user_loginhistory
|
||||
ALTER COLUMN ipaddr TYPE character varying(45);
|
||||
|
||||
INSERT INTO identity.company
|
||||
SELECT * FROM public.company
|
||||
ON CONFLICT (company_id) DO UPDATE SET
|
||||
companyname = EXCLUDED.companyname,
|
||||
companycode = EXCLUDED.companycode,
|
||||
servicetaxno = EXCLUDED.servicetaxno,
|
||||
panno = EXCLUDED.panno,
|
||||
createdon = EXCLUDED.createdon,
|
||||
isactive = EXCLUDED.isactive,
|
||||
cinno = EXCLUDED.cinno;
|
||||
|
||||
INSERT INTO identity.company_branch
|
||||
SELECT * FROM public.company_branch
|
||||
ON CONFLICT (branch_id) DO UPDATE SET
|
||||
company_id = EXCLUDED.company_id,
|
||||
branchname = EXCLUDED.branchname,
|
||||
branchcode = EXCLUDED.branchcode,
|
||||
address1 = EXCLUDED.address1,
|
||||
address2 = EXCLUDED.address2,
|
||||
address3 = EXCLUDED.address3,
|
||||
city = EXCLUDED.city,
|
||||
pincode = EXCLUDED.pincode,
|
||||
landlineno = EXCLUDED.landlineno,
|
||||
faxno = EXCLUDED.faxno,
|
||||
emailid = EXCLUDED.emailid,
|
||||
contactperson1 = EXCLUDED.contactperson1,
|
||||
contactno1 = EXCLUDED.contactno1,
|
||||
emailid1 = EXCLUDED.emailid1,
|
||||
contactperson2 = EXCLUDED.contactperson2,
|
||||
contactno2 = EXCLUDED.contactno2,
|
||||
emailid2 = EXCLUDED.emailid2,
|
||||
isactive = EXCLUDED.isactive,
|
||||
createdon = EXCLUDED.createdon,
|
||||
gstin = EXCLUDED.gstin,
|
||||
cgst = EXCLUDED.cgst,
|
||||
sgst = EXCLUDED.sgst,
|
||||
igst = EXCLUDED.igst;
|
||||
|
||||
INSERT INTO identity.user_group
|
||||
SELECT * FROM public.user_group
|
||||
ON CONFLICT (group_id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
department_id = EXCLUDED.department_id,
|
||||
createdon = EXCLUDED.createdon,
|
||||
createdby = EXCLUDED.createdby,
|
||||
lasteditedon = EXCLUDED.lasteditedon,
|
||||
lasteditedby = EXCLUDED.lasteditedby,
|
||||
isactive = EXCLUDED.isactive;
|
||||
|
||||
INSERT INTO identity.app_user
|
||||
SELECT * FROM public.app_user
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
loginid = EXCLUDED.loginid,
|
||||
displayname = EXCLUDED.displayname,
|
||||
loginpassword = EXCLUDED.loginpassword,
|
||||
group_id = EXCLUDED.group_id,
|
||||
createdon = EXCLUDED.createdon,
|
||||
createdby = EXCLUDED.createdby,
|
||||
activatedon = EXCLUDED.activatedon,
|
||||
activatedby = EXCLUDED.activatedby,
|
||||
lasteditedon = EXCLUDED.lasteditedon,
|
||||
lasteditedby = EXCLUDED.lasteditedby,
|
||||
branch_id = EXCLUDED.branch_id,
|
||||
company_id = EXCLUDED.company_id,
|
||||
isactive = EXCLUDED.isactive,
|
||||
team_id = EXCLUDED.team_id,
|
||||
emailid = EXCLUDED.emailid,
|
||||
entry_time = EXCLUDED.entry_time;
|
||||
|
||||
INSERT INTO identity.pages
|
||||
SELECT * FROM public.pages
|
||||
ON CONFLICT (page_id) DO UPDATE SET
|
||||
menulabel = EXCLUDED.menulabel,
|
||||
targeturl = EXCLUDED.targeturl,
|
||||
parentpage = EXCLUDED.parentpage,
|
||||
targetwindow = EXCLUDED.targetwindow,
|
||||
pageorder = EXCLUDED.pageorder,
|
||||
isvisible = EXCLUDED.isvisible;
|
||||
|
||||
INSERT INTO identity.permission
|
||||
SELECT * FROM public.permission
|
||||
ON CONFLICT (permission_id) DO UPDATE SET
|
||||
page_id = EXCLUDED.page_id,
|
||||
group_id = EXCLUDED.group_id,
|
||||
lasteditedon = EXCLUDED.lasteditedon,
|
||||
lasteditedby = EXCLUDED.lasteditedby,
|
||||
permission = EXCLUDED.permission,
|
||||
requestval = EXCLUDED.requestval;
|
||||
|
||||
INSERT INTO identity.denied_pages
|
||||
SELECT * FROM public.denied_pages
|
||||
ON CONFLICT (uid) DO UPDATE SET
|
||||
page_id = EXCLUDED.page_id,
|
||||
user_id = EXCLUDED.user_id,
|
||||
isdenied = EXCLUDED.isdenied;
|
||||
|
||||
INSERT INTO identity.user_loginhistory
|
||||
SELECT * FROM public.user_loginhistory
|
||||
ON CONFLICT (uid) DO UPDATE SET
|
||||
loginid = EXCLUDED.loginid,
|
||||
logintime = EXCLUDED.logintime,
|
||||
logouttime = EXCLUDED.logouttime,
|
||||
ipaddr = EXCLUDED.ipaddr,
|
||||
user_id = EXCLUDED.user_id;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_identity_app_user_loginid_ci
|
||||
ON identity.app_user (upper(loginid));
|
||||
CREATE INDEX IF NOT EXISTS ix_identity_permission_group_page
|
||||
ON identity.permission (group_id, page_id)
|
||||
WHERE permission <> '000';
|
||||
CREATE INDEX IF NOT EXISTS ix_identity_pages_menu
|
||||
ON identity.pages (parentpage, pageorder DESC)
|
||||
WHERE isvisible = 1;
|
||||
CREATE INDEX IF NOT EXISTS ix_identity_denied_pages_user_page
|
||||
ON identity.denied_pages (user_id, page_id)
|
||||
WHERE isdenied = 1;
|
||||
CREATE INDEX IF NOT EXISTS ix_identity_login_history_user_time
|
||||
ON identity.user_loginhistory (user_id, logintime DESC);
|
||||
|
||||
SELECT setval(
|
||||
'identity.app_user_user_id_seq',
|
||||
GREATEST(COALESCE((SELECT max(user_id) FROM identity.app_user), 1), 1),
|
||||
EXISTS (SELECT 1 FROM identity.app_user));
|
||||
SELECT setval(
|
||||
'identity.company_company_id_seq',
|
||||
GREATEST(COALESCE((SELECT max(company_id) FROM identity.company), 1), 1),
|
||||
EXISTS (SELECT 1 FROM identity.company));
|
||||
SELECT setval(
|
||||
'identity.company_branch_branch_id_seq',
|
||||
GREATEST(COALESCE((SELECT max(branch_id) FROM identity.company_branch), 1), 1),
|
||||
EXISTS (SELECT 1 FROM identity.company_branch));
|
||||
SELECT setval(
|
||||
'identity.user_group_group_id_seq',
|
||||
GREATEST(COALESCE((SELECT max(group_id) FROM identity.user_group), 1), 1),
|
||||
EXISTS (SELECT 1 FROM identity.user_group));
|
||||
SELECT setval(
|
||||
'identity.pages_page_id_seq',
|
||||
GREATEST(COALESCE((SELECT max(page_id) FROM identity.pages), 1), 1),
|
||||
EXISTS (SELECT 1 FROM identity.pages));
|
||||
SELECT setval(
|
||||
'identity.permission_permission_id_seq',
|
||||
GREATEST(COALESCE((SELECT max(permission_id) FROM identity.permission), 1), 1),
|
||||
EXISTS (SELECT 1 FROM identity.permission));
|
||||
SELECT setval(
|
||||
'identity.denied_pages_uid_seq',
|
||||
GREATEST(COALESCE((SELECT max(uid) FROM identity.denied_pages), 1), 1),
|
||||
EXISTS (SELECT 1 FROM identity.denied_pages));
|
||||
SELECT setval(
|
||||
'identity.user_loginhistory_uid_seq',
|
||||
GREATEST(COALESCE((SELECT max(uid) FROM identity.user_loginhistory), 1), 1),
|
||||
EXISTS (SELECT 1 FROM identity.user_loginhistory));
|
||||
|
||||
COMMIT;
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,13 +1,38 @@
|
||||
com/cygnus/cloud/security/CommunicationSecurityProperties.class
|
||||
com/cygnus/cloud/security/JwtReplayProtectionService.class
|
||||
com/cygnus/cloud/system/SystemConfiguration.class
|
||||
com/cygnus/cloud/identity/repository/IdentityRowMapper.class
|
||||
com/cygnus/cloud/security/TokenResponse.class
|
||||
com/cygnus/cloud/identity/api/IdentityErrorHandler.class
|
||||
com/cygnus/cloud/security/MachineTokenController.class
|
||||
com/cygnus/cloud/cache/CacheProperties.class
|
||||
com/cygnus/cloud/security/CloudSecurityConfiguration.class
|
||||
com/cygnus/cloud/database/VertxDatabaseConfiguration.class
|
||||
com/cygnus/cloud/identity/service/AuthenticationException.class
|
||||
com/cygnus/cloud/database/ReactiveDatabaseClient.class
|
||||
com/cygnus/cloud/security/AudienceValidator.class
|
||||
com/cygnus/cloud/security/MachineSecurityConfigurationValidator.class
|
||||
com/cygnus/cloud/identity/api/EncryptedLoginRequest.class
|
||||
com/cygnus/cloud/security/AccessTokenIssuer.class
|
||||
com/cygnus/cloud/identity/api/LoginEncryptionProperties.class
|
||||
com/cygnus/cloud/identity/service/LoginRequestReplayService.class
|
||||
com/cygnus/cloud/identity/service/IdentityAuthenticationService.class
|
||||
com/cygnus/cloud/security/MachineTokenErrorHandler.class
|
||||
com/cygnus/cloud/security/MachineClientPrincipal.class
|
||||
com/cygnus/cloud/system/SystemInfoResponse.class
|
||||
com/cygnus/cloud/identity/api/LoginPayload.class
|
||||
com/cygnus/cloud/security/CommunicationSecurityProperties.class
|
||||
com/cygnus/cloud/system/SystemConfiguration.class
|
||||
com/cygnus/cloud/security/MachineAuthenticationException.class
|
||||
com/cygnus/cloud/security/PemKeyLoader.class
|
||||
com/cygnus/cloud/security/CloudSecurityConfiguration.class
|
||||
com/cygnus/cloud/database/VertxDatabaseConfiguration.class
|
||||
com/cygnus/cloud/security/CommunicationSecurityProperties$MachineClient.class
|
||||
com/cygnus/cloud/identity/repository/IdentityRepository.class
|
||||
com/cygnus/cloud/identity/service/LegacyPasswordVerifier.class
|
||||
com/cygnus/cloud/system/SystemInfoController.class
|
||||
com/cygnus/cloud/CygnusCloudServiceApplication.class
|
||||
com/cygnus/cloud/cache/ReactiveCacheService.class
|
||||
com/cygnus/cloud/identity/model/MenuItem.class
|
||||
com/cygnus/cloud/identity/model/AuthenticatedIdentity.class
|
||||
com/cygnus/cloud/identity/api/CloudLoginController.class
|
||||
com/cygnus/cloud/security/ClientAssertionValidator.class
|
||||
com/cygnus/cloud/identity/model/IdentityUser.class
|
||||
com/cygnus/cloud/identity/api/RsaLoginPayloadDecryptor.class
|
||||
com/cygnus/cloud/database/DatabaseProperties.class
|
||||
com/cygnus/cloud/system/SystemInfoResponse.class
|
||||
|
||||
@@ -4,10 +4,34 @@
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/database/DatabaseProperties.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/database/ReactiveDatabaseClient.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/database/VertxDatabaseConfiguration.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/identity/api/CloudLoginController.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/identity/api/EncryptedLoginRequest.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/identity/api/IdentityErrorHandler.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/identity/api/LoginEncryptionProperties.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/identity/api/LoginPayload.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/identity/api/RsaLoginPayloadDecryptor.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/identity/model/AuthenticatedIdentity.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/identity/model/IdentityUser.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/identity/model/MenuItem.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/identity/repository/IdentityRepository.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/identity/repository/IdentityRowMapper.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/identity/service/AuthenticationException.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/identity/service/IdentityAuthenticationService.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/identity/service/LegacyPasswordVerifier.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/identity/service/LoginRequestReplayService.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/AccessTokenIssuer.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/AudienceValidator.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/ClientAssertionValidator.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/CloudSecurityConfiguration.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/CommunicationSecurityProperties.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/JwtReplayProtectionService.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/MachineAuthenticationException.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/MachineClientPrincipal.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/MachineSecurityConfigurationValidator.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/MachineTokenController.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/MachineTokenErrorHandler.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/PemKeyLoader.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/TokenResponse.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/system/SystemConfiguration.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/system/SystemInfoController.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/main/java/com/cygnus/cloud/system/SystemInfoResponse.java
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
com/cygnus/cloud/identity/service/IdentityAuthenticationServiceTest.class
|
||||
com/cygnus/cloud/InfrastructureConfigurationTest.class
|
||||
com/cygnus/cloud/identity/service/LegacyPasswordVerifierTest.class
|
||||
com/cygnus/cloud/security/MachineTokenFlowTest.class
|
||||
com/cygnus/cloud/system/SystemInfoControllerTest$FixedClockConfiguration.class
|
||||
com/cygnus/cloud/system/SystemInfoControllerTest.class
|
||||
com/cygnus/cloud/InfrastructureConfigurationTest.class
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/test/java/com/cygnus/cloud/InfrastructureConfigurationTest.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/test/java/com/cygnus/cloud/identity/service/IdentityAuthenticationServiceTest.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/test/java/com/cygnus/cloud/identity/service/LegacyPasswordVerifierTest.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/test/java/com/cygnus/cloud/security/MachineTokenFlowTest.java
|
||||
/Users/maddy/Projects/matrix/cygnus-cloud-service/src/test/java/com/cygnus/cloud/system/SystemInfoControllerTest.java
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,130 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.cygnus.cloud.InfrastructureConfigurationTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.673 s -- in com.cygnus.cloud.InfrastructureConfigurationTest
|
||||
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 2.766 s <<< FAILURE! -- in com.cygnus.cloud.InfrastructureConfigurationTest
|
||||
com.cygnus.cloud.InfrastructureConfigurationTest.communicationInfrastructureStartsWithoutOpeningExternalConnections -- Time elapsed: 0.001 s <<< ERROR!
|
||||
java.lang.IllegalStateException: Failed to load ApplicationContext for [ReactiveWebMergedContextConfiguration@5c7c22ce testClass = com.cygnus.cloud.InfrastructureConfigurationTest, locations = [], classes = [com.cygnus.cloud.CygnusCloudServiceApplication], contextInitializerClasses = [], activeProfiles = [], propertySourceDescriptors = [], propertySourceProperties = ["cygnus.security.enabled=false", "org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true", "server.port=0"], contextCustomizers = [org.springframework.boot.web.server.context.SpringBootTestRandomPortContextCustomizer@6ad6443, org.springframework.boot.test.context.PropertyMappingContextCustomizer@0, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@545d2560, org.springframework.boot.test.http.client.DisableReactorResourceFactoryGlobalResourcesContextCustomizerFactory$DisableReactorResourceFactoryGlobalResourcesContextCustomizerCustomizer@46bb0bdf, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@7ceb6c45, org.springframework.boot.test.autoconfigure.OnFailureConditionReportContextCustomizerFactory$OnFailureConditionReportContextCustomizer@723742b2, org.springframework.test.context.support.DynamicPropertiesContextCustomizer@0, org.springframework.boot.test.context.SpringBootTestAnnotation@605d3a8b], contextLoader = org.springframework.boot.test.context.SpringBootContextLoader, parent = null]
|
||||
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.lambda$loadContext$0(DefaultCacheAwareContextLoaderDelegate.java:195)
|
||||
at org.springframework.test.context.cache.DefaultContextCache.put(DefaultContextCache.java:214)
|
||||
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:160)
|
||||
at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:128)
|
||||
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:156)
|
||||
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:111)
|
||||
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:260)
|
||||
at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:242)
|
||||
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
|
||||
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
|
||||
at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
|
||||
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
|
||||
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708)
|
||||
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
|
||||
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
|
||||
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
|
||||
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
|
||||
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
|
||||
at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
|
||||
at java.base/java.util.Optional.orElseGet(Optional.java:364)
|
||||
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
|
||||
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
|
||||
Caused by: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:423)
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:409)
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:613)
|
||||
at java.base/java.lang.Iterable.forEach(Iterable.java:75)
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:379)
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:306)
|
||||
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:1013)
|
||||
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
|
||||
at org.springframework.boot.web.server.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:69)
|
||||
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:756)
|
||||
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:445)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:321)
|
||||
at org.springframework.boot.test.context.SpringBootContextLoader.lambda$loadContext$2(SpringBootContextLoader.java:155)
|
||||
at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:58)
|
||||
at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:46)
|
||||
at org.springframework.boot.SpringApplication.withHook(SpringApplication.java:1465)
|
||||
at org.springframework.boot.test.context.SpringBootContextLoader$ContextLoaderHook.run(SpringBootContextLoader.java:600)
|
||||
at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:155)
|
||||
at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:114)
|
||||
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:247)
|
||||
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.lambda$loadContext$0(DefaultCacheAwareContextLoaderDelegate.java:167)
|
||||
... 21 more
|
||||
Caused by: org.springframework.boot.web.server.WebServerException: Unable to start Netty
|
||||
at org.springframework.boot.reactor.netty.NettyWebServer.start(NettyWebServer.java:140)
|
||||
at org.springframework.boot.web.server.reactive.context.WebServerManager.start(WebServerManager.java:55)
|
||||
at org.springframework.boot.web.server.reactive.context.WebServerStartStopLifecycle.start(WebServerStartStopLifecycle.java:41)
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:420)
|
||||
... 41 more
|
||||
Caused by: reactor.netty.ChannelBindException: Failed to bind on [0.0.0.0:0]
|
||||
Suppressed: java.lang.Exception: #block terminated with an error
|
||||
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:145)
|
||||
at reactor.core.publisher.Mono.block(Mono.java:1800)
|
||||
at reactor.netty.transport.ServerTransport.bindNow(ServerTransport.java:160)
|
||||
at reactor.netty.transport.ServerTransport.bindNow(ServerTransport.java:145)
|
||||
at org.springframework.boot.reactor.netty.NettyWebServer.startHttpServer(NettyWebServer.java:188)
|
||||
at org.springframework.boot.reactor.netty.NettyWebServer.start(NettyWebServer.java:123)
|
||||
at org.springframework.boot.web.server.reactive.context.WebServerManager.start(WebServerManager.java:55)
|
||||
at org.springframework.boot.web.server.reactive.context.WebServerStartStopLifecycle.start(WebServerStartStopLifecycle.java:41)
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:420)
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:409)
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:613)
|
||||
at java.base/java.lang.Iterable.forEach(Iterable.java:75)
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:379)
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:306)
|
||||
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:1013)
|
||||
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
|
||||
at org.springframework.boot.web.server.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:69)
|
||||
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:756)
|
||||
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:445)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:321)
|
||||
at org.springframework.boot.test.context.SpringBootContextLoader.lambda$loadContext$2(SpringBootContextLoader.java:155)
|
||||
at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:58)
|
||||
at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:46)
|
||||
at org.springframework.boot.SpringApplication.withHook(SpringApplication.java:1465)
|
||||
at org.springframework.boot.test.context.SpringBootContextLoader$ContextLoaderHook.run(SpringBootContextLoader.java:600)
|
||||
at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:155)
|
||||
at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:114)
|
||||
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:247)
|
||||
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.lambda$loadContext$0(DefaultCacheAwareContextLoaderDelegate.java:167)
|
||||
at org.springframework.test.context.cache.DefaultContextCache.put(DefaultContextCache.java:214)
|
||||
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:160)
|
||||
at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:128)
|
||||
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:156)
|
||||
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:111)
|
||||
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:260)
|
||||
at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:242)
|
||||
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
|
||||
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
|
||||
at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
|
||||
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
|
||||
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708)
|
||||
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
|
||||
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
|
||||
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
|
||||
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
|
||||
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
|
||||
at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
|
||||
at java.base/java.util.Optional.orElseGet(Optional.java:364)
|
||||
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
|
||||
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
|
||||
Caused by: java.net.SocketException: Operation not permitted
|
||||
at java.base/sun.nio.ch.Net.bind0(Native Method)
|
||||
at java.base/sun.nio.ch.Net.bind(Net.java:565)
|
||||
at java.base/sun.nio.ch.ServerSocketChannelImpl.netBind(ServerSocketChannelImpl.java:344)
|
||||
at java.base/sun.nio.ch.ServerSocketChannelImpl.bind(ServerSocketChannelImpl.java:301)
|
||||
at io.netty.channel.socket.nio.NioServerSocketChannel.doBind(NioServerSocketChannel.java:148)
|
||||
at io.netty.channel.AbstractChannel$AbstractUnsafe.bind(AbstractChannel.java:431)
|
||||
at io.netty.channel.DefaultChannelPipeline$HeadContext.bind(DefaultChannelPipeline.java:1353)
|
||||
at io.netty.channel.AbstractChannelHandlerContext.bind(AbstractChannelHandlerContext.java:500)
|
||||
at io.netty.channel.DefaultChannelPipeline.bind(DefaultChannelPipeline.java:994)
|
||||
at io.netty.channel.Channel.bind(Channel.java:302)
|
||||
at reactor.netty.transport.TransportConnector.lambda$null$0(TransportConnector.java:93)
|
||||
at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:148)
|
||||
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:141)
|
||||
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:535)
|
||||
at io.netty.channel.SingleThreadIoEventLoop.run(SingleThreadIoEventLoop.java:201)
|
||||
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:1195)
|
||||
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
|
||||
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
|
||||
at java.base/java.lang.Thread.run(Thread.java:1583)
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.cygnus.cloud.identity.service.IdentityAuthenticationServiceTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 2, Failures: 0, Errors: 2, Skipped: 0, Time elapsed: 21.33 s <<< FAILURE! -- in com.cygnus.cloud.identity.service.IdentityAuthenticationServiceTest
|
||||
com.cygnus.cloud.identity.service.IdentityAuthenticationServiceTest.rejectsInactiveUserWithoutLoadingMenu -- Time elapsed: 21.32 s <<< ERROR!
|
||||
java.lang.IllegalStateException: Could not initialize plugin: interface org.mockito.plugins.MockMaker (alternate: null)
|
||||
at org.mockito.internal.configuration.plugins.PluginLoader$1.invoke(PluginLoader.java:85)
|
||||
at jdk.proxy2/jdk.proxy2.$Proxy46.isTypeMockable(Unknown Source)
|
||||
at org.mockito.internal.util.MockUtil.typeMockabilityOf(MockUtil.java:78)
|
||||
at org.mockito.internal.util.MockCreationValidator.validateType(MockCreationValidator.java:22)
|
||||
at org.mockito.internal.creation.MockSettingsImpl.validatedSettings(MockSettingsImpl.java:275)
|
||||
at org.mockito.internal.creation.MockSettingsImpl.build(MockSettingsImpl.java:236)
|
||||
at org.mockito.internal.MockitoCore.mock(MockitoCore.java:82)
|
||||
at org.mockito.Mockito.mock(Mockito.java:2198)
|
||||
at org.mockito.internal.configuration.MockAnnotationProcessor.processAnnotationForMock(MockAnnotationProcessor.java:79)
|
||||
at org.mockito.internal.configuration.MockAnnotationProcessor.process(MockAnnotationProcessor.java:28)
|
||||
at org.mockito.internal.configuration.MockAnnotationProcessor.process(MockAnnotationProcessor.java:25)
|
||||
at org.mockito.internal.configuration.IndependentAnnotationEngine.createMockFor(IndependentAnnotationEngine.java:44)
|
||||
at org.mockito.internal.configuration.IndependentAnnotationEngine.process(IndependentAnnotationEngine.java:72)
|
||||
at org.mockito.internal.configuration.InjectingAnnotationEngine.processIndependentAnnotations(InjectingAnnotationEngine.java:62)
|
||||
at org.mockito.internal.configuration.InjectingAnnotationEngine.process(InjectingAnnotationEngine.java:47)
|
||||
at org.mockito.MockitoAnnotations.openMocks(MockitoAnnotations.java:81)
|
||||
at org.mockito.internal.framework.DefaultMockitoSession.<init>(DefaultMockitoSession.java:43)
|
||||
at org.mockito.internal.session.DefaultMockitoSessionBuilder.startMocking(DefaultMockitoSessionBuilder.java:83)
|
||||
at org.mockito.junit.jupiter.MockitoExtension.beforeEach(MockitoExtension.java:160)
|
||||
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
|
||||
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
|
||||
Caused by: java.lang.IllegalStateException: Internal problem occurred, please report it. Mockito is unable to load the default implementation of class that is a part of Mockito distribution. Failed to load interface org.mockito.plugins.MockMaker
|
||||
at org.mockito.internal.configuration.plugins.DefaultMockitoPlugins.create(DefaultMockitoPlugins.java:105)
|
||||
at org.mockito.internal.configuration.plugins.DefaultMockitoPlugins.getDefaultPlugin(DefaultMockitoPlugins.java:79)
|
||||
at org.mockito.internal.configuration.plugins.PluginLoader.loadPlugin(PluginLoader.java:75)
|
||||
at org.mockito.internal.configuration.plugins.PluginLoader.loadPlugin(PluginLoader.java:49)
|
||||
at org.mockito.internal.configuration.plugins.PluginRegistry.<init>(PluginRegistry.java:29)
|
||||
at org.mockito.internal.configuration.plugins.Plugins.<clinit>(Plugins.java:26)
|
||||
at org.mockito.internal.MockitoCore.<clinit>(MockitoCore.java:71)
|
||||
at org.mockito.Mockito.<clinit>(Mockito.java:1777)
|
||||
at org.mockito.junit.jupiter.MockitoExtension.beforeEach(MockitoExtension.java:156)
|
||||
... 2 more
|
||||
Caused by: java.lang.reflect.InvocationTargetException
|
||||
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
|
||||
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486)
|
||||
at org.mockito.internal.configuration.plugins.DefaultMockitoPlugins.create(DefaultMockitoPlugins.java:103)
|
||||
... 10 more
|
||||
Caused by: org.mockito.exceptions.base.MockitoInitializationException:
|
||||
Could not initialize inline Byte Buddy mock maker.
|
||||
|
||||
It appears as if your JDK does not supply a working agent attachment mechanism.
|
||||
Java : 21
|
||||
JVM vendor name : Microsoft
|
||||
JVM vendor version : 21.0.8+9-LTS
|
||||
JVM name : OpenJDK 64-Bit Server VM
|
||||
JVM version : 21.0.8+9-LTS
|
||||
JVM info : mixed mode, sharing
|
||||
OS name : Mac OS X
|
||||
OS version : 26.5.2
|
||||
|
||||
at org.mockito.internal.creation.bytebuddy.InlineDelegateByteBuddyMockMaker.<init>(InlineDelegateByteBuddyMockMaker.java:275)
|
||||
at org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker.<init>(InlineByteBuddyMockMaker.java:23)
|
||||
... 13 more
|
||||
Caused by: java.lang.IllegalStateException: Could not self-attach to current VM using external process - set a property net.bytebuddy.agent.attacher.dump to dump the process output to a file at the specified location
|
||||
at net.bytebuddy.agent.ByteBuddyAgent.installExternal(ByteBuddyAgent.java:672)
|
||||
at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:599)
|
||||
at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:579)
|
||||
at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:531)
|
||||
at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:508)
|
||||
at org.mockito.internal.PremainAttachAccess.getInstrumentation(PremainAttachAccess.java:80)
|
||||
at org.mockito.internal.creation.bytebuddy.InlineDelegateByteBuddyMockMaker.<clinit>(InlineDelegateByteBuddyMockMaker.java:138)
|
||||
... 14 more
|
||||
|
||||
com.cygnus.cloud.identity.service.IdentityAuthenticationServiceTest.authenticatesActiveUserAndReturnsStructuredSessionData -- Time elapsed: 0.003 s <<< ERROR!
|
||||
java.lang.IllegalStateException: Could not initialize plugin: interface org.mockito.plugins.MockMaker (alternate: null)
|
||||
at org.mockito.internal.configuration.plugins.PluginLoader$1.invoke(PluginLoader.java:85)
|
||||
at jdk.proxy2/jdk.proxy2.$Proxy46.isTypeMockable(Unknown Source)
|
||||
at org.mockito.internal.util.MockUtil.typeMockabilityOf(MockUtil.java:78)
|
||||
at org.mockito.internal.util.MockCreationValidator.validateType(MockCreationValidator.java:22)
|
||||
at org.mockito.internal.creation.MockSettingsImpl.validatedSettings(MockSettingsImpl.java:275)
|
||||
at org.mockito.internal.creation.MockSettingsImpl.build(MockSettingsImpl.java:236)
|
||||
at org.mockito.internal.MockitoCore.mock(MockitoCore.java:82)
|
||||
at org.mockito.Mockito.mock(Mockito.java:2198)
|
||||
at org.mockito.internal.configuration.MockAnnotationProcessor.processAnnotationForMock(MockAnnotationProcessor.java:79)
|
||||
at org.mockito.internal.configuration.MockAnnotationProcessor.process(MockAnnotationProcessor.java:28)
|
||||
at org.mockito.internal.configuration.MockAnnotationProcessor.process(MockAnnotationProcessor.java:25)
|
||||
at org.mockito.internal.configuration.IndependentAnnotationEngine.createMockFor(IndependentAnnotationEngine.java:44)
|
||||
at org.mockito.internal.configuration.IndependentAnnotationEngine.process(IndependentAnnotationEngine.java:72)
|
||||
at org.mockito.internal.configuration.InjectingAnnotationEngine.processIndependentAnnotations(InjectingAnnotationEngine.java:62)
|
||||
at org.mockito.internal.configuration.InjectingAnnotationEngine.process(InjectingAnnotationEngine.java:47)
|
||||
at org.mockito.MockitoAnnotations.openMocks(MockitoAnnotations.java:81)
|
||||
at org.mockito.internal.framework.DefaultMockitoSession.<init>(DefaultMockitoSession.java:43)
|
||||
at org.mockito.internal.session.DefaultMockitoSessionBuilder.startMocking(DefaultMockitoSessionBuilder.java:83)
|
||||
at org.mockito.junit.jupiter.MockitoExtension.beforeEach(MockitoExtension.java:160)
|
||||
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
|
||||
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
|
||||
Caused by: java.lang.IllegalStateException: Internal problem occurred, please report it. Mockito is unable to load the default implementation of class that is a part of Mockito distribution. Failed to load interface org.mockito.plugins.MockMaker
|
||||
at org.mockito.internal.configuration.plugins.DefaultMockitoPlugins.create(DefaultMockitoPlugins.java:105)
|
||||
at org.mockito.internal.configuration.plugins.DefaultMockitoPlugins.getDefaultPlugin(DefaultMockitoPlugins.java:79)
|
||||
at org.mockito.internal.configuration.plugins.PluginLoader.loadPlugin(PluginLoader.java:75)
|
||||
at org.mockito.internal.configuration.plugins.PluginLoader.loadPlugin(PluginLoader.java:49)
|
||||
at org.mockito.internal.configuration.plugins.PluginRegistry.<init>(PluginRegistry.java:29)
|
||||
at org.mockito.internal.configuration.plugins.Plugins.<clinit>(Plugins.java:26)
|
||||
at org.mockito.internal.MockitoCore.<clinit>(MockitoCore.java:71)
|
||||
at org.mockito.Mockito.<clinit>(Mockito.java:1777)
|
||||
at org.mockito.junit.jupiter.MockitoExtension.beforeEach(MockitoExtension.java:156)
|
||||
... 2 more
|
||||
Caused by: java.lang.reflect.InvocationTargetException
|
||||
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
|
||||
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486)
|
||||
at org.mockito.internal.configuration.plugins.DefaultMockitoPlugins.create(DefaultMockitoPlugins.java:103)
|
||||
... 10 more
|
||||
Caused by: org.mockito.exceptions.base.MockitoInitializationException:
|
||||
Could not initialize inline Byte Buddy mock maker.
|
||||
|
||||
It appears as if your JDK does not supply a working agent attachment mechanism.
|
||||
Java : 21
|
||||
JVM vendor name : Microsoft
|
||||
JVM vendor version : 21.0.8+9-LTS
|
||||
JVM name : OpenJDK 64-Bit Server VM
|
||||
JVM version : 21.0.8+9-LTS
|
||||
JVM info : mixed mode, sharing
|
||||
OS name : Mac OS X
|
||||
OS version : 26.5.2
|
||||
|
||||
at org.mockito.internal.creation.bytebuddy.InlineDelegateByteBuddyMockMaker.<init>(InlineDelegateByteBuddyMockMaker.java:275)
|
||||
at org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker.<init>(InlineByteBuddyMockMaker.java:23)
|
||||
... 13 more
|
||||
Caused by: java.lang.IllegalStateException: Could not self-attach to current VM using external process - set a property net.bytebuddy.agent.attacher.dump to dump the process output to a file at the specified location
|
||||
at net.bytebuddy.agent.ByteBuddyAgent.installExternal(ByteBuddyAgent.java:672)
|
||||
at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:599)
|
||||
at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:579)
|
||||
at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:531)
|
||||
at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:508)
|
||||
at org.mockito.internal.PremainAttachAccess.getInstrumentation(PremainAttachAccess.java:80)
|
||||
at org.mockito.internal.creation.bytebuddy.InlineDelegateByteBuddyMockMaker.<clinit>(InlineDelegateByteBuddyMockMaker.java:138)
|
||||
... 14 more
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.cygnus.cloud.identity.service.LegacyPasswordVerifierTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.052 s -- in com.cygnus.cloud.identity.service.LegacyPasswordVerifierTest
|
||||
@@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.cygnus.cloud.security.MachineTokenFlowTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.101 s -- in com.cygnus.cloud.security.MachineTokenFlowTest
|
||||
@@ -1,4 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.cygnus.cloud.system.SystemInfoControllerTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.908 s -- in com.cygnus.cloud.system.SystemInfoControllerTest
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.383 s -- in com.cygnus.cloud.system.SystemInfoControllerTest
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user