"Spring Boot JWT"でGoogle検索するとヒットする多くの記事・チュートリアルはJwtAuthenticationFilterを自作して、JWTの検証が不十分だったりします。
Spring SecurityにはJWT認証・認可が用意されています。
https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html
これを使えばJwtAuthenticationFilterを作ることなく、フレームワークにトークンの検証や認証・認可を任せることができます。
この記事ではSpring SecurityのJWT認証を使って、簡単なREST APIの認証・認可を実装します。
以下の作業はLinux(Ubuntu)上で検証しています。Macだと一部のコマンドの引数が異なり、エラーが出ると思います。
目次
- 雛形プロジェクトの作成
- JWTの署名・検証用のRSA鍵をOpenSSLで生成
- JWTで認証・認可されたREST APIの作成
- JWTをOpenSSLで生成
- JWTを生成するAPIの作成
- Integration Testの作成
雛形プロジェクトの作成
まずはSpring Initializrで雛形プロジェクトを作成します。
curl -s https://start.spring.io/starter.tgz \
-d artifactId=hello-jwt \
-d baseDir=hello-jwt \
-d packageName=com.example \
-d dependencies=web,actuator,security,configuration-processor \
-d type=maven-project \
-d name=hello-jwt \
-d applicationName=HelloJwtApplication | tar -xzvf -
cd hello-jwt
Spring SecurityにはJWT認証・認可機能を使う場合は、以下のdependencyをpom.xmlに追加する必要があります。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
JWTの署名・検証用のRSA鍵をOpenSSLで生成
次のコマンドでJWTの署名と検証用の鍵を生成します。
cd src/main/resources
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
openssl pkcs8 -topk8 -inform PEM -in private.pem -out private_key.pem -nocrypt
rm -f private.pem
private_key.pemが署名用の秘密鍵で、public.pemが検証用の公開鍵です。
JWTで認証・認可されたREST APIの作成
メッセージを読み書きする簡単なREST APIを作成します。Spring Securityの機能を使えば、Controllerの引数に@AuthenticationPrincipalをつけて認証済みのJwtオブジェクトを取得できます。
cat <<'EOF' > src/main/java/com/example/MessageController.java
package com.example;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MessageController {
private final List<Message> messages = new CopyOnWriteArrayList<>();
@GetMapping(path = "/messages")
List<Message> getMessages() {
return this.messages;
}
@PostMapping(path = "/messages")
Message postMessages(@RequestBody String text, @AuthenticationPrincipal Jwt jwt) {
Message message = new Message(text, jwt.getSubject());
this.messages.add(message);
return message;
}
record Message(String text, String username) {
}
}
EOF
JWT認証・認可のための設定は以下の通りです。
ここでメッセージの書き込みにはmessage:write、読み込みにはmessage:readスコープがJWTに含まれていないといけない、とします。
cat <<'EOF' > src/main/java/com/example/SecurityConfig.java
package com.example;
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.web.SecurityFilterChain;
import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;
@Configuration(proxyBeanMethods = false)
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authz -> authz
.requestMatchers(HttpMethod.GET, "/messages").access(hasScope("message:read"))
.requestMatchers(HttpMethod.POST, "/messages").access(hasScope("message:write"))
.anyRequest().permitAll())
.oauth2ResourceServer(oauth -> oauth.jwt(jwt -> {
}))
.csrf(csrf -> csrf.disable())
.build();
}
}
EOF
JWTを検証するための設定をapplication.propertiesに記述します。今回は公開鍵を直接指定します。
cat <<'EOF' > src/main/resources/application.properties
spring.application.name=hello-jwt
spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public.pem
EOF
Note
OpenID Connect ProviderからJWTを取得する場合は、spring.security.oauth2.resourceserver.jwt.public-key-locationではなく、
spring.security.oauth2.resourceserver.jwt.issuer-uri=<OIDC Issuer URI>
を設定すると良いです。起動時にSpring Securityが<OIDC Issuer URI>/.well-known/openid-configurationにアクセスして、jwks_uriキーに設定されたURIから公開鍵をダウンロードします。
本記事では後に同一アプリケーション内でトークンの生成まで行います。その場合は、spring.security.oauth2.resourceserver.jwt.issuer-uriは使えません。
アプリを起動します。
./mvnw spring-boot:run
JWTをOpenSSLで生成
JWTをコマンドラインで作成してみましょう。application.propertiesにに設定したpublic.pemとペアとなるprivate_key.pemを使ってJWTを署名します。
まず、JWTのヘッダーとペイロードを準備する必要があります。ヘッダーとペイロードをBase64エンコードし、それらを連結した文字列を生成します。その後、この連結文字列に対してRSA署名を行います。
ヘッダーとペイロードを作成し、それをBase64エンコードします。例えば、以下のようなヘッダーとペイロードを使用します。
ヘッダー:
{
"alg": "RS256",
"typ": "JWT"
}
ペイロード:
{
"sub": "foo@example.com",
"issuer": "http://localhost:8080",
"scope": ["message:read", "message:write"],
"iat": 1725333684,
"exp": 1725355284
}
以下のコマンドで、ヘッダーとペイロードのJSONをBase64エンコードし、さらにそれらを.で連結した文字列をRSA署名します。そして、ヘッダー・ペイロード・署名を.で結合してJWTができます。
IAT=$(date +%s)
EXP=$(date -d '+10 min' +%s)
HEADER=$(echo -n '{"alg":"RS256","typ":"JWT"}' | openssl enc -base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
PAYLOAD=$(echo -n '{"sub":"foo@example.com","issuer":"http://localhost:8080","scope":["message:read","message:write"],"iat":'$IAT',"exp":'$EXP'}' | openssl enc -base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
SIGNATURE=$(echo -n "${HEADER}.${PAYLOAD}" | openssl dgst -sha256 -sign private_key.pem | openssl enc -base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
JWT="${HEADER}.${PAYLOAD}.${SIGNATURE}"
echo "$JWT"
jwt.ioにアクセスし、JWTとRSA鍵を貼り付けると、"Signature Verified"を表示されるでしょう。
これにより今回のREST APIに対して使用できるJWTを手動で生成できます。
IAT=$(date +%s)
EXP=$(date -d '+10 min' +%s)
HEADER=$(echo -n '{"alg":"RS256","typ":"JWT"}' | openssl enc -base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
PAYLOAD=$(echo -n '{"sub":"foo@example.com","issuer":"http://localhost:8080","scope":["message:read","message:write"],"iat":'$IAT',"exp":'$EXP'}' | openssl enc -base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
SIGNATURE=$(echo -n "${HEADER}.${PAYLOAD}" | openssl dgst -sha256 -sign private_key.pem | openssl enc -base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
JWT="${HEADER}.${PAYLOAD}.${SIGNATURE}"
このJWTを使ってメッセージを書き込みましょう。
$ curl http://localhost:8080/messages -H "Authorization: Bearer $JWT" -H "Content-Type: text/plain" -d "Hello World"
{"text":"Hello World","username":"foo@example.com"}
JWTのsub Claimに指定した値がユーザー名として使われていることを確認できます。
同じくメッセージを取得します。
$ curl http://localhost:8080/messages -H "Authorization: Bearer $JWT"
[{"text":"Hello World","username":"foo@example.com"}]
今度はmessage:read scopeしかないJWTを生成します。
IAT=$(date +%s)
EXP=$(date -d '+10 min' +%s)
HEADER=$(echo -n '{"alg":"RS256","typ":"JWT"}' | openssl enc -base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
PAYLOAD=$(echo -n '{"sub":"foo@example.com","issuer":"http://localhost:8080","scope":["message:read"],"iat":'$IAT',"exp":'$EXP'}' | openssl enc -base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
SIGNATURE=$(echo -n "${HEADER}.${PAYLOAD}" | openssl dgst -sha256 -sign private_key.pem | openssl enc -base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
JWT="${HEADER}.${PAYLOAD}.${SIGNATURE}"
このJWTを使ってメッセージを書き込もうとすると、401エラーになります。
curl http://localhost:8080/messages -H "Authorization: Bearer $JWT" -H "Content-Type: text/plain" -d "Hello World" -v
エラーメッセージはWWW-Authenticateヘッダーに設定されます。
< WWW-Authenticate: Bearer error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token.", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
今回は有効期限が10分のJWTを作成しました。10分経過した後にこのJWTを使うと、401エラーになり、次のメッセージが返ります。
< WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: Jwt expired at 2024-09-03T03:38:43Z", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
また、アプリに設定した公開鍵とはペアではない秘密鍵を新規に生成して署名したJWTを使った場合は、次のエラーメッセージが返ります。
< WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: Signed JWT rejected: Invalid signature", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
JWTを生成するAPIの作成
JWTの生成やユーザーの管理は外部のOIDC Providerに任せる方が安全ですが、Basic認証の代わりに気軽にJWT認証を使いたいというケースでは同一のアプリ内でJWTを生成したいこともあるでしょう。
Spring SecurityではSpring Authorization Serverという別プロジェクトを使うことでOIDC Provider/OAuth2認可サーバーを実装することもできますが、
OAuth2やOIDCに準拠するほどでなく、Basic認証の代わりとなるようなシンプルなトークンエンドポイントが欲しいだけであれば、次のように実装できます。
まずは、公開鍵と秘密鍵をプロパティから取得できるようにします。
cat <<'EOF' > src/main/java/com/example/JwtProperties.java
package com.example;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "jwt")
public record JwtProperties(RSAPublicKey publicKey, RSAPrivateKey privateKey) {
}
EOF
プロパティを読み込めるように@ConfigurationPropertiesScanアノテーションをメインクラスにつけます。
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan; // <---
@SpringBootApplication
@ConfigurationPropertiesScan // <---
public class HelloJwtApplication {
public static void main(String[] args) {
SpringApplication.run(HelloJwtApplication.class, args);
}
}
鍵のパスをapplication.propertiesに定義します。
cat <<'EOF' > src/main/resources/application.properties
jwt.private-key=classpath:private_key.pem
jwt.public-key=classpath:public.pem
spring.application.name=hello-jwt
spring.security.oauth2.resourceserver.jwt.public-key-location=${jwt.public-key}
EOF
秘密鍵を使ってJWT署名するためのTokenSignerを作成します。署名にはSpring Security内で使われているnimbus-jose-jwtを使用します。
cat <<'EOF' > src/main/java/com/example/TokenSigner.java
package com.example;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
@Component
public class TokenSigner implements InitializingBean {
private final JWSSigner signer;
private final JWSVerifier verifier;
public TokenSigner(JwtProperties jwtProps) {
this.signer = new RSASSASigner(jwtProps.privateKey());
this.verifier = new RSASSAVerifier(jwtProps.publicKey());
}
public SignedJWT sign(JWTClaimsSet claimsSet) {
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
.type(JOSEObjectType.JWT)
.build();
SignedJWT signedJWT = new SignedJWT(header, claimsSet);
try {
signedJWT.sign(this.signer);
}
catch (JOSEException e) {
throw new IllegalStateException(e);
}
return signedJWT;
}
@Override
public void afterPropertiesSet() throws Exception {
// Validate the key-pair
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().subject("test").build();
SignedJWT signedJWT = sign(claimsSet);
if (!signedJWT.verify(this.verifier)) {
throw new IllegalStateException("The pair of public key and private key is wrong.");
}
}
}
EOF
Tokenを生成するControllerを作成します。
cat <<'EOF' > src/main/java/com/example/TokenController.java
package com.example;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import com.nimbusds.jwt.JWTClaimsSet;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
import static org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType.BEARER;
@RestController
public class TokenController {
private final TokenSigner tokenSigner;
private final AuthenticationManager authenticationManager;
private final Clock clock;
public TokenController(TokenSigner tokenSigner, AuthenticationManager authenticationManager, Clock clock) {
this.tokenSigner = tokenSigner;
this.authenticationManager = authenticationManager;
this.clock = clock;
}
@PostMapping(path = "/token")
public Object issueToken(@RequestParam String username, @RequestParam String password, UriComponentsBuilder builder) {
try {
Authentication authenticated = authenticationManager.authenticate(UsernamePasswordAuthenticationToken.unauthenticated(username, password));
UserDetails userDetails = (UserDetails) authenticated.getPrincipal();
String issuer = builder.path("").build().toString();
Instant issuedAt = Instant.now(this.clock);
Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);
Set<String> scope = Set.of("message:read", "message:write");
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.issuer(issuer)
.expirationTime(Date.from(expiresAt))
.subject(userDetails.getUsername())
.issueTime(Date.from(issuedAt))
.claim("scope", scope)
.build();
String tokenValue = this.tokenSigner.sign(claimsSet).serialize();
return ResponseEntity.ok(Map.of("access_token", tokenValue,
"token_type", BEARER.getValue(),
"expires_in", Duration.between(issuedAt, expiresAt).getSeconds(),
"scope", scope));
}
catch (AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "unauthorized",
"error_description", e.getMessage()));
}
}
}
EOF
ControllerでAuthenticationManagerを使うためにSecurityConfigに以下の設定を追加します。
import java.time.Clock;
// ...
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
// ...
@Bean
public Clock clock() {
return Clock.systemUTC();
}
// https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/index.html#publish-authentication-manager-bean
@Bean
public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(authenticationProvider);
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withUsername("bar@example.com")
.password("{noop}secret")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
アプリケーションを再起動して、次のコマンドでトークンを発行します。
$ curl -s http://localhost:8080/token -d username=bar@example.com -d password=secret | jq .
{
"scope": [
"message:write",
"message:read"
],
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwOi8vaG9zdC5vcmIuaW50ZXJuYWw6ODA4MCIsInN1YiI6ImJhckBleGFtcGxlLmNvbSIsImV4cCI6MTcyNTM0MzA0MSwiaWF0IjoxNzI1MzM5NDQxLCJzY29wZSI6WyJtZXNzYWdlOndyaXRlIiwibWVzc2FnZTpyZWFkIl19.wxraPTTuQFb4gEPfmUUXBuHAd6nLgiCVO6gTbPw5lYam1XQfe8m1c1HgapI0HwzkUEW4VT243t6E2erl43F7RWeVSsSVfiT5vGogqYd6iwVQ1mK2BPrGPyWyT8jUw0yuVGzHw03tpK6oBhL8j90R2CX1kUsWslQDQu2w1JPcP7MTeD0vN-gHj_dapx-ClBo5CtO7rLLvNc0US6REBbIisI45DTQliR3HypoZN8YaGHyaal2Q6uIi9JnL2Zow9VW4l8Drol-oIGdM-sx_ZrpHu3xmVpZfv2o-q4pAGEMvtYW_l1buzHB8anSuW2xV6AvPH5YF2jHYE6LDtuxach-2AQ",
"token_type": "Bearer",
"expires_in": 3600
}
JWTを取り出し、メッセージAPIにアクセスします。
JWT=$(curl -s http://localhost:8080/token -d username=bar@example.com -d password=secret | jq -r .access_token)
curl http://localhost:8080/messages -H "Authorization: Bearer $JWT" -H "Content-Type: text/plain" -d "Hello World"
curl http://localhost:8080/messages -H "Authorization: Bearer $JWT"
Integration Testの作成
最後に簡単なIntegration Testを作成します。
cat <<'EOF' > src/test/java/com/example/HelloJwtApplicationTests.java
package com.example;
import java.util.List;
import com.example.MessageController.Message;
import com.fasterxml.jackson.databind.JsonNode;
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.json.AutoConfigureJsonTesters;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.NoOpResponseErrorHandler;
import org.springframework.web.client.RestClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureJsonTesters
class HelloJwtApplicationTests {
@LocalServerPort
int port;
RestClient restClient;
@Autowired
JacksonTester<Message> messageTester;
@Autowired
JacksonTester<List<Message>> listTester;
@BeforeEach
void setUp(@Autowired RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder
.baseUrl("http://localhost:" + port)
.defaultStatusHandler(new NoOpResponseErrorHandler())
.build();
}
@Test
void issueTokenUsingValidCredentialsAndAccessMessageApi() throws Exception {
String token;
{
ResponseEntity<JsonNode> response = this.restClient.post()
.uri("/token")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body("username=bar@example.com&password=secret")
.retrieve()
.toEntity(JsonNode.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotEmpty();
token = response.getBody().get("access_token").asText();
}
{
ResponseEntity<Message> response = this.restClient.post()
.uri("/messages")
.contentType(MediaType.TEXT_PLAIN)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.body("Hello World")
.retrieve()
.toEntity(Message.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(this.messageTester.write(response.getBody())).isEqualToJson("""
{
"username": "bar@example.com",
"text": "Hello World"
}
""");
}
{
ResponseEntity<List<Message>> response = this.restClient.get()
.uri("/messages")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.retrieve()
.toEntity(new ParameterizedTypeReference<>() {
});
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(this.listTester.write(response.getBody())).isEqualToJson("""
[
{
"username": "bar@example.com",
"text": "Hello World"
}
]
""");
}
}
@Test
void issueTokenUsingInvalidCredentials() {
assertThatThrownBy(() -> this.restClient.post()
.uri("/token")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body("username=bar@example.com&password=bar")
.retrieve()
.toEntity(JsonNode.class))
.isInstanceOf(HttpClientErrorException.Unauthorized.class);
}
}
EOF
テストがパスすればOKです。
./mvnw clean test
Spring Boot + SecurityでJWT認証・認可を試しました。
作成したコードは https://github.com/making/hello-jwt です。
その他JWTのaudience Claimを検証したり、カスタム検証したりできます。詳しくはドキュメントを参照してください。