• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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