Skip to content

Add support for mTLS-based application authentication#1255

Merged
minwoox merged 8 commits intoline:mainfrom
minwoox:mtls
Mar 6, 2026
Merged

Add support for mTLS-based application authentication#1255
minwoox merged 8 commits intoline:mainfrom
minwoox:mtls

Conversation

@minwoox
Copy link
Contributor

@minwoox minwoox commented Feb 5, 2026

This commit merges the #1228 and #1230 to the main branch.

minwoox and others added 3 commits December 16, 2025 15:37
Motivation:
This change introduces mutual TLS (mTLS) as a first-class authentication method for client applications. mTLS provides a stronger security posture by authenticating the client machine itself through a cryptographic certificate. By supporting mTLS, we can offer a more secure, automatable, and manageable authentication mechanism for server-to-server communication.

Modifications:
- Introduced a new `AppIdentity` interface to abstract the concept of a client machine that connects to Central Dogma.
  - Created two implementations of this interface: `Token` for legacy token-based auth, and `CertificateAppIdentity` for the new mTLS-based auth.
- An `appId` is now used as the primary identifier, which maps to a certificate's identity (extracted from the SPIFFE URI by default) or a token ID. Permissions are granted to this `appId`.
- Renamed `Tokens` to `AppIdentityRegistry` to reflect that it now manages both tokens and certificates.
- Renamed the corresponding service from `TokenService` to `AppIdentityRegistryService`.
- Refactored the codebase by extracting token-related logic from `MetadataService` into the new `AppIdentityService` for better encapsulation and clarity.
- Renamed `ProjectMetadata.tokens()` and `Roles.tokens()` to `ProjectMetadata.appIds()` and `Roles.appIds()` respectively.
- Deprecated the existing token-based REST APIs in favor of new, more generic `/appIdentities` APIs that handle both types.

Result:
- Client applications can now authenticate with the Central Dogma server using mTLS client certificates in addition to API tokens.

To-do:
- Prepare the backward and forward compatiblity logic because tokens are renamed to applications in various places.
Motivation:
This commit is the follow-up to line#1228, which introduced the backend support for mTLS and the generic `AppIdentity` model.

Modifications:
- Replaced all frontend API calls from the deprecated `/tokens` endpoints to the new `/appIdentities` endpoints.
- The UI now fetches the server configuration from the `/configs` endpoint upon loading.
  - Based on the `mtlsEnabled` flag in the server configuration, the UI will conditionally display an option to create a certificate-based App Identity.
- Performed a comprehensive renaming throughout the frontend codebase, replacing the term "token" with "App Identity".

Result:
- You can now create, view, and manage both token-based and certificate-based App Identities through the web UI.
@minwoox minwoox added this to the 0.80.0 milestone Feb 5, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Renames token concepts to "app identity" across backend and frontend, adds mTLS support and certificate-based authentication, replaces TokenService/Tokens with AppIdentityRegistry/AppIdentityService, updates metadata and APIs (/appIdentities), and adjusts web UI and tests to the new model.

Changes

Cohort / File(s) Summary
Core identity primitives
common/src/main/java/com/linecorp/centraldogma/common/Author.java, common/src/main/java/com/linecorp/centraldogma/internal/Util.java
Renamed isToken()isAppIdentity(), replaced TOKEN_EMAIL_SUFFIX with APP_IDENTITY_EMAIL_SUFFIX, added legacy suffix constant.
Server auth / mTLS wiring
server/src/main/java/com/linecorp/centraldogma/server/CentralDogma.java, CentralDogmaBuilder.java, server/src/main/java/com/linecorp/centraldogma/server/auth/AuthConfig.java, server/src/main/java/com/linecorp/centraldogma/server/auth/MtlsConfig.java
Added MtlsConfig and mtlsEnabled propagation, extended TLS setup to support mTLS, updated auth provider signatures and wiring to include certificate authorizer when MTLS enabled.
Certificate authorizers & extractors
server/src/main/java/com/linecorp/centraldogma/server/auth/ApplicationCertificateIdExtractor.java, server/src/main/java/com/linecorp/centraldogma/server/internal/api/auth/ApplicationCertificateAuthorizer.java, server/src/main/java/com/linecorp/centraldogma/server/internal/api/auth/SpiffeIdExtractor.java
Introduced ApplicationCertificateIdExtractor, SPIFEE extractor implementation, and ApplicationCertificateAuthorizer to authenticate via peer certificates.
App identity registry & services
server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentityRegistry.java, AppIdentityRegistryTransformer.java, server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentityService.java
Replaced Tokens → AppIdentityRegistry, added AppIdentityService to manage tokens and certificate identities, transformer updated to AppIdentityRegistry.
Sysadmin HTTP API
server/src/main/java/com/linecorp/centraldogma/server/internal/api/sysadmin/AppIdentityRegistryService.java, (removed) .../TokenService.java
Added AppIdentityRegistryService exposing /appIdentities (legacy /tokens endpoints preserved/delegated); removed legacy TokenService.
Metadata layer & project models
server/src/main/java/com/linecorp/centraldogma/server/metadata/*, ProjectMetadata.java, MetadataService.java, AppIdentityRegistration.java, AppIdentityNotFoundException.java
Renamed Token* types to AppIdentity* (Registration, NotFoundException, registry usage), updated metadata APIs and public method names to app-identity semantics.
Project & repository logic
server/src/main/java/com/linecorp/centraldogma/server/internal/api/ProjectServiceV1.java, RepositoryServiceUtil.java, ProjectApiManager.java, DefaultProject.java
Switched token-based role resolution to app-identity based (UserWithAppIdentity, AppIdentityRegistration), updated initialization and repository helpers to populate appIds.
Purge scheduling & storage init
server/src/main/java/com/linecorp/centraldogma/server/internal/storage/PurgeSchedulingService.java, InternalProjectInitializer.java
Purge and initialization flows updated from Tokens to AppIdentityRegistry naming and behavior.
HTTP API handlers
server/src/main/java/com/linecorp/centraldogma/server/internal/api/MetadataApiService.java, HttpApiExceptionHandler.java
Added app-identity endpoints (add/update/remove/app-level and repo-level), deprecated token endpoints delegate to new flows; adjusted exception mappings to AppIdentityNotFoundException.
Auth token authorizer
server/src/main/java/com/linecorp/centraldogma/server/internal/api/auth/ApplicationTokenAuthorizer.java
Switched validation/logging to use AppIdentityRegistry and UserWithAppIdentity; replaced Token exceptions with AppIdentity equivalents.
Server tests & integration
server/src/test/..., it/*/.../NonRandomTokenTest.java, CertificateAppIdentityAuthTest.java, AppIdentityRegistryServiceTest.java, AppIdentityRegistryServiceViaHttpTest.java
Added/updated tests to cover AppIdentity flows, mTLS scenarios, and adjusted integration tests to POST /appIdentities with type=TOKEN where applicable.
Frontend - types & API layer
webapp/src/dogma/features/app-identity/AppIdentity.ts, webapp/src/dogma/features/api/apiSlice.ts
Introduced AppIdentity TypeScript union (Token
Frontend - UI components/pages
webapp/src/dogma/features/app-identity/*, webapp/src/pages/app/settings/app-identities/index.tsx, webapp/src/pages/app/settings/tokens/index.tsx (removed), Navbar.tsx, multiple settings and repo components
Renamed token components to app-identity variants, added certificate UI (CertificateWrapper), new App Identities settings page, removed legacy Token page/components.
Frontend - state & server config
webapp/src/dogma/features/server-config/serverConfigSlice.ts, ServerConfigLoader.tsx, store.ts, pages/_app.tsx
Added serverConfig slice to fetch /configs (mtlsEnabled); ServerConfigLoader fetches config before render and integrates into store.
Frontend - many DTO/prop renames
assorted files under webapp/src/dogma/features/* and pages/...
Renamed DTOs and prop types (AppTokenDto → AppIdDto, TokenDto → AppIdentityDto, DeleteUserOrTokenRepositoryRoleDto → DeleteUserOrAppIdentityRepositoryRoleDto, etc.) and updated numerous components and pages to new shapes and prop unions.
Tests & infra updates
testing-internal/src/main/java/.../TestAuthMessageUtil.java, webapp e2e/tests, multiple unit tests
Updated test utilities to call /appIdentities with type=TOKEN, updated many tests to reflect renames and mTLS scenarios; removed TokenServiceTest and legacy token UI tests replaced by AppIdentity tests.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant TLS as TLS/mTLS Layer
    participant CertAuth as ApplicationCertificateAuthorizer
    participant AppRegistry as AppIdentityRegistry
    participant Metadata as MetadataService

    Client->>TLS: HTTPS request with client certificate
    TLS->>CertAuth: authorize(ctx, request)
    CertAuth->>CertAuth: extract peer cert(s) from SSLSession
    CertAuth->>CertAuth: extract certificateId via ID_EXTRACTOR
    CertAuth->>AppRegistry: findByCertificateId(certificateId)
    AppRegistry->>Metadata: lookup identity data
    Metadata-->>AppRegistry: CertificateAppIdentity
    AppRegistry-->>CertAuth: AppIdentity (or NOT_FOUND)
    CertAuth->>CertAuth: bind authenticated UserWithAppIdentity to context
    CertAuth-->>TLS: authorization result (success/failure)
    TLS-->>Client: HTTP response
Loading
sequenceDiagram
    participant Web as Web App
    participant Api as RTK Query (apiSlice)
    participant Backend as Server API
    participant Metadata as MetadataService

    Web->>Api: useGetAppIdentitiesQuery()
    Api->>Backend: GET /api/v1/appIdentities
    Backend->>Metadata: getAppIdentityRegistry()
    Metadata-->>Backend: AppIdentityRegistry (masked/selections)
    Backend-->>Api: AppIdentityDto[]
    Api-->>Web: query result
    Web->>Web: render rows via isToken / isCertificate guards
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • trustin
  • jrhee17
  • ikhoon

"🐰 I hopped through code with nimble feet,
Tokens grew identities — a tidy feat.
Certs wear their IDs, mTLS keeps them neat,
Now systems and rabbits authenticate sweet! 🥕"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
server/src/test/java/com/linecorp/centraldogma/server/metadata/RepositoryMetadataDeserializerTest.java (1)

30-46: ⚠️ Potential issue | 🟡 Minor

Keep the legacy-field coverage in deserializeWithTokens().

Line 43 now uses appIds, so the test no longer verifies backward compatibility for the legacy tokens field as its name/comment indicate. Either restore tokens here or rename the test and update the comment.

💡 Suggested fix (restore legacy field)
-                             "    \"appIds\": {" +
+                             "    \"tokens\": {" +
webapp/src/dogma/features/repo/settings/ConfirmAddUserOrAppIdentityRepositoryRole.tsx (1)

35-60: ⚠️ Potential issue | 🟡 Minor

Humanize entityType in user-facing copy.

Interpolating appIdentity directly yields awkward UI text (“appIdentity”). Map to a display label before use.

✅ Suggested change
   const dispatch = useAppDispatch();
   const data = {
     id: loginId,
     role: repositoryRole as RepositoryRole,
   };
+  const entityLabel = entityType === 'appIdentity' ? 'app identity' : 'user';
 
   const handleUpdate = async () => {
     try {
       const response = await addUserRepositoryRole({ projectName, repoName, data }).unwrap();
@@
       dispatch(
         newNotification(
-          `Repository ${entityType} role is added`,
+          `Repository ${entityLabel} role is added`,
           `Successfully updated ${repoName}`,
           'success',
         ),
       );
@@
         <ModalBody>
-          Add a {entityType} {loginId}?
+          Add a {entityLabel} {loginId}?
         </ModalBody>
server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentityRegistration.java (1)

43-45: ⚠️ Potential issue | 🟡 Minor

Fix stale Javadoc reference to Token.

The class was renamed, but the role doc still references Token, which is now misleading.

✏️ Suggested fix
-    /**
-     * A role of the {`@link` Token} in a project.
-     */
+    /**
+     * A role of the {`@link` AppIdentity} in a project.
+     */
🤖 Fix all issues with AI agents
In
`@server/src/main/java/com/linecorp/centraldogma/server/metadata/MetadataService.java`:
- Around line 562-566: The duplicate-item error message is using outdated
"Token" wording; update the ChangeConflictException thrown inside
ProjectMetadataTransformer (the lambda checking
projectMetadata.appIds().containsKey(registration.id())) to use the new App
Identity wording—for example throw new ChangeConflictException("App identity
already exists: " + registration.id())—so the message references App Identity
and still includes registration.id() for context.

In
`@server/src/test/java/com/linecorp/centraldogma/server/internal/api/AppIdentityRegistryServiceTest.java`:
- Around line 307-310: The test is asserting exceptions inconsistently for
asynchronous calls: when you invoke
appIdentityRegistryService.createToken(...).join() (an async CompletableFuture),
assertThatThrownBy should expect a CompletionException or assert the join's
cause; update the assertions that call .join() (e.g., the createToken(...) usage
in AppIdentityRegistryServiceTest) to either expect CompletionException.class
via .isInstanceOf(CompletionException.class) or use
.hasCauseInstanceOf(IllegalArgumentException.class) to check the underlying
cause, and conversely if you remove .join() and assert on the CompletableFuture
itself, assert the IllegalArgumentException on the future’s completion; review
all assertThatThrownBy usages in AppIdentityRegistryServiceTest and make them
consistent (match .join() usages to CompletionException or hasCauseInstanceOf
for the underlying IllegalArgumentException).
- Around line 156-161: The test currently asserts IllegalArgumentException
directly when calling appIdentityRegistryService.createAppIdentity(...).join();
update the assertion to account for the async future by expecting a
CompletionException that wraps an IllegalArgumentException: replace the direct
isInstanceOf(IllegalArgumentException.class) check with an assertion that the
thrown exception is a CompletionException and that its cause is an
IllegalArgumentException (use appIdentityRegistryService.createAppIdentity(...)
and .join() as the action and assert
.isInstanceOf(CompletionException.class).hasCauseInstanceOf(IllegalArgumentException.class)
or equivalent).
- Around line 116-119: The test incorrectly asserts IllegalArgumentException
directly while createToken(...).join() will throw a CompletionException wrapping
the real cause; update the assertion to expect CompletionException and assert
its cause is IllegalArgumentException — e.g., use assertThatThrownBy(() ->
appIdentityRegistryService.createToken("forAdmin2", true, null, guestAuthor,
guest).join()).isInstanceOf(CompletionException.class).hasCauseInstanceOf(IllegalArgumentException.class)
so the test checks the wrapper and the underlying exception from the
CompletableFuture.

In
`@server/src/test/java/com/linecorp/centraldogma/server/internal/api/AppIdentityRegistryServiceViaHttpTest.java`:
- Around line 73-94: The test method createTokenAndUpdateLevel should assert the
PATCH response status before parsing the body: after calling
systemAdminClient.execute(headers, body) and assigning AggregatedHttpResponse
response, add an assertion that response.status() (or response.status().code())
equals the expected success status (e.g. HttpStatus.OK) to fail fast on errors,
then proceed to parse response.contentUtf8() into JSON; update the assertion
placement in createTokenAndUpdateLevel so the status check happens immediately
after the response is received.

In
`@server/src/test/java/com/linecorp/centraldogma/server/metadata/TokenGuestPermissionTest.java`:
- Around line 152-155: In testNormalToken(), the request uses a lowercase
"token" value for the type parameter which fails since AppIdentityDeserializer
calls AppIdentityType.valueOf() (case-sensitive); update the test's POST call in
testNormalToken() to use the uppercase enum constant "TOKEN" (i.e., set the
query param key "type" to value "TOKEN" to match AppIdentityType and the usage
in TestAuthMessageUtil).

In `@webapp/src/dogma/features/server-config/ServerConfigLoader.tsx`:
- Around line 14-22: The fetch block handling '/configs' should validate
response.ok and ensure mtlsEnabled is a boolean before dispatching
setServerConfig; update the promise chain in ServerConfigLoader.tsx to check
response.ok (and handle non-OK by throwing or defaulting), parse JSON only on
OK, and defensively coerce or default data.mtlsEnabled to false when missing or
not boolean before calling dispatch(setServerConfig({ mtlsEnabled })). Ensure
the .catch still logs the error and dispatches a safe boolean fallback.

In `@webapp/src/pages/app/settings/app-identities/index.tsx`:
- Around line 70-76: The action visibility logic in the columnHelper.accessor
for AppIdentityDto is using strict === undefined checks against info.getValue(),
which mismatches the status truthiness logic and mishandles null; update the
hidden conditions for ActivateAppIdentity, DeactivateAppIdentity, and
DeleteAppIdentity to use truthiness like info.getValue() (or !info.getValue())
so that a null deactivation is treated the same as undefined/false—i.e., hide
Activate and Delete when deactivation is truthy, and hide Deactivate when
deactivation is falsy—so the actions align with the status display.
🧹 Nitpick comments (18)
server/src/test/java/com/linecorp/centraldogma/server/metadata/CertificateAppIdentityTest.java (2)

86-125: Consider using isEqualTo(original) for the round-trip assertion.

If CertificateAppIdentity implements equals() and hashCode() (common for value objects), the entire field-by-field comparison could be simplified to assertThat(deserialized).isEqualTo(original). The current approach is also valid and provides more explicit failure messages, so this is optional.


26-145: Consider adding test coverage for the deletion field.

The serializeAndDeserialize test validates deactivation when non-null but always passes null for deletion. Adding a test case with a non-null deletion value would ensure complete coverage of the object's lifecycle states.

webapp/src/dogma/features/app-identity/CertificateWrapper.tsx (1)

18-22: Consider accessibility: nested interactive elements in popover trigger.

The HStack containing an IconButton inside PopoverTrigger creates nested interactive elements. The PopoverTrigger makes its child clickable, but the IconButton is also an interactive element. This can cause confusing behavior for keyboard users and screen readers.

Consider using a single Button component as the trigger:

♻️ Suggested refactor
       <PopoverTrigger>
-        <HStack>
-          <Text fontWeight="bold">{appId}</Text>
-          <IconButton aria-label="View certificate" icon={<MdArrowDropDown />} variant="ghost" />
-        </HStack>
+        <Button
+          variant="ghost"
+          rightIcon={<MdArrowDropDown />}
+          aria-label="View certificate details"
+        >
+          <Text fontWeight="bold">{appId}</Text>
+        </Button>
       </PopoverTrigger>
webapp/src/dogma/features/repo/settings/RepositorySettingsView.tsx (1)

131-136: Consider simplifying tab label rendering.

The capitalization logic at lines 133-134 is now redundant since TabName values like 'App Identities' and 'Danger Zone' are already properly cased. The current code works correctly but could be simplified to just render tab.name directly.

♻️ Optional simplification
                 return (
                   <Tab as={Link} key={tab.name} href={link} isDisabled={!allowed}>
-                    <Heading size="sm">
-                      {tab.name.charAt(0).toUpperCase()}
-                      {tab.name.slice(1)}
-                    </Heading>
+                    <Heading size="sm">{tab.name}</Heading>
                   </Tab>
                 );
server/src/main/java/com/linecorp/centraldogma/server/internal/storage/project/ProjectApiManager.java (1)

92-95: Resolve the TODO: distinguish user vs. app-identity types.

Leaving this TODO in permission logic risks long-term ambiguity; please track it with an issue or implement before release.

webapp/src/pages/api/v1/appIdentities.ts (1)

6-14: Consider randomizing allowGuestAccess for more realistic mock data.

The systemAdmin field is randomized via faker.datatype.boolean(), but allowGuestAccess is hardcoded to true. For more representative test data, consider randomizing this field as well:

💡 Suggested improvement
   const token: Token = {
     appId: `${faker.animal.snake().replaceAll(' ', '-').toLowerCase()}-${id}`,
     type: 'TOKEN',
     secret: faker.datatype.uuid(),
     systemAdmin: faker.datatype.boolean(),
-    allowGuestAccess: true,
+    allowGuestAccess: faker.datatype.boolean(),
     creation: { user: faker.internet.email(), timestamp: faker.datatype.datetime().toISOString() },
   };
server/src/main/java/com/linecorp/centraldogma/server/auth/ApplicationCertificateIdExtractor.java (1)

28-32: Consider enhancing JavaDoc with expected ID format details.

The interface is part of the public API and allows custom implementations. Documenting the expected format of the returned certificate ID (e.g., "a SPIFFE ID path like trust-domain/workload-id") would help implementers understand what downstream components expect.

server/src/main/java/com/linecorp/centraldogma/server/internal/api/auth/SpiffeIdExtractor.java (1)

52-55: Consider handling edge case of empty SPIFFE ID.

If a certificate contains spiffe:// with nothing after it, substring(9) returns an empty string. Depending on how the ID is used downstream, an empty string might cause unexpected behavior versus returning null.

💡 Optional defensive check
                         if (uri.startsWith("spiffe://")) {
-                            return uri.substring(9); // Remove "spiffe://"
+                            final String spiffeId = uri.substring(9); // Remove "spiffe://"
+                            return spiffeId.isEmpty() ? null : spiffeId;
                         }
webapp/src/dogma/features/server-config/serverConfigSlice.ts (1)

3-6: Consider adding error state for failed config fetches.

If the server config fetch fails, the UI currently has no way to distinguish between "still loading" and "failed to load". Adding an error field could improve UX by allowing the app to display appropriate feedback.

💡 Optional enhancement
 export interface ServerConfigState {
   mtlsEnabled: boolean;
   isLoaded: boolean;
+  error: string | null;
 }

 const initialState: ServerConfigState = {
   mtlsEnabled: false,
   isLoaded: false,
+  error: null,
 };
webapp/javaTest/java/com/linecorp/centraldogma/webapp/ShiroCentralDogmaMtlsTestServer.java (2)

64-64: Privileged port 443 may fail on typical developer machines.

Port 443 requires elevated privileges (root/admin) on most systems. Consider using a non-privileged port (e.g., 8443) for easier local testing, or document that elevated privileges are required.

Suggested change
-                .port(443, SessionProtocol.HTTPS)
+                .port(8443, SessionProtocol.HTTPS)

51-51: Temporary directory is not cleaned up on shutdown.

The shutdown hook only closes the server but does not delete the temporary directory created at line 51. This could lead to accumulation of orphaned temp directories over multiple test runs.

Add cleanup in shutdown hook
-        Runtime.getRuntime().addShutdownHook(new Thread(server::close));
+        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+            server.close();
+            // Optionally clean up rootDir
+        }));

Also applies to: 70-71

webapp/src/dogma/features/repo/RepositoriesMetadataDto.ts (1)

26-28: Interface name is descriptive but verbose.

UserOrAppIdentityRepositoryRoleDto clearly conveys its purpose. If brevity is preferred, consider RepositoryRoleMapDto or similar, though the current name is acceptable.

server/src/test/java/com/linecorp/centraldogma/server/metadata/ProjectMetadataTest.java (1)

42-44: Variable naming is misleading.

The variable is named token but its type is AppIdentityRegistration. Consider renaming to appIdentity or appIdentityRegistration for consistency with the new terminology.

Suggested rename
-        final AppIdentityRegistration token = metadata.appIds().get("app-token-1");
-        assertThat(token.id()).isEqualTo("app-token-1");
-        assertThat(token.role()).isEqualTo(ProjectRole.MEMBER);
+        final AppIdentityRegistration appIdentity = metadata.appIds().get("app-token-1");
+        assertThat(appIdentity.id()).isEqualTo("app-token-1");
+        assertThat(appIdentity.role()).isEqualTo(ProjectRole.MEMBER);

And similarly for line 58:

-        final AppIdentityRegistration token = metadata.appIds().get("app-id-1");
-        assertThat(token.id()).isEqualTo("app-id-1");
-        assertThat(token.role()).isEqualTo(ProjectRole.OWNER);
+        final AppIdentityRegistration appIdentity = metadata.appIds().get("app-id-1");
+        assertThat(appIdentity.id()).isEqualTo("app-id-1");
+        assertThat(appIdentity.role()).isEqualTo(ProjectRole.OWNER);

Also applies to: 58-60

webapp/src/dogma/features/app-identity/DisplaySecretModal.tsx (1)

32-36: Make the response contract consistent with the guard.

response is required but the component returns undefined when it’s falsy. Either remove the guard or make response optional and return null.

✅ Suggested change
 }: {
   isOpen: boolean;
   onClose: () => void;
-  response: AppIdentityDto;
+  response?: AppIdentityDto;
 }) => {
   const dispatch = useAppDispatch();
-  if (!response) return;
+  if (!response) return null;
   const { appId, systemAdmin, creation } = response;
server/src/test/java/com/linecorp/centraldogma/server/internal/api/AppIdentityRegistryServiceTest.java (1)

95-108: TearDown may fail silently if purge operations throw exceptions.

The tearDown method iterates over all app identities and calls destroy/purge without handling potential exceptions. If one identity fails to purge, subsequent identities won't be cleaned up, potentially causing test pollution.

Consider wrapping each cleanup operation in a try-catch:

♻️ Suggested improvement for robust cleanup
 `@AfterEach`
 public void tearDown() {
     final AppIdentityRegistry registry = metadataService.fetchAppIdentityRegistry().join();
     registry.appIds().forEach((appId, appIdentity) -> {
-        if (!appIdentity.isDeleted()) {
-            if (appIdentity.type() == AppIdentityType.TOKEN) {
-                metadataService.destroyToken(systemAdminAuthor, appId);
-            } else {
-                metadataService.destroyCertificate(systemAdminAuthor, appId);
+        try {
+            if (!appIdentity.isDeleted()) {
+                if (appIdentity.type() == AppIdentityType.TOKEN) {
+                    metadataService.destroyToken(systemAdminAuthor, appId).join();
+                } else {
+                    metadataService.destroyCertificate(systemAdminAuthor, appId).join();
+                }
             }
+            metadataService.purgeAppIdentity(systemAdminAuthor, appId).join();
+        } catch (Exception e) {
+            // Log and continue cleanup
         }
-        metadataService.purgeAppIdentity(systemAdminAuthor, appId);
     });
 }
webapp/src/dogma/features/project/settings/ProjectSettingsView.tsx (1)

134-138: Minor: Tab label capitalization logic may produce unexpected results for multi-word tab names.

The current logic at lines 136-137 capitalizes only the first character:

{tab.name.charAt(0).toUpperCase()}
{tab.name.slice(1)}

For 'App Identities', this works correctly since it's already capitalized. However, this pattern is fragile - if any tab name were lowercase (e.g., 'app identities'), it would display as "App identities" rather than "App Identities".

Since tab names now include pre-capitalized multi-word names, consider whether this transformation is still needed or could be simplified.

server/src/main/java/com/linecorp/centraldogma/server/internal/api/auth/ApplicationCertificateAuthorizer.java (1)

62-63: TODO: SPI-based certificate ID extractor configuration.

The hardcoded SpiffeIdExtractor.INSTANCE limits flexibility. The TODO acknowledges this should be configurable via SPI. Consider tracking this as a follow-up item if SPIFFE is not the only expected certificate ID format.

Would you like me to open an issue to track making the ApplicationCertificateIdExtractor configurable via SPI?

server/src/main/java/com/linecorp/centraldogma/server/metadata/MetadataService.java (1)

882-924: TODO for API exposure is still open.
If you want, I can draft the MetadataApiService endpoint for this update path.

Comment on lines 562 to 566
final ProjectMetadataTransformer transformer =
new ProjectMetadataTransformer((headRevision, projectMetadata) -> {
if (projectMetadata.appIds().containsKey(registration.id())) {
throw new ChangeConflictException("Token already exists: " + registration.id());
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use App Identity wording in duplicate error message.
The exception still says “Token already exists,” which is misleading after the rename.

🛠️ Suggested fix
-                        throw new ChangeConflictException("Token already exists: " + registration.id());
+                        throw new ChangeConflictException("App identity already exists: " +
+                                                          registration.id());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
final ProjectMetadataTransformer transformer =
new ProjectMetadataTransformer((headRevision, projectMetadata) -> {
if (projectMetadata.appIds().containsKey(registration.id())) {
throw new ChangeConflictException("Token already exists: " + registration.id());
}
final ProjectMetadataTransformer transformer =
new ProjectMetadataTransformer((headRevision, projectMetadata) -> {
if (projectMetadata.appIds().containsKey(registration.id())) {
throw new ChangeConflictException("App identity already exists: " +
registration.id());
}
🤖 Prompt for AI Agents
In
`@server/src/main/java/com/linecorp/centraldogma/server/metadata/MetadataService.java`
around lines 562 - 566, The duplicate-item error message is using outdated
"Token" wording; update the ChangeConflictException thrown inside
ProjectMetadataTransformer (the lambda checking
projectMetadata.appIds().containsKey(registration.id())) to use the new App
Identity wording—for example throw new ChangeConflictException("App identity
already exists: " + registration.id())—so the message references App Identity
and still includes registration.id() for context.

Comment on lines +70 to +76
columnHelper.accessor((row: AppIdentityDto) => row.deactivation, {
cell: (info) => (
<Wrap>
<ActivateAppIdentity appId={info.row.original.appId} hidden={info.getValue() === undefined} />
<DeactivateAppIdentity appId={info.row.original.appId} hidden={info.getValue() !== undefined} />
<DeleteAppIdentity appId={info.row.original.appId} hidden={info.getValue() === undefined} />
</Wrap>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align action visibility with status logic.

Status uses truthiness, but actions use === undefined. If deactivation is null, status shows Active while actions show Activate/Delete.

✅ Suggested change
       columnHelper.accessor((row: AppIdentityDto) => row.deactivation, {
         cell: (info) => (
+          (() => {
+            const isInactive = Boolean(info.getValue());
+            return (
           <Wrap>
-            <ActivateAppIdentity appId={info.row.original.appId} hidden={info.getValue() === undefined} />
-            <DeactivateAppIdentity appId={info.row.original.appId} hidden={info.getValue() !== undefined} />
-            <DeleteAppIdentity appId={info.row.original.appId} hidden={info.getValue() === undefined} />
+            <ActivateAppIdentity appId={info.row.original.appId} hidden={!isInactive} />
+            <DeactivateAppIdentity appId={info.row.original.appId} hidden={isInactive} />
+            <DeleteAppIdentity appId={info.row.original.appId} hidden={!isInactive} />
           </Wrap>
+            );
+          })()
         ),
🤖 Prompt for AI Agents
In `@webapp/src/pages/app/settings/app-identities/index.tsx` around lines 70 - 76,
The action visibility logic in the columnHelper.accessor for AppIdentityDto is
using strict === undefined checks against info.getValue(), which mismatches the
status truthiness logic and mishandles null; update the hidden conditions for
ActivateAppIdentity, DeactivateAppIdentity, and DeleteAppIdentity to use
truthiness like info.getValue() (or !info.getValue()) so that a null
deactivation is treated the same as undefined/false—i.e., hide Activate and
Delete when deactivation is truthy, and hide Deactivate when deactivation is
falsy—so the actions align with the status display.

Copy link
Contributor

@jrhee17 jrhee17 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving as already reviewed 👍 👍

Copy link
Contributor

@ikhoon ikhoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍👍

@ikhoon
Copy link
Contributor

ikhoon commented Feb 5, 2026

There are lint violations in the webapp.

@minwoox
Copy link
Contributor Author

minwoox commented Feb 5, 2026

There are lint violations in the webapp.

Fixed. 😉

@minwoox minwoox modified the milestones: 0.80.0, 0.81.0 Feb 20, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
server/src/main/java/com/linecorp/centraldogma/server/metadata/MetadataService.java (1)

564-566: ⚠️ Potential issue | 🟡 Minor

Use App Identity wording in duplicate conflict message.

Line 565 still says "Token already exists" although this path is AppIdentity-based. Please rename it to AppIdentity terminology for consistency.

Suggested fix
-                        throw new ChangeConflictException("Token already exists: " + registration.id());
+                        throw new ChangeConflictException("App identity already exists: " +
+                                                          registration.id());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@server/src/main/java/com/linecorp/centraldogma/server/metadata/MetadataService.java`
around lines 564 - 566, The duplicate-conflict message uses "Token" but this
branch is AppIdentity-based; in MetadataService where you check
projectMetadata.appIds().containsKey(registration.id()) and throw new
ChangeConflictException(...), change the message to use AppIdentity wording
(e.g. "AppIdentity already exists: " + registration.id() or "App identity
already exists: " + registration.id()) so the exception text matches the
AppIdentity code path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/src/main/java/com/linecorp/centraldogma/server/auth/MtlsConfig.java`:
- Around line 98-105: caCertInputStream currently treats the converted value as
certificate data bytes instead of a filesystem path, causing
CertificateUtil.toX509Certificates to fail; change caCertInputStream to
interpret the converted string (from convertValue) as a file path and return an
InputStream for that file (e.g., open the file with
Files.newInputStream(Paths.get(converted)) or similar), ensuring you propagate
IOExceptions appropriately and keep the method name caCertInputStream and the
convertValue call intact so callers (and CertificateUtil.toX509Certificates)
receive the actual certificate file contents.

In `@server/src/main/java/com/linecorp/centraldogma/server/CentralDogma.java`:
- Around line 755-766: The code silently disables mtls by setting mtlsEnabled =
false and logging a warning when TLS config is missing; instead fail fast during
startup to avoid unintentionally starting without certificate auth. In
CentralDogma (the block handling mtlsEnabled), replace the mtlsEnabled=false +
logger.warn branch with an explicit startup error (e.g., throw new
IllegalStateException or use Exceptions.throwUnsafely) that includes a clear
message like "mTLS is enabled but TLS is not configured; aborting startup", so
the process fails rather than continuing; update any related test expectations
to expect a startup failure.

In
`@server/src/main/java/com/linecorp/centraldogma/server/internal/api/auth/ApplicationCertificateAuthorizer.java`:
- Around line 108-126: The loop in ApplicationCertificateAuthorizer currently
extracts certificate IDs from any X509Certificate in peerCertificates; re-enable
the CA/intermediate skip by checking x509Certificate.getBasicConstraints() and
continue when it != -1 (i.e., it's a CA), logging that a CA cert was skipped
(use ctx.clientAddress() and x509Certificate as in existing logs) before calling
ID_EXTRACTOR.extractCertificateId; ensure extraction only runs for end-entity
certs so certificateId comes from the leaf client certificate.

In
`@server/src/main/java/com/linecorp/centraldogma/server/internal/storage/project/DefaultProject.java`:
- Line 208: Update the stale inline comment in DefaultProject to reflect app
identities instead of "token": change the comment that reads "author.name() is
the appId of the token." to something like "author.name() is the appId of the
app identity." so it accurately describes the current semantics where
author.name() represents an app identity/appId.

In
`@server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentityRegistry.java`:
- Around line 198-203: The withoutSecret() method currently removes secrets but
also clears certificate ID mappings by passing ImmutableMap.of() for
certificateIds; update withoutSecret() in AppIdentityRegistry so it
preserves/rebuilds the certificateIds mapping instead of erasing it—either pass
the existing certificateIds map through to the new AppIdentityRegistry or
reconstruct certificateIds from the transformed appIds (use each AppIdentity's
certificateId to map to its id) before calling the AppIdentityRegistry
constructor; keep the secret removal logic for Token.withoutSecret() intact.

---

Duplicate comments:
In
`@server/src/main/java/com/linecorp/centraldogma/server/metadata/MetadataService.java`:
- Around line 564-566: The duplicate-conflict message uses "Token" but this
branch is AppIdentity-based; in MetadataService where you check
projectMetadata.appIds().containsKey(registration.id()) and throw new
ChangeConflictException(...), change the message to use AppIdentity wording
(e.g. "AppIdentity already exists: " + registration.id() or "App identity
already exists: " + registration.id()) so the exception text matches the
AppIdentity code path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 382328ab-7f6b-44e6-ab93-8c766ab933aa

📥 Commits

Reviewing files that changed from the base of the PR and between 4c29cf1 and 9f6ab2c.

📒 Files selected for processing (18)
  • server/src/main/java/com/linecorp/centraldogma/server/CentralDogma.java
  • server/src/main/java/com/linecorp/centraldogma/server/CentralDogmaBuilder.java
  • server/src/main/java/com/linecorp/centraldogma/server/auth/ApplicationCertificateIdExtractor.java
  • server/src/main/java/com/linecorp/centraldogma/server/auth/AuthConfig.java
  • server/src/main/java/com/linecorp/centraldogma/server/auth/MtlsConfig.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/api/HttpApiExceptionHandler.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/api/ProjectServiceV1.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/api/RepositoryServiceUtil.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/api/auth/ApplicationCertificateAuthorizer.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/api/auth/SpiffeIdExtractor.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/api/sysadmin/AppIdentityRegistryService.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/storage/PurgeSchedulingService.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/storage/project/DefaultProject.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/storage/project/ProjectApiManager.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentityRegistry.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/MetadataService.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/ProjectMetadata.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/Roles.java
🚧 Files skipped from review as they are similar to previous changes (5)
  • server/src/main/java/com/linecorp/centraldogma/server/internal/api/RepositoryServiceUtil.java
  • server/src/main/java/com/linecorp/centraldogma/server/metadata/Roles.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/storage/project/ProjectApiManager.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/api/ProjectServiceV1.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/api/sysadmin/AppIdentityRegistryService.java

Comment on lines +108 to +126
for (Certificate peerCert : peerCertificates) {
logger.trace("Peer certificate: addr={}, cert={}", ctx.clientAddress(), peerCert);
if (!(peerCert instanceof X509Certificate)) {
continue;
}
final X509Certificate x509Certificate = (X509Certificate) peerCert;
/* // Uncomment the following lines after fixing Singned Certificate Extension to
generate end-entity certs.
if (x509Certificate.getBasicConstraints() != -1) {
logger.trace("Skipping CA certificate: addr={}, cert={}", ctx.clientAddress(), x509Certificate);
continue;
}
*/

certificateId = ID_EXTRACTOR.extractCertificateId(x509Certificate);
if (certificateId != null) {
break;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Do not authorize using certificate IDs from CA/intermediate certificates.

With the CA-skip block disabled, Line 122 can accept a SPIFFE ID from any certificate in the chain, not strictly the end-entity client certificate. That weakens identity binding and can lead to incorrect authorization.

🔒 Proposed fix
-            /* // Uncomment the following lines after fixing Singned Certificate Extension to
-            generate end-entity certs.
             if (x509Certificate.getBasicConstraints() != -1) {
                 logger.trace("Skipping CA certificate: addr={}, cert={}", ctx.clientAddress(), x509Certificate);
                 continue;
             }
-            */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@server/src/main/java/com/linecorp/centraldogma/server/internal/api/auth/ApplicationCertificateAuthorizer.java`
around lines 108 - 126, The loop in ApplicationCertificateAuthorizer currently
extracts certificate IDs from any X509Certificate in peerCertificates; re-enable
the CA/intermediate skip by checking x509Certificate.getBasicConstraints() and
continue when it != -1 (i.e., it's a CA), logging that a CA cert was skipped
(use ctx.clientAddress() and x509Certificate as in existing logs) before calling
ID_EXTRACTOR.extractCertificateId; ensure extraction only runs for end-entity
certs so certificateId comes from the leaf client certificate.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
webapp/tests/dogma/feature/project/ProjectMetadata.test.tsx (1)

27-29: Rename leftover AppToken* helper naming to AppId* for consistency.

Line 27’s comment and Line 28’s alias still use token terminology, which is now outdated in this PR and makes the test harder to scan.

♻️ Suggested rename
-// Helper type for testing - AppTokenDto is Map<string, AppTokenDetailDto> but server returns object
-type AppTokenObject = { [key: string]: AppIdDetailDto };
+// Helper type for testing - AppIdDto is Map<string, AppIdDetailDto> but server returns object
+type AppIdObject = { [key: string]: AppIdDetailDto };
-const appIds = mockMetadata.appIds as unknown as AppTokenObject;
+const appIds = mockMetadata.appIds as unknown as AppIdObject;

(Apply the usage rename similarly at Line 91, Line 96, Line 106, and Line 114.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/tests/dogma/feature/project/ProjectMetadata.test.tsx` around lines 27
- 29, The comment and helper type names still use "AppToken" terminology; rename
the helper symbols and comment to use "AppId" instead — change the comment
referencing AppTokenDto to AppIdDto (or the correct current type name) and
rename the alias AppTokenObject to AppIdObject, then update all usages of that
alias in this file (notably where referenced around the previous Line 91, 96,
106, and 114) so tests and comments consistently use AppId* names.
webapp/e2e/project-tokens.spec.ts (1)

43-56: Consider finishing the terminology cleanup in test names and local variables.

Assertions/routes are updated, but names like tokensTab and test titles/comments still say “token(s)”, which makes the suite harder to scan and grep.

♻️ Optional naming cleanup
- test('should display tokens tab in project settings', async ({ page }) => {
+ test('should display App Identities tab in project settings', async ({ page }) => {
...
-   const tokensTab = page.getByRole('tab', { name: 'App Identities' });
-   await expect(tokensTab).toBeVisible();
+   const appIdentitiesTab = page.getByRole('tab', { name: 'App Identities' });
+   await expect(appIdentitiesTab).toBeVisible();

-   await tokensTab.click();
+   await appIdentitiesTab.click();
- test('should display Add Token button', async ({ page }) => {
+ test('should display Add App Identity button', async ({ page }) => {
- test('should display tokens tab in repository settings', async ({ page }) => {
+ test('should display App Identities tab in repository settings', async ({ page }) => {
...
-   const tokensTab = page.getByRole('tab', { name: 'App Identities' });
-   await expect(tokensTab).toBeVisible();
+   const appIdentitiesTab = page.getByRole('tab', { name: 'App Identities' });
+   await expect(appIdentitiesTab).toBeVisible();

-   await tokensTab.click();
+   await appIdentitiesTab.click();

Also applies to: 58-63, 119-132

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/e2e/project-tokens.spec.ts` around lines 43 - 56, Rename remaining
"token(s)" terminology in the test to match the updated route/assertions: change
the test title string "should display tokens tab in project settings" to "should
display App Identities tab in project settings", rename the local variable
tokensTab to appIdentitiesTab (and update its usage in the click/assert), and
update any comments that mention "tokens" accordingly; apply the same
rename/cleanup for the other occurrences noted (around the blocks referenced at
58-63 and 119-132) so test names, variable names, and comments consistently use
"App Identities" / appIdentities.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@webapp/e2e/project-tokens.spec.ts`:
- Around line 43-56: Rename remaining "token(s)" terminology in the test to
match the updated route/assertions: change the test title string "should display
tokens tab in project settings" to "should display App Identities tab in project
settings", rename the local variable tokensTab to appIdentitiesTab (and update
its usage in the click/assert), and update any comments that mention "tokens"
accordingly; apply the same rename/cleanup for the other occurrences noted
(around the blocks referenced at 58-63 and 119-132) so test names, variable
names, and comments consistently use "App Identities" / appIdentities.

In `@webapp/tests/dogma/feature/project/ProjectMetadata.test.tsx`:
- Around line 27-29: The comment and helper type names still use "AppToken"
terminology; rename the helper symbols and comment to use "AppId" instead —
change the comment referencing AppTokenDto to AppIdDto (or the correct current
type name) and rename the alias AppTokenObject to AppIdObject, then update all
usages of that alias in this file (notably where referenced around the previous
Line 91, 96, 106, and 114) so tests and comments consistently use AppId* names.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2ef1c543-304d-4117-acca-89aff02f65d8

📥 Commits

Reviewing files that changed from the base of the PR and between 9f6ab2c and 3b49670.

📒 Files selected for processing (2)
  • webapp/e2e/project-tokens.spec.ts
  • webapp/tests/dogma/feature/project/ProjectMetadata.test.tsx

@minwoox minwoox merged commit 384cce0 into line:main Mar 6, 2026
14 checks passed
@minwoox minwoox deleted the mtls branch March 6, 2026 01:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants