Use HMAC for ident hashes and adds dedicated activateEmail page.

This commit is contained in:
Dominik Picheta 2018-05-21 15:00:24 +01:00
commit 595b0ea086
5 changed files with 106 additions and 17 deletions

View file

@ -15,6 +15,7 @@ skipExt = @["nim"]
requires "nim >= 0.14.0" requires "nim >= 0.14.0"
requires "jester#64295c8" requires "jester#64295c8"
requires "bcrypt#head" requires "bcrypt#head"
requires "hmac#9c61ebe2fd134cf97"
requires "recaptcha 1.0.2" requires "recaptcha 1.0.2"
requires "sass" requires "sass"

View file

@ -1,6 +1,6 @@
import random, md5 import random, md5
import bcrypt import bcrypt, hmac
proc randomSalt(): string = proc randomSalt(): string =
result = "" result = ""
@ -47,12 +47,44 @@ proc makePassword*(password, salt: string, comparingTo = ""): string =
let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8) let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8)
result = hash(getMD5(salt & getMD5(password)), bcryptSalt) result = hash(getMD5(salt & getMD5(password)), bcryptSalt)
proc makeIdentHash*(user, password: string, epoch: int64, secret: string, proc makeIdentHash*(user, password: string, epoch: int64,
comparingTo = ""): string = secret: string): string =
## Creates a hash verifying the identity of a user. Used for password reset ## Creates a hash verifying the identity of a user. Used for password reset
## links and email activation links. ## links and email activation links.
## The ``epoch`` determines the creation time of this hash, it will be checked ## The ``epoch`` determines the creation time of this hash, it will be checked
## during verification to ensure the hash hasn't expired. ## during verification to ensure the hash hasn't expired.
## The ``secret`` is the 'salt' field in the ``person`` table. ## The ``secret`` is the 'salt' field in the ``person`` table.
let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8) result = hmac_sha256(secret, user & password & $epoch).toHex()
result = hash(user & password & $epoch & secret, bcryptSalt)
when isMainModule:
block:
let ident = makeIdentHash("test", "pass", 1526908753, "randomtext")
let ident2 = makeIdentHash("test", "pass", 1526908753, "randomtext")
doAssert ident == ident2
let invalid = makeIdentHash("test", "pass", 1526908754, "randomtext")
doAssert ident != invalid
block:
let ident = makeIdentHash(
"test",
"$2a$08$bY85AhoD1e9u0IsD9sM7Ee6kFSLeXRLxJ6rMgfb1wDnU9liaymoTG",
1526908753,
"*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\um"
)
let ident2 = makeIdentHash(
"test",
"$2a$08$bY85AhoD1e9u0IsD9sM7Ee6kFSLeXRLxJ6rMgfb1wDnU9liaymoTG",
1526908753,
"*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\um"
)
doAssert ident == ident2
let invalid = makeIdentHash(
"test",
"$2a$08$bY85AhoD1e9u0IsD9sM7Ee6kFSLeXRLxJ6rMgfb1wDnU9liaymoTG",
1526908754,
"*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\um"
)
doAssert ident != invalid

View file

@ -206,7 +206,7 @@ proc verifyIdentHash(
var row = getRow(db, query, name) var row = getRow(db, query, name)
if row[0] == "": if row[0] == "":
raise newForumError("User doesn't exist.", @["nick"]) raise newForumError("User doesn't exist.", @["nick"])
let newIdent = makeIdentHash(name, row[0], epoch, row[1], ident) let newIdent = makeIdentHash(name, row[0], epoch, row[1])
# Check that it hasn't expired. # Check that it hasn't expired.
let diff = getTime() - epoch.fromUnix() let diff = getTime() - epoch.fromUnix()
if diff.hours > 2: if diff.hours > 2:
@ -1250,7 +1250,7 @@ routes:
except ForumError as exc: except ForumError as exc:
resp Http400, $(%exc.data),"application/json" resp Http400, $(%exc.data),"application/json"
get "/activateEmail": post "/activateEmail":
createTFD() createTFD()
cond(@"nick" != "") cond(@"nick" != "")
cond(@"epoch" != "") cond(@"epoch" != "")
@ -1267,9 +1267,9 @@ routes:
""", """,
$Rank.Moderated, @"nick" $Rank.Moderated, @"nick"
) )
redirect(uri("/activateEmail/success")) resp Http200, "{}", "application/json"
except ForumError as exc: except ForumError as exc:
redirect(uri("/activateEmail/failure/" & encodeUrl(exc.data.message))) resp Http400, $(%exc.data),"application/json"
get "/t/@id": get "/t/@id":
cond "id" in request.params cond "id" in request.params

View file

@ -0,0 +1,58 @@
when defined(js):
import sugar, httpcore, options, json
import dom except Event
include karax/prelude
import karax / [kajax, kdom]
import error, replybox, threadlist, post
import karaxutils
type
ActivateEmail* = ref object
loading: bool
status: HttpCode
error: Option[PostError]
newPassword: kstring
proc newActivateEmail*(): ActivateEmail =
ActivateEmail(
status: Http200,
newPassword: ""
)
proc onPassChange(e: Event, n: VNode, state: ActivateEmail) =
state.newPassword = n.value
proc onPost(httpStatus: int, response: kstring, state: ActivateEmail) =
postFinished:
navigateTo(makeUri("/activateEmail/success"))
proc onSetClick(
ev: Event, n: VNode,
state: ActivateEmail
) =
state.loading = true
state.error = none[PostError]()
let uri = makeUri("activateEmail", search = $kdom.window.location.search)
ajaxPost(uri, @[], "",
(s: int, r: kstring) => onPost(s, r, state))
proc render*(state: ActivateEmail): VNode =
result = buildHtml():
section(class="container grid-xl"):
tdiv(id="activateemail"):
tdiv(class="title"):
p(): text "Activate Email"
tdiv(class="content"):
button(class=class(
{"loading": state.loading},
"btn btn-primary"
),
onClick=(ev: Event, n: VNode) =>
(onSetClick(ev, n, state))):
text "Activate"
if state.error.isSome():
p(class="text-error"):
text state.error.get().message

View file

@ -5,7 +5,7 @@ include karax/prelude
import jester/patterns import jester/patterns
import threadlist, postlist, header, profile, newthread, error, about import threadlist, postlist, header, profile, newthread, error, about
import resetpassword import resetpassword, activateemail
import karaxutils import karaxutils
type type
@ -15,6 +15,7 @@ type
newThread: NewThread newThread: NewThread
about: About about: About
resetPassword: ResetPassword resetPassword: ResetPassword
activateEmail: ActivateEmail
proc copyLocation(loc: Location): Location = proc copyLocation(loc: Location): Location =
# TODO: It sucks that I had to do this. We need a nice way to deep copy in JS. # TODO: It sucks that I had to do this. We need a nice way to deep copy in JS.
@ -35,7 +36,8 @@ proc newState(): State =
profile: newProfileState(), profile: newProfileState(),
newThread: newNewThread(), newThread: newNewThread(),
about: newAbout(), about: newAbout(),
resetPassword: newResetPassword() resetPassword: newResetPassword(),
activateEmail: newActivateEmail()
) )
var state = newState() var state = newState()
@ -104,13 +106,9 @@ proc render(): VNode =
) )
) )
), ),
r("/activateEmail/failure/@msg", r("/activateEmail",
(params: Params) => ( (params: Params) => (
renderMessage( render(state.activateEmail)
"Email activation failed",
decodeUrl(params["msg"]),
"fa-exclamation"
)
) )
), ),
r("/resetPassword/success", r("/resetPassword/success",