-
Notifications
You must be signed in to change notification settings - Fork 828
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Do not expire invitations on GET requests (#1128)
At the moment, when the user visits: ``` /invitations/accept?code=some-code ``` the invitation code from their email is immediately expired and replaced with a newly generated code which is put in a hidden input in the HTML form. Each time the user submits the form, the code is expired and (if necessary - e.g. if there's a validation issue) replaced with a new one. This is fine so long as the user fills the form in immediately, but there are a number of edge cases where this approach causes usability problems: 1) If the user refreshes the page it will tell them their invitation has expired. 2) If the user closes the tab without submitting the form, and then follows the invitation link from their email later it will show as expired. 3) If the user's email client or web browser pre-fetches the link for any reason (e.g. virus scanning / spam detection / performance optimisation) then the link will not work when they follow it for real. The third issue is the most serious. We (GOV.UK PaaS) have had some very users working in places that pre-fetch links in emails (for some reason or other), and this means they're completely unable to accept invitations. Judging from the irate support tickets we've had from these users the experience is pretty frustrating. This commit changes the GET request to /invitations/accept so that it does not expire the token (unless the invitation is being auto-accepted). The POST handler is unchanged, so if the user actually submits the form then the token will change (as it did before), even if there's a validation issue that prevents the invitation being accepted. This change fixes the usability issues, and makes the behaviour more consistent with HTTP's semantics (in the sense that GET requests should be "safe" - should not modify the state of the server).
- Loading branch information
1 parent
4f7ec0c
commit 7ccb94e
Showing
7 changed files
with
78 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -150,8 +150,7 @@ public void testAcceptInvitationsPage() throws Exception { | |
codeData.put("email", "[email protected]"); | ||
codeData.put("client_id", "client-id"); | ||
codeData.put("redirect_uri", "blah.test.com"); | ||
when(expiringCodeStore.retrieveCode("code", IdentityZoneHolder.get().getId())).thenReturn(createCode(codeData), null); | ||
when(expiringCodeStore.generateCode(anyString(), any(), eq(INVITATION.name()), eq(IdentityZoneHolder.get().getId()))).thenReturn(createCode(codeData)); | ||
when(expiringCodeStore.peekCode("code", IdentityZoneHolder.get().getId())).thenReturn(createCode(codeData), null); | ||
IdentityProvider provider = new IdentityProvider(); | ||
provider.setType(OriginKeys.UAA); | ||
when(providerProvisioning.retrieveByOrigin(any(), any())).thenReturn(provider); | ||
|
@@ -193,8 +192,7 @@ public void incorrectCodeIntent() throws Exception { | |
@Test | ||
public void acceptInvitePage_for_unverifiedSamlUser() throws Exception { | ||
Map<String,String> codeData = getInvitationsCode("test-saml"); | ||
when(expiringCodeStore.retrieveCode("the_secret_code", IdentityZoneHolder.get().getId())).thenReturn(createCode(codeData)); | ||
when(expiringCodeStore.generateCode(anyString(), any(), eq(INVITATION.name()), eq(IdentityZoneHolder.get().getId()))).thenReturn(createCode(codeData)); | ||
when(expiringCodeStore.peekCode("the_secret_code", IdentityZoneHolder.get().getId())).thenReturn(createCode(codeData)); | ||
IdentityProvider provider = new IdentityProvider(); | ||
SamlIdentityProviderDefinition definition = new SamlIdentityProviderDefinition() | ||
.setMetaDataLocation("http://test.saml.com") | ||
|
@@ -220,8 +218,7 @@ public void acceptInvitePage_for_unverifiedSamlUser() throws Exception { | |
@Test | ||
public void acceptInvitePage_for_unverifiedOIDCUser() throws Exception { | ||
Map<String,String> codeData = getInvitationsCode("test-oidc"); | ||
when(expiringCodeStore.retrieveCode("the_secret_code", IdentityZoneHolder.get().getId())).thenReturn(createCode(codeData)); | ||
when(expiringCodeStore.generateCode(anyString(), any(), eq(INVITATION.name()), eq(IdentityZoneHolder.get().getId()))).thenReturn(createCode(codeData)); | ||
when(expiringCodeStore.peekCode("the_secret_code", IdentityZoneHolder.get().getId())).thenReturn(createCode(codeData)); | ||
|
||
OIDCIdentityProviderDefinition definition = new OIDCIdentityProviderDefinition(); | ||
definition.setAuthUrl(new URL("https://oidc10.auth.url")); | ||
|
@@ -247,8 +244,7 @@ public void acceptInvitePage_for_unverifiedOIDCUser() throws Exception { | |
@Test | ||
public void acceptInvitePage_for_unverifiedLdapUser() throws Exception { | ||
Map<String, String> codeData = getInvitationsCode(LDAP); | ||
when(expiringCodeStore.retrieveCode("the_secret_code", IdentityZoneHolder.get().getId())).thenReturn(createCode(codeData)); | ||
when(expiringCodeStore.generateCode(anyString(), any(), eq(INVITATION.name()), eq(IdentityZoneHolder.get().getId()))).thenReturn(createCode(codeData)); | ||
when(expiringCodeStore.peekCode("the_secret_code", IdentityZoneHolder.get().getId())).thenReturn(createCode(codeData)); | ||
|
||
IdentityProvider provider = new IdentityProvider(); | ||
provider.setType(LDAP); | ||
|
@@ -263,7 +259,7 @@ public void acceptInvitePage_for_unverifiedLdapUser() throws Exception { | |
.andExpect(content().string(containsString("Email: " + "[email protected]"))) | ||
.andExpect(content().string(containsString("Sign in with enterprise credentials:"))) | ||
.andExpect(content().string(containsString("username"))) | ||
.andExpect(model().attribute("code", "code")) | ||
.andExpect(model().attribute("code", "the_secret_code")) | ||
.andReturn(); | ||
} | ||
|
||
|
@@ -397,8 +393,7 @@ public void acceptInvitePage_for_verifiedUser() throws Exception { | |
codeData.put("email", "[email protected]"); | ||
codeData.put("origin", "some-origin"); | ||
|
||
when(expiringCodeStore.retrieveCode("the_secret_code", IdentityZoneHolder.get().getId())).thenReturn(createCode(codeData), null); | ||
when(expiringCodeStore.generateCode(anyString(), any(), eq(INVITATION.name()), eq(IdentityZoneHolder.get().getId()))).thenReturn(createCode(codeData)); | ||
when(expiringCodeStore.peekCode("the_secret_code", IdentityZoneHolder.get().getId())).thenReturn(createCode(codeData), null); | ||
when(invitationsService.acceptInvitation(anyString(), eq(""))).thenReturn(new AcceptedInvitation("blah.test.com", new ScimUser())); | ||
IdentityProvider provider = new IdentityProvider(); | ||
provider.setType(OriginKeys.UAA); | ||
|
@@ -668,10 +663,8 @@ public void testAcceptInvite_displaysConsentText() throws Exception { | |
Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); | ||
String codeDataString = JsonUtils.writeValueAsString(codeData); | ||
ExpiringCode expiringCode = new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()); | ||
when(expiringCodeStore.retrieveCode("thecode", IdentityZoneHolder.get().getId())) | ||
when(expiringCodeStore.peekCode("thecode", IdentityZoneHolder.get().getId())) | ||
.thenReturn(expiringCode, null); | ||
when(expiringCodeStore.generateCode(anyString(), any(), eq(INVITATION.name()), eq(IdentityZoneHolder.get().getId()))) | ||
.thenReturn(expiringCode); | ||
|
||
mockMvc.perform(get("/invitations/accept") | ||
.param("code", "thecode")) | ||
|
@@ -714,6 +707,8 @@ public void testAcceptInvite_displaysErrorMessageIfConsentNotChecked() throws Ex | |
Map<String,String> codeData = getInvitationsCode(OriginKeys.UAA); | ||
String codeDataString = JsonUtils.writeValueAsString(codeData); | ||
ExpiringCode expiringCode = new ExpiringCode("thecode", new Timestamp(1), codeDataString, INVITATION.name()); | ||
when(expiringCodeStore.peekCode(anyString(), eq(IdentityZoneHolder.get().getId()))) | ||
.thenReturn(expiringCode); | ||
when(expiringCodeStore.retrieveCode(anyString(), eq(IdentityZoneHolder.get().getId()))) | ||
.thenReturn(expiringCode); | ||
when(expiringCodeStore.generateCode(anyString(), any(), eq(INVITATION.name()), eq(IdentityZoneHolder.get().getId()))) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters