From 4b21a6b225ea648f7af759202f969b6d0e192e2e Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Mon, 25 Nov 2024 06:13:11 -0500 Subject: [PATCH] First shot at new login API --- .../java/org/gusdb/wdk/core/api/JsonKeys.java | 1 + .../wdk/service/service/SessionService.java | 97 +++++++++++++------ 2 files changed, 69 insertions(+), 29 deletions(-) diff --git a/Model/src/main/java/org/gusdb/wdk/core/api/JsonKeys.java b/Model/src/main/java/org/gusdb/wdk/core/api/JsonKeys.java index 667b21465..fc6c20d82 100644 --- a/Model/src/main/java/org/gusdb/wdk/core/api/JsonKeys.java +++ b/Model/src/main/java/org/gusdb/wdk/core/api/JsonKeys.java @@ -240,6 +240,7 @@ public class JsonKeys { public static final String PREFERENCES = "preferences"; public static final String GLOBAL = "global"; public static final String PROJECT = "project"; + public static final String BEARER_TOKEN = "bearerToken"; // date and date range keys public static final String MIN_DATE = "minDate"; diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java index 8f52c70a1..a87f3c599 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java @@ -7,6 +7,7 @@ import java.util.Set; import java.util.UUID; +import javax.ws.rs.BadRequestException; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; @@ -14,6 +15,7 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; @@ -23,6 +25,7 @@ import org.gusdb.fgputil.EncryptionUtil; import org.gusdb.fgputil.FormatUtil; import org.gusdb.fgputil.Tuples.TwoTuple; +import org.gusdb.fgputil.functional.Either; import org.gusdb.fgputil.web.CookieBuilder; import org.gusdb.fgputil.web.LoginCookieFactory; import org.gusdb.oauth2.client.ValidatedToken; @@ -41,13 +44,20 @@ import org.json.JSONException; import org.json.JSONObject; -import com.google.common.net.HttpHeaders; - @Path("/") public class SessionService extends AbstractWdkService { private static final Logger LOG = Logger.getLogger(SessionService.class); + private enum ResponseType { + REDIRECT, JSON; + } + + private static class UserTupleEither extends Either, String> { + public UserTupleEither(TwoTuple userTuple) { super(userTuple, null); } + public UserTupleEither(String errorMessage) { super(null, errorMessage); } + } + public static final int EXPIRATION_3_YEARS_SECS = 3 * 365 * 24 * 60 * 60; private static final String REFERRER_HEADER_KEY = "Referer"; @@ -106,6 +116,14 @@ private static String generateStateToken(WdkModel wdkModel) throws WdkModelExcep return EncryptionUtil.encrypt(saltedString); } + @GET + @Path("create-guest") + @Produces(MediaType.APPLICATION_JSON) + public Response createGuest() throws WdkModelException { + TwoTuple guest = getWdkModel().getUserFactory().createUnregisteredUser(); + return getSuccessResponse(guest, null, ResponseType.JSON); + } + @GET @Path("login") public Response processOauthLogin( @@ -128,7 +146,8 @@ public Response processOauthLogin( // Was existing bearer token submitted with this request? User oldUser = getRequestingUser(); if (!oldUser.isGuest()) { - return createRedirectResponse(redirectUrl).build(); + // TODO: should this be 409? Work it out with UI + throw new BadRequestException("Only guests can log in"); } try { @@ -157,12 +176,19 @@ public Response processOauthLogin( // transfer ownership from guest to logged-in user transferOwnership(oldUser, newUser, wdkModel); + // determine response type + ResponseType responseType = getHeaders() + .get(HttpHeaders.ACCEPT) + .stream().findFirst() + .map(val -> val.equals(MediaType.APPLICATION_JSON) ? ResponseType.JSON : ResponseType.REDIRECT) + .orElse(ResponseType.REDIRECT); + // login successful; create redirect response - return getSuccessResponse(bearerToken, newUser, oldUser, redirectUrl, true); + return getSuccessResponse(new TwoTuple<>(bearerToken, newUser), redirectUrl, responseType); } catch (InvalidPropertiesException ex) { LOG.error("Could not authenticate user's identity. Exception thrown: ", ex); - return createJsonResponse(false, "Invalid auth token or redirect URI", null).build(); + return createJsonResponse(new UserTupleEither("Invalid auth token or redirect URI"), null); } catch (Exception ex) { LOG.error("Unsuccessful login attempt: " + ex.getMessage(), ex); @@ -206,7 +232,8 @@ public Response processDbLogin(@HeaderParam(REFERRER_HEADER_KEY) String referrer // transfer ownership from guest to logged-in user transferOwnership(oldUser, newUser, wdkModel); - return getSuccessResponse(bearerToken, newUser, oldUser, redirectUrl, false); + // return success JSON response + return getSuccessResponse(new TwoTuple<>(bearerToken, newUser), redirectUrl, ResponseType.JSON); } catch (JSONException e) { @@ -214,7 +241,7 @@ public Response processDbLogin(@HeaderParam(REFERRER_HEADER_KEY) String referrer } catch (InvalidPropertiesException ex) { LOG.error("Could not authenticate user's identity. Exception thrown: ", ex); - return createJsonResponse(false, "Invalid username or password", null).build(); + return createJsonResponse(new UserTupleEither("Invalid username or password"), null); } } @@ -231,33 +258,33 @@ protected void transferOwnership(User oldUser, User newUser, WdkModel wdkModel) * * @param bearerToken bearer token for the new user * @param newUser newly logged in user - * @param oldUser user previously on session, if any * @param redirectUrl incoming original page * @param isRedirectResponse whether to return redirect or JSON response * @return success response * @throws WdkModelException */ - private Response getSuccessResponse(ValidatedToken bearerToken, User newUser, User oldUser, - String redirectUrl, boolean isRedirectResponse) throws WdkModelException { + private Response getSuccessResponse(TwoTuple newUser, + String redirectUrl, ResponseType responseType) throws WdkModelException { + // only using this to synchronize on the user TemporaryUserData tmpData = getTemporaryUserData(); synchronized(tmpData) { // 3-year expiration (should change secret key before then) + // FIXME: cookie sending should be removed/deleted once client has support for header/response body transmission CookieBuilder bearerTokenCookie = new CookieBuilder( HttpHeaders.AUTHORIZATION, - bearerToken.getTokenValue()); + "Bearer " + newUser.getFirst().getTokenValue()); bearerTokenCookie.setMaxAge(EXPIRATION_3_YEARS_SECS); - redirectUrl = getSuccessRedirectUrl(redirectUrl, newUser, bearerTokenCookie); + redirectUrl = getSuccessRedirectUrl(redirectUrl, newUser.getSecond(), bearerTokenCookie); - return (isRedirectResponse ? - createRedirectResponse(redirectUrl) : - createJsonResponse(true, null, redirectUrl) - ) - .cookie(bearerTokenCookie.toJaxRsCookie()) - .build(); + switch(responseType) { + case REDIRECT: return createRedirectResponse(redirectUrl).cookie(bearerTokenCookie.toJaxRsCookie()).build(); + case JSON: return createJsonResponse(new UserTupleEither(newUser), redirectUrl); + default: throw new IllegalStateException(); // should never happen + } } } @@ -316,19 +343,31 @@ public static NewCookie getAuthCookie(String tokenValue) { } /** - * Convenience method to set up a response builder that returns JSON containing request result + * Creates a JSON response for requests served by this class * - * @param success whether the login was successful - * @param message a failure message if not successful - * @param redirectUrl url to which to redirect the user if successful - * @return partially constructed response + * @param userTupleOrErrorMessage Either object containing a token/user (success case) or error message + * @param redirectUrl URL to which use should eventually be redirected + * @return JSON response */ - private static ResponseBuilder createJsonResponse(boolean success, String message, String redirectUrl) { - return Response.ok(new JSONObject() - .put(JsonKeys.SUCCESS, success) - .put(JsonKeys.MESSAGE, message) - .put(JsonKeys.REDIRECT_URL, redirectUrl) - .toString()); + private static Response createJsonResponse(UserTupleEither userTupleOrErrorMessage, String redirectUrl) { + JSONObject json = new JSONObject() + .put(JsonKeys.SUCCESS, userTupleOrErrorMessage.isLeft()) + .put(JsonKeys.REDIRECT_URL, redirectUrl); + userTupleOrErrorMessage + .ifLeft(userTuple -> + json + .put(JsonKeys.BEARER_TOKEN, userTuple.getFirst().getTokenValue()) + .put(JsonKeys.USER_ID, userTuple.getSecond().getUserId()) + .put(JsonKeys.IS_GUEST, userTuple.getSecond().isGuest()) + ) + .ifRight(errorMessage -> + json + .put(JsonKeys.MESSAGE, errorMessage) + ); + return Response + .ok(json.toString()) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .build(); } /**