diff --git a/server/src/main/java/com/objectcomputing/checkins/security/ImpersonationController.java b/server/src/main/java/com/objectcomputing/checkins/security/ImpersonationController.java index 4cf604a786..faa201df2b 100644 --- a/server/src/main/java/com/objectcomputing/checkins/security/ImpersonationController.java +++ b/server/src/main/java/com/objectcomputing/checkins/security/ImpersonationController.java @@ -1,5 +1,8 @@ package com.objectcomputing.checkins.security; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; import com.objectcomputing.checkins.Environments; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; @@ -14,11 +17,7 @@ import io.micronaut.http.HttpResponse; import io.micronaut.http.MediaType; import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.annotation.Consumes; -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Get; -import io.micronaut.http.annotation.Post; -import io.micronaut.http.annotation.Produces; +import io.micronaut.http.annotation.*; import io.micronaut.http.cookie.Cookie; import io.micronaut.http.cookie.SameSite; import io.micronaut.http.netty.cookies.NettyCookie; @@ -29,16 +28,13 @@ import io.micronaut.security.event.LoginSuccessfulEvent; import io.micronaut.security.handlers.LoginHandler; import io.micronaut.security.rules.SecurityRule; +import io.micronaut.security.token.jwt.generator.JwtTokenGenerator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Locale; -import java.util.Map; -import java.util.Set; +import java.text.ParseException; +import java.util.*; import java.util.stream.Collectors; @Requires(env = {Environments.LOCAL, Environment.DEVELOPMENT}) @@ -54,67 +50,90 @@ public class ImpersonationController { private final MemberProfileServices memberProfileServices; private final RoleServices roleServices; private final RolePermissionServices rolePermissionServices; + private final JwtTokenGenerator generator; /** - * @param loginHandler A collaborator which helps to build HTTP response depending on success or failure. - * @param eventPublisher The application event publisher - * @param roleServices Role services - * @param rolePermissionServices Role permission services - * @param memberProfileServices Member profile services + * @param loginHandler A collaborator which helps to build HTTP response depending on success or failure. + * @param eventPublisher The application event publisher + * @param roleServices Role services + * @param rolePermissionServices Role permission services + * @param memberProfileServices Member profile services + * @param generator Generator for creating and signing the new token */ public ImpersonationController(LoginHandler loginHandler, ApplicationEventPublisher eventPublisher, RoleServices roleServices, RolePermissionServices rolePermissionServices, - MemberProfileServices memberProfileServices) { + MemberProfileServices memberProfileServices, + JwtTokenGenerator generator) { this.loginHandler = loginHandler; this.eventPublisher = eventPublisher; this.roleServices = roleServices; this.rolePermissionServices = rolePermissionServices; this.memberProfileServices = memberProfileServices; + this.generator = generator; } @Consumes({MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON}) @Post("/begin") @RequiredPermission(Permission.CAN_IMPERSONATE_MEMBERS) public HttpResponse auth(HttpRequest request, String email) { - final Cookie jwt = request.getCookies().get(JWT); - if (jwt == null) { - // The user is required to be logged in. If this is null, - // we are in an impossible state! - LOG.error("Unable to locate the JWT"); + final Cookie jwt = request.getCookies().get(JWT); + if (jwt == null) { + // The user is required to be logged in. If this is null, + // we are in an impossible state! + LOG.error("Unable to locate the JWT"); return HttpResponse.unauthorized(); - } else { - LOG.info("Processing request to switch to user \'{}\'", email); + } else { + LOG.info("Processing request to switch to user '{}'", email); Set memberProfiles = memberProfileServices.findByValues(null, null, null, null, email, null, Boolean.FALSE); Iterator iterator = memberProfiles.iterator(); - if(!iterator.hasNext()) return HttpResponse.badRequest(); + if (!iterator.hasNext()) return HttpResponse.badRequest(); MemberProfile memberProfile = iterator.next(); - LOG.info("Profile exists for \'{}\'", email); - String firstName = memberProfile.getFirstName() != null ? memberProfile.getFirstName() : ""; - String lastName = memberProfile.getLastName() != null ? memberProfile.getLastName() : ""; + LOG.info("Profile exists for '{}'", email); + String firstName = memberProfile.getFirstName() != null ? memberProfile.getFirstName() : ""; + String lastName = memberProfile.getLastName() != null ? memberProfile.getLastName() : ""; Set roles = roleServices.findUserRoles(memberProfile.getId()).stream().map(role -> role.getRole()).collect(Collectors.toSet()); Set permissions = rolePermissionServices.findUserPermissions(memberProfile.getId()).stream().map(permission -> permission.name()).collect(Collectors.toSet()); Map newAttributes = new HashMap<>(); - newAttributes.put("email", memberProfile.getWorkEmail()); - newAttributes.put("name", firstName + ' ' + lastName); - newAttributes.put("picture", ""); + newAttributes.put("email", memberProfile.getWorkEmail()); + newAttributes.put("name", firstName + ' ' + lastName); + newAttributes.put("picture", ""); newAttributes.put("roles", roles); newAttributes.put("permissions", permissions); - newAttributes.put("openIdToken", ""); + JWTClaimsSet newSet = null; + try { + JWT parse = JWTParser.parse(jwt.getValue()); + JWTClaimsSet jwtClaimsSet = parse.getJWTClaimsSet(); + Map claims = new HashMap<>(); + claims.put("email", memberProfile.getWorkEmail()); + claims.put("name", firstName + ' ' + lastName); + claims.put("picture", ""); + claims.put("exp", ((Date) jwtClaimsSet.getClaims().get("exp")).getTime()); + claims.put("iss", jwtClaimsSet.getClaims().get("iss")); + claims.put("aud", jwtClaimsSet.getClaims().get("aud")); + claims.put("sub", jwtClaimsSet.getClaims().get("sub")); + newSet = JWTClaimsSet.parse(claims); + Optional signed = generator.generateToken(claims); + + String token = signed.get(); + if (newSet != null) newAttributes.put("openIdToken", token); + } catch (ParseException e) { + throw new RuntimeException(e); + } LOG.info("Building authentication"); Authentication updatedAuth = Authentication.build(email, roles, newAttributes); LOG.info("Publishing login"); - eventPublisher.publishEvent(new LoginSuccessfulEvent(updatedAuth, null, Locale.getDefault())); - // Store the old JWT to allow the user to revert the impersonation. + eventPublisher.publishEvent(new LoginSuccessfulEvent(updatedAuth, null, Locale.getDefault())); + // Store the old JWT to allow the user to revert the impersonation. LOG.info("Attempting to swap tokens"); - return ((MutableHttpResponse)loginHandler.loginSuccess(updatedAuth, request)).cookie( - new NettyCookie(originalJWT, jwt.getValue()).path("/").sameSite(SameSite.Strict) - .maxAge(jwt.getMaxAge())); - } + return ((MutableHttpResponse) loginHandler.loginSuccess(updatedAuth, request)).cookie( + new NettyCookie(originalJWT, jwt.getValue()).path("/").sameSite(SameSite.Strict) + .maxAge(jwt.getMaxAge())); + } } @Produces(MediaType.TEXT_HTML) @@ -127,13 +146,13 @@ public HttpResponse revert(HttpRequest request) { // Swap the OJWT back to the JWT and remove the original JWT Set cookies = new HashSet(); cookies.add(new NettyCookie(JWT, ojwt.getValue()).path("/") - .sameSite(SameSite.Strict) - .maxAge(ojwt.getMaxAge()).httpOnly()); + .sameSite(SameSite.Strict) + .maxAge(ojwt.getMaxAge()).httpOnly()); cookies.add(new NettyCookie(originalJWT, "").path("/").maxAge(0)); // Redirect to "/" while setting the cookies. return HttpResponse.temporaryRedirect(URI.create("/")) - .cookies(cookies); + .cookies(cookies); } } } diff --git a/server/src/main/resources/application-localgcp.yml b/server/src/main/resources/application-localgcp.yml new file mode 100755 index 0000000000..3caf8c7b8f --- /dev/null +++ b/server/src/main/resources/application-localgcp.yml @@ -0,0 +1,22 @@ +micronaut: + server: + cors: + enabled: true + configurations: + web: + allowedOriginsRegex: + - ^http(|s):\/\/localhost:.*$ +--- +datasources: + default: + url: ${JDBC_URL:`jdbc:postgresql://localhost:5432/checkinsdb`} + username: postgres + password: "postgres" +--- +flyway: + enabled: enabled + datasources: + default: + locations: + - "classpath:db/common" + - "classpath:db/dev" diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index f6ba1a2099..6cc66f0f05 100755 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -48,6 +48,11 @@ micronaut: secret: ${ OAUTH_REFRESH_TOKEN_SECRET:'pleaseChangeThisSecretForANewOne' } access-token: expiration: 28800 + signatures: + secret: + generator: + jws-algorithm: HS256 + secret: ${ JWT_GENERATOR_SIGNATURE_SECRET:'pleaseChangeThisSecretForANewOne' } oauth2: callback-uri: ${ OAUTH_CALLBACK_URI } clients: diff --git a/server/src/test/java/com/objectcomputing/checkins/security/ImpersonationControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/security/ImpersonationControllerTest.java index d4ac367c8f..d1f393be0d 100644 --- a/server/src/test/java/com/objectcomputing/checkins/security/ImpersonationControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/security/ImpersonationControllerTest.java @@ -46,7 +46,7 @@ class ImpersonationControllerTest extends TestContainersSuite implements MemberP private MemberProfile nonAdmin; private MemberProfile admin; - private String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJjb21wYW55IjoiRnV0dXJlRWQiLCJzdWIiOjEsImlzcyI6Imh0dHA6XC9cL2Z1dHVyZWVkLmRldlwvYXBpXC92MVwvc3R1ZGVudFwvbG9naW5cL3VzZXJuYW1lIiwiaWF0IjoiMTQyNzQyNjc3MSIsImV4cCI6IjE0Mjc0MzAzNzEiLCJuYmYiOiIxNDI3NDI2NzcxIiwianRpIjoiNmFlZDQ3MGFiOGMxYTk0MmE0MTViYTAwOTBlMTFlZTUifQ.MmM2YTUwMjEzYTE0OGNhNjk5Y2Y2MjEwZDdkN2Y1OTQ2NWVhZTdmYmI4OTA5YmM1Y2QwYTMzZjUwNTgwY2Y0MQ"; + private String jwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImVlMTkzZDQ2NDdhYjRhMzU4NWFhOWIyYjNiNDg0YTg3YWE2OGJiNDIiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4MzIxNDAwMjA1OTMtMTJxNWh2b2psajc0N24xdnV1cG9rNXZ2aTk5NXJlYzIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4MzIxNDAwMjA1OTMtMTJxNWh2b2psajc0N24xdnV1cG9rNXZ2aTk5NXJlYzIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTczOTEzNDIwMzMxOTg1MTI1ODEiLCJoZCI6Im9iamVjdGNvbXB1dGluZy5jb20iLCJlbWFpbCI6ImtpbWJlcmxpbm1Ab2JqZWN0Y29tcHV0aW5nLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoibENmaWkyWEdYaFEtOC1TeEs2N2Z5QSIsIm5vbmNlIjoiYzRkNTkyNjQtYmY3Ny00M2MwLTliMGMtYjZhYzZjNzgxN2Y1IiwibmFtZSI6Ik1pY2hhZWwgS2ltYmVybGluIiwicGljdHVyZSI6Imh0dHBzOi8vbGgzLmdvb2dsZXVzZXJjb250ZW50LmNvbS9hL0FDZzhvY0tIRHNOdlprclpaUS0xN1NiTTJqNWdXdDdpa09SOTI2UXZ0WFZSSnU5cTRRQl82UHZ4PXM5Ni1jIiwiZ2l2ZW5fbmFtZSI6Ik1pY2hhZWwiLCJmYW1pbHlfbmFtZSI6IktpbWJlcmxpbiIsImlhdCI6MTc0MjQxMjQxMiwiZXhwIjoxNzQyNDE2MDEyfQ.gVgncacfKyzr5_NSMOAu7xGRKnlBA_tg_1JCcfJ4KBMa0Xnvvhhx9Fix252_SQ5xpaG7b-sDApEl1fTcMKqUfYNrj-5s3SzWzCoV6g4NI44YN1j_KyjKFUZ6RWJ79U1U8_DA_wBATQvA-_NNMT8WL9A4muolH-cjoXixymCkl6dgd5QjriEOQC20QYCSbp_nHpeAgbl6fvCh8ZvpK2bHb6zwDmjYuN_xRuQyztfKS1X24nSB0k840Jdw2kUSzXZITUE6zOskapYKlTxcP8BGQ3GPydrNFxGy9Ec6GR3I0J-QhcE5b4mBijae9hgWw6Yz93SSjpm_w8w9D_fO_6yVSA"; @BeforeEach void setUp() {