Skip to content

Commit 4cf0b47

Browse files
committed
Refresh Token when Getting Username
Previously, when the username was extracted from the current token, if it had expired at some point that was propagated as a failure. This change updates the code such that if there is token expiration failure, the token is refreshed and the username is extracted again. If this does not work, an error is propagated. While working on the UsernameProvider, I also updated how signing keys were acquired. The new implementation deals with signing key rotation by attempting to use a cached copy of the keys where possible, but when a key is requested that does not exist, it refreshes the collection of signing keys and returns the newly matching one. If this refresh doesn't find the specified signing key, an error is propagated. [resolves #718]
1 parent 125508f commit 4cf0b47

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)