Skip to content
Open
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
@@ -0,0 +1,140 @@
/*
* Copyright 2024-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.grpc.server.security;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.concurrent.atomic.AtomicReference;

import org.junit.jupiter.api.Test;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;

import com.google.protobuf.Empty;
import io.grpc.Attributes;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.protobuf.ProtoUtils;

/**
* Tests for the composition of the {@link GrpcAuthenticationExtractor} instances
* registered on {@link GrpcSecurity}.
*/
class GrpcSecurityAuthenticationExtractorTests {

private static final MethodDescriptor<Empty, Empty> METHOD = MethodDescriptor.<Empty, Empty>newBuilder()
.setType(MethodDescriptor.MethodType.UNARY)
.setFullMethodName("Simple/SayHello")
.setRequestMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance()))
.setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance()))
.build();

@Test
void firstNonNullExtractorWins() {
assertThat(authenticate(none(), named("first"), named("second"))).isEqualTo("first");
}

@Test
void unauthenticatedWhenNoExtractorMatches() {
assertThat(authenticate(none(), none())).isNull();
}

@Test
void extractorsAreAppliedInAnnotatedOrder() {
assertThat(authenticate(new LateExtractor(), new EarlyExtractor())).isEqualTo("early");
}

/**
* Drives a call through the interceptor built by {@link GrpcSecurity} and reports the
* name of the {@link Authentication} the extractors produced, or {@code null} if the
* call was left unauthenticated.
*/
private static String authenticate(GrpcAuthenticationExtractor... extractors) {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
context.refresh();
GrpcSecurity grpc = new GrpcSecurity(ObjectPostProcessor.identity(),
new AuthenticationManagerBuilder(ObjectPostProcessor.identity()), context);
AtomicReference<String> authenticated = new AtomicReference<>();
grpc.authenticationManager((authentication) -> {
authenticated.set(authentication.getName());
return authentication;
});
grpc.authorizationManager(AuthenticatedAuthorizationManager.authenticated());
for (GrpcAuthenticationExtractor extractor : extractors) {
grpc.authenticationExtractor(extractor);
}

AuthenticationProcessInterceptor interceptor = grpc.build();
@SuppressWarnings("unchecked")
ServerCall<Empty, Empty> call = mock(ServerCall.class);
when(call.getAttributes()).thenReturn(Attributes.EMPTY);
when(call.getMethodDescriptor()).thenReturn(METHOD);
@SuppressWarnings("unchecked")
ServerCallHandler<Empty, Empty> next = mock(ServerCallHandler.class);
try {
interceptor.interceptCall(call, new Metadata(), next);
}
catch (RuntimeException ex) {
// an unauthenticated call is rejected before any extractor result is
// recorded
}
return authenticated.get();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}

private static GrpcAuthenticationExtractor named(String name) {
return (headers, attributes, method) -> new TestingAuthenticationToken(name, "n/a", "ROLE_USER");
}

private static GrpcAuthenticationExtractor none() {
return (headers, attributes, method) -> null;
}

@Order(1)
static class EarlyExtractor implements GrpcAuthenticationExtractor {

@Override
public Authentication extract(Metadata headers, Attributes attributes, MethodDescriptor<?, ?> method) {
return new TestingAuthenticationToken("early", "n/a", "ROLE_USER");
}

}

@Order(2)
static class LateExtractor implements GrpcAuthenticationExtractor {

@Override
public Authentication extract(Metadata headers, Attributes attributes, MethodDescriptor<?, ?> method) {
return new TestingAuthenticationToken("late", "n/a", "ROLE_USER");
}

}

}
29 changes: 29 additions & 0 deletions spring-grpc-docs/src/main/antora/modules/ROOT/pages/server.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,35 @@ We also enable HTTP Basic authentication and preauthentication (mTLS) (`withDefa

Similar to the way Spring Boot works https://docs.spring.io/spring-boot/reference/web/spring-security.html#web.security.oauth2.server[with normal web applications], if you have the `spring-security-oauth2-resource-server` dependency on the classpath, Spring gRPC will be able to configure an OAuth2 resource server through the javadoc:org.springframework.grpc.server.security.GrpcSecurity[] configurer.

==== Custom Credentials

Authentication is extracted from the call by a javadoc:org.springframework.grpc.server.security.GrpcAuthenticationExtractor[], which reads the request `Metadata`, the transport `Attributes` and the `MethodDescriptor` being invoked.
If your credentials do not arrive in the `Authorization` metadata entry, implement that interface and register it with `authenticationExtractor()`:

[source,java]
----
static final Metadata.Key<String> SESSION_KEY = Metadata.Key.of("x-session-token",
Metadata.ASCII_STRING_MARSHALLER);

@Bean
@GlobalServerInterceptor
AuthenticationProcessInterceptor customSecurityFilterChain(GrpcSecurity grpc) throws Exception {
return grpc
.authorizeRequests(requests -> requests
.methods("grpc.*/*").permitAll()
.allRequests().authenticated())
.authenticationExtractor((headers, attributes, method) -> {
String token = headers.get(SESSION_KEY);
return (token != null) ? new BearerTokenAuthenticationToken(token) : null;
})
.build();
}
----

The `AuthenticationManager` still has to be able to authenticate whatever token you return, so pair the extractor with a matching `AuthenticationProvider`.

Extractors are applied in order and the first non-`null` `Authentication` wins, so a custom extractor can coexist with the built-in ones.

=== Servlet

The servlet-based server supports any security configuration that the servlet container supports, including Spring Security.
Expand Down