Skip to main content

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!