Saturday, July 22, 2023

Securing GraphQL / REST APIs with OAuth2 Resource Server Spring Security 6.1 & Keycloak 22 & Angular 16

Introduction:


Sometimes we need to interact with Spring Boot GraphQL API and Spring Boot Rest API in the same time, in this case we can use keycloak  for authentication to applications and secure services with minimum effort. so in this post, I have used OAuth 2.0 Resource Server in Spring Boot GraphQL and Rest APIs & Keycloak OpenID Connect  with Angular 16. here a basic UML sequence diagram.




Overview:

  • Basic concepts
  • Configuration Spring Boot GraphQL API project
  • Configuration Spring Boot Rest API project  
  • Configuration  and installing Keycloak 22 by docker 
  • Configuration  Angular Application
  • Run & Test

Basic Concepts 


OAuth 2.0 Resource Server


By official documentation Spring Security supports protecting endpoints by using two forms of OAuth 2.0 Bearer Tokens:

  • JWT
  • Opaque Tokens

This is handy in circumstances where an application has delegated its authority management to an authorization server (for example, KeyCloak, Okta or Ping Identity). This authorization server can be consulted by resource servers to authorize requests.

JSON Web Tokens


JSON Web Token JWTs can be encrypted to also provide secrecy between parties, we will focus on signed tokens. Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.

JSON Web Token structure


JSON Web Tokens consist of three parts separated by dots (.), which are:

  • Header
  • Payload
  • Signature

OpenID Connect


OpenID Connect is a simple identity layer on top of the OAuth 2.0 protocol. It allows Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server, as well as to obtain basic profile information about the End-User in an interoperable and REST-like manner.

OpenID Connect allows clients of all types, including Web-based, mobile, and JavaScript clients, to request and receive information about authenticated sessions and end-users. The specification suite is extensible, allowing participants to use optional features such as encryption of identity data, discovery of OpenID Providers, and logout, when it makes sense for them. in OpenID Connect, OAuth 2.0 capabilities are integrated with the protocol itself.

Keycloak


Keycloak is an open source identity and access management for modern applications and services, no need to deal with storing users or authenticating users. it's all available out of the box.

Add authentication to applications and secure services with minimum effort. No need to deal with storing users or authenticating users.

Keycloak provides user federation, strong authentication, user management, fine-grained authorization, and more.

GraphQL API


GraphQL is an open-source query language and runtime for APIs (Application Programming Interfaces). It was developed by Facebook and released in 2015. GraphQL provides a flexible and efficient way to define, query, and manipulate data in APIs.

At its core, GraphQL enables clients to request specific data from the server using a single API endpoint. Unlike traditional REST APIs that expose fixed endpoints with pre-defined data structures, GraphQL allows clients to specify exactly what data they need and receive it in a hierarchical structure. This reduces over-fetching and under-fetching of data, optimizing network usage and improving performance.

REST API


REST API (Representational State Transfer Application Programming Interface) is an architectural style and set of principles for designing networked applications. It is commonly used in web development to create web services that can be accessed and interacted with over the internet.

Angular 16

Angular 16 was released in April 2022, and it includes a number of new features and improvements. Some of the key features of Angular 16 include:

  • Standalone components: Angular 16 introduces the concepts of standalone components, which are components that can be used independently of any other angular code. this makes it easier to reuse components and to create more modular applications.
  • Signals:  Signals are a new way of handling reactivity in Angular. Signals provide a simpler and more efficient way to notify the framework when data changes.
  • Non-destructive hydration: Angular 16 introduces a new approach to hydration called no-destructive hydration. This approach allows Angular applications to be loaded more efficiently and to perform better in terms of performance.
  • Improved security: Angular 16 includes a number of security improvements, including support for native Trusted Types and CSP.
  • For more.


Technology

  • Spring Boot 3.1.1
  • Spring Security 6.1
  • OAuth2 Resource Server
  • Java 17
  • Maven 
  • IntelliJ IDEA Community

Configuration Spring Boot GraphQL API project  :

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.1.1</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.henry</groupId>
	<artifactId>spring-boot3--security6-oauth2-jwt-graphql</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-boot3--security6-oauth2-jwt-graphql</name>
	<description>Integration OAuth 2.0 Resource Server with GraphQL</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>

		<!-- oauth2 resourcer server -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-graphql</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<!-- https://mvnrepository.com/artifact/com.graphql-java/graphql-java-tools -->
		<dependency>
			<groupId>com.graphql-java</groupId>
			<artifactId>graphql-java-tools</artifactId>
			<version>5.2.4</version>
		</dependency>
		<dependency>
			<groupId>com.graphql-java</groupId>
			<artifactId>graphql-java-extended-scalars</artifactId>
			<version>20.2</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.springframework.graphql</groupId>
			<artifactId>spring-graphql-test</artifactId>
			<scope>test</scope>
		</dependency>

    </dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>


application.yml:


server:
  port: 8081
  servlet:
    context-path: /

spring:
  security:
    oauth2:
      resource-server:
        jwt:
          issuer-uri: http://localhost:8080/realms/demo
          jwk-set-uri: http://localhost:8080/realms/demo/protocol/openid-connect/certs

  # graphql Configuration
  graphql:
    graphiql:
      enabled: true

Schema

Spring for GraphQL application, create a directory src/main/resources/graphql. Add a new file schema.graphqls to this folder with the following content:

type Query {
    getName: String
    getJWTByUser: JWTTokenDto
}

type JWTTokenDto {
    sub: String
    resourceAccess: ResourceAccess
    emailVerified: Boolean
    allowedOrigins: [String]
    iss: String
    typ: String
    preferredUsername: String
    givenName: String
    sid: String
    aud: [String]
    acr: String
    realmAccess: RealmAccess
    azp: String
    scope: String
    name: String
    exp: Float
    sessionState: String
    iat: Float
    familyName: String
    jti: String
    email: String
    additionalProperties: JSON
}

type ResourceAccess {
    account: Account
    additionalProperties: JSON
}

type Account {
    roles: [String]
    additionalProperties: JSON
}

type RealmAccess {
    roles: [String]
    additionalProperties: JSON
}

scalar JSON


Configuration class


  • @PreAuthorize annotation checks the given expression before entering the method.
  • @EnableGlobalMethodSecurity To apply security using an annotation-driven approach.
  • Configure the CORS policies. You can do this by defining a bean for CorsConfigurationSource.

package com.henry.springboot3security6oauth2jwtgraphql.configuration;


import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {

        httpSecurity
                .authorizeHttpRequests(registry -> {
                           registry.requestMatchers("/graphql").permitAll();
                           registry.requestMatchers(HttpMethod.POST, "/graphql")
                                   .hasAnyRole("ADMIN", "USER");
                           registry.anyRequest().authenticated();

                        }
                )
                .oauth2ResourceServer(oauth2Configure -> oauth2Configure.jwt(jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwt -> {
                    Map<String, Collection<String>> realmAccess = jwt.getClaim("realm_access");
                    Collection<String> roles = realmAccess.get("roles");
                    var grantedAuthorities = roles.stream()
                            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                            .collect(Collectors.toList());
                    return new JwtAuthenticationToken(jwt, grantedAuthorities);
                })))
                .cors(Customizer.withDefaults()) // Enable CORS support
        ;

        return httpSecurity.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedOrigin("*"); // Set allowed origins here (you can specify specific origins instead of "*")
        configuration.addAllowedMethod("*"); // Set allowed HTTP methods (e.g., GET, POST, etc.)
        configuration.addAllowedHeader("*"); // Set allowed headers (e.g., Content-Type, Authorization, etc.)

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @Bean
    public CorsFilter corsFilter() {
        return new CorsFilter(corsConfigurationSource());
    }
}

package com.henry.springboot3security6oauth2jwtgraphql.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class WebConfig  {

    @Bean
    public WebMvcConfigurer corsConfigurer()
    {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**").allowedOrigins("*");
            }
        };
    }
}


package com.henry.springboot3security6oauth2jwtgraphql.configuration;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Collections;
import java.util.Map;
import graphql.schema.Coercing;
import graphql.schema.CoercingParseLiteralException;
import graphql.schema.CoercingParseValueException;
import graphql.schema.CoercingSerializeException;
import static graphql.scalars.ExtendedScalars.Object;

/**
 * from Stack Overflow
 * https://stackoverflow.com/questions/72915766/spring-graphql-how-to-map-json-extended-scalar-to-pojo-attribute
 * Custom Coercing implementation in order to parse GQL Objects,
 * which get mapped as LinkedHashMap instances by {@link graphql.scalars.object.ObjectScalar}
 * into Jackson's JsonNode objects.
 */
public class JsonNodeCoercing implements Coercing<JsonNode, Object> {

    private static final Coercing<?, ?> COERCING = Object.getCoercing();

    private final ObjectMapper objectMapper;

    public JsonNodeCoercing(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public Object serialize(Object input) throws CoercingSerializeException {
        return input;
    }

    @Override
    public JsonNode parseValue(Object input) throws CoercingParseValueException {
        return objectMapper.valueToTree(input);
    }

    @Override
    public JsonNode parseLiteral(Object input) throws CoercingParseLiteralException {
        return parseLiteral(input, Collections.emptyMap());
    }

    @Override
    public JsonNode parseLiteral(Object input, Map<String, Object> variables) throws CoercingParseLiteralException {
        return objectMapper.valueToTree(COERCING.parseLiteral(input, variables));
    }

}

package com.henry.springboot3security6oauth2jwtgraphql.configuration;

import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.schema.GraphQLScalarType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;

@Configuration
public class GraphQlConfig {

    @Bean
    public RuntimeWiringConfigurer runtimeWiringConfigurer(ObjectMapper objectMapper) {
        GraphQLScalarType jsonScalarType = GraphQLScalarType.newScalar()
                .name("JSON")
                .description("A JSON scalar")
                .coercing(new JsonNodeCoercing(objectMapper))
                .build();

        return wiringBuilder -> wiringBuilder
                .scalar(jsonScalarType);
    }
}


DTO class (in GitHub Repo)


  • Account
  • RealmAccess
  • ResourceAccess
  • JWTTokenDto



Resolver

package com.henry.springboot3security6oauth2jwtgraphql.resolver;


import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.henry.springboot3security6oauth2jwtgraphql.dto.JWTTokenDto;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class Resolver implements GraphQLQueryResolver, GraphQLMutationResolver {

    public static ObjectMapper objectMapper = new ObjectMapper();

    public String getName(JwtAuthenticationToken jwt) throws JsonProcessingException {
        objectMapper.findAndRegisterModules();
        var respData = objectMapper.writeValueAsString(jwt.getTokenAttributes());

        var user = objectMapper.readValue(respData, JWTTokenDto.class);

        return user.getName();
    }

    public JWTTokenDto getJWTByUser(JwtAuthenticationToken jwt) throws JsonProcessingException {
        objectMapper.findAndRegisterModules();
        var respData = objectMapper.writeValueAsString(jwt.getTokenAttributes());

        var user = objectMapper.readValue(respData, JWTTokenDto.class);
        return user;
    }
}


Controller

  • @PreAuthorize annotation checks the given expression before entering the method.

package com.henry.springboot3security6oauth2jwtgraphql.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.henry.springboot3security6oauth2jwtgraphql.dto.JWTTokenDto;
import com.henry.springboot3security6oauth2jwtgraphql.resolver.Resolver;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Controller;

@Controller
public class DefaultJwtController {

    private final Resolver resolver;

    public DefaultJwtController(Resolver resolver) {
        this.resolver = resolver;
    }

    @QueryMapping
    @PreAuthorize("hasAnyRole('ADMIN', 'USER')")
    public String getName(JwtAuthenticationToken jwt) throws JsonProcessingException {
        return resolver.getName(jwt);
    }

    @QueryMapping
    @PreAuthorize("hasAnyRole('ADMIN')")
    public JWTTokenDto getJWTByUser(JwtAuthenticationToken jwt) throws JsonProcessingException {
        return resolver.getJWTByUser(jwt);
    }
}


Configuration Spring Boot Rest API project  :


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.1.1</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.henry</groupId>
	<artifactId>spring-boot3-spring-security6-oauth2-jwt</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-boot3-spring-security6-oauth2-jwt</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<!-- oauth2 resourcer server -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.jsonschema2pojo</groupId>
			<artifactId>jsonschema2pojo-core</artifactId>
			<version>1.1.1</version>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.datatype</groupId>
			<artifactId>jackson-datatype-jsr310</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

application.yml


server:
  port: 8082
  servlet:
    context-path: /

spring:
  security:
    oauth2:
      resource-server:
        jwt:
          issuer-uri: http://localhost:8080/realms/demo
          jwk-set-uri: http://localhost:8080/realms/demo/protocol/openid-connect/certs


Configuration class


  • Configure the CORS policies. You can do this by defining a bean for CorsConfigurationSource.

package com.henry.springboot3springsecurity6oauth2jwt.configuration;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;

@Configuration
@EnableWebSecurity
public class SecurityConfig {


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {

        httpSecurity
                .authorizeHttpRequests(registry -> {
                            registry.requestMatchers("/admin/**").hasRole("ADMIN");
                            registry.requestMatchers("/api/**").hasAnyRole("ADMIN", "USER");
                            registry.anyRequest().authenticated();
                        }
                )
                .oauth2ResourceServer(oauth2Configure -> oauth2Configure.jwt(jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwt -> {
                    Map<String, Collection<String>> realmAccess = jwt.getClaim("realm_access");
                    Collection<String> roles = realmAccess.get("roles");
                    var grantedAuthorities = roles.stream()
                            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                            .collect(Collectors.toList());
                    return new JwtAuthenticationToken(jwt, grantedAuthorities);
                })))
                .cors(Customizer.withDefaults())

        ;

        return httpSecurity.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedOrigin("*"); // Set allowed origins here (you can specify specific origins instead of "*")
        configuration.addAllowedMethod("*"); // Set allowed HTTP methods (e.g., GET, POST, etc.)
        configuration.addAllowedHeader("*"); // Set allowed headers (e.g., Content-Type, Authorization, etc.)

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @Bean
    public CorsFilter corsFilter() {
        return new CorsFilter(corsConfigurationSource());
    }


}
package com.henry.springboot3springsecurity6oauth2jwt.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class WebConfig {

    @Bean
    public WebMvcConfigurer corsConfigurer()
    {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**").allowedOrigins("*");
            }
        };
    }
}


DTO class (in GitHub Repo)


  • Account
  • RealmAccess
  • ResourceAccess
  • JWTTokenDto


Services


package com.henry.springboot3springsecurity6oauth2jwt.services;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.henry.springboot3springsecurity6oauth2jwt.dto.JWTTokenDto;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;

import java.util.Map;

public interface DefaultService {

    public Map<String, Object> getName(JwtAuthenticationToken jwt) throws JsonProcessingException;

    public JWTTokenDto getJWTByUser(JwtAuthenticationToken jwt) throws JsonProcessingException;


}

package com.henry.springboot3springsecurity6oauth2jwt.services;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.henry.springboot3springsecurity6oauth2jwt.dto.JWTTokenDto;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class DefaultServiceImpl implements DefaultService{

    public static ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public Map<String, Object> getName(JwtAuthenticationToken jwt) throws JsonProcessingException {
        objectMapper.findAndRegisterModules();
        var respData = objectMapper.writeValueAsString(jwt.getTokenAttributes());
        var user = objectMapper.readValue(respData, JWTTokenDto.class);

        Map<String, Object> dataMap = new HashMap<>();
        dataMap.put("name", user.getName());

        return dataMap;
    }

    @Override
    public JWTTokenDto getJWTByUser(JwtAuthenticationToken jwt) throws JsonProcessingException {
        objectMapper.findAndRegisterModules();
        var respData = objectMapper.writeValueAsString(jwt.getTokenAttributes());

        var user = objectMapper.readValue(respData, JWTTokenDto.class);
        return user;
    }
}



Controller


package com.henry.springboot3springsecurity6oauth2jwt.controller;


import com.fasterxml.jackson.core.JsonProcessingException;
import com.henry.springboot3springsecurity6oauth2jwt.dto.JWTTokenDto;
import com.henry.springboot3springsecurity6oauth2jwt.services.DefaultService;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/api")
public class HelloController {

    private  final DefaultService defaultService;

    public HelloController(DefaultService defaultService) {
        this.defaultService = defaultService;
    }

    @GetMapping("/name")
    public Map<String, Object> getName(JwtAuthenticationToken jwt) throws JsonProcessingException {
        return  defaultService.getName(jwt);
    }

    @GetMapping("/principal")
    public JWTTokenDto getJWTByUser(JwtAuthenticationToken jwt) throws JsonProcessingException {
        return defaultService.getJWTByUser(jwt);
    }

}

package com.henry.springboot3springsecurity6oauth2jwt.controller;

import com.henry.springboot3springsecurity6oauth2jwt.services.DefaultService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/admin")
public class AdminRestController {

    private  final DefaultService defaultService;

    public AdminRestController(DefaultService defaultService) {
        this.defaultService = defaultService;
    }

}


Configuration  and installing Keycloak 22 by docker :


docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:22.0.0 start-dev

This will start Keycloak exposed on the local port 8080. It will also create an initial admin user with username admin and password admin.

Setting Up a keycloack server.


Open a browser  http://localhost:8080





















Note: If you want to omit next steps (less Creating a Users) then just to import realm.json (in GitHub Repo), only is necessary to create a realm demo, next Realm settings -> Action -> Partial Import -> Browse -> choose realm.json. next Choose the resources you want to import: select all, next If a resource already exists, specify what should be done: select overwrite. next import. 

Creating a Realm


For default the console opens up Master realm, Let's navigate left corner Add realm button. I created realm demo. we'll click the button create.












Creating a Client


Add a new client with the name spring-boot-keycloak with openid-connect.



Enabled Client authentication by default is disabled.













Add Valid Redirect URIs.
  • Backend Graphql API (http://localhost:8081/*)
  • Backend Rest API (http://localhost:8082/*)






Credentials Tab: We get Client Secret






 








Creating a Roles


Add two roles ADMIN and USER.











  • Creating a Users for demo
Add two users: admin and henry password admin and henry.

















Credentitals Tab: Disable temporary 









Role mapping: 
  • admin user has ADMIN and USER roles.
  • henry user has USER role.











Configuration  Angular Application

  • Configuration Angular and keycloack.
  • Components
  • Services

Technology

  • Visual Studio Code 
  • Node 18
  • Npm 9
  • Angular Cli 16
  • Angular Material 16
  • sweetalert2

Project Structure



















Configuration Angular Keycloak project 

environment.ts 
  • client_ID
  • client_secret
  • grant_type
  • keycloakEndpoint
  • graphql_api
  • rest_api

//REPLACE  client_secret for new secret will be generated by keycloak
export const environment = {
  production: false,
  client_ID: "spring-boot-keycloak",
  client_secret: "YOUR_CLIENT_SECRET", 
  grant_type: "password",
  keycloakEndpoint: "http://localhost:8080/realms/demo/protocol/openid-connect/token",
  graphql_api: "http://localhost:8081/graphql",
  rest_api: "http://localhost:8082/"
};

Services Folder:


Login:
auth.service.ts

import { Injectable } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { Router } from '@angular/router';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, map } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class AuthService {

  constructor(private http: HttpClient, public router: Router) { }

  login(username: string, password: string): Observable<any> {
    const body = new URLSearchParams();
    body.set('grant_type', environment.grant_type);
    body.set('username', username);
    body.set('password', password);

    const headers = new HttpHeaders({
      'Content-Type': 'application/x-www-form-urlencoded',
      'Authorization': 'Basic ' + btoa(environment.client_ID+':'+environment.client_secret) 
    });

    return this.http.post(environment.keycloakEndpoint, body.toString(), { headers })
      .pipe(
        map((data: any) => {
          return data;
        })
      );
  }

  user(): string {
    const payload = this.getDataToken(this.token);
    return payload.preferred_username;
  }

  public get token(): string {

    let resul: any;

    if (localStorage.getItem('token') != null) {
      resul = localStorage.getItem('token');
    }
    return resul;
  }

  saveToken(accessToken: string): void {
    localStorage.setItem('token', accessToken);
  }

  getUser() {
    const payload = this.getDataToken(this.token);
    return payload.name
  }

  getDataToken(accessToken: string): any {
    if (accessToken != null) {

      return JSON.parse(atob(accessToken.split('.')[1]));
    }
    return null;
  }

  isAuthenticated(): boolean {
    const payload = this.getDataToken(this.token);
    if (payload != null && payload.preferred_username && payload.preferred_username.length > 0) {
      return true;
    }
    return false;
  }

  logout(): void {
    localStorage.clear();
  }
}



Interceptors:

auth.interceptor.ts

import { Injectable } from '@angular/core';
import {
  HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';
import { AuthService } from '../login/auth.service';

import { Observable, throwError } from 'rxjs';
import Swal from 'sweetalert2';
import { catchError } from 'rxjs/operators';
import { Router } from '@angular/router';
/** Pass untouched request through to the next request handler. */
@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private auth: AuthService, private router: Router) {
  }
  intercept(req: HttpRequest<any>, next: HttpHandler):
    Observable<HttpEvent<any>> {  
    return next.handle(req).pipe(
      catchError(e => {

        console.log("****")
        console.log(e)
        if (e.status === 401) {
          if (this.auth.isAuthenticated()) {
            this.auth.logout();
          }
          this.router.navigate(['/login']);
        }
        if (e.status === 403) {
          Swal.fire('Access denied', `You don't have access this resource!!!`, 'warning');
          this.router.navigate(['/login']);
        }

        throw new Error(e);
      })
    );
  }
}

token.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';
import { AuthService } from '../login/auth.service';

import { Observable } from 'rxjs';

/** Pass untouched request through to the next request handler. */
@Injectable()
export class TokenInterceptor implements HttpInterceptor {

  constructor(private auth: AuthService) {
  }
  intercept(req: HttpRequest<any>, next: HttpHandler):
    Observable<HttpEvent<any>> {
    const token = this.auth.token;
    if (token != null) {
      const authReq = req.clone({
        headers: req.headers.set('Authorization', `Bearer ${token}`)
      });
      return next.handle(authReq);
    }
    return next.handle(req);
  }
}

Guards:

auth-guard.service.ts

import { Injectable, inject } from "@angular/core";
import {
  Router,
} from "@angular/router";

import { AuthService } from "../login/auth.service";
import { ActivatedRoute } from "@angular/router";

@Injectable({
  providedIn: "root",
})
export class AuthGuardService {
  constructor(
    public router: Router,
    public rt: ActivatedRoute,
    private authService: AuthService,
  ) { }

  isAuth(isAuth: boolean) {
    if (this.authService.isAuthenticated()) {
      return true;
    }
    this.router.navigate(["/login"]);
    return false;
  }
}

export const canActivate = (isAuth: boolean, authGuardService = inject(AuthGuardService)) => authGuardService.isAuth(true);


GraphQL Service:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class GrahpqlService {

  constructor(private http: HttpClient) { }

  getName(): Observable<any> {
    const query = `
    query {
      getName
    }
    `;
    return this.http.post<any>(environment.graphql_api, { query });
  }

  getJWTByUser(): Observable<any> {
    const query = `
    query {
      getJWTByUser {
      sub
      resourceAccess {
      account {
      roles
      }
      additionalProperties
      }
      emailVerified
      allowedOrigins
      iss
      typ
      preferredUsername
      givenName
      sid
      aud
      acr
      realmAccess {
      roles
      additionalProperties
      }
      azp
      scope
      name
      exp
      sessionState
      iat
      familyName
      jti
      email
      additionalProperties
      }
      }
    `;
    return this.http.post<any>(environment.graphql_api, { query });
  }
}


Rest Service:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http";

@Injectable({
  providedIn: 'root'
})
export class RestService {

  constructor(public http: HttpClient) { }

  get(url: any){
    return this.http.get<any>(url);
  }

}



Component Folder:


GraphQL Component:

import { Component, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { GrahpqlService } from '../../../../services/graphql/grahpql.service';

@Component({
  selector: 'app-json-jwt',
  templateUrl: './json-jwt.component.html',
  styleUrls: ['./json-jwt.component.scss']
})
export class JsonJwtComponent implements OnInit{
  
  name = '';
  jsonData = '';
  private subscription: Subscription = new Subscription;
  
  constructor(private _grahpqlService: GrahpqlService) { }

  ngOnInit(): void {
    this.getName();
    this.getJWTByUser();
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }

   

  getName(): void {

    this.subscription = this._grahpqlService.getName().subscribe({
      next: (response: any) => {
        // Handle successful response, if needed
        this.name = response.data.getName
      },
      error: (error: any) => {
        console.error('Error getName:', error);
      },    // errorHandler 
    });

  }

  getJWTByUser(): void {

    this.subscription = this._grahpqlService.getJWTByUser().subscribe({
      next: (response: any) => {
        // Handle successful response, if needed
       this.jsonData = response.data.getJWTByUser;
      },
      error: (error: any) => {
        console.error('Error getJWTByUser:', error);
      },    // errorHandler 
    });

  }
  

}

Rest Component:

import { AfterViewInit, Component, OnInit } from '@angular/core';
import { environment } from '../../../../../environments/environment';
import { RestService } from 'src/app/services/rest/rest.service';
import { Subscription } from 'rxjs';


@Component({
  selector: 'app-json-jwt-rest',
  templateUrl: './json-jwt-rest.component.html',
  styleUrls: ['./json-jwt-rest.component.scss']
})
export class JsonJwtRestComponent implements AfterViewInit, OnInit {


  name = '';
  jsonData = '';
  private subscription: Subscription = new Subscription;

  constructor(private _restService: RestService) { }

  ngAfterViewInit(): void {
    this.getName();
    this.getJWTByUser();
  }


  ngOnInit(): void {
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }

  getName(): void {

    let url = `${environment.rest_api}api/name`;


    this.subscription = this._restService.get(url).subscribe({
      next: (data: any) => {
        // Handle successful response, if needed
        console.log(data.name);

      },
      error: (error: any) => {
        console.error('Error getName:', error);
        console.error(error);
      },    // errorHandler 
    });



  }

  getJWTByUser(): void {

    let url = `${environment.rest_api}api/principal`;

    this.subscription = this._restService.get(url).subscribe({
      next: (data: any) => {
        // Handle successful response, if needed
        console.log(data)
        this.jsonData = data
      },
      error: (error: any) => {
        console.error('Error getJWTByUser:', error);
      },    // errorHandler 
    });

  }

}

Login Component:

import { Component, OnInit } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Router } from '@angular/router';
import { AuthService } from "../services/auth/login/auth.service";
import { Subscription } from 'rxjs';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {

  error = '';
  private subscription: Subscription = new Subscription;
  constructor(private http: HttpClient, private router: Router, private auth: AuthService, private fb: FormBuilder) { }

  loginForm!: FormGroup;

  ngOnInit() {
    this.loginForm = this.fb.group({
      username: ['', [Validators.required]],
      password: ['', [Validators.required]]
    });
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }

  login() {
   
    if (this.loginForm.valid) {
      const username = this.loginForm.get('username')!.value;
      const password = this.loginForm.get('password')!.value;

      this.subscription = this.auth.login(username, password).subscribe({
        next: (data: any) => {
          // Handle successful response, if needed
          // Access token and other data will be available here
          console.log(data);
          this.auth.saveToken(data.access_token);
          // Redirect to the new page after successful login
          this.router.navigate(['/']);
        },
        error: (error: any) => {
          this.error = 'Invalid credentials. Please try again.';
          console.error('Error during login:', error);
        },    // errorHandler 
      });
    } 
  }
}
<div class="container">

  <form [formGroup]="loginForm" (ngSubmit)="login()">
  <mat-card class="login-card">
    <mat-card-header>
      <mat-card-title>Login</mat-card-title>
    </mat-card-header>
    <mat-card-content>
      <mat-form-field appearance="fill">
        <mat-label>Username</mat-label>
        <input matInput formControlName="username" autocomplete="off" required>
        <mat-error *ngIf="loginForm.get('username')!.hasError('required')">Username is required</mat-error>
      </mat-form-field>

      <mat-form-field appearance="fill">
        <mat-label>Password</mat-label>
        <input matInput type="password" formControlName="password" autocomplete="off" required>
        <mat-error *ngIf="loginForm.get('password')!.hasError('required')">Password is required</mat-error>
      </mat-form-field>
      <div *ngIf="error" class="error-message">{{ error }}</div>
    </mat-card-content>
    <mat-card-actions>
      <button mat-raised-button color="primary" type="submit" [disabled]="loginForm.invalid">Login</button>
    </mat-card-actions>
  </mat-card>
</form>
</div>
  

Run & Test

Run Both Spring Boot application with command: mvn spring-boot:run. by console, IntelliJ etc.

  • GraphQL in port 8081





  • Rest API in port 8082



  • Run Angular app -> ng serve.



Keycloak provides access token URL.

1. We need to acquire an access token from Keycloak from this url. 

  • localhost .
  • Port: 8080 .
  • realms Url defined by keycloak.
  • demo The name of realm.
  • protocol/openid-connect/token The url defined by keycloak.

 http://localhost:8080/realms/demo/protocol/openid-connect/token

2. Configurate on Postman.

Tab Authorizacion: 
       Type: OAuth 2.0
       Grant Type: Password Credentials
       Access Token URL: http://localhost:8080/realms/demo/protocol/openid-connect/token
       Client ID: spring-boot-keycloak
       Client Secret: your_new_client_secret
       Username: admin
       Password: admin
       Client Authentication: Send client credentials in body















3. GraphQL Request.













4. Rest Request.













5. Angular app







































Go to jwt.io and copy and paste token.

Source Code


Here on GitHub:





No comments:

Post a Comment

GraphQL Error Handling with Spring Boot 3

  Why HTTP 200 is Different from REST GraphQL servers typically return an HTTP 200 status code for all responses, regardless of whether the ...