fix(dex): store raw bcrypt hash in Password CR, not base64-encoded
CI / Lint / Test / Vet (push) Successful in 26s
CI / Build & Import (push) Successful in 12s

The original NOTE claimed Dex's kubernetes storage types Hash as []byte,
requiring the bcrypt string to be base64-encoded before storage. This was
wrong: Dex v2.41 stores and compares the hash field as a plain string. The
base64-encoding caused every invite login to fail with 'Invalid credentials'
because Dex passed the base64 bytes (starting with 'J' not '$') directly to
bcrypt. Static passwords in the configmap always used raw bcrypt strings and
worked fine — confirming the dynamic CR encoding was the bug.
This commit is contained in:
2026-06-07 09:28:11 +02:00
parent 084b73907d
commit c812c71ecc
2 changed files with 11 additions and 15 deletions
+9 -10
View File
@@ -17,7 +17,6 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -98,11 +97,12 @@ func newClient(server, token string, hc *http.Client) *PasswordClient {
return &PasswordClient{server: server, token: token, http: hc}
}
// password is the wire form of a Dex Password CR. NOTE: Dex's kubernetes storage
// types the hash as []byte, which Kubernetes JSON-marshals as base64. So the
// `hash` field must carry the base64 encoding of the bcrypt string, NOT the raw
// bcrypt string — store the raw string and Dex's base64-decode on login yields
// garbage and every login fails. CreatePassword does that encoding.
// password is the wire form of a Dex Password CR. The hash field is a plain
// bcrypt string (e.g. "$2a$12$..."). Dex v2.41+ stores and compares it as-is —
// it does NOT base64-decode the field. Earlier code base64-encoded the hash
// based on a misread of Dex's internal []byte type; that caused every dynamic
// invite login to fail with "Invalid credentials" while static passwords (set as
// plain strings in the configmap) worked fine.
type password struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
@@ -123,10 +123,9 @@ func (c *PasswordClient) CreatePassword(ctx context.Context, email, bcryptHash,
Kind: "Password",
Metadata: map[string]string{"name": passwordName(email), "namespace": "auth"},
Email: email,
// base64 of the bcrypt string — see the password type's NOTE.
Hash: base64.StdEncoding.EncodeToString([]byte(bcryptHash)),
Username: email,
UserID: userID,
Hash: bcryptHash, // raw bcrypt string — Dex compares it directly
Username: email,
UserID: userID,
})
if err != nil {
return fmt.Errorf("dex: marshal password: %w", err)
+2 -5
View File
@@ -2,7 +2,6 @@ package dex
import (
"context"
"encoding/base64"
"encoding/json"
"io"
"net/http"
@@ -46,10 +45,8 @@ func TestCreatePasswordSuccess(t *testing.T) {
require.Equal(t, "new-dot-user-at-example-dot-com", gotBody.Metadata["name"])
require.Equal(t, "auth", gotBody.Metadata["namespace"])
// The hash is the BASE64 of the bcrypt string (Dex stores hash as []byte).
decoded, err := base64.StdEncoding.DecodeString(gotBody.Hash)
require.NoError(t, err)
require.Equal(t, "$2a$12$abcdefghijklmnopqrstuv", string(decoded))
// Hash is stored as the raw bcrypt string Dex compares it directly.
require.Equal(t, "$2a$12$abcdefghijklmnopqrstuv", gotBody.Hash)
}
func TestCreatePasswordConflict(t *testing.T) {