Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class AuthorizationServerConfig(
buildVaultClient(vaultClientSecret),
buildHeadlampClient(),
buildImmichClient(),
buildHermesClient(),
)

// The JdbcOAuth2AuthorizationService constructor calls getColumnMetadata()
Expand Down Expand Up @@ -242,6 +243,10 @@ class AuthorizationServerConfig(
"headlamp" to ServicePermission.DASHBOARD,
"rabbitmq" to ServicePermission.RABBITMQ,
"immich" to ServicePermission.IMMICH,
// Hermes runs its own OIDC flow, so its route carries no
// forward-auth and the HERMES grant is enforced here at the
// authorize endpoint instead — the same shape as outline.
"hermes" to ServicePermission.HERMES,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,31 @@ fun buildHeadlampClient(): RegisteredClient =
.tokenSettings(defaultTokenSettings())
.build()

fun buildHermesClient(): RegisteredClient =
RegisteredClient
.withId(deterministicId("hermes"))
.clientId("hermes")
// Public client with PKCE — same pattern as headlamp and rabbitmq.
// Hermes' dashboard takes only HERMES_DASHBOARD_OIDC_ISSUER,
// _CLIENT_ID and _SCOPES; it has no field for a client secret, so a
// confidential client could not be configured even if we wanted one.
// The dashboard proves possession of the auth code with the PKCE
// verifier instead, and there is no secret to rotate in Vault.
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
// Hermes builds the callback as <public_url>/auth/callback verbatim,
// where public_url is HERMES_DASHBOARD_PUBLIC_URL. These must match
// that construction exactly or the flow fails at the redirect.
.redirectUri("https://hermes.jorisjonkers.dev/auth/callback")
.redirectUri("https://hermes.jorisjonkers.test/auth/callback")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope(OidcScopes.EMAIL)
.clientSettings(noConsentSettings(requirePkce = true))
.tokenSettings(defaultTokenSettings())
.build()

fun buildImmichClient(): RegisteredClient =
RegisteredClient
.withId(deterministicId("immich"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,36 @@ class RegisteredClientsTest {
"http://localhost/callback",
)
}

@Test
fun `hermes is a public PKCE client whose redirect matches the dashboard callback`() {
val client = buildHermesClient()

assertThat(client.id).isEqualTo(UUID.nameUUIDFromBytes("hermes".toByteArray()).toString())
assertThat(client.clientId).isEqualTo("hermes")
// Hermes' dashboard has no client-secret field, so a confidential
// client could not authenticate at all.
assertThat(client.clientAuthenticationMethods).containsExactly(ClientAuthenticationMethod.NONE)
assertThat(client.clientSettings.isRequireProofKey).isTrue
assertThat(client.authorizationGrantTypes)
.containsExactlyInAnyOrder(
AuthorizationGrantType.AUTHORIZATION_CODE,
AuthorizationGrantType.REFRESH_TOKEN,
)
assertThat(client.scopes)
.containsExactlyInAnyOrder(
OidcScopes.OPENID,
OidcScopes.PROFILE,
OidcScopes.EMAIL,
)
// Hermes constructs the callback as <public_url>/auth/callback
// verbatim. If these drift from HERMES_DASHBOARD_PUBLIC_URL in
// fleet-infra, the login fails at the redirect with a mismatch the
// dashboard reports only as a generic error.
assertThat(client.redirectUris)
.containsExactlyInAnyOrder(
"https://hermes.jorisjonkers.dev/auth/callback",
"https://hermes.jorisjonkers.test/auth/callback",
)
}
}
Loading