414 lines
17 KiB
Java
414 lines
17 KiB
Java
/*
|
|
* Copyright 2015 Robert von Burg <eitch@eitchnet.ch>
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package li.strolch.rest.endpoint;
|
|
|
|
import com.google.gson.JsonArray;
|
|
import com.google.gson.JsonObject;
|
|
import com.google.gson.JsonParser;
|
|
import com.google.gson.JsonPrimitive;
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
import jakarta.ws.rs.*;
|
|
import jakarta.ws.rs.core.*;
|
|
import jakarta.ws.rs.core.Response.Status;
|
|
import li.strolch.exception.StrolchException;
|
|
import li.strolch.exception.StrolchNotAuthenticatedException;
|
|
import li.strolch.privilege.base.AccessDeniedException;
|
|
import li.strolch.privilege.base.InvalidCredentialsException;
|
|
import li.strolch.privilege.base.NotAuthenticatedException;
|
|
import li.strolch.privilege.base.PrivilegeException;
|
|
import li.strolch.privilege.model.Certificate;
|
|
import li.strolch.privilege.model.Privilege;
|
|
import li.strolch.privilege.model.PrivilegeContext;
|
|
import li.strolch.privilege.model.Usage;
|
|
import li.strolch.rest.RestfulStrolchComponent;
|
|
import li.strolch.rest.helper.ResponseUtil;
|
|
import li.strolch.runtime.privilege.PrivilegeHandler;
|
|
import li.strolch.runtime.sessions.StrolchSessionHandler;
|
|
import li.strolch.utils.iso8601.ISO8601;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import java.text.MessageFormat;
|
|
import java.time.LocalDateTime;
|
|
import java.time.ZoneId;
|
|
import java.util.Base64;
|
|
import java.util.Date;
|
|
import java.util.Set;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import static li.strolch.rest.StrolchRestfulConstants.STROLCH_AUTHORIZATION;
|
|
import static li.strolch.rest.StrolchRestfulConstants.STROLCH_AUTHORIZATION_EXPIRATION_DATE;
|
|
import static li.strolch.rest.helper.RestfulHelper.getRemoteIp;
|
|
import static li.strolch.utils.helper.ExceptionHelper.getRootCause;
|
|
import static li.strolch.utils.helper.ExceptionHelper.hasCause;
|
|
|
|
/**
|
|
* @author Robert von Burg <eitch@eitchnet.ch>
|
|
*/
|
|
@Path("strolch/authentication")
|
|
public class AuthenticationResource {
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(AuthenticationResource.class);
|
|
|
|
@POST
|
|
@Consumes(MediaType.APPLICATION_JSON)
|
|
@Produces(MediaType.APPLICATION_JSON)
|
|
public Response authenticate(@Context HttpServletRequest request, @Context HttpHeaders headers, String data) {
|
|
JsonObject login = JsonParser.parseString(data).getAsJsonObject();
|
|
|
|
try {
|
|
if (!login.has("username") || login.get("username").getAsString().length() < 2) {
|
|
logger.error("Authentication failed: Username was not given or is too short!");
|
|
JsonObject loginResult = new JsonObject();
|
|
loginResult.addProperty("msg", MessageFormat.format("Could not log in due to: {0}",
|
|
"Username was not given or is too short!")); //$NON-NLS-2$
|
|
return Response.status(Status.BAD_REQUEST).entity(loginResult.toString()).build();
|
|
}
|
|
|
|
if (!login.has("password") || login.get("password").getAsString().length() < 3) {
|
|
logger.error("Authentication failed: Password was not given or is too short!");
|
|
JsonObject loginResult = new JsonObject();
|
|
loginResult.addProperty("msg", MessageFormat.format("Could not log in due to: {0}",
|
|
"Password was not given or is too short!")); //$NON-NLS-2$
|
|
return Response.status(Status.BAD_REQUEST).entity(loginResult.toString()).build();
|
|
}
|
|
|
|
String username = login.get("username").getAsString();
|
|
String passwordEncoded = login.get("password").getAsString();
|
|
boolean keepAlive = login.has("keepAlive") && login.get("keepAlive").getAsBoolean();
|
|
|
|
byte[] decode = Base64.getDecoder().decode(passwordEncoded);
|
|
char[] password = new String(decode).toCharArray();
|
|
|
|
if (password.length < 3) {
|
|
logger.error("Authentication failed: Password was not given or is too short!");
|
|
JsonObject loginResult = new JsonObject();
|
|
loginResult.addProperty("msg", MessageFormat.format("Could not log in due to: {0}",
|
|
"Password was not given or is too short!")); //$NON-NLS-2$
|
|
return Response.status(Status.BAD_REQUEST).entity(loginResult.toString()).build();
|
|
}
|
|
|
|
StrolchSessionHandler sessionHandler = RestfulStrolchComponent.getInstance().getSessionHandler();
|
|
String source = getRemoteIp(request);
|
|
Certificate certificate = sessionHandler.authenticate(username, password, source, Usage.ANY, keepAlive);
|
|
|
|
return getAuthenticationResponse(request, certificate, source, true);
|
|
|
|
} catch (Exception e) {
|
|
return handleAuthenticationException(e);
|
|
}
|
|
}
|
|
|
|
@POST
|
|
@Produces(MediaType.APPLICATION_JSON)
|
|
@Path("sso")
|
|
public Response authenticateSingleSignOn(@Context HttpServletRequest request, @Context HttpHeaders headers) {
|
|
try {
|
|
StrolchSessionHandler sessionHandler = RestfulStrolchComponent.getInstance().getSessionHandler();
|
|
String source = getRemoteIp(request);
|
|
Certificate certificate = sessionHandler.authenticateSingleSignOn(request.getUserPrincipal(), source);
|
|
return getAuthenticationResponse(request, certificate, source, true);
|
|
} catch (Exception e) {
|
|
return handleAuthenticationException(e);
|
|
}
|
|
}
|
|
|
|
@DELETE
|
|
@Consumes(MediaType.APPLICATION_JSON)
|
|
@Produces(MediaType.APPLICATION_JSON)
|
|
@Path("{authToken}")
|
|
public Response invalidateSession(@Context HttpServletRequest request, @PathParam("authToken") String authToken) {
|
|
|
|
JsonObject logoutResult = new JsonObject();
|
|
|
|
try {
|
|
StrolchSessionHandler sessionHandler = RestfulStrolchComponent.getInstance().getSessionHandler();
|
|
String source = getRemoteIp(request);
|
|
Certificate certificate = sessionHandler.validate(authToken, source);
|
|
sessionHandler.invalidate(certificate);
|
|
|
|
logoutResult.addProperty("username", certificate.getUsername());
|
|
logoutResult.addProperty("authToken", authToken);
|
|
logoutResult.addProperty("msg",
|
|
MessageFormat.format("{0} has been logged out.", certificate.getUsername()));
|
|
return Response.ok().entity(logoutResult.toString()).build();
|
|
|
|
} catch (Exception e) {
|
|
return handleSessionException("Failed to invalidate session", e);
|
|
}
|
|
}
|
|
|
|
@HEAD
|
|
@Consumes(MediaType.APPLICATION_JSON)
|
|
@Produces(MediaType.APPLICATION_JSON)
|
|
@Path("{authToken}")
|
|
public Response validateSession(@Context HttpServletRequest request, @PathParam("authToken") String authToken) {
|
|
try {
|
|
StrolchSessionHandler sessionHandler = RestfulStrolchComponent.getInstance().getSessionHandler();
|
|
String source = getRemoteIp(request);
|
|
sessionHandler.validate(authToken, source);
|
|
return Response.ok().build();
|
|
} catch (Exception e) {
|
|
return handleSessionException("Session validation failed", e);
|
|
}
|
|
}
|
|
|
|
private static Response handleAuthenticationException(Exception e) {
|
|
logger.error(e.getMessage(), e);
|
|
Throwable rootCause = getRootCause(e);
|
|
String msg = MessageFormat.format("Could not log in due to: {0}", rootCause);
|
|
if (hasCause(e, InvalidCredentialsException.class))
|
|
msg = "Could not log in as the given credentials are invalid";
|
|
return evaluateResponseByCause(e, msg);
|
|
}
|
|
|
|
private static Response evaluateResponseByCause(Exception e, String msg) {
|
|
Status status;
|
|
if (hasCause(e, InvalidCredentialsException.class)) {
|
|
status = Status.UNAUTHORIZED;
|
|
} else if (hasCause(e, AccessDeniedException.class)) {
|
|
status = Status.UNAUTHORIZED;
|
|
} else if (hasCause(e, StrolchException.class)) {
|
|
status = Status.FORBIDDEN;
|
|
} else if (hasCause(e, PrivilegeException.class)) {
|
|
status = Status.FORBIDDEN;
|
|
} else {
|
|
status = Status.INTERNAL_SERVER_ERROR;
|
|
}
|
|
|
|
JsonObject loginResult = new JsonObject();
|
|
loginResult.addProperty("msg", msg);
|
|
return Response.status(status).entity(loginResult.toString()).build();
|
|
}
|
|
|
|
private static Response handleSessionException(String context, Exception e) {
|
|
if (e instanceof StrolchNotAuthenticatedException || e instanceof NotAuthenticatedException)
|
|
logger.error("Session exception: " + e.getMessage());
|
|
else
|
|
logger.error(e.getMessage(), e);
|
|
Throwable rootCause = getRootCause(e);
|
|
String msg = MessageFormat.format("{0}: {1}", context, rootCause);
|
|
return evaluateResponseByCause(e, msg);
|
|
}
|
|
|
|
@GET
|
|
@Consumes(MediaType.APPLICATION_JSON)
|
|
@Produces(MediaType.APPLICATION_JSON)
|
|
@Path("{authToken}")
|
|
public Response getValidatedSession(@Context HttpServletRequest request, @PathParam("authToken") String authToken) {
|
|
try {
|
|
StrolchSessionHandler sessionHandler = RestfulStrolchComponent.getInstance().getSessionHandler();
|
|
String source = getRemoteIp(request);
|
|
Certificate certificate = sessionHandler.validate(authToken, source);
|
|
return getAuthenticationResponse(request, certificate, source, false);
|
|
} catch (Exception e) {
|
|
return handleSessionException("Session validation failed", e);
|
|
}
|
|
}
|
|
|
|
@PUT
|
|
@Consumes(MediaType.APPLICATION_JSON)
|
|
@Produces(MediaType.APPLICATION_JSON)
|
|
@Path("{authToken}")
|
|
public Response refreshSession(@Context HttpServletRequest request, @PathParam("authToken") String authToken) {
|
|
try {
|
|
StrolchSessionHandler sessionHandler = RestfulStrolchComponent.getInstance().getSessionHandler();
|
|
String source = getRemoteIp(request);
|
|
Certificate certificate = sessionHandler.validate(authToken, source);
|
|
Certificate refreshedCert = sessionHandler.refreshSession(certificate, source);
|
|
return getAuthenticationResponse(request, refreshedCert, source, true);
|
|
} catch (Exception e) {
|
|
return handleSessionException("Session validation failed", e);
|
|
}
|
|
}
|
|
|
|
@POST
|
|
@Produces(MediaType.APPLICATION_JSON)
|
|
@Path("challenge")
|
|
public Response initiateChallenge(@Context HttpServletRequest request, String data) {
|
|
try {
|
|
JsonObject jsonObject = JsonParser.parseString(data).getAsJsonObject();
|
|
String username = jsonObject.get("username").getAsString();
|
|
String usage = jsonObject.get("usage").getAsString();
|
|
|
|
StrolchSessionHandler sessionHandler = RestfulStrolchComponent.getInstance().getSessionHandler();
|
|
String source = getRemoteIp(request);
|
|
sessionHandler.initiateChallengeFor(Usage.byValue(usage), username, source);
|
|
return ResponseUtil.toResponse();
|
|
} catch (Exception e) {
|
|
return handleSessionException("Challenge initialization failed", e);
|
|
}
|
|
}
|
|
|
|
@PUT
|
|
@Produces(MediaType.APPLICATION_JSON)
|
|
@Path("challenge")
|
|
public Response validateChallenge(@Context HttpServletRequest request, String data) {
|
|
try {
|
|
|
|
JsonObject jsonObject = JsonParser.parseString(data).getAsJsonObject();
|
|
String username = jsonObject.get("username").getAsString();
|
|
String challenge = jsonObject.get("challenge").getAsString();
|
|
|
|
RestfulStrolchComponent restComponent = RestfulStrolchComponent.getInstance();
|
|
StrolchSessionHandler sessionHandler = restComponent.getSessionHandler();
|
|
String source = getRemoteIp(request);
|
|
Certificate certificate = sessionHandler.validateChallenge(username, challenge, source);
|
|
int sessionMaxKeepAliveMinutes = sessionHandler.getSessionMaxKeepAliveMinutes();
|
|
int cookieMaxAge = getCookieMaxAge(certificate, restComponent, sessionMaxKeepAliveMinutes);
|
|
LocalDateTime expirationDate = LocalDateTime.now().plusSeconds(cookieMaxAge);
|
|
|
|
JsonObject result = new JsonObject();
|
|
String authToken = certificate.getAuthToken();
|
|
result.addProperty("authToken", authToken);
|
|
|
|
return setCookiesAndReturnResponse(request, restComponent, cookieMaxAge, expirationDate, result, authToken);
|
|
|
|
} catch (Exception e) {
|
|
return handleSessionException("Challenge validation failed", e);
|
|
}
|
|
}
|
|
|
|
private Response getAuthenticationResponse(HttpServletRequest request, Certificate certificate, String source,
|
|
boolean setCookies) {
|
|
|
|
RestfulStrolchComponent restComponent = RestfulStrolchComponent.getInstance();
|
|
StrolchSessionHandler sessionHandler = restComponent.getSessionHandler();
|
|
int sessionMaxKeepAliveMinutes = sessionHandler.getSessionMaxKeepAliveMinutes();
|
|
int cookieMaxAge = getCookieMaxAge(certificate, restComponent, sessionMaxKeepAliveMinutes);
|
|
LocalDateTime expirationDate = LocalDateTime.now().plusSeconds(cookieMaxAge);
|
|
|
|
JsonObject loginResult = new JsonObject();
|
|
|
|
PrivilegeHandler privilegeHandler = restComponent.getContainer().getPrivilegeHandler();
|
|
PrivilegeContext privilegeContext = privilegeHandler.validate(certificate, source);
|
|
loginResult.addProperty("sessionId", certificate.getSessionId());
|
|
String authToken = certificate.getAuthToken();
|
|
loginResult.addProperty("authToken", authToken);
|
|
loginResult.addProperty("username", certificate.getUsername());
|
|
loginResult.addProperty("firstname", certificate.getFirstname());
|
|
loginResult.addProperty("lastname", certificate.getLastname());
|
|
loginResult.addProperty("locale", certificate.getLocale().toLanguageTag());
|
|
loginResult.addProperty("keepAlive", certificate.isKeepAlive());
|
|
loginResult.addProperty("keepAliveMinutes", sessionMaxKeepAliveMinutes);
|
|
loginResult.addProperty("cookieMaxAge", cookieMaxAge);
|
|
loginResult.addProperty("authorizationExpiration", ISO8601.toString(expirationDate));
|
|
loginResult.addProperty("refreshAllowed", sessionHandler.isRefreshAllowed());
|
|
loginResult.addProperty("usage", certificate.getUsage().getValue());
|
|
|
|
if (!certificate.getPropertyMap().isEmpty()) {
|
|
JsonObject propObj = new JsonObject();
|
|
loginResult.add("properties", propObj);
|
|
for (String propKey : certificate.getPropertyMap().keySet()) {
|
|
propObj.addProperty(propKey, certificate.getPropertyMap().get(propKey));
|
|
}
|
|
}
|
|
|
|
if (!certificate.getUserRoles().isEmpty()) {
|
|
JsonArray rolesArr = new JsonArray();
|
|
loginResult.add("roles", rolesArr);
|
|
for (String role : certificate.getUserRoles()) {
|
|
rolesArr.add(new JsonPrimitive(role));
|
|
}
|
|
}
|
|
|
|
if (!privilegeContext.getPrivilegeNames().isEmpty()) {
|
|
JsonArray privArr = new JsonArray();
|
|
loginResult.add("privileges", privArr);
|
|
|
|
for (String name : privilegeContext.getPrivilegeNames()) {
|
|
Privilege privilege = privilegeContext.getPrivilege(name);
|
|
|
|
JsonObject privObj = new JsonObject();
|
|
privArr.add(privObj);
|
|
|
|
privObj.addProperty("name", name);
|
|
privObj.addProperty("allAllowed", privilege.isAllAllowed());
|
|
|
|
Set<String> allowSet = privilege.getAllowList();
|
|
if (!allowSet.isEmpty()) {
|
|
JsonArray allowArr = new JsonArray();
|
|
privObj.add("allowList", allowArr);
|
|
for (String allow : allowSet) {
|
|
allowArr.add(new JsonPrimitive(allow));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (setCookies)
|
|
return setCookiesAndReturnResponse(request, restComponent, cookieMaxAge, expirationDate, loginResult,
|
|
authToken);
|
|
return Response.ok().entity(loginResult.toString()).header(HttpHeaders.AUTHORIZATION, authToken).build();
|
|
}
|
|
|
|
private static Response setCookiesAndReturnResponse(HttpServletRequest request,
|
|
RestfulStrolchComponent restComponent, int cookieMaxAge, LocalDateTime expirationDate,
|
|
JsonObject loginResult, String authToken) {
|
|
boolean secureCookie = restComponent.isSecureCookie();
|
|
if (secureCookie && !request.getScheme().equals("https")) {
|
|
String msg
|
|
= "Authorization cookie is secure, but connection is not secure! Cookie won't be passed to client!";
|
|
logger.error(msg);
|
|
}
|
|
|
|
String expirationDateS = ISO8601.toString(expirationDate);
|
|
String domain = restComponent.isDomainSet() ? restComponent.getDomain() : request.getServerName();
|
|
String path = (restComponent.isPathSet() ? restComponent.getPath() : "/") + ";SameSite=Strict";
|
|
|
|
Date expiry = Date.from(expirationDate.atZone(ZoneId.systemDefault()).toInstant());
|
|
boolean httpOnly = false;
|
|
int version = 1;
|
|
|
|
NewCookie authCookie = getNewCookie(STROLCH_AUTHORIZATION, authToken, path, domain, version,
|
|
"Strolch Authorization header", cookieMaxAge, expiry, secureCookie, httpOnly);
|
|
NewCookie authExpirationCookie = getNewCookie(STROLCH_AUTHORIZATION_EXPIRATION_DATE, expirationDateS, path,
|
|
domain, version, "Strolch Authorization Expiration Date", cookieMaxAge, expiry, secureCookie, httpOnly);
|
|
|
|
return Response.ok().entity(loginResult.toString()) //
|
|
.header(HttpHeaders.AUTHORIZATION, authToken) //
|
|
.cookie(authCookie) //
|
|
.cookie(authExpirationCookie) //
|
|
.build();
|
|
}
|
|
|
|
private static int getCookieMaxAge(Certificate certificate, RestfulStrolchComponent restComponent,
|
|
int sessionMaxKeepAliveMinutes) {
|
|
int cookieMaxAge;
|
|
if (certificate.isKeepAlive()) {
|
|
cookieMaxAge = (int) TimeUnit.MINUTES.toSeconds(sessionMaxKeepAliveMinutes);
|
|
} else {
|
|
cookieMaxAge = restComponent.getCookieMaxAge();
|
|
}
|
|
return cookieMaxAge;
|
|
}
|
|
|
|
private static NewCookie getNewCookie(String strolchAuthorization, String authToken, String path, String domain,
|
|
int version, String comment, int cookieMaxAge, Date expiry, boolean secureCookie, boolean httpOnly) {
|
|
return new NewCookie.Builder(strolchAuthorization) //
|
|
.value(authToken)
|
|
.path(path)
|
|
.domain(domain)
|
|
.version(version)
|
|
.comment(comment)
|
|
.maxAge(cookieMaxAge)
|
|
.expiry(expiry)
|
|
.secure(secureCookie)
|
|
.httpOnly(httpOnly)
|
|
.build();
|
|
}
|
|
}
|