Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Spring Security's OAuth Support can integrate with `RestClient` and `WebClient`

[[configuration]]
== Configuration
After xref:features/integrations/rest/http-interface.adoc#configuration-restclient[RestClient] or xref:features/integrations/rest/http-interface.adoc#configuration-webclient[WebClient] specific configuration, usage of xref:features/integrations/rest/http-interface.adoc[] only requires adding a xref:features/integrations/rest/http-interface.adoc#client-registration-id[`@ClientRegistrationId`] to methods that require OAuth.
After xref:features/integrations/rest/http-interface.adoc#configuration-restclient[RestClient] or xref:features/integrations/rest/http-interface.adoc#configuration-webclient[WebClient] specific configuration, usage of xref:features/integrations/rest/http-interface.adoc[] only requires adding a xref:features/integrations/rest/http-interface.adoc#client-registration-id[`@ClientRegistrationId`] to methods that require OAuth or their declaring HTTP interface.

Since the presence of xref:features/integrations/rest/http-interface.adoc#client-registration-id[`@ClientRegistrationId`] determines if and how the OAuth token will be resolved, it is safe to add Spring Security's OAuth support any configuration.

Expand Down Expand Up @@ -51,6 +51,13 @@ include-code::./UserService[tag=getAuthenticatedUser]

The xref:features/integrations/rest/http-interface.adoc#client-registration-id[`@ClientRegistrationId`] will be processed by xref:features/integrations/rest/http-interface.adoc#client-registration-id-processor[`ClientRegistrationIdProcessor`]

[[type]]
=== Type Level Declarations

`@ClientRegistrationId` can also be added at the type level to avoid repeating the declaration on every method.

include-code::./UserService[tag=type]

[[client-registration-id-processor]]
== `ClientRegistrationIdProcessor`

Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/whats-new.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ http.csrf((csrf) -> csrf.spa());
* Added OAuth2 Support for xref:features/integrations/rest/http-interface.adoc[HTTP Interface Integration]
* Added support for custom `JwkSource` in `NimbusJwtDecoder`, allowing usage of Nimbus's `JwkSourceBuilder` API
* Added builder for `NimbusJwtEncoder`, supports specifying an EC or RSA key pair or a secret key
* Added support for `@ClientRegistrationId` at the xref:features/integrations/rest/http-interface.adoc#type[type level], eliminating the need for method level repetition

== SAML 2.0

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2004-present 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
*
* https://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.docs.features.integrations.rest.type;

/**
* Used to ensure {@link UserService} compiles, but not show in the documentation.
*
* @author Rob Winch
*/
public record Hovercard() {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2004-present 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 clients copy of the License at
*
* https://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.docs.features.integrations.rest.type;

import org.springframework.security.docs.features.integrations.rest.clientregistrationid.User;
import org.springframework.security.oauth2.client.annotation.ClientRegistrationId;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;

/**
* Demonstrates a service for {@link ClientRegistrationId} at the type level.
* @author Rob Winch
*/
// tag::type[]
@HttpExchange
@ClientRegistrationId("github")
public interface UserService {

@GetExchange("/user")
User getAuthenticatedUser();

@GetExchange("/users/{username}/hovercard")
Hovercard getHovercard(@PathVariable String username);

}
// end::type[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2004-present 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
*
* https://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.kt.docs.features.integrations.rest.type

/**
* Used to ensure [UserService] compiles, but not show in the documentation.
*
* @author Rob Winch
*/
class Hovercard
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2004-present 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 clients copy of the License at
*
* https://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.kt.docs.features.integrations.rest.type

import org.springframework.security.kt.docs.features.integrations.rest.clientregistrationid.User
import org.springframework.security.oauth2.client.annotation.ClientRegistrationId
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.service.annotation.GetExchange
import org.springframework.web.service.annotation.HttpExchange

/**
* Demonstrates a service for [ClientRegistrationId] at the type level.
* @author Rob Winch
*/
// tag::type[]
@HttpExchange
@ClientRegistrationId("github")
interface UserService {
@GetExchange("/user")
fun getAuthenticatedUser(): User

@GetExchange("/users/{username}/hovercard")
fun getHovercard(@PathVariable username: String): Hovercard
}
// end::type[]
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
* @since 7.0
* @see org.springframework.security.oauth2.client.web.client.ClientRegistrationIdProcessor
*/
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ClientRegistrationId {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
import org.jspecify.annotations.Nullable;

import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.security.core.annotation.SecurityAnnotationScanner;
import org.springframework.security.core.annotation.SecurityAnnotationScanners;
import org.springframework.security.oauth2.client.annotation.ClientRegistrationId;
import org.springframework.security.oauth2.client.web.ClientAttributes;
import org.springframework.web.service.invoker.HttpRequestValues;
Expand All @@ -37,10 +38,14 @@ public final class ClientRegistrationIdProcessor implements HttpRequestValues.Pr

public static ClientRegistrationIdProcessor DEFAULT_INSTANCE = new ClientRegistrationIdProcessor();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private SecurityAnnotationScanner<ClientRegistrationId> scanner = SecurityAnnotationScanners.requireUnique(ClientRegistrationId.class);

Use a SecurityAnnotationScanner<ClientRegistrationId> instead. This will automatically follow the same logic as we do for method security. It ensures that we do not get duplicate annotations which might provide conflicting information and thus use the wrong registration id.

private SecurityAnnotationScanner<ClientRegistrationId> securityAnnotationScanner = SecurityAnnotationScanners
.requireUnique(ClientRegistrationId.class);

@Override
public void process(Method method, MethodParameter[] parameters, @Nullable Object[] arguments,
HttpRequestValues.Builder builder) {
ClientRegistrationId registeredId = AnnotationUtils.findAnnotation(method, ClientRegistrationId.class);
ClientRegistrationId registeredId = this.securityAnnotationScanner.scan(method, method.getDeclaringClass());

if (registeredId != null) {
String registrationId = registeredId.registrationId();
builder.configureAttributes(ClientAttributes.clientRegistrationId(registrationId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@

import org.junit.jupiter.api.Test;

import org.springframework.core.annotation.AnnotationConfigurationException;
import org.springframework.security.oauth2.client.annotation.ClientRegistrationId;
import org.springframework.security.oauth2.client.web.ClientAttributes;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.service.invoker.HttpRequestValues;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

/**
* Unit tests for {@link ClientRegistrationIdProcessor}.
Expand All @@ -39,6 +41,8 @@
*/
class ClientRegistrationIdProcessorTests {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add tests that verify failure in the event that there are duplicate annotations. This should be handled by the SecurityAnnotationScanners.requireUnique. An example would be AggregateService.toTest() should fail due to duplicate annotations at the same level.

@ClientRegistrationId("a")
interface AService {}

@ClientRegistrationId("b")
interface BService {}

interface AggregateService extends AService, BService {
	void toTest();
}


private static final String REGISTRATION_ID = "registrationId";

ClientRegistrationIdProcessor processor = ClientRegistrationIdProcessor.DEFAULT_INSTANCE;

@Test
Expand All @@ -48,7 +52,7 @@ void processWhenClientRegistrationIdPresentThenSet() {
this.processor.process(hasClientRegistrationId, null, null, builder);

String registrationId = ClientAttributes.resolveClientRegistrationId(builder.build().getAttributes());
assertThat(registrationId).isEqualTo(RestService.REGISTRATION_ID);
assertThat(registrationId).isEqualTo(REGISTRATION_ID);
}

@Test
Expand All @@ -58,7 +62,7 @@ void processWhenMetaClientRegistrationIdPresentThenSet() {
this.processor.process(hasClientRegistrationId, null, null, builder);

String registrationId = ClientAttributes.resolveClientRegistrationId(builder.build().getAttributes());
assertThat(registrationId).isEqualTo(RestService.REGISTRATION_ID);
assertThat(registrationId).isEqualTo(REGISTRATION_ID);
}

@Test
Expand All @@ -71,9 +75,28 @@ void processWhenNoClientRegistrationIdPresentThenNull() {
assertThat(registrationId).isNull();
}

interface RestService {
@Test
void processWhenClientRegistrationIdPresentOnDeclaringClassThenSet() {
HttpRequestValues.Builder builder = HttpRequestValues.builder();
Method declaringClassHasClientRegistrationId = ReflectionUtils.findMethod(TypeAnnotatedRestService.class,
"declaringClassHasClientRegistrationId");
this.processor.process(declaringClassHasClientRegistrationId, null, null, builder);

String registrationId = ClientAttributes.resolveClientRegistrationId(builder.build().getAttributes());
assertThat(registrationId).isEqualTo(REGISTRATION_ID);
}

String REGISTRATION_ID = "registrationId";
@Test
void processWhenDuplicateClientRegistrationIdPresentOnAggregateServiceThenException() {
HttpRequestValues.Builder builder = HttpRequestValues.builder();
Method shouldFailDueToDuplicateClientRegistrationId = ReflectionUtils.findMethod(AggregateRestService.class,
"shouldFailDueToDuplicateClientRegistrationId");

assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(
() -> this.processor.process(shouldFailDueToDuplicateClientRegistrationId, null, null, builder));
}

interface RestService {

@ClientRegistrationId(REGISTRATION_ID)
void hasClientRegistrationId();
Expand All @@ -86,9 +109,32 @@ interface RestService {
}

@Retention(RetentionPolicy.RUNTIME)
@ClientRegistrationId(RestService.REGISTRATION_ID)
@ClientRegistrationId(REGISTRATION_ID)
@interface MetaClientRegistrationId {

}

@ClientRegistrationId(REGISTRATION_ID)
interface TypeAnnotatedRestService {

void declaringClassHasClientRegistrationId();

}

@ClientRegistrationId("a")
interface ARestService {

}

@ClientRegistrationId("b")
interface BRestService {

}

interface AggregateRestService extends ARestService, BRestService {

void shouldFailDueToDuplicateClientRegistrationId();

}

}