1313import jakarta .servlet .http .HttpServletRequest ;
1414import jakarta .servlet .http .HttpSession ;
1515import jakarta .validation .Valid ;
16+ import myconext .config .CreateFromInstitutionProperties ;
1617import myconext .cron .DisposableEmailProviders ;
1718import myconext .exceptions .DuplicateUserEmailException ;
1819import myconext .exceptions .ForbiddenException ;
2930import myconext .security .EmailGuessingPrevention ;
3031import myconext .security .UserAuthentication ;
3132import myconext .security .VerificationCodeGenerator ;
33+ import myconext .util .CreateFromInstitutionReturnUrlSupport ;
3234import myconext .verify .AttributeMapper ;
3335import myconext .verify .VerifyState ;
3436import org .apache .commons .logging .Log ;
4850import org .springframework .util .StringUtils ;
4951import org .springframework .web .bind .annotation .*;
5052import org .springframework .web .client .RestTemplate ;
53+ import org .springframework .web .server .ResponseStatusException ;
5154import org .springframework .web .util .UriComponents ;
5255import 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 );
0 commit comments