• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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