Class 8: Security
Duration: 30 minutes Difficulty: Intermediate-Advanced Prerequisites: Completed Classes 1-7, basic Spring Security knowledge
What You'll Learn
By the end of this class, you will:
- Add authentication to your GraphQL API
- Implement authorization at query/mutation level
- Create field-level security rules
- Protect sensitive data based on user roles
- Secure subscriptions
GraphQL Security Challenges
GraphQL's flexibility creates unique security challenges:
┌─────────────────────────────────────────────────────────────────────┐
│ GRAPHQL SECURITY CHALLENGES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. SINGLE ENDPOINT │
│ - Can't use URL-based security rules │
│ - All operations go through /graphql │
│ │
│ 2. FLEXIBLE QUERIES │
│ - Clients choose which fields to request │
│ - Same type may have public and private fields │
│ │
│ 3. NESTED DATA │
│ - user.posts.comments.author.privateEmail │
│ - Authorization must be checked at each level │
│ │
│ 4. COMPLEXITY ATTACKS │
│ - Deeply nested queries can DOS your server │
│ - { a { b { c { d { e { f { ... } } } } } } } │
│ │
└─────────────────────────────────────────────────────────────────────┘
Step 1: Add Spring Security
Add the dependency to your pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Step 2: Configure Security
📁 src/main/java/com/example/moviedb/config/SecurityConfig.java
package com.example.moviedb.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // Enable @PreAuthorize
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // Disable CSRF for GraphQL
.authorizeHttpRequests(auth -> auth
.requestMatchers("/graphiql/**").permitAll() // Allow GraphiQL
.requestMatchers("/graphql/**").permitAll() // Allow GraphQL (we'll secure at resolver level)
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults()) // Enable Basic Auth
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
var admin = User.builder()
.username("admin")
.password(encoder.encode("admin123"))
.roles("ADMIN", "USER")
.build();
var editor = User.builder()
.username("editor")
.password(encoder.encode("editor123"))
.roles("EDITOR", "USER")
.build();
var user = User.builder()
.username("user")
.password(encoder.encode("user123"))
.roles("USER")
.build();
var guest = User.builder()
.username("guest")
.password(encoder.encode("guest123"))
.roles("GUEST")
.build();
return new InMemoryUserDetailsManager(admin, editor, user, guest);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Step 3: Add Security Context to GraphQL
📁 src/main/java/com/example/moviedb/config/GraphQLSecurityConfig.java
package com.example.moviedb.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import reactor.core.publisher.Mono;
@Configuration
public class GraphQLSecurityConfig {
@Bean
public WebGraphQlInterceptor securityInterceptor() {
return (request, chain) -> {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// Add authentication to GraphQL context
request.configureExecutionInput((input, builder) ->
builder.graphQLContext(ctx -> {
ctx.put("authentication", auth);
ctx.put("isAuthenticated", auth != null && auth.isAuthenticated()
&& !auth.getName().equals("anonymousUser"));
ctx.put("username", auth != null ? auth.getName() : null);
ctx.put("roles", auth != null ? auth.getAuthorities() : null);
}).build()
);
return chain.next(request);
};
}
}
Step 4: Secure Mutations with @PreAuthorize
📁 Update src/main/java/com/example/moviedb/controller/MutationController.java:
package com.example.moviedb.controller;
import org.springframework.security.access.prepost.PreAuthorize;
// ... other imports
@Controller
public class MutationController {
// ... constructor and fields
@MutationMapping
@PreAuthorize("hasRole('EDITOR') or hasRole('ADMIN')")
public Movie createMovie(@Argument CreateMovieInput input) {
// Only EDITOR and ADMIN can create movies
// ... implementation
}
@MutationMapping
@PreAuthorize("hasRole('EDITOR') or hasRole('ADMIN')")
public Movie updateMovie(@Argument String id, @Argument UpdateMovieInput input) {
// Only EDITOR and ADMIN can update movies
// ... implementation
}
@MutationMapping
@PreAuthorize("hasRole('ADMIN')")
public DeleteResponse deleteMovie(@Argument String id) {
// Only ADMIN can delete movies
// ... implementation
}
@MutationMapping
@PreAuthorize("hasRole('ADMIN')")
public Actor createActor(@Argument CreateActorInput input) {
// ... implementation
}
@MutationMapping
@PreAuthorize("hasRole('ADMIN')")
public Director createDirector(@Argument CreateDirectorInput input) {
// ... implementation
}
@MutationMapping
@PreAuthorize("hasRole('EDITOR') or hasRole('ADMIN')")
public Movie addActorToMovie(@Argument String movieId, @Argument String actorId) {
// ... implementation
}
@MutationMapping
@PreAuthorize("hasRole('EDITOR') or hasRole('ADMIN')")
public Movie removeActorFromMovie(@Argument String movieId, @Argument String actorId) {
// ... implementation
}
}
Step 5: Field-Level Security
Some fields should only be visible to certain users. Let's add admin-only fields:
📁 Update schema:
type Movie {
id: ID!
title: String!
releaseYear: Int!
genre: Genre!
rating: Float
runtime: Int
plot: String
inTheaters: Boolean!
director: Director!
actors: [Actor!]!
# Admin-only fields
"Internal notes (admin only)"
internalNotes: String
"Revenue data (admin only)"
boxOfficeRevenue: Float
"Production budget (admin only)"
productionBudget: Float
}
type Actor {
id: ID!
name: String!
birthYear: Int
nationality: String
movies: [Movie!]!
# Admin-only field
"Agent contact info (admin only)"
agentContact: String
}
📁 Create secured field resolvers:
package com.example.moviedb.controller;
import com.example.moviedb.exception.ForbiddenException;
import com.example.moviedb.model.Movie;
import com.example.moviedb.model.Actor;
import graphql.schema.DataFetchingEnvironment;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Controller;
import java.util.Collection;
@Controller
public class SecuredFieldController {
// ===== ADMIN-ONLY MOVIE FIELDS =====
@SchemaMapping(typeName = "Movie", field = "internalNotes")
public String internalNotes(Movie movie, DataFetchingEnvironment env) {
requireRole(env, "ROLE_ADMIN");
return "Internal note for movie: " + movie.getId(); // Mock data
}
@SchemaMapping(typeName = "Movie", field = "boxOfficeRevenue")
public Double boxOfficeRevenue(Movie movie, DataFetchingEnvironment env) {
requireRole(env, "ROLE_ADMIN");
return movie.getRating() != null ? movie.getRating() * 100_000_000 : null; // Mock
}
@SchemaMapping(typeName = "Movie", field = "productionBudget")
public Double productionBudget(Movie movie, DataFetchingEnvironment env) {
requireRole(env, "ROLE_ADMIN");
return movie.getRuntime() != null ? movie.getRuntime() * 500_000.0 : null; // Mock
}
// ===== ADMIN-ONLY ACTOR FIELDS =====
@SchemaMapping(typeName = "Actor", field = "agentContact")
public String agentContact(Actor actor, DataFetchingEnvironment env) {
requireRole(env, "ROLE_ADMIN");
return "agent@" + actor.getName().toLowerCase().replace(" ", "") + ".com"; // Mock
}
// ===== HELPER METHODS =====
private void requireRole(DataFetchingEnvironment env, String role) {
Authentication auth = env.getGraphQlContext().get("authentication");
if (auth == null || !auth.isAuthenticated()) {
throw new ForbiddenException("Authentication required");
}
boolean hasRole = auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.anyMatch(r -> r.equals(role));
if (!hasRole) {
throw new ForbiddenException("Access denied: requires " + role);
}
}
private boolean hasAnyRole(DataFetchingEnvironment env, String... roles) {
Authentication auth = env.getGraphQlContext().get("authentication");
if (auth == null) return false;
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
for (String role : roles) {
if (authorities.stream().anyMatch(a -> a.getAuthority().equals(role))) {
return true;
}
}
return false;
}
}
Step 6: Add User Query for Current User
📁 Update schema:
type Query {
# ... existing queries
"Get current authenticated user info"
me: UserInfo
}
type UserInfo {
username: String!
roles: [String!]!
isAdmin: Boolean!
}
📁 Add to controller:
@QueryMapping
public UserInfo me(DataFetchingEnvironment env) {
Authentication auth = env.getGraphQlContext().get("authentication");
if (auth == null || !auth.isAuthenticated() || auth.getName().equals("anonymousUser")) {
return null;
}
List<String> roles = auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList();
boolean isAdmin = roles.contains("ROLE_ADMIN");
return new UserInfo(auth.getName(), roles, isAdmin);
}
📁 Create UserInfo class:
package com.example.moviedb.model;
import java.util.List;
public class UserInfo {
private String username;
private List<String> roles;
private boolean isAdmin;
public UserInfo(String username, List<String> roles, boolean isAdmin) {
this.username = username;
this.roles = roles;
this.isAdmin = isAdmin;
}
public String getUsername() { return username; }
public List<String> getRoles() { return roles; }
public boolean isAdmin() { return isAdmin; }
}
Step 7: Test Security
Restart your application and test with different users.
Test Without Authentication
query {
movies {
title
}
}
This should work - queries are public.
Test Admin-Only Fields Without Auth
query {
movie(id: "1") {
title
internalNotes
}
}
You'll get an error for internalNotes.
Test With Basic Auth
In GraphiQL or Postman, add the Authorization header:
Authorization: Basic YWRtaW46YWRtaW4xMjM=
(This is base64 for admin:admin123)
Now the admin-only fields should work!
Test Role-Based Mutations
As a regular user, try:
mutation {
createMovie(input: {
title: "Unauthorized Movie"
releaseYear: 2024
genre: ACTION
directorId: "1"
}) {
id
}
}
With user credentials, this should fail with "Access Denied".
Test Me Query
query {
me {
username
roles
isAdmin
}
}
Security Best Practices
┌─────────────────────────────────────────────────────────────────────┐
│ GRAPHQL SECURITY CHECKLIST │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ Authentication │
│ □ Validate tokens/credentials on every request │
│ □ Use secure token storage (HttpOnly cookies or secure headers) │
│ □ Implement token refresh mechanism │
│ │
│ ✅ Authorization │
│ □ Check permissions at resolver level │
│ □ Implement field-level security for sensitive data │
│ □ Use @PreAuthorize for method-level security │
│ │
│ ✅ Input Validation │
│ □ Validate all input types │
│ □ Sanitize string inputs to prevent injection │
│ □ Limit list sizes and string lengths │
│ │
│ ✅ Query Protection │
│ □ Implement query complexity limits │
│ □ Limit query depth │
│ □ Set timeouts on queries │
│ □ Rate limit by user/IP │
│ │
│ ✅ Error Handling │
│ □ Don't expose internal errors to clients │
│ □ Log security-related errors │
│ □ Return generic errors for auth failures │
│ │
└─────────────────────────────────────────────────────────────────────┘
Query Complexity Limits
Prevent complex queries from overwhelming your server:
📁 src/main/java/com/example/moviedb/config/QueryComplexityConfig.java
package com.example.moviedb.config;
import graphql.analysis.MaxQueryComplexityInstrumentation;
import graphql.analysis.MaxQueryDepthInstrumentation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
@Configuration
public class QueryComplexityConfig {
@Bean
public MaxQueryDepthInstrumentation maxQueryDepthInstrumentation() {
return new MaxQueryDepthInstrumentation(10); // Max 10 levels deep
}
@Bean
public MaxQueryComplexityInstrumentation maxQueryComplexityInstrumentation() {
return new MaxQueryComplexityInstrumentation(100); // Max complexity score
}
}
Exercises
Exercise 1: Add Rate Limiting
Implement a simple rate limiter that limits users to 100 queries per minute.
Exercise 2: Owner-Based Access
Make it so users can only update movies they created (add a createdBy field to Movie).
Exercise 3: Subscription Security
Secure subscriptions so only authenticated users can subscribe to movieAdded.
Solution
@SubscriptionMapping
public Flux<Movie> movieAdded(DataFetchingEnvironment env) {
Authentication auth = env.getGraphQlContext().get("authentication");
if (auth == null || !auth.isAuthenticated()) {
return Flux.error(new ForbiddenException("Authentication required"));
}
return eventPublisher.getMovieAddedFlux();
}
Summary
In this class, you learned:
✅ Configuring Spring Security for GraphQL
✅ Using @PreAuthorize for mutation security
✅ Implementing field-level security
✅ Adding authentication context to GraphQL
✅ Query complexity and depth limits
✅ Security best practices for GraphQL
What's Next?
In Class 9: Testing, we'll learn:
- Unit testing resolvers
- Integration testing with GraphQlTester
- Testing mutations and subscriptions
- Mocking data sources
Time to ensure your API works correctly!