Skip to content

Commit 4f09792

Browse files
committed
solution: authenticated requests
1 parent eb80329 commit 4f09792

17 files changed

Lines changed: 1226 additions & 2 deletions

emerald-api/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ dependencies {
4747
implementation 'io.emeraldpay.etherjar:etherjar-domain:0.11.0'
4848

4949
testImplementation "org.codehaus.groovy:groovy:${groovyVersion}"
50+
testImplementation 'net.bytebuddy:byte-buddy:1.14.17'
5051
testImplementation "org.spockframework:spock-core:${spockVersion}"
5152
testImplementation "io.grpc:grpc-testing:${grpcVersion}"
5253
testImplementation "com.salesforce.servicelibs:reactor-grpc-test:${reactiveGrpcVersion}"

emerald-api/src/main/java/io/emeraldpay/api/EmeraldConnection.java

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
package io.emeraldpay.api;
22

3+
import io.emeraldpay.api.proto.AuthGrpc;
4+
import io.emeraldpay.impl.AuthHolder;
5+
import io.emeraldpay.impl.AuthInterceptor;
6+
import io.emeraldpay.impl.TokenCredentials;
37
import io.grpc.Channel;
8+
import io.grpc.ClientInterceptor;
49
import io.grpc.ManagedChannelBuilder;
510
import io.grpc.netty.NettyChannelBuilder;
611

712
import java.net.InetAddress;
13+
import java.net.URI;
814
import java.util.function.Function;
915

1016
/**
@@ -13,9 +19,16 @@
1319
public class EmeraldConnection {
1420

1521
private final Channel channel;
22+
private final ClientInterceptor credentials;
1623

1724
public EmeraldConnection(Channel channel) {
1825
this.channel = channel;
26+
this.credentials = null;
27+
}
28+
29+
public EmeraldConnection(Channel channel, ClientInterceptor credentials) {
30+
this.channel = channel;
31+
this.credentials = credentials;
1932
}
2033

2134
/**
@@ -38,6 +51,22 @@ public Channel getChannel() {
3851
return channel;
3952
}
4053

54+
/**
55+
* Credentials used to authenticate on Emerald API calls
56+
* @return credentials
57+
*/
58+
public ClientInterceptor getCredentials() {
59+
return credentials;
60+
}
61+
62+
/**
63+
* Check if the connection has credentials
64+
* @return true if credentials are set
65+
*/
66+
public boolean hasCredentials() {
67+
return credentials != null;
68+
}
69+
4170
public static class Builder {
4271
private String host;
4372
private Integer port;
@@ -51,6 +80,8 @@ public static class Builder {
5180

5281
private Function<NettyChannelBuilder, ManagedChannelBuilder<?>> customChannel = null;
5382

83+
private String secretToken;
84+
5485
/**
5586
* Set target address as a host and port pair
5687
*
@@ -80,6 +111,18 @@ public Builder connectTo(String host) {
80111
return this;
81112
}
82113

114+
public Builder connectTo(URI url) {
115+
boolean isSecure = "https".equals(url.getScheme());
116+
if (isSecure) {
117+
this.usePlaintext = false;
118+
}
119+
int port = url.getPort();
120+
if (port == -1) {
121+
port = isSecure ? 443 : 80;
122+
}
123+
return this.connectTo(url.getHost(), port);
124+
}
125+
83126
public Builder connectTo(InetAddress host, int port) {
84127
this.port = port;
85128
return this.connectTo(host);
@@ -111,6 +154,17 @@ public Builder maxMessageSize(Integer value) {
111154
return this;
112155
}
113156

157+
/**
158+
* Authenticate on Emerald API using the provided secret token
159+
*
160+
* @param secret a token like `emrld_y40SYbbZclSZPX4r6nL9hNKUGaknAwqyv2qslI`
161+
* @return builder
162+
*/
163+
public Builder withAuthToken(String secret) {
164+
this.secretToken = secret;
165+
return this;
166+
}
167+
114168
/**
115169
* Customize Channel Builder by applying any custom options not covered by this Builder
116170
*
@@ -162,7 +216,18 @@ public EmeraldConnection build() {
162216
channelBuilder.defaultLoadBalancingPolicy("round_robin");
163217
}
164218

165-
return new EmeraldConnection(channelBuilder.build());
219+
Channel channel = channelBuilder.build();
220+
221+
AuthInterceptor authInterceptor = null;
222+
if (secretToken != null) {
223+
if (usePlaintext) {
224+
System.err.println("WARNING: Authentication with a secret token over an unsecure plaintext connection.");
225+
}
226+
AuthHolder holder = new AuthHolder(new TokenCredentials(secretToken, AuthGrpc.newBlockingStub(channel)));
227+
authInterceptor = new AuthInterceptor(holder);
228+
}
229+
230+
return new EmeraldConnection(channel, authInterceptor);
166231
}
167232

168233

emerald-api/src/main/java/io/emeraldpay/api/blockchain/BlockchainApi.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ public class BlockchainApi {
1010

1111
public BlockchainApi(EmeraldConnection connection) {
1212
this.connection = connection;
13-
this.blockchainStub = ReactorBlockchainGrpc.newReactorStub(connection.getChannel());
13+
ReactorBlockchainGrpc.ReactorBlockchainStub stub = ReactorBlockchainGrpc.newReactorStub(connection.getChannel());
14+
if (connection.hasCredentials()) {
15+
stub = stub.withInterceptors(connection.getCredentials());
16+
}
17+
this.blockchainStub = stub;
1418
}
1519

1620
/**
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package io.emeraldpay.impl;
2+
3+
import java.time.Instant;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
import java.util.concurrent.locks.ReentrantReadWriteLock;
7+
import java.util.function.Consumer;
8+
9+
/**
10+
* Holds the authentication handler
11+
*/
12+
public class AuthHolder {
13+
14+
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
15+
private final List<Consumer<MetadataHandler>> authQueue = new ArrayList<>();
16+
17+
private MetadataHandler auth;
18+
private Instant requestedAt;
19+
20+
/**
21+
* Create an instance of the handler.
22+
*
23+
* @param auth current auth, can be null
24+
*/
25+
public AuthHolder(MetadataHandler auth) {
26+
this.auth = auth;
27+
}
28+
29+
/**
30+
* Wait for the authentication to be set and ready.
31+
* If it's already set and ready, the listener will be called immediately.
32+
*
33+
* @param listener a listener to be called when the authentication is ready
34+
*/
35+
public void awaitAuth(Consumer<MetadataHandler> listener) {
36+
lock.writeLock().lock();
37+
try {
38+
if (auth != null && auth.isReady()) {
39+
listener.accept(auth);
40+
} else {
41+
authQueue.add(listener);
42+
}
43+
} finally {
44+
lock.writeLock().unlock();
45+
}
46+
}
47+
48+
/**
49+
* Get current auth handler, if it's set and ready. If it's set but is not ready, it will automatically request a refresh.
50+
* If returns `null` subscribe for a callback with `awaitAuth`
51+
*
52+
* @return current auth handler, or null if it's not ready
53+
* @see #awaitAuth(Consumer)
54+
*/
55+
public MetadataHandler getAuth() {
56+
lock.readLock().lock();
57+
try {
58+
MetadataHandler auth = this.auth;
59+
if (auth != null && auth.isReady()) {
60+
return auth;
61+
}
62+
} finally {
63+
lock.readLock().unlock();
64+
}
65+
lock.writeLock().lock();
66+
try {
67+
// simultaneous calls may trigger multiple refreshes, so check if it's already requested
68+
if (auth == null || requestedAt == null || requestedAt.isBefore(Instant.now().minusSeconds(60))) {
69+
requestedAt = Instant.now();
70+
auth.request(this);
71+
}
72+
} finally {
73+
lock.writeLock().unlock();
74+
}
75+
return null;
76+
}
77+
78+
/**
79+
* Set the authentication handler and fire all listeners
80+
*
81+
* @param auth new auth handler
82+
*/
83+
public void setAuth(MetadataHandler auth) {
84+
lock.writeLock().lock();
85+
try {
86+
this.auth = auth;
87+
if (auth.isReady()) {
88+
for (Consumer<MetadataHandler> listener : authQueue) {
89+
listener.accept(auth);
90+
}
91+
authQueue.clear();
92+
}
93+
} finally {
94+
lock.writeLock().unlock();
95+
}
96+
}
97+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package io.emeraldpay.impl;
2+
3+
import io.grpc.*;
4+
5+
public class AuthInterceptor implements ClientInterceptor {
6+
7+
private final AuthHolder authHolder;
8+
9+
public AuthInterceptor(AuthHolder authHolder) {
10+
this.authHolder = authHolder;
11+
}
12+
13+
@Override
14+
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
15+
MetadataHandler auth = authHolder.getAuth();
16+
if (auth != null) {
17+
// if auth is ready, just use it as is
18+
return new AuthenticatedClientCall<>(next.newCall(method, callOptions), auth);
19+
} else {
20+
// has to wait for auth to be ready
21+
DeferredClientCall<ReqT, RespT> deferred = new DeferredClientCall<>();
22+
authHolder.awaitAuth(metadataConsumer -> {
23+
AuthenticatedClientCall<ReqT, RespT> authenticated = new AuthenticatedClientCall<>(next.newCall(method, callOptions), metadataConsumer);
24+
deferred.accept(authenticated);
25+
});
26+
return deferred;
27+
}
28+
}
29+
30+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.emeraldpay.impl;
2+
3+
import io.grpc.ClientCall;
4+
import io.grpc.Metadata;
5+
6+
import java.util.function.Consumer;
7+
8+
/**
9+
* A wrapper around a ClientCall that adds authentication headers from provided MetadataHandler.
10+
* Used by an interceptor when the client is already authenticated. Otherwise, see `DeferredClientCall`
11+
*
12+
* @see DeferredClientCall
13+
*/
14+
public class AuthenticatedClientCall<ReqT, RespT> extends ClientCall<ReqT, RespT> {
15+
16+
private final ClientCall<ReqT, RespT> delegate;
17+
private MetadataHandler auth;
18+
19+
public AuthenticatedClientCall(ClientCall<ReqT, RespT> delegate, MetadataHandler auth) {
20+
this.delegate = delegate;
21+
this.auth = auth;
22+
}
23+
24+
public void setAuth(MetadataHandler auth) {
25+
this.auth = auth;
26+
}
27+
28+
@Override
29+
public void start(Listener<RespT> responseListener, Metadata headers) {
30+
auth.accept(headers);
31+
delegate.start(responseListener, headers);
32+
}
33+
34+
@Override
35+
public void request(int numMessages) {
36+
delegate.request(numMessages);
37+
}
38+
39+
@Override
40+
public void cancel(String message, Throwable cause) {
41+
delegate.cancel(message, cause);
42+
}
43+
44+
@Override
45+
public void halfClose() {
46+
delegate.halfClose();
47+
}
48+
49+
@Override
50+
public void sendMessage(ReqT message) {
51+
delegate.sendMessage(message);
52+
}
53+
}

0 commit comments

Comments
 (0)