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..6519e0b --- /dev/null +++ b/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthAuthorizationServlet.java @@ -0,0 +1,106 @@ +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. Get a Jersey HTTP client + Client client = OAuthUtils.getClient(getServletContext()); + + // 2. We will wrap the incoming query string in a JSON object + Map requestMap = Collections.singletonMap("parameters", request.getQueryString()); + + // 3. Call the Authlete Authorization endpoint + String url = "https://api.authlete.com/api/auth/authorization"; + + logger.info("Sending API request to {}:\n{}", url, OAuthUtils.prettyPrint(requestMap)); + + // 4. Make the API call, parsing the JSON response into a map + Map authApiResponse = client.target(url) + .request() + .post(Entity.entity(requestMap, MediaType.APPLICATION_JSON_TYPE), new GenericType<>() { + }); + + logger.info("Received API response:\n{}", OAuthUtils.prettyPrint(authApiResponse)); + + // 5. 'action' tells us what to do next, 'responseContent' is the payload we'll return + String action = (String) authApiResponse.get("action"); + String responseContent = (String) authApiResponse.get("responseContent"); + + // 6. Perform the action + switch (action) { + case "INTERACTION": + List prompts = (List) authApiResponse.get("prompts"); + for (Object prompt : prompts) { + if (prompt.equals("LOGIN")) { + // 7. Prompt the user to login + request.getSession().setAttribute("authApiResponse", authApiResponse); + LoginUtils.redirectForLogin(request, response); + return; + } + } + break; + + // 8. Handle errors + case "INTERNAL_SERVER_ERROR": + OAuthUtils.setResponseBody(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, responseContent); + return; + + case "BAD_REQUEST": + OAuthUtils.setResponseBody(response, HttpServletResponse.SC_BAD_REQUEST, responseContent); + return; + } + + // 9. 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 { + // Not yet implemented! + Map errorResponse = Map.of( + "error", "not_yet_implemented", + "error_description", "This step is not yet implemented" + ); + OAuthUtils.setResponseBody(response, HttpServletResponse.SC_NOT_IMPLEMENTED, mapper.writeValueAsString(errorResponse)); + } +} \ No newline at end of file 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..8088241 --- /dev/null +++ b/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthUtils.java @@ -0,0 +1,63 @@ +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 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; + } + } +}