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
- 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;
}
}
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/*)
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:
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.tsimport { 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.tsimport { 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