Move all file in fr.dcproject.

This commit is contained in:
2021-02-11 01:37:29 +01:00
parent c85401aa86
commit 066b01e86f
148 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
package fr.dcproject.component.auth
import fr.dcproject.component.citizen.CitizenRepository
import io.ktor.application.ApplicationCall
import io.ktor.auth.authentication
import io.ktor.util.AttributeKey
import io.ktor.util.pipeline.PipelineContext
import org.koin.core.context.GlobalContext
import fr.dcproject.component.citizen.Citizen as CitizenEntity
class ForbiddenException(message: String) : Exception(message)
private val citizenAttributeKey = AttributeKey<CitizenEntity>("CitizenContext")
val ApplicationCall.citizen: CitizenEntity
get() = attributes.computeIfAbsent(citizenAttributeKey) {
val user = authentication.principal<UserI>() ?: throw ForbiddenException("No User Connected")
GlobalContext.get().koin.get<CitizenRepository>().findByUser(user)
?: throw ForbiddenException("Citizen not found for this user id \"${user.id}\"")
}
val ApplicationCall.citizenOrNull: CitizenEntity?
get() = authentication.principal<UserI>()?.let {
GlobalContext.get().koin.get<CitizenRepository>().findByUser(it)
}
val PipelineContext<Unit, ApplicationCall>.citizen get() = context.citizen
val PipelineContext<Unit, ApplicationCall>.citizenOrNull get() = context.citizenOrNull
val ApplicationCall.user get() = authentication.principal<User>()

View File

@@ -0,0 +1,14 @@
package fr.dcproject.component.auth
import fr.dcproject.application.Configuration
import fr.dcproject.common.email.Mailer
import org.koin.dsl.module
val authKoinModule = module {
single { UserRepository(get()) }
// Used to send a connexion link by email
single {
val config: Configuration = get()
PasswordlessAuth(get<Mailer>(), config.domain, get())
}
}

View File

@@ -0,0 +1,57 @@
package fr.dcproject.component.auth
import com.sendgrid.helpers.mail.Mail
import com.sendgrid.helpers.mail.objects.Content
import com.sendgrid.helpers.mail.objects.Email
import fr.dcproject.common.email.Mailer
import fr.dcproject.component.auth.jwt.makeToken
import fr.dcproject.component.citizen.CitizenRepository
import fr.dcproject.component.citizen.CitizenWithEmail
import fr.dcproject.component.citizen.CitizenWithUserI
import io.ktor.http.URLBuilder
/**
* Send a connexion link by email
*/
class PasswordlessAuth(
private val mailer: Mailer,
private val domain: String,
private val citizenRepo: CitizenRepository
) {
fun sendEmail(email: String, url: String) {
val citizen = citizenRepo.findByEmail(email) ?: noEmail(email)
sendEmail(citizen, url)
}
fun <C> sendEmail(citizen: C, url: String) where C : CitizenWithEmail, C : CitizenWithUserI {
mailer.sendEmail {
val token = citizen.user.makeToken()
Mail(
Email("passwordless-auth@$domain"),
"Connection",
Email(citizen.email),
Content("text/plain", generateContent(token, url))
).apply {
addContent(Content("text/html", generateHtmlContent(token, url)))
}
}
}
private fun generateHtmlContent(token: String, url: String): String? {
val urlObject = URLBuilder(url)
urlObject.parameters.append("token", token)
return "Click <a href=\"${urlObject.buildString()}\">here</a> for connect to $domain"
}
private fun generateContent(token: String, url: String): String {
val urlObject = URLBuilder(url)
urlObject.parameters.append("token", token)
return "Copy this link into your browser for connect to $domain: \n${urlObject.buildString()}"
}
class EmailNotFound(val email: String) : Exception() {
override val message: String = "No Citizen with this email : $email"
}
private fun noEmail(email: String): Nothing = throw EmailNotFound(email)
}

View File

@@ -0,0 +1,54 @@
package fr.dcproject.component.auth
import fr.dcproject.component.auth.UserI.Roles
import fr.postgresjson.entity.EntityCreatedAt
import fr.postgresjson.entity.EntityCreatedAtImp
import fr.postgresjson.entity.EntityUpdatedAt
import fr.postgresjson.entity.EntityUpdatedAtImp
import fr.postgresjson.entity.UuidEntity
import fr.postgresjson.entity.UuidEntityI
import io.ktor.auth.Principal
import org.joda.time.DateTime
import java.util.UUID
class UserForCreate(
id: UUID = UUID.randomUUID(),
username: String,
override val password: String,
blockedAt: DateTime? = null,
roles: List<Roles> = emptyList()
) : User(id, username, blockedAt, roles),
UserWithPasswordI
open class User(
id: UUID = UUID.randomUUID(),
var username: String,
var blockedAt: DateTime? = null,
var roles: List<Roles> = emptyList()
) : UserRef(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityUpdatedAt by EntityUpdatedAtImp()
interface UserWithPasswordI {
val id: UUID
val password: String
}
class UserWithPassword(
id: UUID,
override val password: String,
) : UserWithPasswordI,
UserRef(id)
open class UserRef(
id: UUID = UUID.randomUUID()
) : UserI, UuidEntity(id)
interface UserI : UuidEntityI, Principal {
enum class Roles { ROLE_USER, ROLE_ADMIN }
}
interface UserForAuthI : UserI {
var roles: List<Roles>
var blockedAt: DateTime?
}

View File

@@ -0,0 +1,41 @@
package fr.dcproject.component.auth
import fr.postgresjson.connexion.Requester
import fr.postgresjson.repository.RepositoryI
import io.ktor.auth.UserPasswordCredential
import java.util.UUID
class UserRepository(override var requester: Requester) : RepositoryI {
fun findByCredentials(credentials: UserPasswordCredential): User? {
return requester
.getFunction("check_user")
.selectOne(
"username" to credentials.name,
"password" to credentials.password
)
}
fun findById(id: UUID): User {
return requester
.getFunction("find_user_by_id")
.selectOne(
"id" to id
) ?: throw UserNotFound(id)
}
fun insert(user: User): User? {
return requester
.getFunction("insert_user")
.selectOne("resource" to user)
}
fun changePassword(user: UserWithPassword) {
requester
.getFunction("change_user_password")
.sendQuery("resource" to user)
}
class UserNotFound(override val message: String?, override val cause: Throwable?) : Throwable(message, cause) {
constructor(id: UUID) : this("No User with ID $id", null)
}
}

View File

@@ -0,0 +1,14 @@
package fr.dcproject.component.auth.jwt
import com.auth0.jwt.JWT
import fr.dcproject.component.auth.UserI
/**
* Produce a token for this combination of User and Account
*/
fun UserI.makeToken(): String = JWT.create()
.withSubject("Authentication")
.withIssuer(JwtConfig.issuer)
.withClaim("id", id.toString())
.withExpiresAt(JwtConfig.getExpiration())
.sign(JwtConfig.algorithm)

View File

@@ -0,0 +1,25 @@
package fr.dcproject.component.auth.jwt
import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm
import java.util.Date
object JwtConfig {
private const val secret = "zAP5MBA4B4Ijz0MZaS48"
const val issuer = "dc-project.fr"
private const val validityInMs = 3_600_000 * 10 // 10 hours
// TODO change to RSA512
val algorithm: Algorithm = Algorithm.HMAC512(secret)
val verifier: JWTVerifier = JWT
.require(algorithm)
.withIssuer(issuer)
.build()
/**
* Calculate the expiration Date based on current time + the given validity
*/
fun getExpiration() = Date(System.currentTimeMillis() + validityInMs)
}

View File

@@ -0,0 +1,43 @@
package fr.dcproject.component.auth.jwt
import fr.dcproject.component.auth.User
import fr.dcproject.component.auth.UserRepository
import io.ktor.application.ApplicationCall
import io.ktor.auth.Authentication
import io.ktor.auth.jwt.jwt
import io.ktor.http.auth.HttpAuthHeader
import io.ktor.routing.Routing
import java.util.UUID
fun jwtInstallation(userRepo: UserRepository): Authentication.Configuration.() -> Unit = {
/**
* Setup the JWT authentication to be used in [Routing].
* If the token is valid, the corresponding [User] is fetched from the database.
* The [User] can then be accessed in each [ApplicationCall].
*/
jwt {
verifier(JwtConfig.verifier)
realm = "dc-project.fr"
validate {
it.payload.getClaim("id").asString()?.let { id ->
userRepo.findById(UUID.fromString(id))
}
}
}
/* Token in URL */
jwt("url") {
verifier(JwtConfig.verifier)
realm = "dc-project.fr"
authHeader { call ->
call.request.queryParameters["token"]?.let {
HttpAuthHeader.Single("Bearer", it)
}
}
validate {
it.payload.getClaim("id").asString()?.let { id ->
userRepo.findById(UUID.fromString(id))
}
}
}
}

View File

@@ -0,0 +1,43 @@
package fr.dcproject.component.auth.routes
import com.fasterxml.jackson.databind.exc.MismatchedInputException
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.UserRepository
import fr.dcproject.component.auth.jwt.makeToken
import fr.dcproject.component.auth.routes.Login.LoginRequest.Input
import io.ktor.application.call
import io.ktor.auth.UserPasswordCredential
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.Route
@KtorExperimentalLocationsAPI
object Login {
@Location("/login")
class LoginRequest {
data class Input(
val username: String,
val password: String,
)
}
fun Route.authLogin(userRepo: UserRepository) {
post<LoginRequest> {
try {
val credentials = call.receiveOrBadRequest<Input>().run {
UserPasswordCredential(username, password)
}
userRepo.findByCredentials(credentials)?.let { user ->
call.respondText(user.makeToken())
} ?: call.respond(HttpStatusCode.BadRequest, "Username not exist or password is wrong")
} catch (e: MismatchedInputException) {
call.respond(HttpStatusCode.BadRequest, "You must be send name and password to the request")
}
}
}
}

View File

@@ -0,0 +1,72 @@
package fr.dcproject.component.auth.routes
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.User
import fr.dcproject.component.auth.UserForCreate
import fr.dcproject.component.auth.UserI
import fr.dcproject.component.auth.jwt.makeToken
import fr.dcproject.component.auth.routes.Register.RegisterRequest.Input
import fr.dcproject.component.citizen.CitizenForCreate
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.citizen.CitizenRepository
import io.ktor.application.call
import io.ktor.features.BadRequestException
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.Route
import org.joda.time.DateTime
@KtorExperimentalLocationsAPI
object Register {
@Location("/register")
class RegisterRequest {
data class Input(
val name: Name,
val email: String,
val birthday: DateTime,
val voteAnonymous: Boolean = true,
val followAnonymous: Boolean = true,
val user: User
) {
data class Name(
val firstName: String,
val lastName: String,
val civility: String? = null
)
data class User(
val username: String,
val password: String
)
}
}
fun Route.authRegister(citizenRepo: CitizenRepository) {
fun Input.toCitizen(): CitizenForCreate = CitizenForCreate(
name = CitizenI.Name(name.firstName, name.lastName, name.civility),
birthday = birthday,
email = email,
followAnonymous = followAnonymous,
voteAnonymous = voteAnonymous,
user = UserForCreate(
username = user.username,
password = user.password,
roles = listOf(UserI.Roles.ROLE_USER)
)
)
post<RegisterRequest> {
try {
val citizen = call.receiveOrBadRequest<Input>().toCitizen()
val createdCitizen = citizenRepo.insertWithUser(citizen)?.user ?: throw BadRequestException("Bad request")
call.respondText(createdCitizen.makeToken())
} catch (e: MissingKotlinParameterException) {
call.respond(HttpStatusCode.BadRequest)
}
}
}
}

View File

@@ -0,0 +1,36 @@
package fr.dcproject.component.auth.routes
import fr.dcproject.common.utils.receiveOrBadRequest
import fr.dcproject.component.auth.PasswordlessAuth
import fr.dcproject.component.auth.routes.Sso.PasswordlessRequest.Input
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.response.respond
import io.ktor.routing.Route
@KtorExperimentalLocationsAPI
object Sso {
@Location("/auth/passwordless")
class PasswordlessRequest {
data class Input(val email: String, val url: String)
}
/**
* Send an email to the citizen with a link to automatically connect
*/
fun Route.authPasswordless(passwordlessAuth: PasswordlessAuth) {
post<PasswordlessRequest> {
call.receiveOrBadRequest<Input>().run {
try {
passwordlessAuth.sendEmail(email, url)
} catch (e: PasswordlessAuth.EmailNotFound) {
call.respond(HttpStatusCode.NotFound)
}
call.respond(HttpStatusCode.NoContent)
}
}
}
}

View File

@@ -0,0 +1,18 @@
package fr.dcproject.component.auth.routes
import fr.dcproject.component.auth.routes.Login.authLogin
import fr.dcproject.component.auth.routes.Register.authRegister
import fr.dcproject.component.auth.routes.Sso.authPasswordless
import io.ktor.auth.authenticate
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Routing
import org.koin.ktor.ext.get
@KtorExperimentalLocationsAPI
fun Routing.installAuthRoutes() {
authenticate(optional = true) {
authLogin(get())
authRegister(get())
authPasswordless(get())
}
}