Skip to content

Commit 1171aee

Browse files
rstoyanchevjzheaux
andcommitted
Add AuthenticationWebSocketInterceptor
See gh-268 Co-authored-by: Josh Cummings <[email protected]>
1 parent f354950 commit 1171aee

File tree

6 files changed

+343
-0
lines changed

6 files changed

+343
-0
lines changed

spring-graphql/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dependencies {
1919
compileOnly 'jakarta.validation:jakarta.validation-api'
2020

2121
compileOnly 'org.springframework.security:spring-security-core'
22+
compileOnly 'org.springframework.security:spring-security-oauth2-resource-server'
2223

2324
compileOnly 'com.querydsl:querydsl-core'
2425
compileOnly 'org.springframework.data:spring-data-commons'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.server.support;
18+
19+
import java.util.Map;
20+
21+
import reactor.core.publisher.Mono;
22+
import reactor.util.context.ContextView;
23+
24+
import org.springframework.graphql.server.WebGraphQlRequest;
25+
import org.springframework.graphql.server.WebGraphQlResponse;
26+
import org.springframework.graphql.server.WebSocketGraphQlInterceptor;
27+
import org.springframework.graphql.server.WebSocketGraphQlRequest;
28+
import org.springframework.graphql.server.WebSocketSessionInfo;
29+
import org.springframework.security.core.Authentication;
30+
import org.springframework.security.core.context.SecurityContext;
31+
32+
/**
33+
* Base class for interceptors that extract an {@link Authentication} from
34+
* the payload of a {@code "connection_init"} GraphQL over WebSocket message.
35+
* The authentication is saved in WebSocket attributes from where it is later
36+
* accessed and propagated to subsequent {@code "subscribe"} messages.
37+
*
38+
* @author Joshua Cummings
39+
* @author Rossen Stoyanchev
40+
* @since 1.3.0
41+
*/
42+
public abstract class AbstractAuthenticationWebSocketInterceptor implements WebSocketGraphQlInterceptor {
43+
44+
private static final String AUTHENTICATION_ATTRIBUTE =
45+
AbstractAuthenticationWebSocketInterceptor.class.getName() + ".AUTHENTICATION";
46+
47+
48+
private final AuthenticationExtractor authenticationExtractor;
49+
50+
51+
/**
52+
* Constructor with the strategy to use to extract the authentication value
53+
* from the {@code "connection_init"} message.
54+
* @param authExtractor the extractor to use
55+
*/
56+
public AbstractAuthenticationWebSocketInterceptor(AuthenticationExtractor authExtractor) {
57+
this.authenticationExtractor = authExtractor;
58+
}
59+
60+
@Override
61+
public Mono<Object> handleConnectionInitialization(WebSocketSessionInfo info, Map<String, Object> payload) {
62+
return this.authenticationExtractor.getAuthentication(payload)
63+
.flatMap(this::getSecurityContext)
64+
.doOnNext((securityContext) -> info.getAttributes().put(AUTHENTICATION_ATTRIBUTE, securityContext))
65+
.then(Mono.empty());
66+
}
67+
68+
/**
69+
* Subclasses implement this method to return an authenticated
70+
* {@link SecurityContext} or an error.
71+
* @param authentication the authentication value extracted from the payload
72+
*/
73+
protected abstract Mono<SecurityContext> getSecurityContext(Authentication authentication);
74+
75+
@Override
76+
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
77+
if (!(request instanceof WebSocketGraphQlRequest webSocketRequest)) {
78+
return chain.next(request);
79+
}
80+
Map<String, Object> attributes = webSocketRequest.getSessionInfo().getAttributes();
81+
SecurityContext securityContext = (SecurityContext) attributes.get(AUTHENTICATION_ATTRIBUTE);
82+
ContextView contextView = getContextToWrite(securityContext);
83+
return chain.next(request).contextWrite(contextView);
84+
}
85+
86+
/**
87+
* Subclasses implement this to decide how to insert the {@link SecurityContext}
88+
* into the Reactor context of the {@link WebSocketGraphQlInterceptor} chain.
89+
* @param securityContext the {@code SecurityContext} to write to the context
90+
*/
91+
protected abstract ContextView getContextToWrite(SecurityContext securityContext);
92+
93+
}
94+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.server.support;
18+
19+
import java.util.Map;
20+
21+
import reactor.core.publisher.Mono;
22+
23+
import org.springframework.security.core.Authentication;
24+
25+
/**
26+
* Strategy to extract an {@link Authentication} from the payload of a
27+
* {@code "connection_init"} GraphQL over WebSocket message.
28+
*
29+
* @author Joshua Cummings
30+
* @author Rossen Stoyanchev
31+
* @since 1.3.0
32+
*/
33+
public interface AuthenticationExtractor {
34+
35+
/**
36+
* Return the authentication contained in the given payload, or an empty {@code Mono}.
37+
* @param payload the payload to extract the authentication value from
38+
*/
39+
Mono<Authentication> getAuthentication(Map<String, Object> payload);
40+
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.server.support;
18+
19+
import java.util.Map;
20+
import java.util.regex.Matcher;
21+
import java.util.regex.Pattern;
22+
23+
import reactor.core.publisher.Mono;
24+
25+
import org.springframework.security.core.Authentication;
26+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
27+
import org.springframework.security.oauth2.server.resource.BearerTokenError;
28+
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
29+
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
30+
import org.springframework.util.StringUtils;
31+
32+
/**
33+
* {@link AuthenticationExtractor} that extracts a
34+
* <a href="https://datatracker.ietf.org/doc/html/rfc6750#section-1.2">bearer token</a>.
35+
*
36+
* @author Joshua Cummings
37+
* @author Rossen Stoyanchev
38+
* @since 1.3.0
39+
*/
40+
public final class BearerTokenAuthenticationExtractor implements AuthenticationExtractor {
41+
42+
private static final Pattern authorizationPattern =
43+
Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$", Pattern.CASE_INSENSITIVE);
44+
45+
46+
private final String authorizationKey;
47+
48+
49+
/**
50+
* Constructor that defaults the payload key to use to "Authorization".
51+
*/
52+
public BearerTokenAuthenticationExtractor() {
53+
this("Authorization");
54+
}
55+
56+
/**
57+
* Constructor with the key for the authorization value.
58+
* @param authorizationKey the key under which to look up the authorization
59+
* value in the {@code "connection_init"} payload.
60+
*/
61+
public BearerTokenAuthenticationExtractor(String authorizationKey) {
62+
this.authorizationKey = authorizationKey;
63+
}
64+
65+
66+
@Override
67+
public Mono<Authentication> getAuthentication(Map<String, Object> payload) {
68+
String authorizationValue = (String) payload.get(this.authorizationKey);
69+
if (!StringUtils.startsWithIgnoreCase(authorizationValue, "bearer")) {
70+
return Mono.empty();
71+
}
72+
73+
Matcher matcher = authorizationPattern.matcher(authorizationValue);
74+
if (matcher.matches()) {
75+
String token = matcher.group("token");
76+
return Mono.just(new BearerTokenAuthenticationToken(token));
77+
}
78+
79+
BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
80+
return Mono.error(new OAuth2AuthenticationException(error));
81+
}
82+
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.server.webflux;
18+
19+
import reactor.core.publisher.Mono;
20+
import reactor.util.context.ContextView;
21+
22+
import org.springframework.graphql.server.support.AbstractAuthenticationWebSocketInterceptor;
23+
import org.springframework.graphql.server.support.AuthenticationExtractor;
24+
import org.springframework.security.authentication.ReactiveAuthenticationManager;
25+
import org.springframework.security.core.Authentication;
26+
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
27+
import org.springframework.security.core.context.SecurityContext;
28+
import org.springframework.security.core.context.SecurityContextImpl;
29+
30+
/**
31+
* Extension of {@link AbstractAuthenticationWebSocketInterceptor} for use with
32+
* the WebFlux GraphQL transport.
33+
*
34+
* @author Joshua Cummings
35+
* @author Rossen Stoyanchev
36+
* @since 1.3.0
37+
*/
38+
public class AuthenticationWebSocketInterceptor extends AbstractAuthenticationWebSocketInterceptor {
39+
40+
private final ReactiveAuthenticationManager authenticationManager;
41+
42+
43+
public AuthenticationWebSocketInterceptor(
44+
AuthenticationExtractor extractor, ReactiveAuthenticationManager manager) {
45+
46+
super(extractor);
47+
this.authenticationManager = manager;
48+
}
49+
50+
@Override
51+
protected Mono<SecurityContext> getSecurityContext(Authentication authentication) {
52+
return this.authenticationManager.authenticate(authentication).map(SecurityContextImpl::new);
53+
}
54+
55+
@Override
56+
protected ContextView getContextToWrite(SecurityContext securityContext) {
57+
return ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext));
58+
}
59+
60+
}
61+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.server.webmvc;
18+
19+
import reactor.core.publisher.Mono;
20+
import reactor.util.context.Context;
21+
import reactor.util.context.ContextView;
22+
23+
import org.springframework.graphql.server.support.AbstractAuthenticationWebSocketInterceptor;
24+
import org.springframework.graphql.server.support.AuthenticationExtractor;
25+
import org.springframework.security.authentication.AuthenticationManager;
26+
import org.springframework.security.core.Authentication;
27+
import org.springframework.security.core.context.SecurityContext;
28+
import org.springframework.security.core.context.SecurityContextImpl;
29+
30+
/**
31+
* Extension of {@link AbstractAuthenticationWebSocketInterceptor} for use with
32+
* the WebMVC GraphQL transport.
33+
*
34+
* @author Joshua Cummings
35+
* @author Rossen Stoyanchev
36+
* @since 1.3.0
37+
*/
38+
public class AuthenticationWebSocketInterceptor extends AbstractAuthenticationWebSocketInterceptor {
39+
40+
private final AuthenticationManager authenticationManager;
41+
42+
43+
public AuthenticationWebSocketInterceptor(
44+
AuthenticationManager authManager, AuthenticationExtractor authExtractor) {
45+
46+
super(authExtractor);
47+
this.authenticationManager = authManager;
48+
}
49+
50+
@Override
51+
protected Mono<SecurityContext> getSecurityContext(Authentication authentication) {
52+
Authentication authenticate = this.authenticationManager.authenticate(authentication);
53+
return Mono.just(new SecurityContextImpl(authenticate));
54+
}
55+
56+
@Override
57+
protected ContextView getContextToWrite(SecurityContext securityContext) {
58+
String key = SecurityContext.class.getName(); // match SecurityContextThreadLocalAccessor key
59+
return Context.of(key, securityContext);
60+
}
61+
62+
}
63+

0 commit comments

Comments
 (0)