Skip to content

Commit 1f83505

Browse files
committed
Added OAuth2 auth provider module
1 parent 4802564 commit 1f83505

File tree

5 files changed

+664
-0
lines changed

5 files changed

+664
-0
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?xml version="1.0"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
5+
<parent>
6+
<groupId>tech.ydb</groupId>
7+
<artifactId>ydb-sdk-parent</artifactId>
8+
<version>2.2.3-SNAPSHOT</version>
9+
<relativePath>../../pom.xml</relativePath>
10+
</parent>
11+
12+
<groupId>tech.ydb.auth</groupId>
13+
<artifactId>ydb-oauth2-provider</artifactId>
14+
15+
<name>OAuth2 Authentication provider</name>
16+
<description>Provider for OAuth2 Token Exchange authentication</description>
17+
18+
<dependencies>
19+
<dependency>
20+
<groupId>tech.ydb</groupId>
21+
<artifactId>ydb-sdk-core</artifactId>
22+
</dependency>
23+
24+
<dependency>
25+
<groupId>org.apache.httpcomponents</groupId>
26+
<artifactId>httpclient</artifactId>
27+
<version>4.5.14</version>
28+
</dependency>
29+
30+
<dependency>
31+
<groupId>junit</groupId>
32+
<artifactId>junit</artifactId>
33+
<scope>test</scope>
34+
</dependency>
35+
36+
<dependency>
37+
<groupId>org.mockito</groupId>
38+
<artifactId>mockito-inline</artifactId>
39+
<scope>test</scope>
40+
</dependency>
41+
42+
<dependency>
43+
<groupId>org.mock-server</groupId>
44+
<artifactId>mockserver-netty-no-dependencies</artifactId>
45+
<version>5.15.0</version>
46+
<scope>test</scope>
47+
</dependency>
48+
49+
<dependency>
50+
<groupId>org.apache.logging.log4j</groupId>
51+
<artifactId>log4j-slf4j-impl</artifactId>
52+
<scope>test</scope>
53+
</dependency>
54+
</dependencies>
55+
</project>
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
package tech.ydb.auth;
2+
3+
import java.io.IOException;
4+
import java.io.InputStreamReader;
5+
import java.io.Reader;
6+
import java.io.UnsupportedEncodingException;
7+
import java.time.Clock;
8+
import java.time.Instant;
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
import java.util.concurrent.CompletableFuture;
12+
import java.util.concurrent.ExecutorService;
13+
14+
import com.google.common.annotations.VisibleForTesting;
15+
import com.google.gson.Gson;
16+
import com.google.gson.annotations.SerializedName;
17+
import org.apache.http.HttpEntity;
18+
import org.apache.http.NameValuePair;
19+
import org.apache.http.client.entity.UrlEncodedFormEntity;
20+
import org.apache.http.client.methods.CloseableHttpResponse;
21+
import org.apache.http.client.methods.HttpPost;
22+
import org.apache.http.impl.client.CloseableHttpClient;
23+
import org.apache.http.impl.client.HttpClients;
24+
import org.apache.http.message.BasicNameValuePair;
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
28+
import tech.ydb.core.Status;
29+
import tech.ydb.core.StatusCode;
30+
import tech.ydb.core.UnexpectedResultException;
31+
import tech.ydb.core.auth.BackgroundIdentity;
32+
import tech.ydb.core.grpc.GrpcTransport;
33+
34+
/**
35+
*
36+
* @author Aleksandr Gorshenin
37+
*/
38+
public class OAuth2TokenExchangeProvider implements AuthRpcProvider<GrpcTransport> {
39+
public static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
40+
41+
public static final String ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token";
42+
public static final String JWT_TOKEN = "urn:ietf:params:oauth:token-type:jwt";
43+
44+
public static final String REFRESH_TOKEN = "urn:ietf:params:oauth:token-type:refresh_token";
45+
public static final String ID_TOKEN = "urn:ietf:params:oauth:token-type:id_token";
46+
public static final String SAML1_TOKEN = "urn:ietf:params:oauth:token-type:saml1";
47+
public static final String SAML2_TOKEN = "urn:ietf:params:oauth:token-type:saml2";
48+
49+
private static final Logger logger = LoggerFactory.getLogger(OAuth2TokenExchangeProvider.class);
50+
private static final Gson GSON = new Gson();
51+
52+
private final Clock clock;
53+
private final String endpoint;
54+
private final HttpEntity updateTokenForm;
55+
private final int timeoutSeconds;
56+
57+
private OAuth2TokenExchangeProvider(Clock clock, String endpoint, HttpEntity entity, int timeoutSeconds) {
58+
this.clock = clock;
59+
this.endpoint = endpoint;
60+
this.updateTokenForm = entity;
61+
this.timeoutSeconds = timeoutSeconds;
62+
}
63+
64+
@Override
65+
public AuthIdentity createAuthIdentity(GrpcTransport rpc) {
66+
return new BackgroundIdentity(clock, new OAuth2Rpc(rpc.getScheduler()));
67+
}
68+
69+
private class OAuth2Rpc implements BackgroundIdentity.Rpc {
70+
private final ExecutorService executor;
71+
private final CloseableHttpClient client;
72+
73+
OAuth2Rpc(ExecutorService executor) {
74+
this.executor = executor;
75+
this.client = HttpClients.createDefault();
76+
}
77+
78+
@Override
79+
public void close() {
80+
try {
81+
this.client.close();
82+
} catch (IOException e) {
83+
logger.error("io exception on closing of http client", e);
84+
}
85+
}
86+
87+
private Token updateToken() throws IOException {
88+
HttpPost post = new HttpPost(endpoint);
89+
post.setEntity(updateTokenForm);
90+
CloseableHttpResponse response = client.execute(post);
91+
92+
int httpCode = response.getStatusLine().getStatusCode();
93+
if (httpCode != 200) {
94+
StatusCode code;
95+
switch (httpCode) {
96+
case 400:
97+
code = StatusCode.BAD_REQUEST;
98+
break;
99+
case 401:
100+
case 403:
101+
code = StatusCode.UNAUTHORIZED;
102+
break;
103+
case 404:
104+
code = StatusCode.NOT_FOUND;
105+
break;
106+
case 500:
107+
code = StatusCode.INTERNAL_ERROR;
108+
break;
109+
case 503:
110+
case 504:
111+
code = StatusCode.UNAVAILABLE;
112+
break;
113+
default:
114+
code = StatusCode.CLIENT_INTERNAL_ERROR;
115+
break;
116+
}
117+
String message = "Cannot get OAuth2 token with code " + httpCode
118+
+ "[" + response.getStatusLine().getReasonPhrase() + "]";
119+
throw new UnexpectedResultException(message, Status.of(code));
120+
}
121+
122+
try (Reader reader = new InputStreamReader(response.getEntity().getContent())) {
123+
OAuth2Response json = GSON.fromJson(reader, OAuth2Response.class);
124+
125+
if (!"Bearer".equals(json.getTokenType())) {
126+
throw new UnexpectedResultException(
127+
"OAuth2 token exchange: unsupported token type: " + json.getTokenType(),
128+
Status.of(StatusCode.INTERNAL_ERROR)
129+
);
130+
}
131+
if (json.getExpiredIn() == null || json.getExpiredIn() <= 0) {
132+
throw new UnexpectedResultException(
133+
"OAuth2 token exchange: incorrect expiration time: " + json.getExpiredIn(),
134+
Status.of(StatusCode.INTERNAL_ERROR)
135+
);
136+
}
137+
138+
String token = json.getTokenType() + " " + json.getAccessToken();
139+
Instant expireAt = clock.instant().plusSeconds(json.getExpiredIn());
140+
Instant updateAt = clock.instant().plusSeconds(json.getExpiredIn() / 2);
141+
return new Token(token, expireAt, updateAt);
142+
} finally {
143+
response.close();
144+
}
145+
}
146+
147+
@Override
148+
public CompletableFuture<Token> getTokenAsync() {
149+
final CompletableFuture<Token> future = new CompletableFuture<>();
150+
executor.submit(() -> {
151+
try {
152+
future.complete(updateToken());
153+
} catch (IOException | RuntimeException ex) {
154+
future.completeExceptionally(ex);
155+
}
156+
});
157+
return future;
158+
}
159+
160+
@Override
161+
public int getTimeoutSeconds() {
162+
return timeoutSeconds;
163+
}
164+
}
165+
166+
public static Builder newBuilder(String endpoint, String jwtToken) {
167+
return new Builder(endpoint, jwtToken, JWT_TOKEN);
168+
}
169+
170+
public static Builder newBuilder(String endpoint, String token, String type) {
171+
return new Builder(endpoint, token, type);
172+
}
173+
174+
public static class Builder {
175+
private final String endpoint;
176+
private final String subjectToken;
177+
private final String subjectType;
178+
179+
private Clock clock = Clock.systemUTC();
180+
private int timeoutSeconds = 60;
181+
182+
private String actorToken = null;
183+
private String actorType = null;
184+
185+
private String scope = null;
186+
private String resource = null;
187+
private String audience = null;
188+
189+
private String grantType = GRANT_TYPE;
190+
private String requestedTokenType = ACCESS_TOKEN;
191+
192+
private Builder(String endpoint, String subjectToken, String subjectType) {
193+
this.endpoint = endpoint;
194+
this.subjectToken = subjectToken;
195+
this.subjectType = subjectType;
196+
}
197+
198+
@VisibleForTesting
199+
Builder withClock(Clock clock) {
200+
this.clock = clock;
201+
return this;
202+
}
203+
204+
public Builder withActor(String token, String type) {
205+
this.actorToken = token;
206+
this.actorType = type;
207+
return this;
208+
}
209+
210+
public Builder withScope(String scope) {
211+
this.scope = scope;
212+
return this;
213+
}
214+
215+
public Builder withResource(String resource) {
216+
this.resource = resource;
217+
return this;
218+
}
219+
220+
public Builder withAudience(String audience) {
221+
this.audience = audience;
222+
return this;
223+
}
224+
225+
public Builder withTimeoutSeconds(int timeoutSeconds) {
226+
this.timeoutSeconds = timeoutSeconds;
227+
return this;
228+
}
229+
230+
public Builder withCustomGrantType(String grantType) {
231+
this.grantType = grantType;
232+
return this;
233+
}
234+
235+
public Builder withCustomRequestedTokenType(String tokenType) {
236+
this.requestedTokenType = tokenType;
237+
return this;
238+
}
239+
240+
public OAuth2TokenExchangeProvider build() {
241+
return new OAuth2TokenExchangeProvider(clock, endpoint, buildUpdateHttpForm(), timeoutSeconds);
242+
}
243+
244+
private HttpEntity buildUpdateHttpForm() {
245+
List<NameValuePair> params = new ArrayList<>();
246+
247+
// Required parameters
248+
params.add(new BasicNameValuePair("grand_type", grantType));
249+
params.add(new BasicNameValuePair("requested_token_type", requestedTokenType));
250+
251+
params.add(new BasicNameValuePair("subject_token", subjectToken));
252+
params.add(new BasicNameValuePair("subject_token_type", subjectType));
253+
254+
// Optional parameters
255+
if (resource != null) {
256+
params.add(new BasicNameValuePair("resource", resource));
257+
}
258+
if (audience != null) {
259+
params.add(new BasicNameValuePair("audience", audience));
260+
}
261+
if (scope != null) {
262+
params.add(new BasicNameValuePair("scope", scope));
263+
}
264+
if (actorToken != null) {
265+
params.add(new BasicNameValuePair("actor_token", actorToken));
266+
}
267+
if (actorType != null) {
268+
params.add(new BasicNameValuePair("actor_token_type", actorType));
269+
}
270+
271+
try {
272+
return new UrlEncodedFormEntity(params);
273+
} catch (UnsupportedEncodingException ex) {
274+
throw new RuntimeException("Cannot build OAuth2 http form", ex);
275+
}
276+
}
277+
}
278+
279+
private static class OAuth2Response {
280+
@SerializedName("access_token")
281+
private String accessToken;
282+
@SerializedName("issued_token_type")
283+
private String issuedTokenType;
284+
@SerializedName("token_type")
285+
private String tokenType;
286+
@SerializedName("expires_in")
287+
private Long expiredIn;
288+
@SerializedName("scope")
289+
private String scope;
290+
@SerializedName("refresh_token")
291+
private String refreshToken;
292+
293+
public String getAccessToken() {
294+
return this.accessToken;
295+
}
296+
297+
public String getIssuedTokenType() {
298+
return this.issuedTokenType;
299+
}
300+
301+
public String getTokenType() {
302+
return this.tokenType;
303+
}
304+
305+
public Long getExpiredIn() {
306+
return this.expiredIn;
307+
}
308+
309+
public String getScope() {
310+
return this.scope;
311+
}
312+
313+
public String getRefreshToken() {
314+
return this.refreshToken;
315+
}
316+
}
317+
}

0 commit comments

Comments
 (0)