fix(kms): post client_credentials/auth_code to /v1/iam/oauth/token

The hanzoai/iam OAuth API moved under the /v1/iam prefix. The legacy
root /login/oauth/access_token is now a frontend redirect, not the
token API, so the KMS auth-login proxy and OIDC SSO code-exchange both
got HTML back -> empty access_token -> 401 'invalid credentials' to the
kms-operator (breaking every KMSSecret sync, incl. luxd-staking on
devnet/testnet/mainnet).

Route both call sites to the canonical token endpoint via a single
env-overridable knob IAM_TOKEN_PATH (default /v1/iam/oauth/token), so a
future path move needs no rebuild. cmd/kms tests updated + green.
This commit is contained in:
z
2026-06-24 09:34:56 -07:00
parent 0ec84ee854
commit de98a2992b
3 changed files with 16 additions and 8 deletions
+9 -4
View File
@@ -110,6 +110,11 @@ func main() {
vaultID := envOr("MPC_VAULT_ID", "")
nodeID := envOr("KMS_NODE_ID", "kms-0")
iamEndpoint := envOr("IAM_ENDPOINT", "https://hanzo.id")
// Canonical OAuth2 token endpoint on hanzoai/iam. The API moved under
// the `/v1/iam` prefix; the legacy root `/login/oauth/access_token`
// route is now a frontend redirect, not the token API. Env-overridable
// so a future path move needs no rebuild — one knob, one default.
iamTokenPath := envOr("IAM_TOKEN_PATH", "/v1/iam/oauth/token")
dataDir := envOr("KMS_DATA_DIR", "/data/kms")
listen := envOr("KMS_LISTEN", ":8080")
@@ -204,10 +209,10 @@ func main() {
"client_id": {req.ClientID},
"client_secret": {req.ClientSecret},
}
// Canonical OAuth2 path on hanzoai/iam — no `/api/` prefix (killed in
// v2.381.0). `/login/oauth/access_token` is mounted at root per the
// OAuth2 spec; `/oauth/access_token` is also wired as an alias.
resp, err := http.PostForm(iamEndpoint+"/login/oauth/access_token", form)
// Canonical OAuth2 token endpoint on hanzoai/iam, under the `/v1/iam`
// API prefix (default IAM_TOKEN_PATH=/v1/iam/oauth/token). The legacy
// root `/login/oauth/access_token` is a frontend redirect, not the API.
resp, err := http.PostForm(iamEndpoint+iamTokenPath, form)
if err != nil {
writeJSON(w, http.StatusBadGateway, map[string]any{"statusCode": 502, "message": "identity provider unreachable"})
return
+5 -3
View File
@@ -77,6 +77,7 @@ const (
type oidcConfig struct {
iamEndpoint string // e.g. https://iam.dev.example.com
iamHost string // parsed Host of iamEndpoint (for origin comparisons)
tokenPath string // OAuth2 token endpoint path (IAM_TOKEN_PATH)
clientID string // e.g. lux-kms
clientSecret string // KMSSecret-injected
stateSecret []byte // HMAC key for state-nonce signing (>=32 bytes)
@@ -130,6 +131,7 @@ func loadOIDCConfig() *oidcConfig {
return &oidcConfig{
iamEndpoint: strings.TrimRight(iam, "/"),
iamHost: iamURL.Host,
tokenPath: envOr("IAM_TOKEN_PATH", "/v1/iam/oauth/token"),
clientID: cid,
clientSecret: cs,
stateSecret: []byte(ss),
@@ -508,8 +510,8 @@ func (c *oidcConfig) handleLogout(w http.ResponseWriter, _ *http.Request) {
}
// exchangeCode posts the auth code to IAM's token endpoint and returns
// the access_token. The IAM endpoint is canonical Hanzo IAM at
// /login/oauth/access_token (no /api/ prefix — killed in iam v2.381).
// the access_token. The canonical Hanzo IAM token endpoint lives under
// the /v1/iam API prefix (c.tokenPath, default /v1/iam/oauth/token).
func (c *oidcConfig) exchangeCode(ctx context.Context, code, redirectURI string) (string, error) {
form := url.Values{
"grant_type": {"authorization_code"},
@@ -519,7 +521,7 @@ func (c *oidcConfig) exchangeCode(ctx context.Context, code, redirectURI string)
"redirect_uri": {redirectURI},
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
c.iamEndpoint+"/login/oauth/access_token",
c.iamEndpoint+c.tokenPath,
strings.NewReader(form.Encode()))
if err != nil {
return "", err
+2 -1
View File
@@ -29,6 +29,7 @@ func newTestCfg(t *testing.T, iamEndpoint string) *oidcConfig {
return &oidcConfig{
iamEndpoint: strings.TrimRight(iamEndpoint, "/"),
iamHost: u.Host,
tokenPath: "/v1/iam/oauth/token",
clientID: "lux-kms",
clientSecret: "test-secret",
stateSecret: []byte("test-secret-test-secret-test-32!"),
@@ -132,7 +133,7 @@ func TestSsoOidcLogin_includesStateNonce(t *testing.T) {
// 4. good state → token exchange + 302 /
func TestSsoOidcCallback_validatesState(t *testing.T) {
iam := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/login/oauth/access_token" {
if r.URL.Path != "/v1/iam/oauth/token" {
t.Errorf("unexpected IAM path: %s", r.URL.Path)
}
body, _ := io.ReadAll(r.Body)