1 /* 2 * Copyright 2022 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.assertFalse; 36 import static org.junit.Assert.assertNull; 37 import static org.junit.Assert.assertTrue; 38 import static org.junit.Assert.fail; 39 40 import com.google.api.client.json.GenericJson; 41 import java.io.IOException; 42 import java.math.BigDecimal; 43 import java.time.Instant; 44 import org.junit.Test; 45 46 /** Tests for {@link ExecutableResponse}. */ 47 public class ExecutableResponseTest { 48 49 private static final String TOKEN_TYPE_OIDC = "urn:ietf:params:oauth:token-type:id_token"; 50 private static final String TOKEN_TYPE_SAML = "urn:ietf:params:oauth:token-type:saml2"; 51 private static final String ID_TOKEN = "header.payload.signature"; 52 private static final String SAML_RESPONSE = "samlResponse"; 53 54 private static final int EXECUTABLE_SUPPORTED_MAX_VERSION = 1; 55 private static final int EXPIRATION_DURATION = 3600; 56 57 @Test constructor_successOidcResponse()58 public void constructor_successOidcResponse() throws IOException { 59 ExecutableResponse response = new ExecutableResponse(buildOidcResponse()); 60 61 assertTrue(response.isSuccessful()); 62 assertTrue(response.isValid()); 63 assertEquals(EXECUTABLE_SUPPORTED_MAX_VERSION, response.getVersion()); 64 assertEquals(TOKEN_TYPE_OIDC, response.getTokenType()); 65 assertEquals(ID_TOKEN, response.getSubjectToken()); 66 assertTrue( 67 Instant.now().getEpochSecond() + EXPIRATION_DURATION == response.getExpirationTime()); 68 } 69 70 @Test constructor_successOidcResponseMissingExpirationTimeField_notExpired()71 public void constructor_successOidcResponseMissingExpirationTimeField_notExpired() 72 throws IOException { 73 GenericJson jsonResponse = buildOidcResponse(); 74 jsonResponse.remove("expiration_time"); 75 76 ExecutableResponse response = new ExecutableResponse(jsonResponse); 77 78 assertTrue(response.isSuccessful()); 79 assertTrue(response.isValid()); 80 assertFalse(response.isExpired()); 81 assertEquals(EXECUTABLE_SUPPORTED_MAX_VERSION, response.getVersion()); 82 assertEquals(TOKEN_TYPE_OIDC, response.getTokenType()); 83 assertEquals(ID_TOKEN, response.getSubjectToken()); 84 assertNull(response.getExpirationTime()); 85 } 86 87 @Test constructor_successSamlResponse()88 public void constructor_successSamlResponse() throws IOException { 89 ExecutableResponse response = new ExecutableResponse(buildSamlResponse()); 90 91 assertTrue(response.isSuccessful()); 92 assertTrue(response.isValid()); 93 assertEquals(EXECUTABLE_SUPPORTED_MAX_VERSION, response.getVersion()); 94 assertEquals(TOKEN_TYPE_SAML, response.getTokenType()); 95 assertEquals(SAML_RESPONSE, response.getSubjectToken()); 96 assertTrue( 97 Instant.now().getEpochSecond() + EXPIRATION_DURATION == response.getExpirationTime()); 98 } 99 100 @Test constructor_successSamlResponseMissingExpirationTimeField_notExpired()101 public void constructor_successSamlResponseMissingExpirationTimeField_notExpired() 102 throws IOException { 103 GenericJson jsonResponse = buildSamlResponse(); 104 jsonResponse.remove("expiration_time"); 105 106 ExecutableResponse response = new ExecutableResponse(jsonResponse); 107 108 assertTrue(response.isSuccessful()); 109 assertTrue(response.isValid()); 110 assertFalse(response.isExpired()); 111 assertEquals(EXECUTABLE_SUPPORTED_MAX_VERSION, response.getVersion()); 112 assertEquals(TOKEN_TYPE_SAML, response.getTokenType()); 113 assertEquals(SAML_RESPONSE, response.getSubjectToken()); 114 assertNull(response.getExpirationTime()); 115 } 116 117 @Test constructor_validErrorResponse()118 public void constructor_validErrorResponse() throws IOException { 119 ExecutableResponse response = new ExecutableResponse(buildErrorResponse()); 120 121 assertFalse(response.isSuccessful()); 122 assertFalse(response.isValid()); 123 assertFalse(response.isExpired()); 124 assertNull(response.getSubjectToken()); 125 assertNull(response.getTokenType()); 126 assertNull(response.getExpirationTime()); 127 assertEquals(EXECUTABLE_SUPPORTED_MAX_VERSION, response.getVersion()); 128 assertEquals("401", response.getErrorCode()); 129 assertEquals("Caller not authorized.", response.getErrorMessage()); 130 } 131 132 @Test constructor_errorResponseMissingCode_throws()133 public void constructor_errorResponseMissingCode_throws() throws IOException { 134 GenericJson jsonResponse = buildErrorResponse(); 135 136 Object[] values = new Object[] {null, ""}; 137 for (Object value : values) { 138 jsonResponse.put("code", value); 139 try { 140 new ExecutableResponse(jsonResponse); 141 fail("Should not be able to continue without exception."); 142 } catch (PluggableAuthException exception) { 143 assertEquals( 144 "Error code INVALID_EXECUTABLE_RESPONSE: The executable response must contain " 145 + "`error` and `message` fields when unsuccessful.", 146 exception.getMessage()); 147 } 148 } 149 } 150 151 @Test constructor_errorResponseMissingMessage_throws()152 public void constructor_errorResponseMissingMessage_throws() throws IOException { 153 GenericJson jsonResponse = buildErrorResponse(); 154 155 Object[] values = new Object[] {null, ""}; 156 for (Object value : values) { 157 jsonResponse.put("message", value); 158 159 try { 160 new ExecutableResponse(jsonResponse); 161 fail("Should not be able to continue without exception."); 162 } catch (PluggableAuthException exception) { 163 assertEquals( 164 "Error code INVALID_EXECUTABLE_RESPONSE: The executable response must contain " 165 + "`error` and `message` fields when unsuccessful.", 166 exception.getMessage()); 167 } 168 } 169 } 170 171 @Test constructor_successResponseMissingVersionField_throws()172 public void constructor_successResponseMissingVersionField_throws() throws IOException { 173 GenericJson jsonResponse = buildOidcResponse(); 174 jsonResponse.remove("version"); 175 176 try { 177 new ExecutableResponse(jsonResponse); 178 fail("Should not be able to continue without exception."); 179 } catch (PluggableAuthException exception) { 180 assertEquals( 181 "Error code INVALID_EXECUTABLE_RESPONSE: The executable response is missing the " 182 + "`version` field.", 183 exception.getMessage()); 184 } 185 } 186 187 @Test constructor_successResponseMissingSuccessField_throws()188 public void constructor_successResponseMissingSuccessField_throws() throws Exception { 189 GenericJson jsonResponse = buildOidcResponse(); 190 jsonResponse.remove("success"); 191 192 try { 193 new ExecutableResponse(jsonResponse); 194 fail("Should not be able to continue without exception."); 195 } catch (PluggableAuthException exception) { 196 assertEquals( 197 "Error code INVALID_EXECUTABLE_RESPONSE: The executable response is missing the " 198 + "`success` field.", 199 exception.getMessage()); 200 } 201 } 202 203 @Test constructor_successResponseMissingTokenTypeField_throws()204 public void constructor_successResponseMissingTokenTypeField_throws() throws IOException { 205 GenericJson jsonResponse = buildOidcResponse(); 206 jsonResponse.remove("token_type"); 207 208 try { 209 new ExecutableResponse(jsonResponse); 210 fail("Should not be able to continue without exception."); 211 } catch (PluggableAuthException exception) { 212 assertEquals( 213 "Error code INVALID_EXECUTABLE_RESPONSE: The executable response is missing the " 214 + "`token_type` field.", 215 exception.getMessage()); 216 } 217 } 218 219 @Test constructor_samlResponseMissingSubjectToken_throws()220 public void constructor_samlResponseMissingSubjectToken_throws() throws IOException { 221 GenericJson jsonResponse = buildSamlResponse(); 222 223 Object[] values = new Object[] {null, ""}; 224 for (Object value : values) { 225 jsonResponse.put("saml_response", value); 226 227 try { 228 new ExecutableResponse(jsonResponse); 229 fail("Should not be able to continue without exception."); 230 } catch (PluggableAuthException exception) { 231 assertEquals( 232 "Error code INVALID_EXECUTABLE_RESPONSE: The executable response does not " 233 + "contain a valid token.", 234 exception.getMessage()); 235 } 236 } 237 } 238 239 @Test constructor_oidcResponseMissingSubjectToken_throws()240 public void constructor_oidcResponseMissingSubjectToken_throws() throws IOException { 241 GenericJson jsonResponse = buildOidcResponse(); 242 243 Object[] values = new Object[] {null, ""}; 244 for (Object value : values) { 245 jsonResponse.put("id_token", value); 246 247 try { 248 new ExecutableResponse(jsonResponse); 249 fail("Should not be able to continue without exception."); 250 } catch (PluggableAuthException exception) { 251 assertEquals( 252 "Error code INVALID_EXECUTABLE_RESPONSE: The executable response does not " 253 + "contain a valid token.", 254 exception.getMessage()); 255 } 256 } 257 } 258 259 @Test isExpired()260 public void isExpired() throws IOException { 261 GenericJson jsonResponse = buildOidcResponse(); 262 263 BigDecimal[] values = 264 new BigDecimal[] { 265 BigDecimal.valueOf(Instant.now().getEpochSecond() - 1000), 266 BigDecimal.valueOf(Instant.now().getEpochSecond() + 1000) 267 }; 268 boolean[] expectedResults = new boolean[] {true, false}; 269 270 for (int i = 0; i < values.length; i++) { 271 jsonResponse.put("expiration_time", values[i]); 272 273 ExecutableResponse response = new ExecutableResponse(jsonResponse); 274 275 assertEquals(expectedResults[i], response.isExpired()); 276 } 277 } 278 buildOidcResponse()279 private static GenericJson buildOidcResponse() { 280 GenericJson json = new GenericJson(); 281 json.setFactory(OAuth2Utils.JSON_FACTORY); 282 json.put("version", EXECUTABLE_SUPPORTED_MAX_VERSION); 283 json.put("success", true); 284 json.put("token_type", TOKEN_TYPE_OIDC); 285 json.put("id_token", ID_TOKEN); 286 json.put("expiration_time", Instant.now().getEpochSecond() + EXPIRATION_DURATION); 287 return json; 288 } 289 buildSamlResponse()290 private static GenericJson buildSamlResponse() { 291 GenericJson json = new GenericJson(); 292 json.setFactory(OAuth2Utils.JSON_FACTORY); 293 json.put("version", EXECUTABLE_SUPPORTED_MAX_VERSION); 294 json.put("success", true); 295 json.put("token_type", TOKEN_TYPE_SAML); 296 json.put("saml_response", "samlResponse"); 297 json.put("expiration_time", Instant.now().getEpochSecond() + EXPIRATION_DURATION); 298 return json; 299 } 300 buildErrorResponse()301 private static GenericJson buildErrorResponse() { 302 GenericJson json = new GenericJson(); 303 json.setFactory(OAuth2Utils.JSON_FACTORY); 304 json.put("version", EXECUTABLE_SUPPORTED_MAX_VERSION); 305 json.put("success", false); 306 json.put("code", "401"); 307 json.put("message", "Caller not authorized."); 308 return json; 309 } 310 } 311