email/pass login with sqlite db

This commit is contained in:
Claude Brisson
2024-02-19 23:32:55 +01:00
parent 2f79f224a2
commit 999221de9d
10 changed files with 125 additions and 10 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ target
/tournamentfiles /tournamentfiles
*.iml *.iml
*~ *~
pairgoth.db

9
create-user.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
read -rp 'email: ' EMAIL
read -rp 'password: ' PASSWORD
ENCPASS=$(echo -n $PASSWORD | sha256sum)
ENCPASS=${ENCPASS% -}
sqlite3 pairgoth.db "INSERT INTO cred (email, password) VALUES ('$EMAIL', '$ENCPASS')"

View File

@@ -286,6 +286,12 @@
<artifactId>lucene-queryparser</artifactId> <artifactId>lucene-queryparser</artifactId>
<version>${lucene.version}</version> <version>${lucene.version}</version>
</dependency> </dependency>
<!-- sqlite -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.45.1.0</version>
</dependency>
<!-- tests --> <!-- tests -->
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>

View File

@@ -0,0 +1,35 @@
package org.jeudego.pairgoth.util
import com.republicate.kson.Json
import java.io.File
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.sql.DriverManager
object CredentialsChecker {
private const val CREDENTIALS_DB = "pairgoth.db"
private val hasher = MessageDigest.getInstance("SHA-256")
@OptIn(ExperimentalStdlibApi::class)
fun check(email: String, password: String): String? {
initDatabase()
val sha256 = hasher.digest(password.toByteArray(StandardCharsets.UTF_8)).toHexString()
DriverManager.getConnection("jdbc:sqlite:$CREDENTIALS_DB").use { conn ->
val rs =
conn.prepareStatement("SELECT 1 FROM cred WHERE email = ? AND password = ?").apply {
setString(1, email)
setString(2, password)
}.executeQuery()
return if (rs.next()) email else null
}
}
@Synchronized
fun initDatabase() {
if (!File(CREDENTIALS_DB).exists()) {
DriverManager.getConnection("jdbc:sqlite:$CREDENTIALS_DB").use { conn ->
conn.createStatement().executeUpdate("CREATE TABLE cred (email VARCHAR(200) UNIQUE NOT NULL, password VARCHAR(200) NOT NULL)")
}
}
}
}

View File

@@ -1,6 +1,7 @@
package org.jeudego.pairgoth.web package org.jeudego.pairgoth.web
import com.republicate.kson.Json import com.republicate.kson.Json
import org.jeudego.pairgoth.util.CredentialsChecker
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import javax.servlet.http.HttpServlet import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
@@ -17,7 +18,7 @@ class LoginServlet: HttpServlet() {
val payload = Json.Companion.parse(req.reader.readText())?.asObject() ?: throw Error("null json") val payload = Json.Companion.parse(req.reader.readText())?.asObject() ?: throw Error("null json")
val user = when (WebappManager.getProperty("auth")) { val user = when (WebappManager.getProperty("auth")) {
"sesame" -> checkSesame(payload) "sesame" -> checkSesame(payload)
else -> null else -> checkLoginPass(payload)
} ?: throw Error("authentication failed") } ?: throw Error("authentication failed")
req.session.setAttribute("logged", user) req.session.setAttribute("logged", user)
val ret = Json.Object("status" to "ok") val ret = Json.Object("status" to "ok")
@@ -25,7 +26,9 @@ class LoginServlet: HttpServlet() {
resp.writer.println(ret.toString()) resp.writer.println(ret.toString())
} catch (t: Throwable) { } catch (t: Throwable) {
logger.error("exception while logging in", t) logger.error("exception while logging in", t)
resp.sendError(HttpServletResponse.SC_BAD_REQUEST) resp.contentType = "application/json"
resp.status = HttpServletResponse.SC_BAD_REQUEST
resp.writer.println(errorJson)
} }
} }
@@ -34,8 +37,15 @@ class LoginServlet: HttpServlet() {
return if (payload.getString("sesame")?.equals(expected) == true) true else null return if (payload.getString("sesame")?.equals(expected) == true) true else null
} }
fun checkLoginPass(payload: Json.Object): String? {
return CredentialsChecker.check(
payload.getString("email") ?: throw Error("Missing login field"),
payload.getString("password") ?: throw Error("missing password field"))
}
companion object { companion object {
fun isJson(mimeType: String) = "text/json" == mimeType || "application/json" == mimeType || mimeType.endsWith("+json") fun isJson(mimeType: String) = "text/json" == mimeType || "application/json" == mimeType || mimeType.endsWith("+json")
val logger = LoggerFactory.getLogger("login") val logger = LoggerFactory.getLogger("login")
private val errorJson = "{ \"status\": \"error\", \"error\": \"authentication failed\"}"
} }
} }

View File

@@ -215,6 +215,10 @@
width: initial; width: initial;
} }
.ui.form .centered.inline.fields {
justify-content: center;
}
.ui.accordion .content { .ui.accordion .content {
display: block; display: block;
max-height: 0; max-height: 0;

View File

@@ -162,3 +162,11 @@ white blanc
White Blanc White Blanc
white vs. black blanc vs. Noir white vs. black blanc vs. Noir
confirmed. confirmé(s). confirmed. confirmé(s).
Note that login to this instance is reserved to French federation actors plus several external people at our discretion. Send us La connexion à cette instance est réservée aux acteurs de la FFG et à quelques personnes extérieures, à notre discrétion. Envoyez-nous
an email un email
to request an access. pour demander un accès.
(not yet available) (pas encore disponible)
Log in using Se connecter avec
(reserved to FFG actors) (réservé aux acteurs FFG)
Log in using an email Se connecter avec un email
password mot de passe

View File

@@ -8,6 +8,7 @@
<ol> <ol>
<li> <li>
<p><b>Stay in the browser</b>: If you prefer convenience, you can simply use the <span class="logo">pairgoth</span> instance graciously hosted by the French Go Federation.</p> <p><b>Stay in the browser</b>: If you prefer convenience, you can simply use the <span class="logo">pairgoth</span> instance graciously hosted by the French Go Federation.</p>
<p>Note that login to this instance is reserved to French federation actors plus several external people at our discretion. Send us <a href="mailto:pairgothjeudego.org">an email</a> to request an access.</p>
<blockquote> <blockquote>
<a class="nobreak" href="/login">Launch <span class="logo">pairgoth</span></a> <a class="nobreak" href="/login">Launch <span class="logo">pairgoth</span></a>
</blockquote> </blockquote>

View File

@@ -114,7 +114,7 @@ HTMLFormElement.prototype.val = function(name, value) {
let tag = ctl.tagName; let tag = ctl.tagName;
let type = tag === 'INPUT' ? ctl.attr('type') : undefined; let type = tag === 'INPUT' ? ctl.attr('type') : undefined;
if ( if (
(tag === 'INPUT' && ['text', 'number', 'hidden'].includes(ctl.attr('type'))) || (tag === 'INPUT' && ['text', 'number', 'hidden', 'password'].includes(ctl.attr('type'))) ||
tag === 'SELECT' tag === 'SELECT'
) { ) {
if (hasValue) { if (hasValue) {

View File

@@ -30,30 +30,71 @@
#elseif($auth == 'oauth') #elseif($auth == 'oauth')
<div id="login" class="section"> <div id="login" class="section">
<div>Log in using</div> <div id="oauth-buttons" class="roundbox">
<div id="oauth-buttons">
#foreach($provider in $oauthProviders) #foreach($provider in $oauthProviders)
<div> <form>
<button id="login-$provider" class="ui floating basic button">$provider</button> <label>Log in using</label>
</div> <button id="login-$provider" type="button" class="ui green floating button">$provider</button>
#if($provider == 'ffg')
(reserved to FFG actors)
#end #end
</form>
#end
</div>
<div class="roundbox">
Log in using an email
<form id="login-form" class="ui form">
<div class="centered inline fields">
<div class="field">
<input name="email" type="text" placeholder="email"/>
</div>
<div class="field">
<input name="password" type="password" placeholder="password"/>
</div>
<button id="login-email" type="button" class="ui green floating button">Log in</button>
</div>
</form>
</div> </div>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
async function digestMessage(message) {
const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); // hash the message
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string
return hashHex;
}
onLoad(()=> { onLoad(()=> {
#foreach($provider in $oauthProviders) #foreach($provider in $oauthProviders)
let buttonId = '#login-$provider'; let buttonId = '#login-$provider';
let loginURL= '$application.getAttribute("${provider}Provider").getLoginURL($session.id)'; let loginURL= '$application.getAttribute("${provider}Provider").getLoginURL($session.id)';
// #[[ // #[[
console.log(`buttonId = ${buttonId}`);
console.log(`loginURL = ${loginURL}`);
$(buttonId).on('click', e => { $(buttonId).on('click', e => {
document.location.href = loginURL; document.location.href = loginURL;
}); });
// ]]# // ]]#
#end #end
// #[[
$('#login-email').on('click', e => {
let form = $('#login-form')[0]
let password = form.val('password');
digestMessage(password).then(enc => {
let payload = {
'email': form.val('email'),
'password': enc
}
api.postJson('login', payload)
.then(resp => {
if (resp !== 'error' && resp.status === 'ok') {
document.location.href = '/index'
}
});
});
});
// ]]#
}); });
</script> </script>