Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement some login features #137

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/me/kavin/piped/ServerLauncher.java
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,8 @@ AsyncServlet mainServlet(Executor executor) {
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());
}
Expand Down
105 changes: 100 additions & 5 deletions src/main/java/me/kavin/piped/utils/ResponseHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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;
Expand Down Expand Up @@ -69,6 +70,12 @@
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;
Expand Down Expand Up @@ -97,6 +104,7 @@
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;
Expand Down Expand Up @@ -597,7 +605,11 @@ public static final byte[] registerResponse(String user, String pass) throws IOE

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)
Expand All @@ -616,14 +628,20 @@ public static final byte[] loginResponse(String user, String pass)
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();
Expand All @@ -632,6 +650,83 @@ public static final byte[] loginResponse(String user, String pass)

}

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 {

Expand Down
11 changes: 11 additions & 0 deletions src/main/java/me/kavin/piped/utils/obj/db/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -86,6 +89,14 @@ public void setPassword(String password) {
this.password = password;
}

public String getTotp() {
return totp;
}

public void setTotp(String totp) {
this.totp = totp;
}

public List<String> getSubscribed() {
return subscribed_ids;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

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.";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.kavin.piped.utils.resp;

public class InvalidOldPasswordResponse {

public String error = "The old password you provided is incorrect.";

}
2 changes: 1 addition & 1 deletion src/main/java/me/kavin/piped/utils/resp/LoginRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

public class LoginRequest {

public String username, password;
public String username, password, totp;

}