From a904ebd5e800aeaea801d26e83bfb88ac79ed7ef Mon Sep 17 00:00:00 2001 From: Pat Patterson Date: Thu, 17 Mar 2022 17:56:34 -0700 Subject: [PATCH 1/3] After Step 1 --- .../com/authlete/simpleauth/LoginUtils.java | 3 +- .../simpleauth/oauth/AuthleteCredential.java | 31 +++++ .../oauth/OAuthAuthorizationServlet.java | 106 ++++++++++++++++++ .../authlete/simpleauth/oauth/OAuthUtils.java | 63 +++++++++++ 4 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 loyalty/src/main/java/com/authlete/simpleauth/oauth/AuthleteCredential.java create mode 100644 loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthAuthorizationServlet.java create mode 100644 loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthUtils.java 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; + } + } +} From 3eb873c909e318cb1f64f5a608dd5a480b8e1fa2 Mon Sep 17 00:00:00 2001 From: Pat Patterson Date: Thu, 17 Mar 2022 17:56:34 -0700 Subject: [PATCH 2/3] After Step 2 --- .../oauth/OAuthAuthorizationServlet.java | 92 ++++++++----------- .../authlete/simpleauth/oauth/OAuthUtils.java | 34 +++++++ 2 files changed, 74 insertions(+), 52 deletions(-) diff --git a/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthAuthorizationServlet.java b/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthAuthorizationServlet.java index 6519e0b..3c91994 100644 --- a/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthAuthorizationServlet.java +++ b/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthAuthorizationServlet.java @@ -29,7 +29,8 @@ public class OAuthAuthorizationServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { - Map authApiResponse = (Map) request.getSession().getAttribute("authApiResponse"); + 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) { @@ -40,67 +41,54 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t } private void initiateAuthleteAuthorization(HttpServletRequest request, HttpServletResponse response) throws IOException { - // 1. Get a Jersey HTTP client - Client client = OAuthUtils.getClient(getServletContext()); + // 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. 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"); + // 2. handleAuthleteApiCall() returns null if it already returned a response to the client + if (authApiResponse == null) { + return; + } - // 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; - } + // 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; } - 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! + // 4. We should never get here! Map errorResponse = Map.of( - "error", "unexpected_error", - "error_description", "Contact the service owner for details" + "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)); + // 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/OAuthUtils.java b/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthUtils.java index 8088241..3e4444c 100644 --- a/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthUtils.java +++ b/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthUtils.java @@ -22,6 +22,8 @@ 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 { @@ -60,4 +62,36 @@ public static String prettyPrint(Map requestMap) { 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": + 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; + } } From 754f94125929f46f098c83fbf1ba8f41793eed59 Mon Sep 17 00:00:00 2001 From: Pat Patterson Date: Thu, 17 Mar 2022 17:56:34 -0700 Subject: [PATCH 3/3] After Step 3 --- .../simpleauth/oauth/OAuthTokenServlet.java | 48 +++++++++++++++++++ .../authlete/simpleauth/oauth/OAuthUtils.java | 1 + 2 files changed, 49 insertions(+) create mode 100644 loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthTokenServlet.java 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 index 3e4444c..99a923d 100644 --- a/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthUtils.java +++ b/loyalty/src/main/java/com/authlete/simpleauth/oauth/OAuthUtils.java @@ -82,6 +82,7 @@ public static Map handleAuthleteApiCall(ServletContext context, 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":