View Javadoc
1   /*
2    * Cloud Foundry 2012.02.03 Beta
3    * Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved.
4    *
5    * This product is licensed to you under the Apache License, Version 2.0 (the "License").
6    * You may not use this product except in compliance with the License.
7    *
8    * This product includes a number of subcomponents with
9    * separate copyright notices and license terms. Your use of these
10   * subcomponents is subject to the terms and conditions of the
11   * subcomponent's license, as noted in the LICENSE file.
12   */
13  package org.springframework.security.oauth2.provider.token.store;
14  
15  import org.apache.commons.logging.Log;
16  import org.apache.commons.logging.LogFactory;
17  import org.springframework.beans.factory.InitializingBean;
18  import org.springframework.security.crypto.codec.Base64;
19  import org.springframework.security.jwt.Jwt;
20  import org.springframework.security.jwt.JwtHelper;
21  import org.springframework.security.jwt.crypto.sign.*;
22  import org.springframework.security.oauth2.common.*;
23  import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
24  import org.springframework.security.oauth2.common.util.JsonParser;
25  import org.springframework.security.oauth2.common.util.JsonParserFactory;
26  import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
27  import org.springframework.security.oauth2.provider.OAuth2Authentication;
28  import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
29  import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
30  import org.springframework.security.oauth2.provider.token.TokenEnhancer;
31  import org.springframework.util.Assert;
32  
33  import java.security.KeyPair;
34  import java.security.PrivateKey;
35  import java.security.interfaces.RSAPrivateKey;
36  import java.security.interfaces.RSAPublicKey;
37  import java.util.Date;
38  import java.util.LinkedHashMap;
39  import java.util.Map;
40  
41  /**
42   * Helper that translates between JWT encoded token values and OAuth authentication
43   * information (in both directions). Also acts as a {@link TokenEnhancer} when tokens are
44   * granted.
45   *
46   * @see TokenEnhancer
47   * @see AccessTokenConverter
48   *
49   * @author Dave Syer
50   * @author Luke Taylor
51   */
52  public class JwtAccessTokenConverter implements TokenEnhancer, AccessTokenConverter, InitializingBean {
53  
54  	/**
55  	 * Field name for token id.
56  	 */
57  	public static final String TOKEN_ID = AccessTokenConverter.JTI;
58  
59  	/**
60  	 * Field name for access token id.
61  	 */
62  	public static final String ACCESS_TOKEN_ID = AccessTokenConverter.ATI;
63  
64  	private static final Log logger = LogFactory.getLog(JwtAccessTokenConverter.class);
65  
66  	private AccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
67  
68  	private JwtClaimsSetVerifier jwtClaimsSetVerifier = new NoOpJwtClaimsSetVerifier();
69  
70  	private JsonParser objectMapper = JsonParserFactory.create();
71  
72  	private String verifierKey = new RandomValueStringGenerator().generate();
73  
74  	private Signer signer = new MacSigner(verifierKey);
75  
76  	private String signingKey = verifierKey;
77  
78  	private SignatureVerifier verifier;
79  
80  	/**
81  	 * @param tokenConverter the tokenConverter to set
82  	 */
83  	public void setAccessTokenConverter(AccessTokenConverter tokenConverter) {
84  		this.tokenConverter = tokenConverter;
85  	}
86  
87  	/**
88  	 * @return the tokenConverter in use
89  	 */
90  	public AccessTokenConverter getAccessTokenConverter() {
91  		return tokenConverter;
92  	}
93  
94  	/**
95  	 * @return the {@link JwtClaimsSetVerifier} used to verify the claim(s) in the JWT Claims Set
96  	 */
97  	public JwtClaimsSetVerifier getJwtClaimsSetVerifier() {
98  		return this.jwtClaimsSetVerifier;
99  	}
100 
101 	/**
102 	 * @param jwtClaimsSetVerifier the {@link JwtClaimsSetVerifier} used to verify the claim(s) in the JWT Claims Set
103 	 */
104 	public void setJwtClaimsSetVerifier(JwtClaimsSetVerifier jwtClaimsSetVerifier) {
105 		Assert.notNull(jwtClaimsSetVerifier, "jwtClaimsSetVerifier cannot be null");
106 		this.jwtClaimsSetVerifier = jwtClaimsSetVerifier;
107 	}
108 
109 	@Override
110 	public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
111 		return tokenConverter.convertAccessToken(token, authentication);
112 	}
113 
114 	@Override
115 	public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) {
116 		return tokenConverter.extractAccessToken(value, map);
117 	}
118 
119 	@Override
120 	public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
121 		return tokenConverter.extractAuthentication(map);
122 	}
123 
124 	/**
125 	 * Unconditionally set the verifier (the verifer key is then ignored).
126 	 *
127 	 * @param verifier the value to use
128 	 */
129 	public void setVerifier(SignatureVerifier verifier) {
130 		this.verifier = verifier;
131 	}
132 
133 	/**
134 	 * Unconditionally set the signer to use (if needed). The signer key is then ignored.
135 	 *
136 	 * @param signer the value to use
137 	 */
138 	public void setSigner(Signer signer) {
139 		this.signer = signer;
140 	}
141 
142 	/**
143 	 * Get the verification key for the token signatures.
144 	 *
145 	 * @return the key used to verify tokens
146 	 */
147 	public Map<String, String> getKey() {
148 		Map<String, String> result = new LinkedHashMap<String, String>();
149 		result.put("alg", signer.algorithm());
150 		result.put("value", verifierKey);
151 		return result;
152 	}
153 
154 	public void setKeyPair(KeyPair keyPair) {
155 		PrivateKey privateKey = keyPair.getPrivate();
156 		Assert.state(privateKey instanceof RSAPrivateKey, "KeyPair must be an RSA ");
157 		signer = new RsaSigner((RSAPrivateKey) privateKey);
158 		RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
159 		verifier = new RsaVerifier(publicKey);
160 		verifierKey = "-----BEGIN PUBLIC KEY-----\n" + new String(Base64.encode(publicKey.getEncoded()))
161 				+ "\n-----END PUBLIC KEY-----";
162 	}
163 
164 	/**
165 	 * Sets the JWT signing key. It can be either a simple MAC key or an RSA key. RSA keys
166 	 * should be in OpenSSH format, as produced by <tt>ssh-keygen</tt>.
167 	 *
168 	 * @param key the key to be used for signing JWTs.
169 	 */
170 	public void setSigningKey(String key) {
171 		Assert.hasText(key);
172 		key = key.trim();
173 
174 		this.signingKey = key;
175 
176 		if (isPublic(key)) {
177 			signer = new RsaSigner(key);
178 			logger.info("Configured with RSA signing key");
179 		}
180 		else {
181 			// Assume it's a MAC key
182 			this.verifierKey = key;
183 			signer = new MacSigner(key);
184 		}
185 	}
186 
187 	/**
188 	 * @return true if the key has a public verifier
189 	 */
190 	private boolean isPublic(String key) {
191 		return key.startsWith("-----BEGIN");
192 	}
193 
194 	/**
195 	 * @return true if the signing key is a public key
196 	 */
197 	public boolean isPublic() {
198 		return signer instanceof RsaSigner;
199 	}
200 
201 	/**
202 	 * The key used for verifying signatures produced by this class. This is not used but
203 	 * is returned from the endpoint to allow resource servers to obtain the key.
204 	 *
205 	 * For an HMAC key it will be the same value as the signing key and does not need to
206 	 * be set. For and RSA key, it should be set to the String representation of the
207 	 * public key, in a standard format (e.g. OpenSSH keys)
208 	 *
209 	 * @param key the signature verification key (typically an RSA public key)
210 	 */
211 	public void setVerifierKey(String key) {
212 		this.verifierKey = key;
213 	}
214 
215 	public OAuth2AccessToken../../../../org/springframework/security/oauth2/common/OAuth2AccessToken.html#OAuth2AccessToken">OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
216 		DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
217 		Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
218 		String tokenId = result.getValue();
219 		if (!info.containsKey(TOKEN_ID)) {
220 			info.put(TOKEN_ID, tokenId);
221 		}
222 		else {
223 			tokenId = (String) info.get(TOKEN_ID);
224 		}
225 		result.setAdditionalInformation(info);
226 		result.setValue(encode(result, authentication));
227 		OAuth2RefreshToken refreshToken = result.getRefreshToken();
228 		if (refreshToken != null) {
229 			DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
230 			encodedRefreshToken.setValue(refreshToken.getValue());
231 			// Refresh tokens do not expire unless explicitly of the right type
232 			encodedRefreshToken.setExpiration(null);
233 			try {
234 				Map<String, Object> claims = objectMapper
235 						.parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
236 				if (claims.containsKey(TOKEN_ID)) {
237 					encodedRefreshToken.setValue(claims.get(TOKEN_ID).toString());
238 				}
239 			}
240 			catch (IllegalArgumentException e) {
241 			}
242 			Map<String, Object> refreshTokenInfo = new LinkedHashMap<String, Object>(
243 					accessToken.getAdditionalInformation());
244 			refreshTokenInfo.put(TOKEN_ID, encodedRefreshToken.getValue());
245 			refreshTokenInfo.put(ACCESS_TOKEN_ID, tokenId);
246 			encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);
247 			DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(
248 					encode(encodedRefreshToken, authentication));
249 			if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
250 				Date expiration = ((ExpiringOAuth2RefreshToken) refreshToken).getExpiration();
251 				encodedRefreshToken.setExpiration(expiration);
252 				token = new DefaultExpiringOAuth2RefreshToken(encode(encodedRefreshToken, authentication), expiration);
253 			}
254 			result.setRefreshToken(token);
255 		}
256 		return result;
257 	}
258 
259 	public boolean isRefreshToken(OAuth2AccessToken token) {
260 		return token.getAdditionalInformation().containsKey(ACCESS_TOKEN_ID);
261 	}
262 
263 	protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
264 		String content;
265 		try {
266 			content = objectMapper.formatMap(tokenConverter.convertAccessToken(accessToken, authentication));
267 		}
268 		catch (Exception e) {
269 			throw new IllegalStateException("Cannot convert access token to JSON", e);
270 		}
271 		String token = JwtHelper.encode(content, signer).getEncoded();
272 		return token;
273 	}
274 
275 	protected Map<String, Object> decode(String token) {
276 		try {
277 			Jwt jwt = JwtHelper.decodeAndVerify(token, verifier);
278 			String claimsStr = jwt.getClaims();
279 			Map<String, Object> claims = objectMapper.parseMap(claimsStr);
280 			if (claims.containsKey(EXP) && claims.get(EXP) instanceof Integer) {
281 				Integer intValue = (Integer) claims.get(EXP);
282 				claims.put(EXP, new Long(intValue));
283 			}
284 			this.getJwtClaimsSetVerifier().verify(claims);
285 			return claims;
286 		}
287 		catch (Exception e) {
288 			throw new InvalidTokenException("Cannot convert access token to JSON", e);
289 		}
290 	}
291 
292 	public void afterPropertiesSet() throws Exception {
293 		if (verifier != null) {
294 			// Assume signer also set independently if needed
295 			return;
296 		}
297 		SignatureVerifier verifier = new MacSigner(verifierKey);
298 		try {
299 			verifier = new RsaVerifier(verifierKey);
300 		}
301 		catch (Exception e) {
302 			logger.warn("Unable to create an RSA verifier from verifierKey (ignoreable if using MAC)");
303 		}
304 		// Check the signing and verification keys match
305 		if (signer instanceof RsaSigner) {
306 			byte[] test = "test".getBytes();
307 			try {
308 				verifier.verify(test, signer.sign(test));
309 				logger.info("Signing and verification RSA keys match");
310 			}
311 			catch (InvalidSignatureException e) {
312 				logger.error("Signing and verification RSA keys do not match");
313 			}
314 		}
315 		else if (verifier instanceof MacSigner) {
316 			// Avoid a race condition where setters are called in the wrong order. Use of
317 			// == is intentional.
318 			Assert.state(this.signingKey == this.verifierKey,
319 					"For MAC signing you do not need to specify the verifier key separately, and if you do it must match the signing key");
320 		}
321 		this.verifier = verifier;
322 	}
323 
324 	private class NoOpJwtClaimsSetVerifier implements JwtClaimsSetVerifier {
325 		@Override
326 		public void verify(Map<String, Object> claims) throws InvalidTokenException {
327 		}
328 	}
329 }