diff --git a/source/java/org/alfresco/rest/api/Authentications.java b/source/java/org/alfresco/rest/api/Authentications.java index 5edccb814d..9415174732 100644 --- a/source/java/org/alfresco/rest/api/Authentications.java +++ b/source/java/org/alfresco/rest/api/Authentications.java @@ -32,7 +32,7 @@ public interface Authentications LoginTicketResponse createTicket(LoginTicket loginRequest, Parameters parameters); - LoginTicketResponse validateTicket(String ticket, Parameters parameters, WithResponse withResponse); + LoginTicketResponse validateTicket(String me, Parameters parameters, WithResponse withResponse); - void deleteTicket(String ticket, Parameters parameters, WithResponse withResponse); + void deleteTicket(String me, Parameters parameters, WithResponse withResponse); } diff --git a/source/java/org/alfresco/rest/api/authentications/AuthenticationTicketsEntityResource.java b/source/java/org/alfresco/rest/api/authentications/AuthenticationTicketsEntityResource.java index 0ca3d0f4f8..e98e8f2764 100644 --- a/source/java/org/alfresco/rest/api/authentications/AuthenticationTicketsEntityResource.java +++ b/source/java/org/alfresco/rest/api/authentications/AuthenticationTicketsEntityResource.java @@ -72,17 +72,17 @@ public class AuthenticationTicketsEntityResource implements EntityResourceAction return Collections.singletonList(result); } - @WebApiDescription(title = "Validate login ticket", description = "Validates the specified ticket is still valid.") + @WebApiDescription(title = "Validate login ticket", description = "Validate login ticket.") @Override - public LoginTicket readById(String ticket, Parameters parameters, WithResponse withResponse) + public LoginTicket readById(String me, Parameters parameters, WithResponse withResponse) { - return authentications.validateTicket(ticket, parameters, withResponse); + return authentications.validateTicket(me, parameters, withResponse); } @WebApiDescription(title = "Logout", description = "Logout.") @Override - public void delete(String ticket, Parameters parameters, WithResponse withResponse) + public void delete(String me, Parameters parameters, WithResponse withResponse) { - authentications.deleteTicket(ticket, parameters, withResponse); + authentications.deleteTicket(me, parameters, withResponse); } } diff --git a/source/java/org/alfresco/rest/api/impl/AuthenticationsImpl.java b/source/java/org/alfresco/rest/api/impl/AuthenticationsImpl.java index 100417e139..4491e71a35 100644 --- a/source/java/org/alfresco/rest/api/impl/AuthenticationsImpl.java +++ b/source/java/org/alfresco/rest/api/impl/AuthenticationsImpl.java @@ -21,8 +21,10 @@ package org.alfresco.rest.api.impl; import org.alfresco.repo.security.authentication.AuthenticationException; import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.Authorization; import org.alfresco.repo.security.authentication.TicketComponent; import org.alfresco.rest.api.Authentications; +import org.alfresco.rest.api.People; import org.alfresco.rest.api.model.LoginTicket; import org.alfresco.rest.api.model.LoginTicketResponse; import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; @@ -32,6 +34,7 @@ import org.alfresco.rest.framework.webscripts.WithResponse; import org.alfresco.service.cmr.security.AuthenticationService; import org.alfresco.util.PropertyCheck; import org.apache.commons.lang.StringUtils; +import org.springframework.extensions.surf.util.Base64; import org.springframework.extensions.webscripts.Status; /** @@ -39,6 +42,9 @@ import org.springframework.extensions.webscripts.Status; */ public class AuthenticationsImpl implements Authentications { + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String PARAM_ALF_TICKET = "alf_ticket"; + private AuthenticationService authenticationService; private TicketComponent ticketComponent; @@ -65,11 +71,11 @@ public class AuthenticationsImpl implements Authentications try { // get ticket - authenticationService.authenticate(loginRequest.getUsername(), loginRequest.getPassword().toCharArray()); + authenticationService.authenticate(loginRequest.getUserId(), loginRequest.getPassword().toCharArray()); LoginTicketResponse response = new LoginTicketResponse(); - response.setUsername(loginRequest.getUsername()); - response.setTicket(authenticationService.getCurrentTicket()); + response.setUserId(loginRequest.getUserId()); + response.setId(authenticationService.getCurrentTicket()); return response; } @@ -84,18 +90,19 @@ public class AuthenticationsImpl implements Authentications } @Override - public LoginTicketResponse validateTicket(String ticket, Parameters parameters, WithResponse withResponse) + public LoginTicketResponse validateTicket(String me, Parameters parameters, WithResponse withResponse) { - if (StringUtils.isEmpty(ticket)) + if (!People.DEFAULT_USER.equals(me)) { - throw new InvalidArgumentException("ticket can't be null or empty."); + throw new InvalidArgumentException("Invalid parameter: " + me); } + final String ticket = getTicket(parameters); try { - String ticketUser = ticketComponent.validateTicket(ticket); + final String ticketUser = ticketComponent.validateTicket(ticket); - String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + final String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); // do not go any further if tickets are different // or the user is not fully authenticated if (currentUser == null || !currentUser.equals(ticketUser)) @@ -108,23 +115,24 @@ public class AuthenticationsImpl implements Authentications withResponse.setStatus(Status.STATUS_NOT_FOUND); } LoginTicketResponse response = new LoginTicketResponse(); - response.setTicket(ticket); + response.setId(ticket); return response; } @Override - public void deleteTicket(String ticket, Parameters parameters, WithResponse withResponse) + public void deleteTicket(String me, Parameters parameters, WithResponse withResponse) { - if (StringUtils.isEmpty(ticket)) + if (!People.DEFAULT_USER.equals(me)) { - throw new InvalidArgumentException("ticket can't be null or empty."); + throw new InvalidArgumentException("Invalid parameter: " + me); } + final String ticket = getTicket(parameters); try { - String ticketUser = ticketComponent.validateTicket(ticket); + final String ticketUser = ticketComponent.validateTicket(ticket); - String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + final String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); // do not go any further if tickets are different // or the user is not fully authenticated if (currentUser == null || !currentUser.equals(ticketUser)) @@ -145,9 +153,40 @@ public class AuthenticationsImpl implements Authentications protected void validateLoginRequest(LoginTicket loginTicket) { - if (loginTicket == null || loginTicket.getUsername() == null || loginTicket.getPassword() == null) + if (loginTicket == null || loginTicket.getUserId() == null || loginTicket.getPassword() == null) { throw new InvalidArgumentException("Invalid login details."); } } + + protected String getTicket(Parameters parameters) + { + // First check the alf_ticket in the URL + final String alfTicket = parameters.getParameter(PARAM_ALF_TICKET); + if (StringUtils.isNotEmpty(alfTicket)) + { + return alfTicket; + } + + // Check the Authorization header + final String authorization = parameters.getRequest().getHeader(AUTHORIZATION_HEADER); + if (StringUtils.isEmpty(authorization)) + { + throw new InvalidArgumentException("Authorization header is required."); + } + + final String[] authorizationParts = authorization.split(" "); + if (!authorizationParts[0].equalsIgnoreCase("basic")) + { + throw new InvalidArgumentException("Authorization '" + authorizationParts[0] + "' not supported."); + } + + final String decodedAuthorisation = new String(Base64.decode(authorizationParts[1])); + Authorization authObj = new Authorization(decodedAuthorisation); + if (!authObj.isTicket()) + { + throw new InvalidArgumentException("Ticket base authentication required."); + } + return authObj.getTicket(); + } } diff --git a/source/java/org/alfresco/rest/api/model/LoginTicket.java b/source/java/org/alfresco/rest/api/model/LoginTicket.java index d3327277bd..4aa3f540d6 100644 --- a/source/java/org/alfresco/rest/api/model/LoginTicket.java +++ b/source/java/org/alfresco/rest/api/model/LoginTicket.java @@ -24,18 +24,18 @@ package org.alfresco.rest.api.model; */ public class LoginTicket { - protected String username; + protected String userId; protected String password; - protected String ticket; + protected String id; - public String getUsername() + public String getUserId() { - return username; + return userId; } - public void setUsername(String username) + public void setUserId(String userId) { - this.username = username; + this.userId = userId; } public String getPassword() @@ -48,23 +48,23 @@ public class LoginTicket this.password = password; } - public String getTicket() + public String getId() { - return ticket; + return id; } - public void setTicket(String ticket) + public void setId(String id) { - this.ticket = ticket; + this.id = id; } @Override public String toString() { final StringBuilder sb = new StringBuilder(150); - sb.append("LoginTicket [username=").append(username) + sb.append("LoginTicket [userId=").append(userId) .append(", password=").append(password) - .append(", ticket=").append(ticket) + .append(", id=").append(id) .append(']'); return sb.toString(); } diff --git a/source/java/org/alfresco/rest/api/model/LoginTicketResponse.java b/source/java/org/alfresco/rest/api/model/LoginTicketResponse.java index 24a1f53235..7c469d8c9a 100644 --- a/source/java/org/alfresco/rest/api/model/LoginTicketResponse.java +++ b/source/java/org/alfresco/rest/api/model/LoginTicketResponse.java @@ -46,9 +46,9 @@ public class LoginTicketResponse extends LoginTicket public String toString() { final StringBuilder sb = new StringBuilder(150); - sb.append("LoginTicketResponse [username=").append(username) + sb.append("LoginTicketResponse [userId=").append(userId) .append(", password=").append(password) - .append(", ticket=").append(ticket) + .append(", id=").append(id) .append(']'); return sb.toString(); } diff --git a/source/test-java/org/alfresco/rest/api/tests/AuthenticationsTest.java b/source/test-java/org/alfresco/rest/api/tests/AuthenticationsTest.java index 9a1d78480a..048cee2cb3 100644 --- a/source/test-java/org/alfresco/rest/api/tests/AuthenticationsTest.java +++ b/source/test-java/org/alfresco/rest/api/tests/AuthenticationsTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertNotNull; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.People; import org.alfresco.rest.api.model.LoginTicket; import org.alfresco.rest.api.model.LoginTicketResponse; import org.alfresco.rest.api.sites.SiteEntityResource; @@ -113,7 +114,7 @@ public class AuthenticationsTest extends AbstractBaseApiTest getAll(SiteEntityResource.class, null, paging, null, 401); /* - * user1 login + * user1 login - via alf_ticket parameter */ // User1 login request @@ -121,54 +122,60 @@ public class AuthenticationsTest extends AbstractBaseApiTest // Invalid login details post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 400); - loginRequest.setUsername(null); + loginRequest.setUserId(null); loginRequest.setPassword("user1Password"); // Invalid login details post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 400); - loginRequest.setUsername(user1); + loginRequest.setUserId(user1); loginRequest.setPassword(null); // Invalid login details post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 400); - loginRequest.setUsername(user1); + loginRequest.setUserId(user1); loginRequest.setPassword("user1Password"); // Authenticate and create a ticket HttpResponse response = post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 201); LoginTicketResponse loginResponse = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), LoginTicketResponse.class); - assertNotNull(loginResponse.getTicket()); - assertNotNull(loginResponse.getUsername()); + assertNotNull(loginResponse.getId()); + assertNotNull(loginResponse.getUserId()); // Get list of sites by appending the alf_ticket to the URL // e.g. .../alfresco/versions/1/sites/?alf_ticket=TICKET_57866258ea56c28491bb3e75d8355ebf6fbaaa23 - Map ticket = Collections.singletonMap("alf_ticket", loginResponse.getTicket()); + Map ticket = Collections.singletonMap("alf_ticket", loginResponse.getId()); getAll(SiteEntityResource.class, null, paging, ticket, 200); - // Validate ticket - response = getSingle("tickets", null, loginResponse.getTicket(), ticket, 200); - LoginTicketResponse validatedTicket = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), LoginTicketResponse.class); - assertEquals(loginResponse.getTicket(), validatedTicket.getTicket()); + // Unauthorized - Invalid ticket + getAll(SiteEntityResource.class, null, paging, Collections.singletonMap("alf_ticket", "TICKET_" + System.currentTimeMillis()), 401); - // Validate ticket - non-existent ticket - getSingle("tickets", null, "TICKET_" + System.currentTimeMillis(), ticket, 404); + // Validate ticket - Invalid parameter. Only '-me-' is supported + getSingle("tickets", null, loginResponse.getId(), ticket, 400); + + // Validate ticket + response = getSingle("tickets", null, People.DEFAULT_USER, ticket, 200); + LoginTicketResponse validatedTicket = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), LoginTicketResponse.class); + assertEquals(loginResponse.getId(), validatedTicket.getId()); + + // Validate ticket - Invalid parameter. Only '-me-' is supported + getSingle("tickets", null, loginResponse.getId(), ticket, 400); // Delete the ticket - Logout - delete("tickets", null, loginResponse.getTicket(), ticket, 204); + delete("tickets", null, People.DEFAULT_USER, ticket, 204); // Validate ticket - 401 as ticket has been invalidated so the API call is unauthorized - getSingle("tickets", null, loginResponse.getTicket(), ticket, 401); + getSingle("tickets", null, People.DEFAULT_USER, ticket, 401); // Check the ticket has been invalidated - the difference with the above is that the API call is authorized - getSingle("tickets", user1, loginResponse.getTicket(), ticket, 404); + getSingle("tickets", user1, People.DEFAULT_USER, ticket, 404); // Ticket has already been invalidated - delete("tickets", user1, loginResponse.getTicket(), ticket, 404); + delete("tickets", user1, People.DEFAULT_USER, ticket, 404); // Get list of site by appending the invalidated ticket getAll(SiteEntityResource.class, null, paging, ticket, 401); /* - * user2 login + * user2 login - Via Authorization header */ // User2 create a folder within his home folder (-my-) @@ -179,26 +186,26 @@ public class AuthenticationsTest extends AbstractBaseApiTest // User2 login request loginRequest = new LoginTicket(); - loginRequest.setUsername(user2); + loginRequest.setUserId(user2); loginRequest.setPassword("wrongPassword"); // Authentication failed - wrong password post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 403); - loginRequest.setUsername(user1); + loginRequest.setUserId(user1); loginRequest.setPassword("user2Password"); - // Authentication failed - username/password mismatch + // Authentication failed - userId/password mismatch post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 403); // Set the correct details - loginRequest.setUsername(user2); + loginRequest.setUserId(user2); loginRequest.setPassword("user2Password"); // Authenticate and create a ticket response = post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 201); loginResponse = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), LoginTicketResponse.class); - assertNotNull(loginResponse.getTicket()); - assertNotNull(loginResponse.getUsername()); + assertNotNull(loginResponse.getId()); + assertNotNull(loginResponse.getUserId()); - String encodedTicket = Base64.encodeBase64String(loginResponse.getTicket().getBytes()); + String encodedTicket = encodeB64(loginResponse.getId()); // Set the authorization (encoded ticket only) header rather than appending the ticket to the URL Map header = Collections.singletonMap("Authorization", "Basic " + encodedTicket); // Get children of user2 home folder @@ -206,67 +213,47 @@ public class AuthenticationsTest extends AbstractBaseApiTest List nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Document.class); assertEquals(1, nodes.size()); + // Validate ticket - Invalid parameter. Only '-me-' is supported + getSingle("tickets", null, loginResponse.getId(), null, header, 400); + // Validate ticket - user2 - response = getSingle("tickets", null, loginResponse.getTicket(), null, header, 200); + response = getSingle("tickets", null, People.DEFAULT_USER, null, header, 200); validatedTicket = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), LoginTicketResponse.class); - assertEquals(loginResponse.getTicket(), validatedTicket.getTicket()); + assertEquals(loginResponse.getId(), validatedTicket.getId()); // Try list children for user2 again. - // Encode Alfresco predefined username for ticket authentication, ROLE_TICKET, and the ticket - String encodedUsernameAndTicket = Base64.encodeBase64String(("ROLE_TICKET:" + loginResponse.getTicket()).getBytes()); - // Set the authorization (encoded username:ticket) header rather than appending the ticket to the URL - header = Collections.singletonMap("Authorization", "Basic " + encodedUsernameAndTicket); + // Encode Alfresco predefined userId for ticket authentication, ROLE_TICKET, and the ticket + String encodedUserIdAndTicket = encodeB64("ROLE_TICKET:" + loginResponse.getId()); + // Set the authorization (encoded userId:ticket) header rather than appending the ticket to the URL + header = Collections.singletonMap("Authorization", "Basic " + encodedUserIdAndTicket); // Get children of user2 home folder response = getAll(getNodeChildrenUrl(Nodes.PATH_MY), null, paging, null, header, 200); nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Document.class); assertEquals(1, nodes.size()); // Try list children for user2 again - appending ticket - ticket = Collections.singletonMap("alf_ticket", loginResponse.getTicket()); + ticket = Collections.singletonMap("alf_ticket", loginResponse.getId()); response = getAll(getNodeChildrenUrl(Nodes.PATH_MY), null, paging, ticket, 200); nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Document.class); assertEquals(1, nodes.size()); + // Try to validate the ticket without supplying the Authorization header or the alf_ticket param + getSingle("tickets", user2, People.DEFAULT_USER, null, null, 400); + + // Delete the ticket - Invalid parameter. Only '-me-' is supported + header = Collections.singletonMap("Authorization", "Basic " + encodedUserIdAndTicket); + delete("tickets", null, loginResponse.getId(), null, header, 400); + // Delete the ticket - Logout - header = Collections.singletonMap("Authorization", "Basic " + encodedUsernameAndTicket); - delete("tickets", null, loginResponse.getTicket(), null, header, 204); + delete("tickets", null, People.DEFAULT_USER, null, header, 204); // Get children of user2 home folder - invalidated ticket getAll(getNodeChildrenUrl(Nodes.PATH_MY), null, paging, null, header, 401); + } - /* - * user1 and user2 login - */ - loginRequest = new LoginTicket(); - loginRequest.setUsername(user1); - loginRequest.setPassword("user1Password"); - // Authenticate and create a ticket - response = post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 201); - LoginTicketResponse user1_loginResponse = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), LoginTicketResponse.class); - Map user1_ticket = Collections.singletonMap("alf_ticket", user1_loginResponse.getTicket()); - - loginRequest = new LoginTicket(); - loginRequest.setUsername(user2); - loginRequest.setPassword("user2Password"); - // Authenticate and create a ticket - response = post("tickets", null, RestApiUtil.toJsonAsString(loginRequest), 201); - LoginTicketResponse user2_loginResponse = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), LoginTicketResponse.class); - Map user2_ticket = Collections.singletonMap("alf_ticket", user2_loginResponse.getTicket()); - - // Validate ticket - user1 tries to validate user2's ticket - getSingle("tickets", null, user2_loginResponse.getTicket(), user1_ticket, 404); - - // Check that user2 ticket is still valid - getSingle("tickets", null, user2_loginResponse.getTicket(), user2_ticket, 200); - - // User1 tries to delete user2's ticket - delete("tickets", null, user2_loginResponse.getTicket(), user1_ticket, 404); - - // User1 logs out - delete("tickets", null, user1_loginResponse.getTicket(), user1_ticket, 204); - - // User2 logs out - delete("tickets", null, user2_loginResponse.getTicket(), user2_ticket, 204); + private String encodeB64(String str) + { + return Base64.encodeBase64String(str.getBytes()); } @Override