From e30d947f23029ad1da812fe92a9e0ad77295cc3f Mon Sep 17 00:00:00 2001 From: habara keigo Date: Wed, 1 Apr 2026 17:02:47 +0900 Subject: [PATCH 1/2] Implement wrapper method of issue_stateless_channel_token --- .../client/ChannelAccessTokenClient.java | 15 +++++ .../ChannelAccessTokenClientExTest.java | 56 +++++++++++++++++++ .../resources/line-java-codegen/api.pebble | 2 +- .../body/api/ChannelAccessTokenClient.java | 20 +++++++ 4 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 generator/src/main/resources/line-java-codegen/body/api/ChannelAccessTokenClient.java diff --git a/clients/line-channel-access-token-client/src/main/java/com/linecorp/bot/oauth/client/ChannelAccessTokenClient.java b/clients/line-channel-access-token-client/src/main/java/com/linecorp/bot/oauth/client/ChannelAccessTokenClient.java index abcfd3346..2730ebfc2 100644 --- a/clients/line-channel-access-token-client/src/main/java/com/linecorp/bot/oauth/client/ChannelAccessTokenClient.java +++ b/clients/line-channel-access-token-client/src/main/java/com/linecorp/bot/oauth/client/ChannelAccessTokenClient.java @@ -173,4 +173,19 @@ public static ApiClientBuilder builder() { ChannelAccessTokenClient.class, new ChannelAccessTokenExceptionBuilder()); } + + default CompletableFuture> + issueStatelessChannelTokenByJWTAssertion(String clientAssertion) { + return issueStatelessChannelToken( + "client_credentials", + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + clientAssertion, + null, + null); + } + + default CompletableFuture> + issueStatelessChannelTokenByClientSecret(String clientId, String clientSecret) { + return issueStatelessChannelToken("client_credentials", null, null, clientId, clientSecret); + } } diff --git a/clients/line-channel-access-token-client/src/test/java/com/linecorp/bot/oauth/client/ChannelAccessTokenClientExTest.java b/clients/line-channel-access-token-client/src/test/java/com/linecorp/bot/oauth/client/ChannelAccessTokenClientExTest.java index 0cc5dd565..a25777a71 100644 --- a/clients/line-channel-access-token-client/src/test/java/com/linecorp/bot/oauth/client/ChannelAccessTokenClientExTest.java +++ b/clients/line-channel-access-token-client/src/test/java/com/linecorp/bot/oauth/client/ChannelAccessTokenClientExTest.java @@ -177,4 +177,60 @@ public void issueStatelessChannelToken() { "grant_type=client_credentials&client_id=1234&client_secret=clientSecret"))); assertThat(actualResponse.tokenType()).isEqualTo("Bearer"); } + + @Test + public void issueStatelessChannelTokenByJWTAssertion() { + stubFor(post(urlEqualTo("/oauth2/v3/token")).willReturn( + aResponse() + .withStatus(200) + .withBody(""" + { + "access_token":"accessToken", + "expires_in":30, + "token_type":"Bearer" + }""" + ))); + + // Do + final IssueStatelessChannelAccessTokenResponse actualResponse = + target.issueStatelessChannelTokenByJWTAssertion("dummyClientAssertion") + .join().body(); + + // Verify + verify(postRequestedFor( + urlEqualTo("/oauth2/v3/token") + ).withRequestBody( + WireMock.equalTo( + "grant_type=client_credentials" + + "&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer" + + "&client_assertion=dummyClientAssertion"))); + assertThat(actualResponse.tokenType()).isEqualTo("Bearer"); + } + + @Test + public void issueStatelessChannelTokenByClientSecret() { + stubFor(post(urlEqualTo("/oauth2/v3/token")).willReturn( + aResponse() + .withStatus(200) + .withBody(""" + { + "access_token":"accessToken", + "expires_in":30, + "token_type":"Bearer" + }""" + ))); + + // Do + final IssueStatelessChannelAccessTokenResponse actualResponse = + target.issueStatelessChannelTokenByClientSecret("1234", "clientSecret") + .join().body(); + + // Verify + verify(postRequestedFor( + urlEqualTo("/oauth2/v3/token") + ).withRequestBody( + WireMock.equalTo( + "grant_type=client_credentials&client_id=1234&client_secret=clientSecret"))); + assertThat(actualResponse.tokenType()).isEqualTo("Bearer"); + } } diff --git a/generator/src/main/resources/line-java-codegen/api.pebble b/generator/src/main/resources/line-java-codegen/api.pebble index 6a9346ea5..22b3128d9 100644 --- a/generator/src/main/resources/line-java-codegen/api.pebble +++ b/generator/src/main/resources/line-java-codegen/api.pebble @@ -83,5 +83,5 @@ public interface {{classname}} { return new ApiClientBuilder<>(URI.create("{{ endpoint(classname) }}"), {{classname}}.class, new {{ exceptionbuilderclassname(classname) }}()); } {% endif %} -{% if ["LiffClient"] contains classname %}{% include "./body/api/" + classname + ".java" %}{% endif %} +{% if ["LiffClient", "ChannelAccessTokenClient"] contains classname %}{% include "./body/api/" + classname + ".java" %}{% endif %} } diff --git a/generator/src/main/resources/line-java-codegen/body/api/ChannelAccessTokenClient.java b/generator/src/main/resources/line-java-codegen/body/api/ChannelAccessTokenClient.java new file mode 100644 index 000000000..eacc71d6a --- /dev/null +++ b/generator/src/main/resources/line-java-codegen/body/api/ChannelAccessTokenClient.java @@ -0,0 +1,20 @@ +default CompletableFuture> issueStatelessChannelTokenByJWTAssertion( + String clientAssertion) { + return issueStatelessChannelToken( + "client_credentials", + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + clientAssertion, + null, + null); +} + +default CompletableFuture> issueStatelessChannelTokenByClientSecret( + String clientId, + String clientSecret) { + return issueStatelessChannelToken( + "client_credentials", + null, + null, + clientId, + clientSecret); +} From 4fc2bf40271535c2080bbef56127ca74eb46dc86 Mon Sep 17 00:00:00 2001 From: habara keigo Date: Thu, 2 Apr 2026 17:07:11 +0900 Subject: [PATCH 2/2] Add in-code docs --- .../client/ChannelAccessTokenClient.java | 27 +++++++++++++++++++ .../ChannelAccessTokenClientExTest.java | 1 + .../resources/line-java-codegen/api.pebble | 6 +++++ .../body/api/ChannelAccessTokenClient.java | 19 +++++++++++++ 4 files changed, 53 insertions(+) diff --git a/clients/line-channel-access-token-client/src/main/java/com/linecorp/bot/oauth/client/ChannelAccessTokenClient.java b/clients/line-channel-access-token-client/src/main/java/com/linecorp/bot/oauth/client/ChannelAccessTokenClient.java index 2730ebfc2..6d952576e 100644 --- a/clients/line-channel-access-token-client/src/main/java/com/linecorp/bot/oauth/client/ChannelAccessTokenClient.java +++ b/clients/line-channel-access-token-client/src/main/java/com/linecorp/bot/oauth/client/ChannelAccessTokenClient.java @@ -98,10 +98,13 @@ CompletableFuture> issueChannelTokenByJW * key of the Assertion Signing Key. (optional) * @param clientId Channel ID. (optional) * @param clientSecret Channel secret. (optional) + * @deprecated Use {@link #issueStatelessChannelTokenByJWTAssertion(String)} or {@link + * #issueStatelessChannelTokenByClientSecret(String, String)} instead. * @see * Documentation */ + @Deprecated @POST("/oauth2/v3/token") @FormUrlEncoded CompletableFuture> issueStatelessChannelToken( @@ -174,6 +177,18 @@ public static ApiClientBuilder builder() { new ChannelAccessTokenExceptionBuilder()); } + /** + * Issues a new stateless channel access token by JWT assertion. The newly issued token is only + * valid for 15 minutes but can not be revoked until it naturally expires. + * + * @param clientAssertion A JSON Web Token the client needs to create and sign with the private + * key of the Assertion Signing Key. + * @return A {@link CompletableFuture} containing the {@link + * IssueStatelessChannelAccessTokenResponse}. + * @see Documentation + */ + @SuppressWarnings("deprecation") default CompletableFuture> issueStatelessChannelTokenByJWTAssertion(String clientAssertion) { return issueStatelessChannelToken( @@ -184,6 +199,18 @@ public static ApiClientBuilder builder() { null); } + /** + * Issues a new stateless channel access token by client secret. The newly issued token is only + * valid for 15 minutes but can not be revoked until it naturally expires. + * + * @param clientId Channel ID. + * @param clientSecret Channel secret. + * @return A {@link CompletableFuture} containing the {@link + * IssueStatelessChannelAccessTokenResponse}. + * @see Documentation + */ + @SuppressWarnings("deprecation") default CompletableFuture> issueStatelessChannelTokenByClientSecret(String clientId, String clientSecret) { return issueStatelessChannelToken("client_credentials", null, null, clientId, clientSecret); diff --git a/clients/line-channel-access-token-client/src/test/java/com/linecorp/bot/oauth/client/ChannelAccessTokenClientExTest.java b/clients/line-channel-access-token-client/src/test/java/com/linecorp/bot/oauth/client/ChannelAccessTokenClientExTest.java index a25777a71..dcceb6d2f 100644 --- a/clients/line-channel-access-token-client/src/test/java/com/linecorp/bot/oauth/client/ChannelAccessTokenClientExTest.java +++ b/clients/line-channel-access-token-client/src/test/java/com/linecorp/bot/oauth/client/ChannelAccessTokenClientExTest.java @@ -41,6 +41,7 @@ import com.linecorp.bot.oauth.model.IssueShortLivedChannelAccessTokenResponse; import com.linecorp.bot.oauth.model.IssueStatelessChannelAccessTokenResponse; +@SuppressWarnings("deprecation") public class ChannelAccessTokenClientExTest { static { SLF4JBridgeHandler.removeHandlersForRootLogger(); diff --git a/generator/src/main/resources/line-java-codegen/api.pebble b/generator/src/main/resources/line-java-codegen/api.pebble index 22b3128d9..c3a0c6266 100644 --- a/generator/src/main/resources/line-java-codegen/api.pebble +++ b/generator/src/main/resources/line-java-codegen/api.pebble @@ -49,6 +49,9 @@ public interface {{classname}} { {% if op.isDeprecated -%} * @deprecated {% endif -%} + {% if op.operationId == "issueStatelessChannelToken" -%} + * @deprecated Use {@link #issueStatelessChannelTokenByJWTAssertion(String)} or {@link #issueStatelessChannelTokenByClientSecret(String, String)} instead. + {% endif -%} {% if op.externalDocs != null -%} * {{op.externalDocs.description}} * @see {{op.summary}} Documentation @@ -57,6 +60,9 @@ public interface {{classname}} { {% if op.isDeprecated -%} @Deprecated {% endif %} + {% if op.operationId == "issueStatelessChannelToken" -%} + @Deprecated + {% endif %} {% if op.isResponseFile -%} @Streaming {% endif %} diff --git a/generator/src/main/resources/line-java-codegen/body/api/ChannelAccessTokenClient.java b/generator/src/main/resources/line-java-codegen/body/api/ChannelAccessTokenClient.java index eacc71d6a..5c6c0b870 100644 --- a/generator/src/main/resources/line-java-codegen/body/api/ChannelAccessTokenClient.java +++ b/generator/src/main/resources/line-java-codegen/body/api/ChannelAccessTokenClient.java @@ -1,3 +1,12 @@ +/** + * Issues a new stateless channel access token by JWT assertion. + * The newly issued token is only valid for 15 minutes but can not be revoked until it naturally expires. + * + * @param clientAssertion A JSON Web Token the client needs to create and sign with the private key of the Assertion Signing Key. + * @return A {@link CompletableFuture} containing the {@link IssueStatelessChannelAccessTokenResponse}. + * @see Documentation + */ +@SuppressWarnings("deprecation") default CompletableFuture> issueStatelessChannelTokenByJWTAssertion( String clientAssertion) { return issueStatelessChannelToken( @@ -8,6 +17,16 @@ default CompletableFuture> issu null); } +/** + * Issues a new stateless channel access token by client secret. + * The newly issued token is only valid for 15 minutes but can not be revoked until it naturally expires. + * + * @param clientId Channel ID. + * @param clientSecret Channel secret. + * @return A {@link CompletableFuture} containing the {@link IssueStatelessChannelAccessTokenResponse}. + * @see Documentation + */ +@SuppressWarnings("deprecation") default CompletableFuture> issueStatelessChannelTokenByClientSecret( String clientId, String clientSecret) {