1 /* 2 * Copyright 2019, Google LLC 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * 15 * * Neither the name of Google LLC nor the names of its 16 * contributors may be used to endorse or promote products derived from 17 * this software without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32 package com.google.auth.oauth2; 33 34 import com.google.api.client.json.webtoken.JsonWebSignature; 35 import com.google.api.client.json.webtoken.JsonWebToken; 36 import com.google.api.client.util.Clock; 37 import com.google.auth.Credentials; 38 import com.google.auth.http.AuthHttpConstants; 39 import com.google.common.annotations.VisibleForTesting; 40 import com.google.common.base.Preconditions; 41 import com.google.errorprone.annotations.CanIgnoreReturnValue; 42 import java.io.IOException; 43 import java.net.URI; 44 import java.security.GeneralSecurityException; 45 import java.security.PrivateKey; 46 import java.util.Collections; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Objects; 50 import java.util.concurrent.TimeUnit; 51 52 /** 53 * Credentials class for calling Google APIs using a JWT with custom claims. 54 * 55 * <p>Uses a JSON Web Token (JWT) directly in the request metadata to provide authorization. 56 * 57 * <pre><code> 58 * JwtClaims claims = JwtClaims.newBuilder() 59 * .setAudience("https://example.com/some-audience") 60 * .setIssuer("some-issuer@example.com") 61 * .setSubject("some-subject@example.com") 62 * .build(); 63 * Credentials = JwtCredentials.newBuilder() 64 * .setPrivateKey(privateKey) 65 * .setPrivateKeyId("private-key-id") 66 * .setJwtClaims(claims) 67 * .build(); 68 * </code></pre> 69 */ 70 public class JwtCredentials extends Credentials implements JwtProvider { 71 private static final String JWT_ACCESS_PREFIX = OAuth2Utils.BEARER_PREFIX; 72 private static final String JWT_INCOMPLETE_ERROR_MESSAGE = 73 "JWT claims must contain audience, " + "issuer, and subject."; 74 private static final long CLOCK_SKEW = TimeUnit.MINUTES.toSeconds(5); 75 76 // byte[] is serializable, so the lock variable can be final 77 private final Object lock = new byte[0]; 78 private final PrivateKey privateKey; 79 private final String privateKeyId; 80 private final JwtClaims jwtClaims; 81 private final Long lifeSpanSeconds; 82 @VisibleForTesting transient Clock clock; 83 84 private transient String jwt; 85 // The date (represented as seconds since the epoch) that the generated JWT expires 86 private transient Long expiryInSeconds; 87 JwtCredentials(Builder builder)88 private JwtCredentials(Builder builder) { 89 this.privateKey = Preconditions.checkNotNull(builder.getPrivateKey()); 90 this.privateKeyId = builder.getPrivateKeyId(); 91 this.jwtClaims = Preconditions.checkNotNull(builder.getJwtClaims()); 92 Preconditions.checkState(jwtClaims.isComplete(), JWT_INCOMPLETE_ERROR_MESSAGE); 93 this.lifeSpanSeconds = Preconditions.checkNotNull(builder.getLifeSpanSeconds()); 94 this.clock = Preconditions.checkNotNull(builder.getClock()); 95 } 96 newBuilder()97 public static Builder newBuilder() { 98 return new Builder(); 99 } 100 101 /** Refresh the token by discarding the cached token and metadata and rebuilding a new one. */ 102 @Override refresh()103 public void refresh() throws IOException { 104 JsonWebSignature.Header header = new JsonWebSignature.Header(); 105 header.setAlgorithm("RS256"); 106 header.setType("JWT"); 107 header.setKeyId(privateKeyId); 108 109 JsonWebToken.Payload payload = new JsonWebToken.Payload(); 110 payload.setAudience(jwtClaims.getAudience()); 111 payload.setIssuer(jwtClaims.getIssuer()); 112 payload.setSubject(jwtClaims.getSubject()); 113 114 long currentTime = clock.currentTimeMillis(); 115 payload.setIssuedAtTimeSeconds(currentTime / 1000); 116 payload.setExpirationTimeSeconds(currentTime / 1000 + lifeSpanSeconds); 117 118 // Add all additional claims 119 payload.putAll(jwtClaims.getAdditionalClaims()); 120 121 synchronized (lock) { 122 this.expiryInSeconds = payload.getExpirationTimeSeconds(); 123 124 try { 125 this.jwt = 126 JsonWebSignature.signUsingRsaSha256( 127 privateKey, OAuth2Utils.JSON_FACTORY, header, payload); 128 } catch (GeneralSecurityException e) { 129 throw new IOException( 130 "Error signing service account JWT access header with private key.", e); 131 } 132 } 133 } 134 shouldRefresh()135 private boolean shouldRefresh() { 136 return expiryInSeconds == null 137 || getClock().currentTimeMillis() / 1000 > expiryInSeconds - CLOCK_SKEW; 138 } 139 140 /** 141 * Returns a copy of these credentials with modified claims. 142 * 143 * @param newClaims new claims. Any unspecified claim fields default to the the current values. 144 * @return new credentials 145 */ 146 @Override jwtWithClaims(JwtClaims newClaims)147 public JwtCredentials jwtWithClaims(JwtClaims newClaims) { 148 return JwtCredentials.newBuilder() 149 .setPrivateKey(privateKey) 150 .setPrivateKeyId(privateKeyId) 151 .setJwtClaims(jwtClaims.merge(newClaims)) 152 .build(); 153 } 154 155 @Override getAuthenticationType()156 public String getAuthenticationType() { 157 return "JWT"; 158 } 159 160 @Override getRequestMetadata(URI uri)161 public Map<String, List<String>> getRequestMetadata(URI uri) throws IOException { 162 synchronized (lock) { 163 if (shouldRefresh()) { 164 refresh(); 165 } 166 List<String> newAuthorizationHeaders = Collections.singletonList(JWT_ACCESS_PREFIX + jwt); 167 return Collections.singletonMap(AuthHttpConstants.AUTHORIZATION, newAuthorizationHeaders); 168 } 169 } 170 171 @Override hasRequestMetadata()172 public boolean hasRequestMetadata() { 173 return true; 174 } 175 176 @Override hasRequestMetadataOnly()177 public boolean hasRequestMetadataOnly() { 178 return true; 179 } 180 181 @Override equals(Object obj)182 public boolean equals(Object obj) { 183 if (!(obj instanceof JwtCredentials)) { 184 return false; 185 } 186 JwtCredentials other = (JwtCredentials) obj; 187 return Objects.equals(this.privateKey, other.privateKey) 188 && Objects.equals(this.privateKeyId, other.privateKeyId) 189 && Objects.equals(this.jwtClaims, other.jwtClaims) 190 && Objects.equals(this.lifeSpanSeconds, other.lifeSpanSeconds); 191 } 192 193 @Override hashCode()194 public int hashCode() { 195 return Objects.hash(this.privateKey, this.privateKeyId, this.jwtClaims, this.lifeSpanSeconds); 196 } 197 getClock()198 Clock getClock() { 199 if (clock == null) { 200 clock = Clock.SYSTEM; 201 } 202 return clock; 203 } 204 205 public static class Builder { 206 private PrivateKey privateKey; 207 private String privateKeyId; 208 private JwtClaims jwtClaims; 209 private Clock clock = Clock.SYSTEM; 210 private Long lifeSpanSeconds = TimeUnit.HOURS.toSeconds(1); 211 Builder()212 protected Builder() {} 213 214 @CanIgnoreReturnValue setPrivateKey(PrivateKey privateKey)215 public Builder setPrivateKey(PrivateKey privateKey) { 216 this.privateKey = Preconditions.checkNotNull(privateKey); 217 return this; 218 } 219 getPrivateKey()220 public PrivateKey getPrivateKey() { 221 return privateKey; 222 } 223 224 @CanIgnoreReturnValue setPrivateKeyId(String privateKeyId)225 public Builder setPrivateKeyId(String privateKeyId) { 226 this.privateKeyId = privateKeyId; 227 return this; 228 } 229 getPrivateKeyId()230 public String getPrivateKeyId() { 231 return privateKeyId; 232 } 233 234 @CanIgnoreReturnValue setJwtClaims(JwtClaims claims)235 public Builder setJwtClaims(JwtClaims claims) { 236 this.jwtClaims = Preconditions.checkNotNull(claims); 237 return this; 238 } 239 getJwtClaims()240 public JwtClaims getJwtClaims() { 241 return jwtClaims; 242 } 243 244 @CanIgnoreReturnValue setLifeSpanSeconds(Long lifeSpanSeconds)245 public Builder setLifeSpanSeconds(Long lifeSpanSeconds) { 246 this.lifeSpanSeconds = Preconditions.checkNotNull(lifeSpanSeconds); 247 return this; 248 } 249 getLifeSpanSeconds()250 public Long getLifeSpanSeconds() { 251 return lifeSpanSeconds; 252 } 253 254 @CanIgnoreReturnValue setClock(Clock clock)255 Builder setClock(Clock clock) { 256 this.clock = Preconditions.checkNotNull(clock); 257 return this; 258 } 259 getClock()260 Clock getClock() { 261 return clock; 262 } 263 build()264 public JwtCredentials build() { 265 return new JwtCredentials(this); 266 } 267 } 268 } 269