Skip to content

Commit

Permalink
doc: Custom binding of authenticated user (#1524)
Browse files Browse the repository at this point in the history
Close: #1430
  • Loading branch information
sdelamo authored Dec 6, 2023
1 parent 1f8d428 commit 22dbbc0
Show file tree
Hide file tree
Showing 11 changed files with 347 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

You can create a custom argument binder to bind the authenticated user to a custom class tailored to your application needs.

If, in your application, the authenticated user has an email address, you can create a class such as:

snippet::io.micronaut.security.docs.customauthentication.AuthenticationWithEmail[]

and then a `TypedRequestArgumentBinder`:

snippet::io.micronaut.security.docs.customauthentication.AuthenticationWithEmailArgumentBinder[]

Then you can bind it in a controller method parameter:

snippet::io.micronaut.security.docs.customauthentication.CustomAuthenticationTest[tags="method"]
1 change: 1 addition & 0 deletions src/main/docs/guide/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ endpoints:
introspectionendpoint: Introspection Endpoint
retrievingAuthenticatedUser:
title: Retrieve the authenticated user
customAuthenticatedUser: Custom Binding
securityService: User outside of a controller
securityEvents: Security Events
oauth:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.micronaut.security.docs.customauthentication

import groovy.transform.Canonical
import io.micronaut.security.authentication.Authentication
import io.micronaut.serde.annotation.Serdeable

@Canonical
@Serdeable
class AuthenticationWithEmail {
String username
String email

static AuthenticationWithEmail of(Authentication authentication) {
new AuthenticationWithEmail(authentication.getName(), authentication.getAttributes().get("email")?.toString())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.micronaut.security.docs.customauthentication

import io.micronaut.context.annotation.Requires
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder
import io.micronaut.security.authentication.Authentication
import io.micronaut.security.filters.SecurityFilter
import jakarta.inject.Singleton

@Requires(property = "spec.name", value = "CustomAuthenticationTest")
@Singleton
class AuthenticationWithEmailArgumentBinder implements TypedRequestArgumentBinder<AuthenticationWithEmail> {
private final Argument<AuthenticationWithEmail> argumentType;
AuthenticationWithEmailArgumentBinder() {
argumentType = Argument.of(AuthenticationWithEmail.class);
}

@Override
Argument<AuthenticationWithEmail> argumentType() {
return argumentType;
}

@Override
BindingResult<AuthenticationWithEmail> bind(ArgumentConversionContext<AuthenticationWithEmail> context, HttpRequest<?> source) {
if (!source.getAttributes().contains(SecurityFilter.KEY)) {
return BindingResult.UNSATISFIED
}
final Optional<Authentication> existing = source.getUserPrincipal(Authentication.class)
existing.isPresent() ? (() -> existing.map(AuthenticationWithEmail::of)) : BindingResult.EMPTY
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.micronaut.security.docs.customauthentication

import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Requires
import io.micronaut.core.util.StringUtils
import io.micronaut.http.HttpRequest
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Produces
import io.micronaut.http.client.BlockingHttpClient
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.security.annotation.Secured
import io.micronaut.security.authentication.Authentication
import io.micronaut.security.rules.SecurityRule
import io.micronaut.security.token.validator.TokenValidator
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import org.reactivestreams.Publisher
import reactor.core.publisher.Mono
import spock.lang.Specification


@Property(name = "spec.name", value = "CustomAuthenticationTest")
@Property(name = "micronaut.security.reject-not-found", value = StringUtils.FALSE)
@MicronautTest
class CustomAuthenticationTest extends Specification {

@Inject
@Client("/")
HttpClient httpClient;

void "customAuthentication"() {
given:
BlockingHttpClient client = httpClient.toBlocking()

when:
HttpRequest<?> request = HttpRequest.GET("/custom-authentication")
.accept(MediaType.TEXT_PLAIN)
.bearerAuth("xxx")
String email = client.retrieve(request)

then:
noExceptionThrown()
"[email protected]" == email
}

@Requires(property = "spec.name", value = "CustomAuthenticationTest")
@Controller
static class CustomAuthenticationController {
//tag::method[]
@Secured(SecurityRule.IS_AUTHENTICATED)
@Produces(MediaType.TEXT_PLAIN)
@Get("/custom-authentication")
String index(AuthenticationWithEmail authentication) {
authentication.email
}
//end::method[]
}

@Requires(property = "spec.name", value = "CustomAuthenticationTest")
@Controller
static class CustomAuthenticationProvider implements TokenValidator<HttpRequest<?>> {
@Override
Publisher<Authentication> validateToken(String token, HttpRequest<?> request) {
Mono.just(Authentication.build("sherlock", [email: "[email protected]"]))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.micronaut.security.docs.customauthentication

import io.micronaut.security.authentication.Authentication
import io.micronaut.serde.annotation.Serdeable

@Serdeable
data class AuthenticationWithEmail(val username: String, val email: String?) {
companion object {
fun of(authentication: Authentication): AuthenticationWithEmail {
val obj = authentication.getAttributes()["email"]
val email = obj?.toString()
return AuthenticationWithEmail(authentication.name, email)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.micronaut.security.docs.customauthentication

import io.micronaut.context.annotation.Requires
import io.micronaut.core.bind.ArgumentBinder
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder
import io.micronaut.security.authentication.Authentication
import io.micronaut.security.filters.SecurityFilter
import jakarta.inject.Singleton

@Requires(property = "spec.name", value = "CustomAuthenticationTest")
@Singleton
class AuthenticationWithEmailArgumentBinder : TypedRequestArgumentBinder<AuthenticationWithEmail> {
override fun bind(context: ArgumentConversionContext<AuthenticationWithEmail>, source: HttpRequest<*>): ArgumentBinder.BindingResult<AuthenticationWithEmail> {
if (!source.attributes.contains(SecurityFilter.KEY)) {
return ArgumentBinder.BindingResult.unsatisfied()
}
val existing = source.getUserPrincipal(Authentication::class.java)
return if (existing.isPresent) ArgumentBinder.BindingResult {
existing.map(AuthenticationWithEmail::of)
} else ArgumentBinder.BindingResult.empty()
}

override fun argumentType(): Argument<AuthenticationWithEmail> {
return Argument.of(AuthenticationWithEmail::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.micronaut.security.docs.customauthentication

import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Requires
import io.micronaut.core.util.StringUtils
import io.micronaut.http.HttpRequest
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Produces
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.security.annotation.Secured
import io.micronaut.security.authentication.Authentication
import io.micronaut.security.rules.SecurityRule
import io.micronaut.security.token.validator.TokenValidator
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.reactivestreams.Publisher
import reactor.core.publisher.Mono

@Property(name = "spec.name", value = "CustomAuthenticationTest")
@Property(name = "micronaut.security.reject-not-found", value = StringUtils.FALSE)
@MicronautTest
internal class CustomAuthenticationTest {
@Test
fun customAuthentication(@Client("/") httpClient: HttpClient) {
val client = httpClient.toBlocking()
val request: HttpRequest<*> = HttpRequest.GET<Any>("/custom-authentication")
.accept(MediaType.TEXT_PLAIN)
.bearerAuth("xxx")
val email = Assertions.assertDoesNotThrow<String> {
client.retrieve(request)
}
Assertions.assertEquals("[email protected]", email)
}

@Requires(property = "spec.name", value = "CustomAuthenticationTest")
@Controller
internal class CustomAuthenticationController {
//tag::method[]
@Secured(SecurityRule.IS_AUTHENTICATED)
@Produces(MediaType.TEXT_PLAIN)
@Get("/custom-authentication")
fun index(authentication: AuthenticationWithEmail) = authentication.email
//end::method[]
}

@Requires(property = "spec.name", value = "CustomAuthenticationTest")
@Controller
internal class CustomAuthenticationProvider : TokenValidator<HttpRequest<*>> {
override fun validateToken(token: String, request: HttpRequest<*>): Publisher<Authentication> {
return Mono.just(Authentication.build("sherlock", mapOf("email" to "[email protected]")))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.micronaut.security.docs.customauthentication;

import io.micronaut.security.authentication.Authentication;
import io.micronaut.serde.annotation.Serdeable;

@Serdeable
public record AuthenticationWithEmail(String username,
String email) {
public static AuthenticationWithEmail of(Authentication authentication) {
Object obj = authentication.getAttributes().get("email");
String email = obj == null ? null : obj.toString();
return new AuthenticationWithEmail(authentication.getName(), email);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.micronaut.security.docs.customauthentication;

import io.micronaut.context.annotation.Requires;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.filters.SecurityFilter;
import jakarta.inject.Singleton;
import java.util.Optional;

@Requires(property = "spec.name", value = "CustomAuthenticationTest")
@Singleton
public class AuthenticationWithEmailArgumentBinder implements TypedRequestArgumentBinder<AuthenticationWithEmail> {
private final Argument<AuthenticationWithEmail> argumentType;
public AuthenticationWithEmailArgumentBinder() {
argumentType = Argument.of(AuthenticationWithEmail.class);
}

@Override
public Argument<AuthenticationWithEmail> argumentType() {
return argumentType;
}

@Override
public BindingResult<AuthenticationWithEmail> bind(ArgumentConversionContext<AuthenticationWithEmail> context, HttpRequest<?> source) {
if (!source.getAttributes().contains(SecurityFilter.KEY)) {
return BindingResult.UNSATISFIED;
}
final Optional<Authentication> existing = source.getUserPrincipal(Authentication.class);
return existing.isPresent() ? (() -> existing.map(AuthenticationWithEmail::of)) : BindingResult.EMPTY;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.micronaut.security.docs.customauthentication;

import io.micronaut.context.annotation.Property;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.rules.SecurityRule;
import io.micronaut.security.token.validator.TokenValidator;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;

import java.util.Collections;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;

@Property(name = "spec.name", value = "CustomAuthenticationTest")
@Property(name = "micronaut.security.reject-not-found", value = StringUtils.FALSE)
@MicronautTest
class CustomAuthenticationTest {

@Test
void customAuthentication(@Client("/") HttpClient httpClient) {
BlockingHttpClient client = httpClient.toBlocking();
HttpRequest<?> request = HttpRequest.GET("/custom-authentication")
.accept(MediaType.TEXT_PLAIN)
.bearerAuth("xxx");
String email = assertDoesNotThrow(() -> client.retrieve(request));
assertEquals("[email protected]", email);
}

@Requires(property = "spec.name", value = "CustomAuthenticationTest")
@Controller
static class CustomAuthenticationController {
//tag::method[]
@Secured(SecurityRule.IS_AUTHENTICATED)
@Produces(MediaType.TEXT_PLAIN)
@Get("/custom-authentication")
String index(AuthenticationWithEmail authentication) {
return authentication.email();
}
//end::method[]
}

@Requires(property = "spec.name", value = "CustomAuthenticationTest")
@Controller
static class CustomAuthenticationProvider implements TokenValidator<HttpRequest<?>> {
@Override
public Publisher<Authentication> validateToken(String token, HttpRequest<?> request) {
return Mono.just(Authentication.build("sherlock", Collections.singletonMap("email", "[email protected]")));
}
}
}

0 comments on commit 22dbbc0

Please sign in to comment.