From 604fa651fcc14f893c96b8280928dca3fd9438e7 Mon Sep 17 00:00:00 2001 From: Jeidnx Date: Sun, 18 Jun 2023 18:28:18 +0200 Subject: [PATCH 01/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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; + + +} From 074e4bc136e9040352ca15d5f17386d27ad75305 Mon Sep 17 00:00:00 2001 From: Jeidnx Date: Tue, 12 Nov 2024 14:59:22 +0100 Subject: [PATCH 14/21] chore: properly implement oidc --- build.gradle | 2 +- config.properties | 14 +- .../java/me/kavin/piped/consts/Constants.java | 8 +- .../me/kavin/piped/server/ServerLauncher.java | 9 +- .../server/handlers/auth/UserHandlers.java | 192 +++++++++++------- .../piped/utils/DatabaseSessionFactory.java | 2 +- .../me/kavin/piped/utils/obj/OidcData.java | 13 +- .../kavin/piped/utils/obj/OidcProvider.java | 29 ++- .../piped/utils/obj/db/OidcUserData.java | 42 +++- .../me/kavin/piped/utils/obj/db/User.java | 2 +- .../changelog/db.changelog-master.xml | 1 + .../changelog/version/2-add-oidc.xml | 21 ++ 12 files changed, 228 insertions(+), 107 deletions(-) create mode 100644 src/main/resources/changelog/version/2-add-oidc.xml diff --git a/build.gradle b/build.gradle index 2fb8183c..32b1a006 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ dependencies { implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1' implementation 'com.github.FireMasterK.NewPipeExtractor:NewPipeExtractor:a64e202bb498032e817a702145263590829f3c1d' implementation 'com.github.FireMasterK:nanojson:9f4af3b739cc13f3d0d9d4b758bbe2b2ae7119d7' - implementation 'com.nimbusds:oauth2-oidc-sdk:11.5' + implementation 'com.nimbusds:oauth2-oidc-sdk:11.20.1' implementation 'com.fasterxml.jackson.core:jackson-core:2.17.2' implementation 'com.fasterxml.jackson.core:jackson-annotations:2.17.2' implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2' diff --git a/config.properties b/config.properties index 2821e3f3..cde7ff12 100644 --- a/config.properties +++ b/config.properties @@ -90,10 +90,10 @@ hibernate.connection.password:changeme #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.authUri:INSERT_HERE -#oidc.provider.INSERT_HERE.tokenUri:INSERT_HERE -#oidc.provider.INSERT_HERE.userinfoUri:INSERT_HERE +# SSO via OIDC +# each provider needs to have these three options specified. is the +# friendly name which will be shown to the clients and used in the database. +# If you want to change the name later, you will have to update the database. +# oidc.provider..clientId: +# oidc.provider..clientSecret: +# oidc.provider..issuer: diff --git a/src/main/java/me/kavin/piped/consts/Constants.java b/src/main/java/me/kavin/piped/consts/Constants.java index e4d0920b..49128f07 100644 --- a/src/main/java/me/kavin/piped/consts/Constants.java +++ b/src/main/java/me/kavin/piped/consts/Constants.java @@ -196,17 +196,13 @@ else if (key.startsWith("oidc.provider")) { } }); oidcProviderConfig.forEach((provider, config) -> { - ObjectNode providerNode = frontendProperties.putObject(provider); OIDC_PROVIDERS.add(new OidcProvider( - getRequiredMapValue(config, "name"), + provider, getRequiredMapValue(config, "clientId"), getRequiredMapValue(config, "clientSecret"), - getRequiredMapValue(config, "authUri"), - getRequiredMapValue(config, "tokenUri"), - getRequiredMapValue(config, "userinfoUri") + getRequiredMapValue(config, "issuer") )); providerNames.add(provider); - config.forEach(providerNode::put); }); 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 41f015be..f70c9798 100644 --- a/src/main/java/me/kavin/piped/server/ServerLauncher.java +++ b/src/main/java/me/kavin/piped/server/ServerLauncher.java @@ -274,7 +274,7 @@ AsyncServlet mainServlet(Executor executor) { 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())); + case "delete" -> UserHandlers.oidcDeleteCallback(provider, URI.create(request.getFullUrl())); default -> HttpResponse.ofCode(500).withHtml("Invalid function `" + function + "`"); }; } catch (Exception e) { @@ -491,6 +491,13 @@ AsyncServlet mainServlet(Executor executor) { } catch (Exception e) { return getErrorResponse(e, request.getPath()); } + })).map(GET, "/user/delete", AsyncServlet.ofBlocking(executor, request -> { + try { + var session = request.getQueryParameter("session"); + return UserHandlers.oidcDeleteRequest(session); + } catch (Exception e) { + return getErrorResponse(e, request.getPath()); + } })).map(POST, "/logout", AsyncServlet.ofBlocking(executor, request -> { try { return getJsonResponse(UserHandlers.logoutResponse(request.getHeader(AUTHORIZATION)), "private"); 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 7fb56ca1..3cfe3ee3 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,12 +1,18 @@ package me.kavin.piped.server.handlers.auth; import com.fasterxml.jackson.core.JsonProcessingException; -import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; 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.oauth2.sdk.pkce.CodeChallengeMethod; +import com.nimbusds.oauth2.sdk.pkce.CodeVerifier; import com.nimbusds.openid.connect.sdk.*; +import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet; import com.nimbusds.openid.connect.sdk.claims.UserInfo; import io.activej.http.HttpResponse; import jakarta.persistence.criteria.CriteriaBuilder; @@ -131,7 +137,8 @@ public static HttpResponse oidcLoginResponse(OidcProvider provider, String redir } URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback"); - OidcData data = new OidcData(redirectUri); + CodeVerifier codeVerifier = new CodeVerifier(); + OidcData data = new OidcData(redirectUri, codeVerifier); String state = data.getState(); PENDING_OIDC.put(state, data); @@ -139,8 +146,11 @@ public static HttpResponse oidcLoginResponse(OidcProvider provider, String redir 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(); + provider.clientID, callback) + .endpointURI(provider.authUri) + .codeChallenge(codeVerifier, CodeChallengeMethod.S256) + .state(new State(state)) + .nonce(data.nonce).build(); if (redirectUri.equals(Constants.FRONTEND_URL + "/login")) { return HttpResponse.redirect302(oidcRequest.toURI().toString()); @@ -155,11 +165,9 @@ public static HttpResponse oidcLoginResponse(OidcProvider provider, String redir "\">here"); } public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI requestUri) throws Exception { - ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret); - - AuthenticationSuccessResponse sr = parseOidcUri(requestUri); + AuthenticationSuccessResponse authResponse = parseOidcUri(requestUri); - OidcData data = PENDING_OIDC.get(sr.getState().toString()); + OidcData data = PENDING_OIDC.get(authResponse.getState().toString()); if (data == null) { return HttpResponse.ofCode(400).withHtml( "Your oidc provider sent invalid state data. Try again or contact your oidc admin" @@ -167,12 +175,15 @@ public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI reque } URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback"); - AuthorizationCode code = sr.getAuthorizationCode(); - AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback); + AuthorizationCode code = authResponse.getAuthorizationCode(); + + AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, data.pkceVerifier); + ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret); + TokenRequest tokenReq = new TokenRequest.Builder(provider.tokenUri, clientAuth, codeGrant).build(); - TokenRequest tokenReq = new TokenRequest(provider.tokenUri, clientAuth, codeGrant); - OIDCTokenResponse tokenResponse = (OIDCTokenResponse) OIDCTokenResponseParser.parse(tokenReq.toHTTPRequest().send()); + com.nimbusds.oauth2.sdk.http.HTTPResponse tokenResponseText = tokenReq.toHTTPRequest().send(); + OIDCTokenResponse tokenResponse = (OIDCTokenResponse) OIDCTokenResponseParser.parse(tokenResponseText); if (!tokenResponse.indicatesSuccess()) { TokenErrorResponse errorResponse = tokenResponse.toErrorResponse(); @@ -181,11 +192,17 @@ public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI reque 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" - ); - } + JWT idToken = JWTParser.parse(successResponse.getOIDCTokens().getIDTokenString()); + + try { + provider.validator.validate(idToken, data.nonce); + } catch (BadJOSEException e) { + System.out.println("Invalid token received: " + e.toString()); + return HttpResponse.ofCode(400).withHtml("Received a bad token. Please try again"); + } catch (JOSEException e) { + System.out.println("Token processing error" + e.toString()); + return HttpResponse.ofCode(500).withHtml("Internal processing error. Please try again"); + } UserInfoRequest ur = new UserInfoRequest(provider.userinfoUri, successResponse.getOIDCTokens().getBearerAccessToken()); UserInfoResponse userInfoResponse = UserInfoResponse.parse(ur.toHTTPRequest().send()); @@ -200,38 +217,86 @@ public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI reque UserInfo userInfo = userInfoResponse.toSuccessResponse().getUserInfo(); - - String uid = userInfo.getSubject().toString(); + String sub = userInfo.getSubject().toString(); String sessionId; 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); - Root root = cr.from(User.class); - cr.select(root).where(root.get("username").in( - dbName - )); + CriteriaQuery cr = cb.createQuery(OidcUserData.class); + Root root = cr.from(OidcUserData.class); - User dbuser = s.createQuery(cr).uniqueResult(); + cr.select(root).where(root.get("sub").in(sub)); - if (dbuser == null) { - User newuser = new User(dbName, "", Set.of()); + OidcUserData dbuser = s.createQuery(cr).uniqueResult(); + + if (dbuser != null) { + sessionId = dbuser.getUser().getSessionId(); + } else { + String username = userInfo.getPreferredUsername(); + OidcUserData newUser = new OidcUserData(sub, username, provider.name); var tr = s.beginTransaction(); - s.persist(newuser); + s.persist(newUser); tr.commit(); - - sessionId = newuser.getSessionId(); - } else sessionId = dbuser.getSessionId(); + sessionId = newUser.getUser().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); + public static HttpResponse oidcDeleteRequest(String session) throws Exception { + + if (StringUtils.isBlank(session)) { + return HttpResponse.ofCode(400).withHtml("session is a required parameter"); + } + + OidcProvider provider = null; + try (Session s = DatabaseSessionFactory.createSession()) { + + User user = DatabaseHelper.getUserFromSession(session); + + if (user == null) { + return HttpResponse.ofCode(400).withHtml("User not found"); + } + + 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)); + + OidcUserData oidcUserData = s.createQuery(cr).uniqueResult(); + + for (OidcProvider test: Constants.OIDC_PROVIDERS) { + if (test.name.equals(oidcUserData.getProvider())) { + provider = test; + } + } + } + + if (provider == null) { + return HttpResponse.ofCode(400).withHtml("Invalid user"); + } + CodeVerifier pkceVerifier = new CodeVerifier(); + + URI callback = URI.create(String.format("%s/oidc/%s/delete", Constants.PUBLIC_URL, provider.name)); + OidcData data = new OidcData(session + "|" + Instant.now().getEpochSecond(), pkceVerifier); + 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) + .codeChallenge(pkceVerifier, CodeChallengeMethod.S256) + .state(new State(state)) + .nonce(data.nonce) + // This parameter is optional and the idp does't have to honor it. + .maxAge(0) + .build(); + + return HttpResponse.redirect302(oidcRequest.toURI().toString()); + } + public static HttpResponse oidcDeleteCallback(OidcProvider provider, URI requestUri) throws Exception { AuthenticationSuccessResponse sr = parseOidcUri(requestUri); @@ -247,10 +312,11 @@ public static HttpResponse oidcDeleteResponse(OidcProvider provider, URI request URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/delete"); AuthorizationCode code = sr.getAuthorizationCode(); - AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback); + AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, data.pkceVerifier); + ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret); - TokenRequest tokenRequest = new TokenRequest(provider.tokenUri, clientAuth, codeGrant); + TokenRequest tokenRequest = new TokenRequest.Builder(provider.tokenUri, clientAuth, codeGrant).build(); TokenResponse tokenResponse = OIDCTokenResponseParser.parse(tokenRequest.toHTTPRequest().send()); if (!tokenResponse.indicatesSuccess()) { @@ -260,15 +326,24 @@ public static HttpResponse oidcDeleteResponse(OidcProvider provider, URI request OIDCTokenResponse successResponse = (OIDCTokenResponse) tokenResponse.toSuccessResponse(); - JWTClaimsSet claims = successResponse.getOIDCTokens().getIDToken().getJWTClaimsSet(); + JWT idToken = JWTParser.parse(successResponse.getOIDCTokens().getIDTokenString()); + + IDTokenClaimsSet claims; + try { + claims = provider.validator.validate(idToken, data.nonce); + } catch (BadJOSEException e) { + System.out.println("Invalid token received: " + e.toString()); + return HttpResponse.ofCode(400).withHtml("Received a bad token. Please try again"); + } catch (JOSEException e) { + System.out.println("Token processing error" + e.toString()); + return HttpResponse.ofCode(500).withHtml("Internal processing error. Please try again"); + } - 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.getNumberClaim("auth_time"); - long authTime = (long) claims.getClaim("auth_time"); + if (authTime == null) { + return HttpResponse.ofCode(400).withHtml("Couldn't get the `auth_time` claim from the provided id token"); + } if (authTime < start) { return HttpResponse.ofCode(500).withHtml( @@ -277,7 +352,6 @@ public static HttpResponse oidcDeleteResponse(OidcProvider provider, URI request } try (Session s = DatabaseSessionFactory.createSession()) { - var tr = s.beginTransaction(); s.remove(DatabaseHelper.getUserFromSession(session)); tr.commit(); @@ -297,31 +371,6 @@ 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)); - 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 mapper.writeValueAsBytes(mapper.createObjectNode() - .put("redirect", oidcRequest.toURI().toString())); - } if (!hashMatch(hash, pass)) ExceptionHandler.throwErrorResponse(new IncorrectCredentialsResponse()); @@ -333,7 +382,6 @@ public static byte[] deleteUserResponse(String session, String pass) throws IOEx } } - public static byte[] logoutResponse(String session) throws JsonProcessingException { if (StringUtils.isBlank(session)) diff --git a/src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java b/src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java index edc60892..9edade81 100644 --- a/src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java +++ b/src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java @@ -20,7 +20,7 @@ public class DatabaseSessionFactory { sessionFactory = configuration.addAnnotatedClass(User.class).addAnnotatedClass(Channel.class) .addAnnotatedClass(Video.class).addAnnotatedClass(PubSub.class).addAnnotatedClass(Playlist.class) - .addAnnotatedClass(PlaylistVideo.class).addAnnotatedClass(UnauthenticatedSubscription.class).buildSessionFactory(); + .addAnnotatedClass(PlaylistVideo.class).addAnnotatedClass(UnauthenticatedSubscription.class).addAnnotatedClass(OidcUserData.class).buildSessionFactory(); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/src/main/java/me/kavin/piped/utils/obj/OidcData.java b/src/main/java/me/kavin/piped/utils/obj/OidcData.java index f1bfd6c0..62758762 100644 --- a/src/main/java/me/kavin/piped/utils/obj/OidcData.java +++ b/src/main/java/me/kavin/piped/utils/obj/OidcData.java @@ -1,6 +1,8 @@ package me.kavin.piped.utils.obj; +import com.nimbusds.oauth2.sdk.pkce.CodeVerifier; import com.nimbusds.openid.connect.sdk.Nonce; +import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; @@ -9,16 +11,17 @@ public class OidcData { public final Nonce nonce; + public final CodeVerifier pkceVerifier; + public final String data; - public String data; - - public OidcData(String data) { + public OidcData(String data, CodeVerifier pkceVerifier) { this.nonce = new Nonce(); + this.pkceVerifier = pkceVerifier; this.data = data; } - public boolean isInvalidNonce(String nonce) { - return !nonce.equals(this.nonce.toString()); + public boolean validateNonce(String nonce) { + return this.nonce.toString().equals(nonce); } public String getState() { 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 aedce630..3a1859cc 100644 --- a/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java +++ b/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java @@ -2,28 +2,35 @@ import com.nimbusds.oauth2.sdk.auth.Secret; import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.id.Issuer; +import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; +import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator; import java.net.URI; -import java.net.URISyntaxException; public class OidcProvider { - public String name; - public ClientID clientID; - public Secret clientSecret; + public final String name; + public final ClientID clientID; + public final Secret clientSecret; public URI authUri; public URI tokenUri; public URI userinfoUri; + public IDTokenValidator validator; - public OidcProvider(String name, String clientID, String clientSecret, String authUri, String tokenUri, String userinfoUri) { + public OidcProvider(String name, String clientId, String clientSecret, String issuer){ this.name = name; - this.clientID = new ClientID(clientID); + this.clientID = new ClientID(clientId); this.clientSecret = new Secret(clientSecret); + 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."); + Issuer iss = new Issuer(issuer); + OIDCProviderMetadata providerData = OIDCProviderMetadata.resolve(iss); + this.authUri = providerData.getAuthorizationEndpointURI(); + this.tokenUri = providerData.getTokenEndpointURI(); + this.userinfoUri = providerData.getUserInfoEndpointURI(); + this.validator = new IDTokenValidator(iss, this.clientID, providerData.getIDTokenJWSAlgs().getFirst(), providerData.getJWKSetURI().toURL()); + } catch (Exception e ) { + System.err.println("Failed to get configuration for '" + name + "': " + e); System.exit(1); } } 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 index e23d6216..748e7d02 100644 --- a/src/main/java/me/kavin/piped/utils/obj/db/OidcUserData.java +++ b/src/main/java/me/kavin/piped/utils/obj/db/OidcUserData.java @@ -1,19 +1,57 @@ package me.kavin.piped.utils.obj.db; +import java.util.Set; + +import org.hibernate.annotations.Cascade; + +import java.io.Serializable; + import jakarta.persistence.*; @Entity @Table(name = "oidc_user_data") -public class OidcUserData { +public class OidcUserData implements Serializable { + + public OidcUserData() { + } - @Column(unique = true) + public OidcUserData(String sub, String username, String provider) { + this.sub = sub; + this.provider = provider; + this.user = new User(username,"", Set.of()); + } + + @Column(name = "sub", unique = true, length = 255) @Id private String sub; @OneToOne + @Cascade(org.hibernate.annotations.CascadeType.ALL) private User user; + @Column(name = "provider", nullable = false) private String provider; + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + public String getSub() { + return sub; + } + + public void setSub(String sub) { + this.sub = sub; + } + + public String getProvider() { + return provider; + } + public void setProvider(String provider) { + this.provider = provider; + } } 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 4a3e343c..2b8f603f 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 @@ -21,7 +21,7 @@ public class User implements Serializable { @Column(name = "id") private long id; - @Column(name = "username", unique = true, length = 32) + @Column(name = "username", unique = true, length = 24) private String username; @Column(name = "password", columnDefinition = "text") diff --git a/src/main/resources/changelog/db.changelog-master.xml b/src/main/resources/changelog/db.changelog-master.xml index 4d3a056a..40184931 100644 --- a/src/main/resources/changelog/db.changelog-master.xml +++ b/src/main/resources/changelog/db.changelog-master.xml @@ -6,4 +6,5 @@ + diff --git a/src/main/resources/changelog/version/2-add-oidc.xml b/src/main/resources/changelog/version/2-add-oidc.xml new file mode 100644 index 00000000..a968d23d --- /dev/null +++ b/src/main/resources/changelog/version/2-add-oidc.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + From 580eb7fbbe74f5580da83eb1e67aee101a14bd81 Mon Sep 17 00:00:00 2001 From: Kavin <20838718+FireMasterK@users.noreply.github.com> Date: Mon, 18 Nov 2024 00:23:24 +0530 Subject: [PATCH 15/21] Simplify oidc hash generation. --- .../me/kavin/piped/utils/obj/OidcData.java | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/main/java/me/kavin/piped/utils/obj/OidcData.java b/src/main/java/me/kavin/piped/utils/obj/OidcData.java index 62758762..1db4ed42 100644 --- a/src/main/java/me/kavin/piped/utils/obj/OidcData.java +++ b/src/main/java/me/kavin/piped/utils/obj/OidcData.java @@ -2,14 +2,11 @@ import com.nimbusds.oauth2.sdk.pkce.CodeVerifier; import com.nimbusds.openid.connect.sdk.Nonce; -import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.digest.DigestUtils; public class OidcData { + public final Nonce nonce; public final CodeVerifier pkceVerifier; public final String data; @@ -21,19 +18,13 @@ public OidcData(String data, CodeVerifier pkceVerifier) { } public boolean validateNonce(String nonce) { - return this.nonce.toString().equals(nonce); + return this.nonce.getValue().equals(nonce); } 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); - } + byte[] hash = DigestUtils.sha256(value); + return Base64.encodeBase64String(hash); } } From 9520a3cc6f1f49fa6d046d4dae7a08ce0c698165 Mon Sep 17 00:00:00 2001 From: Kavin <20838718+FireMasterK@users.noreply.github.com> Date: Mon, 18 Nov 2024 01:39:50 +0530 Subject: [PATCH 16/21] Simplify error handling code a little. --- .../java/me/kavin/piped/consts/Constants.java | 22 ++++++++++------ .../kavin/piped/utils/obj/OidcProvider.java | 26 +++++++++---------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/main/java/me/kavin/piped/consts/Constants.java b/src/main/java/me/kavin/piped/consts/Constants.java index 49128f07..87fe2160 100644 --- a/src/main/java/me/kavin/piped/consts/Constants.java +++ b/src/main/java/me/kavin/piped/consts/Constants.java @@ -6,11 +6,11 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.nimbusds.oauth2.sdk.GeneralException; 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; import me.kavin.piped.utils.resp.ListLinkHandlerMixin; import okhttp3.OkHttpClient; @@ -25,9 +25,10 @@ import java.io.File; import java.io.FileReader; -import java.util.Map; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Properties; @@ -196,12 +197,17 @@ else if (key.startsWith("oidc.provider")) { } }); oidcProviderConfig.forEach((provider, config) -> { - OIDC_PROVIDERS.add(new OidcProvider( - provider, - getRequiredMapValue(config, "clientId"), - getRequiredMapValue(config, "clientSecret"), - getRequiredMapValue(config, "issuer") - )); + try { + OIDC_PROVIDERS.add(new OidcProvider( + provider, + getRequiredMapValue(config, "clientId"), + getRequiredMapValue(config, "clientSecret"), + getRequiredMapValue(config, "issuer") + )); + } catch (GeneralException | IOException e) { + System.err.println("Failed to get configuration for '" + provider + "': " + e); + System.exit(1); + } providerNames.add(provider); }); frontendProperties.put("imageProxyUrl", IMAGE_PROXY_PART); 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 3a1859cc..54e1af5f 100644 --- a/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java +++ b/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java @@ -1,37 +1,35 @@ package me.kavin.piped.utils.obj; +import com.nimbusds.oauth2.sdk.GeneralException; import com.nimbusds.oauth2.sdk.auth.Secret; import com.nimbusds.oauth2.sdk.id.ClientID; import com.nimbusds.oauth2.sdk.id.Issuer; import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator; +import java.io.IOException; import java.net.URI; public class OidcProvider { + public final String name; public final ClientID clientID; public final Secret clientSecret; - public URI authUri; - public URI tokenUri; + public final URI authUri; + public final URI tokenUri; public URI userinfoUri; public IDTokenValidator validator; - public OidcProvider(String name, String clientId, String clientSecret, String issuer){ + public OidcProvider(String name, String clientId, String clientSecret, String issuer) throws GeneralException, IOException { this.name = name; this.clientID = new ClientID(clientId); this.clientSecret = new Secret(clientSecret); - try { - Issuer iss = new Issuer(issuer); - OIDCProviderMetadata providerData = OIDCProviderMetadata.resolve(iss); - this.authUri = providerData.getAuthorizationEndpointURI(); - this.tokenUri = providerData.getTokenEndpointURI(); - this.userinfoUri = providerData.getUserInfoEndpointURI(); - this.validator = new IDTokenValidator(iss, this.clientID, providerData.getIDTokenJWSAlgs().getFirst(), providerData.getJWKSetURI().toURL()); - } catch (Exception e ) { - System.err.println("Failed to get configuration for '" + name + "': " + e); - System.exit(1); - } + Issuer iss = new Issuer(issuer); + OIDCProviderMetadata providerData = OIDCProviderMetadata.resolve(iss); + this.authUri = providerData.getAuthorizationEndpointURI(); + this.tokenUri = providerData.getTokenEndpointURI(); + this.userinfoUri = providerData.getUserInfoEndpointURI(); + this.validator = new IDTokenValidator(iss, this.clientID, providerData.getIDTokenJWSAlgs().getFirst(), providerData.getJWKSetURI().toURL()); } } From b0725f882a2bc02695bc885b250b7d3a08549aa5 Mon Sep 17 00:00:00 2001 From: Kavin <20838718+FireMasterK@users.noreply.github.com> Date: Mon, 18 Nov 2024 01:40:24 +0530 Subject: [PATCH 17/21] Remove debug code and format. --- .../server/handlers/auth/UserHandlers.java | 112 ++++++++++-------- 1 file changed, 61 insertions(+), 51 deletions(-) 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 3cfe3ee3..e0045d4c 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 @@ -131,7 +131,7 @@ public static byte[] loginResponse(String user, String pass) } } - public static HttpResponse oidcLoginResponse(OidcProvider provider, String redirectUri) throws Exception{ + public static HttpResponse oidcLoginResponse(OidcProvider provider, String redirectUri) throws Exception { if (StringUtils.isBlank(redirectUri)) { return HttpResponse.ofCode(400).withHtml("redirect is a required parameter"); } @@ -146,9 +146,10 @@ public static HttpResponse oidcLoginResponse(OidcProvider provider, String redir AuthenticationRequest oidcRequest = new AuthenticationRequest.Builder( new ResponseType("code"), new Scope("openid"), - provider.clientID, callback) - .endpointURI(provider.authUri) - .codeChallenge(codeVerifier, CodeChallengeMethod.S256) + provider.clientID, callback + ) + .endpointURI(provider.authUri) + .codeChallenge(codeVerifier, CodeChallengeMethod.S256) .state(new State(state)) .nonce(data.nonce).build(); @@ -164,6 +165,7 @@ public static HttpResponse oidcLoginResponse(OidcProvider provider, String redir oidcRequest.toURI().toString() + "\">here"); } + public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI requestUri) throws Exception { AuthenticationSuccessResponse authResponse = parseOidcUri(requestUri); @@ -196,20 +198,18 @@ public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI reque try { provider.validator.validate(idToken, data.nonce); - } catch (BadJOSEException e) { - System.out.println("Invalid token received: " + e.toString()); - return HttpResponse.ofCode(400).withHtml("Received a bad token. Please try again"); - } catch (JOSEException e) { - System.out.println("Token processing error" + e.toString()); - return HttpResponse.ofCode(500).withHtml("Internal processing error. Please try again"); - } + } catch (BadJOSEException e) { + System.err.println("Invalid token received: " + e); + return HttpResponse.ofCode(400).withHtml("Received a bad token. Please try again"); + } catch (JOSEException e) { + System.err.println("Token processing error: " + e); + return HttpResponse.ofCode(500).withHtml("Internal processing error. Please try again"); + } 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()); @@ -245,19 +245,19 @@ public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI reque } public static HttpResponse oidcDeleteRequest(String session) throws Exception { - + if (StringUtils.isBlank(session)) { return HttpResponse.ofCode(400).withHtml("session is a required parameter"); } OidcProvider provider = null; - try (Session s = DatabaseSessionFactory.createSession()) { - User user = DatabaseHelper.getUserFromSession(session); + try (Session s = DatabaseSessionFactory.createSession()) { + User user = DatabaseHelper.getUserFromSession(session); - if (user == null) { - return HttpResponse.ofCode(400).withHtml("User not found"); - } + if (user == null) { + return HttpResponse.ofCode(400).withHtml("User not found"); + } CriteriaBuilder cb = s.getCriteriaBuilder(); CriteriaQuery cr = cb.createQuery(OidcUserData.class); @@ -266,41 +266,50 @@ public static HttpResponse oidcDeleteRequest(String session) throws Exception { OidcUserData oidcUserData = s.createQuery(cr).uniqueResult(); - for (OidcProvider test: Constants.OIDC_PROVIDERS) { - if (test.name.equals(oidcUserData.getProvider())) { - provider = test; - } + if (oidcUserData == null) { + return HttpResponse.ofCode(400).withHtml("User doesn't have an oidc account"); + } + + for (OidcProvider oidcProvider : Constants.OIDC_PROVIDERS) { + if (oidcProvider.name.equals(oidcUserData.getProvider())) { + provider = oidcProvider; + break; + } } - } + } - if (provider == null) { + if (provider == null) { return HttpResponse.ofCode(400).withHtml("Invalid user"); - } - CodeVerifier pkceVerifier = new CodeVerifier(); - - URI callback = URI.create(String.format("%s/oidc/%s/delete", Constants.PUBLIC_URL, provider.name)); - OidcData data = new OidcData(session + "|" + Instant.now().getEpochSecond(), pkceVerifier); - 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) - .codeChallenge(pkceVerifier, CodeChallengeMethod.S256) - .state(new State(state)) - .nonce(data.nonce) - // This parameter is optional and the idp does't have to honor it. - .maxAge(0) - .build(); + } + + CodeVerifier pkceVerifier = new CodeVerifier(); + + URI callback = URI.create(String.format("%s/oidc/%s/delete", Constants.PUBLIC_URL, provider.name)); + OidcData data = new OidcData(session + "|" + Instant.now().getEpochSecond(), pkceVerifier); + 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) + .codeChallenge(pkceVerifier, CodeChallengeMethod.S256) + .state(new State(state)) + .nonce(data.nonce) + // This parameter is optional and the idp doesn't have to honor it. + .maxAge(0) + .build(); return HttpResponse.redirect302(oidcRequest.toURI().toString()); } + public static HttpResponse oidcDeleteCallback(OidcProvider provider, URI requestUri) throws Exception { AuthenticationSuccessResponse sr = parseOidcUri(requestUri); - OidcData data = UserHandlers.PENDING_OIDC.get(sr.getState().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" @@ -331,13 +340,13 @@ public static HttpResponse oidcDeleteCallback(OidcProvider provider, URI request IDTokenClaimsSet claims; try { claims = provider.validator.validate(idToken, data.nonce); - } catch (BadJOSEException e) { - System.out.println("Invalid token received: " + e.toString()); - return HttpResponse.ofCode(400).withHtml("Received a bad token. Please try again"); - } catch (JOSEException e) { - System.out.println("Token processing error" + e.toString()); - return HttpResponse.ofCode(500).withHtml("Internal processing error. Please try again"); - } + } catch (BadJOSEException e) { + System.err.println("Invalid token received: " + e); + return HttpResponse.ofCode(400).withHtml("Received a bad token. Please try again"); + } catch (JOSEException e) { + System.err.println("Token processing error: " + e); + return HttpResponse.ofCode(500).withHtml("Internal processing error. Please try again"); + } Long authTime = (Long) claims.getNumberClaim("auth_time"); @@ -356,6 +365,7 @@ public static HttpResponse oidcDeleteCallback(OidcProvider provider, URI request s.remove(DatabaseHelper.getUserFromSession(session)); tr.commit(); } + return HttpResponse.redirect302(Constants.FRONTEND_URL + "/preferences?deleted=" + session); } From 74a675136d85d7e1ff7082161a75d35d1bdb0375 Mon Sep 17 00:00:00 2001 From: Jeidnx Date: Wed, 20 Nov 2024 12:20:46 +0100 Subject: [PATCH 18/21] Move OidcData to db + some cleanup --- .../java/me/kavin/piped/consts/Constants.java | 11 ++- .../me/kavin/piped/server/ServerLauncher.java | 3 - .../server/handlers/auth/UserHandlers.java | 65 +++++++++------- .../me/kavin/piped/utils/DatabaseHelper.java | 20 +++++ .../piped/utils/DatabaseSessionFactory.java | 2 +- .../me/kavin/piped/utils/obj/OidcData.java | 30 -------- .../kavin/piped/utils/obj/OidcProvider.java | 4 +- .../me/kavin/piped/utils/obj/db/OidcData.java | 77 +++++++++++++++++++ .../changelog/version/2-add-oidc.xml | 19 ++++- 9 files changed, 167 insertions(+), 64 deletions(-) delete mode 100644 src/main/java/me/kavin/piped/utils/obj/OidcData.java create mode 100644 src/main/java/me/kavin/piped/utils/obj/db/OidcData.java diff --git a/src/main/java/me/kavin/piped/consts/Constants.java b/src/main/java/me/kavin/piped/consts/Constants.java index 87fe2160..0675e160 100644 --- a/src/main/java/me/kavin/piped/consts/Constants.java +++ b/src/main/java/me/kavin/piped/consts/Constants.java @@ -202,7 +202,8 @@ else if (key.startsWith("oidc.provider")) { provider, getRequiredMapValue(config, "clientId"), getRequiredMapValue(config, "clientSecret"), - getRequiredMapValue(config, "issuer") + getRequiredMapValue(config, "issuer"), + getOptionalMapValue(config, "sendMaxAge", "true") )); } catch (GeneralException | IOException e) { System.err.println("Failed to get configuration for '" + provider + "': " + e); @@ -262,4 +263,12 @@ private static String getRequiredMapValue(final Map map, Object key) } return value; } + + private static String getOptionalMapValue(final Map map, Object key, String def) { + String value = map.get(key); + if (StringUtils.isBlank(value)) { + return def; + } + return value; + } } diff --git a/src/main/java/me/kavin/piped/server/ServerLauncher.java b/src/main/java/me/kavin/piped/server/ServerLauncher.java index f70c9798..66bc8655 100644 --- a/src/main/java/me/kavin/piped/server/ServerLauncher.java +++ b/src/main/java/me/kavin/piped/server/ServerLauncher.java @@ -17,10 +17,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.*; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; 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 e0045d4c..9edc87b9 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 @@ -23,8 +23,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.OidcData; import me.kavin.piped.utils.obj.db.OidcUserData; import me.kavin.piped.utils.obj.db.User; import me.kavin.piped.utils.resp.*; @@ -38,8 +38,6 @@ 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; @@ -48,7 +46,6 @@ 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 { @@ -141,7 +138,7 @@ public static HttpResponse oidcLoginResponse(OidcProvider provider, String redir OidcData data = new OidcData(redirectUri, codeVerifier); String state = data.getState(); - PENDING_OIDC.put(state, data); + DatabaseHelper.setOidcData(data); AuthenticationRequest oidcRequest = new AuthenticationRequest.Builder( new ResponseType("code"), @@ -151,7 +148,7 @@ public static HttpResponse oidcLoginResponse(OidcProvider provider, String redir .endpointURI(provider.authUri) .codeChallenge(codeVerifier, CodeChallengeMethod.S256) .state(new State(state)) - .nonce(data.nonce).build(); + .nonce(data.getOidNonce()).build(); if (redirectUri.equals(Constants.FRONTEND_URL + "/login")) { return HttpResponse.redirect302(oidcRequest.toURI().toString()); @@ -169,7 +166,7 @@ public static HttpResponse oidcLoginResponse(OidcProvider provider, String redir public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI requestUri) throws Exception { AuthenticationSuccessResponse authResponse = parseOidcUri(requestUri); - OidcData data = PENDING_OIDC.get(authResponse.getState().toString()); + OidcData data = DatabaseHelper.getOidcData(authResponse.getState().toString()); if (data == null) { return HttpResponse.ofCode(400).withHtml( "Your oidc provider sent invalid state data. Try again or contact your oidc admin" @@ -179,7 +176,7 @@ public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI reque URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback"); AuthorizationCode code = authResponse.getAuthorizationCode(); - AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, data.pkceVerifier); + AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, data.getOidVerifier()); ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret); TokenRequest tokenReq = new TokenRequest.Builder(provider.tokenUri, clientAuth, codeGrant).build(); @@ -197,7 +194,7 @@ public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI reque JWT idToken = JWTParser.parse(successResponse.getOIDCTokens().getIDTokenString()); try { - provider.validator.validate(idToken, data.nonce); + provider.validator.validate(idToken, data.getOidNonce()); } catch (BadJOSEException e) { System.err.println("Invalid token received: " + e); return HttpResponse.ofCode(400).withHtml("Received a bad token. Please try again"); @@ -287,28 +284,31 @@ public static HttpResponse oidcDeleteRequest(String session) throws Exception { URI callback = URI.create(String.format("%s/oidc/%s/delete", Constants.PUBLIC_URL, provider.name)); OidcData data = new OidcData(session + "|" + Instant.now().getEpochSecond(), pkceVerifier); String state = data.getState(); - PENDING_OIDC.put(state, data); - AuthenticationRequest oidcRequest = new AuthenticationRequest.Builder( + DatabaseHelper.setOidcData(data); + + com.nimbusds.openid.connect.sdk.AuthenticationRequest.Builder oidcRequestBuilder = new AuthenticationRequest.Builder( new ResponseType("code"), new Scope("openid"), provider.clientID, callback ) .endpointURI(provider.authUri) .codeChallenge(pkceVerifier, CodeChallengeMethod.S256) .state(new State(state)) - .nonce(data.nonce) + .nonce(data.getOidNonce()); + + if (provider.sendMaxAge) { // This parameter is optional and the idp doesn't have to honor it. - .maxAge(0) - .build(); + oidcRequestBuilder.maxAge(0); + } - return HttpResponse.redirect302(oidcRequest.toURI().toString()); + return HttpResponse.redirect302(oidcRequestBuilder.build().toURI().toString()); } public static HttpResponse oidcDeleteCallback(OidcProvider provider, URI requestUri) throws Exception { AuthenticationSuccessResponse sr = parseOidcUri(requestUri); - OidcData data = PENDING_OIDC.get(sr.getState().toString()); + OidcData data = DatabaseHelper.getOidcData(sr.getState().toString()); if (data == null) { return HttpResponse.ofCode(400).withHtml( @@ -321,7 +321,7 @@ public static HttpResponse oidcDeleteCallback(OidcProvider provider, URI request URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/delete"); AuthorizationCode code = sr.getAuthorizationCode(); - AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, data.pkceVerifier); + AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, data.getOidVerifier()); ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret); @@ -339,7 +339,7 @@ public static HttpResponse oidcDeleteCallback(OidcProvider provider, URI request IDTokenClaimsSet claims; try { - claims = provider.validator.validate(idToken, data.nonce); + claims = provider.validator.validate(idToken, data.getOidNonce()); } catch (BadJOSEException e) { System.err.println("Invalid token received: " + e); return HttpResponse.ofCode(400).withHtml("Received a bad token. Please try again"); @@ -348,21 +348,32 @@ public static HttpResponse oidcDeleteCallback(OidcProvider provider, URI request return HttpResponse.ofCode(500).withHtml("Internal processing error. Please try again"); } - Long authTime = (Long) claims.getNumberClaim("auth_time"); + if (provider.sendMaxAge) { + Long authTime = (Long) claims.getNumberClaim("auth_time"); - if (authTime == null) { - return HttpResponse.ofCode(400).withHtml("Couldn't get the `auth_time` claim from the provided id token"); - } + if (authTime == null) { + return HttpResponse.ofCode(400).withHtml("Couldn't get the `auth_time` claim from the provided id token"); + } - if (authTime < start) { - return HttpResponse.ofCode(500).withHtml( - "Your oidc provider didn't verify your identity. Please try again or contact your oidc admin." - ); + 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)); + + User toDelete = DatabaseHelper.getUserFromSession(session); + + 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"), toDelete)); + + s.remove(s.createQuery(cr).uniqueResult()); tr.commit(); } diff --git a/src/main/java/me/kavin/piped/utils/DatabaseHelper.java b/src/main/java/me/kavin/piped/utils/DatabaseHelper.java index a4af6272..c4bb30a6 100644 --- a/src/main/java/me/kavin/piped/utils/DatabaseHelper.java +++ b/src/main/java/me/kavin/piped/utils/DatabaseHelper.java @@ -8,6 +8,7 @@ import me.kavin.piped.utils.obj.db.*; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; +import org.hibernate.Session; import org.hibernate.SharedSessionContract; import org.hibernate.StatelessSession; import org.schabi.newpipe.extractor.channel.ChannelInfo; @@ -236,4 +237,23 @@ public static Channel saveChannel(String channelId) { return channel; } + + public static void setOidcData(OidcData data) { + try (Session s = DatabaseSessionFactory.createSession()) { + var tr = s.beginTransaction(); + s.persist(data); + tr.commit(); + } + } + + public static OidcData getOidcData(String state) { + try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) { + CriteriaBuilder cb = s.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery(OidcData.class); + Root root = cr.from(OidcData.class); + cr.select(root).where(cb.equal(root.get("state"), state)); + + return s.createQuery(cr).uniqueResult(); + } + } } diff --git a/src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java b/src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java index 9edade81..f333cc2b 100644 --- a/src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java +++ b/src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java @@ -20,7 +20,7 @@ public class DatabaseSessionFactory { sessionFactory = configuration.addAnnotatedClass(User.class).addAnnotatedClass(Channel.class) .addAnnotatedClass(Video.class).addAnnotatedClass(PubSub.class).addAnnotatedClass(Playlist.class) - .addAnnotatedClass(PlaylistVideo.class).addAnnotatedClass(UnauthenticatedSubscription.class).addAnnotatedClass(OidcUserData.class).buildSessionFactory(); + .addAnnotatedClass(PlaylistVideo.class).addAnnotatedClass(UnauthenticatedSubscription.class).addAnnotatedClass(OidcUserData.class).addAnnotatedClass(OidcData.class).buildSessionFactory(); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/src/main/java/me/kavin/piped/utils/obj/OidcData.java b/src/main/java/me/kavin/piped/utils/obj/OidcData.java deleted file mode 100644 index 1db4ed42..00000000 --- a/src/main/java/me/kavin/piped/utils/obj/OidcData.java +++ /dev/null @@ -1,30 +0,0 @@ -package me.kavin.piped.utils.obj; - -import com.nimbusds.oauth2.sdk.pkce.CodeVerifier; -import com.nimbusds.openid.connect.sdk.Nonce; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.codec.digest.DigestUtils; - -public class OidcData { - - public final Nonce nonce; - public final CodeVerifier pkceVerifier; - public final String data; - - public OidcData(String data, CodeVerifier pkceVerifier) { - this.nonce = new Nonce(); - this.pkceVerifier = pkceVerifier; - this.data = data; - } - - public boolean validateNonce(String nonce) { - return this.nonce.getValue().equals(nonce); - } - - public String getState() { - String value = nonce + data; - - byte[] hash = DigestUtils.sha256(value); - return Base64.encodeBase64String(hash); - } -} 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 54e1af5f..89fb38fd 100644 --- a/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java +++ b/src/main/java/me/kavin/piped/utils/obj/OidcProvider.java @@ -17,13 +17,15 @@ public class OidcProvider { public final Secret clientSecret; public final URI authUri; public final URI tokenUri; + public final Boolean sendMaxAge; public URI userinfoUri; public IDTokenValidator validator; - public OidcProvider(String name, String clientId, String clientSecret, String issuer) throws GeneralException, IOException { + public OidcProvider(String name, String clientId, String clientSecret, String issuer, String sendMaxAge) throws GeneralException, IOException { this.name = name; this.clientID = new ClientID(clientId); this.clientSecret = new Secret(clientSecret); + this.sendMaxAge = Boolean.valueOf(sendMaxAge); Issuer iss = new Issuer(issuer); OIDCProviderMetadata providerData = OIDCProviderMetadata.resolve(iss); diff --git a/src/main/java/me/kavin/piped/utils/obj/db/OidcData.java b/src/main/java/me/kavin/piped/utils/obj/db/OidcData.java new file mode 100644 index 00000000..2e9b50c8 --- /dev/null +++ b/src/main/java/me/kavin/piped/utils/obj/db/OidcData.java @@ -0,0 +1,77 @@ +package me.kavin.piped.utils.obj.db; + +import com.nimbusds.oauth2.sdk.pkce.CodeVerifier; +import com.nimbusds.openid.connect.sdk.Nonce; + +import java.io.Serializable; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.digest.DigestUtils; +import jakarta.persistence.*; + +@Entity +@Table(name = "oidc_logins") +public class OidcData implements Serializable { + + @Column(name = "nonce", unique = true, length = 256) + @Id + public String nonce; + + @Column(name = "verifier", length = 128) + public String verifierSecret; + + @Column(name = "data") + public String data; + + @Column(name = "state") + public String state; + + @Column(name = "start") + public long auth_start; + + public OidcData(String data, CodeVerifier pkceVerifier) { + this.nonce = new Nonce().toString(); + this.verifierSecret = pkceVerifier.getValue(); + this.data = data; + this.auth_start = System.currentTimeMillis() / 1000L; + this.state = getState(); + } + + public OidcData() { + } + + public boolean validateNonce(String nonce) { + return this.nonce.equals(nonce); + } + + public String getState() { + String value = this.nonce + this.data; + + byte[] hash = DigestUtils.sha256(value); + return Base64.encodeBase64String(hash); + } + + public Nonce getOidNonce(){ + return new Nonce(this.nonce); + } + + public void setNonce(String nonce) { + this.nonce = nonce; + } + + public CodeVerifier getOidVerifier(){ + return new CodeVerifier(this.verifierSecret); + } + + public void setVerifier(String verifier) { + this.verifierSecret = verifier; + } + + public void setData(String data) { + this.data = data; + } + + public void setState(String state) { + this.state = state; + } +} diff --git a/src/main/resources/changelog/version/2-add-oidc.xml b/src/main/resources/changelog/version/2-add-oidc.xml index a968d23d..b413f18d 100644 --- a/src/main/resources/changelog/version/2-add-oidc.xml +++ b/src/main/resources/changelog/version/2-add-oidc.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> - + @@ -17,5 +17,22 @@ + + + + + + + + + + + + + + + + + From f76f8e0af9aa896e6e112dd3cbe67b63eb7c43bb Mon Sep 17 00:00:00 2001 From: Jeidnx Date: Wed, 20 Nov 2024 13:10:58 +0100 Subject: [PATCH 19/21] randomize username --- config.properties | 14 ++++++++++---- .../java/me/kavin/piped/server/ServerLauncher.java | 4 ++-- .../piped/server/handlers/auth/UserHandlers.java | 8 ++++---- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/config.properties b/config.properties index cde7ff12..56f9d306 100644 --- a/config.properties +++ b/config.properties @@ -91,9 +91,15 @@ hibernate.connection.password:changeme #frontend.donationUrl:https://kavin.rocks # SSO via OIDC -# each provider needs to have these three options specified. is the +# Each provider needs to have these three options specified. is the # friendly name which will be shown to the clients and used in the database. # If you want to change the name later, you will have to update the database. -# oidc.provider..clientId: -# oidc.provider..clientSecret: -# oidc.provider..issuer: +#oidc.provider..clientId:example_piped_client_id +#oidc.provider..clientSecret:example_piped_client_secret +#oidc.provider..issuer:https://idm.example.com + +# Ask the provider to re-authenticate the user when account deletion is +# requested. This field is optional and you should only set this to false +# if your provider doesn't support the max_age parameter. You will know when +# trying to delete an account. +#oidc.provider..sendMaxAge = true diff --git a/src/main/java/me/kavin/piped/server/ServerLauncher.java b/src/main/java/me/kavin/piped/server/ServerLauncher.java index 66bc8655..d0a32062 100644 --- a/src/main/java/me/kavin/piped/server/ServerLauncher.java +++ b/src/main/java/me/kavin/piped/server/ServerLauncher.java @@ -269,8 +269,8 @@ AsyncServlet mainServlet(Executor executor) { return HttpResponse.ofCode(500).withHtml("Can't find the provider on the server"); return switch (function) { - case "login" -> UserHandlers.oidcLoginResponse(provider, request.getQueryParameter("redirect")); - case "callback" -> UserHandlers.oidcCallbackResponse(provider, URI.create(request.getFullUrl())); + case "login" -> UserHandlers.oidcLoginRequest(provider, request.getQueryParameter("redirect")); + case "callback" -> UserHandlers.oidcLoginCallback(provider, URI.create(request.getFullUrl())); case "delete" -> UserHandlers.oidcDeleteCallback(provider, URI.create(request.getFullUrl())); default -> HttpResponse.ofCode(500).withHtml("Invalid function `" + function + "`"); }; 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 9edc87b9..8104ca0b 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 @@ -29,6 +29,7 @@ import me.kavin.piped.utils.obj.db.User; import me.kavin.piped.utils.resp.*; import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.hibernate.Session; import org.hibernate.StatelessSession; @@ -128,7 +129,7 @@ public static byte[] loginResponse(String user, String pass) } } - public static HttpResponse oidcLoginResponse(OidcProvider provider, String redirectUri) throws Exception { + public static HttpResponse oidcLoginRequest(OidcProvider provider, String redirectUri) throws Exception { if (StringUtils.isBlank(redirectUri)) { return HttpResponse.ofCode(400).withHtml("redirect is a required parameter"); } @@ -163,7 +164,7 @@ public static HttpResponse oidcLoginResponse(OidcProvider provider, String redir "\">here"); } - public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI requestUri) throws Exception { + public static HttpResponse oidcLoginCallback(OidcProvider provider, URI requestUri) throws Exception { AuthenticationSuccessResponse authResponse = parseOidcUri(requestUri); OidcData data = DatabaseHelper.getOidcData(authResponse.getState().toString()); @@ -228,8 +229,7 @@ public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI reque if (dbuser != null) { sessionId = dbuser.getUser().getSessionId(); } else { - String username = userInfo.getPreferredUsername(); - OidcUserData newUser = new OidcUserData(sub, username, provider.name); + OidcUserData newUser = new OidcUserData(sub, RandomStringUtils.randomAlphabetic(24), provider.name); var tr = s.beginTransaction(); s.persist(newUser); From e4ba19556c9f71e1ddc41beab2276a5d0290b6ce Mon Sep 17 00:00:00 2001 From: Jeidnx Date: Wed, 20 Nov 2024 16:34:25 +0100 Subject: [PATCH 20/21] add redirect to oidc delete; more cleanup --- src/main/java/me/kavin/piped/Main.java | 28 ++++++++++++++++++ .../me/kavin/piped/server/ServerLauncher.java | 5 ++-- .../server/handlers/auth/UserHandlers.java | 29 +++++++++++++++---- .../me/kavin/piped/utils/DatabaseHelper.java | 15 ++++++++-- .../me/kavin/piped/utils/obj/db/OidcData.java | 4 +-- 5 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/main/java/me/kavin/piped/Main.java b/src/main/java/me/kavin/piped/Main.java index ab710fa9..2c34df96 100644 --- a/src/main/java/me/kavin/piped/Main.java +++ b/src/main/java/me/kavin/piped/Main.java @@ -9,6 +9,7 @@ import me.kavin.piped.utils.*; import me.kavin.piped.utils.matrix.SyncRunner; import me.kavin.piped.utils.obj.MatrixHelper; +import me.kavin.piped.utils.obj.db.OidcData; import me.kavin.piped.utils.obj.db.PlaylistVideo; import me.kavin.piped.utils.obj.db.PubSub; import me.kavin.piped.utils.obj.db.Video; @@ -253,5 +254,32 @@ public void run() { } }, 0, TimeUnit.MINUTES.toMillis(60)); + new Timer().scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) { + + var cb = s.getCriteriaBuilder(); + var cd = cb.createCriteriaDelete(OidcData.class); + var root = cd.from(OidcData.class); + cd.where(cb.lessThan(root.get("start"), System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(3))); + + var tr = s.beginTransaction(); + + var query = s.createMutationQuery(cd); + + int affected = query.executeUpdate(); + + tr.commit(); + + if (affected > 0) { + System.out.printf("Cleanup: Removed %o orphaned oidc logins%n", affected); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }, 0, TimeUnit.MINUTES.toMillis(5)); + } } diff --git a/src/main/java/me/kavin/piped/server/ServerLauncher.java b/src/main/java/me/kavin/piped/server/ServerLauncher.java index d0a32062..f48d5073 100644 --- a/src/main/java/me/kavin/piped/server/ServerLauncher.java +++ b/src/main/java/me/kavin/piped/server/ServerLauncher.java @@ -490,8 +490,9 @@ AsyncServlet mainServlet(Executor executor) { } })).map(GET, "/user/delete", AsyncServlet.ofBlocking(executor, request -> { try { - var session = request.getQueryParameter("session"); - return UserHandlers.oidcDeleteRequest(session); + String session = request.getQueryParameter("session"); + String redirect = request.getQueryParameter("redirect"); + return UserHandlers.oidcDeleteRequest(session, redirect); } catch (Exception e) { return getErrorResponse(e, request.getPath()); } 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 8104ca0b..b3471dcd 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 @@ -177,6 +177,12 @@ public static HttpResponse oidcLoginCallback(OidcProvider provider, URI requestU URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback"); AuthorizationCode code = authResponse.getAuthorizationCode(); + if (code == null) { + return HttpResponse.ofCode(400).withHtml( + "Your oidc provider sent an invalid code. Try again or contact your oidc admin" + ); + } + AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, data.getOidVerifier()); ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret); @@ -241,12 +247,16 @@ public static HttpResponse oidcLoginCallback(OidcProvider provider, URI requestU return HttpResponse.redirect302(data.data + "?session=" + sessionId); } - public static HttpResponse oidcDeleteRequest(String session) throws Exception { + public static HttpResponse oidcDeleteRequest(String session, String redirect) throws Exception { if (StringUtils.isBlank(session)) { return HttpResponse.ofCode(400).withHtml("session is a required parameter"); } + if (StringUtils.isBlank(redirect)) { + return HttpResponse.ofCode(400).withHtml("redirect is a required parameter"); + } + OidcProvider provider = null; try (Session s = DatabaseSessionFactory.createSession()) { @@ -282,7 +292,7 @@ public static HttpResponse oidcDeleteRequest(String session) throws Exception { CodeVerifier pkceVerifier = new CodeVerifier(); URI callback = URI.create(String.format("%s/oidc/%s/delete", Constants.PUBLIC_URL, provider.name)); - OidcData data = new OidcData(session + "|" + Instant.now().getEpochSecond(), pkceVerifier); + OidcData data = new OidcData(session + "|" + redirect, pkceVerifier); String state = data.getState(); DatabaseHelper.setOidcData(data); @@ -297,7 +307,7 @@ public static HttpResponse oidcDeleteRequest(String session) throws Exception { .nonce(data.getOidNonce()); if (provider.sendMaxAge) { - // This parameter is optional and the idp doesn't have to honor it. + // This parameter is optional and the idp doesn't have to honor it. oidcRequestBuilder.maxAge(0); } @@ -316,11 +326,18 @@ public static HttpResponse oidcDeleteCallback(OidcProvider provider, URI request ); } - long start = Long.parseLong(data.data.split("\\|")[1]); + String redirect = 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(); + + if (code == null) { + return HttpResponse.ofCode(400).withHtml( + "Your oidc provider sent an invalid code. Try again or contact your oidc admin" + ); + } + AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, data.getOidVerifier()); ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret); @@ -355,7 +372,7 @@ public static HttpResponse oidcDeleteCallback(OidcProvider provider, URI request return HttpResponse.ofCode(400).withHtml("Couldn't get the `auth_time` claim from the provided id token"); } - if (authTime < start) { + if (authTime <= data.start) { return HttpResponse.ofCode(500).withHtml( "Your oidc provider didn't verify your identity. Please try again or contact your oidc admin." ); @@ -377,7 +394,7 @@ public static HttpResponse oidcDeleteCallback(OidcProvider provider, URI request tr.commit(); } - return HttpResponse.redirect302(Constants.FRONTEND_URL + "/preferences?deleted=" + session); + return HttpResponse.redirect302(redirect + "?deleted=true"); } public static byte[] deleteUserResponse(String session, String pass) throws IOException { diff --git a/src/main/java/me/kavin/piped/utils/DatabaseHelper.java b/src/main/java/me/kavin/piped/utils/DatabaseHelper.java index c4bb30a6..b7b9b151 100644 --- a/src/main/java/me/kavin/piped/utils/DatabaseHelper.java +++ b/src/main/java/me/kavin/piped/utils/DatabaseHelper.java @@ -247,13 +247,24 @@ public static void setOidcData(OidcData data) { } public static OidcData getOidcData(String state) { - try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) { + try (Session s = DatabaseSessionFactory.createSession()) { + CriteriaBuilder cb = s.getCriteriaBuilder(); CriteriaQuery cr = cb.createQuery(OidcData.class); Root root = cr.from(OidcData.class); cr.select(root).where(cb.equal(root.get("state"), state)); - return s.createQuery(cr).uniqueResult(); + OidcData data = s.createQuery(cr).uniqueResult(); + + if (data == null){ + return null; + } + + var tr = s.beginTransaction(); + s.remove(data); + tr.commit(); + + return data; } } } diff --git a/src/main/java/me/kavin/piped/utils/obj/db/OidcData.java b/src/main/java/me/kavin/piped/utils/obj/db/OidcData.java index 2e9b50c8..cb283446 100644 --- a/src/main/java/me/kavin/piped/utils/obj/db/OidcData.java +++ b/src/main/java/me/kavin/piped/utils/obj/db/OidcData.java @@ -27,13 +27,13 @@ public class OidcData implements Serializable { public String state; @Column(name = "start") - public long auth_start; + public long start; public OidcData(String data, CodeVerifier pkceVerifier) { this.nonce = new Nonce().toString(); this.verifierSecret = pkceVerifier.getValue(); this.data = data; - this.auth_start = System.currentTimeMillis() / 1000L; + this.start = System.currentTimeMillis() / 1000L; this.state = getState(); } From 77cd736c06e9c5e2536d4328babafd0d42bca829 Mon Sep 17 00:00:00 2001 From: Jeidnx Date: Thu, 21 Nov 2024 10:46:44 +0100 Subject: [PATCH 21/21] explicitly reject empty hashes --- .../java/me/kavin/piped/server/handlers/auth/UserHandlers.java | 3 +++ 1 file changed, 3 insertions(+) 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 b3471dcd..1412ce84 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 @@ -96,6 +96,9 @@ public static byte[] registerResponse(String user, String pass) throws Exception } private static boolean hashMatch(String hash, String pass) { + if (hash.isBlank()) { + return false; + } return hash.startsWith("$argon2") ? argon2PasswordEncoder.matches(pass, hash) : bcryptPasswordEncoder.matches(pass, hash);