mirror of
https://github.com/TeamPiped/Piped-Backend.git
synced 2024-08-14 23:51:41 +00:00
Implement some login features.
See https://github.com/TeamPiped/Piped/issues/349
This commit is contained in:
parent
dc009f38fc
commit
3b4d02a7c1
7 changed files with 123 additions and 8 deletions
|
@ -36,6 +36,7 @@ dependencies {
|
||||||
implementation 'org.hibernate:hibernate-hikaricp:5.6.1.Final'
|
implementation 'org.hibernate:hibernate-hikaricp:5.6.1.Final'
|
||||||
implementation 'com.zaxxer:HikariCP:5.0.0'
|
implementation 'com.zaxxer:HikariCP:5.0.0'
|
||||||
implementation 'org.springframework.security:spring-security-crypto:5.6.0'
|
implementation 'org.springframework.security:spring-security-crypto:5.6.0'
|
||||||
|
implementation 'dev.samstevens.totp:totp:1.7.1'
|
||||||
implementation 'commons-logging:commons-logging:1.2'
|
implementation 'commons-logging:commons-logging:1.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -212,7 +212,8 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
||||||
try {
|
try {
|
||||||
LoginRequest body = Constants.mapper.readValue(request.loadBody().getResult().asArray(),
|
LoginRequest body = Constants.mapper.readValue(request.loadBody().getResult().asArray(),
|
||||||
LoginRequest.class);
|
LoginRequest.class);
|
||||||
return getJsonResponse(ResponseHelper.loginResponse(body.username, body.password), "private");
|
return getJsonResponse(ResponseHelper.loginResponse(body.username, body.password, body.totp),
|
||||||
|
"private");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return getErrorResponse(e, request.getPath());
|
return getErrorResponse(e, request.getPath());
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
@ -69,6 +70,12 @@ import com.rometools.rome.feed.synd.SyndPersonImpl;
|
||||||
import com.rometools.rome.io.FeedException;
|
import com.rometools.rome.io.FeedException;
|
||||||
import com.rometools.rome.io.SyndFeedOutput;
|
import com.rometools.rome.io.SyndFeedOutput;
|
||||||
|
|
||||||
|
import dev.samstevens.totp.code.CodeGenerator;
|
||||||
|
import dev.samstevens.totp.code.CodeVerifier;
|
||||||
|
import dev.samstevens.totp.code.DefaultCodeGenerator;
|
||||||
|
import dev.samstevens.totp.code.DefaultCodeVerifier;
|
||||||
|
import dev.samstevens.totp.time.SystemTimeProvider;
|
||||||
|
import dev.samstevens.totp.time.TimeProvider;
|
||||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||||
import me.kavin.piped.consts.Constants;
|
import me.kavin.piped.consts.Constants;
|
||||||
|
@ -97,6 +104,7 @@ import me.kavin.piped.utils.resp.AuthenticationFailureResponse;
|
||||||
import me.kavin.piped.utils.resp.CompromisedPasswordResponse;
|
import me.kavin.piped.utils.resp.CompromisedPasswordResponse;
|
||||||
import me.kavin.piped.utils.resp.DisabledRegistrationResponse;
|
import me.kavin.piped.utils.resp.DisabledRegistrationResponse;
|
||||||
import me.kavin.piped.utils.resp.IncorrectCredentialsResponse;
|
import me.kavin.piped.utils.resp.IncorrectCredentialsResponse;
|
||||||
|
import me.kavin.piped.utils.resp.InvalidOldPasswordResponse;
|
||||||
import me.kavin.piped.utils.resp.InvalidRequestResponse;
|
import me.kavin.piped.utils.resp.InvalidRequestResponse;
|
||||||
import me.kavin.piped.utils.resp.LoginResponse;
|
import me.kavin.piped.utils.resp.LoginResponse;
|
||||||
import me.kavin.piped.utils.resp.SubscribeStatusResponse;
|
import me.kavin.piped.utils.resp.SubscribeStatusResponse;
|
||||||
|
@ -597,7 +605,11 @@ public class ResponseHelper {
|
||||||
|
|
||||||
private static final BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder();
|
private static final BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder();
|
||||||
|
|
||||||
public static final byte[] loginResponse(String user, String pass)
|
private static final TimeProvider timeProvider = new SystemTimeProvider();
|
||||||
|
private static final CodeGenerator codeGenerator = new DefaultCodeGenerator();
|
||||||
|
private static final CodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
|
||||||
|
|
||||||
|
public static final byte[] loginResponse(String user, String pass, String totp)
|
||||||
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
|
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
|
||||||
|
|
||||||
if (user == null || pass == null)
|
if (user == null || pass == null)
|
||||||
|
@ -616,14 +628,20 @@ public class ResponseHelper {
|
||||||
if (dbuser != null) {
|
if (dbuser != null) {
|
||||||
String hash = dbuser.getPassword();
|
String hash = dbuser.getPassword();
|
||||||
if (hash.startsWith("$argon2")) {
|
if (hash.startsWith("$argon2")) {
|
||||||
if (argon2PasswordEncoder.matches(pass, hash)) {
|
if (!argon2PasswordEncoder.matches(pass, hash)) {
|
||||||
s.close();
|
s.close();
|
||||||
return Constants.mapper.writeValueAsBytes(new LoginResponse(dbuser.getSessionId()));
|
return Constants.mapper.writeValueAsBytes(new IncorrectCredentialsResponse());
|
||||||
}
|
}
|
||||||
} else if (bcryptPasswordEncoder.matches(pass, hash)) {
|
} else if (!bcryptPasswordEncoder.matches(pass, hash)) {
|
||||||
s.close();
|
s.close();
|
||||||
return Constants.mapper.writeValueAsBytes(new LoginResponse(dbuser.getSessionId()));
|
return Constants.mapper.writeValueAsBytes(new IncorrectCredentialsResponse());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String totpSecret = dbuser.getTotp();
|
||||||
|
if (totpSecret != null && !verifier.isValidCode(totpSecret, totp))
|
||||||
|
return Constants.mapper.writeValueAsBytes(new IncorrectCredentialsResponse());
|
||||||
|
|
||||||
|
return Constants.mapper.writeValueAsBytes(new LoginResponse(dbuser.getSessionId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
s.close();
|
s.close();
|
||||||
|
@ -632,6 +650,83 @@ public class ResponseHelper {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final byte[] changePasswordResponse(String session, String oldpass, String newpass)
|
||||||
|
throws IOException, InterruptedException, URISyntaxException {
|
||||||
|
|
||||||
|
if (oldpass == null || newpass == null)
|
||||||
|
return Constants.mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||||
|
|
||||||
|
Session s = DatabaseSessionFactory.createSession();
|
||||||
|
|
||||||
|
User user = DatabaseHelper.getUserFromSession(s, session);
|
||||||
|
|
||||||
|
if (user != null) {
|
||||||
|
String hash = user.getPassword();
|
||||||
|
if (hash.startsWith("$argon2")) {
|
||||||
|
if (!argon2PasswordEncoder.matches(oldpass, hash)) {
|
||||||
|
s.close();
|
||||||
|
return Constants.mapper.writeValueAsBytes(new InvalidOldPasswordResponse());
|
||||||
|
}
|
||||||
|
} else if (!bcryptPasswordEncoder.matches(oldpass, hash)) {
|
||||||
|
s.close();
|
||||||
|
return Constants.mapper.writeValueAsBytes(new InvalidOldPasswordResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Constants.COMPROMISED_PASSWORD_CHECK) {
|
||||||
|
String sha1Hash = DigestUtils.sha1Hex(newpass).toUpperCase();
|
||||||
|
String prefix = sha1Hash.substring(0, 5);
|
||||||
|
String suffix = sha1Hash.substring(5);
|
||||||
|
String[] entries = RequestUtils
|
||||||
|
.sendGet("https://api.pwnedpasswords.com/range/" + prefix, "github.com/TeamPiped/Piped-Backend")
|
||||||
|
.split("\n");
|
||||||
|
for (String entry : entries)
|
||||||
|
if (StringUtils.substringBefore(entry, ":").equals(suffix))
|
||||||
|
return Constants.mapper.writeValueAsBytes(new CompromisedPasswordResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
user.setPassword(argon2PasswordEncoder.encode(newpass));
|
||||||
|
s.saveOrUpdate(user);
|
||||||
|
s.getTransaction().begin();
|
||||||
|
s.getTransaction().commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
s.close();
|
||||||
|
|
||||||
|
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final byte[] authValidResponse(String session) throws JsonProcessingException {
|
||||||
|
Session s = DatabaseSessionFactory.createSession();
|
||||||
|
|
||||||
|
if (((Long) s.createQuery("SELECT COUNT(user) from User user where user.sessionId = :sessionId")
|
||||||
|
.setParameter("sessionId", session).uniqueResult()).intValue() > 0) {
|
||||||
|
s.close();
|
||||||
|
return Constants.mapper.writeValueAsBytes(new AcceptedResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
s.close();
|
||||||
|
|
||||||
|
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final byte[] logoutResponse(String session) throws JsonProcessingException {
|
||||||
|
Session s = DatabaseSessionFactory.createSession();
|
||||||
|
|
||||||
|
s.getTransaction().begin();
|
||||||
|
|
||||||
|
if (s.createQuery("UPDATE User user SET user.sessionId = :newSessionId where user.sessionId = :sessionId")
|
||||||
|
.setParameter("sessionId", session).setParameter("newSessionId", String.valueOf(UUID.randomUUID()))
|
||||||
|
.executeUpdate() > 0) {
|
||||||
|
s.getTransaction().commit();
|
||||||
|
s.close();
|
||||||
|
return Constants.mapper.writeValueAsBytes(new AcceptedResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
s.close();
|
||||||
|
|
||||||
|
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||||
|
}
|
||||||
|
|
||||||
public static final byte[] subscribeResponse(String session, String channelId)
|
public static final byte[] subscribeResponse(String session, String channelId)
|
||||||
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
|
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,9 @@ public class User implements Serializable {
|
||||||
@Column(name = "session_id", length = 36)
|
@Column(name = "session_id", length = 36)
|
||||||
private String sessionId;
|
private String sessionId;
|
||||||
|
|
||||||
|
@Column(name = "totp_token", length = 32, insertable = false)
|
||||||
|
private String totp;
|
||||||
|
|
||||||
@ElementCollection
|
@ElementCollection
|
||||||
@CollectionTable(name = "users_subscribed", joinColumns = @JoinColumn(name = "subscriber"), indexes = {
|
@CollectionTable(name = "users_subscribed", joinColumns = @JoinColumn(name = "subscriber"), indexes = {
|
||||||
@Index(columnList = "subscriber", name = "users_subscribed_subscriber_idx"),
|
@Index(columnList = "subscriber", name = "users_subscribed_subscriber_idx"),
|
||||||
|
@ -86,6 +89,14 @@ public class User implements Serializable {
|
||||||
this.password = password;
|
this.password = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getTotp() {
|
||||||
|
return totp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotp(String totp) {
|
||||||
|
this.totp = totp;
|
||||||
|
}
|
||||||
|
|
||||||
public List<String> getSubscribed() {
|
public List<String> getSubscribed() {
|
||||||
return subscribed_ids;
|
return subscribed_ids;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,6 @@ package me.kavin.piped.utils.resp;
|
||||||
|
|
||||||
public class IncorrectCredentialsResponse {
|
public class IncorrectCredentialsResponse {
|
||||||
|
|
||||||
public String error = "The username or password you have entered is incorrect.";
|
public String error = "Invalid credentials. Re-check your username, password and totp.";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package me.kavin.piped.utils.resp;
|
||||||
|
|
||||||
|
public class InvalidOldPasswordResponse {
|
||||||
|
|
||||||
|
public String error = "The old password you provided is incorrect.";
|
||||||
|
|
||||||
|
}
|
|
@ -2,6 +2,6 @@ package me.kavin.piped.utils.resp;
|
||||||
|
|
||||||
public class LoginRequest {
|
public class LoginRequest {
|
||||||
|
|
||||||
public String username, password;
|
public String username, password, totp;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue