Implement account deletion and cleanup some code

This commit is contained in:
Jeidnx 2023-10-25 10:03:15 +02:00
parent 9b7246a029
commit e7f2187b47
No known key found for this signature in database
GPG Key ID: 0E9E697B7E99DF39
4 changed files with 170 additions and 40 deletions

View File

@ -18,7 +18,7 @@ dependencies {
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
implementation 'com.github.FireMasterK.NewPipeExtractor:NewPipeExtractor:48beff184a9792c4787cfa05fce577c3adf89f56'
implementation 'com.github.FireMasterK:nanojson:9f4af3b739cc13f3d0d9d4b758bbe2b2ae7119d7'
implementation 'com.nimbusds:oauth2-oidc-sdk:11.5.0'
implementation 'com.nimbusds:oauth2-oidc-sdk:11.5'
implementation 'com.fasterxml.jackson.core:jackson-core:2.15.2'
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.15.2'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'

View File

@ -2,10 +2,10 @@ package me.kavin.piped.server;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
import com.nimbusds.oauth2.sdk.id.Identifier;
import com.nimbusds.oauth2.sdk.id.State;
import com.nimbusds.openid.connect.sdk.*;
import com.nimbusds.openid.connect.sdk.claims.UserInfo;
@ -28,6 +28,7 @@ import me.kavin.piped.server.handlers.auth.UserHandlers;
import me.kavin.piped.utils.ErrorResponse;
import me.kavin.piped.utils.*;
import me.kavin.piped.utils.obj.MatrixHelper;
import me.kavin.piped.utils.obj.OidcData;
import me.kavin.piped.utils.obj.OidcProvider;
import me.kavin.piped.utils.obj.federation.FederatedVideoInfo;
import me.kavin.piped.utils.resp.*;
@ -43,6 +44,8 @@ import org.xml.sax.InputSource;
import java.io.ByteArrayInputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
@ -58,6 +61,7 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
private static final HttpHeader FILE_NAME = HttpHeaders.of("x-file-name");
private static final HttpHeader LAST_ETAG = HttpHeaders.of("x-last-etag");
private static final Map<String, OidcData> PENDING_OIDC = new HashMap<>();
@Provides
Executor executor() {
@ -285,7 +289,7 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
String function = request.getPathParameter("function");
OidcProvider provider = getOidcProvider(request.getPathParameter("provider"));
if (provider == null)
return HttpResponse.ofCode(500).withHtml("Can't find the provider on the server.");
return HttpResponse.ofCode(500).withHtml("Can't find the provider on the server");
URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback");
@ -294,62 +298,62 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
String redirectUri = request.getQueryParameter("redirect");
if (StringUtils.isBlank(redirectUri)) {
return HttpResponse.ofCode(400).withHtml("Missing redirect parameter");
return HttpResponse.ofCode(400).withHtml("redirect is a required parameter");
}
State state = new State(new Identifier(24) + "." + redirectUri);
Nonce nonce = new Nonce();
OidcData data = new OidcData(redirectUri);
String state = data.getState();
PENDING_OIDC.put(state, data);
AuthenticationRequest oidcRequest = new AuthenticationRequest.Builder(
new ResponseType("code"),
new Scope("openid"),
provider.clientID,
callback)
.endpointURI(provider.authUri)
.state(state)
.nonce(nonce)
.build();
provider.clientID, callback).endpointURI(provider.authUri)
.state(new State(state)).nonce(data.nonce).build();
if (redirectUri.equals(Constants.FRONTEND_URL + "/login")) {
return HttpResponse.redirect302(oidcRequest.toURI().toString());
}
return HttpResponse.ok200().withHtml(
"<!DOCTYPE html><html style= \"color: white;background: #0f0f0f;\"><body>"
+ "<h3>Warning:</h3> You are trying to give <pre style=\"font-size: 1.2rem;\">"
+ redirectUri
+ "</pre> access to your Piped account. If you wish to continue click <a style=\"text-decoration: underline;color: inherit;\"href=\""
+ oidcRequest.toURI().toString()
+ "\">here</a></body></html>");
"<!DOCTYPE html><html style=\"color-scheme: dark light;\"><body>" +
"<h3>Warning:</h3> You are trying to give <pre style=\"font-size: 1.2rem;\">" +
redirectUri +
"</pre> access to your Piped account. If you wish to continue click " +
"<a style=\"text-decoration: underline;color: inherit;\"href=\"" +
oidcRequest.toURI().toString() +
"\">here</a></body></html>");
}
case "callback" -> {
ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret);
AuthenticationResponse response = AuthenticationResponseParser.parse(
URI.create(request.getFullUrl())
);
AuthenticationSuccessResponse sr = parseOidcUri(URI.create(request.getFullUrl()));
if (response instanceof AuthenticationErrorResponse) {
// The OpenID provider returned an error
System.err.println(response.toErrorResponse().getErrorObject());
return HttpResponse.ofCode(500).withHtml("OpenID provider returned an error:\n\n" + response.toErrorResponse().getErrorObject().toString());
OidcData data = PENDING_OIDC.get(sr.getState().toString());
if (data == null) {
return HttpResponse.ofCode(400).withHtml(
"Your oidc provider sent invalid state data. Try again or contact your oidc admin"
);
}
AuthenticationSuccessResponse sr = response.toSuccessResponse();
AuthorizationCode code = sr.getAuthorizationCode();
AuthorizationGrant codeGrant = new AuthorizationCodeGrant(
code, callback
);
AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback);
TokenRequest tr = new TokenRequest(provider.tokenUri, clientAuth, codeGrant);
TokenResponse tokenResponse = OIDCTokenResponseParser.parse(tr.toHTTPRequest().send());
OIDCTokenResponse tokenResponse = (OIDCTokenResponse) OIDCTokenResponseParser.parse(tr.toHTTPRequest().send());
if (!tokenResponse.indicatesSuccess()) {
TokenErrorResponse errorResponse = tokenResponse.toErrorResponse();
return HttpResponse.ofCode(500).withHtml("Failure while trying to request token:\n\n" + errorResponse.getErrorObject().getDescription());
}
OIDCTokenResponse successResponse = (OIDCTokenResponse) tokenResponse.toSuccessResponse();
OIDCTokenResponse successResponse = tokenResponse.toSuccessResponse();
if (data.isInvalidNonce((String) successResponse.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("nonce"))) {
return HttpResponse.ofCode(400).withHtml(
"Your oidc provider sent an invalid nonce. Try again or contact your oidc admin"
);
}
UserInfoRequest ur = new UserInfoRequest(provider.userinfoUri, successResponse.getOIDCTokens().getBearerAccessToken());
UserInfoResponse userInfoResponse = UserInfoResponse.parse(ur.toHTTPRequest().send());
@ -363,11 +367,57 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
UserInfo userInfo = userInfoResponse.toSuccessResponse().getUserInfo();
String sessionId = UserHandlers.oidcCallbackResponse(provider.name, userInfo.getSubject().toString());
return HttpResponse.redirect302(data.data + "?session=" + sessionId);
}
case "delete" -> {
ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret);
return HttpResponse.redirect302(sr.getState().toString().split("\\.", 2)[1] + "?session=" + sessionId);
AuthenticationSuccessResponse sr = parseOidcUri(URI.create(request.getFullUrl()));
OidcData data = UserHandlers.PENDING_OIDC.get(sr.getState().toString());
if (data == null) {
return HttpResponse.ofCode(400).withHtml(
"Your oidc provider sent invalid state data. Try again or contact your oidc admin"
);
}
long start = Long.parseLong(data.data.split("\\|")[1]);
String session = data.data.split("\\|")[0];
AuthorizationCode code = sr.getAuthorizationCode();
AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, new URI(Constants.PUBLIC_URL + request.getPath()));
TokenRequest tr = new TokenRequest(provider.tokenUri, clientAuth, codeGrant);
TokenResponse tokenResponse = OIDCTokenResponseParser.parse(tr.toHTTPRequest().send());
if (!tokenResponse.indicatesSuccess()) {
TokenErrorResponse errorResponse = tokenResponse.toErrorResponse();
return HttpResponse.ofCode(500).withHtml("Failure while trying to request token:\n\n" + errorResponse.getErrorObject().getDescription());
}
OIDCTokenResponse successResponse = (OIDCTokenResponse) tokenResponse.toSuccessResponse();
JWTClaimsSet claims = successResponse.getOIDCTokens().getIDToken().getJWTClaimsSet();
if (data.isInvalidNonce((String) claims.getClaim("nonce"))) {
return HttpResponse.ofCode(400).withHtml(
"Your oidc provider sent an invalid nonce. Please try again or contact your oidc admin."
);
}
long authTime = (long) claims.getClaim("auth_time");
if (authTime < start) {
return HttpResponse.ofCode(500).withHtml(
"Your oidc provider didn't verify your identity. Please try again or contact your oidc admin."
);
}
return HttpResponse.redirect302(Constants.FRONTEND_URL + "/preferences?deleted=" + UserHandlers.deleteOidcUserResponse(session));
}
default -> {
return HttpResponse.ofCode(500).withHtml("Invalid function `" + function + "`.");
return HttpResponse.ofCode(500).withHtml("Invalid function `" + function + "`");
}
}
@ -630,6 +680,17 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
return null;
}
private static AuthenticationSuccessResponse parseOidcUri(URI uri) throws Exception {
AuthenticationResponse response = AuthenticationResponseParser.parse(uri);
if (response instanceof AuthenticationErrorResponse) {
// The OpenID provider returned an error
System.err.println(response.toErrorResponse().getErrorObject());
throw new Exception(response.toErrorResponse().getErrorObject().toString());
}
return response.toSuccessResponse();
}
private static String[] getArray(String s) {
if (s == null) {

View File

@ -1,6 +1,10 @@
package me.kavin.piped.server.handlers.auth;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.nimbusds.oauth2.sdk.ResponseType;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.State;
import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
@ -9,6 +13,8 @@ import me.kavin.piped.utils.DatabaseHelper;
import me.kavin.piped.utils.DatabaseSessionFactory;
import me.kavin.piped.utils.ExceptionHandler;
import me.kavin.piped.utils.RequestUtils;
import me.kavin.piped.utils.obj.OidcData;
import me.kavin.piped.utils.obj.OidcProvider;
import me.kavin.piped.utils.obj.db.User;
import me.kavin.piped.utils.resp.*;
import org.apache.commons.codec.digest.DigestUtils;
@ -19,6 +25,10 @@ import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.io.IOException;
import java.net.URI;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@ -27,6 +37,7 @@ import static me.kavin.piped.consts.Constants.mapper;
public class UserHandlers {
private static final Argon2PasswordEncoder argon2PasswordEncoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
private static final BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder();
public static final Map<String, OidcData> PENDING_OIDC = new HashMap<>();
public static byte[] registerResponse(String user, String pass) throws Exception {
@ -111,6 +122,7 @@ public class UserHandlers {
public static String oidcCallbackResponse(String provider, String uid) {
try (Session s = DatabaseSessionFactory.createSession()) {
// TODO: Add oidc provider to database
String dbName = provider + "-" + uid;
CriteriaBuilder cb = s.getCriteriaBuilder();
CriteriaQuery<User> cr = cb.createQuery(User.class);
@ -148,12 +160,21 @@ public class UserHandlers {
String hash = user.getPassword();
if (hash.equals("")) {
//TODO: Authorize against oidc provider before deletion
var tr = s.beginTransaction();
s.remove(user);
tr.commit();
return mapper.writeValueAsBytes(new DeleteUserResponse(user.getUsername()));
if (hash.isEmpty()) {
//TODO: Get user from oidc table and lookup provider
OidcProvider provider = Constants.OIDC_PROVIDERS.get(0);
URI callback = URI.create(String.format("%s/oidc/%s/delete", Constants.PUBLIC_URL, provider.name));
OidcData data = new OidcData(session + "|" + Instant.now().getEpochSecond());
String state = data.getState();
PENDING_OIDC.put(state, data);
AuthenticationRequest oidcRequest = new AuthenticationRequest.Builder(
new ResponseType("code"),
new Scope("openid"), provider.clientID, callback).endpointURI(provider.authUri)
.state(new State(state)).nonce(data.nonce).maxAge(0).build();
return String.format("{\"redirect\": \"%s\"}", oidcRequest.toURI().toString()).getBytes();
}
if (!hashMatch(hash, pass))
ExceptionHandler.throwErrorResponse(new IncorrectCredentialsResponse());
@ -166,6 +187,18 @@ public class UserHandlers {
}
}
public static String deleteOidcUserResponse(String session) throws IOException {
try (Session s = DatabaseSessionFactory.createSession()) {
User user = DatabaseHelper.getUserFromSession(session);
var tr = s.beginTransaction();
s.remove(user);
tr.commit();
return user.getUsername();
}
}
public static byte[] logoutResponse(String session) throws JsonProcessingException {
if (StringUtils.isBlank(session))

View File

@ -0,0 +1,36 @@
package me.kavin.piped.utils.obj;
import com.nimbusds.openid.connect.sdk.Nonce;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class OidcData {
public final Nonce nonce;
public String data;
public OidcData(String data) {
this.nonce = new Nonce();
this.data = data;
}
public boolean isInvalidNonce(String nonce) {
return !nonce.equals(this.nonce.toString());
}
public String getState() {
String value = nonce + data;
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(value.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 not supported", e);
}
}
}