From a1bef87aa68f20036c3917635fceb550510ceb3d Mon Sep 17 00:00:00 2001 From: Jeremy Grelle Date: Tue, 5 Dec 2023 13:35:10 -0500 Subject: [PATCH 1/6] Add binder for annotated custom `Authentication` A new `@User` annotation is added that can be used to bind a custom `Principal` object to a method argument for the currently active login. --- .../micronaut/security/annotation/User.java | 40 ++++++ .../authentication/UserArgumentBinder.java | 52 +++++++ .../authorization/AuthorizationSpec.groovy | 127 +++++++++++++++++- 3 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 security-annotations/src/main/java/io/micronaut/security/annotation/User.java create mode 100644 security/src/main/java/io/micronaut/security/authentication/UserArgumentBinder.java diff --git a/security-annotations/src/main/java/io/micronaut/security/annotation/User.java b/security-annotations/src/main/java/io/micronaut/security/annotation/User.java new file mode 100644 index 0000000000..e049aa4654 --- /dev/null +++ b/security-annotations/src/main/java/io/micronaut/security/annotation/User.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2023 original 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 io.micronaut.security.annotation; + +import io.micronaut.core.bind.annotation.Bindable; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation that can be applied to a method argument to indicate that it is bound from the user + * object for the currently active authentication. + * + * @author Jeremy Grelle + * @since 4.5.0 + */ +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Bindable +@Inherited +@Documented +public @interface User { +} diff --git a/security/src/main/java/io/micronaut/security/authentication/UserArgumentBinder.java b/security/src/main/java/io/micronaut/security/authentication/UserArgumentBinder.java new file mode 100644 index 0000000000..b3e8a9ece4 --- /dev/null +++ b/security/src/main/java/io/micronaut/security/authentication/UserArgumentBinder.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2023 original 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 io.micronaut.security.authentication; + +import io.micronaut.core.convert.ArgumentConversionContext; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder; +import io.micronaut.security.annotation.User; +import io.micronaut.security.filters.SecurityFilter; +import jakarta.inject.Singleton; + +import java.security.Principal; +import java.util.Optional; + +/** + * Binds the authentication object to a route argument annotated with {@link User}. + * + * @param The bound subtype of {@link Principal} + * @author Jeremy Grelle + * @since 4.5.0 + */ +@Singleton +public class UserArgumentBinder implements AnnotatedRequestArgumentBinder { + + @Override + public Class getAnnotationType() { + return User.class; + } + + @Override + public BindingResult bind(ArgumentConversionContext context, HttpRequest source) { + if (!source.getAttributes().contains(SecurityFilter.KEY)) { + return BindingResult.unsatisfied(); + } + + final Optional existing = source.getUserPrincipal(context.getArgument().getType()); + return existing.isPresent() ? (() -> existing) : BindingResult.empty(); + } +} diff --git a/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy b/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy index fa4ff90f69..86b4778776 100644 --- a/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy +++ b/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy @@ -19,15 +19,23 @@ import io.micronaut.security.FailedAuthenticationScenario import io.micronaut.security.MockAuthenticationProvider import io.micronaut.security.SuccessAuthenticationScenario import io.micronaut.security.annotation.Secured +import io.micronaut.security.annotation.User import io.micronaut.security.authentication.Authentication +import io.micronaut.security.authentication.AuthenticationArgumentBinder import io.micronaut.security.authentication.AuthenticationFailureReason +import io.micronaut.security.authentication.AuthenticationRequest +import io.micronaut.security.authentication.AuthenticationResponse +import io.micronaut.security.authentication.ClientAuthentication import io.micronaut.security.authentication.PrincipalArgumentBinder +import io.micronaut.security.authentication.ServerAuthentication +import io.micronaut.security.authentication.UserArgumentBinder import io.micronaut.security.rules.SecurityRule import io.micronaut.security.rules.SecurityRuleResult import io.micronaut.security.rules.SensitiveEndpointRule import io.micronaut.security.testutils.EmbeddedServerSpecification import jakarta.inject.Singleton import org.reactivestreams.Publisher +import reactor.core.publisher.Flux import reactor.core.publisher.Mono import java.security.Principal @@ -95,7 +103,7 @@ class AuthorizationSpec extends EmbeddedServerSpecification { void "Authentication Argument Binders binds Authentication if return type is Single"() { expect: - embeddedServer.applicationContext.getBean(PrincipalArgumentBinder.class) + embeddedServer.applicationContext.getBean(AuthenticationArgumentBinder.class) when: HttpResponse response = client.exchange(HttpRequest.GET("/argumentbinder/singleauthentication") @@ -105,6 +113,56 @@ class AuthorizationSpec extends EmbeddedServerSpecification { response.body() == 'You are valid' } + void "Authentication Argument Binders binds annotated subtype of Principal"() { + expect: + embeddedServer.applicationContext.getBean(UserArgumentBinder.class) + + when: + HttpResponse response = client.exchange(HttpRequest.GET("/subtypeargumentbinder/single-server-authentication") + .basicAuth("valid", "password"), String) + + then: + response.body() == 'You are valid' + } + + void "Authentication Argument Binders cannot bind annotated subtype of Principal if subtype doesn't match request.getPrincipal"() { + expect: + embeddedServer.applicationContext.getBean(UserArgumentBinder.class) + + when: + client.exchange(HttpRequest.GET("/subtypeargumentbinder/single-client-authentication") + .basicAuth("valid", "password"), String) + + then: + HttpClientResponseException e = thrown(HttpClientResponseException) + e.status == HttpStatus.BAD_REQUEST + } + + void "Authentication Argument Binders cannot bind non-annotated subtype of Principal"() { + expect: + embeddedServer.applicationContext.getBean(UserArgumentBinder.class) + + when: + client.exchange(HttpRequest.GET("/subtypeargumentbinder/single-no-user-authentication") + .basicAuth("valid", "password"), String) + + then: + HttpClientResponseException e = thrown(HttpClientResponseException) + e.status == HttpStatus.BAD_REQUEST + } + + void "Authentication Argument Binders binds annotated custom subtype of Principal"() { + expect: + embeddedServer.applicationContext.getBean(UserArgumentBinder.class) + + when: + HttpResponse response = client.exchange(HttpRequest.GET("/customuserargumentbinder/single-user") + .basicAuth("custom", "password"), String) + + then: + response.body() == 'You are custom' + } + void "test accessing the url map controller without authentication"() { when: client.exchange(HttpRequest.GET("/urlMap/authenticated")) @@ -318,6 +376,42 @@ class AuthorizationSpec extends EmbeddedServerSpecification { } } + @Requires(property = 'spec.name', value = 'AuthorizationSpec') + @Controller('/subtypeargumentbinder') + @Secured("isAuthenticated()") + static class PrincipalSubtypeArgumentBinderController { + + @Get("/single-server-authentication") + @SingleResult + Publisher singleServerAuthentication(@User ServerAuthentication authentication) { + Mono.just("You are ${authentication.getName()}".toString()) + } + + @Get("/single-client-authentication") + @SingleResult + Publisher singleClientAuthentication(@User ClientAuthentication authentication) { + Mono.just("You are ${authentication.getName()}".toString()) + } + + @Get("/single-no-user-authentication") + @SingleResult + Publisher singleNoUserAuthentication(ServerAuthentication authentication) { + Mono.just("You are ${authentication.getName()}".toString()) + } + } + + @Requires(property = 'spec.name', value = 'AuthorizationSpec') + @Controller('/customuserargumentbinder') + @Secured("isAuthenticated()") + static class CustomUserArgumentBinderController { + + @Get("/single-user") + @SingleResult + Publisher singleServerAuthentication(@User TestingAuthenticationProvider.CustomAuthentication authentication) { + Mono.just("You are ${authentication.getName()}".toString()) + } + } + @Requires(property = 'spec.name', value = 'AuthorizationSpec') @Controller("/urlMap") static class UrlMapController { @@ -344,6 +438,7 @@ class AuthorizationSpec extends EmbeddedServerSpecification { TestingAuthenticationProvider() { super([ new SuccessAuthenticationScenario("valid","password"), + new SuccessAuthenticationScenario("custom", "password"), new SuccessAuthenticationScenario("admin",["ROLE_ADMIN"]) ], [ new FailedAuthenticationScenario("disabled", AuthenticationFailureReason.USER_DISABLED), @@ -353,6 +448,36 @@ class AuthorizationSpec extends EmbeddedServerSpecification { new FailedAuthenticationScenario("invalidPassword", AuthenticationFailureReason.CREDENTIALS_DO_NOT_MATCH), ]) } + + @Override + Publisher authenticate(Object httpRequest, AuthenticationRequest authenticationRequest) { + return Flux.from(super.authenticate(httpRequest, authenticationRequest)).map(response -> { + if (response.authenticated && response.getAuthentication().orElseThrow().name == 'custom') { + return new CustomAuthenticationResponse('custom') + } + return response + }) + } + + static class CustomAuthenticationResponse implements AuthenticationResponse { + + private final String username + + CustomAuthenticationResponse(String username) { + this.username = username + } + + @Override + Optional getAuthentication() { + return Optional.of(new CustomAuthentication(this.username, Collections.emptyList(), Collections.emptyMap())) + } + } + + static class CustomAuthentication extends ServerAuthentication { + CustomAuthentication(String name, Collection roles, Map attributes) { + super(name, roles, attributes) + } + } } @Requires(property = 'spec.name', value = 'AuthorizationSpec') From 3d2057c270853752f6380b94279ca882f30ffcc3 Mon Sep 17 00:00:00 2001 From: Jeremy Grelle Date: Wed, 6 Dec 2023 09:53:03 -0500 Subject: [PATCH 2/6] Update security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy Co-authored-by: Sergio del Amo --- .../micronaut/security/authorization/AuthorizationSpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy b/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy index 86b4778776..aee3370257 100644 --- a/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy +++ b/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy @@ -134,7 +134,7 @@ class AuthorizationSpec extends EmbeddedServerSpecification { .basicAuth("valid", "password"), String) then: - HttpClientResponseException e = thrown(HttpClientResponseException) + HttpClientResponseException e = thrown() e.status == HttpStatus.BAD_REQUEST } From a91512018d37d20798476cb0ebaaba25ef30b145 Mon Sep 17 00:00:00 2001 From: Jeremy Grelle Date: Wed, 6 Dec 2023 09:53:12 -0500 Subject: [PATCH 3/6] Update security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy Co-authored-by: Sergio del Amo --- .../micronaut/security/authorization/AuthorizationSpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy b/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy index aee3370257..76867e6b30 100644 --- a/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy +++ b/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy @@ -127,7 +127,7 @@ class AuthorizationSpec extends EmbeddedServerSpecification { void "Authentication Argument Binders cannot bind annotated subtype of Principal if subtype doesn't match request.getPrincipal"() { expect: - embeddedServer.applicationContext.getBean(UserArgumentBinder.class) + embeddedServer.applicationContext.containsBean(UserArgumentBinder.class) when: client.exchange(HttpRequest.GET("/subtypeargumentbinder/single-client-authentication") From 909a127b81b2f69facc8573fac51533cdf5e50d1 Mon Sep 17 00:00:00 2001 From: Jeremy Grelle Date: Wed, 6 Dec 2023 09:53:20 -0500 Subject: [PATCH 4/6] Update security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy Co-authored-by: Sergio del Amo --- .../micronaut/security/authorization/AuthorizationSpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy b/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy index 76867e6b30..3bc22d8e3d 100644 --- a/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy +++ b/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy @@ -140,7 +140,7 @@ class AuthorizationSpec extends EmbeddedServerSpecification { void "Authentication Argument Binders cannot bind non-annotated subtype of Principal"() { expect: - embeddedServer.applicationContext.getBean(UserArgumentBinder.class) + embeddedServer.applicationContext.containsBean(UserArgumentBinder.class) when: client.exchange(HttpRequest.GET("/subtypeargumentbinder/single-no-user-authentication") From b4a3f4eeceea79f7096d109aba88ad2d8091d96c Mon Sep 17 00:00:00 2001 From: Jeremy Grelle Date: Wed, 6 Dec 2023 09:53:26 -0500 Subject: [PATCH 5/6] Update security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy Co-authored-by: Sergio del Amo --- .../micronaut/security/authorization/AuthorizationSpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy b/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy index 3bc22d8e3d..cb7366afad 100644 --- a/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy +++ b/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy @@ -153,7 +153,7 @@ class AuthorizationSpec extends EmbeddedServerSpecification { void "Authentication Argument Binders binds annotated custom subtype of Principal"() { expect: - embeddedServer.applicationContext.getBean(UserArgumentBinder.class) + embeddedServer.applicationContext.contains(UserArgumentBinder.class) when: HttpResponse response = client.exchange(HttpRequest.GET("/customuserargumentbinder/single-user") From 570ca1600993318b9af9a8e77c17db20e8ae1d3b Mon Sep 17 00:00:00 2001 From: Jeremy Grelle Date: Wed, 6 Dec 2023 09:56:54 -0500 Subject: [PATCH 6/6] Update security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy Co-authored-by: Sergio del Amo --- .../micronaut/security/authorization/AuthorizationSpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy b/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy index cb7366afad..b2877de02c 100644 --- a/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy +++ b/security/src/test/groovy/io/micronaut/security/authorization/AuthorizationSpec.groovy @@ -115,7 +115,7 @@ class AuthorizationSpec extends EmbeddedServerSpecification { void "Authentication Argument Binders binds annotated subtype of Principal"() { expect: - embeddedServer.applicationContext.getBean(UserArgumentBinder.class) + embeddedServer.applicationContext.containsBean(UserArgumentBinder.class) when: HttpResponse response = client.exchange(HttpRequest.GET("/subtypeargumentbinder/single-server-authentication")