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