Thursday, July 27, 2023

Single Sign-On with Angular 16 Keycloak 22 Spring Boot 3


Single Sign-On (SSO) is an authentication process that allows users to access multiple applications or services with a single set of login credentials. In traditional authentication systems, users need to enter their username and password separately for each application they want to use, which can become cumbersome and lead to security risks. SSO streamlines this process by providing a centralized authentication mechanism.

Single Sign-On typically works:

  1. User logs in: The user enters their login credentials (username and password) into a centralized authentication system, which is often referred to as the Identity Provider (IdP).
  2. Authentication: The IdP verifies the user's credentials and authenticates their identity.
  3. Token issuance: Upon successful authentication, the IdP generates a secure token that contains information about the user's identity and authentication status.
  4. Access to other services: When the user tries to access a different application or service (referred to as the Service Provider or SP), the application asks for authentication.
  5. Token validation: Instead of prompting the user for credentials again, the application accepts the token issued by the IdP and validates it.
  6. Access granted: If the token is valid and the user is authorized to use the service, access is granted without the need for additional login credentials.

Advantages of Single Sign-On:

  1. Improved user experience: Users only need to remember one set of login credentials, making it more convenient and reducing the risk of forgotten passwords.
  2. Enhanced security: Since users don't need to enter credentials into each service, the likelihood of them using weak or easily guessable passwords is reduced. Additionally, SSO allows for centralized security policies and better monitoring of user access.
  3. Easier administration: IT administrators can manage user access and permissions from a central location, making it simpler to grant or revoke access to various applications.
  4. Cost savings: SSO can reduce support costs related to password resets and account management issues.
Single Sign-On is widely used in various industries and organizations, especially when employees need access to multiple internal systems or when a company offers services to external users across different platforms. It promotes both security and user convenience, striking a balance between these two critical aspects.



Overview:

  • Configuration Angular Application
  • Configuration Spring Boot  project  
  • Configuration and installing Keycloak 22 by docker compose 
  • Run & Test

Configuration  Angular App:


Technology

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

Check the official documentation  keycloak-angular

By default, all HttpClient requests will add the Authorization header in the format of: Authorization: Bearer **_TOKEN_**.

  • Configuration Angular and keycloack.

npm install keycloak-angular keycloak-js


Setup


import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular';
import { HttpClientModule } from '@angular/common/http';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

function initializeKeycloak(keycloak: KeycloakService) {
  return () =>
    keycloak.init({
      config: {
        realm: 'demo',
        url: 'http://localhost:8080',
        clientId: 'spring-boot-angular-keycloak'
      },
      initOptions: {
        onLoad: 'check-sso',
        silentCheckSsoRedirectUri:
          window.location.origin + '/assets/silent-check-sso.html'
      }
    });
}

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, AppRoutingModule, KeycloakAngularModule, HttpClientModule],
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: initializeKeycloak,
      multi: true,
      deps: [KeycloakService]
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Create a file called silent-check-sso.html in the assets directory of your application and paste in the contents as seen below.


<html>
  <body>
    <script>
      parent.postMessage(location.href, location.origin);
    </script>
  </body>
</html>


Configuration Spring Boot project  :


Technology

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

<?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.2</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.henry</groupId>
	<artifactId>sso-spring-boot3-angular16-keycloak22</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>sso-spring-boot3-angular16-keycloak22</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.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: 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


Configuration class

  • Configure the CORS policies. You can do this by defining a bean for CorsConfigurationSource.
package com.henry.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.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("/api/**");
                            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.config;

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("*");
            }
        };
    }
}

Pojo


package com.henry.model;

public class User {

    private Long id;
    private String username;
    private String email;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

Controller


package com.henry.controller;

import com.henry.model.User;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private List<User> users = new ArrayList<>();
    private Long currentUserId = 1L; // Used to assign incremental IDs to users

    @PostMapping
    public User addUser(@RequestBody User user) {
        // Assign an incremental ID to the user before saving
        user.setId(currentUserId++);

        // Save the user to the in-memory list
        users.add(user);
        return user;
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        // Search for the user in the in-memory list based on the provided ID
        User user = users.stream()
                .filter(u -> u.getId().equals(id))
                .findFirst()
                .orElse(null);

        if (user != null) {
            return ResponseEntity.ok(user);
        } else {
            return ResponseEntity.notFound().build();
        }
    }
}

Configuration  and installing Keycloak 22 by docker compose:


version: '3'
services:
  keycloak:
    image: quay.io/keycloak/keycloak:22.0.0
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    ports:
      - 8080:8080
    volumes:
      - ./config/:/opt/keycloak/data/import:ro
    entrypoint: '/opt/keycloak/bin/kc.sh start-dev --import-realm'


Go to root folder in angular project run next commad:






 docker-compose -f docker-compose-keycloak.yml up -d

Add Valid Redirect URIs.

  • http://localhost:4200/*

  • http://localhost:8081/*

Web origings (*).
















Run & Test

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





Run angular app ng serve.






User: admin
Passwd: admin

Hint: To access keycloak admin, please use: http://localhost:8080. Same user and password as above.

1.






2.



























3. 















Source Code


Here on GitHub:




References


https://www.npmjs.com/package/keycloak-angular#example-project










No comments:

Post a Comment

Multiple Data Sources in Spring Boot 3 with Java 21

  In this blog post, we'll explore the configuration and setup for a Spring Boot 3 application with Java 21 that uses multiple data sour...