• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2021 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.http.GenericUrl;
35 import com.google.api.client.http.HttpHeaders;
36 import com.google.api.client.http.HttpRequest;
37 import com.google.api.client.http.HttpRequestFactory;
38 import com.google.api.client.http.HttpResponse;
39 import com.google.api.client.http.HttpResponseException;
40 import com.google.api.client.http.UrlEncodedContent;
41 import com.google.api.client.json.GenericJson;
42 import com.google.api.client.json.JsonObjectParser;
43 import com.google.api.client.json.JsonParser;
44 import com.google.api.client.util.GenericData;
45 import com.google.common.base.Joiner;
46 import com.google.errorprone.annotations.CanIgnoreReturnValue;
47 import java.io.IOException;
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.List;
51 import javax.annotation.Nullable;
52 
53 /** Implements the OAuth 2.0 token exchange based on https://tools.ietf.org/html/rfc8693. */
54 final class StsRequestHandler {
55   private static final String TOKEN_EXCHANGE_GRANT_TYPE =
56       "urn:ietf:params:oauth:grant-type:token-exchange";
57   private static final String PARSE_ERROR_PREFIX = "Error parsing token response.";
58 
59   private final String tokenExchangeEndpoint;
60   private final StsTokenExchangeRequest request;
61   private final HttpRequestFactory httpRequestFactory;
62 
63   @Nullable private final HttpHeaders headers;
64   @Nullable private final String internalOptions;
65 
66   /**
67    * Internal constructor.
68    *
69    * @param tokenExchangeEndpoint the token exchange endpoint
70    * @param request the token exchange request
71    * @param headers optional additional headers to pass along the request
72    * @param internalOptions optional GCP specific STS options
73    * @return an StsTokenExchangeResponse instance if the request was successful
74    */
StsRequestHandler( String tokenExchangeEndpoint, StsTokenExchangeRequest request, HttpRequestFactory httpRequestFactory, @Nullable HttpHeaders headers, @Nullable String internalOptions)75   private StsRequestHandler(
76       String tokenExchangeEndpoint,
77       StsTokenExchangeRequest request,
78       HttpRequestFactory httpRequestFactory,
79       @Nullable HttpHeaders headers,
80       @Nullable String internalOptions) {
81     this.tokenExchangeEndpoint = tokenExchangeEndpoint;
82     this.request = request;
83     this.httpRequestFactory = httpRequestFactory;
84     this.headers = headers;
85     this.internalOptions = internalOptions;
86   }
87 
newBuilder( String tokenExchangeEndpoint, StsTokenExchangeRequest stsTokenExchangeRequest, HttpRequestFactory httpRequestFactory)88   public static Builder newBuilder(
89       String tokenExchangeEndpoint,
90       StsTokenExchangeRequest stsTokenExchangeRequest,
91       HttpRequestFactory httpRequestFactory) {
92     return new Builder(tokenExchangeEndpoint, stsTokenExchangeRequest, httpRequestFactory);
93   }
94 
95   /** Exchanges the provided token for another type of token based on the RFC 8693 spec. */
exchangeToken()96   public StsTokenExchangeResponse exchangeToken() throws IOException {
97     UrlEncodedContent content = new UrlEncodedContent(buildTokenRequest());
98 
99     HttpRequest httpRequest =
100         httpRequestFactory.buildPostRequest(new GenericUrl(tokenExchangeEndpoint), content);
101     httpRequest.setParser(new JsonObjectParser(OAuth2Utils.JSON_FACTORY));
102     if (headers != null) {
103       httpRequest.setHeaders(headers);
104     }
105 
106     try {
107       HttpResponse response = httpRequest.execute();
108       GenericData responseData = response.parseAs(GenericData.class);
109       return buildResponse(responseData);
110     } catch (HttpResponseException e) {
111       throw OAuthException.createFromHttpResponseException(e);
112     }
113   }
114 
buildTokenRequest()115   private GenericData buildTokenRequest() {
116     GenericData tokenRequest =
117         new GenericData()
118             .set("grant_type", TOKEN_EXCHANGE_GRANT_TYPE)
119             .set("subject_token_type", request.getSubjectTokenType())
120             .set("subject_token", request.getSubjectToken());
121 
122     // Add scopes as a space-delimited string.
123     List<String> scopes = new ArrayList<>();
124     if (request.hasScopes()) {
125       scopes.addAll(request.getScopes());
126       tokenRequest.set("scope", Joiner.on(' ').join(scopes));
127     }
128 
129     // Set the requested token type, which defaults to
130     // urn:ietf:params:oauth:token-type:access_token.
131     String requestTokenType =
132         request.hasRequestedTokenType()
133             ? request.getRequestedTokenType()
134             : OAuth2Utils.TOKEN_TYPE_ACCESS_TOKEN;
135     tokenRequest.set("requested_token_type", requestTokenType);
136 
137     // Add other optional params, if possible.
138     if (request.hasResource()) {
139       tokenRequest.set("resource", request.getResource());
140     }
141     if (request.hasAudience()) {
142       tokenRequest.set("audience", request.getAudience());
143     }
144 
145     if (request.hasActingParty()) {
146       tokenRequest.set("actor_token", request.getActingParty().getActorToken());
147       tokenRequest.set("actor_token_type", request.getActingParty().getActorTokenType());
148     }
149 
150     if (internalOptions != null && !internalOptions.isEmpty()) {
151       tokenRequest.set("options", internalOptions);
152     }
153     return tokenRequest;
154   }
155 
buildResponse(GenericData responseData)156   private StsTokenExchangeResponse buildResponse(GenericData responseData) throws IOException {
157     String accessToken =
158         OAuth2Utils.validateString(responseData, "access_token", PARSE_ERROR_PREFIX);
159     String issuedTokenType =
160         OAuth2Utils.validateString(responseData, "issued_token_type", PARSE_ERROR_PREFIX);
161     String tokenType = OAuth2Utils.validateString(responseData, "token_type", PARSE_ERROR_PREFIX);
162 
163     StsTokenExchangeResponse.Builder builder =
164         StsTokenExchangeResponse.newBuilder(accessToken, issuedTokenType, tokenType);
165 
166     if (responseData.containsKey("expires_in")) {
167       builder.setExpiresInSeconds(
168           OAuth2Utils.validateLong(responseData, "expires_in", PARSE_ERROR_PREFIX));
169     }
170     if (responseData.containsKey("refresh_token")) {
171       builder.setRefreshToken(
172           OAuth2Utils.validateString(responseData, "refresh_token", PARSE_ERROR_PREFIX));
173     }
174     if (responseData.containsKey("scope")) {
175       String scope = OAuth2Utils.validateString(responseData, "scope", PARSE_ERROR_PREFIX);
176       builder.setScopes(Arrays.asList(scope.trim().split("\\s+")));
177     }
178     return builder.build();
179   }
180 
parseJson(String json)181   private GenericJson parseJson(String json) throws IOException {
182     JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(json);
183     return parser.parseAndClose(GenericJson.class);
184   }
185 
186   public static class Builder {
187     private final String tokenExchangeEndpoint;
188     private final StsTokenExchangeRequest request;
189     private final HttpRequestFactory httpRequestFactory;
190 
191     @Nullable private HttpHeaders headers;
192     @Nullable private String internalOptions;
193 
Builder( String tokenExchangeEndpoint, StsTokenExchangeRequest stsTokenExchangeRequest, HttpRequestFactory httpRequestFactory)194     private Builder(
195         String tokenExchangeEndpoint,
196         StsTokenExchangeRequest stsTokenExchangeRequest,
197         HttpRequestFactory httpRequestFactory) {
198       this.tokenExchangeEndpoint = tokenExchangeEndpoint;
199       this.request = stsTokenExchangeRequest;
200       this.httpRequestFactory = httpRequestFactory;
201     }
202 
203     @CanIgnoreReturnValue
setHeaders(HttpHeaders headers)204     public StsRequestHandler.Builder setHeaders(HttpHeaders headers) {
205       this.headers = headers;
206       return this;
207     }
208 
209     @CanIgnoreReturnValue
setInternalOptions(String internalOptions)210     public StsRequestHandler.Builder setInternalOptions(String internalOptions) {
211       this.internalOptions = internalOptions;
212       return this;
213     }
214 
build()215     public StsRequestHandler build() {
216       return new StsRequestHandler(
217           tokenExchangeEndpoint, request, httpRequestFactory, headers, internalOptions);
218     }
219   }
220 }
221