mirror of
https://github.com/TeamPiped/Piped-Backend.git
synced 2025-01-09 19:10:29 +05:30
Implement account deletion and cleanup some code
This commit is contained in:
parent
9b7246a029
commit
e7f2187b47
@ -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'
|
||||
|
@ -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) {
|
||||
|
@ -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))
|
||||
|
36
src/main/java/me/kavin/piped/utils/obj/OidcData.java
Normal file
36
src/main/java/me/kavin/piped/utils/obj/OidcData.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user