Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions UnityAuth/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,75 @@
# UnityAuth
Unity foundation security server

## Developer Setup

### Prerequisites

- **Java 21** (required - the project will not build with other versions)
- Gradle (wrapper included)

If you use SDKMAN, you can install and switch to Java 21:
```bash
sdk install java 21.0.2-tem # or any Java 21 distribution
sdk use java 21.0.2-tem
```

### Running Tests

Run tests from the `UnityAuth` directory:
```bash
cd UnityAuth
./gradlew test
```

**Note:** Tests explicitly use the `test` environment via `@MicronautTest(environments = "test")`, so they will work correctly even if you have `MICRONAUT_ENVIRONMENTS=local` set in your shell.

### Running the Application

```bash
cd UnityAuth
source ../setenv.sh # Set environment variables (edit first with your DB credentials)
./gradlew run
```

## CORS Configuration

UnityAuth includes CORS (Cross-Origin Resource Sharing) configuration to allow frontend applications to make requests to the API.

### Allowed Origins

CORS is configured to allow requests from:
- `http://localhost:3000` and `http://localhost:3001` (local development)
- `http://127.0.0.1:3000` and `http://127.0.0.1:3001` (local development)
- Docker container hostnames (e.g., `http://unity-auth-ui:3001`)

### Configuration Files

CORS settings are defined in environment-specific configuration files:
- `application-local.yml` - Local development
- `application-docker.yml` - Docker environment
- `application-test.yml` - Test environment

### Example Configuration

```yaml
micronaut:
server:
cors:
enabled: true
configurations:
web:
allowed-origins-regex: '^http:\/\/(.*?)(?:localhost|127\.0\.0\.1)(?::\d+)?$'
allowedOrigins:
- http://localhost:3000
- http://localhost:3001
localhost-pass-through: true
```

### Production Considerations

For production deployments, update the `allowed-origins-regex` and `allowedOrigins` to match your actual frontend domain(s).

## Usage:
Insert this code to the client application.yaml file
```
Expand Down
31 changes: 31 additions & 0 deletions UnityAuth/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
id("io.micronaut.application") version "${micronautPluginVersion}"
id("io.micronaut.test-resources") version "${micronautPluginVersion}"
id("io.micronaut.aot") version "${micronautPluginVersion}"
id("jacoco")
}

version = "0.1"
Expand Down Expand Up @@ -31,6 +32,8 @@ dependencies {
runtimeOnly("org.flywaydb:flyway-mysql")
runtimeOnly("org.yaml:snakeyaml")
testImplementation("io.micronaut:micronaut-http-client")
testImplementation("org.junit.jupiter:junit-jupiter-params")
testImplementation("org.mockito:mockito-core:5.11.0")

aotPlugins platform("io.micronaut.platform:micronaut-platform:${micronautPluginVersion}")
aotPlugins("io.micronaut.security:micronaut-security-aot")
Expand Down Expand Up @@ -75,3 +78,31 @@ micronaut {
configurationProperties.put("micronaut.security.jwks.enabled","false")
}
}

// JaCoCo Code Coverage Configuration
jacoco {
toolVersion = "0.8.11"
}

tasks.named('test') {
finalizedBy jacocoTestReport
}

tasks.named('jacocoTestReport') {
dependsOn test
reports {
xml.required = true
html.required = true
csv.required = false
}
}

tasks.named('jacocoTestCoverageVerification') {
violationRules {
rule {
limit {
minimum = 0.50 // 50% minimum coverage threshold
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

@Property(name = "jwk.primary", value = "{\"p\":\"_OZyH1Mk3wR0oXw1C31t4kWOcaHFB6Njro1cYx52REnPiznn_JTtwvlAMpvV6LVCIZPgKMzdIEMY1gYs1LsO-5IFqWwegXmYJ0iKXbRrZshfWBCzRLK3QK5fER1le1XUBDhtDk7KIW_Xg-SZF4pf_LUEVKMnyUpspGI5F77jlJ8\",\"kty\":\"RSA\",\"q\":\"s9wvl7z8vkHQvo9xOUp-z0a2Z7LFBDil2uIjPh1FQzs34gFXH8dQPRox83TuN5d4KzdLPqQNQAfMXU9_KmxihNb_qDQahYugeELmcem04munxXqBdyZqWhWCy5YmujYqn44irwvoTbw6_RkMqjCmINPTPadptlPivsZ6RhKn8zk\",\"d\":\"ok3wmhOy8NZEHAotnFiH6ecFD6xf_9x33_fMRkqa3_KE8NZM7vmvNgElox2UvcP_2K5E7jOdL2XQdJCTIW3Qlj66yE2a84SYlbvxIc4hDrIog0XNt4FhavvshxxUIfDQo6Q8qXDR5v7nwt6SCopYC3t3KVRdJh08GzKoVxysd7afJjxXxx178gY29uMRqnwxFN1OGnWaiBr-xGKb1frJ6jOI1zvuuCaljZ4aZjc9vOR4y9ZmobgrzkMFnpDAmQZ7MWcVMyodRMOA2dEOckywPhg-dIVNiVIqzJqe5Yg1ilNookjwtqj2TpNU7Z9gPqzYB73PmQ2p5LMDheAPxcOmEQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"e3be37177a7c42bcbadd7cc63715f216\",\"qi\":\"r--nAtaYPAgJq_8R1-kynpd53E17n-loDUgtVWBCx_RmdORX4Auilv1S83dD1mbcnYCbV_LmxiEjOiz-4gS_E0qVGqakAqQrO1hVUvJa_Y2uftDgwFmuJNGbpRU-K4Td_uUzdm48za8yJCgOdYsWp6PNMCcmQgiInzkR3XYV83I\",\"dp\":\"oQUcvmMSw8gzdin-IB2xW_MLecAVEgLu0dGBdD6N8HbKZQvub_xm0dAfFtnvvWXDAFwFyhR96i-uXX67Bos_Q9-6KSAE4E0KGmDucDESfPOw-QJREbl0QgOD1gLQfVGtVy6SCR0TR2zNXFWtP7bD3MNoSXdEOr5fI97CGSNaBWM\",\"alg\":\"RS256\",\"dq\":\"DM-WJDy10-dkMu6MpgQEXEcxHtnA5rgSODD7SaVUFaHWLSbjScQslu2SuUCO5y7GxG0_0spklzb2-356FE98BPI7a4Oqj_COEYLSXzLCS45XeN1s80utL5Vwp4eeYo0RJCQ_nDBA76iEmxp5qHWmn5f25-FQykfXUrdYZj1V8SE\",\"n\":\"sa6m2i-iNvj6ZSTdSHZaBrnv6DId4AqAXhOyl0yA5fNWYe6r51h24SXqk7DsGYHHh74ii74tP1lTpmy6RD67tCK-tbN-d6yc4Z6FfM8R83v2QZUfaAixgHGtw0n2toqsiHf6EloDV-B8q4GYyKDD6cLecoaIuTmMBTY3kts59U2t9W10YoLGsmFqLSz8qNF5HkahzB6_--2DiBfVGUKAXHC-SICGZCi-8efOetv6pt9vFiWEgwU_DgjRNYzLFt1SEmbGFUU4kbjQ7tNTMkHfzfwcT6qLt4kVKy2FNYsEMk24keWtCvW_RyO_fisZc0W9smX7WtYjEXhcAjDeqHgEZw\"}")
@Property(name = "jwk.secondary", value = "{\"p\":\"4qJ9RNlu6SuDT_MLArfzimvKEwmet_j12Z9EQeb5nMjZIOHTcWw__duebUytfWwxsRHhtSVXeMt-EryQAOulm2p1bfiVuparq93z9P5cPnb0oArFaw3eFNFEmX5U-lY8PzUTTsFxO4aVQYAKXD6DP7p5uPzuwpHFuNc71nNIXZE\",\"kty\":\"RSA\",\"q\":\"v4OhkWMbS_nq77HFanwZAT_obfJuQfOFOQBORL4ATAHGUXm2y4YqLNExZs7Wj1MA_6ya6Y00s2JBM7fWq_fPe4d9xo5aGrPdcp0G8W21kkfh9vuVPlHVQTgSP7FQ9qahvXxNwK_11yNr3p1HBmScJ5mHlMBpIJsFcvHA-uXe0Ps\",\"d\":\"EunrjnQ1-jJPSCrt2L94PUpDrakup8a4pXys52YSkJY-W6XidM0roOS6kr06P3G6VQgc6AL_BkvTQ_XS0oXHbXVprDQ5Syam5p9oxHBhhW_vSqIMgUOfm28uyB3Mtw9rBxdUxW3yElHioaR8a-exYhhyVXb1QEhxL_rcnthmhAkM2NcHi2UnxGKFTsC0abQ2MuQc1OAuW5veDiIF2hfdC41qE0_d8vB6FDWbblgUpbwB6uSZaViPs15Buq2oX9dCCw54-PgzkfehDt7lyqgupktbV1psnVVhL86shzt4QFnhd3k7VpFbjCNFtiJTrufV-XBWT0pl2w3VR9wrHJ1bYQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"0794e938379540dc8eaa559508524a79\",\"qi\":\"jy-TNyXVy_44_n4KGAwIbZO2C4r6uNWuEdehBfQKkPhiP90myG1KZVfOoKNOK9bCv2mvZJcBz4c1ArElgpuSCV4-KFac1ZzQo_ic5aoIej8Qa80y2ogc-_Yv6_ZLHC1S76M-lm4jayk2-rvuBpy2pUvHbW6Srhs_szwz7ZfSkLg\",\"dp\":\"ApqdV9ortRAj7Ro8ySY17SQ56SgWI8T_hiWXUi6GNa_1FrShik8VGSSZ2GWmJKfGlmM_NaadL60e4LY77VbHy1ZYzQ-rIL60cEAXmnwFsU4Kl4AoLoe1QoX5BM53yXyOKqfAdgow898i_eKru82YEnZhCagWUjP8kpgefuNKNJE\",\"alg\":\"RS256\",\"dq\":\"bFF78WoXh0pMCdQHL2oPDnjh8kWa_OxKHmpA2nqIWnTqgSyRKd2xPvX2tgooqpmsx-8NEymNdCQPcrv4y_z2OgzxI3tiFRZEGs4bnjOJ7bmAYZv71mqcbi3TjHiyrT6j3jNPGrurFUpweVGFWWVQOMmKOKT3ELz9QPzhREb9Vj8\",\"n\":\"qYvDpV8DRU5hx9eXpE4Ms8nUXicEwrxUUz5gb5gkXpIeY82mqfQKKCP6PSFnkKYtRFTOUSm9cgGGfOd7O4NFsIsxLwXCj34X7ORr19eXKBLvG3bZJLxqRlbYuQshDMkQOui1sDBxvYnj5p4iHne6l2btH5grHOCShUWG-bKps5Y8bKNHod1pIOOBabVCmn3sUVUkZw8nyXkQqZbv-c8x6z0TEfhNOPOIt2AmmlNgrE_8g7-dnCvqfJnhv0c7qkOJzsb7OMmvVwsQNiM59D6uaWZr-vdANo6NggiZmCKUS3tpUvdXW7ec9WMPJWhrVEkRcbWXQnZ_C7pXFrz7rLeNKw\"}")
@MicronautTest
@MicronautTest(environments = "test")
class UnityIamTest {

@Inject
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package io.unityfoundation.auth;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.*;

/**
* Unit tests for BCryptPasswordEncoder.
* Tests password encoding and verification functionality.
*/
class BCryptPasswordEncoderTest {

private BCryptPasswordEncoder encoder;

@BeforeEach
void setUp() {
encoder = new BCryptPasswordEncoder();
}

@Test
void encode_producesValidBCryptHash() {
String rawPassword = "testPassword123";

String encoded = encoder.encode(rawPassword);

assertNotNull(encoded);
assertTrue(encoded.startsWith("$2a$"), "Should produce BCrypt hash with $2a$ prefix");
assertEquals(60, encoded.length(), "BCrypt hash should be 60 characters");
}

@Test
void encode_producesUniqueHashesForSamePassword() {
String rawPassword = "testPassword123";

String encoded1 = encoder.encode(rawPassword);
String encoded2 = encoder.encode(rawPassword);

assertNotEquals(encoded1, encoded2, "Same password should produce different hashes due to salt");
}

@Test
void matches_returnsTrueForCorrectPassword() {
String rawPassword = "testPassword123";
String encoded = encoder.encode(rawPassword);

boolean matches = encoder.matches(rawPassword, encoded);

assertTrue(matches, "Should match when raw password is correct");
}

@Test
void matches_returnsFalseForIncorrectPassword() {
String rawPassword = "testPassword123";
String wrongPassword = "wrongPassword456";
String encoded = encoder.encode(rawPassword);

boolean matches = encoder.matches(wrongPassword, encoded);

assertFalse(matches, "Should not match when raw password is incorrect");
}

@Test
void matches_returnsFalseForEmptyPassword() {
String rawPassword = "testPassword123";
String encoded = encoder.encode(rawPassword);

boolean matches = encoder.matches("", encoded);

assertFalse(matches, "Should not match empty password");
}

@Test
void matches_isCaseSensitive() {
String rawPassword = "TestPassword123";
String encoded = encoder.encode(rawPassword);

assertFalse(encoder.matches("testpassword123", encoded), "Should be case sensitive");
assertFalse(encoder.matches("TESTPASSWORD123", encoded), "Should be case sensitive");
assertTrue(encoder.matches("TestPassword123", encoded), "Exact match should work");
}

@ParameterizedTest
@ValueSource(strings = {"a", "short", "mediumPassword", "aVeryLongPasswordThatExceeds72Characters123456789012345678901234567890"})
void encode_handlesVariousPasswordLengths(String password) {
String encoded = encoder.encode(password);

assertNotNull(encoded);
assertTrue(encoder.matches(password, encoded), "Should match for password: " + password);
}

@Test
void encode_handlesSpecialCharacters() {
String specialPassword = "p@$$w0rd!#$%^&*()_+-=[]{}|;':\",./<>?";

String encoded = encoder.encode(specialPassword);

assertNotNull(encoded);
assertTrue(encoder.matches(specialPassword, encoded), "Should handle special characters");
}

@Test
void encode_handlesUnicodeCharacters() {
String unicodePassword = "密码パスワード🔐";

String encoded = encoder.encode(unicodePassword);

assertNotNull(encoded);
assertTrue(encoder.matches(unicodePassword, encoded), "Should handle unicode characters");
}

@Test
void matches_handlesWhitespace() {
String passwordWithSpaces = "password with spaces";
String encoded = encoder.encode(passwordWithSpaces);

assertTrue(encoder.matches(passwordWithSpaces, encoded));
assertFalse(encoder.matches("passwordwithspaces", encoded), "Should preserve whitespace");
assertFalse(encoder.matches(" password with spaces", encoded), "Should preserve leading whitespace");
}

@Test
void matches_worksWithKnownBCryptHash() {
// Pre-computed BCrypt hash for "test" (from test data in afterMigrate.sql)
String knownHash = "$2a$10$YJetsyoS.EzlVlb249w07uBR8uSqgtlqVH9Hl7bsHtvvwdKAhJp82";

assertTrue(encoder.matches("test", knownHash), "Should verify against known hash from test data");
assertFalse(encoder.matches("wrong", knownHash), "Should not verify incorrect password against known hash");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.unityfoundation.auth;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.*;

/**
* Unit tests for NullOrNotBlankValidator.
* Tests the custom validation constraint that allows null but rejects blank strings.
*/
class NullOrNotBlankValidatorTest {

private NullOrNotBlankValidator validator;

@BeforeEach
void setUp() {
validator = new NullOrNotBlankValidator();
}

@Test
void isValid_returnsTrueForNull() {
boolean result = validator.isValid(null, null);

assertTrue(result, "Null should be valid");
}

@ParameterizedTest
@ValueSource(strings = {"a", "valid", "valid value", " valid with leading spaces", "valid with trailing spaces "})
void isValid_returnsTrueForNonBlankStrings(String value) {
boolean result = validator.isValid(value, null);

assertTrue(result, "Non-blank string should be valid: '" + value + "'");
}

@Test
void isValid_returnsFalseForEmptyString() {
boolean result = validator.isValid("", null);

assertFalse(result, "Empty string should be invalid");
}

@ParameterizedTest
@ValueSource(strings = {" ", " ", "\t", "\n", "\r", "\t\n\r ", " \t "})
void isValid_returnsFalseForWhitespaceOnlyStrings(String value) {
boolean result = validator.isValid(value, null);

assertFalse(result, "Whitespace-only string should be invalid: '" + value.replace("\t", "\\t").replace("\n", "\\n").replace("\r", "\\r") + "'");
}

@Test
void isValid_returnsTrueForStringWithContent() {
assertTrue(validator.isValid("x", null), "Single character should be valid");
assertTrue(validator.isValid("hello world", null), "Normal string should be valid");
assertTrue(validator.isValid(" hello ", null), "String with content and surrounding whitespace should be valid");
}

@Test
void isValid_returnsTrueForSpecialCharacters() {
assertTrue(validator.isValid("!@#$%^&*()", null), "Special characters should be valid");
assertTrue(validator.isValid("123", null), "Numbers should be valid");
assertTrue(validator.isValid("日本語", null), "Unicode characters should be valid");
}
}
Loading