Skip to content

Commit 3ab8d44

Browse files
authored
Merge pull request #594 from yidongnan/feature/security-api-refactor
Improve authentication capabilities
2 parents d616e98 + 72c249f commit 3ab8d44

File tree

5 files changed

+114
-21
lines changed

5 files changed

+114
-21
lines changed

grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/GrpcAuthenticationReader.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,17 @@
2626

2727
import io.grpc.Metadata;
2828
import io.grpc.ServerCall;
29+
import io.grpc.Status;
2930

3031
/**
3132
* Reads the authentication data from the given {@link ServerCall} and {@link Metadata}. The returned
3233
* {@link Authentication} is not yet validated and needs to be passed to an {@link AuthenticationManager}.
3334
*
3435
* <p>
36+
* This is similar to the {@code org.springframework.security.web.authentication.AuthenticationConverter}.
37+
* </p>
38+
*
39+
* <p>
3540
* <b>Note:</b> The authentication manager needs a corresponding {@link AuthenticationProvider} to actually verify the
3641
* {@link Authentication}.
3742
* </p>
@@ -47,7 +52,9 @@ public interface GrpcAuthenticationReader {
4752
* <p>
4853
* <b>Note:</b> Implementations are free to throw an {@link AuthenticationException} if no credentials could be
4954
* found in the call. If an exception is thrown by an implementation then the authentication attempt should be
50-
* considered as failed and no subsequent {@link GrpcAuthenticationReader}s should be called.
55+
* considered as failed and no subsequent {@link GrpcAuthenticationReader}s should be called. Additionally, the call
56+
* will fail as {@link Status#UNAUTHENTICATED}. If the call instead returns {@code null}, then the call processing
57+
* will proceed unauthenticated.
5158
* </p>
5259
*
5360
* @param call The call to get that send the request.

grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/AuthenticatingServerInterceptor.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package net.devh.boot.grpc.server.security.interceptors;
1919

2020
import org.springframework.security.core.Authentication;
21+
import org.springframework.security.core.context.SecurityContext;
2122

2223
import io.grpc.Context;
2324
import io.grpc.Contexts;
@@ -46,6 +47,14 @@ public interface AuthenticatingServerInterceptor extends ServerInterceptor {
4647
/**
4748
* The context key that can be used to retrieve the associated {@link Authentication}.
4849
*/
49-
public static final Context.Key<Authentication> AUTHENTICATION_CONTEXT_KEY = Context.key("authentication");
50+
Context.Key<SecurityContext> SECURITY_CONTEXT_KEY = Context.key("security-context");
51+
52+
/**
53+
* The context key that can be used to retrieve the originally associated {@link Authentication}.
54+
*
55+
* @deprecated Use {@link #SECURITY_CONTEXT_KEY} instead.
56+
*/
57+
@Deprecated
58+
Context.Key<Authentication> AUTHENTICATION_CONTEXT_KEY = Context.key("authentication");
5059

5160
}

grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/DefaultAuthenticatingServerInterceptor.java

Lines changed: 94 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.security.authentication.BadCredentialsException;
2929
import org.springframework.security.core.Authentication;
3030
import org.springframework.security.core.AuthenticationException;
31+
import org.springframework.security.core.context.SecurityContext;
3132
import org.springframework.security.core.context.SecurityContextHolder;
3233

3334
import io.grpc.Context;
@@ -47,6 +48,10 @@
4748
* authentication to both grpc's {@link Context} and {@link SecurityContextHolder}.
4849
*
4950
* <p>
51+
* This works similar to the {@code org.springframework.security.web.authentication.AuthenticationFilter}.
52+
* </p>
53+
*
54+
* <p>
5055
* <b>Note:</b> This interceptor works similar to
5156
* {@link Contexts#interceptCall(Context, ServerCall, Metadata, ServerCallHandler)}.
5257
* </p>
@@ -89,7 +94,7 @@ public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(final ServerCall<Re
8994
try {
9095
return next.startCall(call, headers);
9196
} catch (final AccessDeniedException e) {
92-
throw new BadCredentialsException("No credentials found in the request", e);
97+
throw newNoCredentialsException(e);
9398
}
9499
}
95100
if (authentication.getDetails() == null && authentication instanceof AbstractAuthenticationToken) {
@@ -103,29 +108,101 @@ public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(final ServerCall<Re
103108
authentication = this.authenticationManager.authenticate(authentication);
104109
} catch (final AuthenticationException e) {
105110
log.debug("Authentication request failed: {}", e.getMessage());
111+
onUnsuccessfulAuthentication(call, headers, e);
106112
throw e;
107113
}
108114

109-
final Context context = Context.current().withValue(AUTHENTICATION_CONTEXT_KEY, authentication);
110-
final Context previousContext = context.attach();
111-
SecurityContextHolder.getContext().setAuthentication(authentication);
115+
final SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
116+
securityContext.setAuthentication(authentication);
117+
SecurityContextHolder.setContext(securityContext);
118+
@SuppressWarnings("deprecation")
119+
final Context grpcContext = Context.current().withValues(
120+
SECURITY_CONTEXT_KEY, securityContext,
121+
AUTHENTICATION_CONTEXT_KEY, authentication);
122+
final Context previousContext = grpcContext.attach();
112123
log.debug("Authentication successful: Continuing as {} ({})", authentication.getName(),
113124
authentication.getAuthorities());
125+
onSuccessfulAuthentication(call, headers, authentication);
114126
try {
115-
return new AuthenticatingServerCallListener<>(next.startCall(call, headers), context, authentication);
127+
return new AuthenticatingServerCallListener<>(next.startCall(call, headers), grpcContext, securityContext);
116128
} catch (final AccessDeniedException e) {
117129
if (authentication instanceof AnonymousAuthenticationToken) {
118-
throw new BadCredentialsException("No credentials found in the request", e);
130+
throw newNoCredentialsException(e);
119131
} else {
120132
throw e;
121133
}
122134
} finally {
123135
SecurityContextHolder.clearContext();
124-
context.detach(previousContext);
136+
grpcContext.detach(previousContext);
125137
log.debug("startCall - Authentication cleared");
126138
}
127139
}
128140

141+
/**
142+
* Hook that will be called on successful authentication. Implementations may only use the call instance in a
143+
* non-disruptive manor, that is accessing call attributes or the call descriptor. Implementations must not pollute
144+
* the current thread/context with any call-related state, including authentication, beyond the duration of the
145+
* method invocation. At the time of calling both the grpc context and the security context have been updated to
146+
* reflect the state of the authentication and thus don't have to be setup manually.
147+
*
148+
* <p>
149+
* <b>Note:</b> This method is called regardless of whether the authenticated user is authorized or not to perform
150+
* the requested action.
151+
* </p>
152+
*
153+
* <p>
154+
* By default, this method does nothing.
155+
* </p>
156+
*
157+
* @param call The call instance to receive response messages.
158+
* @param headers The headers associated with the call.
159+
* @param authentication The successful authentication instance.
160+
*/
161+
protected void onSuccessfulAuthentication(
162+
final ServerCall<?, ?> call,
163+
final Metadata headers,
164+
final Authentication authentication) {
165+
// Overwrite to add custom behavior.
166+
}
167+
168+
/**
169+
* Hook that will be called on unsuccessful authentication. Implementations must use the call instance only in a
170+
* non-disruptive manner, i.e. to access call attributes or the call descriptor. Implementations must not close the
171+
* call and must not pollute the current thread/context with any call-related state, including authentication,
172+
* beyond the duration of the method invocation.
173+
*
174+
* <p>
175+
* <b>Note:</b> This method is called only if the request contains an authentication but the
176+
* {@link AuthenticationManager} considers it invalid. This method is not called if an authenticated user is not
177+
* authorized to perform the requested action.
178+
* </p>
179+
*
180+
* <p>
181+
* By default, this method does nothing.
182+
* </p>
183+
*
184+
* @param call The call instance to receive response messages.
185+
* @param headers The headers associated with the call.
186+
* @param failed The exception related to the unsuccessful authentication.
187+
*/
188+
protected void onUnsuccessfulAuthentication(
189+
final ServerCall<?, ?> call,
190+
final Metadata headers,
191+
final AuthenticationException failed) {
192+
// Overwrite to add custom behavior.
193+
}
194+
195+
/**
196+
* Wraps the given {@link AccessDeniedException} in an {@link AuthenticationException} to reflect, that no
197+
* authentication was originally present in the request.
198+
*
199+
* @param denied The caught exception.
200+
* @return The newly created {@link AuthenticationException}.
201+
*/
202+
private static AuthenticationException newNoCredentialsException(final AccessDeniedException denied) {
203+
return new BadCredentialsException("No credentials found in the request", denied);
204+
}
205+
129206
/**
130207
* A call listener that will set the authentication context using {@link SecurityContextHolder} before each
131208
* invocation and clear it afterwards.
@@ -134,25 +211,25 @@ public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(final ServerCall<Re
134211
*/
135212
private static class AuthenticatingServerCallListener<ReqT> extends AbstractAuthenticatingServerCallListener<ReqT> {
136213

137-
private final Authentication authentication;
214+
private final SecurityContext securityContext;
138215

139216
/**
140217
* Creates a new AuthenticatingServerCallListener which will attach the given security context before delegating
141218
* to the given listener.
142219
*
143220
* @param delegate The listener to delegate to.
144-
* @param context The context to attach.
145-
* @param authentication The authentication instance to attach.
221+
* @param grpcContext The context to attach.
222+
* @param securityContext The security context instance to attach.
146223
*/
147-
public AuthenticatingServerCallListener(final Listener<ReqT> delegate, final Context context,
148-
final Authentication authentication) {
149-
super(delegate, context);
150-
this.authentication = authentication;
224+
public AuthenticatingServerCallListener(final Listener<ReqT> delegate, final Context grpcContext,
225+
final SecurityContext securityContext) {
226+
super(delegate, grpcContext);
227+
this.securityContext = securityContext;
151228
}
152229

153230
@Override
154231
protected void attachAuthenticationContext() {
155-
SecurityContextHolder.getContext().setAuthentication(this.authentication);
232+
SecurityContextHolder.setContext(this.securityContext);
156233
}
157234

158235
@Override
@@ -165,8 +242,8 @@ public void onHalfClose() {
165242
try {
166243
super.onHalfClose();
167244
} catch (final AccessDeniedException e) {
168-
if (this.authentication instanceof AnonymousAuthenticationToken) {
169-
throw new BadCredentialsException("No credentials found in the request", e);
245+
if (this.securityContext.getAuthentication() instanceof AnonymousAuthenticationToken) {
246+
throw newNoCredentialsException(e);
170247
} else {
171248
throw e;
172249
}

tests/src/test/java/net/devh/boot/grpc/test/interceptor/DefaultServerInterceptorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
ManualSecurityConfiguration.class})
4949
@EnableAutoConfiguration
5050
@DirtiesContext
51-
public class DefaultServerInterceptorTest {
51+
class DefaultServerInterceptorTest {
5252

5353
@Autowired
5454
private ApplicationContext applicationContext;

tests/src/test/java/net/devh/boot/grpc/test/server/TestServiceImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ protected void assertSameAuthenticatedGrcContextCancellation(final String method
184184
protected Authentication assertSameAuthenticatedGrcContextOnly(final String method, final Authentication expected,
185185
final Context context) {
186186
return assertSameAuthenticated(method, expected,
187-
AuthenticatingServerInterceptor.AUTHENTICATION_CONTEXT_KEY.get(context));
187+
AuthenticatingServerInterceptor.SECURITY_CONTEXT_KEY.get(context).getAuthentication());
188188
}
189189

190190
protected Authentication assertSameAuthenticated(final String method, final Authentication expected) {

0 commit comments

Comments
 (0)