From 5a49e4643f708d6696b6f1ca05a0d9bad0463236 Mon Sep 17 00:00:00 2001 From: Florian Tack Date: Thu, 18 Jan 2018 08:57:31 +0000 Subject: [PATCH 01/42] Rename PasswordAuthenticationFailureEvent to IdentityProviderAuthenticationFailureEvent [#154404992] https://www.pivotaltracker.com/story/show/154404992 Signed-off-by: Torsten Luh --- docs/UAA-Audit.rst | 2 +- .../identity/uaa/audit/AuditEventType.java | 2 +- ...tyProviderAuthenticationFailureEvent.java} | 6 ++--- .../manager/AuthzAuthenticationManager.java | 4 ++-- .../AuthzAuthenticationManagerTests.java | 4 ++-- .../mock/audit/AuditCheckMockMvcTests.java | 23 +++++++++---------- 6 files changed, 20 insertions(+), 21 deletions(-) rename server/src/main/java/org/cloudfoundry/identity/uaa/authentication/event/{PasswordAuthenticationFailureEvent.java => IdentityProviderAuthenticationFailureEvent.java} (69%) diff --git a/docs/UAA-Audit.rst b/docs/UAA-Audit.rst index 897ccf38b45..f75c8f83c52 100644 --- a/docs/UAA-Audit.rst +++ b/docs/UAA-Audit.rst @@ -74,7 +74,7 @@ Authentication and Password Events - Happens: When a user successfully authenticates for the password login - Data Recorded: User ID and Username -* PasswordVerificationFailure +* IdentityProviderAuthenticationFailure - Happens: When a user authentication fails for the password login, and user exists - Data Recorded: User ID - Notes: Followed by a UserAuthenticationFailureEvent and PrincipalAuthenticationFailureEvent diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/audit/AuditEventType.java b/server/src/main/java/org/cloudfoundry/identity/uaa/audit/AuditEventType.java index 6197ce8a887..8c941afe9a1 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/audit/AuditEventType.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/audit/AuditEventType.java @@ -59,7 +59,7 @@ public enum AuditEventType { UserAccountUnlockedEvent(35), TokenRevocationEvent(36), IdentityProviderAuthenticationSuccess(37), - PasswordAuthenticationFailure(38), + IdentityProviderAuthenticationFailure(38), MfaAuthenticationSuccess(39), MfaAuthenticationFailure(40); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/event/PasswordAuthenticationFailureEvent.java b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/event/IdentityProviderAuthenticationFailureEvent.java similarity index 69% rename from server/src/main/java/org/cloudfoundry/identity/uaa/authentication/event/PasswordAuthenticationFailureEvent.java rename to server/src/main/java/org/cloudfoundry/identity/uaa/authentication/event/IdentityProviderAuthenticationFailureEvent.java index 815437fc709..a9fd79caf5f 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/event/PasswordAuthenticationFailureEvent.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/event/IdentityProviderAuthenticationFailureEvent.java @@ -6,10 +6,10 @@ import org.springframework.security.core.Authentication; import org.springframework.util.Assert; -public class PasswordAuthenticationFailureEvent extends AbstractUaaAuthenticationEvent { +public class IdentityProviderAuthenticationFailureEvent extends AbstractUaaAuthenticationEvent { private final UaaUser user; - public PasswordAuthenticationFailureEvent(UaaUser user, Authentication authentication) { + public IdentityProviderAuthenticationFailureEvent(UaaUser user, Authentication authentication) { super(authentication); this.user = user; } @@ -17,7 +17,7 @@ public PasswordAuthenticationFailureEvent(UaaUser user, Authentication authentic @Override public AuditEvent getAuditEvent() { Assert.notNull(user, "UaaUser cannot be null"); - return createAuditRecord(user.getId(), AuditEventType.PasswordAuthenticationFailure, + return createAuditRecord(user.getId(), AuditEventType.IdentityProviderAuthenticationFailure, getOrigin(getAuthenticationDetails()), user.getUsername()); } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/AuthzAuthenticationManager.java b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/AuthzAuthenticationManager.java index ae76fc5eb0e..e0b9a2a03e0 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/AuthzAuthenticationManager.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/AuthzAuthenticationManager.java @@ -23,7 +23,7 @@ import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.authentication.event.IdentityProviderAuthenticationSuccessEvent; -import org.cloudfoundry.identity.uaa.authentication.event.PasswordAuthenticationFailureEvent; +import org.cloudfoundry.identity.uaa.authentication.event.IdentityProviderAuthenticationFailureEvent; import org.cloudfoundry.identity.uaa.authentication.event.UnverifiedUserAuthenticationEvent; import org.cloudfoundry.identity.uaa.authentication.event.UserAuthenticationFailureEvent; import org.cloudfoundry.identity.uaa.authentication.event.UserNotFoundEvent; @@ -95,7 +95,7 @@ public Authentication authenticate(Authentication req) throws AuthenticationExce if (!passwordMatches) { logger.debug("Password did not match for user " + req.getName()); - publish(new PasswordAuthenticationFailureEvent(user, req)); + publish(new IdentityProviderAuthenticationFailureEvent(user, req)); publish(new UserAuthenticationFailureEvent(user, req)); } else { logger.debug("Password successfully matched for userId["+user.getUsername()+"]:"+user.getId()); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/AuthzAuthenticationManagerTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/AuthzAuthenticationManagerTests.java index 2cab4306c54..ed46b3026a9 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/AuthzAuthenticationManagerTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/AuthzAuthenticationManagerTests.java @@ -26,7 +26,7 @@ import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.authentication.event.IdentityProviderAuthenticationSuccessEvent; -import org.cloudfoundry.identity.uaa.authentication.event.PasswordAuthenticationFailureEvent; +import org.cloudfoundry.identity.uaa.authentication.event.IdentityProviderAuthenticationFailureEvent; import org.cloudfoundry.identity.uaa.authentication.event.UnverifiedUserAuthenticationEvent; import org.cloudfoundry.identity.uaa.authentication.event.UserAuthenticationFailureEvent; import org.cloudfoundry.identity.uaa.authentication.event.UserNotFoundEvent; @@ -208,7 +208,7 @@ public void invalidPasswordPublishesAuthenticationFailureEvent() { } catch (BadCredentialsException expected) { } - verify(publisher).publishEvent(isA(PasswordAuthenticationFailureEvent.class)); + verify(publisher).publishEvent(isA(IdentityProviderAuthenticationFailureEvent.class)); verify(publisher).publishEvent(isA(UserAuthenticationFailureEvent.class)); verify(db, times(0)).updateLastLogonTime(anyString()); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/audit/AuditCheckMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/audit/AuditCheckMockMvcTests.java index d35df3584b8..25a167dda84 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/audit/AuditCheckMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/audit/AuditCheckMockMvcTests.java @@ -12,12 +12,8 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.mock.audit; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.codec.binary.Base64; import org.cloudfoundry.identity.uaa.account.LostPasswordChangeRequest; import org.cloudfoundry.identity.uaa.account.event.PasswordChangeEvent; import org.cloudfoundry.identity.uaa.account.event.PasswordChangeFailureEvent; @@ -34,8 +30,8 @@ import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.event.ClientAuthenticationFailureEvent; import org.cloudfoundry.identity.uaa.authentication.event.ClientAuthenticationSuccessEvent; +import org.cloudfoundry.identity.uaa.authentication.event.IdentityProviderAuthenticationFailureEvent; import org.cloudfoundry.identity.uaa.authentication.event.IdentityProviderAuthenticationSuccessEvent; -import org.cloudfoundry.identity.uaa.authentication.event.PasswordAuthenticationFailureEvent; import org.cloudfoundry.identity.uaa.authentication.event.PrincipalAuthenticationFailureEvent; import org.cloudfoundry.identity.uaa.authentication.event.UnverifiedUserAuthenticationEvent; import org.cloudfoundry.identity.uaa.authentication.event.UserAuthenticationFailureEvent; @@ -60,9 +56,6 @@ import org.cloudfoundry.identity.uaa.zone.ClientServicesExtension; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.MultitenantJdbcClientDetailsService; - -import com.fasterxml.jackson.core.type.TypeReference; -import org.apache.commons.codec.binary.Base64; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -82,6 +75,12 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + import static org.cloudfoundry.identity.uaa.audit.AuditEventType.ClientCreateSuccess; import static org.cloudfoundry.identity.uaa.audit.AuditEventType.ClientUpdateSuccess; import static org.cloudfoundry.identity.uaa.audit.AuditEventType.GroupCreatedEvent; @@ -310,7 +309,7 @@ public void invalidPasswordLoginFailedTest() throws Exception { ArgumentCaptor captor = ArgumentCaptor.forClass(AbstractUaaEvent.class); verify(listener, atLeast(3)).onApplicationEvent(captor.capture()); - PasswordAuthenticationFailureEvent event1 = (PasswordAuthenticationFailureEvent)captor.getAllValues().get(0); + IdentityProviderAuthenticationFailureEvent event1 = (IdentityProviderAuthenticationFailureEvent)captor.getAllValues().get(0); UserAuthenticationFailureEvent event2 = (UserAuthenticationFailureEvent)captor.getAllValues().get(1); PrincipalAuthenticationFailureEvent event3 = (PrincipalAuthenticationFailureEvent)captor.getAllValues().get(2); assertEquals(testUser.getUserName(), event1.getUser().getUsername()); @@ -420,7 +419,7 @@ public void invalidPasswordLoginAuthenticateEndpointTest() throws Exception { ArgumentCaptor captor = ArgumentCaptor.forClass(AbstractUaaEvent.class); verify(listener, atLeast(3)).onApplicationEvent(captor.capture()); - PasswordAuthenticationFailureEvent event1 = (PasswordAuthenticationFailureEvent)captor.getAllValues().get(0); + IdentityProviderAuthenticationFailureEvent event1 = (IdentityProviderAuthenticationFailureEvent)captor.getAllValues().get(0); UserAuthenticationFailureEvent event2 = (UserAuthenticationFailureEvent)captor.getAllValues().get(1); PrincipalAuthenticationFailureEvent event3 = (PrincipalAuthenticationFailureEvent)captor.getAllValues().get(2); assertEquals(testUser.getUserName(), event1.getUser().getUsername()); From bb72c5a40f27d3f1314620beff9eb8a08c043a22 Mon Sep 17 00:00:00 2001 From: Torsten Luh Date: Mon, 29 Jan 2018 11:01:52 +0000 Subject: [PATCH 02/42] Add IdentityProviderAuthenticationFailureEvent for LDAP [#154404992] https://www.pivotaltracker.com/story/show/154404992 Signed-off-by: Florian Tack --- ...ityProviderAuthenticationFailureEvent.java | 28 +++++--- .../manager/AuthzAuthenticationManager.java | 15 ++-- .../DynamicLdapAuthenticationManager.java | 23 ++++++- ...DynamicZoneAwareAuthenticationManager.java | 11 ++- .../mock/audit/AuditCheckMockMvcTests.java | 4 +- .../uaa/mock/ldap/LdapMockMvcTests.java | 68 +++++++++++++------ 6 files changed, 106 insertions(+), 43 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/event/IdentityProviderAuthenticationFailureEvent.java b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/event/IdentityProviderAuthenticationFailureEvent.java index a9fd79caf5f..4a9f5473495 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/event/IdentityProviderAuthenticationFailureEvent.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/event/IdentityProviderAuthenticationFailureEvent.java @@ -2,26 +2,32 @@ import org.cloudfoundry.identity.uaa.audit.AuditEvent; import org.cloudfoundry.identity.uaa.audit.AuditEventType; -import org.cloudfoundry.identity.uaa.user.UaaUser; import org.springframework.security.core.Authentication; import org.springframework.util.Assert; public class IdentityProviderAuthenticationFailureEvent extends AbstractUaaAuthenticationEvent { - private final UaaUser user; - public IdentityProviderAuthenticationFailureEvent(UaaUser user, Authentication authentication) { + private String username; + private String origin; + + public String getUsername() { + return username; + } + + public String getOrigin() { + return origin; + } + + public IdentityProviderAuthenticationFailureEvent(Authentication authentication, String username, String origin) { super(authentication); - this.user = user; + this.username = username; + this.origin = origin; } @Override public AuditEvent getAuditEvent() { - Assert.notNull(user, "UaaUser cannot be null"); - return createAuditRecord(user.getId(), AuditEventType.IdentityProviderAuthenticationFailure, - getOrigin(getAuthenticationDetails()), user.getUsername()); - } - - public UaaUser getUser() { - return user; + Assert.notNull(username, "UaaUser cannot be null"); + return createAuditRecord(null, AuditEventType.IdentityProviderAuthenticationFailure, + getOrigin(getAuthenticationDetails()), username); } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/AuthzAuthenticationManager.java b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/AuthzAuthenticationManager.java index e0b9a2a03e0..fa0e21142f7 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/AuthzAuthenticationManager.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/AuthzAuthenticationManager.java @@ -12,18 +12,13 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.authentication.manager; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.Locale; - import org.cloudfoundry.identity.uaa.authentication.AccountNotVerifiedException; import org.cloudfoundry.identity.uaa.authentication.AuthenticationPolicyRejectionException; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; -import org.cloudfoundry.identity.uaa.authentication.event.IdentityProviderAuthenticationSuccessEvent; import org.cloudfoundry.identity.uaa.authentication.event.IdentityProviderAuthenticationFailureEvent; +import org.cloudfoundry.identity.uaa.authentication.event.IdentityProviderAuthenticationSuccessEvent; import org.cloudfoundry.identity.uaa.authentication.event.UnverifiedUserAuthenticationEvent; import org.cloudfoundry.identity.uaa.authentication.event.UserAuthenticationFailureEvent; import org.cloudfoundry.identity.uaa.authentication.event.UserNotFoundEvent; @@ -36,7 +31,6 @@ import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.cloudfoundry.identity.uaa.util.ObjectUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; - import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; @@ -49,6 +43,11 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.Locale; + public class AuthzAuthenticationManager implements AuthenticationManager, ApplicationEventPublisherAware { private final SanitizedLogFactory.SanitizedLog logger = SanitizedLogFactory.getLog(getClass()); @@ -95,7 +94,7 @@ public Authentication authenticate(Authentication req) throws AuthenticationExce if (!passwordMatches) { logger.debug("Password did not match for user " + req.getName()); - publish(new IdentityProviderAuthenticationFailureEvent(user, req)); + publish(new IdentityProviderAuthenticationFailureEvent(req, req.getName(), OriginKeys.UAA)); publish(new UserAuthenticationFailureEvent(user, req)); } else { logger.debug("Password successfully matched for userId["+user.getUsername()+"]:"+user.getId()); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicLdapAuthenticationManager.java b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicLdapAuthenticationManager.java index 5d36cfb486d..e67443bd57d 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicLdapAuthenticationManager.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicLdapAuthenticationManager.java @@ -1,5 +1,7 @@ package org.cloudfoundry.identity.uaa.authentication.manager; +import org.cloudfoundry.identity.uaa.authentication.event.IdentityProviderAuthenticationFailureEvent; +import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.impl.config.EnvironmentPropertiesFactoryBean; import org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMembershipManager; @@ -7,11 +9,14 @@ import org.cloudfoundry.identity.uaa.util.LdapUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -25,6 +30,7 @@ public class DynamicLdapAuthenticationManager implements AuthenticationManager { private LdapLoginAuthenticationManager ldapLoginAuthenticationManager; private AuthenticationManager manager; private AuthenticationManager ldapManagerActual; + private ApplicationEventPublisher eventPublisher; public DynamicLdapAuthenticationManager(LdapIdentityProviderDefinition definition, @@ -97,7 +103,12 @@ public LdapIdentityProviderDefinition getDefinition() { public Authentication authenticate(Authentication authentication) throws AuthenticationException { AuthenticationManager manager = getLdapAuthenticationManager(); if (manager!=null) { - return manager.authenticate(authentication); + try { + return manager.authenticate(authentication); + } catch (BadCredentialsException e) { + publish(new IdentityProviderAuthenticationFailureEvent(authentication, authentication.getName(), OriginKeys.LDAP)); + throw e; + } } throw new ProviderNotFoundException("LDAP provider not configured"); } @@ -109,4 +120,14 @@ public void destroy() { applicationContext.destroy(); } } + + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.eventPublisher = applicationEventPublisher; + } + + protected void publish(ApplicationEvent event) { + if (eventPublisher != null) { + eventPublisher.publishEvent(event); + } + } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManager.java b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManager.java index c161d9b3c3f..e15aeafd83d 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManager.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/DynamicZoneAwareAuthenticationManager.java @@ -25,6 +25,8 @@ import org.cloudfoundry.identity.uaa.util.ObjectUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; @@ -36,7 +38,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -public class DynamicZoneAwareAuthenticationManager implements AuthenticationManager { +public class DynamicZoneAwareAuthenticationManager implements AuthenticationManager, ApplicationEventPublisherAware { private final IdentityProviderProvisioning provisioning; private final AuthenticationManager internalUaaAuthenticationManager; @@ -44,6 +46,7 @@ public class DynamicZoneAwareAuthenticationManager implements AuthenticationMana private final ScimGroupExternalMembershipManager scimGroupExternalMembershipManager; private final ScimGroupProvisioning scimGroupProvisioning; private final LdapLoginAuthenticationManager ldapLoginAuthenticationManager; + private ApplicationEventPublisher eventPublisher; public DynamicZoneAwareAuthenticationManager(IdentityProviderProvisioning provisioning, AuthenticationManager internalUaaAuthenticationManager, @@ -126,6 +129,7 @@ public DynamicLdapAuthenticationManager getLdapAuthenticationManager(IdentityZon scimGroupExternalMembershipManager, scimGroupProvisioning, ldapLoginAuthenticationManager); + ldapMgr.setApplicationEventPublisher(eventPublisher); ldapAuthManagers.putIfAbsent(zone, ldapMgr); return ldapAuthManagers.get(zone); } @@ -135,4 +139,9 @@ public void destroy() { entry.getValue().destroy(); } } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.eventPublisher = applicationEventPublisher; + } } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/audit/AuditCheckMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/audit/AuditCheckMockMvcTests.java index 25a167dda84..d035f145313 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/audit/AuditCheckMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/audit/AuditCheckMockMvcTests.java @@ -312,7 +312,7 @@ public void invalidPasswordLoginFailedTest() throws Exception { IdentityProviderAuthenticationFailureEvent event1 = (IdentityProviderAuthenticationFailureEvent)captor.getAllValues().get(0); UserAuthenticationFailureEvent event2 = (UserAuthenticationFailureEvent)captor.getAllValues().get(1); PrincipalAuthenticationFailureEvent event3 = (PrincipalAuthenticationFailureEvent)captor.getAllValues().get(2); - assertEquals(testUser.getUserName(), event1.getUser().getUsername()); + assertEquals(testUser.getUserName(), event1.getUsername()); assertEquals(testUser.getUserName(), event2.getUser().getUsername()); assertEquals(testUser.getUserName(), event3.getName()); assertTrue(event1.getAuditEvent().getOrigin().contains("sessionId=")); @@ -422,7 +422,7 @@ public void invalidPasswordLoginAuthenticateEndpointTest() throws Exception { IdentityProviderAuthenticationFailureEvent event1 = (IdentityProviderAuthenticationFailureEvent)captor.getAllValues().get(0); UserAuthenticationFailureEvent event2 = (UserAuthenticationFailureEvent)captor.getAllValues().get(1); PrincipalAuthenticationFailureEvent event3 = (PrincipalAuthenticationFailureEvent)captor.getAllValues().get(2); - assertEquals(testUser.getUserName(), event1.getUser().getUsername()); + assertEquals(testUser.getUserName(), event1.getUsername()); assertEquals(testUser.getUserName(), event2.getUser().getUsername()); assertEquals(testUser.getUserName(), event3.getName()); assertTrue(event1.getAuditEvent().getOrigin().contains("sessionId=")); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index 89a06004bef..73cbdaa6c92 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -12,22 +12,10 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.mock.ldap; -import javax.servlet.http.HttpSession; -import java.io.BufferedReader; -import java.io.File; -import java.io.InputStreamReader; -import java.net.URL; -import java.net.URLEncoder; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; - +import com.fasterxml.jackson.core.type.TypeReference; +import org.cloudfoundry.identity.uaa.audit.event.AbstractUaaEvent; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; +import org.cloudfoundry.identity.uaa.authentication.event.IdentityProviderAuthenticationFailureEvent; import org.cloudfoundry.identity.uaa.authentication.manager.DynamicZoneAwareAuthenticationManager; import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.mfa.GoogleMfaProviderConfig; @@ -57,8 +45,6 @@ import org.cloudfoundry.identity.uaa.zone.JdbcIdentityZoneProvisioning; import org.cloudfoundry.identity.uaa.zone.MfaConfig; import org.cloudfoundry.identity.uaa.zone.UserConfig; - -import com.fasterxml.jackson.core.type.TypeReference; import org.hamcrest.core.StringContains; import org.junit.After; import org.junit.AfterClass; @@ -70,6 +56,8 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; +import org.mockito.ArgumentCaptor; +import org.springframework.context.ApplicationListener; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -90,7 +78,22 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.util.StringUtils; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.XmlWebApplicationContext; + +import javax.servlet.http.HttpSession; +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLEncoder; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; import static java.util.Collections.EMPTY_LIST; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LDAP; @@ -102,6 +105,7 @@ import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -111,6 +115,9 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.springframework.http.HttpHeaders.ACCEPT; import static org.springframework.http.HttpHeaders.AUTHORIZATION; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; @@ -137,10 +144,11 @@ public class LdapMockMvcTests { private static int ldapPortRotation = 0; private String host; - private static WebApplicationContext webApplicationContext; + private static XmlWebApplicationContext webApplicationContext; private static MockMvc mockMvc; + private ApplicationListener listener; - public WebApplicationContext getWebApplicationContext() { + public XmlWebApplicationContext getWebApplicationContext() { return webApplicationContext; } @@ -245,6 +253,9 @@ public void createTestZone() throws Exception { host = zone.getZone().getIdentityZone().getSubdomain() + ".localhost"; IdentityZoneHolder.clear(); + + listener = (ApplicationListener) mock(ApplicationListener.class); + getWebApplicationContext().addApplicationListener(listener); } @After @@ -254,6 +265,7 @@ public void tearDown() throws Exception { .header("Authorization", "Bearer " + zone.getDefaultZoneAdminToken()) .accept(APPLICATION_JSON)) .andExpect(status().isOk()); + MockMvcUtils.utils().removeEventListener(getWebApplicationContext(), listener); } @@ -896,6 +908,14 @@ public void testLogin() throws Exception { .andExpect(unauthenticated()) .andExpect(redirectedUrl("/login?error=login_failure")); + ArgumentCaptor captor = ArgumentCaptor.forClass(AbstractUaaEvent.class); + verify(listener, atLeast(5)).onApplicationEvent(captor.capture()); + List allValues = captor.getAllValues(); + assertThat(allValues.get(4), instanceOf(IdentityProviderAuthenticationFailureEvent.class)); + IdentityProviderAuthenticationFailureEvent event = (IdentityProviderAuthenticationFailureEvent)allValues.get(4); + assertEquals("marissa", event.getUsername()); + assertEquals(OriginKeys.LDAP, event.getOrigin()); + testSuccessfulLogin(); } @@ -1050,6 +1070,14 @@ public void testAuthenticateFailure() throws Exception { .param("password", password); getMockMvc().perform(post) .andExpect(status().isUnauthorized()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AbstractUaaEvent.class); + verify(listener, atLeast(5)).onApplicationEvent(captor.capture()); + List allValues = captor.getAllValues(); + assertThat(allValues.get(3), instanceOf(IdentityProviderAuthenticationFailureEvent.class)); + IdentityProviderAuthenticationFailureEvent event = (IdentityProviderAuthenticationFailureEvent)allValues.get(3); + assertEquals("marissa3", event.getUsername()); + assertEquals(OriginKeys.LDAP, event.getOrigin()); } @Test From 04496341db10ec399253e6807c7e8e73875e76e6 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 29 Jan 2018 12:41:40 -0800 Subject: [PATCH 03/42] remove UAA's dependency on not-yet-commons-ssl. OpenSAML still depends on it. Class org.opensaml.xml.security.x509.tls.StrictHostnameVerifier [#154655059] https://www.pivotaltracker.com/story/show/154655059 --- .../idp/SamlServiceProviderConfigurator.java | 43 +++++------ .../SamlIdentityProviderDefinitionTests.java | 12 ++- .../identity/uaa/login/TokenEndpointDocs.java | 76 ++++++++++--------- .../IdentityProviderEndpointsDocs.java | 16 ++-- .../mock/token/CheckTokenEndpointDocs.java | 8 +- .../token/IntrospectTokenEndpointDocs.java | 3 +- .../oauth/CheckTokenEndpointMockMvcTest.java | 9 ++- .../oauth/IntrospectEndpointMockMvcTest.java | 9 ++- 8 files changed, 88 insertions(+), 88 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderConfigurator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderConfigurator.java index 85a538ab02a..d8d08ffc814 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderConfigurator.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/idp/SamlServiceProviderConfigurator.java @@ -14,19 +14,30 @@ */ package org.cloudfoundry.identity.uaa.provider.saml.idp; -import org.apache.commons.httpclient.contrib.ssl.EasySSLProtocolSocketFactory; -import org.apache.commons.httpclient.params.HttpClientParams; -import org.apache.commons.httpclient.protocol.DefaultProtocolSocketFactory; -import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.http.client.utils.URIBuilder; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; + import org.cloudfoundry.identity.uaa.cache.UrlContentCache; import org.cloudfoundry.identity.uaa.provider.saml.ConfigMetadataProvider; import org.cloudfoundry.identity.uaa.provider.saml.FixedHttpMetaDataProvider; import org.cloudfoundry.identity.uaa.util.UaaHttpRequestUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; + +import org.apache.commons.httpclient.params.HttpClientParams; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.client.utils.URIBuilder; import org.opensaml.common.xml.SAMLConstants; import org.opensaml.saml2.core.NameIDType; import org.opensaml.saml2.metadata.SPSSODescriptor; @@ -37,13 +48,6 @@ import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; -import java.util.*; - /** * Holds internal state of available SAML Service Providers. */ @@ -224,17 +228,7 @@ protected ExtendedMetadataDelegate configureXMLMetadata(SamlServiceProvider prov protected ExtendedMetadataDelegate configureURLMetadata(SamlServiceProvider provider) throws MetadataProviderException { - ProtocolSocketFactory socketFactory = null; SamlServiceProviderDefinition def = provider.getConfig().clone(); - if (def.getMetaDataLocation().startsWith("https")) { - try { - socketFactory = new EasySSLProtocolSocketFactory(); - } catch (GeneralSecurityException | IOException e) { - throw new MetadataProviderException("Error instantiating SSL/TLS socket factory.", e); - } - } else { - socketFactory = new DefaultProtocolSocketFactory(); - } ExtendedMetadata extendedMetadata = new ExtendedMetadata(); extendedMetadata.setAlias(provider.getEntityId()); FixedHttpMetaDataProvider fixedHttpMetaDataProvider; @@ -249,7 +243,6 @@ dummyTimer, getClientParams(), } catch (URISyntaxException e) { throw new MetadataProviderException("Invalid metadata URI: " + def.getMetaDataLocation(), e); } - fixedHttpMetaDataProvider.setSocketFactory(socketFactory); byte[] metadata = fixedHttpMetaDataProvider.fetchMetadata(); def.setMetaDataLocation(new String(metadata, StandardCharsets.UTF_8)); return configureXMLMetadata(provider); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIdentityProviderDefinitionTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIdentityProviderDefinitionTests.java index 9c327065284..5fce765b4ed 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIdentityProviderDefinitionTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/SamlIdentityProviderDefinitionTests.java @@ -1,16 +1,16 @@ package org.cloudfoundry.identity.uaa.provider.saml; -import org.apache.commons.httpclient.contrib.ssl.StrictSSLProtocolSocketFactory; +import java.io.File; +import java.lang.reflect.Field; +import java.util.Arrays; + import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.util.JsonUtils; + import org.junit.Before; import org.junit.Test; import org.springframework.util.ReflectionUtils; -import java.io.File; -import java.lang.reflect.Field; -import java.util.Arrays; - import static org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition.MetadataLocation.DATA; import static org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition.MetadataLocation.UNKNOWN; import static org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition.MetadataLocation.URL; @@ -218,8 +218,6 @@ public void testGetSocketFactoryClassName() throws Exception { assertNull(def.getSocketFactoryClassName()); def.setSocketFactoryClassName("test.class.that.DoesntExist"); assertNull(def.getSocketFactoryClassName()); - def.setSocketFactoryClassName(StrictSSLProtocolSocketFactory.class.getName()); - assertNull(def.getSocketFactoryClassName()); } } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/TokenEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/TokenEndpointDocs.java index cd7d5baf97c..a8654145cb8 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/TokenEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/TokenEndpointDocs.java @@ -17,43 +17,6 @@ import java.net.URI; import java.util.Arrays; -import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.MockSecurityContext; -import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.getClientCredentialsOAuthAccessToken; -import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.getUserOAuthAccessToken; -import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.GRANT_TYPE_SAML2_BEARER; -import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.GRANT_TYPE_USER_TOKEN; -import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.OPAQUE; -import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.REQUEST_TOKEN_FORMAT; -import static org.cloudfoundry.identity.uaa.provider.saml.idp.SamlTestUtils.createLocalSamlIdpDefinition; -import static org.cloudfoundry.identity.uaa.test.SnippetUtils.parameterWithName; -import static org.hamcrest.Matchers.containsString; -import static org.springframework.http.HttpHeaders.AUTHORIZATION; -import static org.springframework.http.HttpHeaders.HOST; -import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED; -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; -import static org.springframework.restdocs.payload.JsonFieldType.STRING; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; -import static org.springframework.security.oauth2.common.util.OAuth2Utils.CLIENT_ID; -import static org.springframework.security.oauth2.common.util.OAuth2Utils.GRANT_TYPE; -import static org.springframework.security.oauth2.common.util.OAuth2Utils.REDIRECT_URI; -import static org.springframework.security.oauth2.common.util.OAuth2Utils.RESPONSE_TYPE; -import static org.springframework.security.oauth2.common.util.OAuth2Utils.SCOPE; -import static org.springframework.security.oauth2.common.util.OAuth2Utils.STATE; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.apache.commons.ssl.Base64; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.mock.token.AbstractTokenMockMvcTests; @@ -72,6 +35,8 @@ import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter; + +import org.apache.commons.codec.binary.Base64; import org.junit.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -93,6 +58,43 @@ import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; +import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.MockSecurityContext; +import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.getClientCredentialsOAuthAccessToken; +import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.getUserOAuthAccessToken; +import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.GRANT_TYPE_SAML2_BEARER; +import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.GRANT_TYPE_USER_TOKEN; +import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.OPAQUE; +import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.REQUEST_TOKEN_FORMAT; +import static org.cloudfoundry.identity.uaa.provider.saml.idp.SamlTestUtils.createLocalSamlIdpDefinition; +import static org.cloudfoundry.identity.uaa.test.SnippetUtils.parameterWithName; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpHeaders.HOST; +import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.CLIENT_ID; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.GRANT_TYPE; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.REDIRECT_URI; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.RESPONSE_TYPE; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.SCOPE; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.STATE; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + public class TokenEndpointDocs extends AbstractTokenMockMvcTests { private final ParameterDescriptor grantTypeParameter = parameterWithName(GRANT_TYPE).required().type(STRING).description("OAuth 2 grant type"); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsDocs.java index 418f14f74b7..3c5b7d261dd 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsDocs.java @@ -12,7 +12,11 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.mock.providers; -import org.apache.commons.lang.ArrayUtils; +import java.net.URL; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.ApacheDSHelper; @@ -38,6 +42,8 @@ import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter; import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture; + +import org.apache.commons.lang.ArrayUtils; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -52,11 +58,6 @@ import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.test.web.servlet.ResultActions; -import java.net.URL; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LDAP; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OIDC10; @@ -398,8 +399,7 @@ public void createSAMLIdentityProvider() throws Exception { fieldWithPath("config.linkText").constrained("Required if the ``showSamlLink`` is set to true").type(STRING).description("The link text for the SAML IDP on the login page"), fieldWithPath("config.groupMappingMode").optional(EXPLICITLY_MAPPED).type(STRING).description("Either ``EXPLICITLY_MAPPED`` in order to map external groups to OAuth scopes using the group mappings, or ``AS_SCOPES`` to use SAML group names as scopes."), fieldWithPath("config.iconUrl").optional(null).type(STRING).description("Reserved for future use"), - fieldWithPath("config.socketFactoryClassName").optional(null).description("Either `\"org.apache.commons.httpclient.protocol.DefaultProtocolSocketFactory\"` or" + - "`\"org.apache.commons.httpclient.contrib.ssl.EasySSLProtocolSocketFactory\"` depending on if the `metaDataLocation` of type `URL` is HTTP or HTTPS, respectively"), + fieldWithPath("config.socketFactoryClassName").optional(null).description("Property is deprecated and value is ignored."), fieldWithPath("config.authnContext").optional(null).type(ARRAY).description("List of AuthnContextClassRef to include in the SAMLRequest. If not specified no AuthnContext will be requested."), ADD_SHADOW_USER_ON_LOGIN, EXTERNAL_GROUPS_WHITELIST, diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/CheckTokenEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/CheckTokenEndpointDocs.java index abb3c0c4741..83aa8304496 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/CheckTokenEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/CheckTokenEndpointDocs.java @@ -12,9 +12,10 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.mock.token; -import org.apache.commons.ssl.Base64; import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.test.UaaTestAccounts; + +import org.apache.commons.codec.binary.Base64; import org.junit.Test; import org.springframework.restdocs.snippet.Snippet; @@ -25,7 +26,10 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.payload.JsonFieldType.*; +import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; +import static org.springframework.restdocs.payload.JsonFieldType.BOOLEAN; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/IntrospectTokenEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/IntrospectTokenEndpointDocs.java index 5b2b2df5935..51da6eb6657 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/IntrospectTokenEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/IntrospectTokenEndpointDocs.java @@ -1,8 +1,9 @@ package org.cloudfoundry.identity.uaa.mock.token; -import org.apache.commons.ssl.Base64; import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.test.UaaTestAccounts; + +import org.apache.commons.codec.binary.Base64; import org.junit.Test; import org.springframework.restdocs.snippet.Snippet; diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/oauth/CheckTokenEndpointMockMvcTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/oauth/CheckTokenEndpointMockMvcTest.java index abd69e37575..fe21b2a1f98 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/oauth/CheckTokenEndpointMockMvcTest.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/oauth/CheckTokenEndpointMockMvcTest.java @@ -15,11 +15,14 @@ package org.cloudfoundry.identity.uaa.oauth; -import com.fasterxml.jackson.core.type.TypeReference; -import org.apache.commons.ssl.Base64; +import java.util.Map; + import org.cloudfoundry.identity.uaa.mock.token.AbstractTokenMockMvcTests; import org.cloudfoundry.identity.uaa.oauth.token.TokenConstants; import org.cloudfoundry.identity.uaa.util.JsonUtils; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.codec.binary.Base64; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -29,8 +32,6 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Map; - import static org.cloudfoundry.identity.uaa.oauth.TokenTestSupport.PASSWORD; import static org.springframework.http.HttpHeaders.ACCEPT; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/oauth/IntrospectEndpointMockMvcTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/oauth/IntrospectEndpointMockMvcTest.java index b884f16efa9..8635f0e669b 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/oauth/IntrospectEndpointMockMvcTest.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/oauth/IntrospectEndpointMockMvcTest.java @@ -1,16 +1,17 @@ package org.cloudfoundry.identity.uaa.oauth; -import com.fasterxml.jackson.core.type.TypeReference; -import org.apache.commons.ssl.Base64; +import java.util.Map; + import org.cloudfoundry.identity.uaa.mock.token.AbstractTokenMockMvcTests; import org.cloudfoundry.identity.uaa.oauth.token.TokenConstants; import org.cloudfoundry.identity.uaa.util.JsonUtils; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.codec.binary.Base64; import org.junit.Before; import org.junit.Test; import org.springframework.security.oauth2.common.util.OAuth2Utils; -import java.util.Map; - import static org.cloudfoundry.identity.uaa.oauth.TokenTestSupport.PASSWORD; import static org.springframework.http.HttpHeaders.ACCEPT; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; From 319e7a39e125f1350a9969388353fe99faa62e7b Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 29 Jan 2018 11:59:15 -0800 Subject: [PATCH 04/42] Backfill tests for hyphen in domain and subdomain --- .../identity/uaa/util/UaaUrlUtilsTest.java | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java index a77d83d2f36..3a54dbd054d 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java @@ -12,9 +12,17 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.util; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -22,13 +30,6 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; @@ -80,7 +81,13 @@ public class UaaUrlUtilsTest { "http://username:password@some.server.com", "http://username:password@some.server.com/path", "http://under_score_subdomain.example.com", - "http://under_score_subdomain.ex_ample.com" + "http://under_score_subdomain.ex_ample.com", + "http://dash-subdomain.example.com", + "http://dash-subdomain.ex-ample.com", + "cool-app://example.com", + "org.cloudfoundry.identity://mobile-windows-app.com/view", + "org+cloudfoundry+identity://mobile-ios-app.com/view", + "org-cl0udfoundry-identity://mobile-android-app.com/view" ); @Before From 48ab91e24019b8325a7ca341491cc5fb9e9d4c49 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 29 Jan 2018 14:20:01 -0800 Subject: [PATCH 05/42] Allow `scheme` to follow URI spec, RFC 3986 [#154735792] https://www.pivotaltracker.com/story/show/154735792 --- .../identity/uaa/util/UaaUrlUtils.java | 21 ++++++++++--------- .../identity/uaa/util/UaaUrlUtilsTest.java | 20 +++++++++++++++--- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtils.java b/server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtils.java index 72c23970994..aa3f0da933a 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtils.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtils.java @@ -14,15 +14,6 @@ */ package org.cloudfoundry.identity.uaa.util; -import org.cloudfoundry.identity.uaa.zone.IdentityZone; -import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; -import org.springframework.util.AntPathMatcher; -import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; -import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import org.springframework.web.util.UriComponentsBuilder; -import org.springframework.web.util.UriUtils; - import javax.servlet.http.HttpServletRequest; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; @@ -34,6 +25,16 @@ import java.util.Map; import java.util.regex.Pattern; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; + +import org.springframework.util.AntPathMatcher; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; + import static java.util.Collections.emptyList; import static java.util.Optional.ofNullable; import static org.springframework.util.StringUtils.hasText; @@ -75,7 +76,7 @@ public static UriComponentsBuilder getURIBuilder(String path, boolean zoneSwitch } private static final Pattern allowedRedirectUriPattern = Pattern.compile( - "^http(\\*|s)?://" + //URL starts with 'www.' or 'http://' or 'https://' or 'http*:// + "^([a-zA-Z][a-zA-Z0-9+\\*\\-.]*)://" + //URL starts with 'some-scheme://' or 'https://' or 'http*:// "(.*:.*@)?" + //username/password in URL "(([a-zA-Z0-9\\-\\*\\_]+\\.)*" + //subdomains "[a-zA-Z0-9\\-\\_]+\\.)?" + //hostname diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java index 3a54dbd054d..a8b04d7b6d8 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java @@ -379,13 +379,27 @@ private void validateRedirectUri(List urls, boolean result) { fail(builder.toString()); } } + + enum CASE { + AS_IS, + UPPER_CASE, + LOWER_CASE + } + private Map getFailedUrls(List urls, boolean result) { Map failed = new LinkedHashMap<>(); urls.stream().forEach( url -> { - String message = "Assertion failed for " + (result ? "" : "in") + "valid url:" + url; - if (result != UaaUrlUtils.isValidRegisteredRedirectUrl(url)) { - failed.put(url, message); + for (CASE c : CASE.values()) { + switch (c) { + case AS_IS: break; + case LOWER_CASE: url = url.toLowerCase(); break; + case UPPER_CASE: url = url.toUpperCase(); break; + } + String message = "Assertion failed for " + (result ? "" : "in") + "valid url:" + url; + if (result != UaaUrlUtils.isValidRegisteredRedirectUrl(url)) { + failed.put(url, message); + } } } ); From 77f8c8081aa0a168bd65de2348db4d2554246391 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 29 Jan 2018 15:05:33 -0800 Subject: [PATCH 06/42] Fix tests [#154735792] https://www.pivotaltracker.com/story/show/154735792 --- .../ClientAdminEndpointsValidatorTests.java | 20 +++++++++---------- .../identity/uaa/util/UaaUrlUtilsTest.java | 3 ++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidatorTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidatorTests.java index f4dd972fdf5..164e8a5f7f6 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidatorTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidatorTests.java @@ -14,13 +14,21 @@ package org.cloudfoundry.identity.uaa.client; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + import org.cloudfoundry.identity.uaa.resources.QueryableResourceManager; import org.cloudfoundry.identity.uaa.security.SecurityContextAccessor; -import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; -import org.junit.Assert; import org.cloudfoundry.identity.uaa.zone.ClientSecretPolicy; import org.cloudfoundry.identity.uaa.zone.ClientSecretValidator; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.ZoneAwareClientSecretPolicyValidator; + +import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -29,13 +37,6 @@ import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.client.BaseClientDetails; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.GRANT_TYPE_JWT_BEARER; import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.GRANT_TYPE_SAML2_BEARER; import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.GRANT_TYPE_USER_TOKEN; @@ -113,7 +114,6 @@ public void test_validate_jwt_bearer_grant_type() throws Exception { validator.validate(client, true, true); } - @Test(expected = InvalidClientDetailsException.class) public void validate_rejectsMalformedUrls() throws Exception { client.setAuthorizedGrantTypes(Arrays.asList("authorization_code")); client.setRegisteredRedirectUri(Collections.singleton("httasdfasp://anything.comadfsfdasfdsa")); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java index a8b04d7b6d8..913b0efee23 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java @@ -59,7 +59,8 @@ public class UaaUrlUtilsTest { "www.invalid.com/*/with/path**", "www.*.invalid.com/*/with/path**", "http://username:password@*.com", - "http://username:password@*.com/path" + "http://username:password@*.com/path", + "org-;cl0udfoundry-identity://mobile-android-app.com/view" ); private List validUrls = Arrays.asList( "http://localhost", From fc5dbd75e9885a69df9f92349a01937f30410298 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Tue, 30 Jan 2018 10:14:45 -0800 Subject: [PATCH 07/42] bump develop branch for next release train --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index bb7ad5d2700..36e0c384160 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=4.9.0-SNAPSHOT +version=4.10.0-SNAPSHOT From 63cfd9f13cd81f98c8b675d1c8c0904a5b306992 Mon Sep 17 00:00:00 2001 From: Florian Tack Date: Wed, 31 Jan 2018 10:58:32 +0000 Subject: [PATCH 08/42] Add authentication type to AuditEvent [#154404992] https://www.pivotaltracker.com/story/show/154404992 Signed-off-by: Torsten Luh --- .../identity/uaa/audit/AuditEvent.java | 11 +++++++-- .../identity/uaa/audit/JdbcAuditService.java | 2 +- .../uaa/audit/LoggingAuditService.java | 4 ++-- .../uaa/audit/event/AbstractUaaEvent.java | 8 +++++-- ...ityProviderAuthenticationFailureEvent.java | 12 +++++----- .../uaa/audit/JdbcAuditServiceTests.java | 2 +- ...cFailedLoginCountingAuditServiceTests.java | 4 ++-- .../uaa/audit/LoggingAuditServiceTest.java | 4 ++-- .../manager/CommonLoginPolicyTest.java | 4 ++-- .../manager/PeriodLockoutPolicyTests.java | 24 +++++++++---------- .../uaa/mock/ldap/LdapMockMvcTests.java | 4 ++-- 11 files changed, 45 insertions(+), 34 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/audit/AuditEvent.java b/server/src/main/java/org/cloudfoundry/identity/uaa/audit/AuditEvent.java index 1cd56d5ffd3..a95cd1f4295 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/audit/AuditEvent.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/audit/AuditEvent.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Cloud Foundry + * Cloud Foundry * Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved. * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). @@ -23,14 +23,18 @@ public class AuditEvent { private final long time; private final String data; private final String identityZoneId; + private final String description; + private final String authenticationType; - public AuditEvent(AuditEventType type, String principalId, String origin, String data, long time, String identityZoneId) { + public AuditEvent(AuditEventType type, String principalId, String origin, String data, long time, String identityZoneId, String authenticationType, String description) { this.type = type; this.data = data; this.origin = origin; this.time = time; this.principalId = principalId; this.identityZoneId = identityZoneId; + this.description = description; + this.authenticationType = authenticationType; } public AuditEventType getType() { @@ -57,4 +61,7 @@ public String getIdentityZoneId() { return identityZoneId; } + public String getDescription() { return description; } + + public String getAuthenticationType() { return authenticationType; } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/audit/JdbcAuditService.java b/server/src/main/java/org/cloudfoundry/identity/uaa/audit/JdbcAuditService.java index b665fb08a05..1712ff7c6e6 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/audit/JdbcAuditService.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/audit/JdbcAuditService.java @@ -63,7 +63,7 @@ public AuditEvent mapRow(ResultSet rs, int rowNum) throws SQLException { long time = rs.getTimestamp(5).getTime(); String identityZoneId = nullSafeTrim(rs.getString(6)); return new AuditEvent(eventType, principalId, origin, - data, time, identityZoneId); + data, time, identityZoneId, null, null); } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/audit/LoggingAuditService.java b/server/src/main/java/org/cloudfoundry/identity/uaa/audit/LoggingAuditService.java index 0d69893ab9e..185b6c4b1ff 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/audit/LoggingAuditService.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/audit/LoggingAuditService.java @@ -111,8 +111,8 @@ public List find(String principal, long after, String zoneId) { @Override public void log(AuditEvent auditEvent, String zoneId) { updateCounters(auditEvent); - log(String.format("%s ('%s'): principal=%s, origin=[%s], identityZoneId=[%s]", auditEvent.getType().name(), auditEvent.getData(), - auditEvent.getPrincipalId(), auditEvent.getOrigin(), auditEvent.getIdentityZoneId())); + log(String.format("%s ('%s'): principal=%s, origin=[%s], identityZoneId=[%s], authenticationType=[%s]", auditEvent.getType().name(), auditEvent.getData(), + auditEvent.getPrincipalId(), auditEvent.getOrigin(), auditEvent.getIdentityZoneId(), auditEvent.getAuthenticationType())); } private void updateCounters(AuditEvent auditEvent) { diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/audit/event/AbstractUaaEvent.java b/server/src/main/java/org/cloudfoundry/identity/uaa/audit/event/AbstractUaaEvent.java index b1a70a62be7..331e0e96fc5 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/audit/event/AbstractUaaEvent.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/audit/event/AbstractUaaEvent.java @@ -69,11 +69,15 @@ public void process(UaaAuditService auditor) { } protected AuditEvent createAuditRecord(String principalId, AuditEventType type, String origin) { - return new AuditEvent(type, principalId, origin, null, System.currentTimeMillis(), identityZone.getId()); + return new AuditEvent(type, principalId, origin, null, System.currentTimeMillis(), identityZone.getId(), "unknown", null); } protected AuditEvent createAuditRecord(String principalId, AuditEventType type, String origin, String data) { - return new AuditEvent(type, principalId, origin, data, System.currentTimeMillis(), identityZone.getId()); + return new AuditEvent(type, principalId, origin, data, System.currentTimeMillis(), identityZone.getId(), "unknown", null); + } + + protected AuditEvent createAuditRecord(String principalId, AuditEventType type, String origin, String data, String authenticationType, String message) { + return new AuditEvent(type, principalId, origin, data, System.currentTimeMillis(), identityZone.getId(), authenticationType, message); } public Authentication getAuthentication() { diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/event/IdentityProviderAuthenticationFailureEvent.java b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/event/IdentityProviderAuthenticationFailureEvent.java index 4a9f5473495..87c52c9e397 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/event/IdentityProviderAuthenticationFailureEvent.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/event/IdentityProviderAuthenticationFailureEvent.java @@ -8,26 +8,26 @@ public class IdentityProviderAuthenticationFailureEvent extends AbstractUaaAuthenticationEvent { private String username; - private String origin; + private String authenticationType; public String getUsername() { return username; } - public String getOrigin() { - return origin; + public String getAuthenticationType() { + return authenticationType; } - public IdentityProviderAuthenticationFailureEvent(Authentication authentication, String username, String origin) { + public IdentityProviderAuthenticationFailureEvent(Authentication authentication, String username, String authenticationType) { super(authentication); this.username = username; - this.origin = origin; + this.authenticationType = authenticationType; } @Override public AuditEvent getAuditEvent() { Assert.notNull(username, "UaaUser cannot be null"); return createAuditRecord(null, AuditEventType.IdentityProviderAuthenticationFailure, - getOrigin(getAuthenticationDetails()), username); + getOrigin(getAuthenticationDetails()), username, authenticationType, null); } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/audit/JdbcAuditServiceTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/audit/JdbcAuditServiceTests.java index bc54f7eeb74..0b11509a620 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/audit/JdbcAuditServiceTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/audit/JdbcAuditServiceTests.java @@ -78,7 +78,7 @@ private AuditEvent getAuditEvent(AuditEventType type, String principal) { } private AuditEvent getAuditEvent(AuditEventType type, String principal, String data) { - return new AuditEvent(type, principal, authDetails, data, System.currentTimeMillis(), IdentityZone.getUaa().getId()); + return new AuditEvent(type, principal, authDetails, data, System.currentTimeMillis(), IdentityZone.getUaa().getId(), null, null); } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/audit/JdbcFailedLoginCountingAuditServiceTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/audit/JdbcFailedLoginCountingAuditServiceTests.java index f53dbe61ec9..756f0a3c1a0 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/audit/JdbcFailedLoginCountingAuditServiceTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/audit/JdbcFailedLoginCountingAuditServiceTests.java @@ -191,11 +191,11 @@ public void clientSecretChangeSuccessResetsData() throws Exception { } private AuditEvent getAuditEvent(AuditEventType type, String principal, String data) { - return new AuditEvent(type, principal, authDetails, data, System.currentTimeMillis(), IdentityZone.getUaa().getId()); + return new AuditEvent(type, principal, authDetails, data, System.currentTimeMillis(), IdentityZone.getUaa().getId(), null, null); } private AuditEvent getAuditEventForAltZone(AuditEventType type, String principal, String data) { - return new AuditEvent(type, principal, authDetails, data, System.currentTimeMillis(), "test-zone"); + return new AuditEvent(type, principal, authDetails, data, System.currentTimeMillis(), "test-zone", null, null); } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/audit/LoggingAuditServiceTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/audit/LoggingAuditServiceTest.java index bdb745fd6ba..1e36788f6a2 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/audit/LoggingAuditServiceTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/audit/LoggingAuditServiceTest.java @@ -26,7 +26,7 @@ public void setup() { @Test public void log_sanitizesMaliciousInput() { - AuditEvent auditEvent = new AuditEvent(PasswordChangeFailure, "principalId", "origin", "data", 100L, "malicious-zone\r\n\t"); + AuditEvent auditEvent = new AuditEvent(PasswordChangeFailure, "principalId", "origin", "data", 100L, "malicious-zone\r\n\t", null, null); loggingAuditService.log(auditEvent, "not-used"); @@ -40,7 +40,7 @@ public void log_sanitizesMaliciousInput() { @Test public void log_doesNotModifyNonMaliciousInput() { - AuditEvent auditEvent = new AuditEvent(PasswordChangeFailure, "principalId", "origin", "data", 100L, "safe-zone"); + AuditEvent auditEvent = new AuditEvent(PasswordChangeFailure, "principalId", "origin", "data", 100L, "safe-zone", null, null); loggingAuditService.log(auditEvent, "not-used"); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/CommonLoginPolicyTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/CommonLoginPolicyTest.java index 5d9fab2df51..df6da0167e5 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/CommonLoginPolicyTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/CommonLoginPolicyTest.java @@ -66,7 +66,7 @@ public void isAllowed_whenLockoutAfterFailuresIsNegative_returnsTrue() { @Test public void isAllowed_whenLockoutAfterFailuresIsPositive_returnsFalseIfTooManyFailedRecentAttempts() { when(lockoutPolicyRetriever.getLockoutPolicy()).thenReturn(new LockoutPolicy(2, 1, 300)); - AuditEvent auditEvent = new AuditEvent(failureEventType, null, null, null, 1L, null); + AuditEvent auditEvent = new AuditEvent(failureEventType, null, null, null, 1L, null, null, null); List list = Arrays.asList(auditEvent); String zoneId = IdentityZoneHolder.get().getId(); when(auditService.find(eq("principal"), anyLong(), eq(zoneId))).thenReturn(list); @@ -80,7 +80,7 @@ public void isAllowed_whenLockoutAfterFailuresIsPositive_returnsFalseIfTooManyFa @Test public void isAllowed_whenLockoutAfterFailuresIsPositive_returnsTrueIfNotTooManyFailedRecentAttempts() { when(lockoutPolicyRetriever.getLockoutPolicy()).thenReturn(new LockoutPolicy(2, 2, 300)); - AuditEvent auditEvent = new AuditEvent(failureEventType, null, null, null, 1L, null); + AuditEvent auditEvent = new AuditEvent(failureEventType, null, null, null, 1L, null, null, null); List list = Arrays.asList(auditEvent); String zoneId = IdentityZoneHolder.get().getId(); when(auditService.find(eq("principal"), anyLong(), eq(zoneId))).thenReturn(list); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/PeriodLockoutPolicyTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/PeriodLockoutPolicyTests.java index c0d559f4005..074269c78b3 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/PeriodLockoutPolicyTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/PeriodLockoutPolicyTests.java @@ -75,8 +75,8 @@ public void setUp() throws Exception { public void loginIsDeniedIfAllowedFailuresIsExceeded() { String zoneId = IdentityZoneHolder.get().getId(); when(as.find(eq("1"), anyLong(), eq(zoneId))).thenReturn(Arrays.asList( - new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 1, IdentityZone.getUaa().getId()), - new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 2, IdentityZone.getUaa().getId()) + new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 1, IdentityZone.getUaa().getId(), null, null), + new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 2, IdentityZone.getUaa().getId(), null, null) )); policyRetriever.getDefaultLockoutPolicy().setLockoutAfterFailures(2); @@ -87,9 +87,9 @@ public void loginIsDeniedIfAllowedFailuresIsExceeded() { public void loginIsAllowedIfSuccessfulLoginIntercedesExcessiveFailures() { String zoneId = IdentityZoneHolder.get().getId(); when(as.find(eq("1"), anyLong(), eq(zoneId))).thenReturn(Arrays.asList( - new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 1, IdentityZone.getUaa().getId()), - new AuditEvent(UserAuthenticationSuccess, "joe", "", "", now - 2, IdentityZone.getUaa().getId()), - new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 3, IdentityZone.getUaa().getId()) + new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 1, IdentityZone.getUaa().getId(), null, null), + new AuditEvent(UserAuthenticationSuccess, "joe", "", "", now - 2, IdentityZone.getUaa().getId(), null, null), + new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 3, IdentityZone.getUaa().getId(), null, null) )); policy.getDefaultLockoutPolicy().setLockoutAfterFailures(2); @@ -100,9 +100,9 @@ public void loginIsAllowedIfSuccessfulLoginIntercedesExcessiveFailures() { public void loginIsAllowedWithExcessiveFailuresIfLockoutPeriodHasElapsed() { String zoneId = IdentityZoneHolder.get().getId(); when(as.find(eq("1"), anyLong(), eq(zoneId))).thenReturn(Arrays.asList( - new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 5001, IdentityZone.getUaa().getId()), - new AuditEvent(UserAuthenticationSuccess, "joe", "", "", now - 5002, IdentityZone.getUaa().getId()), - new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 5003, IdentityZone.getUaa().getId()) + new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 5001, IdentityZone.getUaa().getId(), null, null), + new AuditEvent(UserAuthenticationSuccess, "joe", "", "", now - 5002, IdentityZone.getUaa().getId(), null, null), + new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 5003, IdentityZone.getUaa().getId(), null, null) )); policy.getDefaultLockoutPolicy().setLockoutAfterFailures(2); @@ -115,8 +115,8 @@ public void loginIsAllowedWithExcessiveFailuresIfLockoutPeriodHasElapsed() { public void loginIsAllowedIfAllowedFailuresIsNotExceeded() { String zoneId = IdentityZoneHolder.get().getId(); when(as.find(eq("1"), anyLong(), eq(zoneId))).thenReturn(Arrays.asList( - new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 1, IdentityZone.getUaa().getId()), - new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 2, IdentityZone.getUaa().getId()) + new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 1, IdentityZone.getUaa().getId(), null, null), + new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 2, IdentityZone.getUaa().getId(), null, null) )); policy.getDefaultLockoutPolicy().setLockoutAfterFailures(3); @@ -127,8 +127,8 @@ public void loginIsAllowedIfAllowedFailuresIsNotExceeded() { public void testUseLockoutPolicyFromDbIfPresent() throws Exception { String zoneId = IdentityZoneHolder.get().getId(); when(as.find(eq("1"), anyLong(), eq(zoneId))).thenReturn(Arrays.asList( - new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 1, IdentityZone.getUaa().getId()), - new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 1, IdentityZone.getUaa().getId()) + new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 1, IdentityZone.getUaa().getId(), null, null), + new AuditEvent(UserAuthenticationFailure, "joe", "", "", now - 1, IdentityZone.getUaa().getId(), null, null) )); LockoutPolicy lockoutPolicy = new LockoutPolicy(); lockoutPolicy.setLockoutAfterFailures(2); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index 73cbdaa6c92..65e5ebc39f2 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -914,7 +914,7 @@ public void testLogin() throws Exception { assertThat(allValues.get(4), instanceOf(IdentityProviderAuthenticationFailureEvent.class)); IdentityProviderAuthenticationFailureEvent event = (IdentityProviderAuthenticationFailureEvent)allValues.get(4); assertEquals("marissa", event.getUsername()); - assertEquals(OriginKeys.LDAP, event.getOrigin()); + assertEquals(OriginKeys.LDAP, event.getAuthenticationType()); testSuccessfulLogin(); } @@ -1077,7 +1077,7 @@ public void testAuthenticateFailure() throws Exception { assertThat(allValues.get(3), instanceOf(IdentityProviderAuthenticationFailureEvent.class)); IdentityProviderAuthenticationFailureEvent event = (IdentityProviderAuthenticationFailureEvent)allValues.get(3); assertEquals("marissa3", event.getUsername()); - assertEquals(OriginKeys.LDAP, event.getOrigin()); + assertEquals(OriginKeys.LDAP, event.getAuthenticationType()); } @Test From 85eac70b1e1baf984abe1858988e371c1605169b Mon Sep 17 00:00:00 2001 From: Jennifer Hamon Date: Wed, 31 Jan 2018 13:12:29 -0800 Subject: [PATCH 09/42] Add simple SAML config parsing test with emailDomain property Had a support ticket related to the form of this field. Testing showed emailDomain property expects an array of strings. --- ...ootstrapSamlIdentityProviderDataTests.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/BootstrapSamlIdentityProviderDataTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/BootstrapSamlIdentityProviderDataTests.java index d57b8b4b059..a3a04ab165c 100755 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/BootstrapSamlIdentityProviderDataTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/BootstrapSamlIdentityProviderDataTests.java @@ -326,7 +326,28 @@ public void testGetIdentityProviders() throws Exception { testGetIdentityProviderDefinitions(4); } + @Test + public void testCanParseASimpleSamlConfig() throws Exception { + String yaml = " providers:\n" + + " my-okta:\n" + + " assertionConsumerIndex: 0\n" + + " emailDomain: \n" + + " - mydomain.io\n" + + " iconUrl: https://my.identityprovider.com/icon.png\n" + + " idpMetadata: https://pivotal.oktapreview.com/app/abcdefghasdfsafjdsklf/sso/saml/metadata\n" + + " linkText: Log in with Pivotal OktaPreview\n" + + " metadataTrustCheck: true\n" + + " nameID: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\n" + + " showSamlLoginLink: false\n" + + " signMetaData: false\n" + + " signRequest: false\n" + + " skipSslValidation: false\n" + + " storeCustomAttributes: true"; + bootstrap.setIdentityProviders(parseYaml(yaml)); + bootstrap.afterPropertiesSet(); + } + @Test public void testSetAddShadowUserOnLoginFromYaml() throws Exception { String yaml = " providers:\n" + From 063bde466baeb6c4decd7c98fbdfdbb23e6eb77d Mon Sep 17 00:00:00 2001 From: Mikhail Vyshegorodtsev Date: Thu, 1 Feb 2018 11:46:47 -0800 Subject: [PATCH 10/42] Remove duplicate mentioning of addShadowUserOnLogin property [#154852945] https://www.pivotaltracker.com/story/show/154852945 Signed-off-by: Tian Wang --- .../uaa/mock/providers/IdentityProviderEndpointsDocs.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsDocs.java index 3c5b7d261dd..453a5ba7549 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsDocs.java @@ -121,14 +121,13 @@ public class IdentityProviderEndpointsDocs extends InjectedMockContextTest { private static final FieldDescriptor ATTRIBUTE_MAPPING_PHONE = fieldWithPath("config.attributeMappings.phone_number").optional(null).type(STRING).description(PHONE_NUMBER_DESC); private static final FieldDescriptor ATTRIBUTE_MAPPING_EXTERNAL_GROUP = fieldWithPath("config.attributeMappings.external_groups").optional(null).type(ARRAY).description("Map `external_groups` to the attribute for groups in the provider assertion."); private static final FieldDescriptor ATTRIBUTE_MAPPING_CUSTOM_ATTRIBUTES_DEPARTMENT = fieldWithPath("config.attributeMappings['user.attribute.department']").optional(null).type(STRING).description("Map external attribute to UAA recognized mappings. Mapping should be of the format `user.attribute.`. `department` is used in the documentation as an example attribute."); - private static final FieldDescriptor ADD_SHADOW_USER = fieldWithPath("config.addShadowUserOnLogin").optional(true).type(BOOLEAN).description("Whether users should be allowed to authenticate from LDAP without having a user pre-populated in the users database"); + private static final FieldDescriptor ADD_SHADOW_USER = fieldWithPath("config.addShadowUserOnLogin").optional(true).type(BOOLEAN).description(" Determines whether users should be allowed to authenticate without having a user pre-populated in the users database (if true), or whether shadow users must be created before login by an administrator (if false)."); private static final FieldDescriptor EXTERNAL_GROUPS_WHITELIST = fieldWithPath("config.externalGroupsWhitelist").optional(null).type(ARRAY).description("List of external groups that will be included in the ID Token if the `roles` scope is requested."); private static final FieldDescriptor PROVIDER_DESC = fieldWithPath("config.providerDescription").optional(null).type(STRING).description("Human readable name/description of this provider"); private static final FieldDescriptor EMAIL_DOMAIN = fieldWithPath("config.emailDomain").optional(null).type(ARRAY).description("List of email domains associated with the provider for the purpose of associating users to the correct origin upon invitation. If empty list, no invitations are accepted. Wildcards supported."); private static final FieldDescriptor ACTIVE = fieldWithPath("active").optional(null).description(ACTIVE_DESC); private static final FieldDescriptor NAME = fieldWithPath("name").required().description(NAME_DESC); private static final FieldDescriptor CONFIG = fieldWithPath("config").required().description("Various configuration properties for the identity provider."); - private static final FieldDescriptor ADD_SHADOW_USER_ON_LOGIN = fieldWithPath("config.addShadowUserOnLogin").optional(true).description("Determines whether or not shadow users must be created before login by an administrator."); private static final FieldDescriptor ID = fieldWithPath("id").type(STRING).description(ID_DESC); private static final FieldDescriptor CREATED = fieldWithPath("created").description(CREATED_DESC); private static final FieldDescriptor LAST_MODIFIED = fieldWithPath("last_modified").description(LAST_MODIFIED_DESC); @@ -401,7 +400,6 @@ public void createSAMLIdentityProvider() throws Exception { fieldWithPath("config.iconUrl").optional(null).type(STRING).description("Reserved for future use"), fieldWithPath("config.socketFactoryClassName").optional(null).description("Property is deprecated and value is ignored."), fieldWithPath("config.authnContext").optional(null).type(ARRAY).description("List of AuthnContextClassRef to include in the SAMLRequest. If not specified no AuthnContext will be requested."), - ADD_SHADOW_USER_ON_LOGIN, EXTERNAL_GROUPS_WHITELIST, fieldWithPath("config.attributeMappings.user_name").optional("NameID").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for SAML is `NameID`."), }, attributeMappingFields)); @@ -499,7 +497,6 @@ public void createOAuthIdentityProvider() throws Exception { fieldWithPath("config.checkTokenUrl").optional(null).type(OBJECT).description("Reserved for future OAuth use."), fieldWithPath("config.responseType").optional("code").type(STRING).description("Response type for the authorize request, will be sent to OAuth server, defaults to `code`"), fieldWithPath("config.clientAuthInBody").optional(false).type(BOOLEAN).description("Sends the client credentials in the token retrieval call as body parameters instead of a Basic Authorization header."), - ADD_SHADOW_USER_ON_LOGIN, fieldWithPath("config.issuer").optional(null).type(STRING).description("The OAuth 2.0 token issuer. This value is used to validate the issuer inside the token."), fieldWithPath("config.attributeMappings.user_name").optional("sub").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for OpenID Connect is `sub`"), }, attributeMappingFields)); @@ -570,7 +567,6 @@ public void createOidcIdentityProvider() throws Exception { fieldWithPath("config.clientAuthInBody").optional(false).type(BOOLEAN).description("Sends the client credentials in the token retrieval call as body parameters instead of a Basic Authorization header."), fieldWithPath("config.userInfoUrl").optional(null).type(OBJECT).description("Reserved for future OIDC use. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL."), fieldWithPath("config.responseType").optional("code").type(STRING).description("Response type for the authorize request, defaults to `code`, but can be `code id_token` if the OIDC server can return an id_token as a query parameter in the redirect."), - ADD_SHADOW_USER_ON_LOGIN, fieldWithPath("config.issuer").optional(null).type(STRING).description("The OAuth 2.0 token issuer. This value is used to validate the issuer inside the token."), GROUP_WHITELIST, fieldWithPath("config.attributeMappings.user_name").optional("sub").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for OpenID Connect is `sub`.") From 9b454f86a22e497528b7546b1c42086285dbda4b Mon Sep 17 00:00:00 2001 From: Tian Wang Date: Thu, 1 Feb 2018 12:09:54 -0800 Subject: [PATCH 11/42] Add subdomain switch header for idp endpoints [#154824382] https://www.pivotaltracker.com/story/show/154824382 Signed-off-by: Mikhail Vyshegorodtsev --- .../IdentityProviderEndpointsDocs.java | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsDocs.java index 453a5ba7549..ee70ac1b0b1 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsDocs.java @@ -50,6 +50,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.restdocs.headers.HeaderDescriptor; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.snippet.Attributes; import org.springframework.restdocs.snippet.Snippet; @@ -220,6 +221,10 @@ public static void startApacheDS() throws Exception { private final FieldDescriptor LDAP_ATTRIBUTE_MAPPING_LASTNAME = fieldWithPath("config.attributeMappings.family_name").optional("sn").type(STRING).description(FAMILY_NAME_DESC); private final FieldDescriptor LDAP_ATTRIBUTE_MAPPING_PHONE = fieldWithPath("config.attributeMappings.phone_number").optional("telephonenumber").type(STRING).description(PHONE_NUMBER_DESC); private final FieldDescriptor LDAP_ATTRIBUTE_MAPPING_USER_NAME = fieldWithPath("config.attributeMappings.user_name").optional("user_name").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for LDAP is the User Name filter"); + + + private static final HeaderDescriptor IDENTITY_ZONE_ID_HEADER = headerWithName(IdentityZoneSwitchingFilter.HEADER).description("May include this header to administer another zone if using `zones..admin` or `uaa.admin` scope against the default UAA zone.").optional(); + private static final HeaderDescriptor IDENTITY_ZONE_SUBDOMAIN_HEADER = headerWithName(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER).optional().description("If using a `zones..admin` scope/token, indicates what Identity Zone this request goes to by supplying a subdomain."); private FieldDescriptor[] ldapAllFields = (FieldDescriptor[]) ArrayUtils.addAll(commonProviderFields, new FieldDescriptor[]{ LDAP_TYPE, LDAP_ORIGIN_KEY, @@ -429,7 +434,8 @@ public void createSAMLIdentityProvider() throws Exception { preprocessResponse(prettyPrint()), requestHeaders( headerWithName("Authorization").description("Bearer token containing `zones..admin` or `uaa.admin` or `idps.write` (only in the same zone that you are a user of)"), - headerWithName("X-Identity-Zone-Id").description("May include this header to administer another zone if using `zones..admin` or `uaa.admin` scope against the default UAA zone.").optional() + IDENTITY_ZONE_ID_HEADER, + IDENTITY_ZONE_SUBDOMAIN_HEADER ), commonRequestParams, requestFields, @@ -457,7 +463,8 @@ public void createSAMLIdentityProvider() throws Exception { preprocessResponse(prettyPrint()), requestHeaders( headerWithName("Authorization").description("Bearer token containing `zones..admin` or `uaa.admin` or `idps.write` (only in the same zone that you are a user of)"), - headerWithName("X-Identity-Zone-Id").description("May include this header to administer another zone if using `zones..admin` or `uaa.admin` scope against the default UAA zone.").optional() + IDENTITY_ZONE_ID_HEADER, + IDENTITY_ZONE_SUBDOMAIN_HEADER ), commonRequestParams, requestFields, @@ -524,7 +531,8 @@ public void createOAuthIdentityProvider() throws Exception { preprocessResponse(prettyPrint()), requestHeaders( headerWithName("Authorization").description("Bearer token containing `zones..admin` or `uaa.admin` or `idps.write` (only in the same zone that you are a user of)"), - headerWithName("X-Identity-Zone-Id").description("May include this header to administer another zone if using `zones..admin` or `uaa.admin` scope against the default UAA zone.").optional() + IDENTITY_ZONE_ID_HEADER, + IDENTITY_ZONE_SUBDOMAIN_HEADER ), commonRequestParams, requestFields, @@ -595,7 +603,8 @@ public void createOidcIdentityProvider() throws Exception { preprocessResponse(prettyPrint()), requestHeaders( headerWithName("Authorization").description("Bearer token containing `zones..admin` or `uaa.admin` or `idps.write` (only in the same zone that you are a user of)"), - headerWithName("X-Identity-Zone-Id").description("May include this header to administer another zone if using `zones..admin` or `uaa.admin` scope against the default UAA zone.").optional() + IDENTITY_ZONE_ID_HEADER, + IDENTITY_ZONE_SUBDOMAIN_HEADER ), commonRequestParams, requestFields, @@ -729,7 +738,8 @@ public void createLDAPProvider(IdentityProvider preprocessResponse(prettyPrint()), requestHeaders( headerWithName("Authorization").description("Bearer token containing `zones..admin` or `uaa.admin` or `idps.write` (only in the same zone that you are a user of)"), - headerWithName("X-Identity-Zone-Id").description("May include this header to administer another zone if using `zones..admin` or `uaa.admin` scope against the default UAA zone.").optional() + IDENTITY_ZONE_ID_HEADER, + IDENTITY_ZONE_SUBDOMAIN_HEADER ), commonRequestParams, requestFields, @@ -777,7 +787,8 @@ public void getAllIdentityProviders() throws Exception { preprocessResponse(prettyPrint()), requestHeaders( headerWithName("Authorization").description("Bearer token containing `zones..admin` or `zones..idps.read` or `uaa.admin` or `idps.read` (only in the same zone that you are a user of)"), - headerWithName("X-Identity-Zone-Id").description("May include this header to administer another zone if using `zones..admin` or `zones..idps.read` or `uaa.admin` scope against the default UAA zone.").optional() + headerWithName("X-Identity-Zone-Id").description("May include this header to administer another zone if using `zones..admin` or `zones..idps.read` or `uaa.admin` scope against the default UAA zone.").optional(), + IDENTITY_ZONE_SUBDOMAIN_HEADER ), commonRequestParams, responseFields)); @@ -804,7 +815,8 @@ public void getIdentityProvider() throws Exception { ), requestHeaders( headerWithName("Authorization").description("Bearer token containing `zones..admin` or `zones..idps.read` or `uaa.admin` or `idps.read` (only in the same zone that you are a user of)"), - headerWithName("X-Identity-Zone-Id").description("May include this header to administer another zone if using `zones..admin` or `zones..idps.read` or `uaa.admin` scope against the default UAA zone.").optional() + headerWithName("X-Identity-Zone-Id").description("May include this header to administer another zone if using `zones..admin` or `zones..idps.read` or `uaa.admin` scope against the default UAA zone.").optional(), + IDENTITY_ZONE_SUBDOMAIN_HEADER ), commonRequestParams, responseFields(getCommonProviderFieldsAnyType()))); @@ -863,7 +875,8 @@ public void updateIdentityProvider() throws Exception { ), requestHeaders( headerWithName("Authorization").description("Bearer token containing `zones..admin` or `uaa.admin` or `idps.write` (only in the same zone that you are a user of)"), - headerWithName("X-Identity-Zone-Id").description("May include this header to administer another zone if using `zones..admin` or `uaa.admin` scope against the default UAA zone.").optional() + IDENTITY_ZONE_ID_HEADER, + IDENTITY_ZONE_SUBDOMAIN_HEADER ), commonRequestParams, requestFields, @@ -896,7 +909,8 @@ public void patchIdentityProviderStatus() throws Exception { ), requestHeaders( headerWithName("Authorization").description("Bearer token containing `zones..admin` or `uaa.admin` or `idps.write` (only in the same zone that you are a user of)"), - headerWithName("X-Identity-Zone-Id").description("May include this header to administer another zone if using `zones..admin` or `uaa.admin` scope against the default UAA zone.").optional() + IDENTITY_ZONE_ID_HEADER, + IDENTITY_ZONE_SUBDOMAIN_HEADER ), requestFields, responseFields)); @@ -926,7 +940,8 @@ public void deleteIdentityProvider() throws Exception { ), requestHeaders( headerWithName("Authorization").description("Bearer token containing `zones..admin` or `uaa.admin` or `idps.write` (only in the same zone that you are a user of)"), - headerWithName("X-Identity-Zone-Id").description("May include this header to administer another zone if using `zones..admin` or `uaa.admin` scope against the default UAA zone.").optional() + IDENTITY_ZONE_ID_HEADER, + IDENTITY_ZONE_SUBDOMAIN_HEADER ), commonRequestParams, responseFields(getCommonProviderFieldsAnyType()))); From bcc6753b97fb6be643312a51ea0283e8b6561a8b Mon Sep 17 00:00:00 2001 From: Mikhail Vyshegorodtsev Date: Thu, 1 Feb 2018 12:35:56 -0800 Subject: [PATCH 12/42] Add zone switching headers for identity zone docs [#154818783] https://www.pivotaltracker.com/story/show/154818783 Signed-off-by: Tian Wang --- .../mock/zones/IdentityZoneEndpointDocs.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointDocs.java index 5ad5e724ea9..3320613c68f 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointDocs.java @@ -7,11 +7,13 @@ import org.cloudfoundry.identity.uaa.zone.BrandingInformation.Banner; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter; import org.cloudfoundry.identity.uaa.zone.JdbcIdentityZoneProvisioning; import org.cloudfoundry.identity.uaa.zone.SamlConfig; import org.junit.Before; import org.junit.Test; import org.springframework.http.HttpStatus; +import org.springframework.restdocs.headers.HeaderDescriptor; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.snippet.Snippet; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; @@ -150,6 +152,9 @@ public class IdentityZoneEndpointDocs extends InjectedMockContextTest { private static final String MFA_CONFIG_ENABLED_DESC = "Set `true` to enable Multi-factor Authentication (MFA) for the current zone. Defaults to `false`"; private static final String MFA_CONFIG_PROVIDER_NAME_DESC = "The unique `name` of the MFA provider to use for this zone."; + private static final HeaderDescriptor IDENTITY_ZONE_ID_HEADER = headerWithName(IdentityZoneSwitchingFilter.HEADER).description("May include this header to administer another zone if using `zones..admin` or `uaa.admin` scope against the default UAA zone.").optional(); + private static final HeaderDescriptor IDENTITY_ZONE_SUBDOMAIN_HEADER = headerWithName(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER).optional().description("If using a `zones..admin` scope/token, indicates what Identity Zone this request goes to by supplying a subdomain."); + @Before public void setUp() throws Exception { Map deleteMe = getWebApplicationContext().getBeansOfType(SystemDeletable.class); @@ -291,7 +296,7 @@ public void createIdentityZone() throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName("Authorization").description("Bearer token containing `zones.write` or `zones..admin`") + headerWithName("Authorization").description("Bearer token containing `zones.write` or `uaa.admin`") ), requestFields(fieldDescriptors), getResponseFields() @@ -318,7 +323,9 @@ public void getIdentityZone() throws Exception { parameterWithName("id").description("Unique ID of the identity zone to retrieve") ), requestHeaders( - headerWithName("Authorization").description("Bearer token containing `zones.read` or `zones.write` or `zones..admin` or `zones..read`") + headerWithName("Authorization").description("Bearer token containing `zones.read` or `zones.write` or `uaa.admin`. If you use the zone-switching header, bear token containing `zones..admin` or `zones..read` can be used."), + IDENTITY_ZONE_ID_HEADER, + IDENTITY_ZONE_SUBDOMAIN_HEADER ), getResponseFields() )); @@ -446,7 +453,9 @@ public void getAllIdentityZones() throws Exception { .andDo(document("{ClassName}/{methodName}", preprocessResponse(prettyPrint()), requestHeaders( - headerWithName("Authorization").description("Bearer token containing `zones.read` or `zones..admin`") + headerWithName("Authorization").description("Bearer token containing `zones.read` or `zones.write` or `uaa.admin`. If you use the zone-switching header, bear token containing `zones..admin` can be used."), + IDENTITY_ZONE_ID_HEADER, + IDENTITY_ZONE_SUBDOMAIN_HEADER ), responseFields )); @@ -584,7 +593,9 @@ public void updateIdentityZone() throws Exception { parameterWithName("id").description("Unique ID of the identity zone to update") ), requestHeaders( - headerWithName("Authorization").description("Bearer token containing `zones.write` or `zones..admin`") + headerWithName("Authorization").description("Bearer token containing `zones.write` or `uaa.admin`. If you use the zone-switching header, bear token containing `zones..admin` can be used."), + IDENTITY_ZONE_ID_HEADER, + IDENTITY_ZONE_SUBDOMAIN_HEADER ), requestFields, getResponseFields() @@ -612,7 +623,9 @@ public void deleteIdentityZone() throws Exception { parameterWithName("id").description("Unique ID of the identity zone to delete") ), requestHeaders( - headerWithName("Authorization").description("Bearer token containing `zones.write`") + headerWithName("Authorization").description("Bearer token containing `zones.write` or `uaa.admin`. If you use the zone-switching header, bear token containing `zones..admin` can be used."), + IDENTITY_ZONE_ID_HEADER, + IDENTITY_ZONE_SUBDOMAIN_HEADER ), getResponseFields() )); From faf97ea61f6d98e6406fc194efd833b25539407d Mon Sep 17 00:00:00 2001 From: Mikhail Vyshegorodtsev Date: Fri, 19 Jan 2018 10:27:09 -0800 Subject: [PATCH 13/42] Added Jasmine. Added test for session state calculation. [#153568058] https://www.pivotaltracker.com/story/show/153568058 Signed-off-by: Dennis Leon --- .gitignore | 1 + server/build.gradle | 11 ++++ server/jasmine.json | 11 ++++ server/package-lock.json | 13 ++++ server/package.json | 18 ++++++ .../templates/web/session/session.js | 4 ++ .../resources/templates/web/session/sjcl.js | 60 +++++++++++++++++++ .../helpers/jasmine_examples/SpecHelper.js | 5 ++ .../spec/templates/web/SessionSpec.js | 16 +++++ 9 files changed, 139 insertions(+) create mode 100644 server/jasmine.json create mode 100644 server/package-lock.json create mode 100644 server/package.json create mode 100644 server/src/main/resources/templates/web/session/session.js create mode 100644 server/src/main/resources/templates/web/session/sjcl.js create mode 100644 server/src/test/javascript/spec/helpers/jasmine_examples/SpecHelper.js create mode 100644 server/src/test/javascript/spec/templates/web/SessionSpec.js diff --git a/.gitignore b/.gitignore index b43cb7c1804..1bb149d7618 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ build/ bin phantomjsdriver.log +server/node_modules # Docs uaa/slate/package-lock.json uaa/slate/node_modules diff --git a/server/build.gradle b/server/build.gradle index 00dcfbd2477..b9c68869d00 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -125,3 +125,14 @@ processResources { integrationTest {}.onlyIf { //disable since we don't have any true == false } + +task npmInstall(type: Exec) { + executable 'npm' + args 'install' +} + +task jasmineTest(type: Exec) { + dependsOn npmInstall + executable 'npm' + args 'test' +} \ No newline at end of file diff --git a/server/jasmine.json b/server/jasmine.json new file mode 100644 index 00000000000..090db5fac8c --- /dev/null +++ b/server/jasmine.json @@ -0,0 +1,11 @@ +{ + "spec_dir": "src/test/javascript/spec", + "spec_files": [ + "**/*[sS]pec.js" + ], + "helpers": [ + "helpers/**/*.js" + ], + "stopSpecOnExpectationFailure": false, + "random": true +} diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 00000000000..6cf81a2a5f2 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "server", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "jasmine-core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz", + "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=" + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 00000000000..8b4da01bf64 --- /dev/null +++ b/server/package.json @@ -0,0 +1,18 @@ +{ + "name": "server", + "version": "1.0.0", + "description": "This is used only to fetch dependencies for the UAA session iframe.", + "repository": "https://github.com/cloudfoundry/uaa", + "main": "index.js", + "directories": { + "lib": "lib" + }, + "scripts": { + "test": "jasmine --config=jasmine.json" + }, + "author": "CloudFoundry", + "license": "Apache-2.0", + "dependencies": { + "jasmine-core": "2.8.0" + } +} diff --git a/server/src/main/resources/templates/web/session/session.js b/server/src/main/resources/templates/web/session/session.js new file mode 100644 index 00000000000..79adba4f7ce --- /dev/null +++ b/server/src/main/resources/templates/web/session/session.js @@ -0,0 +1,4 @@ +function session_state(client_id, origin, browser_state, salt) { + var res = sjcl.hash.sha256.hash(client_id + ' ' + origin + ' ' + browser_state + ' ' + salt); + return sjcl.codec.hex.fromBits(res) + "." + salt; +} \ No newline at end of file diff --git a/server/src/main/resources/templates/web/session/sjcl.js b/server/src/main/resources/templates/web/session/sjcl.js new file mode 100644 index 00000000000..482bdce6764 --- /dev/null +++ b/server/src/main/resources/templates/web/session/sjcl.js @@ -0,0 +1,60 @@ +var sjcl={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){this.toString=function(){return"CORRUPT: "+this.message};this.message=a},invalid:function(a){this.toString=function(){return"INVALID: "+this.message};this.message=a},bug:function(a){this.toString=function(){return"BUG: "+this.message};this.message=a},notReady:function(a){this.toString=function(){return"NOT READY: "+this.message};this.message=a}}}; +sjcl.cipher.aes=function(a){this.s[0][0][0]||this.O();var b,c,d,e,f=this.s[0][4],g=this.s[1];b=a.length;var h=1;if(4!==b&&6!==b&&8!==b)throw new sjcl.exception.invalid("invalid aes key size");this.b=[d=a.slice(0),e=[]];for(a=b;a<4*b+28;a++){c=d[a-1];if(0===a%b||8===b&&4===a%b)c=f[c>>>24]<<24^f[c>>16&255]<<16^f[c>>8&255]<<8^f[c&255],0===a%b&&(c=c<<8^c>>>24^h<<24,h=h<<1^283*(h>>7));d[a]=d[a-b]^c}for(b=0;a;b++,a--)c=d[b&3?a:a-4],e[b]=4>=a||4>b?c:g[0][f[c>>>24]]^g[1][f[c>>16&255]]^g[2][f[c>>8&255]]^g[3][f[c& +255]]}; +sjcl.cipher.aes.prototype={encrypt:function(a){return t(this,a,0)},decrypt:function(a){return t(this,a,1)},s:[[[],[],[],[],[]],[[],[],[],[],[]]],O:function(){var a=this.s[0],b=this.s[1],c=a[4],d=b[4],e,f,g,h=[],k=[],l,n,m,p;for(e=0;0x100>e;e++)k[(h[e]=e<<1^283*(e>>7))^e]=e;for(f=g=0;!c[f];f^=l||1,g=k[g]||1)for(m=g^g<<1^g<<2^g<<3^g<<4,m=m>>8^m&255^99,c[f]=m,d[m]=f,n=h[e=h[l=h[f]]],p=0x1010101*n^0x10001*e^0x101*l^0x1010100*f,n=0x101*h[m]^0x1010100*m,e=0;4>e;e++)a[e][f]=n=n<<24^n>>>8,b[e][m]=p=p<<24^p>>>8;for(e= + 0;5>e;e++)a[e]=a[e].slice(0),b[e]=b[e].slice(0)}}; +function t(a,b,c){if(4!==b.length)throw new sjcl.exception.invalid("invalid aes block size");var d=a.b[c],e=b[0]^d[0],f=b[c?3:1]^d[1],g=b[2]^d[2];b=b[c?1:3]^d[3];var h,k,l,n=d.length/4-2,m,p=4,r=[0,0,0,0];h=a.s[c];a=h[0];var q=h[1],v=h[2],w=h[3],x=h[4];for(m=0;m>>24]^q[f>>16&255]^v[g>>8&255]^w[b&255]^d[p],k=a[f>>>24]^q[g>>16&255]^v[b>>8&255]^w[e&255]^d[p+1],l=a[g>>>24]^q[b>>16&255]^v[e>>8&255]^w[f&255]^d[p+2],b=a[b>>>24]^q[e>>16&255]^v[f>>8&255]^w[g&255]^d[p+3],p+=4,e=h,f=k,g=l;for(m= + 0;4>m;m++)r[c?3&-m:m]=x[e>>>24]<<24^x[f>>16&255]<<16^x[g>>8&255]<<8^x[b&255]^d[p++],h=e,e=f,f=g,g=b,b=h;return r} +sjcl.bitArray={bitSlice:function(a,b,c){a=sjcl.bitArray.$(a.slice(b/32),32-(b&31)).slice(1);return void 0===c?a:sjcl.bitArray.clamp(a,c-b)},extract:function(a,b,c){var d=Math.floor(-b-c&31);return((b+c-1^b)&-32?a[b/32|0]<<32-d^a[b/32+1|0]>>>d:a[b/32|0]>>>d)&(1<>b-1,1));return a},partial:function(a,b,c){return 32===a?b:(c?b|0:b<<32-a)+0x10000000000*a},getPartial:function(a){return Math.round(a/0x10000000000)||32},equal:function(a,b){if(sjcl.bitArray.bitLength(a)!==sjcl.bitArray.bitLength(b))return!1;var c=0,d;for(d=0;d>>b),c=a[e]<<32-b;e=a.length?a[a.length-1]:0;a=sjcl.bitArray.getPartial(e);d.push(sjcl.bitArray.partial(b+a&31,32>>24|c>>>8&0xff00|(c&0xff00)<<8|c<<24;return a}}; +sjcl.codec.utf8String={fromBits:function(a){var b="",c=sjcl.bitArray.bitLength(a),d,e;for(d=0;d>>8>>>8>>>8),e<<=8;return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,d=0;for(c=0;c>>g)>>>e),gn){if(!b)try{return sjcl.codec.base32hex.toBits(a)}catch(p){}throw new sjcl.exception.invalid("this isn't "+m+"!");}h>e?(h-=e,f.push(l^n>>>h),l=n<>>e)>>>26),6>e?(g=a[c]<<6-e,e+=26,c++):(g<<=6,e-=6);for(;d.length&3&&!b;)d+="=";return d},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],d,e=0,f=sjcl.codec.base64.B,g=0,h;b&&(f=f.substr(0,62)+"-_");for(d=0;dh)throw new sjcl.exception.invalid("this isn't base64!");26>>e),g=h<<32-e):(e+=6,g^=h<<32-e)}e&56&&c.push(sjcl.bitArray.partial(e&56,g,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.b[0]||this.O();a?(this.F=a.F.slice(0),this.A=a.A.slice(0),this.l=a.l):this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()}; +sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.F=this.Y.slice(0);this.A=[];this.l=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.A=sjcl.bitArray.concat(this.A,a);b=this.l;a=this.l=b+sjcl.bitArray.bitLength(a);if(0x1fffffffffffffb;c++){e=!0;for(d=2;d*d<=c;d++)if(0===c%d){e= + !1;break}e&&(8>b&&(this.Y[b]=a(Math.pow(c,.5))),this.b[b]=a(Math.pow(c,1/3)),b++)}}}; +function u(a,b){var c,d,e,f=a.F,g=a.b,h=f[0],k=f[1],l=f[2],n=f[3],m=f[4],p=f[5],r=f[6],q=f[7];for(c=0;64>c;c++)16>c?d=b[c]:(d=b[c+1&15],e=b[c+14&15],d=b[c&15]=(d>>>7^d>>>18^d>>>3^d<<25^d<<14)+(e>>>17^e>>>19^e>>>10^e<<15^e<<13)+b[c&15]+b[c+9&15]|0),d=d+q+(m>>>6^m>>>11^m>>>25^m<<26^m<<21^m<<7)+(r^m&(p^r))+g[c],q=r,r=p,p=m,m=n+d|0,n=l,l=k,k=h,h=d+(k&l^n&(k^l))+(k>>>2^k>>>13^k>>>22^k<<30^k<<19^k<<10)|0;f[0]=f[0]+h|0;f[1]=f[1]+k|0;f[2]=f[2]+l|0;f[3]=f[3]+n|0;f[4]=f[4]+m|0;f[5]=f[5]+p|0;f[6]=f[6]+r|0;f[7]= + f[7]+q|0} +sjcl.mode.ccm={name:"ccm",G:[],listenProgress:function(a){sjcl.mode.ccm.G.push(a)},unListenProgress:function(a){a=sjcl.mode.ccm.G.indexOf(a);-1k)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(f=2;4>f&&l>>>8*f;f++);f<15-k&&(f=15-k);c=h.clamp(c, + 8*(15-f));b=sjcl.mode.ccm.V(a,b,c,d,e,f);g=sjcl.mode.ccm.C(a,g,c,b,e,f);return h.concat(g.data,g.tag)},decrypt:function(a,b,c,d,e){e=e||64;d=d||[];var f=sjcl.bitArray,g=f.bitLength(c)/8,h=f.bitLength(b),k=f.clamp(b,h-e),l=f.bitSlice(b,h-e),h=(h-e)/8;if(7>g)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(b=2;4>b&&h>>>8*b;b++);b<15-g&&(b=15-g);c=f.clamp(c,8*(15-b));k=sjcl.mode.ccm.C(a,k,c,l,e,b);a=sjcl.mode.ccm.V(a,k.data,c,d,e,b);if(!f.equal(k.tag,a))throw new sjcl.exception.corrupt("ccm: tag doesn't match"); + return k.data},na:function(a,b,c,d,e,f){var g=[],h=sjcl.bitArray,k=h.i;d=[h.partial(8,(b.length?64:0)|d-2<<2|f-1)];d=h.concat(d,c);d[3]|=e;d=a.encrypt(d);if(b.length)for(c=h.bitLength(b)/8,65279>=c?g=[h.partial(16,c)]:0xffffffff>=c&&(g=h.concat([h.partial(16,65534)],[c])),g=h.concat(g,b),b=0;be||16n&&(sjcl.mode.ccm.fa(g/ + k),n+=m),c[3]++,e=a.encrypt(c),b[g]^=e[0],b[g+1]^=e[1],b[g+2]^=e[2],b[g+3]^=e[3];return{tag:d,data:h.clamp(b,l)}}}; +sjcl.mode.ocb2={name:"ocb2",encrypt:function(a,b,c,d,e,f){if(128!==sjcl.bitArray.bitLength(c))throw new sjcl.exception.invalid("ocb iv must be 128 bits");var g,h=sjcl.mode.ocb2.S,k=sjcl.bitArray,l=k.i,n=[0,0,0,0];c=h(a.encrypt(c));var m,p=[];d=d||[];e=e||64;for(g=0;g+4e.bitLength(c)&&(h=f(h,d(h)),c=e.concat(c,[-2147483648,0,0,0]));g=f(g,c); + return a.encrypt(f(d(f(h,d(h))),g))},S:function(a){return[a[0]<<1^a[1]>>>31,a[1]<<1^a[2]>>>31,a[2]<<1^a[3]>>>31,a[3]<<1^135*(a[0]>>>31)]}}; +sjcl.mode.gcm={name:"gcm",encrypt:function(a,b,c,d,e){var f=b.slice(0);b=sjcl.bitArray;d=d||[];a=sjcl.mode.gcm.C(!0,a,f,d,c,e||128);return b.concat(a.data,a.tag)},decrypt:function(a,b,c,d,e){var f=b.slice(0),g=sjcl.bitArray,h=g.bitLength(f);e=e||128;d=d||[];e<=h?(b=g.bitSlice(f,h-e),f=g.bitSlice(f,0,h-e)):(b=f,f=[]);a=sjcl.mode.gcm.C(!1,a,f,d,c,e);if(!g.equal(a.tag,b))throw new sjcl.exception.corrupt("gcm: tag doesn't match");return a.data},ka:function(a,b){var c,d,e,f,g,h=sjcl.bitArray.i;e=[0,0, + 0,0];f=b.slice(0);for(c=0;128>c;c++){(d=0!==(a[Math.floor(c/32)]&1<<31-c%32))&&(e=h(e,f));g=0!==(f[3]&1);for(d=3;0>>1|(f[d-1]&1)<<31;f[0]>>>=1;g&&(f[0]^=-0x1f000000)}return e},j:function(a,b,c){var d,e=c.length;b=b.slice(0);for(d=0;de&&(a=b.hash(a));for(d=0;dd||0>c)throw new sjcl.exception.invalid("invalid params to pbkdf2");"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));"string"===typeof b&&(b=sjcl.codec.utf8String.toBits(b));e=e||sjcl.misc.hmac;a=new e(a);var f,g,h,k,l=[],n=sjcl.bitArray;for(k=1;32*l.length<(d||1);k++){e=f=a.encrypt(n.concat(b,[k]));for(g=1;gg;g++)e.push(0x100000000*Math.random()|0);for(g=0;g=1<this.o&&(this.o= + f);this.P++;this.b=sjcl.hash.sha256.hash(this.b.concat(e));this.L=new sjcl.cipher.aes(this.b);for(d=0;4>d&&(this.h[d]=this.h[d]+1|0,!this.h[d]);d++);}for(d=0;d>>1;this.c[g].update([d,this.N++,2,b,f,a.length].concat(a))}break;case "string":void 0===b&&(b=a.length);this.c[g].update([d,this.N++,3,b,f,a.length]);this.c[g].update(a);break;default:k=1}if(k)throw new sjcl.exception.bug("random: addEntropy only supports number, array of numbers or string");this.m[g]+=b;this.f+=b;h===this.u&&(this.isReady()!==this.u&&A("seeded",Math.max(this.o,this.f)),A("progress",this.getProgress()))}, + isReady:function(a){a=this.T[void 0!==a?a:this.M];return this.o&&this.o>=a?this.m[0]>this.ba&&(new Date).valueOf()>this.Z?this.J|this.I:this.I:this.f>=a?this.J|this.u:this.u},getProgress:function(a){a=this.T[a?a:this.M];return this.o>=a?1:this.f>a?1:this.f/a},startCollectors:function(){if(!this.D){this.a={loadTimeCollector:B(this,this.ma),mouseCollector:B(this,this.oa),keyboardCollector:B(this,this.la),accelerometerCollector:B(this,this.ea),touchCollector:B(this,this.qa)};if(window.addEventListener)window.addEventListener("load", + this.a.loadTimeCollector,!1),window.addEventListener("mousemove",this.a.mouseCollector,!1),window.addEventListener("keypress",this.a.keyboardCollector,!1),window.addEventListener("devicemotion",this.a.accelerometerCollector,!1),window.addEventListener("touchmove",this.a.touchCollector,!1);else if(document.attachEvent)document.attachEvent("onload",this.a.loadTimeCollector),document.attachEvent("onmousemove",this.a.mouseCollector),document.attachEvent("keypress",this.a.keyboardCollector);else throw new sjcl.exception.bug("can't attach event"); + this.D=!0}},stopCollectors:function(){this.D&&(window.removeEventListener?(window.removeEventListener("load",this.a.loadTimeCollector,!1),window.removeEventListener("mousemove",this.a.mouseCollector,!1),window.removeEventListener("keypress",this.a.keyboardCollector,!1),window.removeEventListener("devicemotion",this.a.accelerometerCollector,!1),window.removeEventListener("touchmove",this.a.touchCollector,!1)):document.detachEvent&&(document.detachEvent("onload",this.a.loadTimeCollector),document.detachEvent("onmousemove", + this.a.mouseCollector),document.detachEvent("keypress",this.a.keyboardCollector)),this.D=!1)},addEventListener:function(a,b){this.K[a][this.ga++]=b},removeEventListener:function(a,b){var c,d,e=this.K[a],f=[];for(d in e)e.hasOwnProperty(d)&&e[d]===b&&f.push(d);for(c=0;cb&&(a.h[b]=a.h[b]+1|0,!a.h[b]);b++);return a.L.encrypt(a.h)} +function B(a,b){return function(){b.apply(a,arguments)}}sjcl.random=new sjcl.prng(6); +a:try{var D,E,F,G;if(G="undefined"!==typeof module&&module.exports){var H;try{H=require("crypto")}catch(a){H=null}G=E=H}if(G&&E.randomBytes)D=E.randomBytes(128),D=new Uint32Array((new Uint8Array(D)).buffer),sjcl.random.addEntropy(D,1024,"crypto['randomBytes']");else if("undefined"!==typeof window&&"undefined"!==typeof Uint32Array){F=new Uint32Array(32);if(window.crypto&&window.crypto.getRandomValues)window.crypto.getRandomValues(F);else if(window.msCrypto&&window.msCrypto.getRandomValues)window.msCrypto.getRandomValues(F); +else break a;sjcl.random.addEntropy(F,1024,"crypto['getRandomValues']")}}catch(a){"undefined"!==typeof window&&window.console&&(console.log("There was an error collecting entropy from the browser:"),console.log(a))} +sjcl.json={defaults:{v:1,iter:1E4,ks:128,ts:64,mode:"ccm",adata:"",cipher:"aes"},ja:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json,f=e.g({iv:sjcl.random.randomWords(4,0)},e.defaults),g;e.g(f,c);c=f.adata;"string"===typeof f.salt&&(f.salt=sjcl.codec.base64.toBits(f.salt));"string"===typeof f.iv&&(f.iv=sjcl.codec.base64.toBits(f.iv));if(!sjcl.mode[f.mode]||!sjcl.cipher[f.cipher]||"string"===typeof a&&100>=f.iter||64!==f.ts&&96!==f.ts&&128!==f.ts||128!==f.ks&&192!==f.ks&&0x100!==f.ks||2>f.iv.length|| + 4=b.iter||64!==b.ts&&96!==b.ts&&128!==b.ts||128!==b.ks&&192!==b.ks&&0x100!==b.ks||!b.iv||2>b.iv.length||4 Date: Mon, 22 Jan 2018 12:05:48 -0800 Subject: [PATCH 14/42] Add tests to UaaUrlUtils [#153568058] https://www.pivotaltracker.com/story/show/153568058 Signed-off-by: Mikhail Vyshegorodtsev --- .../identity/uaa/util/UaaUrlUtils.java | 12 ++++- .../identity/uaa/util/UaaUrlUtilsTest.java | 52 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtils.java b/server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtils.java index aa3f0da933a..016e45dd89a 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtils.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtils.java @@ -186,6 +186,13 @@ public static String addSubdomainToUrl(String url) { return addSubdomainToUrl(url, getSubdomain()); } public static String addSubdomainToUrl(String url, String subdomain) { + if (!hasText(subdomain)) { + return url; + } + + subdomain = subdomain.trim(); + subdomain = subdomain.endsWith(".") ? subdomain : subdomain + "."; + UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url); builder.host(subdomain + builder.build().getHost()); return builder.build().toUriString(); @@ -194,9 +201,10 @@ public static String addSubdomainToUrl(String url, String subdomain) { public static String getSubdomain() { String subdomain = IdentityZoneHolder.get().getSubdomain(); if (hasText(subdomain)) { - subdomain += "."; + subdomain = subdomain.trim(); + subdomain = subdomain.endsWith(".") ? subdomain : subdomain + "."; } - return subdomain.trim(); + return subdomain; } public static String extractPathVariableFromUrl(int pathParameterIndex, String path) { diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java index 913b0efee23..b4a2f6756de 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtilsTest.java @@ -370,6 +370,58 @@ public void test_validate_invalid_redirect_uri() { validateRedirectUri(convertToHttps(invalidHttpWildCardUrls), false); } + @Test + public void addSubdomainToUrl_givenUaaUrl() { + IdentityZoneHolder.set(new IdentityZone().setSubdomain("somezone")); + String url = UaaUrlUtils.addSubdomainToUrl("http://localhost:8080"); + assertEquals("http://somezone.localhost:8080", url); + } + + @Test + public void addSubdomainToUrl_givenUaaUrlAndSubdomain() { + String url = UaaUrlUtils.addSubdomainToUrl("http://localhost:8080", "somezone"); + assertEquals("http://somezone.localhost:8080", url); + } + + @Test + public void addSubdomainToUrl_handlesEmptySubdomain() { + String url = UaaUrlUtils.addSubdomainToUrl("http://localhost:8080", ""); + assertEquals("http://localhost:8080", url); + } + + @Test + public void addSubdomainToUrl_handlesEmptySubdomain_defaultZone() { + IdentityZoneHolder.set(new IdentityZone().setSubdomain("")); + String url2 = UaaUrlUtils.addSubdomainToUrl("http://localhost:8080"); + assertEquals("http://localhost:8080", url2); + } + + @Test + public void addSudomain_handlesExtraSpaceInSubdomain() { + String url = UaaUrlUtils.addSubdomainToUrl("http://localhost:8080", " somezone "); + assertEquals("http://somezone.localhost:8080", url); + } + + @Test + public void addSudomain_handlesExtraSpaceInSubdomain_currentZone() { + IdentityZoneHolder.set(new IdentityZone().setSubdomain(" somezone2 ")); + String url2 = UaaUrlUtils.addSubdomainToUrl("http://localhost:8080"); + assertEquals("http://somezone2.localhost:8080", url2); + } + + @Test + public void addSubdomain_handlesUnexpectedDotInSubdomain() { + String url = UaaUrlUtils.addSubdomainToUrl("http://localhost:8080", " somezone. "); + assertEquals("http://somezone.localhost:8080", url); + } + + @Test + public void addSubdomain_handlesUnexpectedDotInSubdomain_currentZone() { + IdentityZoneHolder.set(new IdentityZone().setSubdomain(" somezone2. ")); + String url2 = UaaUrlUtils.addSubdomainToUrl("http://localhost:8080"); + assertEquals("http://somezone2.localhost:8080", url2); + } + private void validateRedirectUri(List urls, boolean result) { Map failed = getFailedUrls(urls, result); if (!failed.isEmpty()) { From 705bca3b553c11312f6cbb881eb31c2453cebbac Mon Sep 17 00:00:00 2001 From: Dennis Leon Date: Wed, 24 Jan 2018 11:25:48 -0800 Subject: [PATCH 15/42] Modify OpenIdSessionStateCalculator to generate session hash from client id, session id, salt and origin (from redirect_uri param) - Breaking Change: Changed OpenIdSessionStateCalculator API to accept origin argument [#153568058] https://www.pivotaltracker.com/story/show/153568058 Signed-off-by: Jennifer Hamon --- .../oauth/OpenIdSessionStateCalculator.java | 39 +++---- .../uaa/oauth/UaaAuthorizationEndpoint.java | 65 ++++++----- .../OpenIdSessionStateCalculatorTest.java | 31 ++++-- .../oauth/UaaAuthorizationEndpointTest.java | 58 ++++------ .../webapp/WEB-INF/spring/oauth-endpoints.xml | 1 + .../WEB-INF/spring/openid-endpoints.xml | 3 + ...ationPromptNoneEntryPointMockMvcTests.java | 101 ++++++++++++++++++ ...orizePromptNoneEntryPointMockMvcTests.java | 57 ---------- uaa/src/test/resources/test/config/uaa.yml | 2 + 9 files changed, 208 insertions(+), 149 deletions(-) create mode 100644 uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java delete mode 100644 uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizePromptNoneEntryPointMockMvcTests.java diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/OpenIdSessionStateCalculator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/OpenIdSessionStateCalculator.java index 374b2c1f34f..917c3affe23 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/OpenIdSessionStateCalculator.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/OpenIdSessionStateCalculator.java @@ -1,41 +1,36 @@ package org.cloudfoundry.identity.uaa.oauth; -import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; +import org.apache.commons.codec.digest.DigestUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.xml.bind.DatatypeConverter; import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; public class OpenIdSessionStateCalculator { - private final String sessionId; - private final String clientId; - private final String origin; - private final String salt; private final Logger logger = LoggerFactory.getLogger(OpenIdSessionStateCalculator.class); + private String uaaUrl; + private SecureRandom secureRandom; - public OpenIdSessionStateCalculator(UaaAuthenticationDetails details, SecureRandom secureRandom) { - this.sessionId = details.getSessionId(); - this.clientId = details.getClientId(); - this.origin = details.getOrigin(); + public OpenIdSessionStateCalculator(String uaaUrl) { + this.uaaUrl = uaaUrl; + this.secureRandom = new SecureRandom(); + } + + public String calculate(String sessionId, String clientId, String origin) { byte[] array = new byte[32]; secureRandom.nextBytes(array); - salt = DatatypeConverter.printHexBinary(array).toLowerCase(); - } + String salt = DatatypeConverter.printHexBinary(array).toLowerCase(); - public String calculate() { String text = String.format("%s %s %s %s", clientId, origin, sessionId, salt); - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] hash = digest.digest(text.getBytes(StandardCharsets.UTF_8)); - return String.format("%s.%s", DatatypeConverter.printHexBinary(hash).toLowerCase(), salt); - } catch (NoSuchAlgorithmException e) { - logger.error("Could not find algorithm SHA-256, aborting"); - return null; - } + byte[] hash = DigestUtils.sha256(text.getBytes(StandardCharsets.UTF_8)); + logger.debug(String.format("Calculated OIDC session state for clientId=%s, origin=%s, sessionId=REDACTED, salt=%s", clientId, origin, salt)); + return String.format("%s.%s", DatatypeConverter.printHexBinary(hash).toLowerCase(), salt); + } + + public void setSecureRandom(SecureRandom secureRandom) { + this.secureRandom = secureRandom; } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java index 34cb883f6cd..e4d302fc6ae 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java @@ -13,7 +13,8 @@ package org.cloudfoundry.identity.uaa.oauth; -import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; +import org.apache.http.HttpHost; +import org.apache.http.client.utils.URIUtils; import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; import org.cloudfoundry.identity.uaa.oauth.token.CompositeAccessToken; import org.cloudfoundry.identity.uaa.util.UaaHttpRequestUtils; @@ -51,7 +52,6 @@ import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices; import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices; import org.springframework.security.oauth2.provider.endpoint.AbstractEndpoint; -import org.springframework.security.oauth2.provider.endpoint.DefaultRedirectResolver; import org.springframework.security.oauth2.provider.endpoint.RedirectResolver; import org.springframework.security.oauth2.provider.implicit.ImplicitTokenRequest; import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestValidator; @@ -67,6 +67,7 @@ import org.springframework.web.bind.support.DefaultSessionAttributeStore; import org.springframework.web.bind.support.SessionAttributeStore; import org.springframework.web.bind.support.SessionStatus; +import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.View; @@ -77,8 +78,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.UnsupportedEncodingException; +import java.net.URI; import java.security.Principal; -import java.security.SecureRandom; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -116,23 +117,10 @@ public class UaaAuthorizationEndpoint extends AbstractEndpoint { private HybridTokenGranterForAuthorizationCode hybridTokenGranterForAuthCode; - public HybridTokenGranterForAuthorizationCode getHybridTokenGranterForAuthCode() { - return hybridTokenGranterForAuthCode; - } - - public void setHybridTokenGranterForAuthCode(HybridTokenGranterForAuthorizationCode hybridTokenGranterForAuthCode) { - this.hybridTokenGranterForAuthCode = hybridTokenGranterForAuthCode; - } - - public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) { - this.sessionAttributeStore = sessionAttributeStore; - } - - public void setErrorPage(String errorPage) { - this.errorPage = errorPage; - } + private OpenIdSessionStateCalculator openIdSessionStateCalculator; private static final List supported_response_types = Arrays.asList("code", "token", "id_token"); + @RequestMapping(value = "/oauth/authorize") public ModelAndView authorize(Map model, @RequestParam Map parameters, @@ -447,14 +435,12 @@ public String buildRedirectURI(AuthorizationRequest authorizationRequest, } } - if (authUser.getDetails() != null && authUser.getDetails() instanceof UaaAuthenticationDetails) { - UaaAuthenticationDetails details = (UaaAuthenticationDetails) authUser.getDetails(); - OpenIdSessionStateCalculator openIdSessionStateCalculator = new OpenIdSessionStateCalculator(details, new SecureRandom()); - String session_state = openIdSessionStateCalculator.calculate(); - if (session_state != null) { - url.append("&session_state=").append(session_state); - } - } + + HttpHost httpHost = URIUtils.extractHost(URI.create(requestedRedirect)); + String sessionState = openIdSessionStateCalculator.calculate(RequestContextHolder.currentRequestAttributes().getSessionId(), + authorizationRequest.getClientId(), httpHost.toURI()); + + url.append("&session_state=").append(sessionState); UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(requestedRedirect); String existingFragment = builder.build(true).getFragment(); @@ -657,11 +643,36 @@ private AuthorizationRequest getAuthorizationRequestForError(ServletWebRequest w } protected ClientServicesExtension getClientServiceExtention() { - return (ClientServicesExtension )super.getClientDetailsService(); + return (ClientServicesExtension) super.getClientDetailsService(); } public void setClientDetailsService(ClientServicesExtension clientDetailsService) { super.setClientDetailsService(clientDetailsService); } + + public HybridTokenGranterForAuthorizationCode getHybridTokenGranterForAuthCode() { + return hybridTokenGranterForAuthCode; + } + + public void setHybridTokenGranterForAuthCode(HybridTokenGranterForAuthorizationCode hybridTokenGranterForAuthCode) { + this.hybridTokenGranterForAuthCode = hybridTokenGranterForAuthCode; + } + + public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) { + this.sessionAttributeStore = sessionAttributeStore; + } + + public void setErrorPage(String errorPage) { + this.errorPage = errorPage; + } + + + public OpenIdSessionStateCalculator getOpenIdSessionStateCalculator() { + return openIdSessionStateCalculator; + } + + public void setOpenIdSessionStateCalculator(OpenIdSessionStateCalculator openIdSessionStateCalculator) { + this.openIdSessionStateCalculator = openIdSessionStateCalculator; + } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/OpenIdSessionStateCalculatorTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/OpenIdSessionStateCalculatorTest.java index 28059cf2d57..4d71149393d 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/OpenIdSessionStateCalculatorTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/OpenIdSessionStateCalculatorTest.java @@ -1,7 +1,7 @@ package org.cloudfoundry.identity.uaa.oauth; -import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; +import org.junit.Before; import org.junit.Test; import java.security.SecureRandom; @@ -13,15 +13,32 @@ public class OpenIdSessionStateCalculatorTest { - @Test - public void calculate() throws Exception { + private OpenIdSessionStateCalculator calculator; + + @Before + public void setup() throws Exception { + String uaaUrl = "http://localhost:8080"; + calculator = new OpenIdSessionStateCalculator(uaaUrl); SecureRandom secureRandom = mock(SecureRandom.class); doNothing().when(secureRandom).nextBytes(any()); + calculator.setSecureRandom(secureRandom); + } - UaaAuthenticationDetails details = new UaaAuthenticationDetails(true, "client-id", "origin", "session-id"); - OpenIdSessionStateCalculator openIdSessionState = new OpenIdSessionStateCalculator(details, secureRandom); + @Test + public void calculate() throws Exception { + String sessionState = calculator.calculate("session_id", "client_id", "http://example.com"); + assertEquals("b6d594e481f023303f2dd9e41af3c653564b34363f6dc0b5a5555fd31d8f56b4.0000000000000000000000000000000000000000000000000000000000000000", sessionState); + } - String sessionState = openIdSessionState.calculate(); - assertEquals("8d6dea62907d8796ffbed3c000cb7cdb9f3e3295545df54da940d7196917b653.0000000000000000000000000000000000000000000000000000000000000000", sessionState); + @Test + public void calculate_shouldChangeSessionIdChanges() { + String sessionState = calculator.calculate("session_id2", "client_id", "http://example.com"); + assertEquals("74992895f9312791755774d9ca7d175352ac7e10803631d23c5e79d228d881b4.0000000000000000000000000000000000000000000000000000000000000000", sessionState); + } + + @Test + public void calculate_shouldChangeClientIdChanges() { + String sessionState = calculator.calculate("session_id", "client_id2", "http://example.com"); + assertEquals("757191b323b642b37d4975bffaafefacc0b1d0386eb97c1983a3c8d18d0d3a13.0000000000000000000000000000000000000000000000000000000000000000", sessionState); } } \ No newline at end of file diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointTest.java index 177028bb6da..c049400d09b 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointTest.java @@ -6,13 +6,13 @@ import org.cloudfoundry.identity.uaa.oauth.token.CompositeAccessToken; import org.junit.Before; import org.junit.Test; -import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.provider.AuthorizationRequest; import org.springframework.security.oauth2.provider.OAuth2Request; import org.springframework.security.oauth2.provider.OAuth2RequestFactory; import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; import java.util.Calendar; import java.util.Collections; @@ -20,7 +20,6 @@ import java.util.Set; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.core.IsNot.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -33,6 +32,7 @@ public class UaaAuthorizationEndpointTest { private UaaAuthorizationEndpoint uaaAuthorizationEndpoint; private AuthorizationCodeServices authorizationCodeServices; private Set responseTypes; + private OpenIdSessionStateCalculator openIdSessionStateCalculator; @Before public void setup() { @@ -40,8 +40,16 @@ public void setup() { uaaAuthorizationEndpoint = new UaaAuthorizationEndpoint(); uaaAuthorizationEndpoint.setOAuth2RequestFactory(oAuth2RequestFactory); authorizationCodeServices = mock(AuthorizationCodeServices.class); + openIdSessionStateCalculator = mock(OpenIdSessionStateCalculator.class); uaaAuthorizationEndpoint.setAuthorizationCodeServices(authorizationCodeServices); + uaaAuthorizationEndpoint.setOpenIdSessionStateCalculator(openIdSessionStateCalculator); responseTypes = new HashSet<>(); + + RequestAttributes requestAttributeMock = mock(RequestAttributes.class); + String sessionId = "sessionid"; + when(requestAttributeMock.getSessionId()).thenReturn(sessionId); + RequestContextHolder.setRequestAttributes(requestAttributeMock, true); + when(openIdSessionStateCalculator.calculate(sessionId, null, "http://example.com")).thenReturn("opbshash"); } @@ -95,12 +103,18 @@ public void testGetGrantType_code_id_token_and_token_is_implicit() { @Test public void testBuildRedirectURI() { AuthorizationRequest authorizationRequest = new AuthorizationRequest(); - authorizationRequest.setRedirectUri("example.com"); - authorizationRequest.setResponseTypes(new HashSet() { { add("code"); add("token"); add("id_token"); } } ); + authorizationRequest.setRedirectUri("http://example.com/somepath"); + authorizationRequest.setResponseTypes(new HashSet() { + { + add("code"); + add("token"); + add("id_token"); + } + }); authorizationRequest.setState("California"); CompositeAccessToken accessToken = new CompositeAccessToken("TOKEN_VALUE+="); accessToken.setIdTokenValue("idTokenValue"); - UaaPrincipal principal = new UaaPrincipal("userid","username","email","origin","extid","zoneid"); + UaaPrincipal principal = new UaaPrincipal("userid", "username", "email", "origin", "extid", "zoneid"); UaaAuthenticationDetails details = new UaaAuthenticationDetails(true, "clientid", "origin", "SOMESESSIONID"); Authentication authUser = new UaaAuthentication(principal, Collections.emptyList(), details); accessToken.setExpiration(Calendar.getInstance().getTime()); @@ -109,35 +123,7 @@ public void testBuildRedirectURI() { when(authorizationCodeServices.createAuthorizationCode(any())).thenReturn("ABCD"); String result = uaaAuthorizationEndpoint.buildRedirectURI(authorizationRequest, accessToken, authUser); - assertThat(result, containsString("example.com#")); - assertThat(result, containsString("token_type=bearer")); - assertThat(result, containsString("access_token=TOKEN_VALUE%2B%3D")); - assertThat(result, containsString("id_token=idTokenValue")); - assertThat(result, containsString("code=ABCD")); - assertThat(result, containsString("state=California")); - assertThat(result, containsString("expires_in=")); - assertThat(result, containsString("scope=null")); - assertThat(result, containsString("session_state=")); - assertThat(result, not(containsString("session_state=null"))); - } - - @Test - public void testBuildRedirectURIAnonymous() { - AuthorizationRequest authorizationRequest = new AuthorizationRequest(); - authorizationRequest.setRedirectUri("example.com"); - authorizationRequest.setResponseTypes(new HashSet() { { add("code"); add("token"); add("id_token"); } } ); - authorizationRequest.setState("California"); - CompositeAccessToken accessToken = new CompositeAccessToken("TOKEN_VALUE+="); - accessToken.setIdTokenValue("idTokenValue"); - UaaPrincipal principal = new UaaPrincipal("userid","username","email","origin","extid","zoneid"); - Authentication authAnonymous = new AnonymousAuthenticationToken("key", principal, Collections.singletonList(new SimpleGrantedAuthority("openid"))); - accessToken.setExpiration(Calendar.getInstance().getTime()); - OAuth2Request storedOAuth2Request = mock(OAuth2Request.class); - when(oAuth2RequestFactory.createOAuth2Request(any())).thenReturn(storedOAuth2Request); - when(authorizationCodeServices.createAuthorizationCode(any())).thenReturn("ABCD"); - String result = uaaAuthorizationEndpoint.buildRedirectURI(authorizationRequest, accessToken, authAnonymous); - - assertThat(result, containsString("example.com#")); + assertThat(result, containsString("http://example.com/somepath#")); assertThat(result, containsString("token_type=bearer")); assertThat(result, containsString("access_token=TOKEN_VALUE%2B%3D")); assertThat(result, containsString("id_token=idTokenValue")); @@ -145,6 +131,6 @@ public void testBuildRedirectURIAnonymous() { assertThat(result, containsString("state=California")); assertThat(result, containsString("expires_in=")); assertThat(result, containsString("scope=null")); - assertThat(result, not(containsString("session_state="))); + assertThat(result, containsString("session_state=opbshash")); } } \ No newline at end of file diff --git a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml index e7a1dcb0336..d37f5ac5867 100755 --- a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml @@ -154,6 +154,7 @@ + diff --git a/uaa/src/main/webapp/WEB-INF/spring/openid-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/openid-endpoints.xml index 9e72619bb9e..a3166ebd68c 100755 --- a/uaa/src/main/webapp/WEB-INF/spring/openid-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/openid-endpoints.xml @@ -38,4 +38,7 @@ + + + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java new file mode 100644 index 00000000000..0e2a76ebf01 --- /dev/null +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java @@ -0,0 +1,101 @@ +package org.cloudfoundry.identity.uaa.mock.oauth; + +import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; +import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; +import org.cloudfoundry.identity.uaa.oauth.OpenIdSessionStateCalculator; +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; +import org.springframework.test.web.servlet.MvcResult; + +import java.security.SecureRandom; +import java.util.Arrays; + +import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class AuthorizationPromptNoneEntryPointMockMvcTests extends InjectedMockContextTest { + + private static boolean isInitDone = false; + + @Before + public void setup() throws Exception { + if(!isInitDone) { + BaseClientDetails client = new BaseClientDetails("ant", "", "openid", "implicit", "", "http://example.com/**"); + client.setAutoApproveScopes(Arrays.asList("openid")); + String adminToken = testClient.getClientCredentialsOAuthAccessToken("admin", "adminsecret", + "clients.read clients.write clients.secret clients.admin uaa.admin"); + MockMvcUtils.createClient(getMockMvc(), adminToken, client); + isInitDone = true; + } + } + + @Test + public void testSilentAuthHonorsAntRedirect_whenNotAuthenticated() throws Exception { + getMockMvc().perform( + get("/oauth/authorize?response_type=token&scope=openid&client_id=ant&prompt=none&redirect_uri=http://example.com/with/path.html") + ) + .andExpect(redirectedUrl("http://example.com/with/path.html#error=login_required")); + } + + @Test + public void testSilentAuthHonorsAntRedirect_whenAuthenticated() throws Exception { + MockHttpSession session = new MockHttpSession(); + login(session); + session.invalidate(); + + getMockMvc().perform( + get("/oauth/authorize?response_type=token&scope=openid&client_id=ant&prompt=none&redirect_uri=http://example.com/with/path.html") + .session(session) + ) + .andExpect(redirectedUrlPattern("http://example.com/**/*")); + } + + @Test + public void testSessionStateIsCorrect() throws Exception { + SecureRandom secureRandom = mock(SecureRandom.class); + doNothing().when(secureRandom).nextBytes(any()); + + OpenIdSessionStateCalculator sessionStateCalculator + = (OpenIdSessionStateCalculator)getWebApplicationContext().getBean("openIdSessionStateCalculator"); + sessionStateCalculator.setSecureRandom(secureRandom); + + //we need to know session id when we are calculating session_state + MockHttpSession session = new MockHttpSession(null, "12345") { + public String changeSessionId() { + return "12345"; + } + }; + login(session); + + MvcResult result = getMockMvc().perform( + get("/oauth/authorize?response_type=token&scope=openid&client_id=ant&prompt=none&redirect_uri=http://example.com/with/path.html") + .session(session) + ) + .andExpect(status().isFound()) + .andReturn(); + + String redirectUrl = result.getResponse().getRedirectedUrl(); + Assert.assertThat(redirectUrl, Matchers.containsString("session_state=707c310bc5aa38acc03d48a099fc999cd77f44df163178df1ca35863913f5711.0000000000000000000000000000000000000000000000000000000000000000")); + } + + private void login(MockHttpSession session) throws Exception { + getMockMvc().perform( + post("/login.do") + .with(cookieCsrf()) + .param("username", "marissa") + .param("password", "koala") + .session(session) + ).andExpect(redirectedUrl("/")); + } +} \ No newline at end of file diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizePromptNoneEntryPointMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizePromptNoneEntryPointMockMvcTests.java deleted file mode 100644 index 83783d66071..00000000000 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizePromptNoneEntryPointMockMvcTests.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.cloudfoundry.identity.uaa.mock.oauth; - -import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; -import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; -import org.junit.Before; -import org.junit.Test; -import org.springframework.mock.web.MockHttpSession; -import org.springframework.security.oauth2.provider.client.BaseClientDetails; - -import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; - -public class AuthorizePromptNoneEntryPointMockMvcTests extends InjectedMockContextTest { - - private static String adminToken; - private static boolean isInitDone = false; - - @Before - public void setup() throws Exception { - if(!isInitDone) { - BaseClientDetails client = new BaseClientDetails("ant", "", "openid", "implicit", "", "http://example.com/**"); - adminToken = testClient.getClientCredentialsOAuthAccessToken("admin", "adminsecret", - "clients.read clients.write clients.secret clients.admin uaa.admin"); - MockMvcUtils.createClient(getMockMvc(), adminToken, client); - isInitDone = true; - } - } - - @Test - public void testSilentAuthHonorsAntRedirect_whenNotAuthenticated() throws Exception { - getMockMvc().perform( - get("/oauth/authorize?response_type=token&scope=openid&client_id=ant&prompt=none&redirect_uri=http://example.com/with/path.html") - .header("Authorization", "Bearer " + adminToken) - ) - .andExpect(redirectedUrl("http://example.com/with/path.html#error=login_required")); - } - - @Test - public void testSilentAuthHonorsAntRedirect_whenAuthenticated() throws Exception { - MockHttpSession session = new MockHttpSession(); - getMockMvc().perform( - post("/login.do") - .with(cookieCsrf()) - .param("username", "marissa") - .param("password", "koala") - .session(session) - ).andExpect(redirectedUrl("/")); - getMockMvc().perform( - get("/oauth/authorize?response_type=token&scope=openid&client_id=ant&prompt=none&redirect_uri=http://example.com/with/path.html") - .session(session) - .header("Authorization", "Bearer " + adminToken) - ) - .andExpect(redirectedUrl("http://example.com/with/path.html#error=interaction_required")); - } -} \ No newline at end of file diff --git a/uaa/src/test/resources/test/config/uaa.yml b/uaa/src/test/resources/test/config/uaa.yml index cdf9f418c9b..d28ae25f36a 100644 --- a/uaa/src/test/resources/test/config/uaa.yml +++ b/uaa/src/test/resources/test/config/uaa.yml @@ -1,3 +1,5 @@ +uaa: + url: http://localhost:8080/uaa foo: bar spring_profiles: hsqldb logging: From 53385cfa775e3b65a2d5dd85f1f3030771cedbbc Mon Sep 17 00:00:00 2001 From: Jennifer Hamon Date: Wed, 24 Jan 2018 11:27:09 -0800 Subject: [PATCH 16/42] Reformatting Signed-off-by: Dennis Leon --- .../uaa/oauth/UaaAuthorizationEndpoint.java | 88 +++++++++---------- 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java index e4d302fc6ae..5361fefabb7 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java @@ -133,8 +133,7 @@ public ModelAndView authorize(Map model, try { clientId = parameters.get("client_id"); client = getClientServiceExtention().loadClientByClientId(clientId, IdentityZoneHolder.get().getId()); - } - catch (NoSuchClientException x) { + } catch (NoSuchClientException x) { throw new InvalidClientException(x.getMessage()); } @@ -166,18 +165,18 @@ public ModelAndView authorize(Map model, resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client); } catch (RedirectMismatchException rme) { throw new RedirectMismatchException( - "Invalid redirect " + redirectUriParameter + " did not match one of the registered values"); + "Invalid redirect " + redirectUriParameter + " did not match one of the registered values"); } if (!StringUtils.hasText(resolvedRedirect)) { throw new RedirectMismatchException( - "A redirectUri must be either supplied or preconfigured in the ClientDetails"); + "A redirectUri must be either supplied or preconfigured in the ClientDetails"); } boolean isAuthenticated = (principal instanceof Authentication) && ((Authentication) principal).isAuthenticated(); if (!isAuthenticated) { throw new InsufficientAuthenticationException( - "User must be authenticated with Spring Security before authorization can be completed."); + "User must be authenticated with Spring Security before authorization can be completed."); } authorizationRequest.setRedirectUri(resolvedRedirect); @@ -188,31 +187,29 @@ public ModelAndView authorize(Map model, // Some systems may allow for approval decisions to be remembered or approved by default. Check for // such logic here, and set the approved flag on the authorization request accordingly. authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest, - (Authentication) principal); - // TODO: is this call necessary? + (Authentication) principal); boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal); authorizationRequest.setApproved(approved); // Validation is all done, so we can check for auto approval... if (authorizationRequest.isApproved()) { - //TODO we must get a code and a token,id_token if (responseTypes.contains("token") || responseTypes.contains("id_token")) { return getImplicitGrantOrHybridResponse( authorizationRequest, (Authentication) principal, grantType - ); + ); } if (responseTypes.contains("code")) { return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest, - (Authentication) principal)); + (Authentication) principal)); } } - if ("none".equals(authorizationRequest.getRequestParameters().get("prompt"))){ + if ("none".equals(authorizationRequest.getRequestParameters().get("prompt"))) { return new ModelAndView( - new RedirectView(UaaUrlUtils.addFragmentComponent(resolvedRedirect, "error=interaction_required")) + new RedirectView(UaaUrlUtils.addFragmentComponent(resolvedRedirect, "error=interaction_required")) ); } else { // Place auth request into the model so that it is stored in the session @@ -233,7 +230,7 @@ public ModelAndView authorize(Map model, private ModelAndView switchIdp(Map model, ClientDetails client, String clientId, HttpServletRequest request) { Map additionalInfo = client.getAdditionalInformation(); String clientDisplayName = (String) additionalInfo.get(ClientConstants.CLIENT_NAME); - model.put("client_display_name", (clientDisplayName != null)? clientDisplayName : clientId); + model.put("client_display_name", (clientDisplayName != null) ? clientDisplayName : clientId); String queryString = UaaHttpRequestUtils.paramsToQueryString(request.getParameterMap()); String redirectUri = request.getRequestURL() + "?" + queryString; @@ -252,7 +249,7 @@ public View approveOrDeny(@RequestParam Map approvalParameters, if (!(principal instanceof Authentication)) { sessionStatus.setComplete(); throw new InsufficientAuthenticationException( - "User must be authenticated with Spring Security before authorizing an access token."); + "User must be authenticated with Spring Security before authorizing an access token."); } AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get("authorizationRequest"); @@ -268,7 +265,7 @@ public View approveOrDeny(@RequestParam Map approvalParameters, authorizationRequest.setApprovalParameters(approvalParameters); authorizationRequest = userApprovalHandler.updateAfterApproval(authorizationRequest, - (Authentication) principal); + (Authentication) principal); boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal); authorizationRequest.setApproved(approved); @@ -279,17 +276,16 @@ public View approveOrDeny(@RequestParam Map approvalParameters, if (!authorizationRequest.isApproved()) { return new RedirectView(getUnsuccessfulRedirect(authorizationRequest, - new UserDeniedAuthorizationException("User denied access"), responseTypes.contains("token")), - false, true, false); + new UserDeniedAuthorizationException("User denied access"), responseTypes.contains("token")), + false, true, false); } if (responseTypes.contains("token") || responseTypes.contains("id_token")) { - //TODO we must get a code and a token,id_token return getImplicitGrantOrHybridResponse( authorizationRequest, (Authentication) principal, grantType - ).getView(); + ).getView(); } return getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal); @@ -318,9 +314,9 @@ private ModelAndView getUserApprovalPageResponse(Map model, // We can grant a token and return it with implicit approval. private ModelAndView getImplicitGrantOrHybridResponse( - AuthorizationRequest authorizationRequest, - Authentication authentication, - String grantType + AuthorizationRequest authorizationRequest, + Authentication authentication, + String grantType ) { OAuth2AccessToken accessToken; try { @@ -331,12 +327,12 @@ private ModelAndView getImplicitGrantOrHybridResponse( throw new UnsupportedResponseTypeException("Unsupported response type: token or id_token"); } return new ModelAndView( - new RedirectView( - buildRedirectURI(authorizationRequest, accessToken, authentication), - false, - true, - false - ) + new RedirectView( + buildRedirectURI(authorizationRequest, accessToken, authentication), + false, + true, + false + ) ); } catch (OAuth2Exception e) { return new ModelAndView(new RedirectView(getUnsuccessfulRedirect(authorizationRequest, e, true), false, @@ -365,13 +361,13 @@ private OAuth2AccessToken getAccessTokenForImplicitGrantOrHybrid(TokenRequest to private View getAuthorizationCodeResponse(AuthorizationRequest authorizationRequest, Authentication authUser) { try { return new RedirectView( - getSuccessfulRedirect( - authorizationRequest, - generateCode(authorizationRequest, authUser) - ), - false, - false, //so that we send absolute URLs always - false + getSuccessfulRedirect( + authorizationRequest, + generateCode(authorizationRequest, authUser) + ), + false, + false, //so that we send absolute URLs always + false ) { @Override protected HttpStatus getHttp11StatusCode(HttpServletRequest request, HttpServletResponse response, String targetUrl) { @@ -402,7 +398,7 @@ public String buildRedirectURI(AuthorizationRequest authorizationRequest, } if (accessToken instanceof CompositeAccessToken && - authorizationRequest.getResponseTypes().contains(CompositeAccessToken.ID_TOKEN)) { + authorizationRequest.getResponseTypes().contains(CompositeAccessToken.ID_TOKEN)) { url.append("&").append(CompositeAccessToken.ID_TOKEN).append("=").append(encode(((CompositeAccessToken) accessToken).getIdTokenValue())); } @@ -455,7 +451,7 @@ public String buildRedirectURI(AuthorizationRequest authorizationRequest, } private String generateCode(AuthorizationRequest authorizationRequest, Authentication authentication) - throws AuthenticationException { + throws AuthenticationException { try { @@ -480,7 +476,7 @@ private String generateCode(AuthorizationRequest authorizationRequest, Authentic private String encode(String value) { try { //return URLEncoder.encode(value,"UTF-8"); - return UriUtils.encodeQueryParam(value,"UTF-8"); + return UriUtils.encodeQueryParam(value, "UTF-8"); } catch (UnsupportedEncodingException x) { throw new IllegalArgumentException(x); } @@ -514,7 +510,7 @@ private String getUnsuccessfulRedirect(AuthorizationRequest authorizationRequest UriComponentsBuilder template = UriComponentsBuilder.fromUriString(authorizationRequest.getRedirectUri()); StringBuilder values = new StringBuilder(); - values.append("error="+encode(failure.getOAuth2ErrorCode())); + values.append("error=" + encode(failure.getOAuth2ErrorCode())); values.append("&error_description=" + encode(failure.getMessage())); if (authorizationRequest.getState() != null) { @@ -575,10 +571,10 @@ public ModelAndView handleOAuth2Exception(OAuth2Exception e, ServletWebRequest w @ExceptionHandler(HttpSessionRequiredException.class) public ModelAndView handleHttpSessionRequiredException(HttpSessionRequiredException e, ServletWebRequest webRequest) - throws Exception { + throws Exception { logger.info("Handling Session required error: " + e.getMessage()); return handleException(new AccessDeniedException("Could not obtain authorization request from session", e), - webRequest); + webRequest); } private ModelAndView handleException(Exception e, ServletWebRequest webRequest) throws Exception { @@ -600,12 +596,12 @@ private ModelAndView handleException(Exception e, ServletWebRequest webRequest) authorizationRequest = getAuthorizationRequestForError(webRequest); String requestedRedirectParam = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI); String requestedRedirect = - redirectResolver.resolveRedirect( - requestedRedirectParam, - getClientServiceExtention().loadClientByClientId(authorizationRequest.getClientId(), IdentityZoneHolder.get().getId())); + redirectResolver.resolveRedirect( + requestedRedirectParam, + getClientServiceExtention().loadClientByClientId(authorizationRequest.getClientId(), IdentityZoneHolder.get().getId())); authorizationRequest.setRedirectUri(requestedRedirect); String redirect = getUnsuccessfulRedirect(authorizationRequest, translate.getBody(), authorizationRequest - .getResponseTypes().contains("token")); + .getResponseTypes().contains("token")); return new ModelAndView(new RedirectView(redirect, false, true, false)); } catch (OAuth2Exception ex) { // If an AuthorizationRequest cannot be created from the incoming parameters it must be @@ -620,7 +616,7 @@ private AuthorizationRequest getAuthorizationRequestForError(ServletWebRequest w // If it's already there then we are in the approveOrDeny phase and we can use the saved request AuthorizationRequest authorizationRequest = (AuthorizationRequest) sessionAttributeStore.retrieveAttribute( - webRequest, "authorizationRequest"); + webRequest, "authorizationRequest"); if (authorizationRequest != null) { return authorizationRequest; } From ef416a25a15635ae171c384dc2a161498c1955bb Mon Sep 17 00:00:00 2001 From: Dennis Leon Date: Wed, 24 Jan 2018 11:27:45 -0800 Subject: [PATCH 17/42] Stop defaulting uaa url to localhost if property is missing [#153568058] https://www.pivotaltracker.com/story/show/153568058 Signed-off-by: Jennifer Hamon --- uaa/src/main/webapp/WEB-INF/spring-servlet.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index 5800dd0161a..19f9df91960 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -313,11 +313,11 @@ - + - + From 54d00a06d73f2aa1bad7575bb22248ebda986bf2 Mon Sep 17 00:00:00 2001 From: Jennifer Hamon Date: Wed, 24 Jan 2018 14:47:48 -0800 Subject: [PATCH 18/42] Only calculate session state for silent authentication [#153568058] https://www.pivotaltracker.com/story/show/153568058 Signed-off-by: Dennis Leon --- .../uaa/oauth/UaaAuthorizationEndpoint.java | 11 +-- ...ationPromptNoneEntryPointMockMvcTests.java | 88 +++++++++++++------ .../identity/uaa/mock/util/MockMvcUtils.java | 46 +++++----- 3 files changed, 89 insertions(+), 56 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java index 5361fefabb7..03359d7f1c5 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java @@ -219,7 +219,6 @@ public ModelAndView authorize(Map model, model.put("original_uri", UrlUtils.buildFullRequestUrl(request)); return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal); } - } catch (RuntimeException e) { sessionStatus.setComplete(); throw e; @@ -432,11 +431,13 @@ public String buildRedirectURI(AuthorizationRequest authorizationRequest, } - HttpHost httpHost = URIUtils.extractHost(URI.create(requestedRedirect)); - String sessionState = openIdSessionStateCalculator.calculate(RequestContextHolder.currentRequestAttributes().getSessionId(), - authorizationRequest.getClientId(), httpHost.toURI()); + if ("none".equals(authorizationRequest.getRequestParameters().get("prompt"))) { + HttpHost httpHost = URIUtils.extractHost(URI.create(requestedRedirect)); + String sessionState = openIdSessionStateCalculator.calculate(RequestContextHolder.currentRequestAttributes().getSessionId(), + authorizationRequest.getClientId(), httpHost.toURI()); - url.append("&session_state=").append(sessionState); + url.append("&session_state=").append(sessionState); + } UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(requestedRedirect); String existingFragment = builder.build(true).getFragment(); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java index 0e2a76ebf01..a05c4d375c3 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java @@ -3,7 +3,7 @@ import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; import org.cloudfoundry.identity.uaa.oauth.OpenIdSessionStateCalculator; -import org.hamcrest.Matchers; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -15,6 +15,8 @@ import java.util.Arrays; import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; @@ -26,48 +28,65 @@ public class AuthorizationPromptNoneEntryPointMockMvcTests extends InjectedMockContextTest { - private static boolean isInitDone = false; + private String adminToken; @Before public void setup() throws Exception { - if(!isInitDone) { - BaseClientDetails client = new BaseClientDetails("ant", "", "openid", "implicit", "", "http://example.com/**"); - client.setAutoApproveScopes(Arrays.asList("openid")); - String adminToken = testClient.getClientCredentialsOAuthAccessToken("admin", "adminsecret", - "clients.read clients.write clients.secret clients.admin uaa.admin"); - MockMvcUtils.createClient(getMockMvc(), adminToken, client); - isInitDone = true; - } + BaseClientDetails client = new BaseClientDetails("ant", "", "openid", "implicit", "", "http://example.com/**"); + client.setAutoApproveScopes(Arrays.asList("openid")); + adminToken = testClient.getClientCredentialsOAuthAccessToken("admin", "adminsecret", "clients.write uaa.admin"); + MockMvcUtils.createClient(getMockMvc(), adminToken, client); + } + + @After + public void cleanup() throws Exception { + MockMvcUtils.deleteClient(getMockMvc(), adminToken, "ant", ""); } @Test public void testSilentAuthHonorsAntRedirect_whenNotAuthenticated() throws Exception { getMockMvc().perform( - get("/oauth/authorize?response_type=token&scope=openid&client_id=ant&prompt=none&redirect_uri=http://example.com/with/path.html") + get("/oauth/authorize?response_type=token&scope=openid&client_id=ant&prompt=none&redirect_uri=http://example.com/with/path.html") ) - .andExpect(redirectedUrl("http://example.com/with/path.html#error=login_required")); + .andExpect(redirectedUrl("http://example.com/with/path.html#error=login_required")); } @Test - public void testSilentAuthHonorsAntRedirect_whenAuthenticated() throws Exception { + public void testSilentAuthHonorsAntRedirect_whenSessionHasBeenInvalidated() throws Exception { MockHttpSession session = new MockHttpSession(); login(session); session.invalidate(); getMockMvc().perform( - get("/oauth/authorize?response_type=token&scope=openid&client_id=ant&prompt=none&redirect_uri=http://example.com/with/path.html") - .session(session) + get("/oauth/authorize?response_type=token&scope=openid&client_id=ant&prompt=none&redirect_uri=http://example.com/with/path.html") + .session(session) ) - .andExpect(redirectedUrlPattern("http://example.com/**/*")); + .andExpect(redirectedUrlPattern("http://example.com/**/*")); } @Test - public void testSessionStateIsCorrect() throws Exception { + public void testSilentAuthentication_whenScopesNotAutoapproved() throws Exception { + MockMvcUtils.deleteClient(getMockMvc(), adminToken, "ant", ""); + BaseClientDetails client = new BaseClientDetails("ant", "", "openid", "implicit", "", "http://example.com/**"); + MockMvcUtils.createClient(getMockMvc(), adminToken, client); + + MockHttpSession session = new MockHttpSession(); + login(session); + + getMockMvc().perform( + get("/oauth/authorize?response_type=token&scope=openid&client_id=ant&prompt=none&redirect_uri=http://example.com/with/path.html") + .session(session) + ) + .andExpect(redirectedUrl("http://example.com/with/path.html#error=interaction_required")); + } + + @Test + public void testSilentAuthentication_testSessionStateIsCorrect() throws Exception { SecureRandom secureRandom = mock(SecureRandom.class); doNothing().when(secureRandom).nextBytes(any()); OpenIdSessionStateCalculator sessionStateCalculator - = (OpenIdSessionStateCalculator)getWebApplicationContext().getBean("openIdSessionStateCalculator"); + = (OpenIdSessionStateCalculator) getWebApplicationContext().getBean("openIdSessionStateCalculator"); sessionStateCalculator.setSecureRandom(secureRandom); //we need to know session id when we are calculating session_state @@ -79,23 +98,36 @@ public String changeSessionId() { login(session); MvcResult result = getMockMvc().perform( - get("/oauth/authorize?response_type=token&scope=openid&client_id=ant&prompt=none&redirect_uri=http://example.com/with/path.html") - .session(session) + get("/oauth/authorize?response_type=token&scope=openid&client_id=ant&prompt=none&redirect_uri=http://example.com/with/path.html") + .session(session) ) - .andExpect(status().isFound()) - .andReturn(); + .andExpect(status().isFound()) + .andReturn(); String redirectUrl = result.getResponse().getRedirectedUrl(); - Assert.assertThat(redirectUrl, Matchers.containsString("session_state=707c310bc5aa38acc03d48a099fc999cd77f44df163178df1ca35863913f5711.0000000000000000000000000000000000000000000000000000000000000000")); + Assert.assertThat(redirectUrl, containsString("session_state=707c310bc5aa38acc03d48a099fc999cd77f44df163178df1ca35863913f5711.0000000000000000000000000000000000000000000000000000000000000000")); + } + + @Test + public void nonSilentAuthentication_doesNotComputeSessionState() throws Exception { + MockHttpSession session = new MockHttpSession(); + login(session); + + MvcResult result = getMockMvc().perform( + get("/oauth/authorize?response_type=token&scope=openid&client_id=ant&redirect_uri=http://example.com/with/path.html") + .session(session) + ) + .andReturn(); + Assert.assertThat(result.getResponse().getRedirectedUrl(), not(containsString("session_state"))); } private void login(MockHttpSession session) throws Exception { getMockMvc().perform( - post("/login.do") - .with(cookieCsrf()) - .param("username", "marissa") - .param("password", "koala") - .session(session) + post("/login.do") + .with(cookieCsrf()) + .param("username", "marissa") + .param("password", "koala") + .session(session) ).andExpect(redirectedUrl("/")); } } \ No newline at end of file diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java index 0d91ff59744..b5864b05616 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java @@ -14,23 +14,11 @@ package org.cloudfoundry.identity.uaa.mock.util; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import java.io.File; -import java.net.URL; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - +import com.fasterxml.jackson.core.type.TypeReference; +import com.warrenstrange.googleauth.GoogleAuthenticator; +import com.warrenstrange.googleauth.GoogleAuthenticatorConfig; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.RandomStringUtils; import org.cloudfoundry.identity.uaa.audit.event.AbstractUaaEvent; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; @@ -74,12 +62,6 @@ import org.cloudfoundry.identity.uaa.zone.Links; import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture; import org.cloudfoundry.identity.uaa.zone.MultitenantJdbcClientDetailsService; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.warrenstrange.googleauth.GoogleAuthenticator; -import com.warrenstrange.googleauth.GoogleAuthenticatorConfig; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.lang.RandomStringUtils; import org.junit.Assert; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -118,11 +100,29 @@ import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponentsBuilder; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.File; +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import static java.util.Arrays.asList; import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; import static org.cloudfoundry.identity.uaa.scim.ScimGroupMember.Type.USER; import static org.cloudfoundry.identity.uaa.web.UaaSavedRequestAwareAuthenticationSuccessHandler.SAVED_REQUEST_SESSION_ATTRIBUTE; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; From 64e0cd6f7477eb7d7451d477ea3e74c3bc0ffe68 Mon Sep 17 00:00:00 2001 From: Dennis Leon Date: Wed, 24 Jan 2018 14:52:07 -0800 Subject: [PATCH 19/42] Reformat MockMvcUtils Signed-off-by: Jennifer Hamon --- .../identity/uaa/mock/util/MockMvcUtils.java | 724 ++++++++++-------- 1 file changed, 385 insertions(+), 339 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java index b5864b05616..5db08320405 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java @@ -141,10 +141,11 @@ public final class MockMvcUtils { - private MockMvcUtils() {} + private MockMvcUtils() { + } public static final String IDP_META_DATA = - "\n" + + "\n" + "\n" + " \n" + " \n" + @@ -180,7 +181,7 @@ private MockMvcUtils() {} public static T getEventOfType(ArgumentCaptor captor, Class type) { for (AbstractUaaEvent event : captor.getAllValues()) { if (event.getClass().equals(type)) { - return (T)event; + return (T) event; } } return null; @@ -192,13 +193,13 @@ public static String performMfaPostVerifyWithCode(int code, MockMvc mvc, MockHtt public static String performMfaPostVerifyWithCode(int code, MockMvc mvc, MockHttpSession session, String host) throws Exception { return mvc.perform(post("/login/mfa/verify.do") - .param("code", Integer.toString(code)) - .header("Host", host) - .session(session) - .with(cookieCsrf())) - .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/login/mfa/completed")) - .andReturn().getResponse().getRedirectedUrl(); + .param("code", Integer.toString(code)) + .header("Host", host) + .session(session) + .with(cookieCsrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/login/mfa/completed")) + .andReturn().getResponse().getRedirectedUrl(); } public static int getMFACodeFromSession(MockHttpSession session) { @@ -216,55 +217,55 @@ public static ResultActions performMfaRegistrationInZone(String username, String //ldap login MockHttpSession session = (MockHttpSession) mockMvc.perform( - post("/login.do") - .with(cookieCsrf()) - .header(HOST, host) - .accept(MediaType.TEXT_HTML) - .param("username", username) - .param("password", password) + post("/login.do") + .with(cookieCsrf()) + .header(HOST, host) + .accept(MediaType.TEXT_HTML) + .param("username", username) + .param("password", password) ) - .andExpect(status().isFound()) - .andExpect(redirectedUrl("/")) - .andReturn().getRequest().getSession(false); + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/")) + .andReturn().getRequest().getSession(false); assertTrue(getUaaAuthentication(session).isAuthenticated()); assertThat(getUaaAuthentication(session).getAuthenticationMethods(), containsInAnyOrder(firstAuthMethods)); //successful login, follow redirect mockMvc.perform( - get("/") - .header(HOST, host) - .accept(MediaType.TEXT_HTML) - .session(session) + get("/") + .header(HOST, host) + .accept(MediaType.TEXT_HTML) + .session(session) ) - .andExpect(status().isFound()) - .andExpect(redirectedUrl("/login/mfa/register")); + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/login/mfa/register")); //follow redirect to mfa register mockMvc.perform( - get("/login/mfa/register") - .header(HOST, host) - .accept(MediaType.TEXT_HTML) - .session(session) + get("/login/mfa/register") + .header(HOST, host) + .accept(MediaType.TEXT_HTML) + .session(session) ) - .andExpect(status().isOk()) - .andExpect(view().name("mfa/qr_code")); + .andExpect(status().isOk()) + .andExpect(view().name("mfa/qr_code")); //post MFA code int code = MockMvcUtils.getMFACodeFromSession(session); String location = MockMvcUtils.performMfaPostVerifyWithCode(code, mockMvc, session, host); //follow redirect to completed location = mockMvc.perform(get(location) - .session(session) - .header(HOST, host) + .session(session) + .header(HOST, host) ) - .andExpect(status().isFound()) - .andExpect(redirectedUrl("http://"+ host +"/")) - .andReturn().getResponse().getRedirectedUrl(); + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://" + host + "/")) + .andReturn().getResponse().getRedirectedUrl(); ResultActions resultActions = mockMvc.perform(get(location) - .session(session) - .header(HOST, host) + .session(session) + .header(HOST, host) ); assertTrue(getUaaAuthentication(session).isAuthenticated()); @@ -280,16 +281,16 @@ public static MfaProvider createMfaProvider(MockMvc mockMvc, String zoneId, Stri provider.setIdentityZoneId(zoneId); provider.setConfig(new GoogleMfaProviderConfig()); MockHttpServletRequestBuilder post = post("/mfa-providers") - .header("Authorization", "Bearer " + adminToken) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(provider)); + .header("Authorization", "Bearer " + adminToken) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(provider)); if (!IdentityZone.getUaa().getId().equalsIgnoreCase(zoneId)) { post.header(IdentityZoneSwitchingFilter.HEADER, zoneId); } return JsonUtils.readValue(mockMvc.perform(post) - .andExpect(status().isCreated()) - .andReturn() - .getResponse().getContentAsByteArray(), MfaProvider.class); + .andExpect(status().isCreated()) + .andReturn() + .getResponse().getContentAsByteArray(), MfaProvider.class); } @@ -315,20 +316,20 @@ public static void resetLimitedModeStatusFile(ApplicationContext context, File f public static String getSPMetadata(MockMvc mockMvc, String subdomain) throws Exception { return mockMvc.perform( - get("/saml/metadata") - .accept(MediaType.APPLICATION_XML) - .header(HOST, hasText(subdomain) ? subdomain + ".localhost" : "localhost") + get("/saml/metadata") + .accept(MediaType.APPLICATION_XML) + .header(HOST, hasText(subdomain) ? subdomain + ".localhost" : "localhost") ).andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(); + .andReturn().getResponse().getContentAsString(); } public static String getIDPMetaData(MockMvc mockMvc, String subdomain) throws Exception { return mockMvc.perform( - get("/saml/idp/metadata") - .accept(MediaType.APPLICATION_XML) - .header(HOST, hasText(subdomain) ? subdomain + ".localhost" : "localhost") + get("/saml/idp/metadata") + .accept(MediaType.APPLICATION_XML) + .header(HOST, hasText(subdomain) ? subdomain + ".localhost" : "localhost") ).andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(); + .andReturn().getResponse().getContentAsString(); } public static MockHttpSession getSavedRequestSession() { @@ -348,20 +349,44 @@ public MockSavedRequest() { public String getRedirectUrl() { return "http://test/redirect/oauth/authorize"; } + @Override public String[] getParameterValues(String name) { if ("client_id".equals(name)) { - return new String[] {"admin"}; + return new String[]{"admin"}; } return new String[0]; } - @Override public List getCookies() { return null; } - @Override public String getMethod() { return null; } - @Override public List getHeaderValues(String name) { return null; } + + @Override + public List getCookies() { + return null; + } + + @Override + public String getMethod() { + return null; + } + @Override - public Collection getHeaderNames() { return null; } - @Override public List getLocales() { return null; } - @Override public Map getParameterMap() { return null; } + public List getHeaderValues(String name) { + return null; + } + + @Override + public Collection getHeaderNames() { + return null; + } + + @Override + public List getLocales() { + return null; + } + + @Override + public Map getParameterMap() { + return null; + } } @@ -424,7 +449,7 @@ public static void setDisableInternalUserManagement(ApplicationContext context, provisioning.update(uaaIdp, zoneId); } - public static void setSelfServiceLinksEnabled(ApplicationContext context, String zoneId,boolean enabled) { + public static void setSelfServiceLinksEnabled(ApplicationContext context, String zoneId, boolean enabled) { IdentityZoneConfiguration config = getZoneConfiguration(context, zoneId); config.getLinks().getSelfService().setSelfServiceLinksEnabled(enabled); setZoneConfiguration(context, zoneId, config); @@ -473,25 +498,25 @@ public static InvitationsResponse sendRequestWithTokenAndReturnResponse(Applicat String subdomain, String clientId, String redirectUri, - String...emails) throws Exception { + String... emails) throws Exception { InvitationsRequest invitations = new InvitationsRequest(emails); String requestBody = JsonUtils.writeValueAsString(invitations); MockHttpServletRequestBuilder post = post("/invite_users") - .param(OAuth2Utils.CLIENT_ID, clientId) - .param(OAuth2Utils.REDIRECT_URI, redirectUri) - .header("Authorization", "Bearer " + token) - .contentType(APPLICATION_JSON) - .content(requestBody); + .param(OAuth2Utils.CLIENT_ID, clientId) + .param(OAuth2Utils.REDIRECT_URI, redirectUri) + .header("Authorization", "Bearer " + token) + .contentType(APPLICATION_JSON) + .content(requestBody); if (hasText(subdomain)) { - post.header("Host",(subdomain+".localhost")); + post.header("Host", (subdomain + ".localhost")); } MvcResult result = mockMvc.perform( - post + post ) - .andExpect(status().isOk()) - .andReturn(); + .andExpect(status().isOk()) + .andReturn(); return JsonUtils.readValue(result.getResponse().getContentAsString(), InvitationsResponse.class); } @@ -517,10 +542,10 @@ public static IdentityProvider createIdentityProvider(MockMvc mockMvc, IdentityZ provider.setType(OriginKeys.UAA); } provider = utils().createIdpUsingWebRequest(mockMvc, - zone.getIdentityZone().getId(), - zone.getZoneAdminToken(), - provider, - status().isCreated()); + zone.getIdentityZone().getId(), + zone.getZoneAdminToken(), + provider, + status().isCreated()); return provider; } @@ -528,21 +553,21 @@ public static ZoneScimInviteData createZoneForInvites(MockMvc mockMvc, Applicati RandomValueStringGenerator generator = new RandomValueStringGenerator(); String superAdmin = getClientCredentialsOAuthAccessToken(mockMvc, "admin", "adminsecret", "", null); IdentityZoneCreationResult zone = utils().createOtherIdentityZoneAndReturnResult(generator.generate().toLowerCase(), mockMvc, context, null); - BaseClientDetails appClient = new BaseClientDetails("app","","scim.invite", "client_credentials,password,authorization_code","uaa.admin,clients.admin,scim.write,scim.read,scim.invite", redirectUri); + BaseClientDetails appClient = new BaseClientDetails("app", "", "scim.invite", "client_credentials,password,authorization_code", "uaa.admin,clients.admin,scim.write,scim.read,scim.invite", redirectUri); appClient.setClientSecret("secret"); appClient = utils().createClient(mockMvc, zone.getZoneAdminToken(), appClient, zone.getIdentityZone(), - status().isCreated()); + status().isCreated()); appClient.setClientSecret("secret"); String adminToken = utils().getClientCredentialsOAuthAccessToken( - mockMvc, - appClient.getClientId(), - appClient.getClientSecret(), - "", - zone.getIdentityZone().getSubdomain() + mockMvc, + appClient.getClientId(), + appClient.getClientSecret(), + "", + zone.getIdentityZone().getSubdomain() ); - String username = new RandomValueStringGenerator().generate().toLowerCase()+"@example.com"; + String username = new RandomValueStringGenerator().generate().toLowerCase() + "@example.com"; ScimUser user = new ScimUser(clientId, username, "given-name", "family-name"); user.setPrimaryEmail(username); user.setPassword("password"); @@ -553,10 +578,10 @@ public static ZoneScimInviteData createZoneForInvites(MockMvc mockMvc, Applicati group.setMembers(Arrays.asList(new ScimGroupMember(user.getId(), USER))); return new ZoneScimInviteData( - adminToken, - zone, - appClient, - superAdmin + adminToken, + zone, + appClient, + superAdmin ); } @@ -577,10 +602,10 @@ public static IdentityZone createZoneUsingWebRequest(MockMvc mockMvc, String acc IdentityZone identityZone = MultitenancyFixture.identityZone(zoneId, zoneId); MvcResult result = mockMvc.perform(post("/identity-zones") - .header("Authorization", "Bearer " + accessToken) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(identityZone))) - .andExpect(status().isCreated()).andReturn(); + .header("Authorization", "Bearer " + accessToken) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(identityZone))) + .andExpect(status().isCreated()).andReturn(); return JsonUtils.readValue(result.getResponse().getContentAsString(), IdentityZone.class); } @@ -610,26 +635,27 @@ public String getZoneAdminToken() { public static IdentityZoneCreationResult createOtherIdentityZoneAndReturnResult(MockMvc mockMvc, ApplicationContext webApplicationContext, ClientDetails bootstrapClient, IdentityZone identityZone) throws Exception { return createOtherIdentityZoneAndReturnResult(mockMvc, - webApplicationContext, - bootstrapClient, - identityZone, - true); + webApplicationContext, + bootstrapClient, + identityZone, + true); } + public static IdentityZoneCreationResult createOtherIdentityZoneAndReturnResult(MockMvc mockMvc, ApplicationContext webApplicationContext, ClientDetails bootstrapClient, IdentityZone identityZone, boolean useWebRequests) throws Exception { String identityToken = getClientCredentialsOAuthAccessToken(mockMvc, "identity", "identitysecret", - "zones.write,scim.zones", null); + "zones.write,scim.zones", null); if (useWebRequests) { mockMvc.perform(post("/identity-zones") - .header("Authorization", "Bearer " + identityToken) - .contentType(APPLICATION_JSON) - .accept(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(identityZone))) - .andExpect(status().isCreated()); + .header("Authorization", "Bearer " + identityToken) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(identityZone))) + .andExpect(status().isCreated()); } else { webApplicationContext.getBean(IdentityZoneProvisioning.class).create(identityZone); IdentityProvider defaultIdp = new IdentityProvider(); @@ -652,32 +678,32 @@ public static IdentityZoneCreationResult createOtherIdentityZoneAndReturnResult( group.setMembers(Collections.singletonList(new ScimGroupMember(marissa.getId()))); if (useWebRequests) { mockMvc.perform(post("/Groups/zones") - .header("Authorization", "Bearer " + identityToken) - .contentType(APPLICATION_JSON) - .accept(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(group))) - .andExpect(status().isCreated()); + .header("Authorization", "Bearer " + identityToken) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(group))) + .andExpect(status().isCreated()); } else { webApplicationContext.getBean(ScimGroupEndpoints.class).addZoneManagers(group, Mockito.mock(HttpServletResponse.class)); } // use that user to create an admin client in the new zone String zoneAdminAuthcodeToken = getUserOAuthAccessTokenAuthCode(mockMvc, "identity", "identitysecret", - marissa.getId(), "marissa", "koala", zoneAdminScope); + marissa.getId(), "marissa", "koala", zoneAdminScope); - if (bootstrapClient!=null) { + if (bootstrapClient != null) { if (useWebRequests) { mockMvc.perform(post("/oauth/clients") - .header("Authorization", "Bearer " + zoneAdminAuthcodeToken) - .header("X-Identity-Zone-Id", identityZone.getId()) - .contentType(APPLICATION_JSON) - .accept(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(bootstrapClient))) - .andExpect(status().isCreated()); + .header("Authorization", "Bearer " + zoneAdminAuthcodeToken) + .header("X-Identity-Zone-Id", identityZone.getId()) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(bootstrapClient))) + .andExpect(status().isCreated()); } else { webApplicationContext.getBean(MultitenantJdbcClientDetailsService.class).addClientDetails( - bootstrapClient, - identityZone.getId() + bootstrapClient, + identityZone.getId() ); } } @@ -695,7 +721,7 @@ public static IdentityZoneCreationResult createOtherIdentityZoneAndReturnResult( } public static IdentityZoneCreationResult createOtherIdentityZoneAndReturnResult(String subdomain, MockMvc mockMvc, - ApplicationContext webApplicationContext, ClientDetails bootstrapClient) throws Exception { + ApplicationContext webApplicationContext, ClientDetails bootstrapClient) throws Exception { return createOtherIdentityZoneAndReturnResult(subdomain, mockMvc, webApplicationContext, bootstrapClient, true); } @@ -704,8 +730,9 @@ public static IdentityZone createOtherIdentityZone(String subdomain, MockMvc moc ApplicationContext webApplicationContext, ClientDetails bootstrapClient) throws Exception { return createOtherIdentityZone(subdomain, mockMvc, webApplicationContext, bootstrapClient, true); } + public static IdentityZone createOtherIdentityZone(String subdomain, MockMvc mockMvc, - ApplicationContext webApplicationContext, ClientDetails bootstrapClient, boolean useWebRequests) throws Exception { + ApplicationContext webApplicationContext, ClientDetails bootstrapClient, boolean useWebRequests) throws Exception { return createOtherIdentityZoneAndReturnResult(subdomain, mockMvc, webApplicationContext, bootstrapClient, useWebRequests).getIdentityZone(); } @@ -714,11 +741,12 @@ public static IdentityZone createOtherIdentityZone(String subdomain, MockMvc moc ApplicationContext webApplicationContext) throws Exception { return createOtherIdentityZone(subdomain, mockMvc, webApplicationContext, true); } + public static IdentityZone createOtherIdentityZone(String subdomain, MockMvc mockMvc, - ApplicationContext webApplicationContext, boolean useWebRequests) throws Exception { + ApplicationContext webApplicationContext, boolean useWebRequests) throws Exception { BaseClientDetails client = new BaseClientDetails("admin", null, null, "client_credentials", - "clients.admin,scim.read,scim.write,idps.write,uaa.admin", "http://redirect.url"); + "clients.admin,scim.read,scim.write,idps.write,uaa.admin", "http://redirect.url"); client.setClientSecret("admin-secret"); return createOtherIdentityZone(subdomain, mockMvc, webApplicationContext, client, useWebRequests); @@ -730,40 +758,41 @@ public static IdentityZone updateIdentityZone(IdentityZone zone, ApplicationCont public static void deleteIdentityZone(String zoneId, MockMvc mockMvc) throws Exception { String identityToken = getClientCredentialsOAuthAccessToken(mockMvc, "identity", "identitysecret", - "zones.write,scim.zones", null); + "zones.write,scim.zones", null); mockMvc.perform(delete("/identity-zones/" + zoneId) - .header("Authorization", "Bearer " + identityToken) - .contentType(APPLICATION_JSON) - .accept(APPLICATION_JSON)) - .andExpect(status().isOk()); + .header("Authorization", "Bearer " + identityToken) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON)) + .andExpect(status().isOk()); } public static IdentityProvider createIdpUsingWebRequest(MockMvc mockMvc, String zoneId, String token, - IdentityProvider identityProvider, ResultMatcher resultMatcher) throws Exception { + IdentityProvider identityProvider, ResultMatcher resultMatcher) throws Exception { return createIdpUsingWebRequest(mockMvc, zoneId, token, identityProvider, resultMatcher, false); } + public static IdentityProvider createIdpUsingWebRequest(MockMvc mockMvc, String zoneId, String token, - IdentityProvider identityProvider, ResultMatcher resultMatcher, boolean update) throws Exception { + IdentityProvider identityProvider, ResultMatcher resultMatcher, boolean update) throws Exception { MockHttpServletRequestBuilder requestBuilder = - update ? - put("/identity-providers/"+identityProvider.getId()) - .header("Authorization", "Bearer " + token) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(identityProvider)) + update ? + put("/identity-providers/" + identityProvider.getId()) + .header("Authorization", "Bearer " + token) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(identityProvider)) : - post("/identity-providers/") - .header("Authorization", "Bearer " + token) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(identityProvider)); + post("/identity-providers/") + .header("Authorization", "Bearer " + token) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(identityProvider)); if (zoneId != null) { requestBuilder.header(IdentityZoneSwitchingFilter.HEADER, zoneId); } MvcResult result = mockMvc.perform(requestBuilder) - .andExpect(resultMatcher) - .andReturn(); + .andExpect(resultMatcher) + .andReturn(); if (hasText(result.getResponse().getContentAsString())) { try { return JsonUtils.readValue(result.getResponse().getContentAsString(), IdentityProvider.class); @@ -779,35 +808,36 @@ public static ScimUser createUser(MockMvc mockMvc, String accessToken, ScimUser return createUserInZone(mockMvc, accessToken, user, ""); } - public static ScimUser createUserInZone(MockMvc mockMvc, String accessToken, ScimUser user, String subdomain) throws Exception { + public static ScimUser createUserInZone(MockMvc mockMvc, String accessToken, ScimUser user, String subdomain) throws Exception { return createUserInZone(mockMvc, accessToken, user, subdomain, null); } - public static ScimUser createUserInZone(MockMvc mockMvc, String accessToken, ScimUser user, String subdomain, String zoneId) throws Exception { + + public static ScimUser createUserInZone(MockMvc mockMvc, String accessToken, ScimUser user, String subdomain, String zoneId) throws Exception { String requestDomain = subdomain.equals("") ? "localhost" : subdomain + ".localhost"; MockHttpServletRequestBuilder post = post("/Users"); post.header("Authorization", "Bearer " + accessToken) - .with(new SetServerNameRequestPostProcessor(requestDomain)) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsBytes(user)); + .with(new SetServerNameRequestPostProcessor(requestDomain)) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsBytes(user)); if (hasText(zoneId)) { post.header(IdentityZoneSwitchingFilter.HEADER, zoneId); } MvcResult userResult = mockMvc.perform(post) - .andExpect(status().isCreated()).andReturn(); + .andExpect(status().isCreated()).andReturn(); return JsonUtils.readValue(userResult.getResponse().getContentAsString(), ScimUser.class); } - public static ScimUser readUserInZone(MockMvc mockMvc, String accessToken, String userId, String subdomain, String zoneId) throws Exception { + public static ScimUser readUserInZone(MockMvc mockMvc, String accessToken, String userId, String subdomain, String zoneId) throws Exception { String requestDomain = subdomain.equals("") ? "localhost" : subdomain + ".localhost"; - MockHttpServletRequestBuilder get = get("/Users/"+userId); + MockHttpServletRequestBuilder get = get("/Users/" + userId); get.header("Authorization", "Bearer " + accessToken) - .with(new SetServerNameRequestPostProcessor(requestDomain)) - .accept(APPLICATION_JSON); + .with(new SetServerNameRequestPostProcessor(requestDomain)) + .accept(APPLICATION_JSON); if (hasText(zoneId)) { get.header(IdentityZoneSwitchingFilter.HEADER, zoneId); } MvcResult userResult = mockMvc.perform(get) - .andExpect(status().isOk()).andReturn(); + .andExpect(status().isOk()).andReturn(); return JsonUtils.readValue(userResult.getResponse().getContentAsString(), ScimUser.class); } @@ -823,7 +853,7 @@ public static ScimUser createAdminForZone(MockMvc mockMvc, String accessToken, S for (String scope : StringUtils.commaDelimitedListToSet(scopes)) { ScimGroup group = getGroup(mockMvc, accessToken, scope); - if (group==null) { + if (group == null) { group = new ScimGroup(null, scope, IdentityZoneHolder.get().getId()); group.setMembers(Arrays.asList(new ScimGroupMember(createdUser.getId()))); createGroup(mockMvc, accessToken, group); @@ -840,20 +870,22 @@ public static ScimUser createAdminForZone(MockMvc mockMvc, String accessToken, S public static ScimGroup getGroup(MockMvc mockMvc, String accessToken, String displayName) throws Exception { return getGroup(mockMvc, accessToken, displayName, null); } + public static ScimGroup getGroup(MockMvc mockMvc, String accessToken, String displayName, String subdomain) throws Exception { - String filter = "displayName eq \""+displayName+"\""; + String filter = "displayName eq \"" + displayName + "\""; MockHttpServletRequestBuilder builder = get("/Groups"); if (hasText(subdomain)) { - builder.header("Host", subdomain+".localhost"); + builder.header("Host", subdomain + ".localhost"); } SearchResults results = JsonUtils.readValue( - mockMvc.perform(builder - .header("Authorization", "Bearer " + accessToken) - .contentType(APPLICATION_JSON) - .param("filter", filter)) - .andReturn().getResponse().getContentAsString(), - new TypeReference>() {}); - if (results==null || results.getResources()==null || results.getResources().isEmpty()) { + mockMvc.perform(builder + .header("Authorization", "Bearer " + accessToken) + .contentType(APPLICATION_JSON) + .param("filter", filter)) + .andReturn().getResponse().getContentAsString(), + new TypeReference>() { + }); + if (results == null || results.getResources() == null || results.getResources().isEmpty()) { return null; } else { return results.getResources().iterator().next(); @@ -866,76 +898,88 @@ public static ScimGroup createGroup(MockMvc mockMvc, String accessToken, ScimGro public static ScimGroup createGroup(MockMvc mockMvc, String accessToken, String subdomain, ScimGroup group) throws Exception { MockHttpServletRequestBuilder post = post("/Groups") - .header("Authorization", "Bearer " + accessToken) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(group)); + .header("Authorization", "Bearer " + accessToken) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(group)); if (hasText(subdomain)) { - post.header("Host", subdomain+".localhost"); + post.header("Host", subdomain + ".localhost"); } return JsonUtils.readValue( - mockMvc.perform(post) - .andExpect(status().isCreated()) - .andReturn().getResponse().getContentAsString(), - ScimGroup.class); + mockMvc.perform(post) + .andExpect(status().isCreated()) + .andReturn().getResponse().getContentAsString(), + ScimGroup.class); } public static ScimGroup createGroup(MockMvc mockMvc, String accessToken, ScimGroup group, String zoneId) throws Exception { MockHttpServletRequestBuilder post = post("/Groups") - .header("Authorization", "Bearer " + accessToken) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(group)); + .header("Authorization", "Bearer " + accessToken) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(group)); if (hasText(zoneId)) { post.header(IdentityZoneSwitchingFilter.HEADER, zoneId); } return JsonUtils.readValue( - mockMvc.perform(post) - .andExpect(status().isCreated()) - .andReturn().getResponse().getContentAsString(), - ScimGroup.class); + mockMvc.perform(post) + .andExpect(status().isCreated()) + .andReturn().getResponse().getContentAsString(), + ScimGroup.class); } public static ScimGroup updateGroup(MockMvc mockMvc, String accessToken, ScimGroup group) throws Exception { return updateGroup(mockMvc, accessToken, group, null); } + public static ScimGroup updateGroup(MockMvc mockMvc, String accessToken, ScimGroup group, IdentityZone zone) throws Exception { MockHttpServletRequestBuilder put = put("/Groups/" + group.getId()); - if (zone!=null) { - put.header("Host", zone.getSubdomain()+".localhost"); + if (zone != null) { + put.header("Host", zone.getSubdomain() + ".localhost"); } return JsonUtils.readValue( - mockMvc.perform(put.header("If-Match", group.getVersion()) - .header("Authorization", "Bearer " + accessToken) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(group))) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(), - ScimGroup.class); + mockMvc.perform(put.header("If-Match", group.getVersion()) + .header("Authorization", "Bearer " + accessToken) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(group))) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), + ScimGroup.class); } public static BaseClientDetails createClient(MockMvc mockMvc, String accessToken, BaseClientDetails clientDetails) throws Exception { return createClient(mockMvc, accessToken, clientDetails, IdentityZone.getUaa(), status().isCreated()); } + public static void deleteClient(MockMvc mockMvc, String accessToken, String clientId, String zoneSubdomain) throws Exception { + MockHttpServletRequestBuilder createClientDelete = delete("/oauth/clients/" + clientId) + .header("Authorization", "Bearer " + accessToken) + .accept(APPLICATION_JSON); + if (!zoneSubdomain.equals(IdentityZone.getUaa())) { + createClientDelete = createClientDelete.header(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER, zoneSubdomain); + } + mockMvc.perform(createClientDelete) + .andExpect(status().is(not(500))); + } + public static BaseClientDetails createClient(MockMvc mockMvc, String accessToken, BaseClientDetails clientDetails, IdentityZone zone, ResultMatcher status) - throws Exception { + throws Exception { MockHttpServletRequestBuilder createClientPost = post("/oauth/clients") - .header("Authorization", "Bearer " + accessToken) - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(clientDetails)); - if (! zone.equals(IdentityZone.getUaa())) { + .header("Authorization", "Bearer " + accessToken) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(clientDetails)); + if (!zone.equals(IdentityZone.getUaa())) { createClientPost = createClientPost.header(IdentityZoneSwitchingFilter.HEADER, zone.getId()); } return JsonUtils.readValue( - mockMvc.perform(createClientPost) - .andExpect(status) - .andReturn().getResponse().getContentAsString(), BaseClientDetails.class); + mockMvc.perform(createClientPost) + .andExpect(status) + .andReturn().getResponse().getContentAsString(), BaseClientDetails.class); } public static BaseClientDetails createClient(ApplicationContext context, BaseClientDetails clientDetails, IdentityZone zone) - throws Exception { + throws Exception { MultitenantJdbcClientDetailsService service = context.getBean(MultitenantJdbcClientDetailsService.class); service.addClientDetails(clientDetails, zone.getId()); @@ -944,19 +988,19 @@ public static BaseClientDetails createClient(ApplicationContext context, BaseCli public static ClientDetails createClient(MockMvc mockMvc, String adminAccessToken, String id, String secret, Collection resourceIds, List scopes, List grantTypes, String authorities) throws Exception { return createClient(mockMvc, adminAccessToken, - id, - secret, - resourceIds, - scopes, - grantTypes, - authorities, - Collections.singleton("http://redirect.url"), - IdentityZone.getUaa()); + id, + secret, + resourceIds, + scopes, + grantTypes, + authorities, + Collections.singleton("http://redirect.url"), + IdentityZone.getUaa()); } public static ClientDetails createClient(MockMvc mockMvc, String adminAccessToken, String id, String secret, Collection resourceIds, Collection scopes, Collection grantTypes, String authorities, Set redirectUris, IdentityZone zone) throws Exception { ClientDetailsModification client = getClientDetailsModification(id, secret, resourceIds, scopes, grantTypes, authorities, redirectUris); - return createClient(mockMvc,adminAccessToken, client, zone, status().isCreated()); + return createClient(mockMvc, adminAccessToken, client, zone, status().isCreated()); } public static ClientDetailsModification getClientDetailsModification(String id, String secret, Collection resourceIds, Collection scopes, Collection grantTypes, String authorities, Set redirectUris) { @@ -973,45 +1017,45 @@ public static ClientDetailsModification getClientDetailsModification(String id, } public static BaseClientDetails updateClient(ApplicationContext context, BaseClientDetails clientDetails, IdentityZone zone) - throws Exception { + throws Exception { MultitenantJdbcClientDetailsService service = context.getBean(MultitenantJdbcClientDetailsService.class); service.updateClientDetails(clientDetails, zone.getId()); - return (BaseClientDetails)service.loadClientByClientId(clientDetails.getClientId(), zone.getId()); + return (BaseClientDetails) service.loadClientByClientId(clientDetails.getClientId(), zone.getId()); } public static BaseClientDetails updateClient(MockMvc mockMvc, String accessToken, BaseClientDetails clientDetails, IdentityZone zone) - throws Exception { + throws Exception { MockHttpServletRequestBuilder updateClientPut = - put("/oauth/clients/" + clientDetails.getClientId()) - .header("Authorization", "Bearer " + accessToken) - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(clientDetails)); - if (! zone.equals(IdentityZone.getUaa())) { + put("/oauth/clients/" + clientDetails.getClientId()) + .header("Authorization", "Bearer " + accessToken) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(clientDetails)); + if (!zone.equals(IdentityZone.getUaa())) { updateClientPut = updateClientPut.header(IdentityZoneSwitchingFilter.HEADER, zone.getId()); } return JsonUtils.readValue( - mockMvc.perform(updateClientPut) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(), BaseClientDetails.class); + mockMvc.perform(updateClientPut) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), BaseClientDetails.class); } public static BaseClientDetails getClient(MockMvc mockMvc, String accessToken, String clientId, IdentityZone zone) - throws Exception { + throws Exception { MockHttpServletRequestBuilder readClientGet = - get("/oauth/clients/" + clientId) - .header("Authorization", "Bearer " + accessToken) - .accept(APPLICATION_JSON) - .contentType(APPLICATION_JSON); - if (! zone.equals(IdentityZone.getUaa())) { + get("/oauth/clients/" + clientId) + .header("Authorization", "Bearer " + accessToken) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON); + if (!zone.equals(IdentityZone.getUaa())) { readClientGet = readClientGet.header(IdentityZoneSwitchingFilter.HEADER, zone.getId()); } return JsonUtils.readValue( - mockMvc.perform(readClientGet) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(), BaseClientDetails.class); + mockMvc.perform(readClientGet) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), BaseClientDetails.class); } public static String getZoneAdminToken(MockMvc mockMvc, String adminToken, String zoneId) throws Exception { @@ -1029,62 +1073,62 @@ public static String getZoneAdminToken(MockMvc mockMvc, String adminToken, Strin group.setMembers(Arrays.asList(new ScimGroupMember(user.getId()))); MockMvcUtils.utils().createGroup(mockMvc, adminToken, group); return getUserOAuthAccessTokenAuthCode(mockMvc, - "identity", - "identitysecret", - user.getId(), - user.getUserName(), - "secr3T", - group.getDisplayName() + "identity", + "identitysecret", + user.getId(), + user.getUserName(), + "secr3T", + group.getDisplayName() ); } public static String getUserOAuthAccessToken(MockMvc mockMvc, - String clientId, - String clientSecret, - String username, - String password, - String scope) throws Exception { + String clientId, + String clientSecret, + String username, + String password, + String scope) throws Exception { return getUserOAuthAccessToken(mockMvc, clientId, clientSecret, username, password, scope, null); } public static String getUserOAuthAccessToken(MockMvc mockMvc, - String clientId, - String clientSecret, - String username, - String password, - String scope, - IdentityZone zone) throws Exception { + String clientId, + String clientSecret, + String username, + String password, + String scope, + IdentityZone zone) throws Exception { return getUserOAuthAccessToken(mockMvc, - clientId, - clientSecret, - username, - password, - scope, - zone, - false); + clientId, + clientSecret, + username, + password, + scope, + zone, + false); } public static String getUserOAuthAccessToken(MockMvc mockMvc, - String clientId, - String clientSecret, - String username, - String password, - String scope, - IdentityZone zone, - boolean opaque) throws Exception { + String clientId, + String clientSecret, + String username, + String password, + String scope, + IdentityZone zone, + boolean opaque) throws Exception { String basicDigestHeaderValue = "Basic " - + new String(Base64.encodeBase64((clientId + ":" + clientSecret).getBytes())); + + new String(Base64.encodeBase64((clientId + ":" + clientSecret).getBytes())); MockHttpServletRequestBuilder oauthTokenPost = - post("/oauth/token") - .header("Authorization", basicDigestHeaderValue) - .param("grant_type", "password") - .param("client_id", clientId) - .param("username", username) - .param("password", password) - .param("scope", scope); - if (zone!=null) { - oauthTokenPost.header("Host", zone.getSubdomain()+".localhost"); + post("/oauth/token") + .header("Authorization", basicDigestHeaderValue) + .param("grant_type", "password") + .param("client_id", clientId) + .param("username", username) + .param("password", password) + .param("scope", scope); + if (zone != null) { + oauthTokenPost.header("Host", zone.getSubdomain() + ".localhost"); } if (opaque) { oauthTokenPost.param(TokenConstants.REQUEST_TOKEN_FORMAT, TokenConstants.OPAQUE); @@ -1092,42 +1136,42 @@ public static String getUserOAuthAccessToken(MockMvc mockMvc, MvcResult result = mockMvc.perform(oauthTokenPost).andDo(print()).andExpect(status().isOk()).andReturn(); InjectedMockContextTest.OAuthToken oauthToken = JsonUtils.readValue(result.getResponse().getContentAsString(), - InjectedMockContextTest.OAuthToken.class); + InjectedMockContextTest.OAuthToken.class); return oauthToken.accessToken; } public static String getClientOAuthAccessToken(MockMvc mockMvc, String clientId, String clientSecret, String scope) - throws Exception { + throws Exception { return getClientCredentialsOAuthAccessToken(mockMvc, clientId, clientSecret, scope, null); } public static String getUserOAuthAccessTokenAuthCode(MockMvc mockMvc, String clientId, String clientSecret, String userId, String username, String password, String scope) throws Exception { String basicDigestHeaderValue = "Basic " - + new String(org.apache.commons.codec.binary.Base64.encodeBase64((clientId + ":" + clientSecret) - .getBytes())); + + new String(org.apache.commons.codec.binary.Base64.encodeBase64((clientId + ":" + clientSecret) + .getBytes())); UaaPrincipal p = new UaaPrincipal(userId, username, "test@test.org", OriginKeys.UAA, "", IdentityZoneHolder.get() - .getId()); + .getId()); UaaAuthentication auth = new UaaAuthentication(p, UaaAuthority.USER_AUTHORITIES, null); Assert.assertTrue(auth.isAuthenticated()); SecurityContextHolder.getContext().setAuthentication(auth); MockHttpSession session = new MockHttpSession(); session.setAttribute( - HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, - new MockSecurityContext(auth) - ); + HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, + new MockSecurityContext(auth) + ); String state = new RandomValueStringGenerator().generate(); MockHttpServletRequestBuilder authRequest = get("/oauth/authorize") - .header("Authorization", basicDigestHeaderValue) - .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .session(session) - .param(OAuth2Utils.GRANT_TYPE, "authorization_code") - .param(OAuth2Utils.RESPONSE_TYPE, "code") - .param(TokenConstants.REQUEST_TOKEN_FORMAT, TokenConstants.OPAQUE) - .param(OAuth2Utils.STATE, state) - .param(OAuth2Utils.CLIENT_ID, clientId) - .param(OAuth2Utils.REDIRECT_URI, "http://localhost/test"); + .header("Authorization", basicDigestHeaderValue) + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .session(session) + .param(OAuth2Utils.GRANT_TYPE, "authorization_code") + .param(OAuth2Utils.RESPONSE_TYPE, "code") + .param(TokenConstants.REQUEST_TOKEN_FORMAT, TokenConstants.OPAQUE) + .param(OAuth2Utils.STATE, state) + .param(OAuth2Utils.CLIENT_ID, clientId) + .param(OAuth2Utils.REDIRECT_URI, "http://localhost/test"); if (StringUtils.hasText(scope)) { authRequest.param(OAuth2Utils.SCOPE, scope); } @@ -1138,48 +1182,48 @@ public static String getUserOAuthAccessTokenAuthCode(MockMvc mockMvc, String cli String code = builder.build().getQueryParams().get("code").get(0); authRequest = post("/oauth/token") - .header("Authorization", basicDigestHeaderValue) - .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .param(OAuth2Utils.GRANT_TYPE, "authorization_code") - .param(OAuth2Utils.RESPONSE_TYPE, "token") - .param("code", code) - .param(OAuth2Utils.CLIENT_ID, clientId) - .param(OAuth2Utils.REDIRECT_URI, "http://localhost/test"); + .header("Authorization", basicDigestHeaderValue) + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .param(OAuth2Utils.GRANT_TYPE, "authorization_code") + .param(OAuth2Utils.RESPONSE_TYPE, "token") + .param("code", code) + .param(OAuth2Utils.CLIENT_ID, clientId) + .param(OAuth2Utils.REDIRECT_URI, "http://localhost/test"); if (StringUtils.hasText(scope)) { authRequest.param(OAuth2Utils.SCOPE, scope); } result = mockMvc.perform(authRequest).andExpect(status().is2xxSuccessful()).andReturn(); InjectedMockContextTest.OAuthToken oauthToken = JsonUtils.readValue(result.getResponse().getContentAsString(), - InjectedMockContextTest.OAuthToken.class); + InjectedMockContextTest.OAuthToken.class); return oauthToken.accessToken; } public static String getScimInviteUserToken(MockMvc mockMvc, String clientId, String clientSecret, IdentityZone zone) throws Exception { String adminToken = getClientCredentialsOAuthAccessToken(mockMvc, - "admin", - zone==null?"adminsecret":"admin-secret", - "", - zone==null?null:zone.getSubdomain() + "admin", + zone == null ? "adminsecret" : "admin-secret", + "", + zone == null ? null : zone.getSubdomain() ); // create a user (with the required permissions) to perform the actual /invite_users action - String username = new RandomValueStringGenerator().generate().toLowerCase()+"@example.com"; + String username = new RandomValueStringGenerator().generate().toLowerCase() + "@example.com"; ScimUser user = new ScimUser(clientId, username, "given-name", "family-name"); user.setPrimaryEmail(username); user.setPassword("password"); - user = (zone == null) ? createUser(mockMvc, adminToken, user) : createUserInZone(mockMvc,adminToken,user,zone.getSubdomain(), null); + user = (zone == null) ? createUser(mockMvc, adminToken, user) : createUserInZone(mockMvc, adminToken, user, zone.getSubdomain(), null); String scope = "scim.invite"; ScimGroupMember member = new ScimGroupMember(user.getId(), ScimGroupMember.Type.USER); ScimGroup inviteGroup = new ScimGroup(scope); - if (zone!=null) { + if (zone != null) { createGroup(mockMvc, adminToken, zone.getSubdomain(), inviteGroup); } ScimGroup group = getGroup(mockMvc, - adminToken, - scope, - zone==null?null:zone.getSubdomain() + adminToken, + scope, + zone == null ? null : zone.getSubdomain() ); group.getMembers().add(member); updateGroup(mockMvc, adminToken, group, zone); @@ -1187,38 +1231,38 @@ public static String getScimInviteUserToken(MockMvc mockMvc, String clientId, St // get a bearer token for the user return getUserOAuthAccessToken(mockMvc, - clientId, - clientSecret, - user.getUserName(), - "password", - "scim.invite", - zone + clientId, + clientSecret, + user.getUserName(), + "password", + "scim.invite", + zone ); } public static String getClientCredentialsOAuthAccessToken(MockMvc mockMvc, - String clientId, - String clientSecret, - String scope, - String subdomain) throws Exception { + String clientId, + String clientSecret, + String scope, + String subdomain) throws Exception { return getClientCredentialsOAuthAccessToken(mockMvc, clientId, clientSecret, scope, subdomain, false); } public static String getClientCredentialsOAuthAccessToken(MockMvc mockMvc, - String clientId, - String clientSecret, - String scope, - String subdomain, - boolean opaque) throws Exception { + String clientId, + String clientSecret, + String scope, + String subdomain, + boolean opaque) throws Exception { String basicDigestHeaderValue = "Basic " - + new String(Base64.encodeBase64((clientId + ":" + clientSecret).getBytes())); + + new String(Base64.encodeBase64((clientId + ":" + clientSecret).getBytes())); MockHttpServletRequestBuilder oauthTokenPost = post("/oauth/token") - .header("Authorization", basicDigestHeaderValue) - .param("grant_type", "client_credentials") - .param("client_id", clientId) - .param("recovable", "true"); - if (!isEmpty(scope)){ + .header("Authorization", basicDigestHeaderValue) + .param("grant_type", "client_credentials") + .param("client_id", clientId) + .param("recovable", "true"); + if (!isEmpty(scope)) { oauthTokenPost.param("scope", scope); } if (subdomain != null && !subdomain.equals("")) { @@ -1228,9 +1272,9 @@ public static String getClientCredentialsOAuthAccessToken(MockMvc mockMvc, oauthTokenPost.param(TokenConstants.REQUEST_TOKEN_FORMAT, TokenConstants.OPAQUE); } MvcResult result = mockMvc.perform(oauthTokenPost) - .andDo(print()) - .andExpect(status().isOk()) - .andReturn(); + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); InjectedMockContextTest.OAuthToken oauthToken = JsonUtils.readValue(result.getResponse().getContentAsString(), InjectedMockContextTest.OAuthToken.class); return oauthToken.accessToken; } @@ -1244,22 +1288,22 @@ public static SecurityContext getUaaSecurityContext(String username, Application } public static SecurityContext getUaaSecurityContext(String username, ApplicationContext context, IdentityZone zone) { - try { - IdentityZoneHolder.set(zone); - ScimUserProvisioning userProvisioning = context.getBean(JdbcScimUserProvisioning.class); - ScimUser user = userProvisioning.query("username eq \""+username+"\" and origin eq \"uaa\"", IdentityZoneHolder.get().getId()).get(0); - UaaPrincipal uaaPrincipal = new UaaPrincipal(user.getId(), user.getUserName(), user.getPrimaryEmail(), user.getOrigin(), user.getExternalId(), IdentityZoneHolder.get().getId()); - UaaAuthentication principal = new UaaAuthentication(uaaPrincipal, null, Arrays.asList(UaaAuthority.fromAuthorities("uaa.user")), new UaaAuthenticationDetails(new MockHttpServletRequest()), true, System.currentTimeMillis()); - SecurityContext securityContext = new SecurityContextImpl(); - securityContext.setAuthentication(principal); - return securityContext; - } finally { - IdentityZoneHolder.clear(); - } + try { + IdentityZoneHolder.set(zone); + ScimUserProvisioning userProvisioning = context.getBean(JdbcScimUserProvisioning.class); + ScimUser user = userProvisioning.query("username eq \"" + username + "\" and origin eq \"uaa\"", IdentityZoneHolder.get().getId()).get(0); + UaaPrincipal uaaPrincipal = new UaaPrincipal(user.getId(), user.getUserName(), user.getPrimaryEmail(), user.getOrigin(), user.getExternalId(), IdentityZoneHolder.get().getId()); + UaaAuthentication principal = new UaaAuthentication(uaaPrincipal, null, Arrays.asList(UaaAuthority.fromAuthorities("uaa.user")), new UaaAuthenticationDetails(new MockHttpServletRequest()), true, System.currentTimeMillis()); + SecurityContext securityContext = new SecurityContextImpl(); + securityContext.setAuthentication(principal); + return securityContext; + } finally { + IdentityZoneHolder.clear(); } + } - public static TestApplicationEventListener addEventListener(ConfigurableApplicationContext applicationContext, Class clazz) { + public static TestApplicationEventListener addEventListener(ConfigurableApplicationContext applicationContext, Class clazz) { TestApplicationEventListener listener = TestApplicationEventListener.forEventClass(clazz); applicationContext.addApplicationListener(listener); return listener; @@ -1305,6 +1349,7 @@ public void setAuthentication(Authentication authentication) { public static class CookieCsrfPostProcessor implements RequestPostProcessor { private boolean useInvalidToken = false; + public CookieCsrfPostProcessor useInvalidToken() { useInvalidToken = true; return this; @@ -1319,7 +1364,7 @@ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) Cookie cookie = new Cookie(token.getParameterName(), tokenValue); cookie.setHttpOnly(true); Cookie[] cookies = request.getCookies(); - if (cookies==null) { + if (cookies == null) { request.setCookies(cookie); } else { addCsrfCookie(request, cookie, cookies); @@ -1330,9 +1375,9 @@ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) protected void addCsrfCookie(MockHttpServletRequest request, Cookie cookie, Cookie[] cookies) { boolean replaced = false; - for (int i=0; i Date: Wed, 24 Jan 2018 16:24:14 -0800 Subject: [PATCH 20/42] Runtime exceptions during silent authentication result in internal_server_error [#153568058] https://www.pivotaltracker.com/story/show/153568058 Signed-off-by: Dennis Leon --- .../uaa/oauth/UaaAuthorizationEndpoint.java | 106 ++++++++++-------- ...ationPromptNoneEntryPointMockMvcTests.java | 36 ++++++ 2 files changed, 94 insertions(+), 48 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java index 03359d7f1c5..1c951acc2f6 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java @@ -158,25 +158,25 @@ public ModelAndView authorize(Map model, throw new InvalidClientException("A client id must be provided"); } + String resolvedRedirect = ""; try { String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI); - String resolvedRedirect; try { resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client); } catch (RedirectMismatchException rme) { throw new RedirectMismatchException( - "Invalid redirect " + redirectUriParameter + " did not match one of the registered values"); + "Invalid redirect " + redirectUriParameter + " did not match one of the registered values"); } if (!StringUtils.hasText(resolvedRedirect)) { throw new RedirectMismatchException( - "A redirectUri must be either supplied or preconfigured in the ClientDetails"); + "A redirectUri must be either supplied or preconfigured in the ClientDetails"); } boolean isAuthenticated = (principal instanceof Authentication) && ((Authentication) principal).isAuthenticated(); if (!isAuthenticated) { throw new InsufficientAuthenticationException( - "User must be authenticated with Spring Security before authorization can be completed."); + "User must be authenticated with Spring Security before authorization can be completed."); } authorizationRequest.setRedirectUri(resolvedRedirect); @@ -187,7 +187,7 @@ public ModelAndView authorize(Map model, // Some systems may allow for approval decisions to be remembered or approved by default. Check for // such logic here, and set the approved flag on the authorization request accordingly. authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest, - (Authentication) principal); + (Authentication) principal); boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal); authorizationRequest.setApproved(approved); @@ -195,21 +195,21 @@ public ModelAndView authorize(Map model, if (authorizationRequest.isApproved()) { if (responseTypes.contains("token") || responseTypes.contains("id_token")) { return getImplicitGrantOrHybridResponse( - authorizationRequest, - (Authentication) principal, - grantType + authorizationRequest, + (Authentication) principal, + grantType ); } if (responseTypes.contains("code")) { return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest, - (Authentication) principal)); + (Authentication) principal)); } } if ("none".equals(authorizationRequest.getRequestParameters().get("prompt"))) { return new ModelAndView( - new RedirectView(UaaUrlUtils.addFragmentComponent(resolvedRedirect, "error=interaction_required")) + new RedirectView(UaaUrlUtils.addFragmentComponent(resolvedRedirect, "error=interaction_required")) ); } else { // Place auth request into the model so that it is stored in the session @@ -219,8 +219,18 @@ public ModelAndView authorize(Map model, model.put("original_uri", UrlUtils.buildFullRequestUrl(request)); return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal); } - } catch (RuntimeException e) { + } catch (RedirectMismatchException e) { sessionStatus.setComplete(); + throw e; + } catch (Exception e) { + sessionStatus.setComplete(); + + if ("none".equals(authorizationRequest.getRequestParameters().get("prompt"))) { + return new ModelAndView( + new RedirectView(UaaUrlUtils.addFragmentComponent(resolvedRedirect, "error=internal_server_error")) + ); + } + throw e; } @@ -248,7 +258,7 @@ public View approveOrDeny(@RequestParam Map approvalParameters, if (!(principal instanceof Authentication)) { sessionStatus.setComplete(); throw new InsufficientAuthenticationException( - "User must be authenticated with Spring Security before authorizing an access token."); + "User must be authenticated with Spring Security before authorizing an access token."); } AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get("authorizationRequest"); @@ -264,7 +274,7 @@ public View approveOrDeny(@RequestParam Map approvalParameters, authorizationRequest.setApprovalParameters(approvalParameters); authorizationRequest = userApprovalHandler.updateAfterApproval(authorizationRequest, - (Authentication) principal); + (Authentication) principal); boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal); authorizationRequest.setApproved(approved); @@ -275,15 +285,15 @@ public View approveOrDeny(@RequestParam Map approvalParameters, if (!authorizationRequest.isApproved()) { return new RedirectView(getUnsuccessfulRedirect(authorizationRequest, - new UserDeniedAuthorizationException("User denied access"), responseTypes.contains("token")), - false, true, false); + new UserDeniedAuthorizationException("User denied access"), responseTypes.contains("token")), + false, true, false); } if (responseTypes.contains("token") || responseTypes.contains("id_token")) { return getImplicitGrantOrHybridResponse( - authorizationRequest, - (Authentication) principal, - grantType + authorizationRequest, + (Authentication) principal, + grantType ).getView(); } @@ -313,9 +323,9 @@ private ModelAndView getUserApprovalPageResponse(Map model, // We can grant a token and return it with implicit approval. private ModelAndView getImplicitGrantOrHybridResponse( - AuthorizationRequest authorizationRequest, - Authentication authentication, - String grantType + AuthorizationRequest authorizationRequest, + Authentication authentication, + String grantType ) { OAuth2AccessToken accessToken; try { @@ -326,16 +336,16 @@ private ModelAndView getImplicitGrantOrHybridResponse( throw new UnsupportedResponseTypeException("Unsupported response type: token or id_token"); } return new ModelAndView( - new RedirectView( - buildRedirectURI(authorizationRequest, accessToken, authentication), - false, - true, - false - ) + new RedirectView( + buildRedirectURI(authorizationRequest, accessToken, authentication), + false, + true, + false + ) ); } catch (OAuth2Exception e) { return new ModelAndView(new RedirectView(getUnsuccessfulRedirect(authorizationRequest, e, true), false, - true, false)); + true, false)); } } @@ -360,13 +370,13 @@ private OAuth2AccessToken getAccessTokenForImplicitGrantOrHybrid(TokenRequest to private View getAuthorizationCodeResponse(AuthorizationRequest authorizationRequest, Authentication authUser) { try { return new RedirectView( - getSuccessfulRedirect( - authorizationRequest, - generateCode(authorizationRequest, authUser) - ), - false, - false, //so that we send absolute URLs always - false + getSuccessfulRedirect( + authorizationRequest, + generateCode(authorizationRequest, authUser) + ), + false, + false, //so that we send absolute URLs always + false ) { @Override protected HttpStatus getHttp11StatusCode(HttpServletRequest request, HttpServletResponse response, String targetUrl) { @@ -397,7 +407,7 @@ public String buildRedirectURI(AuthorizationRequest authorizationRequest, } if (accessToken instanceof CompositeAccessToken && - authorizationRequest.getResponseTypes().contains(CompositeAccessToken.ID_TOKEN)) { + authorizationRequest.getResponseTypes().contains(CompositeAccessToken.ID_TOKEN)) { url.append("&").append(CompositeAccessToken.ID_TOKEN).append("=").append(encode(((CompositeAccessToken) accessToken).getIdTokenValue())); } @@ -432,11 +442,11 @@ public String buildRedirectURI(AuthorizationRequest authorizationRequest, if ("none".equals(authorizationRequest.getRequestParameters().get("prompt"))) { - HttpHost httpHost = URIUtils.extractHost(URI.create(requestedRedirect)); - String sessionState = openIdSessionStateCalculator.calculate(RequestContextHolder.currentRequestAttributes().getSessionId(), - authorizationRequest.getClientId(), httpHost.toURI()); + HttpHost httpHost = URIUtils.extractHost(URI.create(requestedRedirect)); + String sessionState = openIdSessionStateCalculator.calculate(RequestContextHolder.currentRequestAttributes().getSessionId(), + authorizationRequest.getClientId(), httpHost.toURI()); - url.append("&session_state=").append(sessionState); + url.append("&session_state=").append(sessionState); } UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(requestedRedirect); @@ -452,7 +462,7 @@ public String buildRedirectURI(AuthorizationRequest authorizationRequest, } private String generateCode(AuthorizationRequest authorizationRequest, Authentication authentication) - throws AuthenticationException { + throws AuthenticationException { try { @@ -572,10 +582,10 @@ public ModelAndView handleOAuth2Exception(OAuth2Exception e, ServletWebRequest w @ExceptionHandler(HttpSessionRequiredException.class) public ModelAndView handleHttpSessionRequiredException(HttpSessionRequiredException e, ServletWebRequest webRequest) - throws Exception { + throws Exception { logger.info("Handling Session required error: " + e.getMessage()); return handleException(new AccessDeniedException("Could not obtain authorization request from session", e), - webRequest); + webRequest); } private ModelAndView handleException(Exception e, ServletWebRequest webRequest) throws Exception { @@ -597,12 +607,12 @@ private ModelAndView handleException(Exception e, ServletWebRequest webRequest) authorizationRequest = getAuthorizationRequestForError(webRequest); String requestedRedirectParam = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI); String requestedRedirect = - redirectResolver.resolveRedirect( - requestedRedirectParam, - getClientServiceExtention().loadClientByClientId(authorizationRequest.getClientId(), IdentityZoneHolder.get().getId())); + redirectResolver.resolveRedirect( + requestedRedirectParam, + getClientServiceExtention().loadClientByClientId(authorizationRequest.getClientId(), IdentityZoneHolder.get().getId())); authorizationRequest.setRedirectUri(requestedRedirect); String redirect = getUnsuccessfulRedirect(authorizationRequest, translate.getBody(), authorizationRequest - .getResponseTypes().contains("token")); + .getResponseTypes().contains("token")); return new ModelAndView(new RedirectView(redirect, false, true, false)); } catch (OAuth2Exception ex) { // If an AuthorizationRequest cannot be created from the incoming parameters it must be @@ -617,7 +627,7 @@ private AuthorizationRequest getAuthorizationRequestForError(ServletWebRequest w // If it's already there then we are in the approveOrDeny phase and we can use the saved request AuthorizationRequest authorizationRequest = (AuthorizationRequest) sessionAttributeStore.retrieveAttribute( - webRequest, "authorizationRequest"); + webRequest, "authorizationRequest"); if (authorizationRequest != null) { return authorizationRequest; } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java index a05c4d375c3..36a2c090648 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java @@ -3,6 +3,7 @@ import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; import org.cloudfoundry.identity.uaa.oauth.OpenIdSessionStateCalculator; +import org.cloudfoundry.identity.uaa.oauth.UaaAuthorizationEndpoint; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -11,6 +12,7 @@ import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.test.web.servlet.MvcResult; +import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; @@ -18,8 +20,10 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; @@ -108,6 +112,38 @@ public String changeSessionId() { Assert.assertThat(redirectUrl, containsString("session_state=707c310bc5aa38acc03d48a099fc999cd77f44df163178df1ca35863913f5711.0000000000000000000000000000000000000000000000000000000000000000")); } + @Test + public void testSilentAuthentication_RuntimeException_displaysErrorFragment() throws Exception { + OpenIdSessionStateCalculator openIdSessionStateCalculator = mock(OpenIdSessionStateCalculator.class); + UaaAuthorizationEndpoint uaaAuthorizationEndpoint = getWebApplicationContext().getBean(UaaAuthorizationEndpoint.class); + uaaAuthorizationEndpoint.setOpenIdSessionStateCalculator(openIdSessionStateCalculator); + + when(openIdSessionStateCalculator.calculate(anyString(), anyString(), anyString())).thenThrow(NoSuchAlgorithmException.class); + + MockHttpSession session = new MockHttpSession(); + login(session); + + getMockMvc().perform( + get("/oauth/authorize?response_type=token&scope=openid&client_id=ant&prompt=none&redirect_uri=http://example.com/with/path.html") + .session(session) + ) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://example.com/with/path.html#error=internal_server_error")); + + } + + @Test + public void testSilentAuthentication_Returns400_whenInvalidRedirectUrlIsProvided() throws Exception { + MockHttpSession session = new MockHttpSession(); + login(session); + + getMockMvc().perform( + get("/oauth/authorize?response_type=token&scope=openid&client_id=ant&prompt=none&redirect_uri=no good uri") + .session(session) + ) + .andExpect(status().is4xxClientError()); + } + @Test public void nonSilentAuthentication_doesNotComputeSessionState() throws Exception { MockHttpSession session = new MockHttpSession(); From 34edcba6d56a35a7ddf493734ca417fffcc03be4 Mon Sep 17 00:00:00 2001 From: Dennis Leon Date: Thu, 25 Jan 2018 14:54:59 -0800 Subject: [PATCH 21/42] Fix test that asserted session_state present when prompt=none not used This param should only be computed when silent authentication is being used. [#153568058] https://www.pivotaltracker.com/story/show/153568058 Signed-off-by: Jennifer Hamon --- .../oauth/UaaAuthorizationEndpointTest.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointTest.java index c049400d09b..a1a2f191b80 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointTest.java @@ -16,6 +16,7 @@ import java.util.Calendar; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Set; @@ -101,7 +102,7 @@ public void testGetGrantType_code_id_token_and_token_is_implicit() { } @Test - public void testBuildRedirectURI() { + public void testBuildRedirectURI_doesNotIncludeSessionStateWhenNotPromptNone() { AuthorizationRequest authorizationRequest = new AuthorizationRequest(); authorizationRequest.setRedirectUri("http://example.com/somepath"); authorizationRequest.setResponseTypes(new HashSet() { @@ -121,6 +122,7 @@ public void testBuildRedirectURI() { OAuth2Request storedOAuth2Request = mock(OAuth2Request.class); when(oAuth2RequestFactory.createOAuth2Request(any())).thenReturn(storedOAuth2Request); when(authorizationCodeServices.createAuthorizationCode(any())).thenReturn("ABCD"); + String result = uaaAuthorizationEndpoint.buildRedirectURI(authorizationRequest, accessToken, authUser); assertThat(result, containsString("http://example.com/somepath#")); @@ -131,6 +133,25 @@ public void testBuildRedirectURI() { assertThat(result, containsString("state=California")); assertThat(result, containsString("expires_in=")); assertThat(result, containsString("scope=null")); + } + + @Test + public void buildRedirectURI_includesSessionStateForPromptEqualsNone() { + AuthorizationRequest authorizationRequest = new AuthorizationRequest(); + authorizationRequest.setRedirectUri("http://example.com/somepath"); + authorizationRequest.setRequestParameters(new HashMap() { + { + put("prompt", "none"); + } + }); + CompositeAccessToken accessToken = new CompositeAccessToken("TOKEN_VALUE+="); + UaaPrincipal principal = new UaaPrincipal("userid", "username", "email", "origin", "extid", "zoneid"); + UaaAuthenticationDetails details = new UaaAuthenticationDetails(true, "clientid", "origin", "SOMESESSIONID"); + Authentication authUser = new UaaAuthentication(principal, Collections.emptyList(), details); + when(authorizationCodeServices.createAuthorizationCode(any())).thenReturn("ABCD"); + + String result = uaaAuthorizationEndpoint.buildRedirectURI(authorizationRequest, accessToken, authUser); + assertThat(result, containsString("session_state=opbshash")); } } \ No newline at end of file From 6a0eb58429d9f6d241aa2c2d362243f095a40a8d Mon Sep 17 00:00:00 2001 From: Jennifer Hamon Date: Thu, 25 Jan 2018 15:27:31 -0800 Subject: [PATCH 22/42] Unit test and refactor session.js for session management OP iframe [#153568058] https://www.pivotaltracker.com/story/show/153568058 --- .../main/resources/templates/web/session.html | 38 +-- .../templates/web/session/session.js | 104 +++++++- .../spec/templates/web/SessionSpec.js | 233 +++++++++++++++++- 3 files changed, 326 insertions(+), 49 deletions(-) diff --git a/server/src/main/resources/templates/web/session.html b/server/src/main/resources/templates/web/session.html index 0424f537d56..3aed98403ad 100644 --- a/server/src/main/resources/templates/web/session.html +++ b/server/src/main/resources/templates/web/session.html @@ -1,44 +1,16 @@ + diff --git a/server/src/main/resources/templates/web/session/session.js b/server/src/main/resources/templates/web/session/session.js index 79adba4f7ce..6780c3fa198 100644 --- a/server/src/main/resources/templates/web/session/session.js +++ b/server/src/main/resources/templates/web/session/session.js @@ -1,4 +1,102 @@ -function session_state(client_id, origin, browser_state, salt) { - var res = sjcl.hash.sha256.hash(client_id + ' ' + origin + ' ' + browser_state + ' ' + salt); - return sjcl.codec.hex.fromBits(res) + "." + salt; +function SessionManagement(document) { + var RP_MESSAGE_FORMAT_ERROR = 'incorrect data format'; + + function _calculate_session_state(client_id, origin, op_browser_state, salt) { + var res = sjcl.hash.sha256.hash(client_id + ' ' + origin + ' ' + op_browser_state + ' ' + salt); + return sjcl.codec.hex.fromBits(res) + "." + salt; + } + + function isBlank(str) { + return (str.trim().length === 0); + } + + function validate_rp_data_format(rpData) { + var rpDataParts = rpData.split(" "); + if (rpDataParts.length !== 2) { + throw RP_MESSAGE_FORMAT_ERROR; + } + if (isBlank(rpDataParts[0]) || isBlank(rpDataParts[1])) { + throw RP_MESSAGE_FORMAT_ERROR; + } + + var rpSessionStateParts = rpDataParts[1].split("."); + if (rpSessionStateParts.length !== 2) { + throw RP_MESSAGE_FORMAT_ERROR; + } + if (isBlank(rpSessionStateParts[0]) || isBlank(rpSessionStateParts[1])) { + throw RP_MESSAGE_FORMAT_ERROR; + } + } + + function _get_client_id(rpData) { + validate_rp_data_format(rpData); + var rpDataParts = rpData.split(" "); + return rpDataParts[0]; + } + + function _get_session_state(rpData) { + validate_rp_data_format(rpData); + var rpDataParts = rpData.split(" "); + return rpDataParts[1]; + } + + function _get_salt(rpData) { + return _get_session_state(rpData).split(".")[1]; + } + + function _get_op_browser_state() { + var cookies = document.cookie.split(';'); + var opbs = ''; + cookies.forEach(function(c) { + var cookieName = c.split("=")[0].trim(); + var cookieValue = c.split("=")[1]; + if (cookieName === "Current-User") { + opbs = JSON.parse(decodeURIComponent(cookieValue)).userId; + } + }); + + return opbs; + } + + function buildMessageHandler(expected_origin, expected_client) { + return function handleMessage(e) { + // This method follows the implementation described in the OIDC session + // management spec http://openid.net/specs/openid-connect-session-1_0.html#OPiframe + try { + var client_id = _get_client_id(e.data); + var rp_session_state = _get_session_state(e.data); + var salt = _get_salt(e.data); + } catch (err) { + e.source.postMessage('error', e.origin); + return; + } + + if (expected_origin !== e.origin) { + e.source.postMessage('error', e.origin); + return; + } + if (expected_client !== client_id) { + e.source.postMessage('error', e.origin); + return; + } + + var opbs = _get_op_browser_state(); + var op_session_state = _calculate_session_state(client_id, e.origin, opbs, salt); + + if (rp_session_state === op_session_state) { + e.source.postMessage('unchanged', e.origin); + } else { + e.source.postMessage('changed', e.origin); + } + } + } + + return { + _calculate_session_state: _calculate_session_state, + _get_client_id: _get_client_id, + _get_session_state: _get_session_state, + _get_salt: _get_salt, + _get_op_browser_state: _get_op_browser_state, + buildMessageHandler: buildMessageHandler + }; } \ No newline at end of file diff --git a/server/src/test/javascript/spec/templates/web/SessionSpec.js b/server/src/test/javascript/spec/templates/web/SessionSpec.js index 9cc4731f4c9..4ba22f02178 100644 --- a/server/src/test/javascript/spec/templates/web/SessionSpec.js +++ b/server/src/test/javascript/spec/templates/web/SessionSpec.js @@ -1,16 +1,223 @@ describe("Session", function() { - fs = require('fs') - sha256JS = fs.readFileSync('/Users/pivotal/workspace/uaa/server/src/main/resources/templates/web/session/sjcl.js','utf-8') // depends on the file encoding - eval(sha256JS) - sessionJS = fs.readFileSync('/Users/pivotal/workspace/uaa/server/src/main/resources/templates/web/session/session.js','utf-8') // depends on the file encoding - eval(sessionJS) - - it("calculates session state", function () { - var client_id = '1'; - var origin = 'example.com'; - var salt = 'somesalt'; - var browser_state = 'JSESSIONID_VALUE'; - ss = session_state(client_id, origin, browser_state, salt); - expect(ss).toEqual("e53852e9aff5c750c8ba47d760e87be41e44b02697822c86d3bb9e9f61bf5e13.somesalt") + var fs = require('fs'); + eval(fs.readFileSync('src/main/resources/templates/web/session/sjcl.js','utf-8')); + eval(fs.readFileSync('src/main/resources/templates/web/session/session.js','utf-8')); + + var sm; + var document; + + beforeEach(function() { + document = {}; + sm = SessionManagement(document); + }); + + describe("_calculate_session_state", function () { + it("calculates session state", function () { + var client_id = '1'; + var origin = 'example.com'; + var salt = 'somesalt'; + var opbs = 'user-id-1'; + var ss = sm._calculate_session_state(client_id, origin, opbs, salt); + expect(ss).toEqual("26544311372c0e521a5dcbb8725594ba0808d169110f473bd5654e489471425c.somesalt") + }); + }); + + describe("_get_session_state", function () { + it("gets session state from a valid rp data", function () { + expect(sm._get_session_state("clientid sessionstate.salt")).toEqual('sessionstate.salt'); + }); + + it("throws an error if data is invalid", function () { + expect(function() {sm._get_session_state("")}).toThrow('incorrect data format'); + expect(function() {sm._get_session_state("clientidsessionstate")}).toThrow('incorrect data format'); + expect(function() {sm._get_session_state("clientid session_state salt")}).toThrow('incorrect data format'); + expect(function() {sm._get_session_state("clientid session_state")}).toThrow('incorrect data format'); + expect(function() {sm._get_session_state("clientid .salt")}).toThrow(); + expect(function() {sm._get_session_state("clientid session_state.")}).toThrow(); + expect(function() {sm._get_session_state(" session_state.salt")}).toThrow(); + expect(function() {sm._get_session_state("clientid ")}).toThrow(); + expect(function() {sm._get_session_state("clientid session_state.salt")}).not.toThrow(); + }); + }); + + describe("_get_client_id", function () { + it("gets client id from a valid rp data", function () { + expect(sm._get_client_id("clientid sessionstate.salt")).toEqual('clientid'); + }); + + it("throws an error if data is invalid", function () { + expect(function() {sm._get_client_id("")}).toThrow('incorrect data format'); + expect(function() {sm._get_client_id("sessionstate.salt")}).toThrow('incorrect data format'); + expect(function() {sm._get_client_id("clientid session_state salt")}).toThrow('incorrect data format'); + expect(function() {sm._get_client_id("clientid session_state")}).toThrow('incorrect data format'); + expect(function() {sm._get_client_id("clientid session_state.salt")}).not.toThrow(); + }); + }); + + describe("_get_salt", function () { + it("gets salt from a valid rp data", function () { + expect(sm._get_salt("clientid sessionstate.salt")).toEqual('salt'); + }); + + it("throws an error if data is invalid", function () { + expect(function() {sm._get_salt("")}).toThrow('incorrect data format'); + expect(function() {sm._get_salt("sessionstate.salt")}).toThrow('incorrect data format'); + expect(function() {sm._get_salt("clientid session_state salt")}).toThrow('incorrect data format'); + expect(function() {sm._get_salt("clientid session_state")}).toThrow('incorrect data format'); + expect(function() {sm._get_salt("clientid session_state.salt")}).not.toThrow(); + }); + }); + + describe("_get_op_browser_state", function () { + it("gets the Current-User cookie value", function () { + // We cannot use JSESSIONID cookie for the op browser state because the session cookie is httponly + // and cannot be read from javascripts, even javascripts from the same origin. + document = { cookie: 'COOKIE1=foo; COOKIE2=bar; Current-User=%7B%22userId%22%3A%229ab2f713-7baf-4411-8067-774d126327e9%22%7D;'}; + sm = SessionManagement(document); + expect(sm._get_op_browser_state()).toEqual("9ab2f713-7baf-4411-8067-774d126327e9"); + }); + + it("returns empty string when the Current-User cookie is not present", function () { + document = { cookie: 'COOKIE1=foo; COOKIE2=bar;'}; + sm = SessionManagement(document); + expect(sm._get_op_browser_state()).toEqual(""); + }); + }); + + describe("building a message handler", function () { + describe("when a message is received with an unexpected client id", function () { + it("replies with an error", function () { + sm = SessionManagement({}); + var handler = sm.buildMessageHandler("expectedOrigin", "expectedClient"); + var postMessageSpy = jasmine.createSpy('postMessageSpy'); + + handler({ + data: 'otherclientid hash.salt', + origin: 'expectedOrigin', + source: {postMessage: postMessageSpy} + }); + + expect(postMessageSpy).toHaveBeenCalledWith('error', 'expectedOrigin'); + }); + }); + + describe("when a message is received with an unexpected origin", function () { + it("replies with an error", function () { + sm = SessionManagement({}); + var handler = sm.buildMessageHandler("expectedOrigin", "expectedClient"); + var postMessageSpy = jasmine.createSpy('postMessageSpy'); + + handler({ + data: 'expectedClient hash.salt', + origin: 'badOrigin', + source: {postMessage: postMessageSpy} + }); + + expect(postMessageSpy).toHaveBeenCalledWith('error', 'badOrigin'); + }); + }); + + describe("when Current-User id has not changed", function () { + it("replies to RP iframe with 'unchanged'", function () { + // OP frame setup + document = {cookie: 'Current-User=%7B%22userId%22%3A%22theuserid%22%7D;'}; + sm = SessionManagement(document); + + // RP message setup + var clientId = 'apps_manager_js'; + var origin = 'http://relyingparty.com/somepage.html'; + var hash = sm._calculate_session_state(clientId, origin, 'theuserid', 'somesalt'); + var postMessageSpy = jasmine.createSpy('postMessageSpy'); + // The properties of this message event are described here: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage + var messageEvent = { + data: 'apps_manager_js ' + hash, + origin: origin, + source: {postMessage: postMessageSpy} + }; + var handler = sm.buildMessageHandler(origin, clientId); + + + handler(messageEvent); + + expect(postMessageSpy).toHaveBeenCalledWith('unchanged', origin); + }); + }); + + describe("when Current-User id has changed", function () { + it("replies to RP iframe with 'changed'", function () { + // OP frame setup + document = {cookie: ''}; + sm = SessionManagement(document); + + // RP message setup + var clientId = 'apps_manager_js'; + var origin = 'http://relyingparty.com/somepage.html'; + var hash = sm._calculate_session_state(clientId, origin, 'theuserid', 'somesalt'); + var postMessageSpy = jasmine.createSpy('postMessageSpy'); + // The properties of this message event are described here: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage + var messageEvent = { + data: 'apps_manager_js ' + hash, + origin: origin, + source: {postMessage: postMessageSpy} + }; + var handler = sm.buildMessageHandler(origin, clientId); + + handler(messageEvent); + + expect(postMessageSpy).toHaveBeenCalledWith('changed', origin); + }); + }); + + describe("when the RP sends an invalid message format", function () { + var clientId; + var origin; + var hash; + var postMessageSpy; + var handler; + + beforeEach(function () { + // RP message setup + clientId = 'apps_manager_js'; + origin = 'http://relyingparty.com/somepage.html'; + postMessageSpy = jasmine.createSpy('postMessageSpy'); + handler = sm.buildMessageHandler(origin, clientId); + }); + + it("replies to the RP iframe with 'error'", function () { + handler({ + data: hash, + origin: origin, + source: {postMessage: postMessageSpy} + }); + expect(postMessageSpy).toHaveBeenCalledWith('error', origin); + }); + + it("replies to the RP iframe with 'error'", function () { + handler({ + data: 'clientwithnosessionstate asfd', + origin: origin, + source: {postMessage: postMessageSpy} + }); + expect(postMessageSpy).toHaveBeenCalledWith('error', origin); + }); + + it("replies to the RP iframe with 'error'", function () { + handler({ + data: 'apps_manager_js hashwithoutsalt', + origin: origin, + source: {postMessage: postMessageSpy} + }); + expect(postMessageSpy).toHaveBeenCalledWith('error', origin); + }); + + it("replies to the RP iframe with 'error'", function () { + handler({ + data: 'apps_manager_js .saltwithouthash', + origin: origin, + source: {postMessage: postMessageSpy} + }); + expect(postMessageSpy).toHaveBeenCalledWith('error', origin); + }); + }); }); }); \ No newline at end of file From 23a05c0b222cf6a3b12ec6b37f181058cbaf52dc Mon Sep 17 00:00:00 2001 From: Mikhail Vyshegorodtsev Date: Fri, 26 Jan 2018 11:08:08 -0800 Subject: [PATCH 23/42] Move session js to uaa project. [#153568058] https://www.pivotaltracker.com/story/show/153568058 Signed-off-by: Jennifer Hamon --- .gitignore | 2 +- server/build.gradle | 11 ----------- server/src/main/resources/templates/web/session.html | 4 +++- uaa/build.gradle | 11 +++++++++++ {server => uaa}/jasmine.json | 0 {server => uaa}/package-lock.json | 0 {server => uaa}/package.json | 0 .../webapp/resources/javascripts}/session/session.js | 0 .../webapp/resources/javascripts}/session/sjcl.js | 0 .../spec/helpers/jasmine_examples/SpecHelper.js | 0 .../test/javascript/spec/templates/web/SessionSpec.js | 4 ++-- 11 files changed, 17 insertions(+), 15 deletions(-) rename {server => uaa}/jasmine.json (100%) rename {server => uaa}/package-lock.json (100%) rename {server => uaa}/package.json (100%) rename {server/src/main/resources/templates/web => uaa/src/main/webapp/resources/javascripts}/session/session.js (100%) rename {server/src/main/resources/templates/web => uaa/src/main/webapp/resources/javascripts}/session/sjcl.js (100%) rename {server => uaa}/src/test/javascript/spec/helpers/jasmine_examples/SpecHelper.js (100%) rename {server => uaa}/src/test/javascript/spec/templates/web/SessionSpec.js (98%) diff --git a/.gitignore b/.gitignore index 1bb149d7618..ae48d6b8319 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,7 @@ build/ bin phantomjsdriver.log -server/node_modules +uaa/node_modules # Docs uaa/slate/package-lock.json uaa/slate/node_modules diff --git a/server/build.gradle b/server/build.gradle index b9c68869d00..89fc9659920 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -124,15 +124,4 @@ processResources { integrationTest {}.onlyIf { //disable since we don't have any true == false -} - -task npmInstall(type: Exec) { - executable 'npm' - args 'install' -} - -task jasmineTest(type: Exec) { - dependsOn npmInstall - executable 'npm' - args 'test' } \ No newline at end of file diff --git a/server/src/main/resources/templates/web/session.html b/server/src/main/resources/templates/web/session.html index 3aed98403ad..1ac15dd1c3c 100644 --- a/server/src/main/resources/templates/web/session.html +++ b/server/src/main/resources/templates/web/session.html @@ -1,7 +1,9 @@ - + + + - - - -
-
-
- - - From 9f2e2a8a0dffb16a93b2ae26ebf9e7dab4eecb2f Mon Sep 17 00:00:00 2001 From: Jennifer Hamon Date: Fri, 26 Jan 2018 14:37:44 -0800 Subject: [PATCH 25/42] Use Current-User id in generation of OIDC session_state We previously had done the calculation with JSESSIONID, but this cookie is httponly and so it cannot be read by the OP iframe javascript when comparing values sent from the RP iframe. [#153568058] https://www.pivotaltracker.com/story/show/153568058 Signed-off-by: Mikhail Vyshegorodtsev --- .../oauth/OpenIdSessionStateCalculator.java | 8 +++----- .../uaa/oauth/UaaAuthorizationEndpoint.java | 3 ++- .../OpenIdSessionStateCalculatorTest.java | 15 +++++++-------- .../oauth/UaaAuthorizationEndpointTest.java | 8 +------- .../WEB-INF/spring/openid-endpoints.xml | 4 +--- ...ationPromptNoneEntryPointMockMvcTests.java | 19 +++++++++---------- .../identity/uaa/mock/util/MockMvcUtils.java | 14 ++++++++++++++ 7 files changed, 37 insertions(+), 34 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/OpenIdSessionStateCalculator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/OpenIdSessionStateCalculator.java index 917c3affe23..a4c1f1cad13 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/OpenIdSessionStateCalculator.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/OpenIdSessionStateCalculator.java @@ -11,20 +11,18 @@ public class OpenIdSessionStateCalculator { private final Logger logger = LoggerFactory.getLogger(OpenIdSessionStateCalculator.class); - private String uaaUrl; private SecureRandom secureRandom; - public OpenIdSessionStateCalculator(String uaaUrl) { - this.uaaUrl = uaaUrl; + public OpenIdSessionStateCalculator() { this.secureRandom = new SecureRandom(); } - public String calculate(String sessionId, String clientId, String origin) { + public String calculate(String currentUserId, String clientId, String origin) { byte[] array = new byte[32]; secureRandom.nextBytes(array); String salt = DatatypeConverter.printHexBinary(array).toLowerCase(); - String text = String.format("%s %s %s %s", clientId, origin, sessionId, salt); + String text = String.format("%s %s %s %s", clientId, origin, currentUserId, salt); byte[] hash = DigestUtils.sha256(text.getBytes(StandardCharsets.UTF_8)); logger.debug(String.format("Calculated OIDC session state for clientId=%s, origin=%s, sessionId=REDACTED, salt=%s", clientId, origin, salt)); return String.format("%s.%s", DatatypeConverter.printHexBinary(hash).toLowerCase(), salt); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java index 1c951acc2f6..4e2b4139492 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java @@ -15,6 +15,7 @@ import org.apache.http.HttpHost; import org.apache.http.client.utils.URIUtils; +import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; import org.cloudfoundry.identity.uaa.oauth.token.CompositeAccessToken; import org.cloudfoundry.identity.uaa.util.UaaHttpRequestUtils; @@ -443,7 +444,7 @@ public String buildRedirectURI(AuthorizationRequest authorizationRequest, if ("none".equals(authorizationRequest.getRequestParameters().get("prompt"))) { HttpHost httpHost = URIUtils.extractHost(URI.create(requestedRedirect)); - String sessionState = openIdSessionStateCalculator.calculate(RequestContextHolder.currentRequestAttributes().getSessionId(), + String sessionState = openIdSessionStateCalculator.calculate(((UaaPrincipal) authUser.getPrincipal()).getId(), authorizationRequest.getClientId(), httpHost.toURI()); url.append("&session_state=").append(sessionState); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/OpenIdSessionStateCalculatorTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/OpenIdSessionStateCalculatorTest.java index 4d71149393d..9a50b34b43e 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/OpenIdSessionStateCalculatorTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/OpenIdSessionStateCalculatorTest.java @@ -17,8 +17,7 @@ public class OpenIdSessionStateCalculatorTest { @Before public void setup() throws Exception { - String uaaUrl = "http://localhost:8080"; - calculator = new OpenIdSessionStateCalculator(uaaUrl); + calculator = new OpenIdSessionStateCalculator(); SecureRandom secureRandom = mock(SecureRandom.class); doNothing().when(secureRandom).nextBytes(any()); calculator.setSecureRandom(secureRandom); @@ -26,19 +25,19 @@ public void setup() throws Exception { @Test public void calculate() throws Exception { - String sessionState = calculator.calculate("session_id", "client_id", "http://example.com"); - assertEquals("b6d594e481f023303f2dd9e41af3c653564b34363f6dc0b5a5555fd31d8f56b4.0000000000000000000000000000000000000000000000000000000000000000", sessionState); + String sessionState = calculator.calculate("current-user-id", "client_id", "http://example.com"); + assertEquals("3b501628aea599d810e86e06884fd5a468b91a7a1c05c5a0b7211b553ec4aa02.0000000000000000000000000000000000000000000000000000000000000000", sessionState); } @Test public void calculate_shouldChangeSessionIdChanges() { - String sessionState = calculator.calculate("session_id2", "client_id", "http://example.com"); - assertEquals("74992895f9312791755774d9ca7d175352ac7e10803631d23c5e79d228d881b4.0000000000000000000000000000000000000000000000000000000000000000", sessionState); + String sessionState = calculator.calculate("current-user-id2", "client_id", "http://example.com"); + assertEquals("8ccaa974ff0d15740285da892a1296ff4cebcf6dfcc4b76bd36e76565aadf3df.0000000000000000000000000000000000000000000000000000000000000000", sessionState); } @Test public void calculate_shouldChangeClientIdChanges() { - String sessionState = calculator.calculate("session_id", "client_id2", "http://example.com"); - assertEquals("757191b323b642b37d4975bffaafefacc0b1d0386eb97c1983a3c8d18d0d3a13.0000000000000000000000000000000000000000000000000000000000000000", sessionState); + String sessionState = calculator.calculate("current-user-id", "client_id2", "http://example.com"); + assertEquals("cfe7afa30be40cc680db7e0311b7cb559381995632477f05f66d7d88f905a6f4.0000000000000000000000000000000000000000000000000000000000000000", sessionState); } } \ No newline at end of file diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointTest.java index a1a2f191b80..db3eb6cefc8 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointTest.java @@ -11,8 +11,6 @@ import org.springframework.security.oauth2.provider.OAuth2Request; import org.springframework.security.oauth2.provider.OAuth2RequestFactory; import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices; -import org.springframework.web.context.request.RequestAttributes; -import org.springframework.web.context.request.RequestContextHolder; import java.util.Calendar; import java.util.Collections; @@ -46,11 +44,7 @@ public void setup() { uaaAuthorizationEndpoint.setOpenIdSessionStateCalculator(openIdSessionStateCalculator); responseTypes = new HashSet<>(); - RequestAttributes requestAttributeMock = mock(RequestAttributes.class); - String sessionId = "sessionid"; - when(requestAttributeMock.getSessionId()).thenReturn(sessionId); - RequestContextHolder.setRequestAttributes(requestAttributeMock, true); - when(openIdSessionStateCalculator.calculate(sessionId, null, "http://example.com")).thenReturn("opbshash"); + when(openIdSessionStateCalculator.calculate("userid", null, "http://example.com")).thenReturn("opbshash"); } diff --git a/uaa/src/main/webapp/WEB-INF/spring/openid-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/openid-endpoints.xml index a3166ebd68c..97468b03157 100755 --- a/uaa/src/main/webapp/WEB-INF/spring/openid-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/openid-endpoints.xml @@ -38,7 +38,5 @@
- - - + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java index 36a2c090648..4738bd5a443 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java @@ -13,7 +13,6 @@ import org.springframework.test.web.servlet.MvcResult; import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import java.util.Arrays; import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; @@ -21,8 +20,8 @@ import static org.hamcrest.Matchers.not; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -85,13 +84,12 @@ public void testSilentAuthentication_whenScopesNotAutoapproved() throws Exceptio } @Test - public void testSilentAuthentication_testSessionStateIsCorrect() throws Exception { - SecureRandom secureRandom = mock(SecureRandom.class); - doNothing().when(secureRandom).nextBytes(any()); - - OpenIdSessionStateCalculator sessionStateCalculator - = (OpenIdSessionStateCalculator) getWebApplicationContext().getBean("openIdSessionStateCalculator"); - sessionStateCalculator.setSecureRandom(secureRandom); + public void testSilentAuthentication_includesSessionState() throws Exception { + OpenIdSessionStateCalculator calculator = mock(OpenIdSessionStateCalculator.class); + UaaAuthorizationEndpoint uaaAuthorizationEndpoint = (UaaAuthorizationEndpoint) getWebApplicationContext().getBean("uaaAuthorizationEndpoint"); + uaaAuthorizationEndpoint.setOpenIdSessionStateCalculator(calculator); + when(calculator.calculate(anyString(), anyString(), anyString())).thenReturn("sessionhash.saltvalue"); + String currentUserId = MockMvcUtils.getUserByUsername(getMockMvc(), "marissa", adminToken).getId(); //we need to know session id when we are calculating session_state MockHttpSession session = new MockHttpSession(null, "12345") { @@ -109,7 +107,8 @@ public String changeSessionId() { .andReturn(); String redirectUrl = result.getResponse().getRedirectedUrl(); - Assert.assertThat(redirectUrl, containsString("session_state=707c310bc5aa38acc03d48a099fc999cd77f44df163178df1ca35863913f5711.0000000000000000000000000000000000000000000000000000000000000000")); + Assert.assertThat(redirectUrl, containsString("session_state=sessionhash.saltvalue")); + verify(calculator).calculate(currentUserId, "ant", "http://example.com"); } @Test diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java index 5db08320405..cfed6ffc7a1 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java @@ -100,14 +100,17 @@ import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponentsBuilder; +import javax.naming.directory.SearchResult; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.File; import java.net.URL; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -339,6 +342,17 @@ public static MockHttpSession getSavedRequestSession() { return session; } + public static ScimUser getUserByUsername(MockMvc mockMvc, String username, String accessToken) throws Exception { + MockHttpServletRequestBuilder get = get("/Users?filter=userName eq \"" + username + "\"") + .header("Authorization", "Bearer " + accessToken) + .header("Accept", APPLICATION_JSON); + MvcResult userResult = mockMvc.perform(get) + .andExpect(status().isOk()).andReturn(); + SearchResults results = JsonUtils.readValue(userResult.getResponse().getContentAsString(), + new TypeReference>(){}); + return results.getResources().get(0); + } + public static class MockSavedRequest extends DefaultSavedRequest { public MockSavedRequest() { From a14ada80f1a1a9f95f793f0330dd4bfda36a93c2 Mon Sep 17 00:00:00 2001 From: Mikhail Vyshegorodtsev Date: Fri, 26 Jan 2018 14:55:17 -0800 Subject: [PATCH 26/42] Run oidc javascript tests on Travis [#153568058] https://www.pivotaltracker.com/story/show/153568058 Signed-off-by: Jennifer Hamon --- .travis.yml | 9 +++++++++ .../identity/uaa/oauth/UaaAuthorizationEndpoint.java | 1 - .../AuthorizationPromptNoneEntryPointMockMvcTests.java | 1 - .../identity/uaa/mock/util/MockMvcUtils.java | 3 --- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index e602077dda1..b5d33f4e504 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,15 @@ matrix: env: - TESTENV=sqlserver,default - TEST_COMMAND=cloudfoundry-identity-server:test + - os: linux + dist: trusty + jdk: oraclejdk8 + sudo: required + group: deprecated-2017Q4 + language: java + env: + - TESTENV=default + - TEST_COMMAND=jasmineTest - os: linux dist: trusty jdk: oraclejdk8 diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java index 4e2b4139492..616245bb602 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java @@ -68,7 +68,6 @@ import org.springframework.web.bind.support.DefaultSessionAttributeStore; import org.springframework.web.bind.support.SessionAttributeStore; import org.springframework.web.bind.support.SessionStatus; -import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.View; diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java index 4738bd5a443..5de49e986e5 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/oauth/AuthorizationPromptNoneEntryPointMockMvcTests.java @@ -18,7 +18,6 @@ import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java index cfed6ffc7a1..e84fbcfccff 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java @@ -100,17 +100,14 @@ import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponentsBuilder; -import javax.naming.directory.SearchResult; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.File; import java.net.URL; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; From 7ab5a6e89324ece2a23694626250cbe03160b388 Mon Sep 17 00:00:00 2001 From: Jennifer Hamon Date: Fri, 26 Jan 2018 17:34:37 -0800 Subject: [PATCH 27/42] Revert "Remove IT test for session because it's tested on JS side" This reverts commit c8a23c3f2216fb9d8b8c90ae511944551fdf4d0b. Restoring these tests because we need to preserve the old session.html behavior to maintain backwards compatibility with old uaa-singulars that don't support session management. With the introduction of session management, the string being passed from the RP iframe to OP iframe will change in a non-backwards compatible way so UAA needs to provide iframe content for both formats. [#153568058] https://www.pivotaltracker.com/story/show/153568058 Signed-off-by: Mikhail Vyshegorodtsev --- .../integration/feature/SessionPageIT.java | 138 ++++++++++++++++++ .../test/resources/session_frame_test.html | 56 +++++++ 2 files changed, 194 insertions(+) create mode 100644 uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SessionPageIT.java create mode 100644 uaa/src/test/resources/session_frame_test.html diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SessionPageIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SessionPageIT.java new file mode 100644 index 00000000000..ad4de2f09e4 --- /dev/null +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/SessionPageIT.java @@ -0,0 +1,138 @@ +package org.cloudfoundry.identity.uaa.integration.feature; + +import org.cloudfoundry.identity.uaa.login.CurrentUserInformation; +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.openqa.selenium.By; +import org.openqa.selenium.Cookie; +import org.openqa.selenium.WebDriver; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.oauth2.client.test.OAuth2ContextConfiguration; +import org.springframework.security.oauth2.client.test.TestAccounts; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = DefaultIntegrationTestConfig.class) +@OAuth2ContextConfiguration(OAuth2ContextConfiguration.ClientCredentials.class) +public class SessionPageIT { + + @Autowired + WebDriver webDriver; + + @Value("${integration.test.base_url}") + String baseUrl; + + @Autowired + TestAccounts testAccounts; + + String testPage; + + @Before + public void setUp() throws UnsupportedEncodingException { + testPage = "file://" + System.getProperty("user.dir") + "/src/test/resources/session_frame_test.html#~"; + } + + @After + public void tearDown(){ + doLogout(); + } + + @Test + public void testFrameReportsChangedWhenNoUser_whenLoggedIn() throws UnsupportedEncodingException, InterruptedException { + doLogin(); + webDriver.get(testPage); + webDriver.findElement(By.id("noUser")).click(); + assertMessage("changed"); + } + + @Test + public void testFrameReportsUnchangedWhenSendingSameUser_whenLoggedIn() throws UnsupportedEncodingException, InterruptedException { + doLogin(); + webDriver.get(testPage); + webDriver.findElement(By.id("sameUser")).click(); + assertMessage("unchanged"); + } + + @Test + public void testFrameReportsUnchangedWhenSendingDifferentUser_whenLoggedIn() throws UnsupportedEncodingException, InterruptedException { + doLogin(); + webDriver.get(testPage); + webDriver.findElement(By.id("differentUser")).click(); + assertMessage("changed"); + } + + @Test + public void testFrameReportsErrorWhenSendingDifferentUser_whenLoggedIn() throws UnsupportedEncodingException, InterruptedException { + doLogin(); + webDriver.get(testPage); + webDriver.findElement(By.id("wrongClient")).click(); + assertMessage("error"); + } + + @Test + public void testFrameReportsChangedWhenNoUser_whenLoggedOut() throws UnsupportedEncodingException, InterruptedException { + webDriver.get(testPage); + webDriver.findElement(By.id("noUser")).click(); + assertMessage("unchanged"); + } + + @Test + public void testFrameReportsChangedWhenSameUser_whenLoggedOut() throws UnsupportedEncodingException, InterruptedException { + webDriver.get(testPage); + webDriver.findElement(By.id("sameUser")).click(); + assertMessage("unchanged"); + } + + @Test + public void testFrameReportsChangedWhenDifferentUser_whenLoggedOut() throws UnsupportedEncodingException, InterruptedException { + webDriver.get(testPage); + webDriver.findElement(By.id("differentUser")).click(); + assertMessage("changed"); + } + + @Test + public void testFrameReportsErrorWhenSendingDifferentUser_whenLoggedOut() throws UnsupportedEncodingException, InterruptedException { + webDriver.get(testPage); + webDriver.findElement(By.id("wrongClient")).click(); + assertMessage("error"); + } + + @Test + public void testAmountOfJavascriptTests() { + webDriver.get(testPage); + assertEquals(4, webDriver.findElements(By.cssSelector("#testLinks li")).size()); + } + + private void assertMessage(String expected) { + assertEquals(expected, webDriver.findElement(By.id("message")).getText()); + } + + private void doLogin() throws UnsupportedEncodingException { + + webDriver.get(baseUrl + "/login"); + webDriver.findElement(By.name("username")).sendKeys(testAccounts.getUserName()); + webDriver.findElement(By.name("password")).sendKeys(testAccounts.getPassword()); + webDriver.findElement(By.xpath("//input[@value='Sign in']")).click(); + + Cookie currentUserCookie = webDriver.manage().getCookieNamed("Current-User"); + CurrentUserInformation currentUserInformation = JsonUtils.readValue(URLDecoder.decode(currentUserCookie.getValue(), "UTF-8"), CurrentUserInformation.class); + + String userId = currentUserInformation.getUserId(); + testPage = "file://" + System.getProperty("user.dir") + "/src/test/resources/session_frame_test.html#" + userId; + } + + private void doLogout() { + webDriver.get(baseUrl + "/logout.do"); + webDriver.manage().deleteAllCookies(); + } +} diff --git a/uaa/src/test/resources/session_frame_test.html b/uaa/src/test/resources/session_frame_test.html new file mode 100644 index 00000000000..76a5042eb98 --- /dev/null +++ b/uaa/src/test/resources/session_frame_test.html @@ -0,0 +1,56 @@ + + + + + + + +
+
+
+ + + From 617d923b8f208c77af7b495c15a6a6708539c283 Mon Sep 17 00:00:00 2001 From: pivotal Date: Fri, 26 Jan 2018 17:52:39 -0800 Subject: [PATCH 28/42] Restore session.html behavior for uaa-singular backwards compatibility [#153568058] https://www.pivotaltracker.com/story/show/153568058 --- .../identity/uaa/login/SessionController.java | 8 ++++ .../main/resources/templates/web/session.html | 44 +++++++++++++++---- .../templates/web/session_management.html | 19 ++++++++ .../login/SessionControllerMockMvcTests.java | 11 ++++- 4 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 server/src/main/resources/templates/web/session_management.html diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/login/SessionController.java b/server/src/main/java/org/cloudfoundry/identity/uaa/login/SessionController.java index 5e3008888f6..f635cde8558 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/login/SessionController.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/login/SessionController.java @@ -22,9 +22,17 @@ public class SessionController { @RequestMapping("/session") public String session(Model model, @RequestParam String clientId, @RequestParam String messageOrigin) { + // We need to maintain this version of the session page to continue compatibility with the + // original version of uaa-singular. model.addAttribute("clientId", clientId); model.addAttribute("messageOrigin", messageOrigin); return "session"; } + @RequestMapping("/session_management") + public String sessionManagement(Model model, @RequestParam String clientId, @RequestParam String messageOrigin) { + model.addAttribute("clientId", clientId); + model.addAttribute("messageOrigin", messageOrigin); + return "session_management"; + } } diff --git a/server/src/main/resources/templates/web/session.html b/server/src/main/resources/templates/web/session.html index 1ac15dd1c3c..e5d9e2679d0 100644 --- a/server/src/main/resources/templates/web/session.html +++ b/server/src/main/resources/templates/web/session.html @@ -1,19 +1,45 @@ - - - diff --git a/server/src/main/resources/templates/web/session_management.html b/server/src/main/resources/templates/web/session_management.html new file mode 100644 index 00000000000..7d6973b27db --- /dev/null +++ b/server/src/main/resources/templates/web/session_management.html @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/SessionControllerMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/SessionControllerMockMvcTests.java index 5cab67027fe..2e7bcf7c2af 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/SessionControllerMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/SessionControllerMockMvcTests.java @@ -9,11 +9,20 @@ public class SessionControllerMockMvcTests extends InjectedMockContextTest { @Test - public void sessionControllerReturnsSessionView() throws Exception { + public void legacy_sessionControllerReturnsSessionView() throws Exception { getMockMvc().perform(get("/session") .param("clientId","1") .param("messageOrigin", "origin")) .andExpect(view().name("session")) .andExpect(status().isOk()); } + + @Test + public void sessionManagement_ReturnsSessionManagementView() throws Exception { + getMockMvc().perform(get("/session_management") + .param("clientId","1") + .param("messageOrigin", "origin")) + .andExpect(view().name("session_management")) + .andExpect(status().isOk()); + } } From f7e63c8ae0c61e57cd78e5067019213c09df72ed Mon Sep 17 00:00:00 2001 From: Mikhail Vyshegorodtsev Date: Mon, 29 Jan 2018 10:04:49 -0800 Subject: [PATCH 29/42] Version session.html page for singular [#153568058] https://www.pivotaltracker.com/story/show/153568058 Signed-off-by: Jennifer Hamon --- .../org/cloudfoundry/identity/uaa/login/SessionController.java | 2 +- uaa/src/main/webapp/WEB-INF/spring-servlet.xml | 1 + .../identity/uaa/login/SessionControllerMockMvcTests.java | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/login/SessionController.java b/server/src/main/java/org/cloudfoundry/identity/uaa/login/SessionController.java index f635cde8558..e82f8d7efac 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/login/SessionController.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/login/SessionController.java @@ -29,7 +29,7 @@ public String session(Model model, @RequestParam String clientId, @RequestParam return "session"; } - @RequestMapping("/session_management") + @RequestMapping("/v2/session") public String sessionManagement(Model model, @RequestParam String clientId, @RequestParam String messageOrigin) { model.addAttribute("clientId", clientId); model.addAttribute("messageOrigin", messageOrigin); diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index 19f9df91960..85c35f2919d 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -111,6 +111,7 @@ + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/SessionControllerMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/SessionControllerMockMvcTests.java index 2e7bcf7c2af..7263ee18de2 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/SessionControllerMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/SessionControllerMockMvcTests.java @@ -19,7 +19,7 @@ public void legacy_sessionControllerReturnsSessionView() throws Exception { @Test public void sessionManagement_ReturnsSessionManagementView() throws Exception { - getMockMvc().perform(get("/session_management") + getMockMvc().perform(get("/v2/session") .param("clientId","1") .param("messageOrigin", "origin")) .andExpect(view().name("session_management")) From 830b94f18dba7f49472317aca0bb6e1e99976d58 Mon Sep 17 00:00:00 2001 From: Mikhail Vyshegorodtsev Date: Mon, 29 Jan 2018 12:55:24 -0800 Subject: [PATCH 30/42] These default properties are actually needed for backwards compatibility We don't want to break any deployments that were relying on these defaults by making them required. Revert "Stop defaulting uaa url to localhost if property is missing" This reverts commit 1904dd29e67146deb5d267018244584a7d4f1fec. Signed-off-by: Jennifer Hamon --- uaa/src/main/webapp/WEB-INF/spring-servlet.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index 85c35f2919d..4b1592a0888 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -314,11 +314,11 @@ - + - + From 7db12e09214a7cb06f4ed8be6af661a9df393154 Mon Sep 17 00:00:00 2001 From: Jennifer Hamon Date: Mon, 29 Jan 2018 14:00:00 -0800 Subject: [PATCH 31/42] Run jasmine tests on Travis [#153568058] https://www.pivotaltracker.com/story/show/153568058 --- .travis.yml | 14 +----- uaa/package-lock.json | 99 +++++++++++++++++++++++++++++++++++++++++++ uaa/package.json | 3 +- 3 files changed, 103 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index b5d33f4e504..96dbde0ed27 100644 --- a/.travis.yml +++ b/.travis.yml @@ -176,6 +176,8 @@ install: - if [ "$TESTENV" = "keystone,default" ]; then ./scripts/keystone/configure-manifest.sh; fi - mkdir -p $HOME/build/cloudfoundry/uaa/uaa/build/reports/tests - sudo apt-get -qy install lsof +- nvm install node +- nvm use node script: - sudo lsof -i :33389 || echo "Nothing listening on port 33389" - sudo lsof -i :33636 || echo "Nothing listening on port 33636" @@ -187,18 +189,6 @@ after_success: - for i in $(find $HOME/build/cloudfoundry/uaa/ -name reports -type d); do rm -rf $i; done - /bin/df -h - /usr/bin/du -sh * -#- python scripts/travis/travis_after_all.py -#- export $(cat .to_export_back) -#- ! "if [ \"$BUILD_LEADER\" = \"YES\" ]; then\n if [ \"$BUILD_AGGREGATE_STATUS\" -# = \"others_succeeded\" ]; then\n echo \"All Succeeded!\"\n else\n echo \"Some Failed\"\n fi\nfi\n" -#after_failure: -#- python scripts/travis/travis_after_all.py -#- export $(cat .to_export_back) -#- ! "if [ \"$BUILD_LEADER\" = \"YES\" ]; then\n if [ \"$BUILD_AGGREGATE_STATUS\" -# = \"others_failed\" ]; then\n echo \"All Failed\"\n else\n echo \"Some Failed\"\n -# \ fi\nfi\n" -#after_script: -#- echo leader=$BUILD_LEADER status=$BUILD_AGGREGATE_STATUS after_failure: - /bin/df -h diff --git a/uaa/package-lock.json b/uaa/package-lock.json index 6cf81a2a5f2..ab0edb2141b 100644 --- a/uaa/package-lock.json +++ b/uaa/package-lock.json @@ -4,10 +4,109 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "jasmine": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.9.0.tgz", + "integrity": "sha1-dlcfklyHg0CefGFTVy5aY0HPk+s=", + "requires": { + "exit": "0.1.2", + "glob": "7.1.2", + "jasmine-core": "2.9.1" + }, + "dependencies": { + "jasmine-core": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.9.1.tgz", + "integrity": "sha1-trvB2OZSUNVvWIhGFwXr7uuI8i8=" + } + } + }, "jasmine-core": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz", "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.8" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" } } } diff --git a/uaa/package.json b/uaa/package.json index 8b4da01bf64..0a39ac28499 100644 --- a/uaa/package.json +++ b/uaa/package.json @@ -8,11 +8,12 @@ "lib": "lib" }, "scripts": { - "test": "jasmine --config=jasmine.json" + "test": "node_modules/jasmine/bin/jasmine.js --config=jasmine.json" }, "author": "CloudFoundry", "license": "Apache-2.0", "dependencies": { + "jasmine": "^2.9.0", "jasmine-core": "2.8.0" } } From 68df6240e3b9c235038b533b974e29504f69ad28 Mon Sep 17 00:00:00 2001 From: Jennifer Hamon Date: Wed, 31 Jan 2018 16:01:05 -0800 Subject: [PATCH 32/42] Go back to /session_management url for now Returning this page from the nested url containing /v2/ was causing some unexpected issues with the rendering of relative links inside the thymeleaf template [#153568058] https://www.pivotaltracker.com/story/show/153568058 --- .../org/cloudfoundry/identity/uaa/login/SessionController.java | 2 +- uaa/src/main/webapp/WEB-INF/spring-servlet.xml | 2 +- .../identity/uaa/login/SessionControllerMockMvcTests.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/login/SessionController.java b/server/src/main/java/org/cloudfoundry/identity/uaa/login/SessionController.java index e82f8d7efac..f635cde8558 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/login/SessionController.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/login/SessionController.java @@ -29,7 +29,7 @@ public String session(Model model, @RequestParam String clientId, @RequestParam return "session"; } - @RequestMapping("/v2/session") + @RequestMapping("/session_management") public String sessionManagement(Model model, @RequestParam String clientId, @RequestParam String messageOrigin) { model.addAttribute("clientId", clientId); model.addAttribute("messageOrigin", messageOrigin); diff --git a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml index 4b1592a0888..2fd9d7d3c62 100755 --- a/uaa/src/main/webapp/WEB-INF/spring-servlet.xml +++ b/uaa/src/main/webapp/WEB-INF/spring-servlet.xml @@ -111,7 +111,7 @@ - + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/SessionControllerMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/SessionControllerMockMvcTests.java index 7263ee18de2..2e7bcf7c2af 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/SessionControllerMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/SessionControllerMockMvcTests.java @@ -19,7 +19,7 @@ public void legacy_sessionControllerReturnsSessionView() throws Exception { @Test public void sessionManagement_ReturnsSessionManagementView() throws Exception { - getMockMvc().perform(get("/v2/session") + getMockMvc().perform(get("/session_management") .param("clientId","1") .param("messageOrigin", "origin")) .andExpect(view().name("session_management")) From a6ddfffdde18805ea30ad3b10c2e259129dceb1b Mon Sep 17 00:00:00 2001 From: Jennifer Hamon Date: Wed, 31 Jan 2018 17:26:10 -0800 Subject: [PATCH 33/42] Return session_state when silent authorize results in login_required We need to return session_state even when the user is not logged in to prevent uaa-singular from getting stuck in an infinite loop of token requests. Without session_state, uaa-singular's RP iframe cannot find out from the OP iframe whether a change has occurred that would trigger it to request a new token. [#153568058] https://www.pivotaltracker.com/story/show/153568058 --- .../oauth/AuthorizePromptNoneEntryPoint.java | 20 +++++++++++-- .../AuthorizePromptNoneEntryPointTest.java | 4 ++- .../webapp/WEB-INF/spring/oauth-endpoints.xml | 1 + ...ationPromptNoneEntryPointMockMvcTests.java | 30 +++++++++++++++++-- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPoint.java index 03d50709f78..8475af42cda 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPoint.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPoint.java @@ -17,6 +17,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpHost; +import org.apache.http.client.utils.URIUtils; import org.cloudfoundry.identity.uaa.zone.ClientServicesExtension; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.http.HttpStatus; @@ -33,6 +35,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.net.URI; import java.util.Set; import static java.util.Arrays.stream; @@ -50,13 +53,16 @@ public class AuthorizePromptNoneEntryPoint implements AuthenticationEntryPoint { private final AuthenticationFailureHandler failureHandler; private final ClientServicesExtension clientDetailsService; private final RedirectResolver redirectResolver; + private final OpenIdSessionStateCalculator openIdSessionStateCalculator; public AuthorizePromptNoneEntryPoint(AuthenticationFailureHandler failureHandler, ClientServicesExtension clientDetailsService, - RedirectResolver redirectResolver) { + RedirectResolver redirectResolver, + OpenIdSessionStateCalculator openIdSessionStateCalculator) { this.failureHandler = failureHandler; this.clientDetailsService = clientDetailsService; this.redirectResolver = redirectResolver; + this.openIdSessionStateCalculator = openIdSessionStateCalculator; } @Override @@ -100,8 +106,18 @@ public void commence(HttpServletRequest request, HttpServletResponse response, A } failureHandler.onAuthenticationFailure(request, response, authException); + HttpHost httpHost = URIUtils.extractHost(URI.create(resolvedRedirect)); + String sessionState = openIdSessionStateCalculator.calculate("", clientId, httpHost.toURI()); boolean implicit = stream(responseTypes).noneMatch("code"::equalsIgnoreCase); - String redirectLocation = implicit ? addFragmentComponent(resolvedRedirect, "error=login_required") : addQueryParameter(resolvedRedirect, "error", "login_required"); + String redirectLocation; + if (implicit) { + redirectLocation = addFragmentComponent(resolvedRedirect, "error=login_required"); + redirectLocation = addFragmentComponent(redirectLocation, "session_state="+sessionState); + } else { + redirectLocation = addQueryParameter(resolvedRedirect, "error", "login_required"); + redirectLocation = addQueryParameter(redirectLocation, "session_state", sessionState); + } + response.sendRedirect(redirectLocation); } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPointTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPointTest.java index ca5d181d004..8c0b9fe67ba 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPointTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPointTest.java @@ -61,6 +61,7 @@ public class AuthorizePromptNoneEntryPointTest { private MockHttpServletResponse response; private ClientServicesExtension clientDetailsService; private RedirectResolver redirectResolver; + private OpenIdSessionStateCalculator calculator; private final String responseType; private final String redirectHash; @@ -92,13 +93,14 @@ public void setup() { clientDetailsService = mock(ClientServicesExtension.class); redirectResolver = mock(RedirectResolver.class); failureHandler = mock(AuthenticationFailureHandler.class); + calculator = mock(OpenIdSessionStateCalculator.class); String zoneID = IdentityZoneHolder.get().getId(); when(clientDetailsService.loadClientByClientId(eq(client.getClientId()), eq(zoneID))).thenReturn(client); when(redirectResolver.resolveRedirect(eq(redirectUrl), same(client))).thenReturn(redirectUrl); when(redirectResolver.resolveRedirect(eq(HTTP_SOME_OTHER_SITE_CALLBACK), same(client))).thenThrow(new RedirectMismatchException("")); - entryPoint = new AuthorizePromptNoneEntryPoint(failureHandler, clientDetailsService, redirectResolver); + entryPoint = new AuthorizePromptNoneEntryPoint(failureHandler, clientDetailsService, redirectResolver, calculator); request = new MockHttpServletRequest("GET", "/oauth/authorize"); request.setParameter(OAuth2Utils.CLIENT_ID, client.getClientId()); diff --git a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml index d37f5ac5867..b0a94c00f69 100755 --- a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml @@ -364,6 +364,7 @@ + Date: Thu, 1 Feb 2018 11:33:29 -0800 Subject: [PATCH 34/42] Removed conditional test assertion in AuthorizePromptNoneEntryPointTest [#153568058] https://www.pivotaltracker.com/story/show/153568058 Signed-off-by: Dennis Leon --- .../uaa/oauth/AuthorizePromptNoneEntryPointTest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPointTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPointTest.java index 8c0b9fe67ba..6fadbecf570 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPointTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPointTest.java @@ -38,6 +38,7 @@ import java.util.Collections; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.same; @@ -46,7 +47,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.springframework.util.StringUtils.hasText; @RunWith(Parameterized.class) public class AuthorizePromptNoneEntryPointTest { @@ -99,6 +99,7 @@ public void setup() { when(clientDetailsService.loadClientByClientId(eq(client.getClientId()), eq(zoneID))).thenReturn(client); when(redirectResolver.resolveRedirect(eq(redirectUrl), same(client))).thenReturn(redirectUrl); when(redirectResolver.resolveRedirect(eq(HTTP_SOME_OTHER_SITE_CALLBACK), same(client))).thenThrow(new RedirectMismatchException("")); + when(calculator.calculate(anyString(), anyString(), anyString())).thenReturn("sessionstate.salt"); entryPoint = new AuthorizePromptNoneEntryPoint(failureHandler, clientDetailsService, redirectResolver, calculator); @@ -142,9 +143,7 @@ public void test_redirect_contains_error() throws Exception { request.setParameter(OAuth2Utils.REDIRECT_URI, redirectUrl); entryPoint.commence(request, response, authException); assertEquals(HttpStatus.FOUND.value(), response.getStatus()); - String paramValue = "error=login_required"; - String expectedRedirect = REDIRECT_URI + (responseType.equals("code") ? "&" + paramValue + redirectHash : (hasText(redirectHash) ? redirectHash + "&" : "#") + paramValue); - assertEquals(expectedRedirect, response.getHeader("Location")); + assertTrue(response.getHeader("Location").contains("error=login_required")); } @Test From 3f404f7f4209f63ae551d494eb715db774f0e7dc Mon Sep 17 00:00:00 2001 From: Dennis Leon Date: Thu, 1 Feb 2018 17:04:17 -0800 Subject: [PATCH 35/42] Unified /oauth/authorize handling into single class [#153568058] https://www.pivotaltracker.com/story/show/153568058 Signed-off-by: Jennifer Hamon --- .../oauth/AuthorizePromptNoneEntryPoint.java | 123 ------------------ .../uaa/oauth/UaaAuthorizationEndpoint.java | 66 +++++++++- ...thorizationEndpointParamaterizedTest.java} | 60 +++------ .../webapp/WEB-INF/spring/oauth-endpoints.xml | 15 +-- ...ationPromptNoneEntryPointMockMvcTests.java | 109 ++++++++++------ 5 files changed, 147 insertions(+), 226 deletions(-) delete mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPoint.java rename server/src/test/java/org/cloudfoundry/identity/uaa/oauth/{AuthorizePromptNoneEntryPointTest.java => UaaAuthorizationEndpointParamaterizedTest.java} (74%) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPoint.java deleted file mode 100644 index 8475af42cda..00000000000 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPoint.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * **************************************************************************** - * Cloud Foundry - * Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved. - * - * This product is licensed to you under the Apache License, Version 2.0 (the "License"). - * You may not use this product except in compliance with the License. - * - * This product includes a number of subcomponents with - * separate copyright notices and license terms. Your use of these - * subcomponents is subject to the terms and conditions of the - * subcomponent's license, as noted in the LICENSE file. - * **************************************************************************** - */ - -package org.cloudfoundry.identity.uaa.oauth; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.http.HttpHost; -import org.apache.http.client.utils.URIUtils; -import org.cloudfoundry.identity.uaa.zone.ClientServicesExtension; -import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; -import org.springframework.http.HttpStatus; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.oauth2.common.exceptions.RedirectMismatchException; -import org.springframework.security.oauth2.common.util.OAuth2Utils; -import org.springframework.security.oauth2.provider.ClientDetails; -import org.springframework.security.oauth2.provider.ClientRegistrationException; -import org.springframework.security.oauth2.provider.endpoint.RedirectResolver; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.net.URI; -import java.util.Set; - -import static java.util.Arrays.stream; -import static java.util.Collections.EMPTY_SET; -import static java.util.Optional.ofNullable; -import static org.cloudfoundry.identity.uaa.util.UaaUrlUtils.addFragmentComponent; -import static org.cloudfoundry.identity.uaa.util.UaaUrlUtils.addQueryParameter; -import static org.springframework.util.StringUtils.hasText; - - -public class AuthorizePromptNoneEntryPoint implements AuthenticationEntryPoint { - - private static Log logger = LogFactory.getLog(AuthorizePromptNoneEntryPoint.class); - - private final AuthenticationFailureHandler failureHandler; - private final ClientServicesExtension clientDetailsService; - private final RedirectResolver redirectResolver; - private final OpenIdSessionStateCalculator openIdSessionStateCalculator; - - public AuthorizePromptNoneEntryPoint(AuthenticationFailureHandler failureHandler, - ClientServicesExtension clientDetailsService, - RedirectResolver redirectResolver, - OpenIdSessionStateCalculator openIdSessionStateCalculator) { - this.failureHandler = failureHandler; - this.clientDetailsService = clientDetailsService; - this.redirectResolver = redirectResolver; - this.openIdSessionStateCalculator = openIdSessionStateCalculator; - } - - @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { - String clientId = request.getParameter(OAuth2Utils.CLIENT_ID); - String redirectUri = request.getParameter(OAuth2Utils.REDIRECT_URI); - String[] responseTypes = ofNullable(request.getParameter(OAuth2Utils.RESPONSE_TYPE)).map(rt -> rt.split(" ")).orElse(new String[0]); - - //client_id is a required parameter - if (!hasText(clientId)) { - logger.debug("[prompt=none] Missing client_id parameter"); - response.setStatus(HttpStatus.BAD_REQUEST.value()); - return; - } - - ClientDetails client; - try { - client = clientDetailsService.loadClientByClientId(clientId, IdentityZoneHolder.get().getId()); - } catch (ClientRegistrationException e) { - logger.debug("[prompt=none] Unable to look up client for client_id="+clientId, e); - response.setStatus(HttpStatus.BAD_REQUEST.value()); - return; - } - - Set redirectUris = ofNullable(client.getRegisteredRedirectUri()).orElse(EMPTY_SET); - - //if the client doesn't have a redirect uri set, the parameter is required. - if (redirectUris.size()==0 && !hasText(redirectUri)) { - logger.debug("[prompt=none] Missing redirect_uri"); - response.setStatus(HttpStatus.BAD_REQUEST.value()); - return; - } - - String resolvedRedirect; - try { - resolvedRedirect = redirectResolver.resolveRedirect(redirectUri, client); - } catch (RedirectMismatchException rme) { - logger.debug("[prompt=none] Invalid redirect " + redirectUri + " did not match one of the registered values"); - response.setStatus(HttpStatus.BAD_REQUEST.value()); - return; - } - - failureHandler.onAuthenticationFailure(request, response, authException); - HttpHost httpHost = URIUtils.extractHost(URI.create(resolvedRedirect)); - String sessionState = openIdSessionStateCalculator.calculate("", clientId, httpHost.toURI()); - boolean implicit = stream(responseTypes).noneMatch("code"::equalsIgnoreCase); - String redirectLocation; - if (implicit) { - redirectLocation = addFragmentComponent(resolvedRedirect, "error=login_required"); - redirectLocation = addFragmentComponent(redirectLocation, "session_state="+sessionState); - } else { - redirectLocation = addQueryParameter(resolvedRedirect, "error", "login_required"); - redirectLocation = addQueryParameter(redirectLocation, "session_state", sessionState); - } - - response.sendRedirect(redirectLocation); - } -} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java index 616245bb602..96d6aff9ac0 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java @@ -19,7 +19,6 @@ import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; import org.cloudfoundry.identity.uaa.oauth.token.CompositeAccessToken; import org.cloudfoundry.identity.uaa.util.UaaHttpRequestUtils; -import org.cloudfoundry.identity.uaa.util.UaaUrlUtils; import org.cloudfoundry.identity.uaa.zone.ClientServicesExtension; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.http.HttpStatus; @@ -56,6 +55,7 @@ import org.springframework.security.oauth2.provider.endpoint.RedirectResolver; import org.springframework.security.oauth2.provider.implicit.ImplicitTokenRequest; import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestValidator; +import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.util.UrlUtils; import org.springframework.stereotype.Controller; import org.springframework.util.StringUtils; @@ -75,8 +75,10 @@ import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriUtils; +import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.security.Principal; @@ -88,6 +90,13 @@ import java.util.Map; import java.util.Set; +import static java.util.Arrays.stream; +import static java.util.Collections.EMPTY_SET; +import static java.util.Optional.ofNullable; +import static org.cloudfoundry.identity.uaa.util.JsonUtils.hasText; +import static org.cloudfoundry.identity.uaa.util.UaaUrlUtils.addFragmentComponent; +import static org.cloudfoundry.identity.uaa.util.UaaUrlUtils.addQueryParameter; + /** * Authorization endpoint that returns id_token's if requested. * This is a copy of AuthorizationEndpoint.java in @@ -97,7 +106,7 @@ */ @Controller @SessionAttributes("authorizationRequest") -public class UaaAuthorizationEndpoint extends AbstractEndpoint { +public class UaaAuthorizationEndpoint extends AbstractEndpoint implements AuthenticationEntryPoint { private AuthorizationCodeServices authorizationCodeServices = new InMemoryAuthorizationCodeServices(); @@ -209,7 +218,7 @@ public ModelAndView authorize(Map model, if ("none".equals(authorizationRequest.getRequestParameters().get("prompt"))) { return new ModelAndView( - new RedirectView(UaaUrlUtils.addFragmentComponent(resolvedRedirect, "error=interaction_required")) + new RedirectView(addFragmentComponent(resolvedRedirect, "error=interaction_required")) ); } else { // Place auth request into the model so that it is stored in the session @@ -227,7 +236,7 @@ public ModelAndView authorize(Map model, if ("none".equals(authorizationRequest.getRequestParameters().get("prompt"))) { return new ModelAndView( - new RedirectView(UaaUrlUtils.addFragmentComponent(resolvedRedirect, "error=internal_server_error")) + new RedirectView(addFragmentComponent(resolvedRedirect, "error=internal_server_error")) ); } @@ -236,6 +245,55 @@ public ModelAndView authorize(Map model, } + // This method handles /oauth/authorize calls when user is not logged in and the prompt=none param is used + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + String clientId = request.getParameter(OAuth2Utils.CLIENT_ID); + String redirectUri = request.getParameter(OAuth2Utils.REDIRECT_URI); + String[] responseTypes = ofNullable(request.getParameter(OAuth2Utils.RESPONSE_TYPE)).map(rt -> rt.split(" ")).orElse(new String[0]); + + ClientDetails client; + try { + client = getClientServiceExtention().loadClientByClientId(clientId, IdentityZoneHolder.get().getId()); + } catch (ClientRegistrationException e) { + logger.debug("[prompt=none] Unable to look up client for client_id=" + clientId, e); + response.setStatus(HttpStatus.BAD_REQUEST.value()); + return; + } + + Set redirectUris = ofNullable(client.getRegisteredRedirectUri()).orElse(EMPTY_SET); + + //if the client doesn't have a redirect uri set, the parameter is required. + if (redirectUris.size() == 0 && !hasText(redirectUri)) { + logger.debug("[prompt=none] Missing redirect_uri"); + response.setStatus(HttpStatus.BAD_REQUEST.value()); + return; + } + + String resolvedRedirect; + try { + resolvedRedirect = redirectResolver.resolveRedirect(redirectUri, client); + } catch (RedirectMismatchException rme) { + logger.debug("[prompt=none] Invalid redirect " + redirectUri + " did not match one of the registered values"); + response.setStatus(HttpStatus.BAD_REQUEST.value()); + return; + } + + HttpHost httpHost = URIUtils.extractHost(URI.create(resolvedRedirect)); + String sessionState = openIdSessionStateCalculator.calculate("", clientId, httpHost.toURI()); + boolean implicit = stream(responseTypes).noneMatch("code"::equalsIgnoreCase); + String redirectLocation; + if (implicit) { + redirectLocation = addFragmentComponent(resolvedRedirect, "error=login_required"); + redirectLocation = addFragmentComponent(redirectLocation, "session_state=" + sessionState); + } else { + redirectLocation = addQueryParameter(resolvedRedirect, "error", "login_required"); + redirectLocation = addQueryParameter(redirectLocation, "session_state", sessionState); + } + + response.sendRedirect(redirectLocation); + } + private ModelAndView switchIdp(Map model, ClientDetails client, String clientId, HttpServletRequest request) { Map additionalInfo = client.getAdditionalInformation(); String clientDisplayName = (String) additionalInfo.get(ClientConstants.CLIENT_NAME); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPointTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointParamaterizedTest.java similarity index 74% rename from server/src/test/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPointTest.java rename to server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointParamaterizedTest.java index 6fadbecf570..f61fc2b9fd4 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/AuthorizePromptNoneEntryPointTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpointParamaterizedTest.java @@ -29,8 +29,6 @@ import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.security.oauth2.provider.endpoint.RedirectResolver; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.session.SessionAuthenticationException; import java.util.Arrays; @@ -44,18 +42,15 @@ import static org.mockito.Matchers.same; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(Parameterized.class) -public class AuthorizePromptNoneEntryPointTest { +public class UaaAuthorizationEndpointParamaterizedTest { private static final String REDIRECT_URI = "http://sub.domain.com/callback?oauth=true"; private static final String HTTP_SOME_OTHER_SITE_CALLBACK = "http://some.other.site/callback"; private final SessionAuthenticationException authException = new SessionAuthenticationException(""); - private AuthenticationFailureHandler failureHandler; - private AuthenticationEntryPoint entryPoint; + private UaaAuthorizationEndpoint uaaAuthorizationEndpoint; private BaseClientDetails client; private MockHttpServletRequest request; private MockHttpServletResponse response; @@ -64,26 +59,20 @@ public class AuthorizePromptNoneEntryPointTest { private OpenIdSessionStateCalculator calculator; private final String responseType; - private final String redirectHash; private final String redirectUrl; - public AuthorizePromptNoneEntryPointTest(String responseType, String redirectHash) { + public UaaAuthorizationEndpointParamaterizedTest(String responseType) { this.responseType = responseType; - this.redirectHash = redirectHash; - redirectUrl = REDIRECT_URI + redirectHash; + redirectUrl = REDIRECT_URI; } - @Parameterized.Parameters(name = "{index}: {0} {1}") + @Parameterized.Parameters(name = "{index}: {0}") public static Collection parameters() { return Arrays.asList(new Object[][]{ - {"code", ""}, - {"token", ""}, - {"id_token", ""}, - {"token id_token", ""}, - {"code", "#frag"}, - {"token", "#frag"}, - {"id_token", "#frag"}, - {"token id_token", "#frag"} + {"code"}, + {"token"}, + {"id_token"}, + {"token id_token"} }); } @@ -92,7 +81,6 @@ public void setup() { client = new BaseClientDetails("id", "", "openid", "authorization_code", "", redirectUrl); clientDetailsService = mock(ClientServicesExtension.class); redirectResolver = mock(RedirectResolver.class); - failureHandler = mock(AuthenticationFailureHandler.class); calculator = mock(OpenIdSessionStateCalculator.class); String zoneID = IdentityZoneHolder.get().getId(); @@ -101,7 +89,10 @@ public void setup() { when(redirectResolver.resolveRedirect(eq(HTTP_SOME_OTHER_SITE_CALLBACK), same(client))).thenThrow(new RedirectMismatchException("")); when(calculator.calculate(anyString(), anyString(), anyString())).thenReturn("sessionstate.salt"); - entryPoint = new AuthorizePromptNoneEntryPoint(failureHandler, clientDetailsService, redirectResolver, calculator); + uaaAuthorizationEndpoint = new UaaAuthorizationEndpoint(); + uaaAuthorizationEndpoint.setOpenIdSessionStateCalculator(calculator); + uaaAuthorizationEndpoint.setRedirectResolver(redirectResolver); + uaaAuthorizationEndpoint.setClientDetailsService(clientDetailsService); request = new MockHttpServletRequest("GET", "/oauth/authorize"); request.setParameter(OAuth2Utils.CLIENT_ID, client.getClientId()); @@ -109,17 +100,10 @@ public void setup() { response = new MockHttpServletResponse(); } - @Test - public void test_missing_client_id() throws Exception { - request.removeParameter(OAuth2Utils.CLIENT_ID); - entryPoint.commence(request, response, authException); - assertEquals(HttpStatus.BAD_REQUEST.value(), response.getStatus()); - } - @Test public void test_missing_redirect_uri() throws Exception { client.setRegisteredRedirectUri(Collections.emptySet()); - entryPoint.commence(request, response, authException); + uaaAuthorizationEndpoint.commence(request, response, authException); assertEquals(HttpStatus.BAD_REQUEST.value(), response.getStatus()); } @@ -127,33 +111,25 @@ public void test_missing_redirect_uri() throws Exception { public void test_client_not_found() throws Exception { reset(clientDetailsService); when(clientDetailsService.loadClientByClientId(anyString(), anyString())).thenThrow(new NoSuchClientException("not found")); - entryPoint.commence(request, response, authException); + uaaAuthorizationEndpoint.commence(request, response, authException); assertEquals(HttpStatus.BAD_REQUEST.value(), response.getStatus()); } @Test public void test_redirect_mismatch() throws Exception { request.setParameter(OAuth2Utils.REDIRECT_URI, HTTP_SOME_OTHER_SITE_CALLBACK); - entryPoint.commence(request, response, authException); + uaaAuthorizationEndpoint.commence(request, response, authException); assertEquals(HttpStatus.BAD_REQUEST.value(), response.getStatus()); } @Test public void test_redirect_contains_error() throws Exception { request.setParameter(OAuth2Utils.REDIRECT_URI, redirectUrl); - entryPoint.commence(request, response, authException); + uaaAuthorizationEndpoint.commence(request, response, authException); assertEquals(HttpStatus.FOUND.value(), response.getStatus()); assertTrue(response.getHeader("Location").contains("error=login_required")); } - @Test - public void test_failure_handler_is_invoked() throws Exception { - request.setParameter(OAuth2Utils.REDIRECT_URI, redirectUrl); - entryPoint.commence(request, response, authException); - verify(failureHandler, times(1)).onAuthenticationFailure(same(request), same(response), same(authException)); - } - - @Test public void test_redirect_honors_ant_matcher() throws Exception { BaseClientDetails client = new BaseClientDetails("ant", "", "openid", "implicit", "", "http://example.com/**"); @@ -164,7 +140,7 @@ public void test_redirect_honors_ant_matcher() throws Exception { when(redirectResolver.resolveRedirect(eq(redirectUrl), same(client))).thenReturn(redirectUrl); when(redirectResolver.resolveRedirect(eq("http://example.com/some/path"), same(client))).thenReturn("http://example.com/some/path"); - entryPoint.commence(request, response, authException); + uaaAuthorizationEndpoint.commence(request, response, authException); } } \ No newline at end of file diff --git a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml index b0a94c00f69..1be7ad83c3a 100755 --- a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml @@ -354,22 +354,9 @@ - - - - - - - - - - - - - Date: Thu, 1 Feb 2018 18:31:40 -0800 Subject: [PATCH 36/42] Current-User cookie no longer cleared by prompt=none silent auth [#153568058] https://www.pivotaltracker.com/story/show/153568058 --- .../identity/uaa/mock/token/TokenMvcMockTests.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java index 0cc073b5bfa..bb422b873ee 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java @@ -1052,12 +1052,10 @@ public void authorizeEndpointWithPromptNone_WhenNotAuthenticated() throws Except .param(OAuth2Utils.REDIRECT_URI, TEST_REDIRECT_URI) .param(ID_TOKEN_HINT_PROMPT, ID_TOKEN_HINT_PROMPT_NONE)) .andExpect(status().isFound()) - .andExpect(cookie().maxAge("Current-User", 0)) .andReturn(); String url = result.getResponse().getHeader("Location"); - assertEquals(UaaUrlUtils.addQueryParameter(TEST_REDIRECT_URI, "error", "login_required"), url); - + assertTrue(url.startsWith(UaaUrlUtils.addQueryParameter(TEST_REDIRECT_URI, "error", "login_required"))); } @Test From 2206e01018107066bbc661f0e8897f31342961f4 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 31 Jan 2018 13:14:32 -0800 Subject: [PATCH 37/42] Bump xmlsec to 1.5.8 [#154655171] https://www.pivotaltracker.com/story/show/154655171 --- server/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/server/build.gradle b/server/build.gradle index 89fc9659920..79987ad8d23 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -34,6 +34,7 @@ dependencies { exclude(group: 'ca.juliusdavies', module: 'not-yet-commons-ssl') exclude(group: "org.apache.velocity", module: 'velocity') } + compile group: 'org.apache.santuario', name: 'xmlsec', version: parent.openSamlXmlSec compile group: 'org.apache.velocity', name: 'velocity-engine-core', version: '2.0' compile (group:'org.owasp.esapi', name:'esapi', version:parent.esapiVersion) { From 4f4e7bb736cba18a39aa6cb90ff1ce96a3b3ffd4 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Wed, 31 Jan 2018 13:05:09 -0800 Subject: [PATCH 38/42] Upgrade Spring Framework and Spring Security [#152984320] https://www.pivotaltracker.com/story/show/152984320 [#154851617] https://www.pivotaltracker.com/story/show/154851617 --- .../uaa/web/UaaSessionCookieConfig.java | 11 ++- .../uaa/web/UaaSessionCookieConfigTest.java | 24 ++++++- shared_versions.gradle | 7 +- .../util/DecodePathInfoPostProcessor.java | 67 +++++++++++++++++++ ...cimExternalGroupMappingsEndpointsDocs.java | 26 +++++-- 5 files changed, 121 insertions(+), 14 deletions(-) create mode 100644 uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/DecodePathInfoPostProcessor.java diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/web/UaaSessionCookieConfig.java b/server/src/main/java/org/cloudfoundry/identity/uaa/web/UaaSessionCookieConfig.java index 8cce5622c49..b6350fb4a8f 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/web/UaaSessionCookieConfig.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/web/UaaSessionCookieConfig.java @@ -14,13 +14,15 @@ package org.cloudfoundry.identity.uaa.web; +import javax.servlet.ServletContext; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; +import java.util.HashSet; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.web.context.ServletContextAware; -import javax.servlet.ServletContext; -import javax.servlet.SessionCookieConfig; - import static org.springframework.util.StringUtils.hasText; public class UaaSessionCookieConfig implements SessionCookieConfig, ServletContextAware { @@ -67,6 +69,9 @@ public void setServletContext(ServletContext servletContext) { logger.debug(String.format("Configuring session cookie - Name: %s", getName())); config.setName(getName()); } + HashSet trackingModes = new HashSet<>(); + trackingModes.add(SessionTrackingMode.COOKIE); + servletContext.setSessionTrackingModes(trackingModes); } catch (Exception e) { logger.error("Ignoring session cookie config - unable to configure UAA session cookie", e); } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/web/UaaSessionCookieConfigTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/web/UaaSessionCookieConfigTest.java index eeea25b278e..18c538d32be 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/web/UaaSessionCookieConfigTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/web/UaaSessionCookieConfigTest.java @@ -15,14 +15,21 @@ package org.cloudfoundry.identity.uaa.web; -import org.junit.Test; - import javax.servlet.ServletContext; import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; +import java.util.Set; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertThat; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class UaaSessionCookieConfigTest { @@ -35,5 +42,18 @@ public void testSetServletContext() throws Exception { when(context.getSessionCookieConfig()).thenReturn(cookie); doThrow(new IllegalStateException()).when(cookie).setHttpOnly(anyBoolean()); config.setServletContext(context); + verify(cookie, never()).setSecure(anyBoolean()); + } + + @Test + public void verify_cookie_tracking_mode() throws Exception { + ServletContext context = mock(ServletContext.class); + UaaSessionCookieConfig config = new UaaSessionCookieConfig(); + SessionCookieConfig cookie = mock(SessionCookieConfig.class); + when(context.getSessionCookieConfig()).thenReturn(cookie); + config.setServletContext(context); + ArgumentCaptor> tracking = ArgumentCaptor.forClass(Set.class); + verify(context).setSessionTrackingModes(tracking.capture()); + assertThat(tracking.getValue(), containsInAnyOrder(SessionTrackingMode.COOKIE)); } } \ No newline at end of file diff --git a/shared_versions.gradle b/shared_versions.gradle index 4457a51eaff..23085d00472 100644 --- a/shared_versions.gradle +++ b/shared_versions.gradle @@ -16,7 +16,7 @@ ext { hamcrestVersion = '1.3' hibernateValidatorVersion = '4.3.2.Final' hsqldbVersion = '2.3.1' - jacksonVersion = '2.9.2' + jacksonVersion = '2.9.4' javamailVersion = '1.4.7' jsonPathVersion = '2.4.0' junitVersion = '4.12' @@ -25,14 +25,15 @@ ext { powermockVersion = '1.7.0' mssqlVersion = '6.2.2.jre8' openSamlVersion = '2.6.6' + openSamlXmlSec = '1.5.8' passayVersion = '1.2.0' postgresqlVersion = '42.1.4' scimSDKVersion = '1.8.18' servletVersion = '3.1.0' slf4jVersion = '1.7.25' snakeYamlVersion = '1.18' - springVersion = '4.3.12.RELEASE' - springSecurityVersion = '4.2.3.RELEASE' + springVersion = '4.3.14.RELEASE' + springSecurityVersion = '4.2.4.RELEASE' springSecurityJwtVersion = '1.0.8.RELEASE' springSecurityOAuthVersion = '2.0.11.RELEASE' springSecurityLdapVersion = '2.3.2.RELEASE' diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/DecodePathInfoPostProcessor.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/DecodePathInfoPostProcessor.java new file mode 100644 index 00000000000..11ecc12df4b --- /dev/null +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/util/DecodePathInfoPostProcessor.java @@ -0,0 +1,67 @@ +package org.cloudfoundry.identity.uaa.mock.util; + +import javax.servlet.http.HttpServletRequest; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.web.util.UriUtils; +import org.springframework.web.util.WebUtils; + +/** + * Works around a bug in Spring Framework Mock MVC Tests + * that double encodes % + */ + +public class DecodePathInfoPostProcessor implements RequestPostProcessor { + + private static Log logger = LogFactory.getLog(DecodePathInfoPostProcessor.class); + + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + request.setPathInfo(decodeRequestString(request, request.getPathInfo())); + return request; + } + + + /** + * Performs URL decoding on the provided source using the encoding from the request. + * + * @param request the request to use to determine the encoding + * @param source the source to URL encode + * @return the URL encoded string + */ + private String decodeRequestString(HttpServletRequest request, String source) { + String enc = determineEncoding(request); + try { + return UriUtils.decode(source, enc); + } + catch (UnsupportedEncodingException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Could not decode request string [" + source + "] with encoding '" + enc + + "': falling back to platform default encoding; exception message: " + ex.getMessage()); + } + return URLDecoder.decode(source); + } + } + + /** + * Determine the encoding for the given request. + * Can be overridden in subclasses. + *

The default implementation checks the request encoding, + * falling back to {@code WebUtils.DEFAULT_CHARACTER_ENCODING} + * @param request current HTTP request + * @return the encoding for the request (never {@code null}) + * @see javax.servlet.ServletRequest#getCharacterEncoding() + */ + private String determineEncoding(HttpServletRequest request) { + String enc = request.getCharacterEncoding(); + if (enc == null) { + enc = WebUtils.DEFAULT_CHARACTER_ENCODING; + } + return enc; + } +} diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimExternalGroupMappingsEndpointsDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimExternalGroupMappingsEndpointsDocs.java index 512fe214bfb..41ca6cdf4e7 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimExternalGroupMappingsEndpointsDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimExternalGroupMappingsEndpointsDocs.java @@ -14,10 +14,12 @@ import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest; +import org.cloudfoundry.identity.uaa.mock.util.DecodePathInfoPostProcessor; import org.cloudfoundry.identity.uaa.scim.ScimGroup; import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMember; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter; + import org.junit.Before; import org.junit.Test; import org.springframework.restdocs.headers.HeaderDescriptor; @@ -130,9 +132,15 @@ public void deleteExternalGroupMapping() throws Exception { AUTHORIZATION_HEADER, IDENTITY_ZONE_ID_HEADER, IDENTITY_ZONE_SUBDOMAIN_HEADER ); - MockHttpServletRequestBuilder delete = delete("/Groups/External/groupId/{groupId}/externalGroup/{externalGroup}/origin/{origin}", - group.getId(), scimGroupExternalMember.getExternalGroup(), scimGroupExternalMember.getOrigin()) - .header("Authorization", "Bearer " + scimWriteToken); + MockHttpServletRequestBuilder delete = + delete( + "/Groups/External/groupId/{groupId}/externalGroup/{externalGroup}/origin/{origin}", + group.getId(), + scimGroupExternalMember.getExternalGroup(), + scimGroupExternalMember.getOrigin() + ) + .header("Authorization", "Bearer " + scimWriteToken) + .with(new DecodePathInfoPostProcessor()); getMockMvc().perform(delete) .andExpect(status().isOk()) @@ -160,9 +168,15 @@ public void deleteExternalGroupMappingUsingName() throws Exception { AUTHORIZATION_HEADER, IDENTITY_ZONE_ID_HEADER, IDENTITY_ZONE_SUBDOMAIN_HEADER ); - MockHttpServletRequestBuilder delete = delete("/Groups/External/displayName/{displayName}/externalGroup/{externalGroup}/origin/{origin}", - group.getDisplayName(), scimGroupExternalMember.getExternalGroup(), scimGroupExternalMember.getOrigin()) - .header("Authorization", "Bearer " + scimWriteToken); + MockHttpServletRequestBuilder delete = + delete( + "/Groups/External/displayName/{displayName}/externalGroup/{externalGroup}/origin/{origin}", + group.getDisplayName(), + scimGroupExternalMember.getExternalGroup(), + scimGroupExternalMember.getOrigin() + ) + .header("Authorization", "Bearer " + scimWriteToken) + .with(new DecodePathInfoPostProcessor()); getMockMvc().perform(delete) .andExpect(status().isOk()) From 74ecb6026206b4ace69f6fa03795ccbe53f4a7eb Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 5 Feb 2018 09:05:04 -0800 Subject: [PATCH 39/42] Use `all` distribution of gradle This allows the IDE to present documentation in the editor --- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 68327c8b122..e5b4b487e33 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Dec 14 13:52:13 PST 2015 +#Mon Feb 05 09:03:30 PST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-bin.zip \ No newline at end of file +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip From c3716f2d9f55b7132b6edf7c83528745c8e4006e Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 5 Feb 2018 09:10:33 -0800 Subject: [PATCH 40/42] Upgrade jackson JSON library [#154849532] https://www.pivotaltracker.com/story/show/154849532 --- metrics-data/build.gradle | 2 +- statsd/build.gradle | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/metrics-data/build.gradle b/metrics-data/build.gradle index 52f4a395e1a..8d1d166219b 100644 --- a/metrics-data/build.gradle +++ b/metrics-data/build.gradle @@ -17,9 +17,9 @@ description = 'CloudFoundry Identity Metrics Data Jar' dependencies { compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: parent.jacksonVersion + compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: parent.jacksonVersion testCompile group: 'junit', name: 'junit', version: parent.junitVersion testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: parent.hamcrestVersion - } processResources { diff --git a/statsd/build.gradle b/statsd/build.gradle index 71eaa8b82fc..b2eae9ec69a 100644 --- a/statsd/build.gradle +++ b/statsd/build.gradle @@ -36,6 +36,9 @@ dependencies { compile group: 'log4j', name: 'log4j', version: '1.2.14' compile group: 'org.apache.httpcomponents', name: 'httpclient', version: parent.commonsHttpClientVersion compile group: 'org.springframework', name: 'spring-web', version: parent.springVersion + compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: parent.jacksonVersion + compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: parent.jacksonVersion + compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: parent.jacksonVersion providedCompile group: 'javax.servlet', name: 'javax.servlet-api', version: parent.servletVersion providedRuntime group: 'org.springframework.boot', name: 'spring-boot-starter-tomcat' testCompile group: 'org.springframework', name: 'spring-test', version: parent.springVersion From fec1eb3b056bd0a0fc8c894d85ae07801e17f7c1 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Mon, 5 Feb 2018 16:17:44 -0800 Subject: [PATCH 41/42] Convert count metrics to counters (previously gauges) [#154748239] https://www.pivotaltracker.com/story/show/154748239 --- .../identity/statsd/UaaMetricsEmitter.java | 54 +++++---- .../statsd/UaaMetricsEmitterTests.java | 105 +++++++++++++----- .../integration/IntegrationTestUtils.java | 5 +- .../integration/UaaMetricsEmitterIT.java | 32 ++---- 4 files changed, 123 insertions(+), 73 deletions(-) diff --git a/statsd/src/main/java/org/cloudfoundry/identity/statsd/UaaMetricsEmitter.java b/statsd/src/main/java/org/cloudfoundry/identity/statsd/UaaMetricsEmitter.java index f3c3e12b2b4..f43b1b1461b 100644 --- a/statsd/src/main/java/org/cloudfoundry/identity/statsd/UaaMetricsEmitter.java +++ b/statsd/src/main/java/org/cloudfoundry/identity/statsd/UaaMetricsEmitter.java @@ -12,20 +12,6 @@ *******************************************************************************/ package org.cloudfoundry.identity.statsd; -import com.timgroup.statsd.StatsDClient; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.cloudfoundry.identity.uaa.metrics.MetricsQueue; -import org.cloudfoundry.identity.uaa.metrics.RequestMetricSummary; -import org.cloudfoundry.identity.uaa.metrics.StatusCodeGroup; -import org.cloudfoundry.identity.uaa.metrics.UaaMetrics; -import org.cloudfoundry.identity.uaa.util.JsonUtils; -import org.springframework.context.expression.MapAccessor; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.util.ReflectionUtils; - import javax.management.InstanceNotFoundException; import javax.management.MBeanServerConnection; import javax.management.NotificationEmitter; @@ -39,8 +25,25 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.function.Function; +import org.cloudfoundry.identity.uaa.metrics.MetricsQueue; +import org.cloudfoundry.identity.uaa.metrics.RequestMetricSummary; +import org.cloudfoundry.identity.uaa.metrics.StatusCodeGroup; +import org.cloudfoundry.identity.uaa.metrics.UaaMetrics; +import org.cloudfoundry.identity.uaa.util.JsonUtils; + +import com.timgroup.statsd.StatsDClient; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.expression.MapAccessor; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.util.ReflectionUtils; + import static java.util.Optional.ofNullable; import static org.springframework.util.ReflectionUtils.findMethod; @@ -53,6 +56,7 @@ public class UaaMetricsEmitter { private final MetricsUtils metricsUtils; private NotificationEmitter emitter; private boolean notificationsEnabled; + private ConcurrentMap delta = new ConcurrentHashMap<>(); public UaaMetricsEmitter(MetricsUtils metricsUtils, StatsDClient statsDClient, MBeanServerConnection server) { this.statsDClient = statsDClient; @@ -126,19 +130,20 @@ public void emitGlobalRequestMetrics(UaaMetrics metrics) { String prefix = "requests.global."; RequestMetricSummary totals = globals.getTotals(); statsDClient.gauge(prefix + "completed.time", (long) totals.getAverageTime()); - statsDClient.gauge(prefix + "completed.count", totals.getCount()); - statsDClient.gauge(prefix + "unhealthy.count", totals.getIntolerableCount()); + statsDClient.count(prefix + "completed.count", getMetricDelta(prefix + "completed.count",totals.getCount())); + statsDClient.count(prefix + "unhealthy.count",getMetricDelta(prefix + "unhealthy.count",totals.getIntolerableCount())); statsDClient.gauge(prefix + "unhealthy.time", (long) totals.getAverageIntolerableTime()); //status codes for (StatusCodeGroup family : StatusCodeGroup.values()) { RequestMetricSummary summary = ofNullable(globals.getDetailed().get(family)).orElse(MISSING_METRICS); - statsDClient.gauge(prefix + "status_"+family.getName()+".count", summary.getCount()); + String aspect = prefix + "status_" + family.getName() + ".count"; + statsDClient.count(aspect, getMetricDelta(aspect,summary.getCount())); } //database metrics prefix = "database.global."; statsDClient.gauge(prefix + "completed.time", (long) totals.getAverageDatabaseQueryTime()); - statsDClient.gauge(prefix + "completed.count", totals.getDatabaseQueryCount()); - statsDClient.gauge(prefix + "unhealthy.count", totals.getDatabaseIntolerableQueryCount()); + statsDClient.count(prefix + "completed.count", getMetricDelta(prefix + "completed.count",totals.getDatabaseQueryCount())); + statsDClient.count(prefix + "unhealthy.count", getMetricDelta(prefix + "unhealthy.count", totals.getDatabaseIntolerableQueryCount())); statsDClient.gauge(prefix + "unhealthy.time", (long) totals.getAverageDatabaseIntolerableQueryTime()); } @@ -237,6 +242,17 @@ public void enableNotification() { } } + public long getMetricDelta(String name, long gaugeValue) { + long result = gaugeValue; + Long data = delta.get(name); + delta.put(name, gaugeValue); + if (data != null) { + result = gaugeValue - data; + } + return result; + + } + public boolean isNotificationEnabled() { return notificationsEnabled; } diff --git a/statsd/src/test/java/org/cloudfoundry/identity/statsd/UaaMetricsEmitterTests.java b/statsd/src/test/java/org/cloudfoundry/identity/statsd/UaaMetricsEmitterTests.java index 27229ba9592..182abba28e7 100644 --- a/statsd/src/test/java/org/cloudfoundry/identity/statsd/UaaMetricsEmitterTests.java +++ b/statsd/src/test/java/org/cloudfoundry/identity/statsd/UaaMetricsEmitterTests.java @@ -12,13 +12,6 @@ *******************************************************************************/ package org.cloudfoundry.identity.statsd; -import com.timgroup.statsd.ConvenienceMethodProvidingStatsDClient; -import com.timgroup.statsd.StatsDClient; -import org.cloudfoundry.identity.uaa.metrics.UaaMetrics; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mockito; - import javax.management.MBeanServerConnection; import javax.management.Notification; import javax.management.NotificationBroadcasterSupport; @@ -26,6 +19,15 @@ import java.util.HashMap; import java.util.Map; +import org.cloudfoundry.identity.uaa.metrics.UaaMetrics; + +import com.timgroup.statsd.ConvenienceMethodProvidingStatsDClient; +import com.timgroup.statsd.StatsDClient; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.junit.Assert.assertEquals; import static org.mockito.AdditionalMatchers.and; import static org.mockito.AdditionalMatchers.geq; import static org.mockito.AdditionalMatchers.gt; @@ -35,6 +37,7 @@ import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; @@ -48,7 +51,7 @@ public class UaaMetricsEmitterTests { private Map mBeanMap3; private MBeanMap serverRequestsBeanMap; private MetricsUtils metricsUtils; - private UaaMetrics uaaMetrics; + private UaaMetrics uaaMetrics1, uaaMetrics2; private Map urlGroupJsonMap; private NotificationBroadcasterSupport emitter; @@ -61,12 +64,19 @@ public void setUp() throws Exception { urlGroupJsonMap.put("/ui", uiJson); urlGroupJsonMap.put("/static-content", staticContentJson); - uaaMetrics = mock(UaaMetrics.class); - when(uaaMetrics.getGlobals()).thenReturn(globalsJson); - when(uaaMetrics.getSummary()).thenReturn(urlGroupJsonMap); - when(uaaMetrics.getIdleTime()).thenReturn(12349l); - when(uaaMetrics.getUpTime()).thenReturn(12349843l); - when(uaaMetrics.getInflightCount()).thenReturn(3l); + uaaMetrics1 = mock(UaaMetrics.class); + when(uaaMetrics1.getGlobals()).thenReturn(globalsJson1); + when(uaaMetrics1.getSummary()).thenReturn(urlGroupJsonMap); + when(uaaMetrics1.getIdleTime()).thenReturn(12349l); + when(uaaMetrics1.getUpTime()).thenReturn(12349843l); + when(uaaMetrics1.getInflightCount()).thenReturn(3l); + + uaaMetrics2 = mock(UaaMetrics.class); + when(uaaMetrics2.getGlobals()).thenReturn(globalsJson2); + when(uaaMetrics2.getSummary()).thenReturn(urlGroupJsonMap); + when(uaaMetrics2.getIdleTime()).thenReturn(12349l); + when(uaaMetrics2.getUpTime()).thenReturn(12349843l); + when(uaaMetrics2.getInflightCount()).thenReturn(3l); server = mock(MBeanServerConnection.class); @@ -89,7 +99,7 @@ public void setUp() throws Exception { mBeanMap2.put("UaaAudit", mBeanMap1); serverRequestsBeanMap = new MBeanMap(); - serverRequestsBeanMap.put("globals", globalsJson); + serverRequestsBeanMap.put("globals", globalsJson1); serverRequestsBeanMap.put("inflight.count", 3l); serverRequestsBeanMap.put("up.time", 12349843l); serverRequestsBeanMap.put("idle.time", 12349l); @@ -114,24 +124,54 @@ public void auditService_metrics_emitted() throws Exception { @Test public void requestCount_metrics_emitted() throws Exception { - Mockito.when(metricsUtils.getUaaMetrics(any())).thenReturn(uaaMetrics); + Mockito.when(metricsUtils.getUaaMetrics(any())).thenReturn(uaaMetrics1, uaaMetrics2); uaaMetricsEmitter.emitGlobalRequestMetrics(); - Mockito.verify(statsDClient).gauge("requests.global.completed.count", 3087l); + Mockito.verify(statsDClient).count("requests.global.completed.count", 3087l); Mockito.verify(statsDClient).gauge("requests.global.completed.time", 29l); - Mockito.verify(statsDClient).gauge("requests.global.unhealthy.count", 1l); + Mockito.verify(statsDClient).count("requests.global.unhealthy.count", 1l); Mockito.verify(statsDClient).gauge("requests.global.unhealthy.time", 4318l); - Mockito.verify(statsDClient).gauge("requests.global.status_1xx.count", 0l); - Mockito.verify(statsDClient).gauge("requests.global.status_2xx.count", 2148l); - Mockito.verify(statsDClient).gauge("requests.global.status_3xx.count", 763l); - Mockito.verify(statsDClient).gauge("requests.global.status_4xx.count", 175l); - Mockito.verify(statsDClient).gauge("requests.global.status_5xx.count", 1l); + Mockito.verify(statsDClient).count("requests.global.status_1xx.count", 0l); + Mockito.verify(statsDClient).count("requests.global.status_2xx.count", 2148l); + Mockito.verify(statsDClient).count("requests.global.status_3xx.count", 763l); + Mockito.verify(statsDClient).count("requests.global.status_4xx.count", 175l); + Mockito.verify(statsDClient).count("requests.global.status_5xx.count", 1l); Mockito.verify(statsDClient).gauge("server.inflight.count", 3l); Mockito.verify(statsDClient).gauge("server.up.time", 12349843l); Mockito.verify(statsDClient).gauge("server.idle.time", 12349l); - Mockito.verify(statsDClient).gauge("database.global.completed.count", 83797l); + Mockito.verify(statsDClient).count("database.global.completed.count", 83797l); Mockito.verify(statsDClient).gauge("database.global.completed.time", 0l); - Mockito.verify(statsDClient).gauge("database.global.unhealthy.count", 17549l); + Mockito.verify(statsDClient).count("database.global.unhealthy.count", 17549l); Mockito.verify(statsDClient).gauge("database.global.unhealthy.time", 0l); + reset(statsDClient); + uaaMetricsEmitter.emitGlobalRequestMetrics(); + Mockito.verify(statsDClient).count("requests.global.completed.count", 4l); + Mockito.verify(statsDClient).count("requests.global.unhealthy.count", 1l); + Mockito.verify(statsDClient).count("requests.global.status_1xx.count", 0l); + Mockito.verify(statsDClient).count("requests.global.status_2xx.count", 1l); + Mockito.verify(statsDClient).count("requests.global.status_3xx.count", 1l); + Mockito.verify(statsDClient).count("requests.global.status_4xx.count", 1l); + Mockito.verify(statsDClient).count("requests.global.status_5xx.count", 1l); + Mockito.verify(statsDClient).count("database.global.completed.count", 2l); + Mockito.verify(statsDClient).count("database.global.unhealthy.count", 5l); + reset(statsDClient); + uaaMetricsEmitter.emitGlobalRequestMetrics(); + Mockito.verify(statsDClient).count("requests.global.completed.count", 0l); + Mockito.verify(statsDClient).count("requests.global.unhealthy.count", 0l); + Mockito.verify(statsDClient).count("requests.global.status_1xx.count", 0l); + Mockito.verify(statsDClient).count("requests.global.status_2xx.count", 0l); + Mockito.verify(statsDClient).count("requests.global.status_3xx.count", 0l); + Mockito.verify(statsDClient).count("requests.global.status_4xx.count", 0l); + Mockito.verify(statsDClient).count("requests.global.status_5xx.count", 0l); + Mockito.verify(statsDClient).count("database.global.completed.count", 0l); + Mockito.verify(statsDClient).count("database.global.unhealthy.count", 0l); + } + + @Test + public void test_delta_method() throws Exception { + String name = "metric.name"; + assertEquals(5l, uaaMetricsEmitter.getMetricDelta(name, 5l)); + assertEquals(0l, uaaMetricsEmitter.getMetricDelta(name, 5l)); + assertEquals(3l, uaaMetricsEmitter.getMetricDelta(name, 8l)); } @Test @@ -146,7 +186,7 @@ public void vm_vitals() throws Exception { @Test public void perUrlGroup_request_metrics() throws Exception { - Mockito.when(metricsUtils.getUaaMetrics(any())).thenReturn(uaaMetrics); + Mockito.when(metricsUtils.getUaaMetrics(any())).thenReturn(uaaMetrics1); uaaMetricsEmitter.emitUrlGroupRequestMetrics(); Mockito.verify(statsDClient).gauge(eq("requests.ui.completed.count"), gt(0l)); Mockito.verify(statsDClient).gauge(eq("requests.ui.completed.time"), geq(300l)); @@ -332,7 +372,7 @@ public void test(Collection c) { " }\n" + "}"; - String globalsJson = "{\n" + + String globalsJson1 = "{\n" + " \"lastRequests\":[\n" + " {\n" + " \"uri\":\"/uaa/\",\n" + @@ -428,4 +468,15 @@ public void test(Collection c) { " \"averageDatabaseIntolerableQueryTime\":0.05704028719585168\n" + " }\n" + "}"; + + //values have increased + String globalsJson2 = globalsJson1 + .replace("\"count\":3087,\n", "\"count\":3091,\n") //total + .replace(" \"count\":763,\n", " \"count\":764,\n") //redirect + .replace(" \"count\":175,\n", " \"count\":176,\n") //client_error + .replace(" \"count\":2148,\n", " \"count\":2149,\n") //success + .replace(" \"count\":1,\n", " \"count\":2,\n") //error + .replace(" \"databaseQueryCount\":77513,\n", " \"databaseQueryCount\":77515,\n") //database count + .replace(" \"databaseIntolerableQueryCount\":17327,\n", " \"databaseIntolerableQueryCount\":17332,\n") //database unhealthy count + .replace(" \"intolerableCount\":1,\n", " \"intolerableCount\":2,\n"); //intolerable count } diff --git a/statsd/src/test/java/org/cloudfoundry/identity/statsd/integration/IntegrationTestUtils.java b/statsd/src/test/java/org/cloudfoundry/identity/statsd/integration/IntegrationTestUtils.java index 81d81f7623c..c0b4cacd557 100644 --- a/statsd/src/test/java/org/cloudfoundry/identity/statsd/integration/IntegrationTestUtils.java +++ b/statsd/src/test/java/org/cloudfoundry/identity/statsd/integration/IntegrationTestUtils.java @@ -17,6 +17,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; public class IntegrationTestUtils { @@ -35,11 +36,11 @@ public static String extractCookieCsrf(String body) { return null; } - public static long getGaugeValueFromMessage(String message) { + public static long getStatsDValueFromMessage(String message) { assertNotNull(message); String[] parts = message.split("[:|]"); - assertEquals(parts[2], "g"); + assertTrue(parts[2].equals("g") || parts[2].equals("c")); return Long.valueOf(parts[1]); } diff --git a/statsd/src/test/java/org/cloudfoundry/identity/statsd/integration/UaaMetricsEmitterIT.java b/statsd/src/test/java/org/cloudfoundry/identity/statsd/integration/UaaMetricsEmitterIT.java index 030696ab4b7..4ebe0f53740 100644 --- a/statsd/src/test/java/org/cloudfoundry/identity/statsd/integration/UaaMetricsEmitterIT.java +++ b/statsd/src/test/java/org/cloudfoundry/identity/statsd/integration/UaaMetricsEmitterIT.java @@ -49,9 +49,6 @@ public class UaaMetricsEmitterIT { private static byte[] receiveData; private static DatagramPacket receivePacket; private static Map firstBatch; - private static List perRequestFragments = Arrays.asList( - "uaa.requests.ui.latency" - ); private static List metricFragments = Arrays.asList( "uaa.audit_service.user_authentication_count", @@ -81,7 +78,6 @@ public class UaaMetricsEmitterIT { "uaa.requests.ui.completed.count", "uaa.requests.ui.completed.time", "uaa.server.up.time", - "uaa.requests.ui.latency", "uaa.server.idle.time", "uaa.vitals.vm.cpu.count", "uaa.vitals.vm.cpu.load", @@ -126,33 +122,19 @@ public static void setUpOnce() throws IOException { } @Test - public void assert_gauge_metrics() throws IOException { + public void assert_generic_metrics() throws IOException { String data1 = firstBatch.get(statsDKey); String data2 = secondBatch.get(statsDKey); - if(!perRequestFragments.contains(statsDKey)) { - assertNotNull("Expected to find message for:'" + statsDKey + "' in the first batch.", data1); - long first = IntegrationTestUtils.getGaugeValueFromMessage(data1); - assertThat(statsDKey + " first value must have a positive value.", first, greaterThanOrEqualTo(0l)); + assertNotNull("Expected to find message for:'" + statsDKey + "' in the first batch.", data1); + long first = IntegrationTestUtils.getStatsDValueFromMessage(data1); + assertThat(statsDKey + " first value must have a positive value.", first, greaterThanOrEqualTo(0l)); - assertNotNull("Expected to find message for:'"+statsDKey+"' in the second batch.", data2); - long second = IntegrationTestUtils.getGaugeValueFromMessage(data2); - assertThat(statsDKey + " second value must have a positive value.", second, greaterThanOrEqualTo(0l)); - } + assertNotNull("Expected to find message for:'"+statsDKey+"' in the second batch.", data2); + long second = IntegrationTestUtils.getStatsDValueFromMessage(data2); + assertThat(statsDKey + " second value must have a positive value.", second, greaterThanOrEqualTo(0l)); } - @Test - public void assert_per_request_metrics() throws IOException { - String data2 = secondBatch.get(statsDKey); - - if(perRequestFragments.contains(statsDKey)) { - assertNotNull("Expected to find message for:'"+statsDKey+"' in the second batch.", data2); - long second = IntegrationTestUtils.getTimeValueFromMessage(data2); - assertThat(statsDKey + " second value must have a positive value.", second, greaterThanOrEqualTo(0l)); - } - } - - protected static Map getMessages(List fragments, int timeout) throws IOException { long startTime = System.currentTimeMillis(); Map results = new HashMap<>(); From f83e4e50e2afe930ad23a33ba1c3192f4b7405cd Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Tue, 6 Feb 2018 10:24:31 -0800 Subject: [PATCH 42/42] Bump release version to 4.10.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 36e0c384160..e8e60ae5f0e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=4.10.0-SNAPSHOT +version=4.10.0