Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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})
Expand All @@ -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<Void> 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<MemberProfile> memberProfiles = memberProfileServices.findByValues(null, null, null, null, email, null, Boolean.FALSE);
Iterator<MemberProfile> 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<String> roles = roleServices.findUserRoles(memberProfile.getId()).stream().map(role -> role.getRole()).collect(Collectors.toSet());
Set<String> permissions = rolePermissionServices.findUserPermissions(memberProfile.getId()).stream().map(permission -> permission.name()).collect(Collectors.toSet());

Map<String, Object> 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<String, Object> 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<String> 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)
Expand All @@ -127,13 +146,13 @@ public HttpResponse<Object> revert(HttpRequest<?> request) {
// Swap the OJWT back to the JWT and remove the original JWT
Set<Cookie> cookies = new HashSet<Cookie>();
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);
}
}
}
22 changes: 22 additions & 0 deletions server/src/main/resources/application-localgcp.yml
Original file line number Diff line number Diff line change
@@ -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"
5 changes: 5 additions & 0 deletions server/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading