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 static org.junit.Assert.assertEquals; 35 import static org.junit.Assert.assertNull; 36 import static org.junit.Assert.assertThrows; 37 38 import com.google.api.client.http.HttpHeaders; 39 import com.google.api.client.testing.http.MockLowLevelHttpRequest; 40 import com.google.api.client.util.GenericData; 41 import com.google.auth.TestUtils; 42 import com.google.common.base.Joiner; 43 import java.io.IOException; 44 import java.util.Arrays; 45 import java.util.List; 46 import java.util.Map; 47 import org.junit.Before; 48 import org.junit.Test; 49 import org.junit.function.ThrowingRunnable; 50 import org.junit.runner.RunWith; 51 import org.junit.runners.JUnit4; 52 53 /** Tests for {@link StsRequestHandler}. */ 54 @RunWith(JUnit4.class) 55 public final class StsRequestHandlerTest { 56 57 private static final String TOKEN_EXCHANGE_GRANT_TYPE = 58 "urn:ietf:params:oauth:grant-type:token-exchange"; 59 private static final String CLOUD_PLATFORM_SCOPE = 60 "https://www.googleapis.com/auth/cloud-platform"; 61 private static final String DEFAULT_REQUESTED_TOKEN_TYPE = 62 "urn:ietf:params:oauth:token-type:access_token"; 63 private static final String TOKEN_URL = "https://sts.googleapis.com/v1/token"; 64 65 private MockStsTransport transport; 66 67 @Before setup()68 public void setup() { 69 transport = new MockStsTransport(); 70 } 71 72 @Test exchangeToken()73 public void exchangeToken() throws IOException { 74 StsTokenExchangeRequest stsTokenExchangeRequest = 75 StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType") 76 .setScopes(Arrays.asList(CLOUD_PLATFORM_SCOPE)) 77 .build(); 78 79 StsRequestHandler requestHandler = 80 StsRequestHandler.newBuilder( 81 TOKEN_URL, stsTokenExchangeRequest, transport.createRequestFactory()) 82 .build(); 83 84 StsTokenExchangeResponse response = requestHandler.exchangeToken(); 85 86 // Validate response. 87 assertEquals(transport.getAccessToken(), response.getAccessToken().getTokenValue()); 88 assertEquals(transport.getTokenType(), response.getTokenType()); 89 assertEquals(transport.getIssuedTokenType(), response.getIssuedTokenType()); 90 assertEquals(transport.getExpiresIn(), response.getExpiresInSeconds()); 91 92 // Validate request content. 93 GenericData expectedRequestContent = 94 new GenericData() 95 .set("grant_type", TOKEN_EXCHANGE_GRANT_TYPE) 96 .set("scope", CLOUD_PLATFORM_SCOPE) 97 .set("requested_token_type", DEFAULT_REQUESTED_TOKEN_TYPE) 98 .set("subject_token_type", stsTokenExchangeRequest.getSubjectTokenType()) 99 .set("subject_token", stsTokenExchangeRequest.getSubjectToken()); 100 101 MockLowLevelHttpRequest request = transport.getRequest(); 102 Map<String, String> actualRequestContent = TestUtils.parseQuery(request.getContentAsString()); 103 assertEquals(expectedRequestContent.getUnknownKeys(), actualRequestContent); 104 } 105 106 @Test exchangeToken_withOptionalParams()107 public void exchangeToken_withOptionalParams() throws IOException { 108 // Return optional params scope and the refresh_token. 109 transport.addScopeSequence(Arrays.asList("scope1", "scope2", "scope3")); 110 transport.addRefreshTokenSequence("refreshToken"); 111 112 // Build the token exchange request. 113 StsTokenExchangeRequest stsTokenExchangeRequest = 114 StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType") 115 .setAudience("audience") 116 .setResource("resource") 117 .setActingParty(new ActingParty("actorToken", "actorTokenType")) 118 .setRequestTokenType("requestedTokenType") 119 .setScopes(Arrays.asList("scope1", "scope2", "scope3")) 120 .build(); 121 122 HttpHeaders httpHeaders = 123 new HttpHeaders() 124 .setContentType("application/x-www-form-urlencoded") 125 .setAcceptEncoding("gzip") 126 .set("custom_header_key", "custom_header_value"); 127 128 StsRequestHandler requestHandler = 129 StsRequestHandler.newBuilder( 130 TOKEN_URL, stsTokenExchangeRequest, transport.createRequestFactory()) 131 .setHeaders(httpHeaders) 132 .setInternalOptions("internalOptions") 133 .build(); 134 135 StsTokenExchangeResponse response = requestHandler.exchangeToken(); 136 137 // Validate response. 138 assertEquals(transport.getAccessToken(), response.getAccessToken().getTokenValue()); 139 assertEquals(transport.getTokenType(), response.getTokenType()); 140 assertEquals(transport.getIssuedTokenType(), response.getIssuedTokenType()); 141 assertEquals(transport.getExpiresIn(), response.getExpiresInSeconds()); 142 assertEquals(Arrays.asList("scope1", "scope2", "scope3"), response.getScopes()); 143 assertEquals("refreshToken", response.getRefreshToken()); 144 145 // Validate headers. 146 MockLowLevelHttpRequest request = transport.getRequest(); 147 Map<String, List<String>> requestHeaders = request.getHeaders(); 148 assertEquals("application/x-www-form-urlencoded", requestHeaders.get("content-type").get(0)); 149 assertEquals("gzip", requestHeaders.get("accept-encoding").get(0)); 150 assertEquals("custom_header_value", requestHeaders.get("custom_header_key").get(0)); 151 152 // Validate request content. 153 GenericData expectedRequestContent = 154 new GenericData() 155 .set("grant_type", TOKEN_EXCHANGE_GRANT_TYPE) 156 .set("scope", Joiner.on(' ').join(Arrays.asList("scope1", "scope2", "scope3"))) 157 .set("options", "internalOptions") 158 .set("subject_token_type", stsTokenExchangeRequest.getSubjectTokenType()) 159 .set("subject_token", stsTokenExchangeRequest.getSubjectToken()) 160 .set("requested_token_type", stsTokenExchangeRequest.getRequestedTokenType()) 161 .set("actor_token", stsTokenExchangeRequest.getActingParty().getActorToken()) 162 .set("actor_token_type", stsTokenExchangeRequest.getActingParty().getActorTokenType()) 163 .set("resource", stsTokenExchangeRequest.getResource()) 164 .set("audience", stsTokenExchangeRequest.getAudience()); 165 166 Map<String, String> actualRequestContent = TestUtils.parseQuery(request.getContentAsString()); 167 assertEquals(expectedRequestContent.getUnknownKeys(), actualRequestContent); 168 } 169 170 @Test exchangeToken_throwsException()171 public void exchangeToken_throwsException() throws IOException { 172 StsTokenExchangeRequest stsTokenExchangeRequest = 173 StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build(); 174 175 final StsRequestHandler requestHandler = 176 StsRequestHandler.newBuilder( 177 TOKEN_URL, stsTokenExchangeRequest, transport.createRequestFactory()) 178 .build(); 179 180 transport.addResponseErrorSequence( 181 TestUtils.buildHttpResponseException( 182 "invalidRequest", /* errorDescription= */ null, /* errorUri= */ null)); 183 184 OAuthException e = 185 assertThrows( 186 OAuthException.class, 187 new ThrowingRunnable() { 188 @Override 189 public void run() throws Throwable { 190 requestHandler.exchangeToken(); 191 } 192 }); 193 194 assertEquals("invalidRequest", e.getErrorCode()); 195 assertNull(e.getErrorDescription()); 196 assertNull(e.getErrorUri()); 197 } 198 199 @Test exchangeToken_withOptionalParams_throwsException()200 public void exchangeToken_withOptionalParams_throwsException() throws IOException { 201 StsTokenExchangeRequest stsTokenExchangeRequest = 202 StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build(); 203 204 final StsRequestHandler requestHandler = 205 StsRequestHandler.newBuilder( 206 TOKEN_URL, stsTokenExchangeRequest, transport.createRequestFactory()) 207 .build(); 208 209 transport.addResponseErrorSequence( 210 TestUtils.buildHttpResponseException("invalidRequest", "errorDescription", "errorUri")); 211 212 OAuthException e = 213 assertThrows( 214 OAuthException.class, 215 new ThrowingRunnable() { 216 @Override 217 public void run() throws Throwable { 218 requestHandler.exchangeToken(); 219 } 220 }); 221 222 assertEquals("invalidRequest", e.getErrorCode()); 223 assertEquals("errorDescription", e.getErrorDescription()); 224 assertEquals("errorUri", e.getErrorUri()); 225 } 226 227 @Test exchangeToken_ioException()228 public void exchangeToken_ioException() { 229 StsTokenExchangeRequest stsTokenExchangeRequest = 230 StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build(); 231 232 final StsRequestHandler requestHandler = 233 StsRequestHandler.newBuilder( 234 TOKEN_URL, stsTokenExchangeRequest, transport.createRequestFactory()) 235 .build(); 236 237 IOException e = new IOException(); 238 transport.addResponseErrorSequence(e); 239 240 IOException thrownException = 241 assertThrows( 242 IOException.class, 243 new ThrowingRunnable() { 244 @Override 245 public void run() throws Throwable { 246 requestHandler.exchangeToken(); 247 } 248 }); 249 assertEquals(e, thrownException); 250 } 251 252 @Test exchangeToken_noExpiresInReturned()253 public void exchangeToken_noExpiresInReturned() throws IOException { 254 // Don't return expires in. This happens in the CAB flow when the subject token does not belong 255 // to a service account. 256 transport.setReturnExpiresIn(/* returnExpiresIn= */ false); 257 258 StsTokenExchangeRequest stsTokenExchangeRequest = 259 StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType") 260 .setScopes(Arrays.asList(CLOUD_PLATFORM_SCOPE)) 261 .build(); 262 263 StsRequestHandler requestHandler = 264 StsRequestHandler.newBuilder( 265 TOKEN_URL, stsTokenExchangeRequest, transport.createRequestFactory()) 266 .build(); 267 268 StsTokenExchangeResponse response = requestHandler.exchangeToken(); 269 270 // Validate response. 271 assertEquals(transport.getAccessToken(), response.getAccessToken().getTokenValue()); 272 assertNull(response.getAccessToken().getExpirationTime()); 273 274 assertEquals(transport.getTokenType(), response.getTokenType()); 275 assertEquals(transport.getIssuedTokenType(), response.getIssuedTokenType()); 276 assertNull(response.getExpiresInSeconds()); 277 } 278 } 279