Browse Source

Add InMemoryReactiveOAuth2AuthorizedClientService

Issue: gh-4807
Rob Winch 7 years ago
parent
commit
5e9c714ff0

+ 90 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/InMemoryReactiveOAuth2AuthorizedClientService.java

@@ -0,0 +1,90 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client;
+
+import java.util.Base64;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
+import org.springframework.util.Assert;
+
+import reactor.core.publisher.Mono;
+
+/**
+ * An {@link OAuth2AuthorizedClientService} that stores
+ * {@link OAuth2AuthorizedClient Authorized Client(s)} in-memory.
+ *
+ * @author Rob Winch
+ * @since 5.1
+ * @see OAuth2AuthorizedClientService
+ * @see OAuth2AuthorizedClient
+ * @see ClientRegistration
+ * @see Authentication
+ */
+public final class InMemoryReactiveOAuth2AuthorizedClientService implements ReactiveOAuth2AuthorizedClientService {
+	private final Map<String, OAuth2AuthorizedClient> authorizedClients = new ConcurrentHashMap<>();
+	private final ReactiveClientRegistrationRepository clientRegistrationRepository;
+
+	/**
+	 * Constructs an {@code InMemoryOAuth2AuthorizedClientService} using the provided parameters.
+	 *
+	 * @param clientRegistrationRepository the repository of client registrations
+	 */
+	public InMemoryReactiveOAuth2AuthorizedClientService(ReactiveClientRegistrationRepository clientRegistrationRepository) {
+		Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
+		this.clientRegistrationRepository = clientRegistrationRepository;
+	}
+
+	@Override
+	public <T extends OAuth2AuthorizedClient> Mono<T> loadAuthorizedClient(String clientRegistrationId, String principalName) {
+		Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
+		Assert.hasText(principalName, "principalName cannot be empty");
+		return (Mono<T>) getIdentifier(clientRegistrationId, principalName)
+				.flatMap(identifier -> Mono.justOrEmpty(this.authorizedClients.get(identifier)));
+	}
+
+	@Override
+	public Mono<Void> saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {
+		Assert.notNull(authorizedClient, "authorizedClient cannot be null");
+		Assert.notNull(principal, "principal cannot be null");
+		return Mono.fromRunnable(() -> {
+			String identifier = this.getIdentifier(authorizedClient.getClientRegistration(), principal.getName());
+			this.authorizedClients.put(identifier, authorizedClient);
+		});
+	}
+
+	@Override
+	public Mono<Void> removeAuthorizedClient(String clientRegistrationId, String principalName) {
+		Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
+		Assert.hasText(principalName, "principalName cannot be empty");
+		return this.getIdentifier(clientRegistrationId, principalName)
+				.doOnNext(identifier -> this.authorizedClients.remove(identifier))
+				.then(Mono.empty());
+	}
+
+	private Mono<String> getIdentifier(String clientRegistrationId, String principalName) {
+		return this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
+				.map(registration -> getIdentifier(registration, principalName));
+	}
+
+	private String getIdentifier(ClientRegistration registration, String principalName) {
+		String identifier = "[" + registration.getRegistrationId() + "][" + principalName + "]";
+		return Base64.getEncoder().encodeToString(identifier.getBytes());
+	}
+}

+ 73 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientService.java

@@ -0,0 +1,73 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+
+import reactor.core.publisher.Mono;
+
+/**
+ * Implementations of this interface are responsible for the management
+ * of {@link OAuth2AuthorizedClient Authorized Client(s)}, which provide the purpose
+ * of associating an {@link OAuth2AuthorizedClient#getAccessToken() Access Token} credential
+ * to a {@link OAuth2AuthorizedClient#getClientRegistration() Client} and Resource Owner,
+ * who is the {@link OAuth2AuthorizedClient#getPrincipalName() Principal}
+ * that originally granted the authorization.
+ *
+ * @author Rob Winch
+ * @since 5.1
+ * @see OAuth2AuthorizedClient
+ * @see ClientRegistration
+ * @see Authentication
+ * @see OAuth2AccessToken
+ */
+public interface ReactiveOAuth2AuthorizedClientService {
+
+	/**
+	 * Returns the {@link OAuth2AuthorizedClient} associated to the
+	 * provided client registration identifier and End-User's {@code Principal} name
+	 * or {@code null} if not available.
+	 *
+	 * @param clientRegistrationId the identifier for the client's registration
+	 * @param principalName the name of the End-User {@code Principal} (Resource Owner)
+	 * @param <T> a type of OAuth2AuthorizedClient
+	 * @return the {@link OAuth2AuthorizedClient} or {@code null} if not available
+	 */
+	<T extends OAuth2AuthorizedClient> Mono<T> loadAuthorizedClient(String clientRegistrationId,
+			String principalName);
+
+	/**
+	 * Saves the {@link OAuth2AuthorizedClient} associating it to
+	 * the provided End-User {@link Authentication} (Resource Owner).
+	 *
+	 * @param authorizedClient the authorized client
+	 * @param principal the End-User {@link Authentication} (Resource Owner)
+	 */
+	Mono<Void> saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient,
+			Authentication principal);
+
+	/**
+	 * Removes the {@link OAuth2AuthorizedClient} associated to the
+	 * provided client registration identifier and End-User's {@code Principal} name.
+	 *
+	 * @param clientRegistrationId the identifier for the client's registration
+	 * @param principalName the name of the End-User {@code Principal} (Resource Owner)
+	 */
+	Mono<Void> removeAuthorizedClient(String clientRegistrationId, String principalName);
+
+}

+ 211 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/InMemoryReactiveOAuth2AuthorizedClientServiceTests.java

@@ -0,0 +1,211 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.oauth2.client;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Rob Winch
+ * @since 5.1
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class InMemoryReactiveOAuth2AuthorizedClientServiceTests {
+	@Mock
+	private ReactiveClientRegistrationRepository clientRegistrationRepository;
+
+	private InMemoryReactiveOAuth2AuthorizedClientService authorizedClientService;
+
+	private String clientRegistrationId = "github";
+
+	private String principalName = "username";
+
+	private Authentication principal = new TestingAuthenticationToken(this.principalName, "notused");
+
+	OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
+			"token",
+			Instant.now(),
+			Instant.now().plus(Duration.ofDays(1)));
+
+	private ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(this.clientRegistrationId)
+			.redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}")
+			.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
+			.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+			.scope("read:user")
+			.authorizationUri("https://github.com/login/oauth/authorize")
+			.tokenUri("https://github.com/login/oauth/access_token")
+			.userInfoUri("https://api.github.com/user")
+			.userNameAttributeName("id")
+			.clientName("GitHub")
+			.clientId("clientId")
+			.clientSecret("clientSecret")
+			.build();
+
+	@Before
+	public void setup() {
+		this.authorizedClientService = new InMemoryReactiveOAuth2AuthorizedClientService(
+				this.clientRegistrationRepository);
+	}
+
+	@Test
+	public void constructorNullClientRegistrationRepositoryThenThrowsIllegalArgumentException() {
+		this.clientRegistrationRepository = null;
+		assertThatThrownBy(() -> new InMemoryReactiveOAuth2AuthorizedClientService(this.clientRegistrationRepository))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void loadAuthorizedClientWhenClientRegistrationIdNullThenIllegalArgumentException() {
+		this.clientRegistrationId = null;
+		assertThatThrownBy(() -> this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void loadAuthorizedClientWhenClientRegistrationIdEmptyThenIllegalArgumentException() {
+		this.clientRegistrationId = "";
+		assertThatThrownBy(() -> this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void loadAuthorizedClientWhenPrincipalNameNullThenIllegalArgumentException() {
+		this.principalName = null;
+		assertThatThrownBy(() -> this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void loadAuthorizedClientWhenPrincipalNameEmptyThenIllegalArgumentException() {
+		this.principalName = "";
+		assertThatThrownBy(() -> this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void loadAuthorizedClientWhenClientRegistrationIdNotFoundThenEmpty() {
+		when(this.clientRegistrationRepository.findByRegistrationId(this.clientRegistrationId))
+				.thenReturn(Mono.empty());
+		StepVerifier
+			.create(this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName))
+			.verifyComplete();
+	}
+
+	@Test
+	public void loadAuthorizedClientWhenClientRegistrationFoundAndNotAuthorizedClientThenEmpty() {
+		when(this.clientRegistrationRepository.findByRegistrationId(this.clientRegistrationId)).thenReturn(Mono.just(this.clientRegistration));
+		StepVerifier
+				.create(this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName))
+				.verifyComplete();
+	}
+
+	@Test
+	public void loadAuthorizedClientWhenClientRegistrationFoundThenFound() {
+		when(this.clientRegistrationRepository.findByRegistrationId(this.clientRegistrationId)).thenReturn(Mono.just(this.clientRegistration));
+		OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.clientRegistration, this.principalName, this.accessToken);
+		Mono<OAuth2AuthorizedClient> saveAndLoad = this.authorizedClientService.saveAuthorizedClient(authorizedClient, this.principal)
+				.then(this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName));
+
+		StepVerifier.create(saveAndLoad)
+			.expectNext(authorizedClient)
+			.verifyComplete();
+	}
+
+	@Test
+	public void saveAuthorizedClientWhenAuthorizedClientNullThenIllegalArgumentException() {
+		OAuth2AuthorizedClient authorizedClient = null;
+		assertThatThrownBy(() -> this.authorizedClientService.saveAuthorizedClient(authorizedClient, this.principal))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void saveAuthorizedClientWhenPrincipalNullThenIllegalArgumentException() {
+		OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.clientRegistration, this.principalName, this.accessToken);
+		this.principal = null;
+		assertThatThrownBy(() -> this.authorizedClientService.saveAuthorizedClient(authorizedClient, this.principal))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void removeAuthorizedClientWhenClientRegistrationIdNullThenIllegalArgumentException() {
+		this.clientRegistrationId = null;
+		assertThatThrownBy(() -> this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void removeAuthorizedClientWhenClientRegistrationIdEmptyThenIllegalArgumentException() {
+		this.clientRegistrationId = "";
+		assertThatThrownBy(() -> this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void removeAuthorizedClientWhenPrincipalNameNullThenIllegalArgumentException() {
+		this.principalName = null;
+		assertThatThrownBy(() -> this.authorizedClientService.removeAuthorizedClient(this.clientRegistrationId, this.principalName))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void removeAuthorizedClientWhenPrincipalNameEmptyThenIllegalArgumentException() {
+		this.principalName = "";
+		assertThatThrownBy(() -> this.authorizedClientService.removeAuthorizedClient(this.clientRegistrationId, this.principalName))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void removeAuthorizedClientWhenClientIdThenNoException() {
+		when(this.clientRegistrationRepository.findByRegistrationId(this.clientRegistrationId)).thenReturn(Mono.empty());
+		OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.clientRegistration, this.principalName, this.accessToken);
+		Mono<Void> saveAndDeleteAndLoad = this.authorizedClientService.saveAuthorizedClient(authorizedClient, this.principal)
+				.then(this.authorizedClientService.removeAuthorizedClient(this.clientRegistrationId, this.principalName));
+
+		StepVerifier.create(saveAndDeleteAndLoad)
+				.verifyComplete();
+	}
+
+	@Test
+	public void removeAuthorizedClientWhenClientRegistrationFoundRemovedThenNotFound() {
+		when(this.clientRegistrationRepository.findByRegistrationId(this.clientRegistrationId)).thenReturn(Mono.just(this.clientRegistration));
+		OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.clientRegistration, this.principalName, this.accessToken);
+		Mono<OAuth2AuthorizedClient> saveAndDeleteAndLoad = this.authorizedClientService.saveAuthorizedClient(authorizedClient, this.principal)
+				.then(this.authorizedClientService.removeAuthorizedClient(this.clientRegistrationId, this.principalName))
+				.then(this.authorizedClientService.loadAuthorizedClient(this.clientRegistrationId, this.principalName));
+
+		StepVerifier.create(saveAndDeleteAndLoad)
+				.verifyComplete();
+	}
+}