refactor: apply ConfigurationProperties
This commit is contained in:
@@ -9,3 +9,141 @@
|
||||
file tools or Python instead.
|
||||
2. Use English in code. Avoid Chinese, Japanese, or any other non-ASCII
|
||||
natural language inside source files.
|
||||
|
||||
## Boot rules
|
||||
|
||||
The very first time I ran `java -cp ... cn.violin.iam.ViolinIamApplication`,
|
||||
Spring failed with "Parameter 0 of constructor in
|
||||
`cn.violin.core.config.WebMvcConfig` required a bean of type
|
||||
`cn.violin.core.interceptor.AuthenticationInterceptor` that could not be
|
||||
found". The unit-test suite was green but had never actually booted the
|
||||
context. Concrete gotchas to fix:
|
||||
|
||||
- **`@SpringBootApplication` MUST set `scanBasePackages = {"cn.violin"}`**
|
||||
explicitly. The default only scans the app's own sub-package
|
||||
(`cn.violin.iam`), which excludes `cn.violin.core.interceptor.AuthenticationInterceptor`,
|
||||
`CurrentUserHandlerMethodArgumentResolver`, `TraceIdFilter`, and other
|
||||
framework beans. Spring Boot auto-config registers the
|
||||
`@AutoConfiguration` classes themselves, but their `@Bean` dependencies
|
||||
that are not themselves exposed via another `@Bean` still need component-scan
|
||||
coverage. Compare with `violin-auth`, which has always used
|
||||
`scanBasePackages = {"cn.violin"}`; it never tripped over this.
|
||||
|
||||
- **`@MapperScan("cn.violin.*.mapper")` — never use bare `cn.violin`.**
|
||||
The broad form hoovers up every interface in the project, including
|
||||
framework-internal ones like `JwksClient`, and MyBatis then complains
|
||||
at runtime:
|
||||
`Invalid bound statement (not found): cn.violin.core.security.JwksClient.loadKeys`.
|
||||
Narrowing the pattern prevents MyBatis from treating non-mapper
|
||||
interfaces as proxies.
|
||||
|
||||
- **`@Service` impls whose interface is in the same package should be
|
||||
marked `@Primary` when `scanBasePackages` is widened.** Once
|
||||
`cn.violin.*` is scanned, Spring ends up holding two `BeanDefinition`s
|
||||
of the same bean type (the impl + some derived candidate), and
|
||||
`@Autowired` of the interface fails with `NoUniqueBeanDefinitionException:
|
||||
expected single matching bean but found 2: OAuthServiceImpl,OAuthService`.
|
||||
Adding `@Primary` to the impl collapses that to a single candidate.
|
||||
|
||||
- **Spring 6 forbids `@PostConstruct` methods with parameters.**
|
||||
`SnowflakeId.init(SnowflakeProperties props)` looked innocuous but
|
||||
`IllegalStateException: Lifecycle annotation requires a no-arg method`
|
||||
hit on `@SpringBootTest`. Use `InitializingBean` + `@Autowired` for
|
||||
the setter (properties injection remains valid via
|
||||
`wireProperties`). Same pattern applies to any other singleton that
|
||||
needs configuration at boot.
|
||||
|
||||
- **`@ConfigurationProperties` on Java `record` types needs
|
||||
`@ConstructorBinding`** when the canonical constructor does not match
|
||||
Spring's expected discovery. Without it, Spring fails with
|
||||
`No default constructor found`.
|
||||
|
||||
- **Adding `@SpringBootTest` early catches all four of these.** Place a
|
||||
smoke test (`ApplicationContextLoadsTest`) in
|
||||
`src/test/java/.../boot/` and run `mvn test`. If the unit tests are
|
||||
green but the boot test fails, the bug is precisely the kind that
|
||||
silently passes when scanning only `@Component`-annotated classes
|
||||
but blows up at full startup.
|
||||
|
||||
## Design notes (do not skip)
|
||||
|
||||
## Design notes (do not skip)
|
||||
|
||||
### JWT revocation propagation — what we tried, what we picked, and why
|
||||
|
||||
The naive question "should we drop `customerId` from the JWT claim so
|
||||
downstream must call IAM every request?" was the wrong frame. Below is
|
||||
the actual decision tree we walked.
|
||||
|
||||
#### Problem
|
||||
A user's `t_user.customer_id` is migrated from tenant A to tenant B. The
|
||||
JWT issued at login still carries `customerId=A`. Without propagation
|
||||
the user keeps operating under A's RBAC scope (and SAR cache) for the
|
||||
remainder of the token TTL.
|
||||
|
||||
#### Industry patterns surveyed
|
||||
|
||||
1. **Short TTL + RFC 7662 introspection** (Google / AWS / GitHub /
|
||||
Slack). Token TTL = 5-15 min; each request consults an introspection
|
||||
endpoint (with local cache) to check the token is still live.
|
||||
Revocation propagates in <1 s via shared cache / pub-sub.
|
||||
|
||||
2. **`revokedCustomerBindings` cache** (medium-scale).
|
||||
`Cache<userId, oldCustomerId>` with TTL covering the longest possible
|
||||
token lifetime. `UserService.changeCustomer(...)` writes the DB row
|
||||
then publishes `(userId, oldCustomerId)` into the cache. The cache
|
||||
TTL ensures stale entries eventually fall out without a cron.
|
||||
|
||||
3. **Active binding flag** (per-user cache of currently-valid
|
||||
customerId). cache miss → fall back to DB lookup. cache hit but
|
||||
cache.customerId != JWT claim → treat as revoke signal.
|
||||
|
||||
4. **Kafka / event-driven revocation** (large clusters, >10 services).
|
||||
IAM emits `user.tenant.changed` to a topic; subscribers invalidate
|
||||
local cache on receipt. Strictly over-engineered for a 1-issuer /
|
||||
5-subscriber fleet.
|
||||
|
||||
#### What we picked and why
|
||||
|
||||
We picked **option 2** (`revokedCustomerBindings` cache) and combined
|
||||
it with **token TTL reduction** from 24 h to **1 h**. Net result:
|
||||
|
||||
- Worst-case staleness window = `min(token TTL, cache TTL)` = 1 h.
|
||||
- No per-request IAM call (the user's framing-question was "is the
|
||||
extra IAM call worth it?" — answer: no, because it moves IAM onto
|
||||
the hot path and turns it into a single point of failure).
|
||||
- Single-instance IAM means cache state is authoritative without
|
||||
sticky sessions or shared cache.
|
||||
|
||||
#### What we did NOT do (and the reasons)
|
||||
|
||||
- **Drop `customerId` from JWT claim.** Would force every permission
|
||||
check to call `POST /api/v1/internal/check-permission`, adding
|
||||
2-5 ms latency per request and making IAM the hot path. Token
|
||||
claim is now treated as **informational only** — never trusted
|
||||
for authorization. All decisions go through IAM SAR.
|
||||
- **Re-sign JWT on every customer change.** Requires Kafka or
|
||||
webhook infra to broadcast — engineering cost far exceeds risk
|
||||
reduction given the 1 h TTL already covers the window.
|
||||
- **Use Authentik's session revocation directly.** Adds an upstream
|
||||
dependency IAM cannot fail without.
|
||||
|
||||
#### Implementation hooks already in place
|
||||
|
||||
| File | Role |
|
||||
| --- | --- |
|
||||
| `cn.violin.common.context.RequestContext` | exposes `setCustomerIdHint(...)` and `getVerifiedCustomerId()` — verified provenance is now mandatory for SAR / PermissionAspect / RbacController / ResourcePermissionAspect |
|
||||
| `SubjectAccessReviewRequest.customerVerified` | explicit boolean; SAR denies on `false` |
|
||||
| `PermissionAspect` | reads `getVerifiedCustomerId()` first, throws `UnauthorizedException` if null |
|
||||
| `ResourcePermissionAspect` | same |
|
||||
| `RbacController.access-check` | same |
|
||||
| `InternalController.check-permission` | accepts only verified (set from HMAC + ServiceAllowlist mapping) |
|
||||
|
||||
#### Followup still pending (backlog, not on the critical path)
|
||||
|
||||
Add `CustomerBindingRevocationCache` and have `UserService.changeCustomer(...)`
|
||||
push `(sub, oldCustomerId)` into it. Have `PermissionAspect` consult the cache
|
||||
as a secondary check (after verified gate). Reduce default `expirationMs`
|
||||
in `ViolinJwtProperties` from `86_400_000` (24 h) to `3_600_000` (1 h).
|
||||
These three together close the revocation window to the bound of TTL.
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.violin.iam;
|
||||
|
||||
import cn.violin.iam.config.AuthentikProperties;
|
||||
import cn.violin.iam.config.AuthentikOidcProperties;
|
||||
import cn.violin.iam.config.OidcProperties;
|
||||
import cn.violin.iam.config.ServiceAuthProperties;
|
||||
import cn.violin.iam.config.ViolinJwtProperties;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
@@ -10,11 +11,12 @@ import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
||||
|
||||
@SpringBootApplication
|
||||
@MapperScan("cn.violin")
|
||||
@SpringBootApplication(scanBasePackages = {"cn.violin"})
|
||||
@MapperScan("cn.violin.*.mapper")
|
||||
@EnableConfigurationProperties({
|
||||
ViolinJwtProperties.class,
|
||||
AuthentikProperties.class,
|
||||
OidcProperties.class,
|
||||
AuthentikOidcProperties.class,
|
||||
ServiceAuthProperties.class
|
||||
})
|
||||
public class ViolinIamApplication extends SpringBootServletInitializer {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.violin.iam.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.bind.ConstructorBinding;
|
||||
|
||||
/**
|
||||
* Configuration bundle for the Authentik-specific OIDC endpoints. All fields
|
||||
* are optional; when blank, {@link cn.violin.iam.sso.AuthentikConf} falls
|
||||
* back to the canonical Authentik path
|
||||
* {@code <issuer>/application/o/<endpoint>/}.
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "iam.oidc.authentik")
|
||||
public record AuthentikOidcProperties(
|
||||
String tokenEndpoint,
|
||||
String userinfoEndpoint,
|
||||
String authorizeEndpoint
|
||||
) {
|
||||
@ConstructorBinding
|
||||
public AuthentikOidcProperties {
|
||||
tokenEndpoint = tokenEndpoint == null ? "" : tokenEndpoint.strip();
|
||||
userinfoEndpoint = userinfoEndpoint == null ? "" : userinfoEndpoint.strip();
|
||||
authorizeEndpoint = authorizeEndpoint == null ? "" : authorizeEndpoint.strip();
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package cn.violin.iam.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Configuration bundle for Authentik OIDC client.
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "authentik")
|
||||
public class AuthentikProperties {
|
||||
|
||||
private String issuer = "";
|
||||
private String clientId = "";
|
||||
private String clientSecret = "";
|
||||
private String redirectUri = "";
|
||||
private String scope = "openid profile email";
|
||||
|
||||
public String getIssuer() { return issuer; }
|
||||
public void setIssuer(String issuer) { this.issuer = issuer; }
|
||||
public String getClientId() { return clientId; }
|
||||
public void setClientId(String clientId) { this.clientId = clientId; }
|
||||
public String getClientSecret() { return clientSecret; }
|
||||
public void setClientSecret(String clientSecret) { this.clientSecret = clientSecret; }
|
||||
public String getRedirectUri() { return redirectUri; }
|
||||
public void setRedirectUri(String redirectUri) { this.redirectUri = redirectUri; }
|
||||
public String getScope() { return scope; }
|
||||
public void setScope(String scope) { this.scope = scope; }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package cn.violin.iam.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.bind.ConstructorBinding;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Configuration bundle for the OIDC client identity (provider-independent).
|
||||
*
|
||||
* <p>Bound to {@code iam.oidc.client.*}; the actual provider endpoints
|
||||
* (e.g. Authentik-specific paths) live in {@link AuthentikOidcProperties}.</p>
|
||||
*
|
||||
* <p>Two fields ({@code issuer}, {@code clientId}) are mandatory at startup
|
||||
* when the active Spring profile is not {@code local} — local dev may run
|
||||
* with blank values to defer OIDC wiring to a later iteration.</p>
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "iam.oidc.client")
|
||||
public record OidcProperties(
|
||||
String issuer,
|
||||
String clientId,
|
||||
String clientSecret,
|
||||
String redirectUri,
|
||||
String scope,
|
||||
String profile
|
||||
) {
|
||||
@ConstructorBinding
|
||||
public OidcProperties {
|
||||
issuer = StringUtils.trimTrailingCharacter(nullSafe(issuer).trim(), '/');
|
||||
clientId = nullSafe(clientId).trim();
|
||||
clientSecret = nullSafe(clientSecret).strip();
|
||||
redirectUri = nullSafe(redirectUri).strip();
|
||||
scope = StringUtils.hasText(scope) ? scope.strip() : "openid profile email";
|
||||
profile = StringUtils.hasText(profile) ? profile.trim() : "local";
|
||||
}
|
||||
|
||||
/** True when the active Spring profile equals {@code local} (case-insensitive). */
|
||||
public boolean isLocal() {
|
||||
return "local".equalsIgnoreCase(profile);
|
||||
}
|
||||
|
||||
private static String nullSafe(String s) {
|
||||
return s == null ? "" : s;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,113 @@
|
||||
package cn.violin.iam.sso;
|
||||
|
||||
import cn.violin.iam.config.AuthentikProperties;
|
||||
import lombok.Data;
|
||||
import cn.violin.iam.config.AuthentikOidcProperties;
|
||||
import cn.violin.iam.config.OidcProperties;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
@Data
|
||||
/**
|
||||
* Resolves OIDC endpoint URLs for the Authentik provider.
|
||||
*
|
||||
* <p>Composes two configuration sources:
|
||||
* <ul>
|
||||
* <li>{@link OidcProperties} — provider-independent client identity
|
||||
* (issuer, clientId/secret, redirectUri, scope).</li>
|
||||
* <li>{@link AuthentikOidcProperties} — Authentik-specific endpoint paths
|
||||
* (tokenEndpoint, userinfoEndpoint, authorizeEndpoint). Defaults match
|
||||
* Authentik 2023.x installs; overrides are useful for tenant-specific
|
||||
* reverse proxies or self-hosted legacy versions.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>When {@code iam.oidc.authentik.*} paths are present they take precedence;
|
||||
* when absent, URLs are derived from {@code iam.oidc.client.issuer} and the
|
||||
* standard Authentik path {@code /application/o/...}.</p>
|
||||
*/
|
||||
@Configuration
|
||||
@Primary
|
||||
@EnableConfigurationProperties(AuthentikProperties.class)
|
||||
@EnableConfigurationProperties({OidcProperties.class, AuthentikOidcProperties.class})
|
||||
@Slf4j
|
||||
public class AuthentikConf {
|
||||
|
||||
private final AuthentikProperties props;
|
||||
private final OidcProperties client;
|
||||
private final AuthentikOidcProperties endpoints;
|
||||
|
||||
public AuthentikConf(AuthentikProperties props) {
|
||||
this.props = props;
|
||||
public AuthentikConf(OidcProperties client, AuthentikOidcProperties endpoints) {
|
||||
this.client = client;
|
||||
this.endpoints = endpoints;
|
||||
log.info("Authentik OIDC ready: issuer={}, endpoints overridden={}",
|
||||
client.issuer(),
|
||||
endpointsOverrideActive());
|
||||
}
|
||||
|
||||
public String getIssuer() { return props.getIssuer(); }
|
||||
public String getClientId() { return props.getClientId(); }
|
||||
public String getClientSecret() { return props.getClientSecret(); }
|
||||
public String getRedirectUri() { return props.getRedirectUri(); }
|
||||
public String getScope() { return props.getScope(); }
|
||||
/**
|
||||
* Fail-fast validation: blank {@code issuer} / {@code clientId} are only
|
||||
* tolerated when the active profile equals {@code local}.
|
||||
*/
|
||||
@PostConstruct
|
||||
void validate() {
|
||||
if (client.isLocal()) {
|
||||
if (client.issuer().isBlank() || client.clientId().isBlank()) {
|
||||
log.warn("iam.oidc.client.issuer/client-id are blank under local profile; "
|
||||
+ "OIDC login will fail until configured");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (client.issuer().isBlank()) {
|
||||
throw new IllegalStateException(
|
||||
"iam.oidc.client.issuer must be set for profile=" + client.profile()
|
||||
+ " (mount K8s Secret or ConfigMap)");
|
||||
}
|
||||
if (client.clientId().isBlank()) {
|
||||
throw new IllegalStateException(
|
||||
"iam.oidc.client.client-id must be set for profile=" + client.profile());
|
||||
}
|
||||
}
|
||||
|
||||
public String getIssuer() { return client.issuer(); }
|
||||
public String getClientId() { return client.clientId(); }
|
||||
public String getClientSecret() { return client.clientSecret(); }
|
||||
public String getRedirectUri() { return client.redirectUri(); }
|
||||
public String getScope() { return client.scope(); }
|
||||
|
||||
public String getTokenUrl() {
|
||||
return base() + "/application/o/token/";
|
||||
return resolve(endpoints.tokenEndpoint(), "/application/o/token/");
|
||||
}
|
||||
|
||||
public String getUserInfoUrl() {
|
||||
return base() + "/application/o/userinfo/";
|
||||
return resolve(endpoints.userinfoEndpoint(), "/application/o/userinfo/");
|
||||
}
|
||||
|
||||
public String getAuthorizeUrl() {
|
||||
return base() + "/application/o/authorize/";
|
||||
return resolve(endpoints.authorizeEndpoint(), "/application/o/authorize/");
|
||||
}
|
||||
|
||||
private String base() {
|
||||
String s = props.getIssuer();
|
||||
/**
|
||||
* @return base URL for the Authentik OIDC tenant, with trailing slash trimmed.
|
||||
* Empty string when {@link OidcProperties#issuer()} is blank.
|
||||
*/
|
||||
public String getIssuerBaseUrl() {
|
||||
String s = client.issuer();
|
||||
if (s == null) return "";
|
||||
s = s.trim();
|
||||
while (s.endsWith("/")) {
|
||||
s = s.substring(0, s.length() - 1);
|
||||
return stripTrailingSlash(s.trim());
|
||||
}
|
||||
return s;
|
||||
|
||||
/**
|
||||
* @return true when at least one endpoint is overridden via {@code iam.oidc.authentik.*}.
|
||||
*/
|
||||
public boolean endpointsOverrideActive() {
|
||||
return !endpoints.tokenEndpoint().isBlank()
|
||||
|| !endpoints.userinfoEndpoint().isBlank()
|
||||
|| !endpoints.authorizeEndpoint().isBlank();
|
||||
}
|
||||
|
||||
private String resolve(String override, String defaultSuffix) {
|
||||
return override.isBlank() ? getIssuerBaseUrl() + defaultSuffix : override;
|
||||
}
|
||||
|
||||
private static String stripTrailingSlash(String s) {
|
||||
return s.endsWith("/") ? stripTrailingSlash(s.substring(0, s.length() - 1)) : s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,13 +38,17 @@ violin:
|
||||
service-token-secret: ${VIOLIN_IAM_SERVICE_TOKEN_SECRET:}
|
||||
service-allowlist: ${VIOLIN_IAM_SERVICE_ALLOWLIST:}
|
||||
profile: ${SPRING_PROFILES_ACTIVE:local}
|
||||
|
||||
oidc:
|
||||
client:
|
||||
issuer: ${IAM_OIDC_CLIENT_ISSUER:${AUTHENTIK_ISSUER:}}
|
||||
client-id: ${IAM_OIDC_CLIENT_CLIENT_ID:${AUTHENTIK_CLIENT_ID:}}
|
||||
client-secret: ${IAM_OIDC_CLIENT_CLIENT_SECRET:${AUTHENTIK_CLIENT_SECRET:}}
|
||||
redirect-uri: ${IAM_OIDC_CLIENT_REDIRECT_URI:${AUTHENTIK_REDIRECT_URI:}}
|
||||
scope: ${IAM_OIDC_CLIENT_SCOPE:${AUTHENTIK_SCOPE:openid profile email}}
|
||||
authentik:
|
||||
issuer: ${AUTHENTIK_ISSUER:}
|
||||
client-id: ${AUTHENTIK_CLIENT_ID:}
|
||||
client-secret: ${AUTHENTIK_CLIENT_SECRET:}
|
||||
redirect-uri: ${AUTHENTIK_REDIRECT_URI:}
|
||||
scope: ${AUTHENTIK_SCOPE:openid profile email}
|
||||
token-endpoint: ${IAM_OIDC_AUTHENTIK_TOKEN_ENDPOINT:}
|
||||
userinfo-endpoint: ${IAM_OIDC_AUTHENTIK_USERINFO_ENDPOINT:}
|
||||
authorize-endpoint: ${IAM_OIDC_AUTHENTIK_AUTHORIZE_ENDPOINT:}
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package cn.violin.iam.boot;
|
||||
|
||||
import cn.violin.iam.ViolinIamApplication;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
/**
|
||||
* Smoke test that boots the full Spring context. Acts as a regression guard
|
||||
* for the round-5 startup bug: {@link ViolinIamApplication} needed an
|
||||
* explicit {@code scanBasePackages} for {@code cn.violin.*} so that the
|
||||
* core module's {@code @Component}-marked beans (notably
|
||||
* {@code AuthenticationInterceptor} and
|
||||
* {@code CurrentUserHandlerMethodArgumentResolver}) get registered.
|
||||
*
|
||||
* <p>This test exists because round-3 / round-4 unit tests never started
|
||||
* the context — they only instantiated components in isolation — and so
|
||||
* the missing scan range remained hidden until a real
|
||||
* {@code java -cp} / packaged-jar startup was attempted.</p>
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||
@TestPropertySource(properties = {
|
||||
"violin.oidc.client.issuer=https://idp.example.com",
|
||||
"violin.oidc.client.client-id=test-client-id",
|
||||
"violin.jwt.profile=local",
|
||||
"violin.iam.profile=local",
|
||||
// Avoid needing real Postgres for the smoke context.
|
||||
"spring.datasource.url=jdbc:postgresql://localhost:5432/none",
|
||||
"spring.datasource.username=sa",
|
||||
"spring.datasource.password=",
|
||||
"spring.datasource.driver-class-name=org.postgresql.Driver",
|
||||
"spring.flyway.enabled=false"
|
||||
})
|
||||
class ApplicationContextLoadsTest {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
// No assertion: success means every wiring resolved, in particular
|
||||
// WebMvcConfig -> AuthenticationInterceptor.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package cn.violin.iam.sso;
|
||||
|
||||
import cn.violin.iam.config.AuthentikOidcProperties;
|
||||
import cn.violin.iam.config.OidcProperties;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Verifies the AuthentikConf URL composition: provider-specific endpoint
|
||||
* overrides take precedence; blank overrides fall back to issuer + standard
|
||||
* Authentik path suffix.
|
||||
*/
|
||||
class AuthentikConfUrlResolutionTest {
|
||||
|
||||
private static OidcProperties client(String issuer, String profile) {
|
||||
return new OidcProperties(issuer, "client-id-x", "secret", "https://app/cb", "openid", profile);
|
||||
}
|
||||
|
||||
private static AuthentikOidcProperties overrides(String token, String userinfo, String authorize) {
|
||||
return new AuthentikOidcProperties(token, userinfo, authorize);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackToIssuerPlusDefaultPathWhenOverridesAreBlank() {
|
||||
AuthentikConf conf = new AuthentikConf(
|
||||
client("https://idp.example.com/", "prod"),
|
||||
overrides("", "", ""));
|
||||
assertEquals("https://idp.example.com/application/o/token/", conf.getTokenUrl());
|
||||
assertEquals("https://idp.example.com/application/o/userinfo/", conf.getUserInfoUrl());
|
||||
assertEquals("https://idp.example.com/application/o/authorize/", conf.getAuthorizeUrl());
|
||||
assertFalse(conf.endpointsOverrideActive());
|
||||
assertEquals("https://idp.example.com", conf.getIssuerBaseUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitEndpointsTakePrecedence() {
|
||||
AuthentikConf conf = new AuthentikConf(
|
||||
client("https://idp.example.com/", "prod"),
|
||||
overrides(
|
||||
"https://idp.example.com/api/o/token/",
|
||||
"https://idp.example.com/api/o/userinfo/",
|
||||
"https://idp.example.com/api/o/authorize/"));
|
||||
assertEquals("https://idp.example.com/api/o/token/", conf.getTokenUrl());
|
||||
assertEquals("https://idp.example.com/api/o/userinfo/", conf.getUserInfoUrl());
|
||||
assertEquals("https://idp.example.com/api/o/authorize/", conf.getAuthorizeUrl());
|
||||
assertTrue(conf.endpointsOverrideActive());
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialOverrideKeepsDefaultsForOtherEndpoints() {
|
||||
AuthentikConf conf = new AuthentikConf(
|
||||
client("https://idp.example.com", "prod"),
|
||||
overrides("https://custom.example.com/oauth/token", "", ""));
|
||||
assertEquals("https://custom.example.com/oauth/token", conf.getTokenUrl());
|
||||
assertEquals("https://idp.example.com/application/o/userinfo/", conf.getUserInfoUrl());
|
||||
assertEquals("https://idp.example.com/application/o/authorize/", conf.getAuthorizeUrl());
|
||||
assertTrue(conf.endpointsOverrideActive());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user