[{"data":1,"prerenderedAt":766},["ShallowReactive",2],{"Categories":3,"NavIndexCategoriesCountFooter":203,"content-\u002F2023\u002F04\u002F21\u002Fhandling-spring-security-password-hashes-when-migrating-to-ktor\u002F":204},[4,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,68,70,71,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202],{"category":5},"System Administration",{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":27},"Software Development",{"category":5},{"category":5},{"category":5},{"category":5},{"category":27},{"category":27},{"category":5},{"category":5},{"category":5},{"category":27},{"category":5},{"category":5},{"category":5},{"category":27},{"category":27},{"category":27},{"category":27},{"category":5},{"category":5},{"category":5},{"category":27},{"category":27},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":27},{"category":5},{"category":5},{"category":27},{"category":27},{"category":27},{"category":27},{"category":5},{"category":27},{"category":27},{"category":67},"Drones & RC",{"category":69},"DIY Projects",{"category":67},{"category":72},"Photography",{"category":69},{"category":69},{"category":69},{"category":67},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":67},{"category":69},{"category":69},{"category":67},{"category":67},{"category":72},{"category":72},{"category":72},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":5},{"category":5},{"category":72},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":5},{"category":67},{"category":67},{"category":72},{"category":72},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":72},{"category":67},{"category":138},"3D Printing - Laser Cutting - CNC",{"category":138},{"category":138},{"category":138},{"category":138},{"category":138},{"category":138},{"category":138},{"category":138},{"category":138},{"category":138},{"category":138},{"category":5},{"category":138},{"category":27},{"category":27},{"category":138},{"category":138},{"category":72},{"category":158},"Photography,3D Printing - Laser Cutting - CNC",{"category":27},{"category":27},{"category":69},{"category":27},{"category":27},{"category":27},{"category":27},{"category":5},{"category":67},{"category":5},{"category":5},{"category":27},{"category":27},{"category":27},{"category":27},{"category":27},{"category":69},{"category":27},{"category":27},{"category":27},{"category":27},{"category":181},"Home Assistant",{"category":181},{"category":72},{"category":27},{"category":27},{"category":72},{"category":138},{"category":5},{"category":72},{"category":72},{"category":138},{"category":27},{"category":181},{"category":181},{"category":72},{"category":72},{"category":72},{"category":72},{"category":72},{"category":72},{"category":72},{"category":72},191,{"id":205,"title":206,"body":207,"category":27,"date":750,"description":213,"embedImage":751,"extension":752,"image":751,"intro":753,"meta":754,"navigation":445,"path":755,"seo":756,"series":751,"sitemap":757,"stem":758,"tags":759,"__hash__":765},"content\u002F2023\u002F04\u002F21\u002Fhandling-spring-security-password-hashes-when-migrating-to-ktor.md","Handling spring-security password hashes when migrating to ktor",{"type":208,"value":209,"toc":741},"minimark",[210,214,217,222,225,228,231,234,237,243,246,251,254,258,272,303,306,309,313,322,325,328,333,336,370,373,377,380,410,724,728,731,734,737],[211,212,213],"p",{},"Spring boot stores passwords hashed - which is good.",[211,215,216],{},"But - when moving to a new framework - how does that play out?",[218,219,221],"h2",{"id":220},"spring-password-encoders","Spring Password Encoders",[211,223,224],{},"Spring boot requires some sort of PasswordEncoder.",[211,226,227],{},"If you look at PasswordEncoderFactories.createDelegatingPasswordEncoder() you can see that it can handle lots of different encoders - including out of date ones (so that you can still verify older hashes) - and that the default is bcrypt.",[211,229,230],{},"So - how does spring know what sort of hash you have?",[211,232,233],{},"This is stored in the database as part of the hash itself.",[211,235,236],{},"For example - with defaults - the values in the database look something like:",[211,238,239],{},[240,241,242],"code",{},"{bcrypt}$2a$10$....",[211,244,245],{},"The actual format of the bcrypt part is:",[211,247,248],{},[240,249,250],{},"$2\u003Ca\u002Fb\u002Fx\u002Fy>$[cost]$[22 character salt][31 character hash]",[211,252,253],{},"So - here we have $2a with a cost of 10.",[218,255,257],{"id":256},"checking-hashes-with-password4j","Checking hashes with password4j",[211,259,260,261,271],{},"In the ktor app - to check the hash you can use any library that can handle bcrypt - for this post - we'll look at ",[262,263,270],"a",{"href":264,"rel":265,"target":269},"https:\u002F\u002Fgithub.com\u002FPassword4j\u002Fpassword4j",[266,267,268],"nofollow","noopener","noreferer","_blank","password4j",".",[273,274,279],"pre",{"className":275,"code":276,"language":277,"meta":278,"style":278},"language-kotlin shiki shiki-themes github-dark","Password.check(plaintextPassword, storedHash).withBcrypt()\n","kotlin","",[240,280,281],{"__ignoreMap":278},[282,283,286,290,294,297,300],"span",{"class":284,"line":285},"line",1,[282,287,289],{"class":288},"s95oV","Password.",[282,291,293],{"class":292},"svObZ","check",[282,295,296],{"class":288},"(plaintextPassword, storedHash).",[282,298,299],{"class":292},"withBcrypt",[282,301,302],{"class":288},"()\n",[211,304,305],{},"Now - it doesn't know about the prefix - so that has to be stripped off the hash before we check.",[211,307,308],{},"So - users will still be able to login after a migration with the same password.",[218,310,312],{"id":311},"updating-the-hash","Updating the hash",[211,314,315,316,321],{},"However - password4j ",[262,317,320],{"href":318,"rel":319,"target":269},"https:\u002F\u002Fgithub.com\u002FPassword4j\u002Fpassword4j\u002Fwiki\u002FBCrypt",[266,267,268],"recommends"," $2b and cost 12.",[211,323,324],{},"How can we update this hash when we don't know the user's password?",[211,326,327],{},"We can actually use password4j to provide us the new hash at check time.",[329,330,332],"h3",{"id":331},"configuration","Configuration",[211,334,335],{},"Add psw4j.properties to the classpath (src\u002Fmain\u002Fresources)",[273,337,341],{"className":338,"code":339,"language":340,"meta":278,"style":278},"language-ini shiki shiki-themes github-dark","global.banner=false\nhash.bcrypt.minor=b\nhash.bcrypt.rounds=12\n","ini",[240,342,343,352,361],{"__ignoreMap":278},[282,344,345,349],{"class":284,"line":285},[282,346,348],{"class":347},"snl16","global.banner",[282,350,351],{"class":288},"=false\n",[282,353,355,358],{"class":284,"line":354},2,[282,356,357],{"class":347},"hash.bcrypt.minor",[282,359,360],{"class":288},"=b\n",[282,362,364,367],{"class":284,"line":363},3,[282,365,366],{"class":347},"hash.bcrypt.rounds",[282,368,369],{"class":288},"=12\n",[211,371,372],{},"(this also turns off the banner in the logs)",[329,374,376],{"id":375},"simple-user-service-implementation","Simple user service implementation",[211,378,379],{},"This requires some sort of repository that allows you to get the stored hash for a username and to update the hash for a username.",[381,382,383,387,390,397,404,407],"ol",{},[384,385,386],"li",{},"Call checkPassword at login with the user supplied username and password.",[384,388,389],{},"Fetch the current hash from the database (as string)",[384,391,392,393,396],{},"Clean it (remove the ",[240,394,395],{},"{bcrypt}"," prefix)",[384,398,399,400,403],{},"Check that the hash is valid - with ",[240,401,402],{},"andUpdate()"," which gives us a new hash if necessary as well as telling us if it is valid",[384,405,406],{},"If it is valid and it is updated (changed) - save the hash back to the database",[384,408,409],{},"Return valid or not",[273,411,413],{"className":275,"code":412,"language":277,"meta":278,"style":278},"class UserService(private val repository: UserRepository) {\n\n    fun checkPassword(username: String, password: String): Boolean {\n        var validPassword = false\n\n        repository.hashForUser(username)?.let { dbPassword ->\n            val check = Password.check(password, dbPassword.clean()).andUpdate().withBcrypt()\n\n            if (check.isVerified && check.isUpdated) {\n                repository.storeHash(username, check.hash.result)\n            }\n\n            validPassword = check.isVerified\n        }\n\n        return validPassword\n    }\n\n    companion object {\n        const val oldPrefix = \"{bcrypt}\"\n    }\n\n    private fun String.clean() = this.replace(oldPrefix, \"\")\n\n}\n",[240,414,415,441,447,475,491,496,517,552,557,572,584,590,595,606,612,617,626,632,637,648,665,670,675,713,718],{"__ignoreMap":278},[282,416,417,420,423,426,429,432,435,438],{"class":284,"line":285},[282,418,419],{"class":347},"class",[282,421,422],{"class":292}," UserService",[282,424,425],{"class":288},"(",[282,427,428],{"class":347},"private",[282,430,431],{"class":347}," val",[282,433,434],{"class":288}," repository: ",[282,436,437],{"class":292},"UserRepository",[282,439,440],{"class":288},") {\n",[282,442,443],{"class":284,"line":354},[282,444,446],{"emptyLinePlaceholder":445},true,"\n",[282,448,449,452,455,458,461,464,466,469,472],{"class":284,"line":363},[282,450,451],{"class":347},"    fun",[282,453,454],{"class":292}," checkPassword",[282,456,457],{"class":288},"(username: ",[282,459,460],{"class":292},"String",[282,462,463],{"class":288},", password: ",[282,465,460],{"class":292},[282,467,468],{"class":288},"): ",[282,470,471],{"class":292},"Boolean",[282,473,474],{"class":288}," {\n",[282,476,478,481,484,487],{"class":284,"line":477},4,[282,479,480],{"class":347},"        var",[282,482,483],{"class":288}," validPassword ",[282,485,486],{"class":347},"=",[282,488,490],{"class":489},"sDLfK"," false\n",[282,492,494],{"class":284,"line":493},5,[282,495,446],{"emptyLinePlaceholder":445},[282,497,499,502,505,508,511,514],{"class":284,"line":498},6,[282,500,501],{"class":288},"        repository.",[282,503,504],{"class":292},"hashForUser",[282,506,507],{"class":288},"(username)?.",[282,509,510],{"class":292},"let",[282,512,513],{"class":288}," { dbPassword ",[282,515,516],{"class":347},"->\n",[282,518,520,523,526,528,531,533,536,539,542,545,548,550],{"class":284,"line":519},7,[282,521,522],{"class":347},"            val",[282,524,525],{"class":288}," check ",[282,527,486],{"class":347},[282,529,530],{"class":288}," Password.",[282,532,293],{"class":292},[282,534,535],{"class":288},"(password, dbPassword.",[282,537,538],{"class":292},"clean",[282,540,541],{"class":288},"()).",[282,543,544],{"class":292},"andUpdate",[282,546,547],{"class":288},"().",[282,549,299],{"class":292},[282,551,302],{"class":288},[282,553,555],{"class":284,"line":554},8,[282,556,446],{"emptyLinePlaceholder":445},[282,558,560,563,566,569],{"class":284,"line":559},9,[282,561,562],{"class":347},"            if",[282,564,565],{"class":288}," (check.isVerified ",[282,567,568],{"class":347},"&&",[282,570,571],{"class":288}," check.isUpdated) {\n",[282,573,575,578,581],{"class":284,"line":574},10,[282,576,577],{"class":288},"                repository.",[282,579,580],{"class":292},"storeHash",[282,582,583],{"class":288},"(username, check.hash.result)\n",[282,585,587],{"class":284,"line":586},11,[282,588,589],{"class":288},"            }\n",[282,591,593],{"class":284,"line":592},12,[282,594,446],{"emptyLinePlaceholder":445},[282,596,598,601,603],{"class":284,"line":597},13,[282,599,600],{"class":288},"            validPassword ",[282,602,486],{"class":347},[282,604,605],{"class":288}," check.isVerified\n",[282,607,609],{"class":284,"line":608},14,[282,610,611],{"class":288},"        }\n",[282,613,615],{"class":284,"line":614},15,[282,616,446],{"emptyLinePlaceholder":445},[282,618,620,623],{"class":284,"line":619},16,[282,621,622],{"class":347},"        return",[282,624,625],{"class":288}," validPassword\n",[282,627,629],{"class":284,"line":628},17,[282,630,631],{"class":288},"    }\n",[282,633,635],{"class":284,"line":634},18,[282,636,446],{"emptyLinePlaceholder":445},[282,638,640,643,646],{"class":284,"line":639},19,[282,641,642],{"class":347},"    companion",[282,644,645],{"class":347}," object",[282,647,474],{"class":288},[282,649,651,654,656,659,661],{"class":284,"line":650},20,[282,652,653],{"class":347},"        const",[282,655,431],{"class":347},[282,657,658],{"class":288}," oldPrefix ",[282,660,486],{"class":347},[282,662,664],{"class":663},"sU2Wk"," \"{bcrypt}\"\n",[282,666,668],{"class":284,"line":667},21,[282,669,631],{"class":288},[282,671,673],{"class":284,"line":672},22,[282,674,446],{"emptyLinePlaceholder":445},[282,676,678,681,684,687,689,691,694,696,699,701,704,707,710],{"class":284,"line":677},23,[282,679,680],{"class":347},"    private",[282,682,683],{"class":347}," fun",[282,685,686],{"class":292}," String",[282,688,271],{"class":288},[282,690,538],{"class":292},[282,692,693],{"class":288},"() ",[282,695,486],{"class":347},[282,697,698],{"class":489}," this",[282,700,271],{"class":288},[282,702,703],{"class":292},"replace",[282,705,706],{"class":288},"(oldPrefix, ",[282,708,709],{"class":663},"\"\"",[282,711,712],{"class":288},")\n",[282,714,716],{"class":284,"line":715},24,[282,717,446],{"emptyLinePlaceholder":445},[282,719,721],{"class":284,"line":720},25,[282,722,723],{"class":288},"}\n",[329,725,727],{"id":726},"summary","Summary",[211,729,730],{},"This will allow the user to login with their existing password.",[211,732,733],{},"It will also update the user to password4j's recommeded $2b cost 12 setting - and update the database if required.",[211,735,736],{},"This update will be invisible to the user too.",[738,739,740],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":278,"searchDepth":354,"depth":354,"links":742},[743,744,745],{"id":220,"depth":354,"text":221},{"id":256,"depth":354,"text":257},{"id":311,"depth":354,"text":312,"children":746},[747,748,749],{"id":331,"depth":363,"text":332},{"id":375,"depth":363,"text":376},{"id":726,"depth":363,"text":727},"2023-04-21 10:13 +0200",null,"md","When moving a spring boot project to ktor - how to handle existing password hashes in the database?",{},"\u002F2023\u002F04\u002F21\u002Fhandling-spring-security-password-hashes-when-migrating-to-ktor",{"title":206,"description":213},{"loc":755},"2023\u002F04\u002F21\u002Fhandling-spring-security-password-hashes-when-migrating-to-ktor",[760,277,761,762,763,764],"ktor","spring","spring security","bcrypt","password","Q4G9t0UY7tlXrlwuzcjzPute4yBC5JgZrMZsc7LWiPY",1775293006520]