Browse Source

create login + session auth

main
James Fenn 2 months ago
parent
commit
e5d960c407
9 changed files with 145 additions and 18 deletions
  1. +4
    -0
      identity/src/main/kotlin/dev/horrific/identity/Application.kt
  2. +18
    -0
      identity/src/main/kotlin/dev/horrific/identity/controller/HomeController.kt
  3. +71
    -9
      identity/src/main/kotlin/dev/horrific/identity/controller/LoginController.kt
  4. +0
    -1
      identity/src/main/kotlin/dev/horrific/identity/data/UserDao.kt
  5. +18
    -0
      identity/src/main/kotlin/dev/horrific/identity/extensions/UserInfo.kt
  6. +16
    -0
      identity/src/main/kotlin/dev/horrific/identity/model/UserPrincipal.kt
  7. +7
    -0
      identity/src/main/kotlin/dev/horrific/identity/model/view/LoginViewModel.kt
  8. +0
    -2
      identity/src/main/kotlin/dev/horrific/identity/util/PasswordHasher.kt
  9. +11
    -6
      identity/static/templates/login.html

+ 4
- 0
identity/src/main/kotlin/dev/horrific/identity/Application.kt View File

@@ -1,6 +1,7 @@
package dev.horrific.identity

import com.mongodb.reactivestreams.client.MongoClient
import dev.horrific.identity.controller.HomeController
import dev.horrific.identity.controller.LoginController
import dev.horrific.identity.data.UserDao
import dev.horrific.identity.util.PasswordHasher
@@ -38,5 +39,8 @@ fun Application.commonMain() {
// auth/login process, session tokens
LoginController.install(this)

// site homepage
HomeController.install(this)

println("Started: ${env.staticDir}")
}

+ 18
- 0
identity/src/main/kotlin/dev/horrific/identity/controller/HomeController.kt View File

@@ -0,0 +1,18 @@
package dev.horrific.identity.controller

import dev.horrific.shared.controller.BaseController
import io.ktor.application.*
import io.ktor.response.*
import io.ktor.routing.*

object HomeController : BaseController(
authentication = LoginController.AUTH_SESSION
) {

override fun Route.routes() {
get {
call.respondText("Hello!")
}
}

}

+ 71
- 9
identity/src/main/kotlin/dev/horrific/identity/controller/LoginController.kt View File

@@ -2,14 +2,22 @@ package dev.horrific.identity.controller

import at.favre.lib.crypto.bcrypt.BCrypt
import dev.horrific.identity.data.UserDao
import dev.horrific.identity.extensions.user
import dev.horrific.identity.model.UserInfo
import dev.horrific.identity.model.UserPrincipal
import dev.horrific.identity.model.view.LoginViewModel
import dev.horrific.identity.util.PasswordHasher
import dev.horrific.shared.controller.BaseController
import dev.horrific.shared.env
import dev.horrific.shared.extensions.respondTemplate
import dev.horrific.shared.extensions.template
import dev.horrific.shared.utils.redirect
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.request.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.sessions.*
import org.koin.core.component.inject
import java.security.SecureRandom
import kotlin.random.Random
@@ -18,17 +26,61 @@ object LoginController : BaseController(
baseUrl = "/login"
) {

val userDao: UserDao by inject()
const val AUTH_SESSION = "session"

val userDao: UserDao by inject()
val hasher: PasswordHasher by inject()

private suspend fun ApplicationCall.respondLogin(error: String = "") {
respondTemplate("login", LoginViewModel(error))
}

override fun Application.create() {
install(Authentication) {
session<UserPrincipal>(AUTH_SESSION) {
challenge {
// if the user isn't authenticated - display login
call.respondLogin()
}
validate { session ->
println("validating session for ${session.userId}")
// check that the user session hasn't expired
if (!session.isExpired()) session else {
println("SESSION EXPIRED")
null
}
}
}
}

install(Sessions) {
cookie<UserPrincipal>(
"USER_SESSION",
storage = SessionStorageMemory()
) {
cookie.path = "/"
cookie.httpOnly = true
if (env.isProd) {
cookie.secure = true
cookie.domain = env.hostname
}

// Cross-Site Forgery protection in modern browsers.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#SameSite_cookies
cookie.extensions["SameSite"] = "lax"
}
}
}

override fun Route.routes() {
// TODO: if logged in, redirect to home
get {
if (call.sessions.get<UserPrincipal>()?.isExpired() == false)
redirect("/")

template("", "login")
template("/new", "login")
call.respondLogin()
}

post("") {
post {
val params = call.receiveParameters()
val name = params["username"]!!
val password = params["password"]!!
@@ -41,10 +93,17 @@ object LoginController : BaseController(

if (!isValid) error("Password is not correct!")

// TODO: actually auth the user
call.sessions.set(UserPrincipal(user))
redirect("/")
}

get("/new") {
if (call.sessions.get<UserPrincipal>()?.isExpired() == false)
redirect("/")

call.respondLogin()
}

post("/new") {
val params = call.receiveParameters()
val name = params["username"]!!
@@ -55,12 +114,15 @@ object LoginController : BaseController(
if (email.isEmpty()) error("E-mail must be provided!")

val hash = hasher.hash(password)

userDao.create(UserInfo(
val user = UserInfo(
name = name,
email = email,
passwordHash = hash
))
)

userDao.create(user)
call.sessions.set(UserPrincipal(user))
redirect("/")
}
}


+ 0
- 1
identity/src/main/kotlin/dev/horrific/identity/data/UserDao.kt View File

@@ -3,7 +3,6 @@ package dev.horrific.identity.data
import dev.horrific.identity.model.UserInfo
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.litote.kmongo.coroutine.CoroutineClient
import org.litote.kmongo.coroutine.CoroutineDatabase

class UserDao : KoinComponent {


+ 18
- 0
identity/src/main/kotlin/dev/horrific/identity/extensions/UserInfo.kt View File

@@ -0,0 +1,18 @@
package dev.horrific.identity.extensions

import dev.horrific.identity.data.UserDao
import dev.horrific.identity.model.UserPrincipal
import dev.horrific.shared.utils.error
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.*
import io.ktor.sessions.*
import org.koin.ktor.ext.inject

suspend fun ApplicationCall.user() {
val userDao : UserDao by application.inject()
val principal = sessions.get<UserPrincipal>()
principal?.let {
userDao.get(it.userId)
} ?: error(HttpStatusCode.Unauthorized, "User not signed in.")
}

+ 16
- 0
identity/src/main/kotlin/dev/horrific/identity/model/UserPrincipal.kt View File

@@ -0,0 +1,16 @@
package dev.horrific.identity.model

import io.ktor.auth.*

class UserPrincipal(
val userId: String,
val expiration: Long = System.currentTimeMillis() + 864000000 // expire in 10 days
) : Principal {

constructor(user: UserInfo) : this(user.name)

fun isExpired() : Boolean {
return System.currentTimeMillis() > expiration
}

}

+ 7
- 0
identity/src/main/kotlin/dev/horrific/identity/model/view/LoginViewModel.kt View File

@@ -0,0 +1,7 @@
package dev.horrific.identity.model.view

import dev.horrific.shared.model.view.ViewModel

class LoginViewModel(
val error: String = ""
) : ViewModel()

+ 0
- 2
identity/src/main/kotlin/dev/horrific/identity/util/PasswordHasher.kt View File

@@ -2,8 +2,6 @@ package dev.horrific.identity.util

import at.favre.lib.crypto.bcrypt.BCrypt
import at.favre.lib.crypto.bcrypt.LongPasswordStrategies
import java.security.SecureRandom
import java.util.*

class PasswordHasher {



+ 11
- 6
identity/static/templates/login.html View File

@@ -2,29 +2,34 @@
{% extends "_base.html" %}
{% block content %}
<div class="card text-center mx-auto py-3 my-5" style="max-width: 600px;">
{% if path.contains("/new") %}
{% set isNew = path.contains("/new") %}
{% if isNew %}
{% set action = "/login/new" %}
<span class="card-title">Create a new account</span>
<div class="card-body">
Already have an account? <a href="/login">Log in</a>
</div>
{% else %}
{% set action = "/login" %}
<span class="card-title">Sign in to your account</span>
<div class="card-body">
If you don't have an account, then <a href="/login/new">sign up</a>.
</div>
{% endif %}

<form method="post" class="card-body">
<form action="{{ action }}" method="post" class="card-body">
<div class="input d-block">
<input name="username" type="text" placeholder="Username" required>
</div>
{% if path.contains("/new") %}<div class="input d-block">
{% if isNew %}
<div class="input d-block">
<input name="email" type="text" placeholder="E-mail address" required>
</div>{% endif %}
</div>
{% endif %}
<div class="input d-block">
<input name="password" type="password" placeholder="Password" required>
</div>
{% if path.contains("/new") %}
{% if isNew %}
<div class="input d-block">
<input name="password-confirm" type="password" placeholder="Confirm password" required>
</div>
@@ -41,7 +46,7 @@

<br>

<input type="submit" value="Register">
<input type="submit" value="{% if isNew %}Register{% else %}Sign in{% endif %}">
</form>
</div>
{% endblock %}

Loading…
Cancel
Save