0

I built a small spring boot application using spring-boot-starter-oauth2-resource-server. The tests work well on Postman but not on Angular. The test was working on Angular when I used spring-boot-starter-security.

Here is my pom.xml file, I only added 3 staters.

<?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.0.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.devskills</groupId>
    <artifactId>oauth2api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>oauth2api</name>
    <description>Oauth2 security security with Angular in frontend</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        
        <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-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>

Here is my configuration file, I have implemented hardcoded username and password in InMemoryUserDetailsManager.

package com.devskills.oauth2api.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;

import static org.springframework.security.config.Customizer.*;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    private final RsaKeyProperties rsaKeys;
    
    public SecurityConfig(RsaKeyProperties rsaKeys) {
        this.rsaKeys = rsaKeys;
    }
    
    @Bean
    InMemoryUserDetailsManager user() {
        return new InMemoryUserDetailsManager(
                User.withUsername("keen")
                    .password("{noop}password")
                    .authorities("read")
                    .build()
                );
    }
    
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests()
                .requestMatchers("/token").permitAll()
                .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .httpBasic(withDefaults())
                .build();
    }
    
    @Bean
    JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withPublicKey(rsaKeys.publicKey()).build();
    }
    
    @Bean
    JwtEncoder jwtEncoder() {
        JWK jwk = new RSAKey.Builder(rsaKeys.publicKey()).privateKey(rsaKeys.privateKey()).build();
        JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
        return new NimbusJwtEncoder(jwks);
    }

}

And here is my controller which contains the method token(Authentication authentication) which allows to log in.

package com.devskills.oauth2api.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import com.devskills.oauth2api.service.TokenService;

@RestController
public class AuthController {
    
    private static final Logger LOG = LoggerFactory.getLogger(AuthController.class);
    
    private final TokenService tokenService;
    
    public AuthController(TokenService tokenService) {
        this.tokenService = tokenService;
    }
    
    @PostMapping("/token")
    public String token(Authentication authentication) {
        LOG.warn(authentication.getPrincipal().toString());
        LOG.debug("Token requested for user: '{}'", authentication.getName());
        String token = tokenService.generateToken(authentication);
        LOG.debug("Token granted {}", token);
        return token;
    }

}

The connection with Angular has no problem because I added this method in the main file of the application.

    @Bean
    CorsFilter corsFilter() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
        corsConfiguration.setAllowedHeaders(Arrays.asList("Origin", "Access-Control-Allow-Origin", "Content-Type",
                "Accept", "Authorization", "Origin, Accept", "X-Requested-With", "Access-Control-Request-Method",
                "Access-Control-Request-Headers"));
        corsConfiguration.setExposedHeaders(Arrays.asList("Origin", "Content-Type", "Accept", "Authorization",
                "Access-Control-Allow-Origin", "Access-Control-Allow-Credentials"));
        corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(urlBasedCorsConfigurationSource);
    }

Here is the result of the test on Postman, the token is well returned when the login is successful. enter image description here

Here is my Angular code that does not work.

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { map } from 'rxjs/operators';

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

  constructor(private httpClient: HttpClient) { }

  login() {
    const username = 'keen';
    const password = 'password';
    const headers = new HttpHeaders({Authorization: 'Basic ' + btoa(username + ':' + password)});
    return this.httpClient.post<any>('http://localhost:8080/token', {headers})
                .pipe(
                  map(
                    userData => {
                      sessionStorage.setItem('username', username);
                    }
                  )
                );
  }

  isUserLoggedIn() {
    let user = sessionStorage.getItem('username');
    return !(user === null);
  }

  logout() {
    sessionStorage.removeItem('username');
  }
}

And the login() method is called with a button on the view through this component.

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../service/auth.service';

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

  constructor(private authService: AuthService) { }

  ngOnInit(): void {
  }

  login() {
    this.authService.login().subscribe(
      response => {
        alert('login successfully');
      }
    );
  }

}

When I try to login, I get a 401 Unauthorized error. enter image description here

And why do I get this error? Because Spring Boot reports that the Authentication object received as an argument to the token() method is null.

java.lang.NullPointerException: Cannot invoke "org.springframework.security.core.Authentication.getPrincipal()" because "authentication" is null
    at com.devskills.oauth2api.controller.AuthController.token(AuthController.java:24) ~[classes/:na]

This means that the headers sent here this.httpClient.post<any>('http://localhost:8080/token', {headers}) from Angular are not received by the token() method of Spring Boot backend. Why is that? That's what I'm trying to find out.

All help is welcome, Thank you.

  • Are you implementing a resource-server or an authorization-server? Read more about OAuth2: users do not "login" to a resource-server. Clients redirect users to an authorization-server when it (the client) needs an access-token to issue requests on behalf of the user. So, technically, it is the client that is is "logged in" and against the authorization-server. Resource-servers just requires requests to be "authorized" (have an `Authorization` header with a `Bearer` token). – ch4mp Feb 27 '23 at 07:27
  • The problem is on the Angular side. The object ```{headers}``` that I pass as an argument here ```this.httpClient.post('http://localhost:8080/token', {headers})``` does not reach the ```token(Authentication authentication)``` method on the Spring Boot side, that's why the ```Authentication authentication``` object always gets null. How can I solve this problem on angular side? The application works fine on Postman – DONGMO BERNARD GERAUD Feb 28 '23 at 00:16
  • Sorry, but I still suspect a probkem on the developper side: not understanding OAuth2... A successful request with Postman doesn't mean it was an OAuth2 request to a resource-server – ch4mp Feb 28 '23 at 01:08
  • To be more constructive (as you'll probably never get an answer to your question), maybe should you have a look at [those tutorials I wrote](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials). The Introduction README contains a **memo of essential OAuth2 notions you should read carefully before rushing to implementation samples**. – ch4mp Feb 28 '23 at 08:30
  • Thanks for your tutorial ch4mp. All http requests sent from Angular work fine. You were right, I had built the backend wrong. Unfortunately I can't post the solution because I rebuilt the project completely. – DONGMO BERNARD GERAUD Mar 01 '23 at 09:23

0 Answers0