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 com.google.api.client.json.GenericJson; 35 import java.io.IOException; 36 import java.math.BigDecimal; 37 import java.time.Instant; 38 import javax.annotation.Nullable; 39 40 /** 41 * Encapsulates response values for the 3rd party executable response (e.g. OIDC, SAML, error 42 * responses). 43 */ 44 class ExecutableResponse { 45 46 private static final String SAML_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:saml2"; 47 48 private final int version; 49 private final boolean success; 50 51 @Nullable private Long expirationTime; 52 @Nullable private String tokenType; 53 @Nullable private String subjectToken; 54 @Nullable private String errorCode; 55 @Nullable private String errorMessage; 56 ExecutableResponse(GenericJson json)57 ExecutableResponse(GenericJson json) throws IOException { 58 if (!json.containsKey("version")) { 59 throw new PluggableAuthException( 60 "INVALID_EXECUTABLE_RESPONSE", "The executable response is missing the `version` field."); 61 } 62 63 if (!json.containsKey("success")) { 64 throw new PluggableAuthException( 65 "INVALID_EXECUTABLE_RESPONSE", "The executable response is missing the `success` field."); 66 } 67 68 this.version = parseIntField(json.get("version")); 69 this.success = (boolean) json.get("success"); 70 71 if (success) { 72 if (!json.containsKey("token_type")) { 73 throw new PluggableAuthException( 74 "INVALID_EXECUTABLE_RESPONSE", 75 "The executable response is missing the `token_type` field."); 76 } 77 78 this.tokenType = (String) json.get("token_type"); 79 80 if (json.containsKey("expiration_time")) { 81 this.expirationTime = parseLongField(json.get("expiration_time")); 82 } 83 84 if (SAML_SUBJECT_TOKEN_TYPE.equals(tokenType)) { 85 this.subjectToken = (String) json.get("saml_response"); 86 } else { 87 this.subjectToken = (String) json.get("id_token"); 88 } 89 if (subjectToken == null || subjectToken.isEmpty()) { 90 throw new PluggableAuthException( 91 "INVALID_EXECUTABLE_RESPONSE", 92 "The executable response does not contain a valid token."); 93 } 94 } else { 95 // Error response must contain both an error code and message. 96 this.errorCode = (String) json.get("code"); 97 this.errorMessage = (String) json.get("message"); 98 if (errorCode == null 99 || errorCode.isEmpty() 100 || errorMessage == null 101 || errorMessage.isEmpty()) { 102 throw new PluggableAuthException( 103 "INVALID_EXECUTABLE_RESPONSE", 104 "The executable response must contain `error` and `message` fields when unsuccessful."); 105 } 106 } 107 } 108 109 /** 110 * Returns the version of the executable output. Only version `1` is currently supported. This is 111 * useful for future changes to the expected output format. 112 * 113 * @return The version of the JSON output. 114 */ getVersion()115 int getVersion() { 116 return this.version; 117 } 118 119 /** 120 * Returns the status of the response. 121 * 122 * <p>When this is true, the response will contain the 3rd party token for a sign in / refresh 123 * operation. When this is false, the response should contain an additional error code and 124 * message. 125 * 126 * @return Whether the `success` field in the executable response is true. 127 */ isSuccessful()128 boolean isSuccessful() { 129 return this.success; 130 } 131 132 /** Returns true if the subject token is expired, false otherwise. */ isExpired()133 boolean isExpired() { 134 return this.expirationTime != null && this.expirationTime <= Instant.now().getEpochSecond(); 135 } 136 137 /** Returns whether the execution was successful and returned an unexpired token. */ isValid()138 boolean isValid() { 139 return isSuccessful() && !isExpired(); 140 } 141 142 /** Returns the subject token expiration time in seconds (Unix epoch time). */ 143 @Nullable getExpirationTime()144 Long getExpirationTime() { 145 return this.expirationTime; 146 } 147 148 /** 149 * Returns the 3rd party subject token type. 150 * 151 * <p>Possible valid values: 152 * 153 * <ul> 154 * <li>urn:ietf:params:oauth:token-type:id_token 155 * <li>urn:ietf:params:oauth:token-type:jwt 156 * <li>urn:ietf:params:oauth:token-type:saml2 157 * </ul> 158 * 159 * @return The 3rd party subject token type for success responses, null otherwise. 160 */ 161 @Nullable getTokenType()162 String getTokenType() { 163 return this.tokenType; 164 } 165 166 /** Returns the subject token if the execution was successful, null otherwise. */ 167 @Nullable getSubjectToken()168 String getSubjectToken() { 169 return this.subjectToken; 170 } 171 172 /** Returns the error code if the execution was unsuccessful, null otherwise. */ 173 @Nullable getErrorCode()174 String getErrorCode() { 175 return this.errorCode; 176 } 177 178 /** Returns the error message if the execution was unsuccessful, null otherwise. */ 179 @Nullable getErrorMessage()180 String getErrorMessage() { 181 return this.errorMessage; 182 } 183 parseIntField(Object field)184 private static int parseIntField(Object field) { 185 if (field instanceof String) { 186 return Integer.parseInt((String) field); 187 } 188 if (field instanceof BigDecimal) { 189 return ((BigDecimal) field).intValue(); 190 } 191 return (int) field; 192 } 193 parseLongField(Object field)194 private static long parseLongField(Object field) { 195 if (field instanceof String) { 196 return Long.parseLong((String) field); 197 } 198 if (field instanceof BigDecimal) { 199 return ((BigDecimal) field).longValue(); 200 } 201 return (long) field; 202 } 203 } 204