Last active
July 7, 2020 23:36
-
-
Save nbenns/1f3fa4e8395caaa91c4bfb02627722dc to your computer and use it in GitHub Desktop.
Making sure you don't screw up passwords via Phantom types
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
sealed trait SecurityState | |
trait PlainText extends SecurityState | |
trait Hashed extends SecurityState | |
trait Confirmed extends SecurityState | |
trait Verified extends SecurityState | |
sealed trait UpdateState | |
trait New extends UpdateState | |
trait Confirmation extends UpdateState | |
trait Current extends UpdateState | |
trait Provided extends UpdateState | |
sealed trait PasswordError extends Throwable with Product with Serializable | |
final case class PasswordInvalidError() extends PasswordError | |
final case class PasswordsDontMatchError() extends PasswordError | |
sealed abstract class Password[A <: UpdateState, S <: SecurityState] private(val value: String) | |
object Password { | |
private def apply[A <: UpdateState, S <: SecurityState](value: String): Password[A, S] = | |
new Password[A, S](value) {} | |
def setCurrent(ev: Password[Provided, Verified], p: Password[New, Confirmed]): Password[Current, Hashed] = | |
Password(p.value) | |
def hash[A <: UpdateState](p: Password[A, PlainText]): Either[Nothing, Password[A, Hashed]] = | |
Right(Password(p.value)) | |
def confirm(p1: Password[New, Hashed], p2: Password[Confirmation, Hashed]): Either[PasswordsDontMatchError, Password[New, Confirmed]] = | |
if (p1.value == p2.value) Right(Password(p1.value)) | |
else Left(PasswordsDontMatchError()) | |
def verify(p1: Password[Current, Hashed], p2: Password[Provided, Hashed]): Either[PasswordInvalidError, Password[Provided, Verified]] = | |
if (p1.value == p2.value) Right(Password(p2.value)) | |
else Left(PasswordInvalidError()) | |
} | |
case class ChangePasswordRequest( | |
email: String, | |
currentPassword: Password[Provided, PlainText], | |
newPassword: Password[New, PlainText], | |
confirmationPassword: Password[Confirmation, PlainText] | |
) | |
case class User(id: Long, email: String, password: Password[Current, Hashed]) { | |
def updatePassword(req: ChangePasswordRequest): Either[PasswordError, User] = | |
for { | |
currentPassword <- Password.hash(req.currentPassword) | |
verifiedPassword <- Password.verify(password, currentPassword) | |
newPassword <- Password.hash(req.newPassword) | |
confirmationPassword <- Password.hash(req.confirmationPassword) | |
confirmedPassword <- Password.confirm(newPassword, confirmationPassword) | |
updatedPassword = Password.setCurrent(verifiedPassword, confirmedPassword) | |
} yield this.copy(password = updatedPassword) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment