Skip to content

Commit 80671b1

Browse files
authored
Merge pull request #186 from larksuite/codex/keyless-logic
Codex/keyless logic
2 parents 91abd61 + ff32f28 commit 80671b1

34 files changed

Lines changed: 3764 additions & 17 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@ skills
4343
plan
4444
skills-lock.json
4545
channel-diff-tasks
46-
test
46+
/test
4747
docs/superpowers/plans
48+
doc

CHANNEL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public class AgentBot {
9393
<dependency>
9494
<groupId>com.larksuite.oapi</groupId>
9595
<artifactId>oapi-sdk</artifactId>
96-
<version>2.6.1</version>
96+
<version>2.7.3</version>
9797
</dependency>
9898
```
9999

larksuite-oapi/src/main/java/com/lark/oapi/Client.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
import com.lark.oapi.service.cardkit.CardkitService;
7373

7474
import com.lark.oapi.service.ext.ExtService;
75+
import com.lark.oapi.core.auth.ClientAssertionProvider;
7576
import com.lark.oapi.core.httpclient.IHttpTransport;
7677
import com.lark.oapi.core.httpclient.OkHttpTransport;
7778
import com.lark.oapi.core.Transport;
@@ -169,6 +170,7 @@ public class Client {
169170
private CardkitService cardkit;
170171

171172
private ExtService extService;
173+
private com.lark.oapi.core.accesstoken.AccessToken accessToken;
172174

173175
public static Builder newBuilder(String appId, String appSecret) {
174176
return new Builder(appId, appSecret);
@@ -178,6 +180,10 @@ public ExtService ext() {
178180
return extService;
179181
}
180182

183+
public com.lark.oapi.core.accesstoken.AccessToken accessToken() {
184+
return accessToken;
185+
}
186+
181187
public void setConfig(Config config) {
182188
this.config = config;
183189
}
@@ -538,6 +544,16 @@ public Builder openBaseUrl(BaseUrlEnum baseUrl) {
538544
return this;
539545
}
540546

547+
public Builder oauthBaseUrl(String oauthBaseUrl) {
548+
config.setOAuthBaseUrl(oauthBaseUrl);
549+
return this;
550+
}
551+
552+
public Builder clientAssertionProvider(ClientAssertionProvider provider) {
553+
config.setClientAssertionProvider(provider);
554+
return this;
555+
}
556+
541557
public Builder tokenCache(ICache cache) {
542558
config.setCache(cache);
543559
return this;
@@ -585,6 +601,7 @@ public Client build() {
585601
client.setConfig(config);
586602
initCache(config);
587603
initHttpTransport(config);
604+
client.accessToken = new com.lark.oapi.core.accesstoken.AccessToken(config);
588605
client.extService = new ExtService(config);
589606
client.wiki = new WikiService(config);
590607
client.workplace = new WorkplaceService(config);

larksuite-oapi/src/main/java/com/lark/oapi/channel/ChannelClientFactory.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ static Client createRawClient(LarkChannelOptions options) {
2525
if (options.getSource() != null) {
2626
builder.source(options.getSource());
2727
}
28+
if (options.getClientAssertionProvider() != null) {
29+
builder.clientAssertionProvider(options.getClientAssertionProvider());
30+
}
31+
if (options.getOAuthBaseUrl() != null) {
32+
builder.oauthBaseUrl(options.getOAuthBaseUrl());
33+
}
2834
return builder.build();
2935
}
3036

@@ -39,6 +45,7 @@ static com.lark.oapi.ws.Client createWebSocketClient(
3945
.eventHandler(eventDispatcher)
4046
.domain(options.getDomain() == null ? BaseUrlEnum.FeiShu.getUrl() : options.getDomain())
4147
.source(options.getSource())
48+
.clientAssertionProvider(options.getClientAssertionProvider())
4249
.onReconnecting(new Runnable() {
4350
@Override
4451
public void run() {

larksuite-oapi/src/main/java/com/lark/oapi/channel/config/LarkChannelOptions.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.lark.oapi.channel.config;
22

3+
import com.lark.oapi.core.auth.ClientAssertionProvider;
34
import com.lark.oapi.core.cache.ICache;
45
import com.lark.oapi.core.httpclient.IHttpTransport;
56
import com.lark.oapi.core.request.RequestOptions;
@@ -28,6 +29,8 @@ public class LarkChannelOptions {
2829
private final RequestOptions httpInstance;
2930
private final String source;
3031
private final boolean includeRawInMessage;
32+
private final ClientAssertionProvider clientAssertionProvider;
33+
private final String oauthBaseUrl;
3134

3235
private LarkChannelOptions(Builder builder) {
3336
this.appId = builder.appId;
@@ -43,6 +46,8 @@ private LarkChannelOptions(Builder builder) {
4346
this.httpInstance = builder.httpInstance;
4447
this.source = builder.source;
4548
this.includeRawInMessage = builder.includeRawInMessage;
49+
this.clientAssertionProvider = builder.clientAssertionProvider;
50+
this.oauthBaseUrl = builder.oauthBaseUrl;
4651
}
4752

4853
public static Builder newBuilder(String appId, String appSecret) {
@@ -97,6 +102,14 @@ public String getSource() {
97102
return source;
98103
}
99104

105+
public ClientAssertionProvider getClientAssertionProvider() {
106+
return clientAssertionProvider;
107+
}
108+
109+
public String getOAuthBaseUrl() {
110+
return oauthBaseUrl;
111+
}
112+
100113
/**
101114
* Whether normalized events should carry the original Feishu event body.
102115
*
@@ -131,6 +144,8 @@ public static final class Builder {
131144
private RequestOptions httpInstance;
132145
private String source;
133146
private boolean includeRawInMessage;
147+
private ClientAssertionProvider clientAssertionProvider;
148+
private String oauthBaseUrl;
134149

135150
private Builder(String appId, String appSecret) {
136151
this.appId = appId;
@@ -191,6 +206,16 @@ public Builder source(String source) {
191206
return this;
192207
}
193208

209+
public Builder clientAssertionProvider(ClientAssertionProvider clientAssertionProvider) {
210+
this.clientAssertionProvider = clientAssertionProvider;
211+
return this;
212+
}
213+
214+
public Builder oauthBaseUrl(String oauthBaseUrl) {
215+
this.oauthBaseUrl = oauthBaseUrl;
216+
return this;
217+
}
218+
194219
/**
195220
* Attach the raw Feishu event body to normalized events. Useful when a
196221
* handler needs fields that the normalizer intentionally drops, such as

larksuite-oapi/src/main/java/com/lark/oapi/core/Config.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515

1616
import com.lark.oapi.core.cache.ICache;
17+
import com.lark.oapi.core.auth.ClientAssertionProvider;
1718
import com.lark.oapi.core.enums.AppType;
1819
import com.lark.oapi.core.enums.BaseUrlEnum;
1920
import com.lark.oapi.core.httpclient.IHttpTransport;
@@ -37,6 +38,8 @@ public class Config {
3738
private IHttpTransport httpTransport;
3839
private boolean logReqAtDebug;
3940
private String source;
41+
private String oauthBaseUrl;
42+
private ClientAssertionProvider clientAssertionProvider;
4043

4144
public Config() {
4245
this.baseUrl = BaseUrlEnum.FeiShu.getUrl();
@@ -167,4 +170,20 @@ public void setSource(String source) {
167170
this.source = source;
168171
}
169172

173+
public String getOAuthBaseUrl() {
174+
return oauthBaseUrl;
175+
}
176+
177+
public void setOAuthBaseUrl(String oauthBaseUrl) {
178+
this.oauthBaseUrl = oauthBaseUrl;
179+
}
180+
181+
public ClientAssertionProvider getClientAssertionProvider() {
182+
return clientAssertionProvider;
183+
}
184+
185+
public void setClientAssertionProvider(ClientAssertionProvider clientAssertionProvider) {
186+
this.clientAssertionProvider = clientAssertionProvider;
187+
}
188+
170189
}

larksuite-oapi/src/main/java/com/lark/oapi/core/Constants.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ public interface Constants {
4040
String APP_ACCESS_TOKEN_ISV_URL_PATH = "/open-apis/auth/v3/app_access_token";
4141
String TENANT_ACCESS_TOKEN_INTERNAL_URL_PATH = "/open-apis/auth/v3/tenant_access_token/internal";
4242
String TENANT_ACCESS_TOKEN_ISV_URL_PATH = "/open-apis/auth/v3/tenant_access_token";
43+
String OAUTH_TOKEN_URL_PATH = "/oauth/v3/token";
44+
String GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code";
45+
String GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
46+
String GRANT_TYPE_JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer";
47+
String CLIENT_ASSERTION_TYPE_JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
48+
String HEADER_X_TARGET_SERVICE = "X-Target-Service";
49+
int ERR_CODE_CLIENT_ASSERTION_PROVIDER_NOT_CONFIGURED = 7100;
50+
int ERR_CODE_CLIENT_ASSERTION_TOKEN_EMPTY = 7101;
51+
int ERR_CODE_CLIENT_ASSERTION_RETRIEVE_FAILED = 7102;
52+
int ERR_CODE_CLIENT_ASSERTION_MODE_NOT_SUPPORTED = 7103;
53+
int ERR_CODE_APP_SECRET_AND_CLIENT_ASSERTION_EMPTY = 7104;
4354
String APPLY_APP_TICKET_PATH = "/open-apis/auth/v3/app_ticket/resend";
4455
String GET_AUTHEN_ACCESS_TOKEN = "/open-apis/authen/v1/access_token";
4556
String REFRESH_AUTHEN_ACCESS_TOKEN = "/open-apis/authen/v1/refresh_access_token";

larksuite-oapi/src/main/java/com/lark/oapi/core/Transport.java

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import com.lark.oapi.core.exception.AccessTokenNotGivenException;
1616
import com.lark.oapi.core.exception.ClientTimeoutException;
17+
import com.lark.oapi.core.exception.ClientAssertionException;
1718
import com.lark.oapi.core.exception.IllegalAccessTokenTypeException;
1819
import com.lark.oapi.core.exception.ServerTimeoutException;
1920
import com.lark.oapi.core.httpclient.IHttpTransport;
@@ -23,6 +24,7 @@
2324
import com.lark.oapi.core.request.RequestOptions;
2425
import com.lark.oapi.core.response.RawResponse;
2526
import com.lark.oapi.core.token.AccessTokenType;
27+
import com.lark.oapi.core.enums.AppType;
2628
import com.lark.oapi.core.utils.Jsons;
2729
import com.lark.oapi.core.utils.OKHttps;
2830
import com.lark.oapi.core.utils.Strings;
@@ -31,15 +33,46 @@
3133

3234
import java.io.InterruptedIOException;
3335
import java.nio.charset.StandardCharsets;
36+
import java.util.HashMap;
37+
import java.util.List;
38+
import java.util.Locale;
39+
import java.util.Map;
3440
import java.util.Set;
3541

3642
public class Transport {
3743

3844
private static final Logger log = LoggerFactory.getLogger(Transport.class);
3945
private static final ReqTranslator REQ_TRANSLATOR = new ReqTranslator();
46+
private static final String OMITTED = "<omitted>";
4047

4148
private static AccessTokenType determineTokenType(Set<AccessTokenType> accessTokenTypeSet,
42-
RequestOptions requestOptions, boolean disableTokenCache) {
49+
RequestOptions requestOptions, boolean disableTokenCache,
50+
Config config) {
51+
if (config.getClientAssertionProvider() != null) {
52+
validateTokenType(accessTokenTypeSet, requestOptions);
53+
54+
if (Strings.isNotEmpty(requestOptions.getUserAccessToken())
55+
&& accessTokenTypeSet.contains(AccessTokenType.User)) {
56+
return AccessTokenType.User;
57+
}
58+
59+
if (accessTokenTypeSet.contains(AccessTokenType.Tenant)) {
60+
return AccessTokenType.Tenant;
61+
}
62+
63+
if (accessTokenTypeSet.contains(AccessTokenType.App)) {
64+
throw new ClientAssertionException(
65+
Constants.ERR_CODE_CLIENT_ASSERTION_MODE_NOT_SUPPORTED,
66+
"AppAccessToken APIs are not available in ClientAssertion mode");
67+
}
68+
69+
if (accessTokenTypeSet.contains(AccessTokenType.None)) {
70+
return AccessTokenType.None;
71+
}
72+
73+
throw new IllegalAccessTokenTypeException();
74+
}
75+
4376
if (accessTokenTypeSet.contains(AccessTokenType.None)) {
4477
return AccessTokenType.None;
4578
}
@@ -110,7 +143,21 @@ private static void validate(Config config, RequestOptions requestOptions,
110143
throw new IllegalArgumentException("appId is blank");
111144
}
112145

113-
if (Strings.isEmpty(config.getAppSecret())) {
146+
if (config.getClientAssertionProvider() != null
147+
&& config.getAppType() == AppType.MARKETPLACE) {
148+
throw new ClientAssertionException(
149+
Constants.ERR_CODE_CLIENT_ASSERTION_PROVIDER_NOT_CONFIGURED,
150+
"ClientAssertion mode is not supported for marketplace apps");
151+
}
152+
153+
boolean hasManualAccessToken =
154+
(accessTokenType == AccessTokenType.User && Strings.isNotEmpty(requestOptions.getUserAccessToken()))
155+
|| (accessTokenType == AccessTokenType.Tenant && Strings.isNotEmpty(requestOptions.getTenantAccessToken()))
156+
|| (accessTokenType == AccessTokenType.App && Strings.isNotEmpty(requestOptions.getAppAccessToken()));
157+
158+
if (config.getClientAssertionProvider() == null
159+
&& Strings.isEmpty(config.getAppSecret())
160+
&& !hasManualAccessToken) {
114161
throw new IllegalArgumentException("appSecret is blank");
115162
}
116163

@@ -176,7 +223,8 @@ public static RawResponse send(Config config
176223
// 确定token类型
177224
AccessTokenType accessTokenType = determineTokenType(accessTokenTypeSet
178225
, requestOptions
179-
, config.isDisableTokenCache());
226+
, config.isDisableTokenCache()
227+
, config);
180228

181229
// 参数校验
182230
validate(config, requestOptions, accessTokenType);
@@ -203,16 +251,60 @@ private static void logReq(RawRequest req, String httpPath, boolean isUpload) {
203251

204252
if (!isUpload) {
205253
log.debug("req,path:{},header:{},body:{}", httpPath
206-
, Jsons.DEFAULT.toJson(req.getHeaders())
207-
, req.getBody() == null ? "" : Jsons.DEFAULT.toJson(req.getBody()));
254+
, Jsons.DEFAULT.toJson(safeHeaders(req.getHeaders()))
255+
, safeBody(req.getBody()));
208256
} else {
209-
log.debug("req,path:{},header:{}", httpPath, req.getHeaders());
257+
log.debug("req,path:{},header:{}", httpPath, safeHeaders(req.getHeaders()));
210258
}
211259
} catch (Throwable e) {
212260
log.error("logReq error:{}", e);
213261
}
214262
}
215263

264+
private static Map<String, List<String>> safeHeaders(Map<String, List<String>> headers) {
265+
Map<String, List<String>> safeHeaders = new HashMap<>();
266+
if (headers == null) {
267+
return safeHeaders;
268+
}
269+
headers.entrySet().stream().forEach(entry -> {
270+
if (!isSensitiveKey(entry.getKey())) {
271+
safeHeaders.put(entry.getKey(), entry.getValue());
272+
}
273+
});
274+
return safeHeaders;
275+
}
276+
277+
private static String safeBody(Object body) {
278+
if (body == null) {
279+
return "";
280+
}
281+
String json = Jsons.DEFAULT.toJson(body);
282+
return containsSensitiveField(json) ? OMITTED : json;
283+
}
284+
285+
private static boolean containsSensitiveField(String json) {
286+
if (Strings.isEmpty(json)) {
287+
return false;
288+
}
289+
String normalized = json.toLowerCase(Locale.ROOT);
290+
return normalized.contains("\"client_secret\"")
291+
|| normalized.contains("\"clientassertion\"")
292+
|| normalized.contains("\"client_assertion\"")
293+
|| normalized.contains("\"refresh_token\"")
294+
|| normalized.contains("\"access_token\"")
295+
|| normalized.contains("\"tenant_access_token\"")
296+
|| normalized.contains("\"app_access_token\"");
297+
}
298+
299+
private static boolean isSensitiveKey(String key) {
300+
if (Strings.isEmpty(key)) {
301+
return false;
302+
}
303+
String normalized = key.toLowerCase(Locale.ROOT);
304+
return "authorization".equals(normalized)
305+
|| Constants.X_HELPDESK_AUTHORIZATION.toLowerCase(Locale.ROOT).equals(normalized);
306+
}
307+
216308
private static RawResponse doSend(Config config, String httpMethod, String httpPath,
217309
AccessTokenType accessTokenType, Object req, RequestOptions requestOptions) throws Exception {
218310
Exception error = null;

0 commit comments

Comments
 (0)