Skip to content

Commit 56b4e14

Browse files
committed
Merge branch '718-username-provider-refresh-token'
2 parents 125508f + 4cf0b47 commit 56b4e14

File tree

7 files changed

+398
-100
lines changed

7 files changed

+398
-100
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2013-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* 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,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.cloudfoundry.reactor.uaa;
18+
19+
import io.jsonwebtoken.Claims;
20+
import io.jsonwebtoken.JwsHeader;
21+
import io.jsonwebtoken.SigningKeyResolver;
22+
import io.jsonwebtoken.impl.Base64Codec;
23+
import org.cloudfoundry.uaa.tokens.ListTokenKeysRequest;
24+
import org.cloudfoundry.uaa.tokens.ListTokenKeysResponse;
25+
import org.cloudfoundry.uaa.tokens.TokenKey;
26+
import org.cloudfoundry.uaa.tokens.Tokens;
27+
import reactor.core.Exceptions;
28+
29+
import java.security.Key;
30+
import java.security.KeyFactory;
31+
import java.security.NoSuchAlgorithmException;
32+
import java.security.spec.InvalidKeySpecException;
33+
import java.security.spec.X509EncodedKeySpec;
34+
import java.time.Duration;
35+
import java.util.HashMap;
36+
import java.util.Map;
37+
38+
final class UaaSigningKeyResolver implements SigningKeyResolver {
39+
40+
private static final Base64Codec BASE64 = new Base64Codec();
41+
42+
private static final String BEGIN = "-----BEGIN PUBLIC KEY-----";
43+
44+
private static final String END = "-----END PUBLIC KEY-----";
45+
46+
private final Object monitor = new Object();
47+
48+
private final Map<String, Key> signingKeys = new HashMap<>();
49+
50+
private final Tokens tokens;
51+
52+
UaaSigningKeyResolver(Tokens tokens) {
53+
this.tokens = tokens;
54+
}
55+
56+
@Override
57+
@SuppressWarnings("rawtypes")
58+
public Key resolveSigningKey(JwsHeader header, Claims claims) {
59+
return getKey(header.getKeyId());
60+
}
61+
62+
@Override
63+
@SuppressWarnings("rawtypes")
64+
public Key resolveSigningKey(JwsHeader header, String plaintext) {
65+
return getKey(header.getKeyId());
66+
}
67+
68+
private static byte[] decode(TokenKey tokenKey) {
69+
return BASE64.decode(tokenKey.getValue().replace(BEGIN, "").replace(END, "").trim());
70+
}
71+
72+
private static Key generateKey(TokenKey tokenKey) {
73+
try {
74+
return KeyFactory
75+
.getInstance(tokenKey.getKeyType().toString())
76+
.generatePublic(new X509EncodedKeySpec(decode(tokenKey)));
77+
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
78+
throw Exceptions.propagate(e);
79+
}
80+
}
81+
82+
private Key getKey(String keyId) {
83+
synchronized (this.monitor) {
84+
Key key = this.signingKeys.get(keyId);
85+
if (key != null) {
86+
return key;
87+
}
88+
89+
refreshKeys();
90+
91+
key = this.signingKeys.get(keyId);
92+
if (key != null) {
93+
return key;
94+
}
95+
96+
throw new IllegalStateException(String.format("Unable to retrieve signing key %s", keyId));
97+
}
98+
}
99+
100+
private void refreshKeys() {
101+
this.signingKeys.clear();
102+
this.signingKeys.putAll(this.tokens
103+
.listKeys(ListTokenKeysRequest.builder()
104+
.build())
105+
.flatMapIterable(ListTokenKeysResponse::getKeys)
106+
.collectMap(TokenKey::getId, UaaSigningKeyResolver::generateKey)
107+
.block(Duration.ofMinutes(5)));
108+
}
109+
110+
}

cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/uaa/UsernameProvider.java

Lines changed: 27 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -17,81 +17,62 @@
1717
package org.cloudfoundry.reactor.uaa;
1818

1919
import io.jsonwebtoken.Claims;
20+
import io.jsonwebtoken.ExpiredJwtException;
2021
import io.jsonwebtoken.Jws;
2122
import io.jsonwebtoken.Jwts;
22-
import io.jsonwebtoken.impl.Base64Codec;
23+
import io.jsonwebtoken.SigningKeyResolver;
2324
import org.cloudfoundry.reactor.ConnectionContext;
2425
import org.cloudfoundry.reactor.TokenProvider;
25-
import org.cloudfoundry.uaa.tokens.GetTokenKeyRequest;
26-
import org.cloudfoundry.uaa.tokens.GetTokenKeyResponse;
2726
import org.cloudfoundry.uaa.tokens.Tokens;
2827
import reactor.core.publisher.Mono;
2928

30-
import java.security.GeneralSecurityException;
31-
import java.security.KeyFactory;
32-
import java.security.PublicKey;
33-
import java.security.spec.X509EncodedKeySpec;
3429
import java.util.Optional;
3530

36-
import static org.cloudfoundry.util.tuple.TupleUtils.function;
37-
3831
final class UsernameProvider {
3932

40-
private static final Base64Codec BASE64 = new Base64Codec();
41-
42-
private static final String BEGIN = "-----BEGIN PUBLIC KEY-----";
43-
44-
private static final String END = "-----END PUBLIC KEY-----";
45-
4633
private final ConnectionContext connectionContext;
4734

48-
private final TokenProvider tokenProvider;
35+
private final SigningKeyResolver signingKeyResolver;
4936

50-
private final Tokens tokens;
37+
private final TokenProvider tokenProvider;
5138

5239
UsernameProvider(ConnectionContext connectionContext, TokenProvider tokenProvider, Tokens tokens) {
40+
this(connectionContext, new UaaSigningKeyResolver(tokens), tokenProvider);
41+
}
42+
43+
UsernameProvider(ConnectionContext connectionContext, SigningKeyResolver signingKeyResolver, TokenProvider tokenProvider) {
5344
this.connectionContext = connectionContext;
5445
this.tokenProvider = tokenProvider;
55-
this.tokens = tokens;
46+
this.signingKeyResolver = signingKeyResolver;
5647
}
5748

5849
Mono<String> get() {
59-
return Mono
60-
.when(
61-
getSigningKey(this.tokens),
62-
this.tokenProvider.getToken(this.connectionContext)
63-
.map(s -> s.split(" ")[1]))
64-
.map(function(UsernameProvider::getUsername));
50+
return getToken(this.connectionContext, this.tokenProvider)
51+
.map(this::getUsername)
52+
.retry(1, t -> {
53+
if (t instanceof ExpiredJwtException) {
54+
this.tokenProvider.invalidate(this.connectionContext);
55+
return true;
56+
}
57+
58+
return false;
59+
});
6560
}
6661

67-
private static PublicKey generateKey(String pem) {
68-
try {
69-
return KeyFactory
70-
.getInstance("RSA")
71-
.generatePublic(new X509EncodedKeySpec(BASE64.decode(pem)));
72-
} catch (GeneralSecurityException e) {
73-
throw new RuntimeException(e);
74-
}
62+
private static Mono<String> getToken(ConnectionContext connectionContext, TokenProvider tokenProvider) {
63+
return Mono.defer(() -> tokenProvider
64+
.getToken(connectionContext))
65+
.map(s -> s.split(" ")[1]);
7566
}
7667

77-
private static Mono<PublicKey> getSigningKey(Tokens tokens) {
78-
return requestTokenKey(tokens)
79-
.map(GetTokenKeyResponse::getValue)
80-
.map(pem -> pem.replace(BEGIN, "").replace(END, "").trim())
81-
.map(UsernameProvider::generateKey);
82-
}
68+
private String getUsername(String token) {
69+
Jws<Claims> jws = Jwts.parser()
70+
.setSigningKeyResolver(this.signingKeyResolver)
71+
.parseClaimsJws(token);
8372

84-
private static String getUsername(PublicKey publicKey, String token) {
85-
Jws<Claims> jws = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
8673
return Optional
8774
.ofNullable(jws.getBody().get("user_name", String.class))
8875
.orElseThrow(() -> new IllegalStateException("Unable to retrieve username from token"));
8976
}
9077

91-
private static Mono<GetTokenKeyResponse> requestTokenKey(Tokens tokens) {
92-
return tokens
93-
.getKey(GetTokenKeyRequest.builder()
94-
.build());
95-
}
96-
9778
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2013-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* 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,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.cloudfoundry.reactor.uaa;
18+
19+
import org.cloudfoundry.reactor.DefaultConnectionContext;
20+
import org.cloudfoundry.reactor.tokenprovider.PasswordGrantTokenProvider;
21+
import org.cloudfoundry.uaa.UaaClient;
22+
import org.cloudfoundry.util.DelayUtils;
23+
24+
import java.time.Duration;
25+
import java.util.concurrent.CountDownLatch;
26+
27+
public final class TestUsernameProviderRefresh {
28+
29+
public static void main(String[] args) throws InterruptedException {
30+
DefaultConnectionContext connectionContext = DefaultConnectionContext.builder()
31+
.apiHost("api.run.pivotal.io")
32+
.build();
33+
34+
PasswordGrantTokenProvider tokenProvider = PasswordGrantTokenProvider.builder()
35+
.password("rNgdFPs{NgYYVW{PXCAJMqGuokEtVpBgRDu2zjQgJ7nNNFnokA")
36+
.username("bhale@pivotal.io")
37+
.build();
38+
39+
UaaClient uaaClient = ReactorUaaClient.builder()
40+
.connectionContext(connectionContext)
41+
.tokenProvider(tokenProvider)
42+
.build();
43+
44+
CountDownLatch latch = new CountDownLatch(1);
45+
46+
uaaClient.getUsername()
47+
.doOnNext(System.out::println)
48+
.filter(s -> false)
49+
.repeatWhenEmpty(DelayUtils.fixed(Duration.ofSeconds(30)))
50+
.subscribe(System.out::println, t -> {
51+
t.printStackTrace();
52+
latch.countDown();
53+
}, latch::countDown);
54+
55+
latch.await();
56+
}
57+
58+
}

0 commit comments

Comments
 (0)