Machine Installation id based Oauth 2

This commit is contained in:
2026-07-26 15:19:27 +05:30
parent dcb40473da
commit a4daf7e204
365 changed files with 23720 additions and 966 deletions

View 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.

View File

@@ -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();
}
}

View File

@@ -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) {
}

View File

@@ -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");
}
}

View File

@@ -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) {
}

View File

@@ -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) {
}

View File

@@ -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;
}
}
}

View File

@@ -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) {
}

View File

@@ -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) {
}

View File

@@ -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) {
}

View File

@@ -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"));
}
}

View File

@@ -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"));
}
}

View File

@@ -0,0 +1,8 @@
package com.cygnus.cloud.identity.service;
public class AuthenticationException extends RuntimeException {
public AuthenticationException(String message) {
super(message);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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");
}
}

View File

@@ -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()),

View File

@@ -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) {
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,9 @@
package com.cygnus.cloud.security;
import java.util.Set;
record MachineClientPrincipal(
String clientId,
String installationId,
Set<String> allowedScopes) {
}

View File

@@ -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);
}
}

View File

@@ -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+")));
}
}

View File

@@ -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");
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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}

View File

@@ -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;

View File

@@ -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");
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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}

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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