Skip to content

Commit a033e8b

Browse files
authored
Merge pull request #508 from kazuki-ma/channelTokenV2
Support Channel access token v2.1
2 parents cbda38f + a03a8a4 commit a033e8b

File tree

7 files changed

+194
-1
lines changed

7 files changed

+194
-1
lines changed

build.gradle

+5
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ subprojects {
9797
entry 'converter-jackson'
9898
entry 'retrofit'
9999
}
100+
dependencySet(group: 'io.jsonwebtoken', version: '0.11.1') {
101+
entry 'jjwt-api'
102+
entry 'jjwt-impl'
103+
entry 'jjwt-jackson'
104+
}
100105
}
101106
}
102107

line-bot-api-client/build.gradle

+4
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,15 @@ dependencies {
5252

5353
integrationTestCompileOnly 'org.projectlombok:lombok'
5454
integrationTestAnnotationProcessor 'org.projectlombok:lombok'
55+
integrationTestImplementation 'com.google.guava:guava'
56+
integrationTestImplementation 'io.jsonwebtoken:jjwt-api'
57+
integrationTestImplementation 'io.jsonwebtoken:jjwt-jackson'
5558
integrationTestImplementation 'org.springframework.boot:spring-boot-starter-test'
5659
integrationTestImplementation 'org.springframework.boot:spring-boot-starter-logging'
5760
integrationTestImplementation 'com.fasterxml.jackson.core:jackson-core'
5861
integrationTestImplementation 'com.fasterxml.jackson.core:jackson-databind'
5962
integrationTestImplementation 'com.fasterxml.jackson.core:jackson-annotations'
6063
integrationTestImplementation 'com.fasterxml.jackson.module:jackson-module-parameter-names'
6164
integrationTestImplementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml'
65+
integrationTestRuntime 'io.jsonwebtoken:jjwt-impl'
6266
}

line-bot-api-client/src/integrationTest/java/com/linecorp/bot/client/IntegrationTestSettingsLoader.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
2727

2828
public class IntegrationTestSettingsLoader {
29-
public static final URL TEST_RESOURCE = ClassLoader.getSystemResource("integration_test_settings.yml");
29+
private static final URL TEST_RESOURCE = ClassLoader.getSystemResource("integration_test_settings.yml");
3030

3131
public static IntegrationTestSettings load() throws IOException {
3232
// Do not run all test cases in this class when src/test/resources/integration_test_settings.yml doesn't
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright 2020 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.linecorp.bot.client;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import java.io.IOException;
22+
import java.net.URL;
23+
import java.security.KeyFactory;
24+
import java.security.PrivateKey;
25+
import java.security.spec.PKCS8EncodedKeySpec;
26+
import java.time.Duration;
27+
import java.time.Instant;
28+
import java.util.Map;
29+
30+
import org.junit.Assume;
31+
import org.junit.Before;
32+
import org.junit.Test;
33+
import org.slf4j.bridge.SLF4JBridgeHandler;
34+
import org.yaml.snakeyaml.Yaml;
35+
36+
import com.fasterxml.jackson.databind.ObjectMapper;
37+
import com.google.common.collect.ImmutableMap;
38+
import com.google.common.io.BaseEncoding;
39+
40+
import com.linecorp.bot.model.oauth.IssueChannelAccessTokenResponse;
41+
42+
import io.jsonwebtoken.Jwts;
43+
import io.jsonwebtoken.SignatureAlgorithm;
44+
import io.jsonwebtoken.jackson.io.JacksonSerializer;
45+
import lombok.extern.slf4j.Slf4j;
46+
47+
@Slf4j
48+
public class LineOAuthClientIntegrationTest {
49+
private static final URL TEST_RESOURCE = ClassLoader.getSystemResource("integration_test_settings.yml");
50+
51+
private LineOAuthClient target;
52+
private String endpoint;
53+
private String pemPrivateKey;
54+
private String channelId;
55+
private String channelSecret;
56+
private String kid;
57+
58+
@Before
59+
public void setUp() throws IOException {
60+
Assume.assumeTrue(TEST_RESOURCE != null);
61+
62+
final Map<?, ?> map = new ObjectMapper()
63+
.convertValue(new Yaml().load(TEST_RESOURCE.openStream()), Map.class);
64+
65+
endpoint = (String) map.get("endpoint");
66+
target = LineOAuthClient
67+
.builder()
68+
.apiEndPoint(endpoint)
69+
.build();
70+
71+
pemPrivateKey = ((String) map.get("pemPrivateKey")).replaceAll("\n", "");
72+
kid = (String) map.get("kid");
73+
channelId = String.valueOf(map.get("channelId"));
74+
channelSecret = (String) map.get("channelSecret");
75+
}
76+
77+
static {
78+
SLF4JBridgeHandler.removeHandlersForRootLogger();
79+
SLF4JBridgeHandler.install();
80+
}
81+
82+
@Test
83+
public void gwtTokenIntegrationTest() throws Exception {
84+
final Map<String, Object> header = ImmutableMap.of(
85+
"alg", "RS256",
86+
"typ", "JWT",
87+
"kid", kid
88+
);
89+
90+
final Map<String, Object> body = ImmutableMap.of(
91+
"iss", channelId,
92+
"sub", channelId,
93+
"aud", endpoint,
94+
"exp", Instant.now().plusSeconds(10).getEpochSecond(),
95+
"token_exp", Duration.ofMinutes(1).getSeconds()
96+
);
97+
98+
byte[] bytes = BaseEncoding.base64().decode(pemPrivateKey);
99+
100+
KeyFactory kf = KeyFactory.getInstance("RSA");
101+
PrivateKey privateKey = kf.generatePrivate(new PKCS8EncodedKeySpec(bytes));
102+
103+
String jws = Jwts.builder()
104+
.serializeToJsonWith(new JacksonSerializer(new ObjectMapper()))
105+
.setHeader(header)
106+
.setClaims(body)
107+
.signWith(privateKey, SignatureAlgorithm.RS256)
108+
.compact();
109+
110+
log.info("{}", jws);
111+
112+
// Issue
113+
IssueChannelAccessTokenResponse issueChannelAccessTokenResponse =
114+
target.issueChannelTokenByJWT(jws).get();
115+
116+
log.info("{}", issueChannelAccessTokenResponse);
117+
assertThat(issueChannelAccessTokenResponse.getExpiresInSecs()).isEqualTo(60);
118+
119+
// Revoke
120+
target.revokeChannelTokenByJWT(
121+
channelId,
122+
channelSecret,
123+
issueChannelAccessTokenResponse.getAccessToken())
124+
.get();
125+
}
126+
}

line-bot-api-client/src/main/java/com/linecorp/bot/client/LineOAuthClient.java

+26
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,32 @@ static LineOAuthClientBuilder builder() {
3535
return new LineOAuthClientBuilder();
3636
}
3737

38+
/**
39+
* Issues a channel access token. This method lets you use JWT assertion for authentication.
40+
*
41+
* <p>You can issue up to 30 tokens.
42+
* If you reach the maximum limit, additional requests of issuing channel access tokens are blocked.
43+
*
44+
* @param clientAssertion A JSON Web Token the client needs to create and sign with the private key created
45+
* when issuing an assertion signing key.
46+
* @see <a href="https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token-v2-1">Issue channel access token v2.1</a>
47+
*/
48+
CompletableFuture<IssueChannelAccessTokenResponse> issueChannelTokenByJWT(
49+
String clientAssertion);
50+
51+
/**
52+
* Revokes a channel access token.
53+
*
54+
* @param clientId Channel ID
55+
* @param clientSecret Channel Secret
56+
* @param accessToken Channel access token
57+
* @see <a href="https://developers.line.biz/en/reference/messaging-api/#revoke-channel-access-token-v2-1">Revoke channel access token v2.1</a>
58+
*/
59+
CompletableFuture<Void> revokeChannelTokenByJWT(
60+
String clientId,
61+
String clientSecret,
62+
String accessToken);
63+
3864
/**
3965
* Issues a short-lived channel access token. Up to 30 tokens can be issued. If the maximum is exceeded,
4066
* existing channel access tokens are revoked in the order of when they were first issued.

line-bot-api-client/src/main/java/com/linecorp/bot/client/LineOAuthClientImpl.java

+16
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.linecorp.bot.model.objectmapper.ModelObjectMapper;
2828

2929
import lombok.AllArgsConstructor;
30+
import lombok.experimental.PackagePrivate;
3031
import retrofit2.Call;
3132
import retrofit2.Callback;
3233
import retrofit2.Response;
@@ -35,11 +36,26 @@
3536
* An implementation of {@link LineOAuthClient} that issues or revokes channel access tokens.
3637
*/
3738
@AllArgsConstructor
39+
@PackagePrivate
3840
class LineOAuthClientImpl implements LineOAuthClient {
3941
private static final ObjectMapper objectMapper = ModelObjectMapper.createNewObjectMapper();
4042

4143
private final LineOAuthService service;
4244

45+
@Override
46+
public CompletableFuture<IssueChannelAccessTokenResponse> issueChannelTokenByJWT(final String jwt) {
47+
return toFuture(service.issueChannelTokenByJWT(
48+
"client_credentials",
49+
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
50+
jwt));
51+
}
52+
53+
@Override
54+
public CompletableFuture<Void> revokeChannelTokenByJWT(
55+
String clientId, String clientSecret, String accessToken) {
56+
return toFuture(service.revokeChannelTokenByJWT(clientId, clientSecret, accessToken));
57+
}
58+
4359
@Override
4460
public CompletableFuture<IssueChannelAccessTokenResponse> issueChannelToken(
4561
IssueChannelAccessTokenRequest req) {

line-bot-api-client/src/main/java/com/linecorp/bot/client/LineOAuthService.java

+16
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.linecorp.bot.model.oauth.ChannelAccessTokenException;
2222
import com.linecorp.bot.model.oauth.IssueChannelAccessTokenResponse;
2323

24+
import lombok.experimental.PackagePrivate;
2425
import retrofit2.Call;
2526
import retrofit2.http.Field;
2627
import retrofit2.http.FormUrlEncoded;
@@ -31,7 +32,22 @@
3132
* <a href="https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token">document</a>
3233
* for detail.
3334
*/
35+
@PackagePrivate
3436
interface LineOAuthService {
37+
@POST("oauth2/v2.1/token")
38+
@FormUrlEncoded
39+
Call<IssueChannelAccessTokenResponse> issueChannelTokenByJWT(
40+
@Field("grant_type") String grantType,
41+
@Field("client_assertion_type") String clientAssertionType,
42+
@Field("client_assertion") String clientAssertion);
43+
44+
@POST("/oauth2/v2.1/revoke")
45+
@FormUrlEncoded
46+
Call<Void> revokeChannelTokenByJWT(
47+
@Field("client_id") String clientId,
48+
@Field("client_secret") String clientSecret,
49+
@Field("access_token") String accessToken);
50+
3551
/**
3652
* Issues a short-lived channel access token. Up to 30 tokens can be issued. If the maximum is exceeded,
3753
* existing channel access tokens are revoked in the order of when they were first issued.

0 commit comments

Comments
 (0)