Merge branch 'feature/token-renewal'
This commit is contained in:
commit
5799949e00
@ -9,11 +9,27 @@ Content-Type: application/json
|
|||||||
|
|
||||||
> {%
|
> {%
|
||||||
client.global.set("token", response.body.token);
|
client.global.set("token", response.body.token);
|
||||||
|
client.global.set("refreshToken", response.body.refreshToken);
|
||||||
client.test("Request executed successfully", function() {
|
client.test("Request executed successfully", function() {
|
||||||
client.assert(response.status === 200, "Response status is not 200");
|
client.assert(response.status === 200, "Response status is not 200");
|
||||||
});
|
});
|
||||||
%}
|
%}
|
||||||
|
|
||||||
|
### Refresh token
|
||||||
|
POST http://localhost:8081/user/refresh_token
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"refreshToken": "{{refreshToken}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
> {%
|
||||||
|
client.test("Request executed successfully", function() {
|
||||||
|
client.global.set("token", response.body.token);
|
||||||
|
client.assert(response.status === 200, "Response status is not 200");
|
||||||
|
});
|
||||||
|
%}
|
||||||
|
|
||||||
### Get notes
|
### Get notes
|
||||||
GET http://localhost:8081/notes
|
GET http://localhost:8081/notes
|
||||||
Authorization: Bearer {{token}}
|
Authorization: Bearer {{token}}
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import org.kodein.di.generic.instance
|
|||||||
fun Application.authenticationModule() {
|
fun Application.authenticationModule() {
|
||||||
install(Authentication) {
|
install(Authentication) {
|
||||||
jwt {
|
jwt {
|
||||||
val simpleJwt by kodein.instance<SimpleJWT>()
|
val simpleJwt by kodein.instance<SimpleJWT>(tag = "auth")
|
||||||
verifier(simpleJwt.verifier)
|
verifier(simpleJwt.verifier)
|
||||||
validate {
|
validate {
|
||||||
UserIdPrincipal(it.payload.getClaim("name").asString())
|
UserIdPrincipal(it.payload.getClaim("email").asString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,14 +4,15 @@ import com.auth0.jwt.JWT
|
|||||||
import com.auth0.jwt.JWTVerifier
|
import com.auth0.jwt.JWTVerifier
|
||||||
import com.auth0.jwt.algorithms.Algorithm
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class SimpleJWT(secret: String) {
|
class SimpleJWT(secret: String, validity: Long, unit: TimeUnit) {
|
||||||
private val validityInMs = 36_000_00 * 1
|
private val validityInMs = TimeUnit.MILLISECONDS.convert(validity, unit)
|
||||||
private val algorithm = Algorithm.HMAC256(secret)
|
private val algorithm = Algorithm.HMAC256(secret)
|
||||||
|
|
||||||
val verifier: JWTVerifier = JWT.require(algorithm).build()
|
val verifier: JWTVerifier = JWT.require(algorithm).build()
|
||||||
fun sign(name: String): String = JWT.create()
|
fun sign(email: String): String = JWT.create()
|
||||||
.withClaim("name", name)
|
.withClaim("email", email)
|
||||||
.withExpiresAt(getExpiration())
|
.withExpiresAt(getExpiration())
|
||||||
.sign(algorithm)
|
.sign(algorithm)
|
||||||
|
|
||||||
|
|||||||
@ -17,16 +17,18 @@ suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
|
|||||||
respond(status, status.description)
|
respond(status, status.description)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the user email for the currently authenticated user
|
||||||
|
*/
|
||||||
|
fun ApplicationCall.userEmail() = principal<UserIdPrincipal>()!!.name
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the userId for the currently authenticated user
|
* @return the userId for the currently authenticated user
|
||||||
*/
|
*/
|
||||||
fun ApplicationCall.userId(): Int {
|
fun ApplicationCall.userId() = userService.getUserId(userEmail())!!
|
||||||
val email = principal<UserIdPrincipal>()!!.name
|
|
||||||
return userService.getUserId(email)!!
|
|
||||||
}
|
|
||||||
|
|
||||||
class NoteCreate(val title: String, val tags: List<String>)
|
class NoteCreate(val title: String, val tags: List<String>)
|
||||||
|
|
||||||
suspend fun ApplicationCall.receiveNoteCreate(): FullNoteCreateDTO = receive()
|
suspend fun ApplicationCall.receiveNoteCreate(): FullNoteCreateDTO = receive()
|
||||||
|
|
||||||
suspend fun ApplicationCall.receiveNotePatch() : FullNotePatchDTO = receive()
|
suspend fun ApplicationCall.receiveNotePatch(): FullNotePatchDTO = receive()
|
||||||
@ -7,6 +7,7 @@ import io.ktor.application.*
|
|||||||
import org.kodein.di.Kodein
|
import org.kodein.di.Kodein
|
||||||
import org.kodein.di.generic.bind
|
import org.kodein.di.generic.bind
|
||||||
import org.kodein.di.generic.instance
|
import org.kodein.di.generic.instance
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,10 +30,13 @@ fun Application.configurationFeature() {
|
|||||||
HikariDataSource(hikariConfig)
|
HikariDataSource(hikariConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
val simpleJwt = SimpleJWT(environment.config.property("jwt.secret").getString())
|
val jwtSecret = environment.config.property("jwt.secret").getString()
|
||||||
|
val authSimpleJwt = SimpleJWT(jwtSecret, 1, TimeUnit.HOURS)
|
||||||
|
val refreshSimpleJwt = SimpleJWT(jwtSecret, 7, TimeUnit.DAYS)
|
||||||
|
|
||||||
configurationModule = Kodein.Module("Configuration") {
|
configurationModule = Kodein.Module("Configuration") {
|
||||||
bind<DataSource>() with instance(dataSource)
|
bind<DataSource>() with instance(dataSource)
|
||||||
bind<SimpleJWT>() with instance(simpleJwt)
|
bind<SimpleJWT>(tag = "auth") with instance(authSimpleJwt)
|
||||||
|
bind<SimpleJWT>(tag = "refresh") with instance(refreshSimpleJwt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
70
api/src/routing/AuthController.kt
Normal file
70
api/src/routing/AuthController.kt
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package be.vandewalleh.routing
|
||||||
|
|
||||||
|
import be.vandewalleh.auth.SimpleJWT
|
||||||
|
import be.vandewalleh.auth.UsernamePasswordCredential
|
||||||
|
import be.vandewalleh.extensions.respondStatus
|
||||||
|
import be.vandewalleh.services.UserService
|
||||||
|
import com.auth0.jwt.exceptions.JWTVerificationException
|
||||||
|
import io.ktor.application.*
|
||||||
|
import io.ktor.auth.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.request.*
|
||||||
|
import io.ktor.response.*
|
||||||
|
import io.ktor.routing.*
|
||||||
|
import org.kodein.di.Kodein
|
||||||
|
import org.kodein.di.generic.instance
|
||||||
|
import org.mindrot.jbcrypt.BCrypt
|
||||||
|
|
||||||
|
data class RefreshToken(val refreshToken: String)
|
||||||
|
data class DualToken(val token: String, val refreshToken: String)
|
||||||
|
|
||||||
|
fun Routing.auth(kodein: Kodein) {
|
||||||
|
val authSimpleJwt by kodein.instance<SimpleJWT>("auth")
|
||||||
|
val refreshSimpleJwt by kodein.instance<SimpleJWT>("refresh")
|
||||||
|
val userService by kodein.instance<UserService>()
|
||||||
|
|
||||||
|
post("/user/login") {
|
||||||
|
val credential = call.receive<UsernamePasswordCredential>()
|
||||||
|
|
||||||
|
val (email, password) = userService.getEmailAndPasswordFromUsername(credential.username)
|
||||||
|
?: return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
||||||
|
|
||||||
|
if (!BCrypt.checkpw(credential.password, password)) {
|
||||||
|
return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = DualToken(
|
||||||
|
token = authSimpleJwt.sign(email),
|
||||||
|
refreshToken = refreshSimpleJwt.sign(email)
|
||||||
|
)
|
||||||
|
return@post call.respond(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
post("/user/refresh_token") {
|
||||||
|
val token = call.receive<RefreshToken>().refreshToken
|
||||||
|
|
||||||
|
val email = try {
|
||||||
|
val decodedJWT = refreshSimpleJwt.verifier.verify(token)
|
||||||
|
decodedJWT.getClaim("email").asString()
|
||||||
|
} catch (e: JWTVerificationException) {
|
||||||
|
return@post call.respondStatus(HttpStatusCode.Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = DualToken(
|
||||||
|
token = authSimpleJwt.sign(email),
|
||||||
|
refreshToken = refreshSimpleJwt.sign(email)
|
||||||
|
)
|
||||||
|
return@post call.respond(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticate {
|
||||||
|
get("/user/me") {
|
||||||
|
// retrieve email from token
|
||||||
|
val email = call.principal<UserIdPrincipal>()!!.name
|
||||||
|
val info = userService.getUserInfo(email)
|
||||||
|
if (info != null) call.respond(mapOf("user" to info))
|
||||||
|
else call.respondStatus(HttpStatusCode.Unauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,46 +0,0 @@
|
|||||||
package be.vandewalleh.routing
|
|
||||||
|
|
||||||
import be.vandewalleh.auth.SimpleJWT
|
|
||||||
import be.vandewalleh.auth.UsernamePasswordCredential
|
|
||||||
import be.vandewalleh.extensions.respondStatus
|
|
||||||
import be.vandewalleh.services.UserService
|
|
||||||
import io.ktor.application.*
|
|
||||||
import io.ktor.auth.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.request.*
|
|
||||||
import io.ktor.response.*
|
|
||||||
import io.ktor.routing.*
|
|
||||||
import org.kodein.di.Kodein
|
|
||||||
import org.kodein.di.generic.instance
|
|
||||||
import org.mindrot.jbcrypt.BCrypt
|
|
||||||
|
|
||||||
fun Routing.login(kodein: Kodein) {
|
|
||||||
val simpleJwt by kodein.instance<SimpleJWT>()
|
|
||||||
val userService by kodein.instance<UserService>()
|
|
||||||
|
|
||||||
data class TokenResponse(val token: String)
|
|
||||||
|
|
||||||
post("/user/login") {
|
|
||||||
val credential = call.receive<UsernamePasswordCredential>()
|
|
||||||
|
|
||||||
val (email, password) = userService.getEmailAndPasswordFromUsername(credential.username)
|
|
||||||
?: return@post call.respond(HttpStatusCode.Unauthorized)
|
|
||||||
|
|
||||||
if (!BCrypt.checkpw(credential.password, password)) {
|
|
||||||
return@post call.respond(HttpStatusCode.Unauthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
return@post call.respond(TokenResponse(simpleJwt.sign(email)))
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticate {
|
|
||||||
get("/user/me") {
|
|
||||||
// retrieve email from token
|
|
||||||
val email = call.principal<UserIdPrincipal>()!!.name
|
|
||||||
val info = userService.getUserInfo(email)
|
|
||||||
if (info != null) call.respond(mapOf("user" to info))
|
|
||||||
else call.respondStatus(HttpStatusCode.Unauthorized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -5,7 +5,7 @@ import org.kodein.di.Kodein
|
|||||||
|
|
||||||
fun Routing.registerRoutes(kodein: Kodein) {
|
fun Routing.registerRoutes(kodein: Kodein) {
|
||||||
user(kodein)
|
user(kodein)
|
||||||
login(kodein)
|
auth(kodein)
|
||||||
notes(kodein)
|
notes(kodein)
|
||||||
title(kodein)
|
title(kodein)
|
||||||
tags(kodein)
|
tags(kodein)
|
||||||
|
|||||||
@ -34,7 +34,6 @@ fun Routing.user(kodein: Kodein) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
authenticate {
|
authenticate {
|
||||||
|
|
||||||
put {
|
put {
|
||||||
val user = call.receive<UserDto>()
|
val user = call.receive<UserDto>()
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,13 @@ class UserService(override val kodein: Kodein) : KodeinAware {
|
|||||||
return db.from(Users)
|
return db.from(Users)
|
||||||
.select(Users.id)
|
.select(Users.id)
|
||||||
.where { (Users.username eq username) or (Users.email eq email) }
|
.where { (Users.username eq username) or (Users.email eq email) }
|
||||||
.limit(0, 1)
|
.firstOrNull() != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun userExists(userId: Int): Boolean {
|
||||||
|
return db.from(Users)
|
||||||
|
.select(Users.id)
|
||||||
|
.where { Users.id eq userId }
|
||||||
.firstOrNull() != null
|
.firstOrNull() != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user