-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathOAuth2Config.java
More file actions
357 lines (320 loc) · 12.9 KB
/
Copy pathOAuth2Config.java
File metadata and controls
357 lines (320 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
package com.mastercard.developer.oauth2.config;
import com.mastercard.developer.oauth2.config.exception.OAuth2ClientConfigException;
import com.mastercard.developer.oauth2.core.access_token.AccessTokenStore;
import com.mastercard.developer.oauth2.core.access_token.InMemoryAccessTokenStore;
import com.mastercard.developer.oauth2.core.dpop.DPoPKey;
import com.mastercard.developer.oauth2.core.dpop.DPoPKeyProvider;
import com.mastercard.developer.oauth2.core.scope.ScopeResolver;
import com.mastercard.developer.oauth2.http.UserAgent;
import java.net.URL;
import java.security.Key;
import java.security.PrivateKey;
import java.security.interfaces.ECKey;
import java.security.interfaces.RSAKey;
import java.time.Duration;
/**
* Immutable configuration for OAuth2 clients supporting DPoP-bound access tokens.
* This class provides all necessary configuration parameters for establishing OAuth2
* authentication with token endpoint, including client credentials, DPoP proof generation,
* scope management, and access token storage.
* <p>
* Configuration instances are created using the builder pattern. All required fields must
* be provided before building, otherwise an {@link OAuth2ClientConfigException} will be thrown.
* <p>
* Example usage:
* <pre>
* OAuth2Config config = OAuth2Config.builder()
* .securityProfile(SecurityProfile.FAPI2SP_PRIVATE_KEY_DPOP)
* .clientId("ZvT0sklPsqzTNgKJIiex5_wppXz0Tj2wl33LUZtXmCQH8dry")
* .tokenEndpoint(URI.create("https://sandbox.api.mastercard.com/oauth/token"))
* .issuer(URI.create("https://sandbox.api.mastercard.com"))
* .clientKey(clientKey)
* .kid("302449525fad5309874b16298f3cbaaf0000000000000000")
* .accessTokenStore(new InMemoryAccessTokenStore())
* .scopeResolver(new StaticScopeResolver(Set.of("service:scope1", "service:scope2")))
* .dpopKeyProvider(new StaticDPoPKeyProvider(dpopKeyPair))
* .clockSkewTolerance(Duration.ofSeconds(10))
* .build();
* </pre>
*/
public class OAuth2Config {
private final String clientId;
private final URL tokenEndpoint;
private final URL issuer;
private final Duration clockSkewTolerance;
private final String userAgent;
private final ScopeResolver scopeResolver;
private final AccessTokenStore accessTokenStore;
private final PrivateKey clientKey;
private final String kid;
private final DPoPKeyProvider dpopKeyProvider;
private final SecurityProfile securityProfile;
private OAuth2Config(OAuth2ConfigBuilder builder) {
this.clientId = builder.clientId;
this.tokenEndpoint = builder.tokenEndpoint;
this.issuer = builder.issuer;
this.clockSkewTolerance = builder.clockSkewTolerance;
this.userAgent = builder.userAgent;
this.scopeResolver = builder.scopeResolver;
this.accessTokenStore = builder.accessTokenStore;
this.clientKey = builder.clientKey;
this.kid = builder.kid;
this.dpopKeyProvider = builder.dpopKeyProvider;
this.securityProfile = builder.securityProfile;
}
/**
* Create a new builder for {@link OAuth2Config}.
*/
public static OAuth2ConfigBuilder builder() {
return new OAuth2ConfigBuilder();
}
public URL getIssuer() {
return issuer;
}
public String getClientId() {
return clientId;
}
public URL getTokenEndpoint() {
return tokenEndpoint;
}
public Duration getClockSkewTolerance() {
return clockSkewTolerance;
}
public String getUserAgent() {
return userAgent;
}
public ScopeResolver getScopeResolver() {
return scopeResolver;
}
public AccessTokenStore getAccessTokenStore() {
return accessTokenStore;
}
public PrivateKey getClientKey() {
return clientKey;
}
public String getKid() {
return kid;
}
public DPoPKeyProvider getDPoPKeyProvider() {
return dpopKeyProvider;
}
/**
* Builder for constructing {@link OAuth2Config} instances.
* Provides a fluent API for configuring all OAuth2 client parameters with validation
* on build. Default values are provided for optional parameters.
*/
public static class OAuth2ConfigBuilder {
private String clientId;
private URL tokenEndpoint;
private URL issuer;
private Duration clockSkewTolerance = Duration.ofSeconds(5);
private String userAgent = UserAgent.get();
private ScopeResolver scopeResolver;
private AccessTokenStore accessTokenStore = new InMemoryAccessTokenStore();
private PrivateKey clientKey;
private String kid;
private DPoPKeyProvider dpopKeyProvider;
private SecurityProfile securityProfile = SecurityProfile.FAPI2SP_PRIVATE_KEY_DPOP;
private OAuth2ConfigBuilder() {}
/**
* Sets the authorization server's unique identifier.
* See: <a href="https://datatracker.ietf.org/doc/html/rfc8414#section-2">Authorization Server Metadata</a>
*/
public OAuth2ConfigBuilder issuer(URL issuer) {
this.issuer = issuer;
return this;
}
/**
* Sets the OAuth2 client identifier.
*/
public OAuth2ConfigBuilder clientId(String clientId) {
this.clientId = clientId.trim();
return this;
}
/**
* Sets the OAuth2 token endpoint URL where token requests will be sent.
*/
public OAuth2ConfigBuilder tokenEndpoint(URL tokenEndpoint) {
this.tokenEndpoint = tokenEndpoint;
return this;
}
/**
* Sets the tolerance for clock skew when validating token expiration.
* Must be a positive duration. Default is 5 seconds.
*/
public OAuth2ConfigBuilder clockSkewTolerance(Duration tolerance) {
if (tolerance == null || tolerance.isNegative()) {
throw new OAuth2ClientConfigException("Clock skew tolerance must be positive");
}
this.clockSkewTolerance = tolerance;
return this;
}
/**
* Sets the resolver that determines which scopes to request for each API call.
*/
public OAuth2ConfigBuilder scopeResolver(ScopeResolver resolver) {
this.scopeResolver = resolver;
return this;
}
/**
* Sets the storage mechanism for caching access tokens.
* Default is in-memory storage.
*/
public OAuth2ConfigBuilder accessTokenStore(AccessTokenStore accessTokenStore) {
this.accessTokenStore = accessTokenStore;
return this;
}
/**
* Sets the User-Agent header value for HTTP requests.
* Default uses the library's generated user agent string.
*/
public OAuth2ConfigBuilder userAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
/**
* Sets the private key used for client authentication via private_key_jwt.
*/
public OAuth2ConfigBuilder clientKey(PrivateKey clientKey) {
this.clientKey = clientKey;
return this;
}
/**
* Sets the key identifier for the client authentication key.
*/
public OAuth2ConfigBuilder kid(String kid) {
this.kid = kid.trim();
return this;
}
/**
* Sets the provider for DPoP key pairs used to generate DPoP proof tokens.
*/
public OAuth2ConfigBuilder dpopKeyProvider(DPoPKeyProvider dpopKeyProvider) {
this.dpopKeyProvider = dpopKeyProvider;
return this;
}
/**
* Sets the OAuth 2 security profile to use.
* Default is SecurityProfile.FAPI2SP_PRIVATE_KEY_DPOP.
*/
public OAuth2ConfigBuilder securityProfile(SecurityProfile securityProfile) {
this.securityProfile = securityProfile;
return this;
}
/**
* Builds the OAuth2Config instance.
*/
public OAuth2Config build() {
validate();
return new OAuth2Config(this);
}
/**
* Validates the configuration parameters match the security profile requirements.
* For now, only FAPI 2.0 with private_key_jwt and DPoP is supported.
*/
private void validate() {
if (securityProfile == null) {
throw new OAuth2ClientConfigException("Security profile is required");
}
if (securityProfile != SecurityProfile.FAPI2SP_PRIVATE_KEY_DPOP) {
throw new OAuth2ClientConfigException("Security profile must be FAPI 2.0 with private_key_jwt and DPoP");
}
if (clientId == null || clientId.trim().isEmpty()) {
throw new OAuth2ClientConfigException("Client ID is required");
}
if (tokenEndpoint == null) {
throw new OAuth2ClientConfigException("Token endpoint is required");
}
if (issuer == null) {
throw new OAuth2ClientConfigException("Issuer is required");
}
if (scopeResolver == null) {
throw new OAuth2ClientConfigException("Scope resolver is required");
}
if (accessTokenStore == null) {
throw new OAuth2ClientConfigException("Token store is required");
}
if (userAgent == null) {
throw new OAuth2ClientConfigException("User agent is required");
}
if (clientKey == null) {
throw new OAuth2ClientConfigException("Client private key is required");
}
if (kid == null || kid.trim().isEmpty()) {
throw new OAuth2ClientConfigException("Key ID (kid) is required");
}
if (dpopKeyProvider == null) {
throw new OAuth2ClientConfigException("DPoP key provider is required");
}
validateDPoPKey(dpopKeyProvider.getCurrentKey());
validateKey(clientKey);
}
private void validateDPoPKey(DPoPKey dPoPKey) {
if (dPoPKey == null || dPoPKey.getKeyPair() == null) {
throw new OAuth2ClientConfigException("DPoP key provider must return a valid DPoP key");
}
if (dPoPKey.getKeyId() == null) {
throw new OAuth2ClientConfigException("DPoP key provider must return a valid DPoP key ID");
}
validateKey(dPoPKey.getKeyPair().getPrivate());
validateKey(dPoPKey.getKeyPair().getPublic());
}
/**
* Validates that keys meet security requirements.
* See: <a href="https://openid.bitbucket.io/fapi/fapi-security-profile-2_0.html#name-cryptography-and-secrets">5.4. Cryptography and secrets</a>
*/
private void validateKey(Key key) {
if (!(key instanceof RSAKey) && !(key instanceof ECKey)) {
throw new OAuth2ClientConfigException("Key algorithm must be RSA or EC, but was: " + key.getAlgorithm());
}
if (key instanceof RSAKey rsaKey) {
int keyLength = rsaKey.getModulus().bitLength();
if (keyLength < 2048) {
throw new OAuth2ClientConfigException("RSA keys must have a minimum length of 2048 bits, but key length was: " + keyLength);
}
}
if (key instanceof ECKey ecKey) {
int keyLength = ecKey.getParams().getCurve().getField().getFieldSize();
if (keyLength < 224) {
throw new OAuth2ClientConfigException("Elliptic curve keys must have a minimum length of 224 bits, but key length was: " + keyLength);
}
}
}
}
@Override
public String toString() {
return """
OAuth2Config {
clientId='%s',
tokenEndpoint='%s',
issuer='%s',
clockSkewTolerance='%s',
userAgent='%s',
accessTokenStore='%s',
scopeResolver='%s',
kid='%s',
clientKey='%s',
dpopKeyProvider='%s',
securityProfile='%s'
}""".formatted(
clientId,
tokenEndpoint,
issuer,
clockSkewTolerance,
userAgent,
formatName(accessTokenStore),
formatName(scopeResolver),
kid,
formatPrivateKey(clientKey),
formatName(dpopKeyProvider),
securityProfile
);
}
private static String formatPrivateKey(PrivateKey key) {
return String.format("PrivateKey[class=%s, algorithm=%s, format=%s]", formatName(key), key.getAlgorithm(), key.getFormat());
}
private static String formatName(Object object) {
var simpleName = object.getClass().getSimpleName();
return !simpleName.isBlank() ? simpleName : object.getClass().getName();
}
}