Hey! I’m currently looking into making the switch ...
# talk-kratos
q
Hey! I’m currently looking into making the switch from Django user management to Ory Kratos for the startup I work for. We want to preserve the passwords of our users by using the hashed password method that you provide, however I am struggling to make it work. Our Django setup uses pbkdf2-sha256 and I have worked on a migration that assigns the hash,salt, length and iteration into the format you have defined so that we can push our identities over. However, I cannot get it to work. I cloned your repo and wrote my own test case with a test password and hash, and still face issues. Anyone who is available to debug?
This first issue I encountered was that the Django hash includes padding with "=", when stripping those I couldn't get the password to match the hash.
m
Hello Andreas, are you able to share your code so we can reproduce? Otherwise I have it on my list to create a hands-on guide for migrating identities. Also pinging @damp-sunset-69236 since he has much more experience with Django.
q
For sure:
Copy code
def decode_hash(encoded):
    algorithm, iterations, salt, hash = encoded.split("$", 3)
    return f"$pbkdf2-sha256$i={iterations},l=32${salt}${hash}"


def push_identities_to_kratos(apps, schema_editor):
    User = apps.get_model("customers", "User")

    headers = {
        "Authorization": "Bearer ory_pat_xxxxxxxxxxxxxxxxxxxxxxxx",
        "Content-Type": "application/json",
    }
    failed_users = []

    for user in User.objects.all():
        payload = {
            "schema_id": "5b46b51c87f515485905a12d82494e5bceb13e50118d53d7902a4cbae442186afc5df852f4df49589e768780e49fae46a369c3a1d97a7127f3b2dcb4413795c2",
            "traits": {
                "email": user.email,
                "name": {
                    "first": user.first_name,
                    "last": user.last_name,
                },
            },
            "credentials": {
                "password": {"config": {"hashed_password": decode_hash(user.password)}}
            },
        }
        response = <http://requests.post|requests.post>(
            "<https://myprojectkeythingy.projects.oryapis.com/admin/identities>",
            json=payload,
            headers=headers,
        )
        data = response.json()
        errors = data.get("error")
        if errors:
            failed_users.append(user.id)
            logger.warn(
                f"Failed to migrate user to Kratos ({user.email})", error=errors
            )
The password hash that I am testing with (directly copied from Django) is:
pbkdf2_sha256$216000$pknum4sl85ri$g+8PPmkWp9/090lEd0MpxIjYu4d3HGPvPopkS3LaW1w=
where the password is
admin123
s
can you create the same hash through kratos (e.g. using unit tests) and compare it?
or rather, hash for the same password
ah but the salt will be different... so maybe you can force the salt to be the same in some unit test?
q
I tried doing this:
Copy code
func TestComparepbkdf2hash(t *testing.T) {
	assert.Nil(t, hash.Compare(context.Background(), []byte("admin123"), []byte("$pbkdf2-sha256$i=216000,l=32$pknum4sl85ri$pbkdf2_sha256$216000$pknum4sl85ri$g+8PPmkWp9/090lEd0MpxIjYu4d3HGPvPopkS3LaW1w=")))
}
If that makes sense
s
yes it would make sense
maybe add a print somewhere to see the hash before comparison?
to see if there is some encoding discrepancy or whether the hash is completely different?
skimming over https://github.com/ory/kratos/blob/b7e28166f0821db86648cc5f9056a15b2466c3cd/hash/hash_comparator.go#L169 I would expect your hash to not contain as many
$
as it does
q
So with the hash provided in this test, it will fail. But if I strip the hash-part of the string for "="-padding then I get a hash
[131 239 15 62 105 22 167 223 244 247 73 68 119 67 41 196 136 216 187 135 119 28 99 239 62 138 100 75 114 218 91 92]
from the fn
hash, err = base64.RawStdEncoding.Strict().DecodeString(parts[4])
Yeah you're right, I was testing something and forgot to replace properly
Should be:
$pbkdf2-sha256$i=216000,l=32$pknum4sl85ri$g+8PPmkWp9/090lEd0MpxIjYu4d3HGPvPopkS3LaW1w=
assert.Nil(t, hash.Compare(context.Background(), []byte("admin123"), []byte("$pbkdf2-sha256$i=216000,l=32$pknum4sl85ri$g+8PPmkWp9/090lEd0MpxIjYu4d3HGPvPopkS3LaW1w")))
<- stripped for =
s
https://pkg.go.dev/encoding/base64#pkg-variables
RawStdEncoding is the standard raw, unpadded base64 encoding, as defined in RFC 4648 section 3.2. This is the same as StdEncoding but omits padding characters.
q
Yeah, I took a look at those docs. However I had a hard time understanding how to achieve that same setup in Python
s
What hash does kratos produce when hashing that password with the same parameters and salt? does that match as a string or is it completely different?
q
If I just print out the
otherHash
variable in the compare-func then I get a different hash
Hash:
[131 239 15 62 105 22 167 223 244 247 73 68 119 67 41 196 136 216 187 135 119 28 99 239 62 138 100 75 114 218 91 92]
Other hash:
[207 124 123 50 7 138 99 189 75 62 43 103 169 234 56 43 78 61 36 233 43 47 70 138 61 179 100 37 148 55 8 23]
s
hm so it is completely different 🤔
q
I think I found the issue
By looking at another library which compared pbkdf2: https://github.com/meehow/go-django-hashers/blob/5e5d6afe52db9084fd6fb32c9c74b649c64a669d/check.go#L57 In kratos the salt is extracted by doing this:
salt, err = base64.RawStdEncoding.Strict().DecodeString(parts[3])
which made the pass not match. However if I assign the salt like so instead:
salt = []byte(parts[3])
it works (test passes).
However that breaks the other tests. I don't really know Go that well. Why do you think it worked by doing
[]byte(parts[3])
? Is there some formatting with base64 that I should do on the python end?
s
ok I see, then probably in your pyhton code you have to encode the salt one more time
q
Sorry for wasting your time here. Apparently, Django does not base64-encode the salt. So by encoding it before inserting it in the string it works
s
I assume it is already base64 encoded, but you can just encode it a second time
q
yep
s
np, it is a good takeaway because other people might want to do the same at some point, and we can now write a guide for that
it would be awesome if you could share your conversion script 😉
q
Yep, I ended up with something looking like this:
Copy code
def convert_django_to_kratos_hash(encoded):
    _, iterations, salt, hash = encoded.split("$", 3)
    encoded_salt = base64.b64encode(salt.encode("UTF-8")).decode("UTF-8")
    stripped_hash = hash.replace("=", "")
    return f"$pbkdf2-sha256$i={iterations},l=32${encoded_salt}${stripped_hash}"
s
thanks a lot