From 604fa651fcc14f893c96b8280928dca3fd9438e7 Mon Sep 17 00:00:00 2001 From: Jeidnx Date: Sun, 18 Jun 2023 18:28:18 +0200 Subject: [PATCH 01/13] Implement oidc --- build.gradle | 1 + config.properties | 6 ++ .../java/me/kavin/piped/consts/Constants.java | 26 +++++ .../me/kavin/piped/server/ServerLauncher.java | 101 ++++++++++++++++++ .../server/handlers/auth/UserHandlers.java | 38 ++++++- .../kavin/piped/utils/obj/OidcProvider.java | 25 +++++ .../me/kavin/piped/utils/obj/db/User.java | 2 +- 7 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 src/main/java/me/kavin/piped/utils/obj/OidcProvider.java diff --git a/build.gradle b/build.gradle index ad13d64c..a59c346a 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,7 @@ dependencies { implementation 'io.sentry:sentry:6.23.0' implementation 'rocks.kavin:reqwest4j:1.0.4' implementation 'io.minio:minio:8.5.3' + implementation 'com.nimbusds:oauth2-oidc-sdk:10.9.1' } shadowJar { diff --git a/config.properties b/config.properties index 5b147b8d..bd6efcb3 100644 --- a/config.properties +++ b/config.properties @@ -79,3 +79,9 @@ hibernate.connection.password:changeme # Frontend configuration #frontend.statusPageUrl:https://kavin.rocks #frontend.donationUrl:https://kavin.rocks + +# Oidc configuration +#oidc.provider.INSERT_HERE.name:INSERT_HERE +#oidc.provider.INSERT_HERE.clientId:INSERT_HERE +#oidc.provider.INSERT_HERE.clientSecret:INSERT_HERE +#oidc.provider.INSERT_HERE.authUrl:INSERT_HERE diff --git a/src/main/java/me/kavin/piped/consts/Constants.java b/src/main/java/me/kavin/piped/consts/Constants.java index 478b99e5..ef2fd829 100644 --- a/src/main/java/me/kavin/piped/consts/Constants.java +++ b/src/main/java/me/kavin/piped/consts/Constants.java @@ -3,12 +3,14 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import io.minio.MinioClient; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import me.kavin.piped.utils.PageMixin; import me.kavin.piped.utils.RequestUtils; +import me.kavin.piped.utils.obj.OidcProvider; import me.kavin.piped.utils.resp.ListLinkHandlerMixin; import okhttp3.OkHttpClient; import okhttp3.brotli.BrotliInterceptor; @@ -24,6 +26,7 @@ import java.io.FileReader; import java.net.InetSocketAddress; import java.net.ProxySelector; +import java.util.LinkedList; import java.util.List; import java.util.Properties; import java.util.regex.Pattern; @@ -99,6 +102,7 @@ public class Constants { public static final String YOUTUBE_COUNTRY; public static final String VERSION; + public static final LinkedList OIDC_PROVIDERS; public static final ObjectMapper mapper = JsonMapper.builder() .addMixIn(Page.class, PageMixin.class) @@ -162,12 +166,34 @@ public class Constants { MATRIX_SERVER = getProperty(prop, "MATRIX_SERVER", "https://matrix-client.matrix.org"); MATRIX_TOKEN = getProperty(prop, "MATRIX_TOKEN"); GEO_RESTRICTION_CHECKER_URL = getProperty(prop, "GEO_RESTRICTION_CHECKER_URL"); + + OIDC_PROVIDERS = new LinkedList<>(); + ArrayNode providerNames = frontendProperties.putArray("oidcProviders"); prop.forEach((_key, _value) -> { String key = String.valueOf(_key), value = String.valueOf(_value); if (key.startsWith("hibernate")) hibernateProperties.put(key, value); else if (key.startsWith("frontend.")) frontendProperties.put(StringUtils.substringAfter(key, "frontend."), value); + else if (key.startsWith("oidc.provider")) { + String[] split = key.split("\\."); + if (split.length != 4 || !split[3].equals("name")) return; + + try { + OIDC_PROVIDERS.add(new OidcProvider( + value, + getProperty(prop, "oidc.provider." + value + ".clientId"), + getProperty(prop, "oidc.provider." + value + ".clientSecret"), + getProperty(prop, "oidc.provider." + value + ".authUrl"), + getProperty(prop, "oidc.provider." + value + ".tokenUrl"), + getProperty(prop, "oidc.provider." + value + ".userinfoUrl") + )); + } catch (Exception e) { + System.err.println("Error while getting properties for oidc provider '" + value + "'"); + throw new RuntimeException(e); + } + providerNames.add(value); + } }); frontendProperties.put("imageProxyUrl", IMAGE_PROXY_PART); frontendProperties.putArray("countries").addAll( diff --git a/src/main/java/me/kavin/piped/server/ServerLauncher.java b/src/main/java/me/kavin/piped/server/ServerLauncher.java index 44e83ada..6a860fc0 100644 --- a/src/main/java/me/kavin/piped/server/ServerLauncher.java +++ b/src/main/java/me/kavin/piped/server/ServerLauncher.java @@ -2,6 +2,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; +import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; +import com.nimbusds.openid.connect.sdk.claims.UserInfo; import com.rometools.rome.feed.synd.SyndFeed; import com.rometools.rome.io.SyndFeedInput; import io.activej.config.Config; @@ -19,7 +22,9 @@ import me.kavin.piped.server.handlers.auth.StorageHandlers; import me.kavin.piped.server.handlers.auth.UserHandlers; import me.kavin.piped.utils.*; +import me.kavin.piped.utils.ErrorResponse; import me.kavin.piped.utils.obj.MatrixHelper; +import me.kavin.piped.utils.obj.OidcProvider; import me.kavin.piped.utils.obj.federation.FederatedVideoInfo; import me.kavin.piped.utils.resp.*; import org.apache.commons.lang3.StringUtils; @@ -30,12 +35,18 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.localization.DateWrapper; import org.xml.sax.InputSource; +import com.nimbusds.oauth2.sdk.*; +import com.nimbusds.openid.connect.sdk.*; +import com.nimbusds.oauth2.sdk.id.*; import java.io.ByteArrayInputStream; import java.net.InetSocketAddress; +import java.net.URI; +import java.util.LinkedList; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; import static io.activej.config.converter.ConfigConverters.ofInetSocketAddress; import static io.activej.http.HttpHeaders.*; @@ -293,6 +304,88 @@ AsyncServlet mainServlet(Executor executor) { LoginRequest.class); return getJsonResponse(UserHandlers.registerResponse(body.username, body.password), "private"); + } catch (Exception e) { + return getErrorResponse(e, request.getPath()); + } + })).map(GET, "/oidc/:provider/:function", AsyncServlet.ofBlocking(executor, request -> { + try { + String function = request.getPathParameter("function"); + + OidcProvider provider = findOidcProvider(request.getPathParameter("provider"), Constants.OIDC_PROVIDERS); + if(provider == null) + return HttpResponse.ofCode(500).withHtml("Can't find the provider on the server."); + + URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback"); + + switch (function) { + case "login" -> { + + State state = new State(); + Nonce nonce = new Nonce(); + + AuthenticationRequest oidcRequest = new AuthenticationRequest.Builder( + new ResponseType("code"), + new Scope("openid"), + provider.clientID, + callback) + .endpointURI(provider.authUri) + .state(state) + .nonce(nonce) + .build(); + + return HttpResponse.redirect302(oidcRequest.toURI().toString()); + } + case "callback" -> { + ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret); + + AuthenticationResponse response = AuthenticationResponseParser.parse( + 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()); + } + AuthenticationSuccessResponse sr = response.toSuccessResponse(); + + AuthorizationCode code = sr.getAuthorizationCode(); + AuthorizationGrant codeGrant = new AuthorizationCodeGrant( + code, callback + ); + + 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(); + + + UserInfoRequest ur = new UserInfoRequest(provider.userinfoUri, successResponse.getOIDCTokens().getBearerAccessToken()); + UserInfoResponse userInfoResponse = UserInfoResponse.parse(ur.toHTTPRequest().send()); + + if (! userInfoResponse.indicatesSuccess()) { + System.out.println(userInfoResponse.toErrorResponse().getErrorObject().getCode()); + System.out.println(userInfoResponse.toErrorResponse().getErrorObject().getDescription()); + return HttpResponse.ofCode(500).withHtml("Failed to query userInfo:\n\n" + userInfoResponse.toErrorResponse().getErrorObject().getDescription()); + } + + UserInfo userInfo = userInfoResponse.toSuccessResponse().getUserInfo(); + + String sessionId = UserHandlers.oidcCallbackResponse(provider.name, userInfo.getSubject().toString()); + + return HttpResponse.redirect302(Constants.FRONTEND_URL + "/login?session=" + sessionId); + } + default -> { + return HttpResponse.ofCode(500).withHtml("Invalid function `" + function + "`."); + } + } + + } catch (Exception e) { return getErrorResponse(e, request.getPath()); } @@ -542,6 +635,14 @@ AsyncServlet mainServlet(Executor executor) { return new CustomServletDecorator(router); } + private static OidcProvider findOidcProvider(String provider, LinkedList list){ + for(int i = 0; i < list.size(); i++) { + OidcProvider curr = list.get(i); + if(curr == null || !curr.name.equals(provider)) continue; + return curr; + } + return null; + } private static String[] getArray(String s) { if (s == null) { diff --git a/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java b/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java index ad36481f..1c7c21c5 100644 --- a/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java +++ b/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java @@ -107,11 +107,36 @@ public static byte[] loginResponse(String user, String pass) return null; } } + public static String oidcCallbackResponse(String provider, String uid) { + try (Session s = DatabaseSessionFactory.createSession()) { + String dbName = provider + "-" + uid; + System.out.println(dbName); //TODO: + CriteriaBuilder cb = s.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery(User.class); + Root root = cr.from(User.class); + cr.select(root).where(root.get("username").in( + dbName + )); - public static byte[] deleteUserResponse(String session, String pass) throws IOException { + User dbuser = s.createQuery(cr).uniqueResult(); + + if (dbuser == null) { + User newuser = new User(dbName, "", Set.of()); + + var tr = s.beginTransaction(); + s.persist(newuser); + tr.commit(); + + + return newuser.getSessionId(); + } + return dbuser.getSessionId(); + } - if (StringUtils.isBlank(session) || StringUtils.isBlank(pass)) - ExceptionHandler.throwErrorResponse(new InvalidRequestResponse("session and password are required parameters")); + } + public static byte[] deleteUserResponse(String session, String pass) throws IOException { + if (StringUtils.isBlank(session)) + ExceptionHandler.throwErrorResponse(new InvalidRequestResponse("session is a required parameter")); try (Session s = DatabaseSessionFactory.createSession()) { User user = DatabaseHelper.getUserFromSession(session); @@ -121,6 +146,13 @@ public static byte[] deleteUserResponse(String session, String pass) throws IOEx 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 (!hashMatch(hash, pass)) ExceptionHandler.throwErrorResponse(new IncorrectCredentialsResponse()); diff --git a/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java b/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java new file mode 100644 index 00000000..a869c2a1 --- /dev/null +++ b/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java @@ -0,0 +1,25 @@ +package me.kavin.piped.utils.obj; + +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.id.ClientID; + +import java.net.URI; +import java.net.URISyntaxException; + +public class OidcProvider { + public String name; + public ClientID clientID; + public Secret clientSecret; + public URI authUri; + public URI tokenUri; + public URI userinfoUri; + + public OidcProvider(String name, String clientID, String clientSecret, String authUri, String tokenUri, String userinfoUri) throws URISyntaxException { + this.name = name; + this.clientID = new ClientID(clientID); + this.clientSecret = new Secret(clientSecret); + this.authUri = new URI(authUri); + this.tokenUri = new URI(tokenUri); + this.userinfoUri = new URI(userinfoUri); + } +} diff --git a/src/main/java/me/kavin/piped/utils/obj/db/User.java b/src/main/java/me/kavin/piped/utils/obj/db/User.java index fe3ceb4b..1bf1b42b 100644 --- a/src/main/java/me/kavin/piped/utils/obj/db/User.java +++ b/src/main/java/me/kavin/piped/utils/obj/db/User.java @@ -20,7 +20,7 @@ public class User implements Serializable { @Column(name = "id") private long id; - @Column(name = "username", unique = true, length = 24) + @Column(name = "username", unique = true, length = 32) private String username; @Column(name = "password", columnDefinition = "text") From 375ee585c13eaa9a036061dcec1b9d15e872c89c Mon Sep 17 00:00:00 2001 From: Jeidnx Date: Mon, 19 Jun 2023 13:11:20 +0200 Subject: [PATCH 02/13] Better Error handling for oidc config --- config.properties | 4 +- .../java/me/kavin/piped/consts/Constants.java | 39 ++++++++++--------- .../kavin/piped/utils/obj/OidcProvider.java | 13 +++++-- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/config.properties b/config.properties index bd6efcb3..916cffbd 100644 --- a/config.properties +++ b/config.properties @@ -84,4 +84,6 @@ hibernate.connection.password:changeme #oidc.provider.INSERT_HERE.name:INSERT_HERE #oidc.provider.INSERT_HERE.clientId:INSERT_HERE #oidc.provider.INSERT_HERE.clientSecret:INSERT_HERE -#oidc.provider.INSERT_HERE.authUrl:INSERT_HERE +#oidc.provider.INSERT_HERE.authUri:INSERT_HERE +#oidc.provider.INSERT_HERE.tokenUri:INSERT_HERE +#oidc.provider.INSERT_HERE.userinfoUri:INSERT_HERE \ No newline at end of file diff --git a/src/main/java/me/kavin/piped/consts/Constants.java b/src/main/java/me/kavin/piped/consts/Constants.java index ef2fd829..d8586be0 100644 --- a/src/main/java/me/kavin/piped/consts/Constants.java +++ b/src/main/java/me/kavin/piped/consts/Constants.java @@ -26,7 +26,7 @@ import java.io.FileReader; import java.net.InetSocketAddress; import java.net.ProxySelector; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.regex.Pattern; @@ -102,7 +102,7 @@ public class Constants { public static final String YOUTUBE_COUNTRY; public static final String VERSION; - public static final LinkedList OIDC_PROVIDERS; + public static final ArrayList OIDC_PROVIDERS; public static final ObjectMapper mapper = JsonMapper.builder() .addMixIn(Page.class, PageMixin.class) @@ -167,7 +167,7 @@ public class Constants { MATRIX_TOKEN = getProperty(prop, "MATRIX_TOKEN"); GEO_RESTRICTION_CHECKER_URL = getProperty(prop, "GEO_RESTRICTION_CHECKER_URL"); - OIDC_PROVIDERS = new LinkedList<>(); + OIDC_PROVIDERS = new ArrayList<>(); ArrayNode providerNames = frontendProperties.putArray("oidcProviders"); prop.forEach((_key, _value) -> { String key = String.valueOf(_key), value = String.valueOf(_value); @@ -178,21 +178,15 @@ else if (key.startsWith("frontend.")) else if (key.startsWith("oidc.provider")) { String[] split = key.split("\\."); if (split.length != 4 || !split[3].equals("name")) return; - - try { - OIDC_PROVIDERS.add(new OidcProvider( - value, - getProperty(prop, "oidc.provider." + value + ".clientId"), - getProperty(prop, "oidc.provider." + value + ".clientSecret"), - getProperty(prop, "oidc.provider." + value + ".authUrl"), - getProperty(prop, "oidc.provider." + value + ".tokenUrl"), - getProperty(prop, "oidc.provider." + value + ".userinfoUrl") - )); - } catch (Exception e) { - System.err.println("Error while getting properties for oidc provider '" + value + "'"); - throw new RuntimeException(e); - } - providerNames.add(value); + OIDC_PROVIDERS.add(new OidcProvider( + value, + getRequiredOidcProperty(prop, value, "clientId"), + getRequiredOidcProperty(prop, value, "clientSecret"), + getRequiredOidcProperty(prop, value, "authUri"), + getRequiredOidcProperty(prop, value, "tokenUri"), + getRequiredOidcProperty(prop, value, "userinfoUri")) + ); + providerNames.add(value); } }); frontendProperties.put("imageProxyUrl", IMAGE_PROXY_PART); @@ -256,4 +250,13 @@ private static String getProperty(final Properties prop, String key, String def) return prop.getProperty(key, def); } + + private static String getRequiredOidcProperty(final Properties prop, String provider, String key) { + String value = getProperty(prop, "oidc.provider." + provider + "." + key); + if(value == null || value.equals("")){ + System.err.println("Missing " + key + " for oidc provider '" + provider + "'"); + System.exit(1); + } + return value; + } } diff --git a/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java b/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java index a869c2a1..79216ea5 100644 --- a/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java +++ b/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java @@ -14,12 +14,17 @@ public class OidcProvider { public URI tokenUri; public URI userinfoUri; - public OidcProvider(String name, String clientID, String clientSecret, String authUri, String tokenUri, String userinfoUri) throws URISyntaxException { + public OidcProvider(String name, String clientID, String clientSecret, String authUri, String tokenUri, String userinfoUri) { this.name = name; this.clientID = new ClientID(clientID); this.clientSecret = new Secret(clientSecret); - this.authUri = new URI(authUri); - this.tokenUri = new URI(tokenUri); - this.userinfoUri = new URI(userinfoUri); + try { + this.authUri = new URI(authUri); + this.tokenUri = new URI(tokenUri); + this.userinfoUri = new URI(userinfoUri); + } catch(URISyntaxException e) { + System.err.println("Malformed URI for oidc provider '" + name + "' found."); + System.exit(1); + } } } From 143711cb2ae5b2dc771bb7db8b1d96580e7f8cc8 Mon Sep 17 00:00:00 2001 From: Jeidnx Date: Mon, 19 Jun 2023 16:06:26 +0200 Subject: [PATCH 03/13] Save redirect in state --- .../me/kavin/piped/server/ServerLauncher.java | 16 +++++++--------- .../piped/server/handlers/auth/UserHandlers.java | 1 - 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main/java/me/kavin/piped/server/ServerLauncher.java b/src/main/java/me/kavin/piped/server/ServerLauncher.java index 6a860fc0..53b5403a 100644 --- a/src/main/java/me/kavin/piped/server/ServerLauncher.java +++ b/src/main/java/me/kavin/piped/server/ServerLauncher.java @@ -42,11 +42,9 @@ import java.io.ByteArrayInputStream; import java.net.InetSocketAddress; import java.net.URI; -import java.util.LinkedList; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; import static io.activej.config.converter.ConfigConverters.ofInetSocketAddress; import static io.activej.http.HttpHeaders.*; @@ -310,8 +308,7 @@ AsyncServlet mainServlet(Executor executor) { })).map(GET, "/oidc/:provider/:function", AsyncServlet.ofBlocking(executor, request -> { try { String function = request.getPathParameter("function"); - - OidcProvider provider = findOidcProvider(request.getPathParameter("provider"), Constants.OIDC_PROVIDERS); + OidcProvider provider = getOidcProvider(request.getPathParameter("provider")); if(provider == null) return HttpResponse.ofCode(500).withHtml("Can't find the provider on the server."); @@ -319,8 +316,9 @@ AsyncServlet mainServlet(Executor executor) { switch (function) { case "login" -> { + String redirectUri = request.getQueryParameter("redirect"); - State state = new State(); + State state = new State(new Identifier(24) + "." + redirectUri); Nonce nonce = new Nonce(); AuthenticationRequest oidcRequest = new AuthenticationRequest.Builder( @@ -378,7 +376,7 @@ AsyncServlet mainServlet(Executor executor) { String sessionId = UserHandlers.oidcCallbackResponse(provider.name, userInfo.getSubject().toString()); - return HttpResponse.redirect302(Constants.FRONTEND_URL + "/login?session=" + sessionId); + return HttpResponse.redirect302(sr.getState().toString().split("\\.", 2)[1] + "?session=" + sessionId); } default -> { return HttpResponse.ofCode(500).withHtml("Invalid function `" + function + "`."); @@ -635,9 +633,9 @@ AsyncServlet mainServlet(Executor executor) { return new CustomServletDecorator(router); } - private static OidcProvider findOidcProvider(String provider, LinkedList list){ - for(int i = 0; i < list.size(); i++) { - OidcProvider curr = list.get(i); + private static OidcProvider getOidcProvider(String provider){ + for(int i = 0; i < Constants.OIDC_PROVIDERS.size(); i++) { + OidcProvider curr = Constants.OIDC_PROVIDERS.get(i); if(curr == null || !curr.name.equals(provider)) continue; return curr; } diff --git a/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java b/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java index 1c7c21c5..b783819b 100644 --- a/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java +++ b/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java @@ -110,7 +110,6 @@ public static byte[] loginResponse(String user, String pass) public static String oidcCallbackResponse(String provider, String uid) { try (Session s = DatabaseSessionFactory.createSession()) { String dbName = provider + "-" + uid; - System.out.println(dbName); //TODO: CriteriaBuilder cb = s.getCriteriaBuilder(); CriteriaQuery cr = cb.createQuery(User.class); Root root = cr.from(User.class); From 18d93177fbe8009a45379f67734bad6d5d284d45 Mon Sep 17 00:00:00 2001 From: Jeidnx Date: Mon, 19 Jun 2023 16:50:20 +0200 Subject: [PATCH 04/13] Show warning message before oidc login --- src/main/java/me/kavin/piped/server/ServerLauncher.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/me/kavin/piped/server/ServerLauncher.java b/src/main/java/me/kavin/piped/server/ServerLauncher.java index 53b5403a..d549b15b 100644 --- a/src/main/java/me/kavin/piped/server/ServerLauncher.java +++ b/src/main/java/me/kavin/piped/server/ServerLauncher.java @@ -331,7 +331,13 @@ AsyncServlet mainServlet(Executor executor) { .nonce(nonce) .build(); - return HttpResponse.redirect302(oidcRequest.toURI().toString()); + return HttpResponse.ok200().withHtml( + "" + + "

Warning:

You are trying to give
"
+                                                + redirectUri
+                                                + "
access to your Piped account. If you wish to continue click here"); } case "callback" -> { ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret); From f4b9dffebe4a388c58dfe2e5f5277f4b81cad044 Mon Sep 17 00:00:00 2001 From: Jeidnx Date: Tue, 27 Jun 2023 12:24:18 +0200 Subject: [PATCH 05/13] Only show warning when not redirecting to configured frontend --- src/main/java/me/kavin/piped/server/ServerLauncher.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/me/kavin/piped/server/ServerLauncher.java b/src/main/java/me/kavin/piped/server/ServerLauncher.java index d549b15b..f88e7bc9 100644 --- a/src/main/java/me/kavin/piped/server/ServerLauncher.java +++ b/src/main/java/me/kavin/piped/server/ServerLauncher.java @@ -318,6 +318,9 @@ AsyncServlet mainServlet(Executor executor) { case "login" -> { String redirectUri = request.getQueryParameter("redirect"); + if (redirectUri == null || redirectUri.equals("")) { + return HttpResponse.ofCode(400).withHtml("Missing redirect parameter"); + } State state = new State(new Identifier(24) + "." + redirectUri); Nonce nonce = new Nonce(); @@ -331,6 +334,9 @@ AsyncServlet mainServlet(Executor executor) { .nonce(nonce) .build(); + if(redirectUri.equals(Constants.FRONTEND_URL + "/login")) { + return HttpResponse.redirect302(oidcRequest.toURI().toString()); + } return HttpResponse.ok200().withHtml( "" + "

Warning:

You are trying to give
"

From 847f80c0d43b8821bd5e955a824392f4f44cf478 Mon Sep 17 00:00:00 2001
From: Kavin <20838718+FireMasterK@users.noreply.github.com>
Date: Sat, 5 Aug 2023 16:53:38 +0100
Subject: [PATCH 06/13] Simplify config handling.

---
 .../java/me/kavin/piped/consts/Constants.java | 48 +++++++++++--------
 1 file changed, 28 insertions(+), 20 deletions(-)

diff --git a/src/main/java/me/kavin/piped/consts/Constants.java b/src/main/java/me/kavin/piped/consts/Constants.java
index 3340cc63..3b44b07b 100644
--- a/src/main/java/me/kavin/piped/consts/Constants.java
+++ b/src/main/java/me/kavin/piped/consts/Constants.java
@@ -8,6 +8,7 @@
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import io.minio.MinioClient;
 import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
 import me.kavin.piped.utils.PageMixin;
 import me.kavin.piped.utils.RequestUtils;
 import me.kavin.piped.utils.obj.OidcProvider;
@@ -25,10 +26,8 @@
 
 import java.io.File;
 import java.io.FileReader;
-import java.net.InetSocketAddress;
-import java.net.ProxySelector;
-import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Properties;
 import java.util.regex.Pattern;
 
@@ -103,7 +102,7 @@ public class Constants {
     public static final String YOUTUBE_COUNTRY;
 
     public static final String VERSION;
-    public static final ArrayList OIDC_PROVIDERS;
+    public static final List OIDC_PROVIDERS;
 
     public static final ObjectMapper mapper = JsonMapper.builder()
             .addMixIn(Page.class, PageMixin.class)
@@ -169,7 +168,9 @@ public class Constants {
             MATRIX_TOKEN = getProperty(prop, "MATRIX_TOKEN");
             GEO_RESTRICTION_CHECKER_URL = getProperty(prop, "GEO_RESTRICTION_CHECKER_URL");
 
-            OIDC_PROVIDERS = new ArrayList<>();
+            OIDC_PROVIDERS = new ObjectArrayList<>();
+
+            Map> oidcProviderConfig = new Object2ObjectOpenHashMap<>();
             ArrayNode providerNames = frontendProperties.putArray("oidcProviders");
             prop.forEach((_key, _value) -> {
                 String key = String.valueOf(_key), value = String.valueOf(_value);
@@ -178,19 +179,26 @@ public class Constants {
                 else if (key.startsWith("frontend."))
                     frontendProperties.put(StringUtils.substringAfter(key, "frontend."), value);
                 else if (key.startsWith("oidc.provider")) {
-                   String[] split = key.split("\\.");
-                   if (split.length != 4 || !split[3].equals("name")) return;
-                   OIDC_PROVIDERS.add(new OidcProvider(
-                           value,
-                           getRequiredOidcProperty(prop, value, "clientId"),
-                           getRequiredOidcProperty(prop, value, "clientSecret"),
-                           getRequiredOidcProperty(prop, value, "authUri"),
-                           getRequiredOidcProperty(prop, value, "tokenUri"),
-                           getRequiredOidcProperty(prop, value, "userinfoUri"))
-                   );
-                   providerNames.add(value);
+                    String[] split = key.split("\\.");
+                    if (split.length != 4) return;
+                    oidcProviderConfig
+                            .computeIfAbsent(split[2], k -> new Object2ObjectOpenHashMap<>())
+                            .put(split[3], value);
                 }
             });
+            oidcProviderConfig.forEach((provider, config) -> {
+                ObjectNode providerNode = frontendProperties.putObject(provider);
+                OIDC_PROVIDERS.add(new OidcProvider(
+                        getRequiredMapValue(config, "name"),
+                        getRequiredMapValue(config, "clientId"),
+                        getRequiredMapValue(config, "clientSecret"),
+                        getRequiredMapValue(config, "authUri"),
+                        getRequiredMapValue(config, "tokenUri"),
+                        getRequiredMapValue(config, "userinfoUri")
+                ));
+                providerNames.add(provider);
+                config.forEach(providerNode::put);
+            });
             frontendProperties.put("imageProxyUrl", IMAGE_PROXY_PART);
             frontendProperties.putArray("countries").addAll(
                     YOUTUBE_SERVICE.getSupportedCountries().stream().map(ContentCountry::getCountryCode)
@@ -247,10 +255,10 @@ private static String getProperty(final Properties prop, String key, String def)
         return prop.getProperty(key, def);
     }
 
-    private static String getRequiredOidcProperty(final Properties prop, String provider, String key) {
-        String value = getProperty(prop, "oidc.provider." + provider + "." + key);
-        if(value == null || value.equals("")){
-            System.err.println("Missing " + key + " for oidc provider '" + provider + "'");
+    private static String getRequiredMapValue(final Map map, Object key) {
+        String value = map.get(key);
+        if (StringUtils.isBlank(value)) {
+            System.err.println("Missing '" + key + "' in sub-configuration");
             System.exit(1);
         }
         return value;

From 946ac458ed9abaa91e907a946509580ec8ec2dbf Mon Sep 17 00:00:00 2001
From: Kavin <20838718+FireMasterK@users.noreply.github.com>
Date: Sat, 5 Aug 2023 16:56:59 +0100
Subject: [PATCH 07/13] Add missing newline.

---
 config.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/config.properties b/config.properties
index 498d9d05..71df0fb1 100644
--- a/config.properties
+++ b/config.properties
@@ -86,4 +86,4 @@ hibernate.connection.password:changeme
 #oidc.provider.INSERT_HERE.clientSecret:INSERT_HERE
 #oidc.provider.INSERT_HERE.authUri:INSERT_HERE
 #oidc.provider.INSERT_HERE.tokenUri:INSERT_HERE
-#oidc.provider.INSERT_HERE.userinfoUri:INSERT_HERE
\ No newline at end of file
+#oidc.provider.INSERT_HERE.userinfoUri:INSERT_HERE

From 0eb235180bdd0f844ba2cb443a67b740690288ce Mon Sep 17 00:00:00 2001
From: Kavin <20838718+FireMasterK@users.noreply.github.com>
Date: Sat, 5 Aug 2023 17:37:42 +0100
Subject: [PATCH 08/13] Format all code.

---
 .../me/kavin/piped/server/ServerLauncher.java | 30 ++++++++--------
 .../server/handlers/auth/UserHandlers.java    | 34 ++++++++++---------
 .../kavin/piped/utils/obj/OidcProvider.java   |  2 +-
 3 files changed, 35 insertions(+), 31 deletions(-)

diff --git a/src/main/java/me/kavin/piped/server/ServerLauncher.java b/src/main/java/me/kavin/piped/server/ServerLauncher.java
index a62b996d..676fc448 100644
--- a/src/main/java/me/kavin/piped/server/ServerLauncher.java
+++ b/src/main/java/me/kavin/piped/server/ServerLauncher.java
@@ -2,8 +2,12 @@
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
+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;
 import com.rometools.rome.feed.synd.SyndFeed;
 import com.rometools.rome.io.SyndFeedInput;
@@ -21,8 +25,8 @@
 import me.kavin.piped.server.handlers.auth.FeedHandlers;
 import me.kavin.piped.server.handlers.auth.StorageHandlers;
 import me.kavin.piped.server.handlers.auth.UserHandlers;
-import me.kavin.piped.utils.*;
 import me.kavin.piped.utils.ErrorResponse;
+import me.kavin.piped.utils.*;
 import me.kavin.piped.utils.obj.MatrixHelper;
 import me.kavin.piped.utils.obj.OidcProvider;
 import me.kavin.piped.utils.obj.federation.FederatedVideoInfo;
@@ -35,14 +39,10 @@
 import org.schabi.newpipe.extractor.exceptions.ParsingException;
 import org.schabi.newpipe.extractor.localization.DateWrapper;
 import org.xml.sax.InputSource;
-import com.nimbusds.oauth2.sdk.*;
-import com.nimbusds.openid.connect.sdk.*;
-import com.nimbusds.oauth2.sdk.id.*;
 
 import java.io.ByteArrayInputStream;
 import java.net.InetSocketAddress;
 import java.net.URI;
-import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
@@ -330,7 +330,7 @@ AsyncServlet mainServlet(Executor executor) {
                     try {
                         String function = request.getPathParameter("function");
                         OidcProvider provider = getOidcProvider(request.getPathParameter("provider"));
-                        if(provider == null)
+                        if (provider == null)
                             return HttpResponse.ofCode(500).withHtml("Can't find the provider on the server.");
 
                         URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback");
@@ -339,9 +339,10 @@ AsyncServlet mainServlet(Executor executor) {
                             case "login" -> {
                                 String redirectUri = request.getQueryParameter("redirect");
 
-                                if (redirectUri == null || redirectUri.equals("")) {
+                                if (StringUtils.isBlank(redirectUri)) {
                                     return HttpResponse.ofCode(400).withHtml("Missing redirect parameter");
                                 }
+
                                 State state = new State(new Identifier(24) + "." + redirectUri);
                                 Nonce nonce = new Nonce();
 
@@ -355,7 +356,7 @@ AsyncServlet mainServlet(Executor executor) {
                                         .nonce(nonce)
                                         .build();
 
-                                if(redirectUri.equals(Constants.FRONTEND_URL + "/login")) {
+                                if (redirectUri.equals(Constants.FRONTEND_URL + "/login")) {
                                     return HttpResponse.redirect302(oidcRequest.toURI().toString());
                                 }
                                 return HttpResponse.ok200().withHtml(
@@ -388,18 +389,18 @@ AsyncServlet mainServlet(Executor executor) {
                                 TokenRequest tr = new TokenRequest(provider.tokenUri, clientAuth, codeGrant);
                                 TokenResponse tokenResponse = OIDCTokenResponseParser.parse(tr.toHTTPRequest().send());
 
-                                if (! tokenResponse.indicatesSuccess()) {
+                                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 = (OIDCTokenResponse) tokenResponse.toSuccessResponse();
 
 
                                 UserInfoRequest ur = new UserInfoRequest(provider.userinfoUri, successResponse.getOIDCTokens().getBearerAccessToken());
                                 UserInfoResponse userInfoResponse = UserInfoResponse.parse(ur.toHTTPRequest().send());
 
-                                if (! userInfoResponse.indicatesSuccess()) {
+                                if (!userInfoResponse.indicatesSuccess()) {
                                     System.out.println(userInfoResponse.toErrorResponse().getErrorObject().getCode());
                                     System.out.println(userInfoResponse.toErrorResponse().getErrorObject().getDescription());
                                     return HttpResponse.ofCode(500).withHtml("Failed to query userInfo:\n\n" + userInfoResponse.toErrorResponse().getErrorObject().getDescription());
@@ -666,14 +667,15 @@ AsyncServlet mainServlet(Executor executor) {
         return new CustomServletDecorator(router);
     }
 
-    private static OidcProvider getOidcProvider(String provider){
-        for(int i = 0; i < Constants.OIDC_PROVIDERS.size(); i++) {
+    private static OidcProvider getOidcProvider(String provider) {
+        for (int i = 0; i < Constants.OIDC_PROVIDERS.size(); i++) {
             OidcProvider curr = Constants.OIDC_PROVIDERS.get(i);
-            if(curr == null || !curr.name.equals(provider)) continue;
+            if (curr == null || !curr.name.equals(provider)) continue;
             return curr;
         }
         return null;
     }
+
     private static String[] getArray(String s) {
 
         if (s == null) {
diff --git a/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java b/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java
index 8788ce2a..7059bc29 100644
--- a/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java
+++ b/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java
@@ -108,32 +108,34 @@ public static byte[] loginResponse(String user, String pass)
             return null;
         }
     }
+
     public static String oidcCallbackResponse(String provider, String uid) {
         try (Session s = DatabaseSessionFactory.createSession()) {
-                String dbName = provider + "-" + uid;
-                CriteriaBuilder cb = s.getCriteriaBuilder();
-                CriteriaQuery cr = cb.createQuery(User.class);
-                Root root = cr.from(User.class);
-                cr.select(root).where(root.get("username").in(
-                        dbName
-                ));
+            String dbName = provider + "-" + uid;
+            CriteriaBuilder cb = s.getCriteriaBuilder();
+            CriteriaQuery cr = cb.createQuery(User.class);
+            Root root = cr.from(User.class);
+            cr.select(root).where(root.get("username").in(
+                    dbName
+            ));
 
-                User dbuser = s.createQuery(cr).uniqueResult();
+            User dbuser = s.createQuery(cr).uniqueResult();
 
-                if (dbuser == null) {
-                        User newuser = new User(dbName, "", Set.of());
+            if (dbuser == null) {
+                User newuser = new User(dbName, "", Set.of());
 
-                        var tr = s.beginTransaction();
-                        s.persist(newuser);
-                        tr.commit();
+                var tr = s.beginTransaction();
+                s.persist(newuser);
+                tr.commit();
 
 
-                        return newuser.getSessionId();
-                }
-                return dbuser.getSessionId();
+                return newuser.getSessionId();
             }
+            return dbuser.getSessionId();
+        }
 
     }
+
     public static byte[] deleteUserResponse(String session, String pass) throws IOException {
         if (StringUtils.isBlank(session))
             ExceptionHandler.throwErrorResponse(new InvalidRequestResponse("session is a required parameter"));
diff --git a/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java b/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java
index 79216ea5..aedce630 100644
--- a/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java
+++ b/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java
@@ -22,7 +22,7 @@ public OidcProvider(String name, String clientID, String clientSecret, String au
             this.authUri = new URI(authUri);
             this.tokenUri = new URI(tokenUri);
             this.userinfoUri = new URI(userinfoUri);
-        } catch(URISyntaxException e) {
+        } catch (URISyntaxException e) {
             System.err.println("Malformed URI for oidc provider '" + name + "' found.");
             System.exit(1);
         }

From e7f2187b47cbfbcab3cd4c1108e2dcd5446850a7 Mon Sep 17 00:00:00 2001
From: Jeidnx 
Date: Wed, 25 Oct 2023 10:03:15 +0200
Subject: [PATCH 09/13] Implement account deletion and cleanup some code

---
 build.gradle                                  |   2 +-
 .../me/kavin/piped/server/ServerLauncher.java | 127 +++++++++++++-----
 .../server/handlers/auth/UserHandlers.java    |  45 ++++++-
 .../me/kavin/piped/utils/obj/OidcData.java    |  36 +++++
 4 files changed, 170 insertions(+), 40 deletions(-)
 create mode 100644 src/main/java/me/kavin/piped/utils/obj/OidcData.java

diff --git a/build.gradle b/build.gradle
index 3da01944..8d790511 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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'
diff --git a/src/main/java/me/kavin/piped/server/ServerLauncher.java b/src/main/java/me/kavin/piped/server/ServerLauncher.java
index a089b396..3394909b 100644
--- a/src/main/java/me/kavin/piped/server/ServerLauncher.java
+++ b/src/main/java/me/kavin/piped/server/ServerLauncher.java
@@ -2,10 +2,10 @@
 
 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.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 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 PENDING_OIDC = new HashMap<>();
 
     @Provides
     Executor executor() {
@@ -285,7 +289,7 @@ AsyncServlet mainServlet(Executor executor) {
                         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 @@ AsyncServlet mainServlet(Executor executor) {
                                 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(
-                                        ""
-                                                + "

Warning:

You are trying to give
"
-                                                + redirectUri
-                                                + "
access to your Piped account. If you wish to continue click here"); + "" + + "

Warning:

You are trying to give
" +
+                                                redirectUri +
+                                                "
access to your Piped account. If you wish to continue click " + + "here"); } 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 @@ AsyncServlet mainServlet(Executor executor) { 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); + + 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()); - return HttpResponse.redirect302(sr.getState().toString().split("\\.", 2)[1] + "?session=" + sessionId); + 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 @@ private static OidcProvider getOidcProvider(String provider) { 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) { diff --git a/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java b/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java index 7059bc29..207081f4 100644 --- a/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java +++ b/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java @@ -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.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.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 @@ public class UserHandlers { private static final Argon2PasswordEncoder argon2PasswordEncoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8(); private static final BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder(); + public static final Map PENDING_OIDC = new HashMap<>(); public static byte[] registerResponse(String user, String pass) throws Exception { @@ -111,6 +122,7 @@ public static byte[] loginResponse(String user, String pass) 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 cr = cb.createQuery(User.class); @@ -148,12 +160,21 @@ public static byte[] deleteUserResponse(String session, String pass) throws IOEx 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 static byte[] deleteUserResponse(String session, String pass) throws IOEx } } + 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)) diff --git a/src/main/java/me/kavin/piped/utils/obj/OidcData.java b/src/main/java/me/kavin/piped/utils/obj/OidcData.java new file mode 100644 index 00000000..f1bfd6c0 --- /dev/null +++ b/src/main/java/me/kavin/piped/utils/obj/OidcData.java @@ -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); + } + } +} From c1fde372a5d664005490f09578a96cf1ad72d9e4 Mon Sep 17 00:00:00 2001 From: Jeidnx Date: Thu, 26 Oct 2023 13:24:27 +0200 Subject: [PATCH 10/13] Refactor oidc logic into UserHandlers --- .../me/kavin/piped/server/ServerLauncher.java | 159 +--------------- .../server/handlers/auth/UserHandlers.java | 171 ++++++++++++++++-- 2 files changed, 159 insertions(+), 171 deletions(-) diff --git a/src/main/java/me/kavin/piped/server/ServerLauncher.java b/src/main/java/me/kavin/piped/server/ServerLauncher.java index 3394909b..3098bfdd 100644 --- a/src/main/java/me/kavin/piped/server/ServerLauncher.java +++ b/src/main/java/me/kavin/piped/server/ServerLauncher.java @@ -2,13 +2,6 @@ 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.State; -import com.nimbusds.openid.connect.sdk.*; -import com.nimbusds.openid.connect.sdk.claims.UserInfo; import com.rometools.rome.feed.synd.SyndFeed; import com.rometools.rome.io.SyndFeedInput; import io.activej.config.Config; @@ -44,11 +37,8 @@ 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; import static io.activej.config.converter.ConfigConverters.ofInetSocketAddress; import static io.activej.http.HttpHeaders.*; @@ -61,7 +51,6 @@ 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 PENDING_OIDC = new HashMap<>(); @Provides Executor executor() { @@ -291,137 +280,12 @@ AsyncServlet mainServlet(Executor executor) { if (provider == null) return HttpResponse.ofCode(500).withHtml("Can't find the provider on the server"); - URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback"); - - switch (function) { - case "login" -> { - String redirectUri = request.getQueryParameter("redirect"); - - if (StringUtils.isBlank(redirectUri)) { - return HttpResponse.ofCode(400).withHtml("redirect is a required parameter"); - } - - 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(new State(state)).nonce(data.nonce).build(); - - if (redirectUri.equals(Constants.FRONTEND_URL + "/login")) { - return HttpResponse.redirect302(oidcRequest.toURI().toString()); - } - return HttpResponse.ok200().withHtml( - "" + - "

Warning:

You are trying to give
" +
-                                                redirectUri +
-                                                "
access to your Piped account. If you wish to continue click " + - "here"); - } - case "callback" -> { - ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret); - - AuthenticationSuccessResponse sr = parseOidcUri(URI.create(request.getFullUrl())); - - 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" - ); - } - AuthorizationCode code = sr.getAuthorizationCode(); - AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback); - - - TokenRequest tr = new TokenRequest(provider.tokenUri, clientAuth, codeGrant); - 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 = 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()); - - if (!userInfoResponse.indicatesSuccess()) { - System.out.println(userInfoResponse.toErrorResponse().getErrorObject().getCode()); - System.out.println(userInfoResponse.toErrorResponse().getErrorObject().getDescription()); - return HttpResponse.ofCode(500).withHtml("Failed to query userInfo:\n\n" + userInfoResponse.toErrorResponse().getErrorObject().getDescription()); - } - - 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); - - 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 switch (function) { + case "login" -> UserHandlers.oidcLoginResponse(provider, request.getQueryParameter("redirect")); + case "callback" -> UserHandlers.oidcCallbackResponse(provider, URI.create(request.getFullUrl())); + case "delete" -> UserHandlers.oidcDeleteResponse(provider, URI.create(request.getFullUrl())); + default -> HttpResponse.ofCode(500).withHtml("Invalid function `" + function + "`"); + }; } catch (Exception e) { return getErrorResponse(e, request.getPath()); } @@ -680,17 +544,6 @@ private static OidcProvider getOidcProvider(String provider) { 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) { diff --git a/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java b/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java index 207081f4..40df4df6 100644 --- a/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java +++ b/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java @@ -1,10 +1,14 @@ 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.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.State; -import com.nimbusds.openid.connect.sdk.AuthenticationRequest; +import com.nimbusds.openid.connect.sdk.*; +import com.nimbusds.openid.connect.sdk.claims.UserInfo; +import io.activej.http.HttpResponse; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; @@ -120,7 +124,84 @@ public static byte[] loginResponse(String user, String pass) } } - public static String oidcCallbackResponse(String provider, String uid) { + public static HttpResponse oidcLoginResponse(OidcProvider provider, String redirectUri) throws Exception{ + if (StringUtils.isBlank(redirectUri)) { + return HttpResponse.ofCode(400).withHtml("redirect is a required parameter"); + } + + URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback"); + 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(new State(state)).nonce(data.nonce).build(); + + if (redirectUri.equals(Constants.FRONTEND_URL + "/login")) { + return HttpResponse.redirect302(oidcRequest.toURI().toString()); + } + return HttpResponse.ok200().withHtml( + "" + + "

Warning:

You are trying to give
" +
+                        redirectUri +
+                        "
access to your Piped account. If you wish to continue click " + + "here"); + } + public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI requestUri) throws Exception { + ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret); + + AuthenticationSuccessResponse sr = parseOidcUri(requestUri); + + 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" + ); + } + + URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback"); + AuthorizationCode code = sr.getAuthorizationCode(); + AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback); + + + TokenRequest tokenReq = new TokenRequest(provider.tokenUri, clientAuth, codeGrant); + OIDCTokenResponse tokenResponse = (OIDCTokenResponse) OIDCTokenResponseParser.parse(tokenReq.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 = 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()); + + if (!userInfoResponse.indicatesSuccess()) { + System.out.println(userInfoResponse.toErrorResponse().getErrorObject().getCode()); + System.out.println(userInfoResponse.toErrorResponse().getErrorObject().getDescription()); + return HttpResponse.ofCode(500).withHtml( + "The userinfo endpoint returned an error. Please try again or contact your oidc admin\n\n" + + userInfoResponse.toErrorResponse().getErrorObject().getDescription()); + } + + UserInfo userInfo = userInfoResponse.toSuccessResponse().getUserInfo(); + + + String uid = userInfo.getSubject().toString(); + String sessionId; try (Session s = DatabaseSessionFactory.createSession()) { // TODO: Add oidc provider to database String dbName = provider + "-" + uid; @@ -141,11 +222,66 @@ public static String oidcCallbackResponse(String provider, String uid) { tr.commit(); - return newuser.getSessionId(); - } - return dbuser.getSessionId(); + sessionId = newuser.getSessionId(); + } else sessionId = dbuser.getSessionId(); + } + return HttpResponse.redirect302(data.data + "?session=" + sessionId); + + } + + public static HttpResponse oidcDeleteResponse(OidcProvider provider, URI requestUri) throws Exception { + ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret); + + AuthenticationSuccessResponse sr = parseOidcUri(requestUri); + + 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]; + + URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/delete"); + AuthorizationCode code = sr.getAuthorizationCode(); + AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback); + + + TokenRequest tokenRequest = new TokenRequest(provider.tokenUri, clientAuth, codeGrant); + TokenResponse tokenResponse = OIDCTokenResponseParser.parse(tokenRequest.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." + ); + } + + try (Session s = DatabaseSessionFactory.createSession()) { + + var tr = s.beginTransaction(); + s.remove(DatabaseHelper.getUserFromSession(session)); + tr.commit(); + } + return HttpResponse.redirect302(Constants.FRONTEND_URL + "/preferences?deleted=" + session); } public static byte[] deleteUserResponse(String session, String pass) throws IOException { @@ -187,17 +323,6 @@ public static byte[] deleteUserResponse(String session, String pass) throws IOEx } } - 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 { @@ -217,4 +342,14 @@ public static byte[] logoutResponse(String session) throws JsonProcessingExcepti return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse()); } + + private static AuthenticationSuccessResponse parseOidcUri(URI uri) throws Exception { + AuthenticationResponse response = AuthenticationResponseParser.parse(uri); + + if (response instanceof AuthenticationErrorResponse) { + System.err.println(response.toErrorResponse().getErrorObject()); + throw new Exception(response.toErrorResponse().getErrorObject().toString()); + } + return response.toSuccessResponse(); + } } From 024435ff54024d793cd2861196950a475c9eb41c Mon Sep 17 00:00:00 2001 From: Kavin <20838718+FireMasterK@users.noreply.github.com> Date: Thu, 26 Oct 2023 17:39:25 +0100 Subject: [PATCH 11/13] Add database migration for username length change. --- .../resources/changelog/db.changelog-master.xml | 1 + src/main/resources/changelog/version/2-oidc.xml | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 src/main/resources/changelog/version/2-oidc.xml diff --git a/src/main/resources/changelog/db.changelog-master.xml b/src/main/resources/changelog/db.changelog-master.xml index 68fb5f2b..b8b5e3fe 100644 --- a/src/main/resources/changelog/db.changelog-master.xml +++ b/src/main/resources/changelog/db.changelog-master.xml @@ -6,5 +6,6 @@ + diff --git a/src/main/resources/changelog/version/2-oidc.xml b/src/main/resources/changelog/version/2-oidc.xml new file mode 100644 index 00000000..817e29c1 --- /dev/null +++ b/src/main/resources/changelog/version/2-oidc.xml @@ -0,0 +1,15 @@ + + + + + + ALTER TABLE users ALTER COLUMN username TYPE varchar(32); + + ALTER TABLE users ALTER COLUMN username TYPE varchar(24); + + + + From 470efd80683529deefc5d120aa3a65b5a40dfa54 Mon Sep 17 00:00:00 2001 From: Jeidnx Date: Sun, 29 Oct 2023 20:01:24 +0100 Subject: [PATCH 12/13] Revert "Add database migration for username length change." This reverts commit 024435ff54024d793cd2861196950a475c9eb41c. --- .../resources/changelog/db.changelog-master.xml | 1 - src/main/resources/changelog/version/2-oidc.xml | 15 --------------- 2 files changed, 16 deletions(-) delete mode 100644 src/main/resources/changelog/version/2-oidc.xml diff --git a/src/main/resources/changelog/db.changelog-master.xml b/src/main/resources/changelog/db.changelog-master.xml index b8b5e3fe..68fb5f2b 100644 --- a/src/main/resources/changelog/db.changelog-master.xml +++ b/src/main/resources/changelog/db.changelog-master.xml @@ -6,6 +6,5 @@ - diff --git a/src/main/resources/changelog/version/2-oidc.xml b/src/main/resources/changelog/version/2-oidc.xml deleted file mode 100644 index 817e29c1..00000000 --- a/src/main/resources/changelog/version/2-oidc.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - ALTER TABLE users ALTER COLUMN username TYPE varchar(32); - - ALTER TABLE users ALTER COLUMN username TYPE varchar(24); - - - - From 5f6a83aa735040b942fbd4e352731048237a7868 Mon Sep 17 00:00:00 2001 From: Kavin <20838718+FireMasterK@users.noreply.github.com> Date: Sun, 29 Oct 2023 21:26:42 +0000 Subject: [PATCH 13/13] Add code from the meeting. --- .../server/handlers/auth/UserHandlers.java | 12 +++++++++++- .../piped/utils/obj/db/OidcUserData.java | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/main/java/me/kavin/piped/utils/obj/db/OidcUserData.java diff --git a/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java b/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java index 40df4df6..7fb56ca1 100644 --- a/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java +++ b/src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java @@ -19,6 +19,7 @@ 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.OidcUserData; import me.kavin.piped.utils.obj.db.User; import me.kavin.piped.utils.resp.*; import org.apache.commons.codec.digest.DigestUtils; @@ -297,6 +298,14 @@ public static byte[] deleteUserResponse(String session, String pass) throws IOEx String hash = user.getPassword(); if (hash.isEmpty()) { + + CriteriaBuilder cb = s.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery(OidcUserData.class); + Root root = cr.from(OidcUserData.class); + cr.select(root).where(cb.equal(root.get("user"), user.getId())); + + OidcUserData oidcUserData = s.createQuery(cr).uniqueResult(); + //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)); @@ -310,7 +319,8 @@ public static byte[] deleteUserResponse(String session, String pass) throws IOEx .state(new State(state)).nonce(data.nonce).maxAge(0).build(); - return String.format("{\"redirect\": \"%s\"}", oidcRequest.toURI().toString()).getBytes(); + return mapper.writeValueAsBytes(mapper.createObjectNode() + .put("redirect", oidcRequest.toURI().toString())); } if (!hashMatch(hash, pass)) ExceptionHandler.throwErrorResponse(new IncorrectCredentialsResponse()); diff --git a/src/main/java/me/kavin/piped/utils/obj/db/OidcUserData.java b/src/main/java/me/kavin/piped/utils/obj/db/OidcUserData.java new file mode 100644 index 00000000..e23d6216 --- /dev/null +++ b/src/main/java/me/kavin/piped/utils/obj/db/OidcUserData.java @@ -0,0 +1,19 @@ +package me.kavin.piped.utils.obj.db; + +import jakarta.persistence.*; + +@Entity +@Table(name = "oidc_user_data") +public class OidcUserData { + + @Column(unique = true) + @Id + private String sub; + + @OneToOne + private User user; + + private String provider; + + +}