Implement some login features.

See https://github.com/TeamPiped/Piped/issues/349
This commit is contained in:
FireMaskterK 2021-12-03 23:38:24 +00:00
parent dc009f38fc
commit 3b4d02a7c1
No known key found for this signature in database
GPG key ID: 49451E4482CC5BCD
7 changed files with 123 additions and 8 deletions

View file

@ -36,6 +36,7 @@ dependencies {
implementation 'org.hibernate:hibernate-hikaricp:5.6.1.Final'
implementation 'com.zaxxer:HikariCP:5.0.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'
}

View file

@ -212,7 +212,8 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
try {
LoginRequest body = Constants.mapper.readValue(request.loadBody().getResult().asArray(),
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) {
return getErrorResponse(e, request.getPath());
}

View file

@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
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.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.ObjectArrayList;
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.DisabledRegistrationResponse;
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.LoginResponse;
import me.kavin.piped.utils.resp.SubscribeStatusResponse;
@ -597,7 +605,11 @@ public class ResponseHelper {
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 {
if (user == null || pass == null)
@ -616,14 +628,20 @@ public class ResponseHelper {
if (dbuser != null) {
String hash = dbuser.getPassword();
if (hash.startsWith("$argon2")) {
if (argon2PasswordEncoder.matches(pass, hash)) {
if (!argon2PasswordEncoder.matches(pass, hash)) {
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();
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();
@ -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)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {

View file

@ -37,6 +37,9 @@ public class User implements Serializable {
@Column(name = "session_id", length = 36)
private String sessionId;
@Column(name = "totp_token", length = 32, insertable = false)
private String totp;
@ElementCollection
@CollectionTable(name = "users_subscribed", joinColumns = @JoinColumn(name = "subscriber"), indexes = {
@Index(columnList = "subscriber", name = "users_subscribed_subscriber_idx"),
@ -86,6 +89,14 @@ public class User implements Serializable {
this.password = password;
}
public String getTotp() {
return totp;
}
public void setTotp(String totp) {
this.totp = totp;
}
public List<String> getSubscribed() {
return subscribed_ids;
}

View file

@ -2,6 +2,6 @@ package me.kavin.piped.utils.resp;
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.";
}

View file

@ -0,0 +1,7 @@
package me.kavin.piped.utils.resp;
public class InvalidOldPasswordResponse {
public String error = "The old password you provided is incorrect.";
}

View file

@ -2,6 +2,6 @@ package me.kavin.piped.utils.resp;
public class LoginRequest {
public String username, password;
public String username, password, totp;
}