Skip to content

Commit b5b9943

Browse files
authored
Merge pull request #2927 from objectcomputing/bugfix-2926/fix-impersonate-user
Fixed impersonation
2 parents 646c35b + 80736a5 commit b5b9943

File tree

4 files changed

+88
-42
lines changed

4 files changed

+88
-42
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.objectcomputing.checkins.security;
22

3+
import com.nimbusds.jwt.JWT;
4+
import com.nimbusds.jwt.JWTClaimsSet;
5+
import com.nimbusds.jwt.JWTParser;
36
import com.objectcomputing.checkins.Environments;
47
import com.objectcomputing.checkins.services.memberprofile.MemberProfile;
58
import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices;
@@ -14,11 +17,7 @@
1417
import io.micronaut.http.HttpResponse;
1518
import io.micronaut.http.MediaType;
1619
import io.micronaut.http.MutableHttpResponse;
17-
import io.micronaut.http.annotation.Consumes;
18-
import io.micronaut.http.annotation.Controller;
19-
import io.micronaut.http.annotation.Get;
20-
import io.micronaut.http.annotation.Post;
21-
import io.micronaut.http.annotation.Produces;
20+
import io.micronaut.http.annotation.*;
2221
import io.micronaut.http.cookie.Cookie;
2322
import io.micronaut.http.cookie.SameSite;
2423
import io.micronaut.http.netty.cookies.NettyCookie;
@@ -29,16 +28,13 @@
2928
import io.micronaut.security.event.LoginSuccessfulEvent;
3029
import io.micronaut.security.handlers.LoginHandler;
3130
import io.micronaut.security.rules.SecurityRule;
31+
import io.micronaut.security.token.jwt.generator.JwtTokenGenerator;
3232
import org.slf4j.Logger;
3333
import org.slf4j.LoggerFactory;
3434

3535
import java.net.URI;
36-
import java.util.HashMap;
37-
import java.util.HashSet;
38-
import java.util.Iterator;
39-
import java.util.Locale;
40-
import java.util.Map;
41-
import java.util.Set;
36+
import java.text.ParseException;
37+
import java.util.*;
4238
import java.util.stream.Collectors;
4339

4440
@Requires(env = {Environments.LOCAL, Environment.DEVELOPMENT})
@@ -54,67 +50,90 @@ public class ImpersonationController {
5450
private final MemberProfileServices memberProfileServices;
5551
private final RoleServices roleServices;
5652
private final RolePermissionServices rolePermissionServices;
53+
private final JwtTokenGenerator generator;
5754

5855
/**
59-
* @param loginHandler A collaborator which helps to build HTTP response depending on success or failure.
60-
* @param eventPublisher The application event publisher
61-
* @param roleServices Role services
62-
* @param rolePermissionServices Role permission services
63-
* @param memberProfileServices Member profile services
56+
* @param loginHandler A collaborator which helps to build HTTP response depending on success or failure.
57+
* @param eventPublisher The application event publisher
58+
* @param roleServices Role services
59+
* @param rolePermissionServices Role permission services
60+
* @param memberProfileServices Member profile services
61+
* @param generator Generator for creating and signing the new token
6462
*/
6563
public ImpersonationController(LoginHandler loginHandler,
6664
ApplicationEventPublisher eventPublisher,
6765
RoleServices roleServices,
6866
RolePermissionServices rolePermissionServices,
69-
MemberProfileServices memberProfileServices) {
67+
MemberProfileServices memberProfileServices,
68+
JwtTokenGenerator generator) {
7069
this.loginHandler = loginHandler;
7170
this.eventPublisher = eventPublisher;
7271
this.roleServices = roleServices;
7372
this.rolePermissionServices = rolePermissionServices;
7473
this.memberProfileServices = memberProfileServices;
74+
this.generator = generator;
7575
}
7676

7777
@Consumes({MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON})
7878
@Post("/begin")
7979
@RequiredPermission(Permission.CAN_IMPERSONATE_MEMBERS)
8080
public HttpResponse<Void> auth(HttpRequest<?> request, String email) {
81-
final Cookie jwt = request.getCookies().get(JWT);
82-
if (jwt == null) {
83-
// The user is required to be logged in. If this is null,
84-
// we are in an impossible state!
85-
LOG.error("Unable to locate the JWT");
81+
final Cookie jwt = request.getCookies().get(JWT);
82+
if (jwt == null) {
83+
// The user is required to be logged in. If this is null,
84+
// we are in an impossible state!
85+
LOG.error("Unable to locate the JWT");
8686
return HttpResponse.unauthorized();
87-
} else {
88-
LOG.info("Processing request to switch to user \'{}\'", email);
87+
} else {
88+
LOG.info("Processing request to switch to user '{}'", email);
8989
Set<MemberProfile> memberProfiles = memberProfileServices.findByValues(null, null, null, null, email, null, Boolean.FALSE);
9090
Iterator<MemberProfile> iterator = memberProfiles.iterator();
91-
if(!iterator.hasNext()) return HttpResponse.badRequest();
91+
if (!iterator.hasNext()) return HttpResponse.badRequest();
9292

9393
MemberProfile memberProfile = iterator.next();
94-
LOG.info("Profile exists for \'{}\'", email);
95-
String firstName = memberProfile.getFirstName() != null ? memberProfile.getFirstName() : "";
96-
String lastName = memberProfile.getLastName() != null ? memberProfile.getLastName() : "";
94+
LOG.info("Profile exists for '{}'", email);
95+
String firstName = memberProfile.getFirstName() != null ? memberProfile.getFirstName() : "";
96+
String lastName = memberProfile.getLastName() != null ? memberProfile.getLastName() : "";
9797
Set<String> roles = roleServices.findUserRoles(memberProfile.getId()).stream().map(role -> role.getRole()).collect(Collectors.toSet());
9898
Set<String> permissions = rolePermissionServices.findUserPermissions(memberProfile.getId()).stream().map(permission -> permission.name()).collect(Collectors.toSet());
9999

100100
Map<String, Object> newAttributes = new HashMap<>();
101-
newAttributes.put("email", memberProfile.getWorkEmail());
102-
newAttributes.put("name", firstName + ' ' + lastName);
103-
newAttributes.put("picture", "");
101+
newAttributes.put("email", memberProfile.getWorkEmail());
102+
newAttributes.put("name", firstName + ' ' + lastName);
103+
newAttributes.put("picture", "");
104104
newAttributes.put("roles", roles);
105105
newAttributes.put("permissions", permissions);
106-
newAttributes.put("openIdToken", "");
106+
JWTClaimsSet newSet = null;
107+
try {
108+
JWT parse = JWTParser.parse(jwt.getValue());
109+
JWTClaimsSet jwtClaimsSet = parse.getJWTClaimsSet();
110+
Map<String, Object> claims = new HashMap<>();
111+
claims.put("email", memberProfile.getWorkEmail());
112+
claims.put("name", firstName + ' ' + lastName);
113+
claims.put("picture", "");
114+
claims.put("exp", ((Date) jwtClaimsSet.getClaims().get("exp")).getTime());
115+
claims.put("iss", jwtClaimsSet.getClaims().get("iss"));
116+
claims.put("aud", jwtClaimsSet.getClaims().get("aud"));
117+
claims.put("sub", jwtClaimsSet.getClaims().get("sub"));
118+
newSet = JWTClaimsSet.parse(claims);
119+
Optional<String> signed = generator.generateToken(claims);
120+
121+
String token = signed.get();
122+
if (newSet != null) newAttributes.put("openIdToken", token);
123+
} catch (ParseException e) {
124+
throw new RuntimeException(e);
125+
}
107126

108127
LOG.info("Building authentication");
109128
Authentication updatedAuth = Authentication.build(email, roles, newAttributes);
110129
LOG.info("Publishing login");
111-
eventPublisher.publishEvent(new LoginSuccessfulEvent(updatedAuth, null, Locale.getDefault()));
112-
// Store the old JWT to allow the user to revert the impersonation.
130+
eventPublisher.publishEvent(new LoginSuccessfulEvent(updatedAuth, null, Locale.getDefault()));
131+
// Store the old JWT to allow the user to revert the impersonation.
113132
LOG.info("Attempting to swap tokens");
114-
return ((MutableHttpResponse)loginHandler.loginSuccess(updatedAuth, request)).cookie(
115-
new NettyCookie(originalJWT, jwt.getValue()).path("/").sameSite(SameSite.Strict)
116-
.maxAge(jwt.getMaxAge()));
117-
}
133+
return ((MutableHttpResponse) loginHandler.loginSuccess(updatedAuth, request)).cookie(
134+
new NettyCookie(originalJWT, jwt.getValue()).path("/").sameSite(SameSite.Strict)
135+
.maxAge(jwt.getMaxAge()));
136+
}
118137
}
119138

120139
@Produces(MediaType.TEXT_HTML)
@@ -127,13 +146,13 @@ public HttpResponse<Object> revert(HttpRequest<?> request) {
127146
// Swap the OJWT back to the JWT and remove the original JWT
128147
Set<Cookie> cookies = new HashSet<Cookie>();
129148
cookies.add(new NettyCookie(JWT, ojwt.getValue()).path("/")
130-
.sameSite(SameSite.Strict)
131-
.maxAge(ojwt.getMaxAge()).httpOnly());
149+
.sameSite(SameSite.Strict)
150+
.maxAge(ojwt.getMaxAge()).httpOnly());
132151
cookies.add(new NettyCookie(originalJWT, "").path("/").maxAge(0));
133152

134153
// Redirect to "/" while setting the cookies.
135154
return HttpResponse.temporaryRedirect(URI.create("/"))
136-
.cookies(cookies);
155+
.cookies(cookies);
137156
}
138157
}
139158
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
micronaut:
2+
server:
3+
cors:
4+
enabled: true
5+
configurations:
6+
web:
7+
allowedOriginsRegex:
8+
- ^http(|s):\/\/localhost:.*$
9+
---
10+
datasources:
11+
default:
12+
url: ${JDBC_URL:`jdbc:postgresql://localhost:5432/checkinsdb`}
13+
username: postgres
14+
password: "postgres"
15+
---
16+
flyway:
17+
enabled: enabled
18+
datasources:
19+
default:
20+
locations:
21+
- "classpath:db/common"
22+
- "classpath:db/dev"

server/src/main/resources/application.yml

+5
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ micronaut:
4848
secret: ${ OAUTH_REFRESH_TOKEN_SECRET:'pleaseChangeThisSecretForANewOne' }
4949
access-token:
5050
expiration: 28800
51+
signatures:
52+
secret:
53+
generator:
54+
jws-algorithm: HS256
55+
secret: ${ JWT_GENERATOR_SIGNATURE_SECRET:'pleaseChangeThisSecretForANewOne' }
5156
oauth2:
5257
callback-uri: ${ OAUTH_CALLBACK_URI }
5358
clients:

server/src/test/java/com/objectcomputing/checkins/security/ImpersonationControllerTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class ImpersonationControllerTest extends TestContainersSuite implements MemberP
4646

4747
private MemberProfile nonAdmin;
4848
private MemberProfile admin;
49-
private String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJjb21wYW55IjoiRnV0dXJlRWQiLCJzdWIiOjEsImlzcyI6Imh0dHA6XC9cL2Z1dHVyZWVkLmRldlwvYXBpXC92MVwvc3R1ZGVudFwvbG9naW5cL3VzZXJuYW1lIiwiaWF0IjoiMTQyNzQyNjc3MSIsImV4cCI6IjE0Mjc0MzAzNzEiLCJuYmYiOiIxNDI3NDI2NzcxIiwianRpIjoiNmFlZDQ3MGFiOGMxYTk0MmE0MTViYTAwOTBlMTFlZTUifQ.MmM2YTUwMjEzYTE0OGNhNjk5Y2Y2MjEwZDdkN2Y1OTQ2NWVhZTdmYmI4OTA5YmM1Y2QwYTMzZjUwNTgwY2Y0MQ";
49+
private String jwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImVlMTkzZDQ2NDdhYjRhMzU4NWFhOWIyYjNiNDg0YTg3YWE2OGJiNDIiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4MzIxNDAwMjA1OTMtMTJxNWh2b2psajc0N24xdnV1cG9rNXZ2aTk5NXJlYzIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4MzIxNDAwMjA1OTMtMTJxNWh2b2psajc0N24xdnV1cG9rNXZ2aTk5NXJlYzIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTczOTEzNDIwMzMxOTg1MTI1ODEiLCJoZCI6Im9iamVjdGNvbXB1dGluZy5jb20iLCJlbWFpbCI6ImtpbWJlcmxpbm1Ab2JqZWN0Y29tcHV0aW5nLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoibENmaWkyWEdYaFEtOC1TeEs2N2Z5QSIsIm5vbmNlIjoiYzRkNTkyNjQtYmY3Ny00M2MwLTliMGMtYjZhYzZjNzgxN2Y1IiwibmFtZSI6Ik1pY2hhZWwgS2ltYmVybGluIiwicGljdHVyZSI6Imh0dHBzOi8vbGgzLmdvb2dsZXVzZXJjb250ZW50LmNvbS9hL0FDZzhvY0tIRHNOdlprclpaUS0xN1NiTTJqNWdXdDdpa09SOTI2UXZ0WFZSSnU5cTRRQl82UHZ4PXM5Ni1jIiwiZ2l2ZW5fbmFtZSI6Ik1pY2hhZWwiLCJmYW1pbHlfbmFtZSI6IktpbWJlcmxpbiIsImlhdCI6MTc0MjQxMjQxMiwiZXhwIjoxNzQyNDE2MDEyfQ.gVgncacfKyzr5_NSMOAu7xGRKnlBA_tg_1JCcfJ4KBMa0Xnvvhhx9Fix252_SQ5xpaG7b-sDApEl1fTcMKqUfYNrj-5s3SzWzCoV6g4NI44YN1j_KyjKFUZ6RWJ79U1U8_DA_wBATQvA-_NNMT8WL9A4muolH-cjoXixymCkl6dgd5QjriEOQC20QYCSbp_nHpeAgbl6fvCh8ZvpK2bHb6zwDmjYuN_xRuQyztfKS1X24nSB0k840Jdw2kUSzXZITUE6zOskapYKlTxcP8BGQ3GPydrNFxGy9Ec6GR3I0J-QhcE5b4mBijae9hgWw6Yz93SSjpm_w8w9D_fO_6yVSA";
5050

5151
@BeforeEach
5252
void setUp() {

0 commit comments

Comments
 (0)