001/* 002 * oauth2-oidc-sdk 003 * 004 * Copyright 2012-2020, Connect2id Ltd and contributors. 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use 007 * this file except in compliance with the License. You may obtain a copy of the 008 * License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software distributed 013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the 015 * specific language governing permissions and limitations under the License. 016 */ 017 018package com.nimbusds.openid.connect.sdk.federation.trust; 019 020 021import com.nimbusds.oauth2.sdk.ErrorObject; 022import com.nimbusds.oauth2.sdk.ParseException; 023import com.nimbusds.oauth2.sdk.WellKnownPathComposeStrategy; 024import com.nimbusds.oauth2.sdk.http.HTTPRequest; 025import com.nimbusds.oauth2.sdk.http.HTTPRequestSender; 026import com.nimbusds.oauth2.sdk.http.HTTPResponse; 027import com.nimbusds.oauth2.sdk.util.StringUtils; 028import com.nimbusds.openid.connect.sdk.federation.api.FetchEntityStatementRequest; 029import com.nimbusds.openid.connect.sdk.federation.api.FetchEntityStatementResponse; 030import com.nimbusds.openid.connect.sdk.federation.config.FederationEntityConfigurationRequest; 031import com.nimbusds.openid.connect.sdk.federation.config.FederationEntityConfigurationResponse; 032import com.nimbusds.openid.connect.sdk.federation.entities.EntityID; 033import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement; 034 035import java.io.IOException; 036import java.net.URI; 037import java.util.LinkedList; 038import java.util.List; 039 040 041/** 042 * The default entity statement retriever for resolving trust chains. Supports 043 * the {@link WellKnownPathComposeStrategy#POSTFIX postfix} and 044 * {@link WellKnownPathComposeStrategy#INFIX infix} well-known path composition 045 * strategies. 046 */ 047public class DefaultEntityStatementRetriever implements EntityStatementRetriever { 048 049 050 /** 051 * The HTTP connect timeout in milliseconds. 052 */ 053 private final int httpConnectTimeoutMs; 054 055 056 /** 057 * The HTTP read timeout in milliseconds. 058 */ 059 private final int httpReadTimeoutMs; 060 061 062 /** 063 * The default HTTP connect timeout in milliseconds. 064 */ 065 public static final int DEFAULT_HTTP_CONNECT_TIMEOUT_MS = 1000; 066 067 068 /** 069 * The default HTTP read timeout in milliseconds. 070 */ 071 public static final int DEFAULT_HTTP_READ_TIMEOUT_MS = 1000; 072 073 074 private final HTTPRequestSender httpRequestSender; 075 076 077 /** 078 * Running list of the recorded HTTP requests. 079 */ 080 private final List<URI> recordedRequests = new LinkedList<>(); 081 082 083 /** 084 * Creates a new entity statement retriever using the default HTTP 085 * timeout settings. 086 */ 087 public DefaultEntityStatementRetriever() { 088 this(DEFAULT_HTTP_CONNECT_TIMEOUT_MS, DEFAULT_HTTP_READ_TIMEOUT_MS); 089 } 090 091 092 /** 093 * Creates a new entity statement retriever. 094 * 095 * @param httpConnectTimeoutMs The HTTP connect timeout in 096 * milliseconds, zero means timeout 097 * determined by the underlying HTTP client. 098 * @param httpReadTimeoutMs The HTTP read timeout in milliseconds, 099 * zero means timeout determined by the 100 * underlying HTTP client. 101 */ 102 public DefaultEntityStatementRetriever(final int httpConnectTimeoutMs, 103 final int httpReadTimeoutMs) { 104 this.httpConnectTimeoutMs = httpConnectTimeoutMs; 105 this.httpReadTimeoutMs = httpReadTimeoutMs; 106 httpRequestSender = null; 107 } 108 109 110 /** 111 * Creates a new entity statement retriever. 112 * 113 * @param httpRequestSender The HTTP request sender to use. Must not be 114 * {@code null}. 115 */ 116 public DefaultEntityStatementRetriever(final HTTPRequestSender httpRequestSender) { 117 118 if (httpRequestSender == null) { 119 throw new IllegalArgumentException("The HTTP request sender must not be null"); 120 } 121 this.httpRequestSender = httpRequestSender; 122 httpConnectTimeoutMs = 0; 123 httpReadTimeoutMs = 0; 124 } 125 126 127 /** 128 * Returns the configured HTTP connect timeout. 129 * 130 * @return The configured HTTP connect timeout in milliseconds, zero 131 * means timeout determined by the underlying HTTP client. 132 */ 133 public int getHTTPConnectTimeout() { 134 return httpConnectTimeoutMs; 135 } 136 137 138 /** 139 * Returns the configured HTTP read timeout. 140 * 141 * @return The configured HTTP read timeout in milliseconds, zero 142 * means timeout determined by the underlying HTTP client. 143 */ 144 public int getHTTPReadTimeout() { 145 return httpReadTimeoutMs; 146 } 147 148 149 void applyTimeouts(final HTTPRequest httpRequest) { 150 httpRequest.setConnectTimeout(httpConnectTimeoutMs); 151 httpRequest.setReadTimeout(httpReadTimeoutMs); 152 } 153 154 155 @Override 156 public EntityStatement fetchEntityConfiguration(final EntityID target) 157 throws ResolveException { 158 159 FederationEntityConfigurationRequest request = new FederationEntityConfigurationRequest(target); 160 HTTPRequest httpRequest = request.toHTTPRequest(); 161 applyTimeouts(httpRequest); 162 163 record(httpRequest); 164 165 HTTPResponse httpResponse; 166 try { 167 if (httpRequestSender != null) { 168 httpResponse = httpRequest.send(httpRequestSender); 169 } else { 170 httpResponse = httpRequest.send(); 171 } 172 } catch (IOException e) { 173 throw new ResolveException("Couldn't retrieve entity configuration for " + httpRequest.getURL() + ": " + e.getMessage(), e); 174 } 175 176 if (StringUtils.isNotBlank(target.toURI().getPath()) && HTTPResponse.SC_NOT_FOUND == httpResponse.getStatusCode()) { 177 // We have a path in the entity ID URL, try infix strategy 178 request = new FederationEntityConfigurationRequest(target, WellKnownPathComposeStrategy.INFIX); 179 httpRequest = request.toHTTPRequest(); 180 applyTimeouts(httpRequest); 181 182 record(httpRequest); 183 184 try { 185 httpResponse = httpRequest.send(); 186 } catch (IOException e) { 187 throw new ResolveException("Couldn't retrieve entity configuration for " + httpRequest.getURL() + ": " + e.getMessage(), e); 188 } 189 } 190 191 FederationEntityConfigurationResponse response; 192 try { 193 response = FederationEntityConfigurationResponse.parse(httpResponse); 194 } catch (ParseException e) { 195 throw new ResolveException("Error parsing entity configuration response from " + httpRequest.getURL() + ": " + e.getMessage(), e); 196 } 197 198 if (! response.indicatesSuccess()) { 199 ErrorObject errorObject = response.toErrorResponse().getErrorObject(); 200 throw new ResolveException("Entity configuration error response from " + httpRequest.getURL() + ": " + 201 errorObject.getHTTPStatusCode() + 202 (errorObject.getCode() != null ? " " + errorObject.getCode() : ""), 203 errorObject); 204 } 205 206 return response.toSuccessResponse().getEntityStatement(); 207 } 208 209 210 @Override 211 public EntityStatement fetchEntityStatement(final URI federationAPIEndpoint, final EntityID issuer, final EntityID subject) 212 throws ResolveException { 213 214 FetchEntityStatementRequest request = new FetchEntityStatementRequest(federationAPIEndpoint, issuer, subject); 215 HTTPRequest httpRequest = request.toHTTPRequest(); 216 applyTimeouts(httpRequest); 217 218 record(httpRequest); 219 220 HTTPResponse httpResponse; 221 try { 222 httpResponse = httpRequest.send(); 223 } catch (IOException e) { 224 throw new ResolveException("Couldn't fetch entity statement from " + issuer + " at " + federationAPIEndpoint + ": " + e.getMessage(), e); 225 } 226 227 FetchEntityStatementResponse response; 228 try { 229 response = FetchEntityStatementResponse.parse(httpResponse); 230 } catch (ParseException e) { 231 throw new ResolveException("Error parsing entity statement response from " + issuer + " at " + federationAPIEndpoint + ": " + e.getMessage(), e); 232 } 233 234 if (! response.indicatesSuccess()) { 235 ErrorObject errorObject = response.toErrorResponse().getErrorObject(); 236 throw new ResolveException("Entity statement error response from " + issuer + " at " + federationAPIEndpoint + ": " + 237 errorObject.getHTTPStatusCode() + 238 (errorObject.getCode() != null ? " " + errorObject.getCode() : ""), 239 errorObject); 240 } 241 242 return response.toSuccessResponse().getEntityStatement(); 243 } 244 245 246 private void record(final HTTPRequest httpRequest) { 247 248 recordedRequests.add(httpRequest.getURI()); 249 } 250 251 252 /** 253 * Returns the running list of the recorded HTTP requests. 254 * 255 * @return The HTTP request URIs (with query parameters), empty if 256 * none. 257 */ 258 public List<URI> getRecordedRequests() { 259 return recordedRequests; 260 } 261}