Skip to content

Commit 69cdf60

Browse files
authored
Merge pull request #1181 from OpenConext/feature/#1042-add-return-url-to-create-from-institution
Feature/#1042 add return url to create from institution
2 parents 72ca614 + 4332d2e commit 69cdf60

14 files changed

Lines changed: 315 additions & 16 deletions

account-gui/vite.config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,16 @@ export default defineConfig({
2626
changeOrigin: false,
2727
secure: false
2828
},
29+
'/create-from-institution-login': {
30+
target: 'http://localhost:8081',
31+
changeOrigin: false,
32+
secure: false
33+
},
2934
'/tiqr': {
3035
target: 'http://localhost:8081',
3136
changeOrigin: false,
3237
secure: false
3338
}
3439
}
3540
},
36-
});
41+
});

myconext-gui/src/api/index.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,12 @@ export function sendDeactivationPhoneCode() {
236236
}
237237

238238
// Create from Institution
239-
export function startCreateFromInstitutionFlow(forceAuth = false) {
240-
return fetchJson("/myconext/api/sp/create-from-institution?forceAuth=" + forceAuth);
239+
export function startCreateFromInstitutionFlow(forceAuth = false, returnTo = null) {
240+
const params = new URLSearchParams({forceAuth: `${forceAuth}`});
241+
if (returnTo) {
242+
params.set("return_to", returnTo);
243+
}
244+
return fetchJson(`/myconext/api/sp/create-from-institution?${params.toString()}`);
241245
}
242246

243247
export function createInstitutionEduID(email, hash, newUser) {
@@ -294,5 +298,3 @@ export function reValidatePhoneCode(phoneVerification) {
294298
export function reportError(error) {
295299
return postPutJson("/myconext/api/sp/error", error, "post");
296300
}
297-
298-

myconext-gui/src/routes/AwaitLinkFromInstitutionMail.svelte

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@
3535
3636
const verifyCode = code => {
3737
createFromInstitutionVerify(hash, code)
38-
.then(() => {
38+
.then(res => {
39+
if (res.location) {
40+
window.location.href = res.location;
41+
return;
42+
}
3943
navigate("/security?new=true")
4044
}).catch(e => {
4145
if (e.status === 403 || e.status === 400) {
@@ -123,4 +127,4 @@
123127
</div>
124128

125129
</Modal>
126-
</div>
130+
</div>

myconext-gui/src/routes/CreateFromInstitution.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
1717
const startFlow = () => {
1818
busy = true;
19-
startCreateFromInstitutionFlow().then(res => {
19+
const returnTo = new URLSearchParams(window.location.search).get("return_to");
20+
startCreateFromInstitutionFlow(false, returnTo).then(res => {
2021
window.location.href = res.url;
2122
})
2223

myconext-server/src/main/java/myconext/api/AccountLinkerController.java

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import jakarta.servlet.http.HttpServletRequest;
1414
import jakarta.servlet.http.HttpSession;
1515
import jakarta.validation.Valid;
16+
import myconext.config.CreateFromInstitutionProperties;
1617
import myconext.cron.DisposableEmailProviders;
1718
import myconext.exceptions.DuplicateUserEmailException;
1819
import myconext.exceptions.ForbiddenException;
@@ -29,6 +30,7 @@
2930
import myconext.security.EmailGuessingPrevention;
3031
import myconext.security.UserAuthentication;
3132
import myconext.security.VerificationCodeGenerator;
33+
import myconext.util.CreateFromInstitutionReturnUrlSupport;
3234
import myconext.verify.AttributeMapper;
3335
import myconext.verify.VerifyState;
3436
import org.apache.commons.logging.Log;
@@ -48,6 +50,7 @@
4850
import org.springframework.util.StringUtils;
4951
import org.springframework.web.bind.annotation.*;
5052
import org.springframework.web.client.RestTemplate;
53+
import org.springframework.web.server.ResponseStatusException;
5154
import org.springframework.web.util.UriComponents;
5255
import org.springframework.web.util.UriComponentsBuilder;
5356

@@ -100,6 +103,7 @@ public class AccountLinkerController implements UserAuthentication {
100103
private final String mijnEduIDEntityId;
101104
private final String schacHomeOrganization;
102105
private final boolean createEduIDInstitutionEnabled;
106+
private final List<String> createFromInstitutionAllowedReturnDomains;
103107

104108
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(4);
105109
private final RestTemplate restTemplate = new RestTemplate();
@@ -141,6 +145,7 @@ public AccountLinkerController(
141145
@Value("${linked_accounts.removal-duration-days-validated}") long removalValidatedDurationDays,
142146
@Value("${account_linking.myconext_sp_entity_id}") String myConextSpEntityId,
143147
@Value("${feature.create_eduid_institution_enabled}") boolean createEduIDInstitutionEnabled,
148+
CreateFromInstitutionProperties createFromInstitutionProperties,
144149
@Value("${email_guessing_sleep_millis}") int emailGuessingSleepMillis,
145150
@Value("${verify.client_id}") String verifyClientId,
146151
@Value("${verify.secret}") String verifySecret,
@@ -174,6 +179,7 @@ public AccountLinkerController(
174179
this.removalValidatedDurationDays = removalValidatedDurationDays;
175180
this.myConextSpEntityId = myConextSpEntityId;
176181
this.createEduIDInstitutionEnabled = createEduIDInstitutionEnabled;
182+
this.createFromInstitutionAllowedReturnDomains = createFromInstitutionProperties.getReturnUrlAllowedDomains();
177183
this.emailGuessingPreventor = new EmailGuessingPrevention(emailGuessingSleepMillis);
178184
this.verifyClientId = verifyClientId;
179185
this.verifySecret = verifySecret;
@@ -213,9 +219,15 @@ public ResponseEntity startIdPLinkAccountFlow(@PathVariable("id") String id,
213219
@GetMapping("/sp/create-from-institution")
214220
@Hidden
215221
public ResponseEntity<Map<String, String>> createFromInstitution(HttpServletRequest request,
216-
@RequestParam(value = "forceAuth", required = false, defaultValue = "false") boolean forceAuth) throws UnsupportedEncodingException {
222+
@RequestParam(value = "forceAuth", required = false, defaultValue = "false") boolean forceAuth,
223+
@RequestParam(value = "return_to", required = false) String returnTo) throws UnsupportedEncodingException {
217224
LOG.info("Start create from institution");
218-
String state = request.getSession(true).getId();
225+
HttpSession session = request.getSession(true);
226+
String state = session.getId();
227+
String validatedReturnUrl = validateReturnUrl(returnTo);
228+
session.setAttribute("create_from_institution_return_url", validatedReturnUrl);
229+
LOG.info(String.format("Create-from-institution start state=%s, raw return_to=%s, validated return_to=%s",
230+
state, returnTo, validatedReturnUrl));
219231
UriComponents uriComponents = doStartLinkAccountFlow(state, spCreateFromInstitutionRedirectUri, forceAuth, myConextSpEntityId);
220232
return ResponseEntity.ok(Collections.singletonMap("url", uriComponents.toUriString()));
221233
}
@@ -251,6 +263,9 @@ public ResponseEntity spCreateFromInstitutionRedirect(HttpServletRequest request
251263
return eppnAlreadyLinkedOptional.get();
252264
}
253265
RequestInstitutionEduID requestInstitutionEduID = new RequestInstitutionEduID(hash(), userInfo);
266+
requestInstitutionEduID.setReturnUrl((String) session.getAttribute("create_from_institution_return_url"));
267+
LOG.info(String.format("Create-from-institution oidc redirect state=%s, request hash=%s, stored return_to=%s",
268+
storedState, requestInstitutionEduID.getHash(), requestInstitutionEduID.getReturnUrl()));
254269
requestInstitutionEduIDRepository.save(requestInstitutionEduID);
255270
//Now the user needs to enter email and validate this email to finish up the registration
256271
String returnUri = this.spRedirectUrl + "/create-from-institution/link/" + requestInstitutionEduID.getHash();
@@ -367,6 +382,9 @@ public ResponseEntity createFromInstitutionFinish(HttpServletRequest request,
367382
manage);
368383
}
369384
user.setCreateFromInstitutionKey(hash());
385+
user.setCreateFromInstitutionReturnUrl(requestInstitutionEduID.getReturnUrl());
386+
LOG.info(String.format("Create-from-institution verify email=%s, newUser=%s, userId=%s, createFromInstitutionKey=%s, stored return_to=%s",
387+
email, user.isNewUser(), user.getId(), user.getCreateFromInstitutionKey(), user.getCreateFromInstitutionReturnUrl()));
370388
ResponseEntity<Object> responseEntity = saveOrUpdateLinkedAccountToUser(
371389
user,
372390
this.idpBaseRedirectUrl + "/create-from-institution-login?key=" + user.getCreateFromInstitutionKey(),
@@ -388,7 +406,7 @@ public ResponseEntity createFromInstitutionFinish(HttpServletRequest request,
388406
HttpSession session = request.getSession();
389407
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
390408

391-
return responseEntity;
409+
return responseWithJsonLocationForSpa(request, responseEntity);
392410
}
393411

394412
@GetMapping("/sp/oidc/link")
@@ -976,6 +994,31 @@ private Optional<ResponseEntity<Object>> checkEppnAlreadyLinked(String eppnAlrea
976994
return Optional.empty();
977995
}
978996

997+
private String validateReturnUrl(String returnTo) {
998+
if (!StringUtils.hasText(returnTo)) {
999+
return null;
1000+
}
1001+
return CreateFromInstitutionReturnUrlSupport
1002+
.validateAndNormalize(returnTo, this.createFromInstitutionAllowedReturnDomains)
1003+
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid return_to parameter"));
1004+
}
1005+
1006+
private ResponseEntity<?> responseWithJsonLocationForSpa(HttpServletRequest request, ResponseEntity<?> responseEntity) {
1007+
if (!acceptsJson(request) || !responseEntity.getStatusCode().is3xxRedirection()) {
1008+
return responseEntity;
1009+
}
1010+
URI location = responseEntity.getHeaders().getLocation();
1011+
if (location == null) {
1012+
return responseEntity;
1013+
}
1014+
return ResponseEntity.status(HttpStatus.CREATED).body(Collections.singletonMap("location", location.toString()));
1015+
}
1016+
1017+
private boolean acceptsJson(HttpServletRequest request) {
1018+
String accept = request.getHeader(HttpHeaders.ACCEPT);
1019+
return StringUtils.hasText(accept) && accept.contains(MediaType.APPLICATION_JSON_VALUE);
1020+
}
1021+
9791022
private Map<String, Object> requestUserInfo(String code, String oidcRedirectUri) {
9801023
HttpHeaders headers = new HttpHeaders();
9811024
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

myconext-server/src/main/java/myconext/api/LoginController.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
import jakarta.servlet.http.Cookie;
55
import jakarta.servlet.http.HttpServletRequest;
66
import jakarta.servlet.http.HttpServletResponse;
7+
import myconext.config.CreateFromInstitutionProperties;
78
import myconext.exceptions.UserNotFoundException;
89
import myconext.model.SamlAuthenticationRequest;
910
import myconext.model.User;
1011
import myconext.repository.AuthenticationRequestRepository;
1112
import myconext.repository.UserRepository;
13+
import myconext.util.CreateFromInstitutionReturnUrlSupport;
1214
import org.apache.commons.logging.Log;
1315
import org.apache.commons.logging.LogFactory;
1416
import org.springframework.beans.factory.annotation.Value;
@@ -28,6 +30,7 @@
2830
import java.net.URI;
2931
import java.util.Arrays;
3032
import java.util.HashMap;
33+
import java.util.List;
3134
import java.util.Map;
3235
import java.util.Optional;
3336

@@ -45,6 +48,7 @@ public class LoginController {
4548
private final UserRepository userRepository;
4649
private final AuthenticationRequestRepository authenticationRequestRepository;
4750
private final SecurityContextRepository securityContextRepository;
51+
private final List<String> createFromInstitutionAllowedReturnDomains;
4852

4953
public LoginController(UserRepository userRepository,
5054
AuthenticationRequestRepository authenticationRequestRepository,
@@ -76,7 +80,8 @@ public LoginController(UserRepository userRepository,
7680
@Value("${feature.service_desk_active}") boolean serviceDeskActive,
7781
@Value("${feature.use_remote_creation_for_affiliation}") boolean useRemoteCreationForAffiliation,
7882
@Value("${feature.enable_account_linking}") boolean enableAccountLinking,
79-
@Value("${feature.use_app}") boolean useApp
83+
@Value("${feature.use_app}") boolean useApp,
84+
CreateFromInstitutionProperties createFromInstitutionProperties
8085
) {
8186
this.config.put("basePath", basePath);
8287
this.config.put("loginUrl", basePath + "/login");
@@ -111,6 +116,7 @@ public LoginController(UserRepository userRepository,
111116
this.userRepository = userRepository;
112117
this.authenticationRequestRepository = authenticationRequestRepository;
113118
this.securityContextRepository = securityContextRepository;
119+
this.createFromInstitutionAllowedReturnDomains = createFromInstitutionProperties.getReturnUrlAllowedDomains();
114120
}
115121

116122
@GetMapping("/config")
@@ -242,11 +248,16 @@ private void doCreateUserFromInstitutionKey(HttpServletRequest request,
242248
HttpServletResponse response,
243249
String key,
244250
String redirectUrl) throws IOException {
251+
LOG.info(String.format("Create-from-institution-login called with key=%s, fallback redirectUrl=%s", key, redirectUrl));
245252
User user = userRepository.findUserByCreateFromInstitutionKey(key)
246253
.orElseThrow(() -> new UserNotFoundException("User by createFromInstitutionKey not found"));
247254
boolean newUser = user.isNewUser();
255+
String createFromInstitutionReturnUrl = user.getCreateFromInstitutionReturnUrl();
256+
LOG.info(String.format("Create-from-institution-login user=%s, newUser=%s, stored return_to=%s, allowed domains=%s",
257+
user.getEmail(), newUser, createFromInstitutionReturnUrl, createFromInstitutionAllowedReturnDomains));
248258
user.setNewUser(false);
249259
user.setCreateFromInstitutionKey(null);
260+
user.setCreateFromInstitutionReturnUrl(null);
250261
userRepository.save(user);
251262

252263
Cookie usernameCookie = new Cookie("username", user.getEmail());
@@ -260,7 +271,10 @@ private void doCreateUserFromInstitutionKey(HttpServletRequest request,
260271
SecurityContextHolder.getContext().setAuthentication(authentication);
261272
this.securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response);
262273

263-
String redirectLocation = redirectUrl + String.format("?new=%s", newUser ? "true" : "false");
274+
String baseRedirectUrl = CreateFromInstitutionReturnUrlSupport
275+
.validateAndNormalize(createFromInstitutionReturnUrl, createFromInstitutionAllowedReturnDomains)
276+
.orElse(redirectUrl);
277+
String redirectLocation = baseRedirectUrl + String.format(baseRedirectUrl.contains("?") ? "&new=%s" : "?new=%s", newUser ? "true" : "false");
264278

265279
LOG.info(String.format("User %s create from institutionKey. Redirecting to %s", user.getEmail(), redirectLocation));
266280

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package myconext.config;
2+
3+
import lombok.Getter;
4+
import lombok.Setter;
5+
import org.springframework.boot.context.properties.ConfigurationProperties;
6+
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
10+
@Getter
11+
@Setter
12+
@ConfigurationProperties(prefix = "create-from-institution")
13+
public class CreateFromInstitutionProperties {
14+
15+
private List<String> returnUrlAllowedDomains = new ArrayList<>();
16+
}

myconext-server/src/main/java/myconext/model/RequestInstitutionEduID.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ public class RequestInstitutionEduID implements Serializable {
2828

2929
private Map<String, Object> userInfo;
3030

31+
@Setter
32+
private String returnUrl;
33+
3134
@Setter
3235
private CreateInstitutionEduID createInstitutionEduID;
3336

myconext-server/src/main/java/myconext/model/User.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ public class User implements Serializable, UserDetails {
7575
@Setter
7676
@Indexed
7777
private String createFromInstitutionKey;
78+
@Setter
79+
private String createFromInstitutionReturnUrl;
7880
//Attributes and surfSecureId can't be final because of Jackson serialization (despite what your IDE tells tou)
7981
private Map<String, List<String>> attributes = new HashMap<>();
8082
private Map<String, Object> surfSecureId = new HashMap<>();

myconext-server/src/main/java/myconext/security/SecurityConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package myconext.security;
22

33
import lombok.SneakyThrows;
4+
import myconext.config.CreateFromInstitutionProperties;
45
import myconext.crypto.KeyGenerator;
56
import myconext.geo.GeoLocation;
67
import myconext.mail.MailBox;
@@ -52,7 +53,7 @@
5253
import static org.springframework.security.config.Customizer.withDefaults;
5354

5455
@EnableWebSecurity
55-
@EnableConfigurationProperties
56+
@EnableConfigurationProperties(CreateFromInstitutionProperties.class)
5657
@EnableMethodSecurity
5758
@Configuration
5859
public class SecurityConfiguration {

0 commit comments

Comments
 (0)