Merge pull request 'feature/auth-harmonization' (#155) from feature/auth-harmonization into main
Reviewed-on: #155
This commit was merged in pull request #155.
This commit is contained in:
@@ -84,6 +84,12 @@ jobs:
|
|||||||
- name: Build and Test
|
- name: Build and Test
|
||||||
run: mvn clean verify -DskipITs -U --batch-mode
|
run: mvn clean verify -DskipITs -U --batch-mode
|
||||||
|
|
||||||
|
- name: Deploy minter to Maven registry
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
run: mvn deploy -DskipTests -DskipITs --batch-mode -pl .,cameleer-server-core,cameleer-license-minter
|
||||||
|
env:
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.cameleer.server.app.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
@Schema(description = "Authentication capabilities reported to the SPA so it can render the login page deterministically")
|
||||||
|
public record AuthCapabilitiesResponse(
|
||||||
|
@Schema(description = "OIDC interactive login capability") Oidc oidc,
|
||||||
|
@Schema(description = "Local username/password account capability") LocalAccounts localAccounts
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Schema(description = "OIDC interactive login")
|
||||||
|
public record Oidc(
|
||||||
|
@Schema(description = "Whether OIDC is configured AND enabled") boolean enabled,
|
||||||
|
@Schema(description = "Best-effort display label, e.g. \"Logto\", \"Keycloak\", \"Single Sign-On\"") @NotNull String providerName,
|
||||||
|
@Schema(description = "When true, OIDC is the canonical entry point and the SPA hides the local form unless ?local is set") boolean primary
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Schema(description = "Local username/password accounts")
|
||||||
|
public record LocalAccounts(
|
||||||
|
@Schema(description = "Whether the local form is reachable at all") boolean enabled,
|
||||||
|
@Schema(description = "When true, the SPA gates the local form behind ?local with an admin-recovery banner") boolean adminRecoveryOnly
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.cameleer.server.app.security;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure utility — derives a display label for an OIDC provider from its issuer URI.
|
||||||
|
* Used by {@link AuthCapabilitiesController} so the SPA can render
|
||||||
|
* "Sign in with {providerName}" on the login page.
|
||||||
|
*
|
||||||
|
* <p>Pattern-match only — never network-discover. If the issuer doesn't match a
|
||||||
|
* known vendor pattern, we return the generic "Single Sign-On" label rather than
|
||||||
|
* leaking hostnames into the UI.
|
||||||
|
*/
|
||||||
|
public final class OidcProviderNameDeriver {
|
||||||
|
|
||||||
|
private static final String GENERIC = "Single Sign-On";
|
||||||
|
|
||||||
|
private OidcProviderNameDeriver() {}
|
||||||
|
|
||||||
|
public static String deriveName(String issuerUri) {
|
||||||
|
if (issuerUri == null || issuerUri.isBlank()) {
|
||||||
|
return GENERIC;
|
||||||
|
}
|
||||||
|
String host;
|
||||||
|
try {
|
||||||
|
URI uri = URI.create(issuerUri.trim());
|
||||||
|
host = uri.getHost();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return GENERIC;
|
||||||
|
}
|
||||||
|
if (host == null || host.isBlank()) {
|
||||||
|
return GENERIC;
|
||||||
|
}
|
||||||
|
String h = host.toLowerCase();
|
||||||
|
if (h.contains("logto")) return "Logto";
|
||||||
|
if (h.contains("keycloak")) return "Keycloak";
|
||||||
|
if (h.endsWith("auth0.com")) return "Auth0";
|
||||||
|
if (h.endsWith("okta.com") || h.endsWith("oktapreview.com")) return "Okta";
|
||||||
|
return GENERIC;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.cameleer.server.app.security;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/** Unit tests for {@link OidcProviderNameDeriver}. No Spring context. */
|
||||||
|
class OidcProviderNameDeriverTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void logtoIssuer_returnsLogto() {
|
||||||
|
assertThat(OidcProviderNameDeriver.deriveName("https://auth.logto.example/")).isEqualTo("Logto");
|
||||||
|
assertThat(OidcProviderNameDeriver.deriveName("https://logto.cameleer.local")).isEqualTo("Logto");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void keycloakIssuer_returnsKeycloak() {
|
||||||
|
assertThat(OidcProviderNameDeriver.deriveName("https://keycloak.example/realms/cameleer")).isEqualTo("Keycloak");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void auth0Issuer_returnsAuth0() {
|
||||||
|
assertThat(OidcProviderNameDeriver.deriveName("https://example.auth0.com/")).isEqualTo("Auth0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void oktaIssuer_returnsOkta() {
|
||||||
|
assertThat(OidcProviderNameDeriver.deriveName("https://dev-123.okta.com/")).isEqualTo("Okta");
|
||||||
|
assertThat(OidcProviderNameDeriver.deriveName("https://login.oktapreview.com/")).isEqualTo("Okta");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unknownIssuer_returnsGenericLabel() {
|
||||||
|
assertThat(OidcProviderNameDeriver.deriveName("https://idp.example.com/")).isEqualTo("Single Sign-On");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void blankOrNullIssuer_returnsGenericLabel() {
|
||||||
|
assertThat(OidcProviderNameDeriver.deriveName("")).isEqualTo("Single Sign-On");
|
||||||
|
assertThat(OidcProviderNameDeriver.deriveName(null)).isEqualTo("Single Sign-On");
|
||||||
|
assertThat(OidcProviderNameDeriver.deriveName(" ")).isEqualTo("Single Sign-On");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void malformedUri_returnsGenericLabel() {
|
||||||
|
assertThat(OidcProviderNameDeriver.deriveName("not a url")).isEqualTo("Single Sign-On");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void caseInsensitiveMatching() {
|
||||||
|
assertThat(OidcProviderNameDeriver.deriveName("https://AUTH.LOGTO.EXAMPLE/")).isEqualTo("Logto");
|
||||||
|
}
|
||||||
|
}
|
||||||
1167
docs/superpowers/plans/2026-04-26-auth-harmonization.md
Normal file
1167
docs/superpowers/plans/2026-04-26-auth-harmonization.md
Normal file
File diff suppressed because it is too large
Load Diff
11
pom.xml
11
pom.xml
@@ -65,6 +65,17 @@
|
|||||||
</repository>
|
</repository>
|
||||||
</repositories>
|
</repositories>
|
||||||
|
|
||||||
|
<distributionManagement>
|
||||||
|
<repository>
|
||||||
|
<id>gitea</id>
|
||||||
|
<url>https://gitea.siegeln.net/api/packages/cameleer/maven</url>
|
||||||
|
</repository>
|
||||||
|
<snapshotRepository>
|
||||||
|
<id>gitea</id>
|
||||||
|
<url>https://gitea.siegeln.net/api/packages/cameleer/maven</url>
|
||||||
|
</snapshotRepository>
|
||||||
|
</distributionManagement>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<pluginManagement>
|
<pluginManagement>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|||||||
Reference in New Issue
Block a user