2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Spring Boot 2.5.X]WebMvcTestでOAuth2.0の認証をパスしようとしたらハマった話

Last updated at Posted at 2022-04-27

はじめに

OAuth2.0の認証を行うリソースサーバーのコントローラーをテストする際に、Spring Boot 2.5.X で発生するエラーを踏んでしまい、解消に時間がかかったのでメモ。
REST APIのコントローラー部分のみのテストをしたかったので@WebMvcTestを使用。
spring-security-testの@WithMockUserを使用してテストしようとしたところ、テスト実行時にBeanCreationExceptionが発生。

エラー詳細

以下のログが出力されました。

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.1)

2022-04-27 21:41:51.414  INFO 21628 --- [           main] c.e.s.c.SampleRestControllerTest         : Starting SampleRestControllerTest using Java 11.0.13 on chaffstun-PC with PID 21628 (started by chaffstun in C:\Users\chaffstun\Desktop\springsecurityoauth2demo\springsecurityoauth2demo)
2022-04-27 21:41:51.415  INFO 21628 --- [           main] c.e.s.c.SampleRestControllerTest         : No active profile set, falling back to default profiles: default
2022-04-27 21:41:52.029  WARN 21628 --- [           main] o.s.w.c.s.GenericWebApplicationContext   : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springSecurityFilterChain' defined in class path resource [org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [javax.servlet.Filter]: Factory method 'springSecurityFilterChain' threw exception; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jwtDecoderByIssuerUri' defined in class path resource [org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration$JwtDecoderConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.oauth2.jwt.JwtDecoder]: Factory method 'jwtDecoderByIssuerUri' threw exception; nested exception is java.lang.IllegalArgumentException: Unable to resolve the Configuration with the provided Issuer of "https://idp.example.com/issuer"
2022-04-27 21:41:52.031  INFO 21628 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2022-04-27 21:41:52.037 ERROR 21628 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springSecurityFilterChain' defined in class path resource [org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [javax.servlet.Filter]: Factory method 'springSecurityFilterChain' threw exception; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jwtDecoderByIssuerUri' defined in class path resource [org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration$JwtDecoderConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.oauth2.jwt.JwtDecoder]: Factory method 'jwtDecoderByIssuerUri' threw exception; nested exception is java.lang.IllegalArgumentException: Unable to resolve the Configuration with the provided Issuer of "https://idp.example.com/issuer"
	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:658) ~[spring-beans-5.3.8.jar:5.3.8]
	at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:486) ~[spring-beans-5.3.8.jar:5.3.8]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1334) ~[spring-beans-5.3.8.jar:5.3.8]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1177) ~[spring-beans-5.3.8.jar:5.3.8]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:564) ~[spring-beans-5.3.8.jar:5.3.8]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:524) ~[spring-beans-5.3.8.jar:5.3.8]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.8.jar:5.3.8]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spr

以下略

ログよりJwtDecoderのインスタンスを生成するため、jwtDecoderByIssuerUriへアクセスしようとしているみたいですね。

実装例

検証にあたって作成した実装例。

依存関係

Spring Bootは2.5.9を使用。

pom.xml
	<dependencies>
		<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>

		<!-- test -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		
		<dependency>
 			 <groupId>org.springframework.security</groupId>
 			 <artifactId>spring-security-test</artifactId>
 			 <version>5.6.3</version>
		</dependency>
		
		<dependency>
  			<groupId>org.mockito</groupId>
  			<artifactId>mockito-junit-jupiter</artifactId>
 			 <version>4.5.1</version>
		</dependency>
	</dependencies>

コントローラー

GETメソッドでhelloを返却するだけのものを用意。

SampleRestController.java
package com.example.springsecurityoauth2demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SampleRestController {

	@GetMapping("/")
	public String hello() {
		return "hello";
	}
}

リソースサーバーの設定クラス

accessロールを持つ場合にアクセスを許可する。
クライアントクレデンシャルの場合を想定。

SecurityConfig.java
package com.example.springsecurityoauth2demo.controller;

import org.springframework.context.annotation.Configuration;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeHttpRequests(authorize -> authorize
			.mvcMatchers("/")
			.hasRole("access")
			.anyRequest().authenticated())
			.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
	}
}

application.yml

issuer-uriにアクセストークンの検証先を設定する。

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com/issuer

テストクラス

本来であれば以下のテストで問題ないはずだったが、BeanCreationExceptionが発生してしまう。
ちなみにSpring Boot 2.6.0以降は修正されていて以下のテスト問題なく実行され、結果もグリーンになる。

SampleRestControllerTest.java
package com.example.springsecurityoauth2demo.controller;

import static org.hamcrest.CoreMatchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(SampleRestController.class)
class SampleRestControllerTest {

	@Autowired
	private MockMvc mockMvc;

	@Test
	@WithMockUser(roles = "access")
	void accessロールを持つリクエストは許可される() throws Exception {
		this.mockMvc.perform(get("/"))
			.andExpect(status().isOk())
			.andExpect(content().string(containsString("hello")));
	}

}

対処方法

はじめはテストの記載方法が間違っているのかもしれないと思い遠回りをしてしまったが、参考資料をもとにたどり着いた解消方法は以下の通り。

修正後のテストクラス

SampleRestControllerTest.java
package com.example.springsecurityoauth2demo.controller;

import static org.hamcrest.CoreMatchers.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@WebMvcTest(SampleRestController.class)
class SampleRestControllerTest {

	private MockMvc mockMvc;

	@Autowired
	private WebApplicationContext context;

	@MockBean
	private JwtDecoder jwtDecoder;

	@BeforeEach
	void 前準備() {
		this.mockMvc = MockMvcBuilders
			.webAppContextSetup(context)  // JwtDecoderのモックを適用したWebApplicationContextを設定
			.apply(springSecurity())
			.build();
	}

	@Test
	@WithMockUser(roles = "access")
	void accessロールを持つリクエストは許可される() throws Exception {
		this.mockMvc.perform(get("/"))
			.andExpect(status().isOk())
			.andExpect(content().string(containsString("hello")));
	}

}

この実装でも問題ありませんが、Junit4の書き方が混ざっているようだったのでJunit5っぽく?書けないか試行錯誤してみると、以下の記載でも問題なく動作しました。

Junit5っぽく修正したテストクラス

package com.example.springsecurityoauth2demo.controller;

import static org.hamcrest.CoreMatchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(SampleRestController.class)
class SampleRestControllerTest {

	@Autowired
	private MockMvc mockMvc;

	@MockBean
	private JwtDecoder jwtDecoder;

	@Test
	@WithMockUser(roles = "access")
	void accessロールを持つリクエストは許可される() throws Exception {
		mockMvc.perform(get("/"))
			.andExpect(status().isOk())
			.andExpect(content().string(containsString("hello")));
	}

}

単純にJwtDecoderをMockBeanにしてやれば問題ありませんでした。
記事を書く際にエラーログをより詳細に確認しましたが、ログの内容からも上記の発想になりますね。

ソース

サンプルはこちらに置いてます。

おわりに

検証のためSpringInitalizrでサンプルを作成したところ、Spring Bootのバージョンが2.6.7になっていて普通にテストが通ったので少し焦りました笑。
さらに調べてみるとSpring Boot2.5.Xでしか発生していなかったので運が悪かったなと。

参考資料

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?