commit 380946f5a7f6ffad70efe3fcb65f2ff1ea4541b3 Author: damage Date: Sun Dec 22 12:21:47 2024 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6f89c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e0f15db --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic" +} \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..bfbb768 --- /dev/null +++ b/pom.xml @@ -0,0 +1,133 @@ + + + + 4.0.0 + + de.devloop + mavor + 1.0-SNAPSHOT + war + + mavor + + http://www.example.com + + + UTF-8 + 11 + 11 + 3.9.9 + + + + mavor + src/main + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-war-plugin + 3.2.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + + + + + + jakarta.servlet + jakarta.servlet-api + 6.1.0 + provided + + + + jakarta.servlet.jsp.jstl + jakarta.servlet.jsp.jstl-api + 3.0.2 + + + + org.glassfish.web + jakarta.servlet.jsp.jstl + 3.0.1 + + + + jakarta.platform + jakarta.jakartaee-web-api + 11.0.0-M4 + provided + + + + com.google.code.gson + gson + 2.11.0 + + + + org.eclipse.sisu + org.eclipse.sisu.plexus + 0.9.0.M3 + + + + + + org.apache.maven + maven-embedder + 3.9.9 + + + + + org.apache.maven + maven-slf4j-provider + 3.9.9 + + + + + org.apache.maven + maven-core + 3.9.9 + + + + + org.apache.maven + maven-compat + 3.9.9 + + + + + + diff --git a/src/main/de/devloop/mavor/AuthenticatedServlet.java b/src/main/de/devloop/mavor/AuthenticatedServlet.java new file mode 100644 index 0000000..a9cf26e --- /dev/null +++ b/src/main/de/devloop/mavor/AuthenticatedServlet.java @@ -0,0 +1,26 @@ +package de.devloop.mavor; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class AuthenticatedServlet extends HttpServlet { + protected Session session; + + @Override + protected final void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + session = new Session(req.getSession(true)); + if (!session.isAuthenticated()) { + resp.sendRedirect("/mavor/authenticate"); + } else { + doAuthenticatedGet(req, resp); + } + } + + protected void doAuthenticatedGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // nooooothing + } +} diff --git a/src/main/de/devloop/mavor/Session.java b/src/main/de/devloop/mavor/Session.java new file mode 100644 index 0000000..603307f --- /dev/null +++ b/src/main/de/devloop/mavor/Session.java @@ -0,0 +1,57 @@ +package de.devloop.mavor; + +import jakarta.servlet.http.HttpSession; + +public class Session { + private HttpSession session; + + private static final String ATTRIBUTE_USERNAME = "username"; + private static final String ATTRIBUTE_OAUTH_STATE = "oauth.state"; + private static final String ATTRIBUTE_OAUTH_TOKEN = "oauth.token"; + + public Session(HttpSession session) { + this.session = session; + } + + private String getSafeString(String parameter) { + Object value = session.getAttribute(parameter); + + if (value != null) { + return value.toString(); + } else { + return null; + } + } + + public Boolean isAuthenticated() { + return getUsername() != null; + } + + public String getUsername() { + return getSafeString(ATTRIBUTE_USERNAME); + } + + public void setUsername(String username) { + session.setAttribute(ATTRIBUTE_USERNAME, username); + } + + public void setOAuthState(String state) { + session.setAttribute(ATTRIBUTE_OAUTH_STATE, state); + } + + public String getOAuthState() { + return getSafeString(ATTRIBUTE_OAUTH_STATE); + } + + public void clearOAuthState() { + session.removeAttribute(ATTRIBUTE_OAUTH_STATE); + } + + public void setOAuthToken(String token) { + session.setAttribute(ATTRIBUTE_OAUTH_TOKEN, token); + } + + public String getOAuthToken() { + return getSafeString(ATTRIBUTE_OAUTH_TOKEN); + } +} diff --git a/src/main/de/devloop/mavor/annotation/AuthenticationRequired.java b/src/main/de/devloop/mavor/annotation/AuthenticationRequired.java new file mode 100644 index 0000000..3d5b7bf --- /dev/null +++ b/src/main/de/devloop/mavor/annotation/AuthenticationRequired.java @@ -0,0 +1,12 @@ +package de.devloop.mavor.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthenticationRequired { + +} diff --git a/src/main/de/devloop/mavor/servlet/Authentication.java b/src/main/de/devloop/mavor/servlet/Authentication.java new file mode 100644 index 0000000..2b28803 --- /dev/null +++ b/src/main/de/devloop/mavor/servlet/Authentication.java @@ -0,0 +1,54 @@ +package de.devloop.mavor.servlet; + +import java.io.IOException; + +import de.devloop.mavor.Session; +import de.devloop.openid.AuthenticationUrl; +import de.devloop.openid.OpenID; +import de.devloop.openid.OpenIdRequestException; +import de.devloop.openid.Token; +import de.devloop.openid.UserInfo; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@WebServlet("/authenticate") +public class Authentication extends HttpServlet { + + private static final String PARAMETER_STATE = "state"; + private static final String PARAMETER_CODE = "code"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + Session session = new Session(req.getSession(true)); + + OpenID openID = new OpenID(); + + if (req.getParameter(PARAMETER_STATE) != null && req.getParameter(PARAMETER_CODE) != null) { + if (req.getParameter(PARAMETER_STATE).equals(session.getOAuthState())) { + session.clearOAuthState(); + + Token token; + UserInfo userInfo; + try { + token = openID.requestToken(req.getParameter(PARAMETER_CODE)); + userInfo = openID.requestUserInfo(token); + } catch (OpenIdRequestException e) { + throw new ServletException("Login failed", e); + } + + session.setOAuthToken(token.getAccessToken()); + session.setUsername(userInfo.getEmail()); + resp.sendRedirect("/mavor/"); + } else { + throw new ServletException("OpenID state mismatch!"); + } + } else { + AuthenticationUrl authenticationUrl = openID.getAuthenticationUrl(); + session.setOAuthState(authenticationUrl.getState()); + resp.sendRedirect(authenticationUrl.getUrl()); + } + } +} diff --git a/src/main/de/devloop/mavor/servlet/Download.java b/src/main/de/devloop/mavor/servlet/Download.java new file mode 100644 index 0000000..47c7550 --- /dev/null +++ b/src/main/de/devloop/mavor/servlet/Download.java @@ -0,0 +1,40 @@ +package de.devloop.mavor.servlet; + +import java.io.IOException; +import java.io.PrintStream; + +import org.apache.maven.cli.MavenCli; + +import de.devloop.mavor.AuthenticatedServlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@WebServlet("/download") +public class Download extends AuthenticatedServlet { + + private static final String PARAMETER_SITE = "site"; + private static final String PARAMETER_GROUP_ID = "groupId"; + private static final String PARAMETER_ARTEFACT_ID = "artefactId"; + private static final String PARAMETER_VERSION = "version"; + + @Override + protected void doAuthenticatedGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String groupId = req.getParameter(PARAMETER_GROUP_ID); + String artifactId = req.getParameter(PARAMETER_ARTEFACT_ID); + String version = req.getParameter(PARAMETER_VERSION); + + PrintStream out = new PrintStream(resp.getOutputStream()); + + MavenCli cli = new MavenCli(); + System.setProperty("maven.multiModuleProjectDirectory", "/home/damage/Temp"); + cli.doMain(new String[]{"dependency:copy", "-Dartifact=com.google.code.gson:gson:2.11.0", "-DoutputDirectory=/home/damage/Temp"}, "/home/damage/Temp", out, out); + + //RequestDispatcher view = req.getRequestDispatcher("/download.jsp"); + + + //view.forward(req, resp); + + } +} diff --git a/src/main/de/devloop/mavor/servlet/Logout.java b/src/main/de/devloop/mavor/servlet/Logout.java new file mode 100644 index 0000000..491e9e8 --- /dev/null +++ b/src/main/de/devloop/mavor/servlet/Logout.java @@ -0,0 +1,24 @@ +package de.devloop.mavor.servlet; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +@WebServlet("/logout") +public class Logout extends HttpServlet { + private static final String OAUTH_LOGOUT_URL = "https://auth.devloop.de/application/o/devloop-mavor/end-session/"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + HttpSession httpSession = req.getSession(); + if (httpSession != null) { + httpSession.invalidate(); + } + resp.sendRedirect(OAUTH_LOGOUT_URL); + } +} diff --git a/src/main/de/devloop/mavor/servlet/Main.java b/src/main/de/devloop/mavor/servlet/Main.java new file mode 100644 index 0000000..07aedf1 --- /dev/null +++ b/src/main/de/devloop/mavor/servlet/Main.java @@ -0,0 +1,22 @@ +package de.devloop.mavor.servlet; + +import java.io.IOException; + +import de.devloop.mavor.AuthenticatedServlet; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@WebServlet("") +public class Main extends AuthenticatedServlet { + + @Override + protected void doAuthenticatedGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + req.setAttribute("username", session.getUsername()); + RequestDispatcher view = req.getRequestDispatcher("/main.jsp"); + + view.forward(req, resp); + } +} diff --git a/src/main/de/devloop/openid/AuthenticationUrl.java b/src/main/de/devloop/openid/AuthenticationUrl.java new file mode 100644 index 0000000..438440c --- /dev/null +++ b/src/main/de/devloop/openid/AuthenticationUrl.java @@ -0,0 +1,19 @@ +package de.devloop.openid; + +public class AuthenticationUrl { + private String url; + private String state; + + public AuthenticationUrl(String url, String state) { + this.url = url; + this.state = state; + } + + public String getUrl() { + return url; + } + + public String getState() { + return state; + } +} diff --git a/src/main/de/devloop/openid/OpenID.java b/src/main/de/devloop/openid/OpenID.java new file mode 100644 index 0000000..f002030 --- /dev/null +++ b/src/main/de/devloop/openid/OpenID.java @@ -0,0 +1,102 @@ +package de.devloop.openid; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import com.google.gson.Gson; + +public class OpenID { + private static final String CLIENT_ID = "vP9xF2s1yy2n6sR05jV6dguyMeOvIxCg1GarV71O"; + private static final String CLIENT_SECRET = "PrwGSMcucxYPkOdrb23jddWqyn31vphrxCUu9MGdLTCUnbk0OJI5oWCvO0khVhcnJNDbJaKWxNMxaC4bJ92jy8bDjtG6oaWG37qhuLRPMh5DKluZxsCMmCvQ8f9ZQckZ"; + + private static final String REDIRECT_URL = "http://localhost:8080/mavor/authenticate"; + + private static final String OAUTH_AUTH_URL = "https://auth.devloop.de/application/o/authorize/"; + private static final String OAUTH_TOKEN_URL = "https://auth.devloop.de/application/o/token/"; + private static final String OAUTH_USERINFO_URL = "https://auth.devloop.de/application/o/userinfo/"; + + public AuthenticationUrl getAuthenticationUrl() { + String state = UUID.randomUUID().toString(); + String url = String.format("%s?response_type=code&client_id=%s&redirect_uri=%s&state=%s&scope=openid email", OAUTH_AUTH_URL, CLIENT_ID, REDIRECT_URL, state); + return new AuthenticationUrl(url, state); + } + + private URI getUriObject(String url) throws OpenIdRequestException { + try { + return new URI(url); + } catch (URISyntaxException e) { + throw new OpenIdRequestException(String.format("Invalid URL: '%s'", url), e); + } + } + + public Token requestToken(String code) throws OpenIdRequestException { + URI tokenUrl = getUriObject(OAUTH_TOKEN_URL); + HashMap tokenParameter = new HashMap<>(); + tokenParameter.put("grant_type", "authorization_code"); + tokenParameter.put("client_id", CLIENT_ID); + tokenParameter.put("client_secret", CLIENT_SECRET); + tokenParameter.put("code", code); + tokenParameter.put("redirect_uri", REDIRECT_URL); + + HttpRequest tokenRequest = HttpRequest.newBuilder() + .uri(tokenUrl) + .header("Content-Type", "application/x-www-form-urlencoded") + .header("Accept", "application/json") + .POST(BodyPublishers.ofString(getFormDataAsString(tokenParameter))) + .build(); + HttpClient tokenClient = HttpClient.newHttpClient(); + HttpResponse tokenResponse; + try { + tokenResponse = tokenClient.send(tokenRequest, BodyHandlers.ofString()); + } catch (IOException | InterruptedException e) { + throw new OpenIdRequestException("Requesting access token failed", e); + } + + Gson gson = new Gson(); + return gson.fromJson(tokenResponse.body(), Token.class); + } + + public UserInfo requestUserInfo(Token token) throws OpenIdRequestException { + URI userInfoUrl = getUriObject(OAUTH_USERINFO_URL); + HttpRequest userInfoRequest = HttpRequest.newBuilder() + .uri(userInfoUrl) + .header("Accept", "application/json") + .header("Authorization", "Bearer " + token.getAccessToken()) + .GET() + .build(); + HttpClient userInfoClient = HttpClient.newHttpClient(); + HttpResponse userInfoResponse; + try { + userInfoResponse = userInfoClient.send(userInfoRequest, BodyHandlers.ofString()); + } catch (IOException | InterruptedException e) { + throw new OpenIdRequestException("Requesting user info failed", e); + } + + Gson gson = new Gson(); + return gson.fromJson(userInfoResponse.body(), UserInfo.class); + } + + private String getFormDataAsString(Map formData) { + StringBuilder formBodyBuilder = new StringBuilder(); + for (Map.Entry singleEntry : formData.entrySet()) { + if (formBodyBuilder.length() > 0) { + formBodyBuilder.append("&"); + } + formBodyBuilder.append(URLEncoder.encode(singleEntry.getKey(), StandardCharsets.UTF_8)); + formBodyBuilder.append("="); + formBodyBuilder.append(URLEncoder.encode(singleEntry.getValue(), StandardCharsets.UTF_8)); + } + return formBodyBuilder.toString(); + } +} diff --git a/src/main/de/devloop/openid/OpenIdRequestException.java b/src/main/de/devloop/openid/OpenIdRequestException.java new file mode 100644 index 0000000..cc41032 --- /dev/null +++ b/src/main/de/devloop/openid/OpenIdRequestException.java @@ -0,0 +1,7 @@ +package de.devloop.openid; + +public class OpenIdRequestException extends Exception { + public OpenIdRequestException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/de/devloop/openid/Token.java b/src/main/de/devloop/openid/Token.java new file mode 100644 index 0000000..2254ad9 --- /dev/null +++ b/src/main/de/devloop/openid/Token.java @@ -0,0 +1,18 @@ +package de.devloop.openid; + +import com.google.gson.annotations.SerializedName; + +public class Token { + + @SerializedName("access_token") + private String accessToken; + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + +} diff --git a/src/main/de/devloop/openid/UserInfo.java b/src/main/de/devloop/openid/UserInfo.java new file mode 100644 index 0000000..f34bf1f --- /dev/null +++ b/src/main/de/devloop/openid/UserInfo.java @@ -0,0 +1,15 @@ +package de.devloop.openid; + +public class UserInfo { + + private String email; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + +} diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..65b57e6 --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,12 @@ + + + + Archetype Created Web Application + + + 403 + /authenticate + + diff --git a/src/main/webapp/download.jsp b/src/main/webapp/download.jsp new file mode 100644 index 0000000..b1607c6 --- /dev/null +++ b/src/main/webapp/download.jsp @@ -0,0 +1,8 @@ +<%@ taglib uri="jakarta.tags.core" prefix="c" %> +<%@ page isELIgnored="false" %> + + + Downloading ${foo}
+ logout + + diff --git a/src/main/webapp/main.jsp b/src/main/webapp/main.jsp new file mode 100644 index 0000000..5bc3d79 --- /dev/null +++ b/src/main/webapp/main.jsp @@ -0,0 +1,15 @@ +<%@ taglib uri="jakarta.tags.core" prefix="c" %> +<%@ page isELIgnored="false" %> + + +

Hello ${username}

+
+ Site:
+ Group ID:
+ Artifact ID:
+ Version:
+ +
+logout + +