diff --git a/loyalty/src/main/java/com/authlete/simpleauth/LoginUtils.java b/loyalty/src/main/java/com/authlete/simpleauth/LoginUtils.java index dd0e7c9..a41203a 100644 --- a/loyalty/src/main/java/com/authlete/simpleauth/LoginUtils.java +++ b/loyalty/src/main/java/com/authlete/simpleauth/LoginUtils.java @@ -54,6 +54,7 @@ public static boolean isPublicPage(HttpServletRequest request) { return requestUri.equals(contextPath + "/login") || requestUri.equals(contextPath + "/index.html") || requestUri.equals(contextPath + "/") || - requestUri.startsWith(contextPath + "/css"); + requestUri.startsWith(contextPath + "/css") || + requestUri.startsWith(contextPath + "/oauth/"); } } diff --git a/loyalty/src/main/java/com/authlete/simpleauth/oauth/AuthleteCredential.java b/loyalty/src/main/java/com/authlete/simpleauth/oauth/AuthleteCredential.java new file mode 100644 index 0000000..1db65ed --- /dev/null +++ b/loyalty/src/main/java/com/authlete/simpleauth/oauth/AuthleteCredential.java @@ -0,0 +1,31 @@ +package com.authlete.simpleauth.oauth; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class AuthleteCredential { + @JsonProperty("api_key") + private String apiKey; + + @JsonProperty("api_secret") + private String apiSecret; + + @JsonProperty("api_key") + public String getApiKey() { + return apiKey; + } + + @JsonProperty("api_key") + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + @JsonProperty("api_secret") + public String getApiSecret() { + return apiSecret; + } + + @JsonProperty("api_secret") + public void setApiSecret(String apiSecret) { + this.apiSecret = apiSecret; + } +} diff --git a/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthAuthorizationServlet.java b/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthAuthorizationServlet.java new file mode 100644 index 0000000..3c91994 --- /dev/null +++ b/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthAuthorizationServlet.java @@ -0,0 +1,94 @@ +package com.authlete.simpleauth.oauth; + +import com.authlete.simpleauth.LoginUtils; +import com.authlete.simpleauth.UserAccount; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; + +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@WebServlet("/oauth/authorization") +public class OAuthAuthorizationServlet extends HttpServlet { + private static final Logger logger = LogManager.getLogger(); + private static final long serialVersionUID = 1L; + private final ObjectMapper mapper = new ObjectMapper(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + Map authApiResponse = + (Map) request.getSession().getAttribute("authApiResponse"); + request.getSession().removeAttribute("authApiResponse"); + logger.info("{} API response in the session", authApiResponse == null ? "No" : "Found an"); + if (authApiResponse == null) { + initiateAuthleteAuthorization(request, response); + } else { + processAuthleteAuthorization(request, response, authApiResponse); + } + } + + private void initiateAuthleteAuthorization(HttpServletRequest request, HttpServletResponse response) throws IOException { + // 1. Call the Authlete Authorization endpoint, wrapping the incoming query string in a JSON object + Map authApiResponse = OAuthUtils.handleAuthleteApiCall( + getServletContext(), response, "/auth/authorization", + Collections.singletonMap("parameters", request.getQueryString())); + + // 2. handleAuthleteApiCall() returns null if it already returned a response to the client + if (authApiResponse == null) { + return; + } + + // 3. Perform the action + String action = (String)authApiResponse.get("action"); + if (action.equals("INTERACTION")) { + List prompts = (List) authApiResponse.get("prompts"); + for (Object prompt : prompts) { + if (prompt.equals("LOGIN")) { + request.getSession().setAttribute("authApiResponse", authApiResponse); + LoginUtils.redirectForLogin(request, response); + return; + } + } + } + + // 4. We should never get here! + Map errorResponse = Map.of( + "error", "unexpected_error", + "error_description", "Contact the service owner for details" + ); + OAuthUtils.setResponseBody(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, mapper.writeValueAsString(errorResponse)); + } + + private void processAuthleteAuthorization(HttpServletRequest request, HttpServletResponse response, Map authApiResponse) throws IOException { + // 1. Create a Map to send in the Authlete API request + Map requestMap = new HashMap<>(); + + // 2. Copy the ticket from the last API response into the map + requestMap.put("ticket", authApiResponse.get("ticket")); + + // 3. Verify that the user is actually logged in + UserAccount authenticatedUser = LoginUtils.getAuthenticatedUser(request.getSession()); + if (authenticatedUser == null) { + requestMap.put("reason", "NOT_LOGGED_IN"); + OAuthUtils.handleAuthleteApiCall(getServletContext(), response, "/auth/authorization/fail", requestMap); + return; + } + + // 4. Issue the code + requestMap.put("subject", authenticatedUser.getUsername()); + OAuthUtils.handleAuthleteApiCall(getServletContext(), response, "/auth/authorization/issue", requestMap); + } +} \ No newline at end of file diff --git a/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthTokenServlet.java b/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthTokenServlet.java new file mode 100644 index 0000000..f92cc5f --- /dev/null +++ b/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthTokenServlet.java @@ -0,0 +1,48 @@ +package com.authlete.simpleauth.oauth; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Map; + + +@WebServlet("/oauth/token") +public class OAuthTokenServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + private final ObjectMapper mapper = new ObjectMapper(); + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { + // 1. Call the /auth/token endpoint, passing the request body + String body = new String(request.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + Map authApiResponse = + OAuthUtils.handleAuthleteApiCall(getServletContext(), response, "/auth/token", + Collections.singletonMap("parameters", body)); + + // 2. handleAuthleteApiCall() returns null if it already returned a response to the client + if (authApiResponse == null) { + return; + } + + // 3. Perform the action + String action = (String)authApiResponse.get("action"); + if (action.equals("OK")) { + String responseContent = (String)authApiResponse.get("responseContent"); + OAuthUtils.setResponseBody(response, HttpServletResponse.SC_OK, responseContent); + return; + } + + // 4. We should never get here! + Map errorResponse = Map.of( + "error", "unexpected_error", + "error_description", "Contact the service owner for details" + ); + OAuthUtils.setResponseBody(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, mapper.writeValueAsString(errorResponse)); + } +} diff --git a/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthUtils.java b/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthUtils.java new file mode 100644 index 0000000..99a923d --- /dev/null +++ b/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthUtils.java @@ -0,0 +1,98 @@ +package com.authlete.simpleauth.oauth; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.MediaType; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +public class OAuthUtils { + private static final String AUTHLETE_CREDENTIAL_JSON = "/WEB-INF/authleteCredential.json"; + private static final String AUTHLETE_BASE = "https://api.authlete.com/api"; + private static final Logger logger = LogManager.getLogger(); + private static final ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); + + private static AuthleteCredential getAuthleteCredential(ServletContext context) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + InputStream inputStream = context.getResourceAsStream(AUTHLETE_CREDENTIAL_JSON); + if (inputStream == null) { + throw new FileNotFoundException(AUTHLETE_CREDENTIAL_JSON); + } + return mapper.readValue(inputStream, AuthleteCredential.class); + } + + public static synchronized Client getClient(ServletContext context) throws IOException { + Client client = (Client)context.getAttribute("authleteClient"); + if (client == null) { + AuthleteCredential authleteCredential = getAuthleteCredential(context); + client = ClientBuilder.newClient(new ClientConfig()) + .register(HttpAuthenticationFeature.basic(authleteCredential.getApiKey(), authleteCredential.getApiSecret())); + context.setAttribute("authleteClient", client); + } + + return client; + } + + static void setResponseBody(HttpServletResponse response, int statusCode, String responseContent) throws IOException { + response.setStatus(statusCode); + response.setContentType(MediaType.APPLICATION_JSON); + response.setHeader("Cache-Control", "no-store"); + response.setHeader("Pragma", "no-cache"); + response.getWriter().print(responseContent); + } + + public static String prettyPrint(Map requestMap) { + try { + return mapper.writeValueAsString(requestMap); + } catch (JsonProcessingException e) { + return null; + } + } + + public static Map handleAuthleteApiCall(ServletContext context, HttpServletResponse response, + String api, Map requestMap) throws IOException { + logger.debug("Calling API {} with params {}", api, OAuthUtils.prettyPrint(requestMap)); + + Map responseMap = getClient(context).target(AUTHLETE_BASE + api) + .request() + .post(Entity.entity(requestMap, MediaType.APPLICATION_JSON_TYPE), new GenericType<>() { + }); + + logger.debug("Received API response {}", OAuthUtils.prettyPrint(responseMap)); + + String action = (String)responseMap.get("action"); + String responseContent = (String)responseMap.get("responseContent"); + + switch (action) { + case "INTERNAL_SERVER_ERROR": + setResponseBody(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, responseContent); + return null; + case "BAD_REQUEST": + case "INVALID_CLIENT": + setResponseBody(response, HttpServletResponse.SC_BAD_REQUEST, responseContent); + return null; + case "LOCATION": + response.setStatus(HttpServletResponse.SC_FOUND); + response.setHeader("Location", responseContent); + response.setHeader("Cache-Control", "no-store"); + response.setHeader("Pragma", "no-cache"); + return null; + } + + return responseMap; + } +}