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 java.io.IOException; 022import java.net.URI; 023import java.net.URISyntaxException; 024import java.util.LinkedList; 025import java.util.List; 026 027import com.nimbusds.oauth2.sdk.ErrorObject; 028import com.nimbusds.oauth2.sdk.ParseException; 029import com.nimbusds.oauth2.sdk.WellKnownPathComposeStrategy; 030import com.nimbusds.oauth2.sdk.http.HTTPRequest; 031import com.nimbusds.oauth2.sdk.http.HTTPResponse; 032import com.nimbusds.oauth2.sdk.util.StringUtils; 033import com.nimbusds.openid.connect.sdk.federation.api.FetchEntityStatementRequest; 034import com.nimbusds.openid.connect.sdk.federation.api.FetchEntityStatementResponse; 035import com.nimbusds.openid.connect.sdk.federation.config.FederationEntityConfigurationRequest; 036import com.nimbusds.openid.connect.sdk.federation.config.FederationEntityConfigurationResponse; 037import com.nimbusds.openid.connect.sdk.federation.entities.EntityID; 038import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement; 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 /** 075 * Running list of the recorded HTTP requests. 076 */ 077 private final List<URI> recordedRequests = new LinkedList<>(); 078 079 080 /** 081 * Creates a new entity statement retriever using the default HTTP 082 * timeout settings. 083 */ 084 public DefaultEntityStatementRetriever() { 085 this(DEFAULT_HTTP_CONNECT_TIMEOUT_MS, DEFAULT_HTTP_READ_TIMEOUT_MS); 086 } 087 088 089 /** 090 * Creates a new entity statement retriever. 091 * 092 * @param httpConnectTimeoutMs The HTTP connect timeout in 093 * milliseconds, zero means timeout 094 * determined by the underlying HTTP client. 095 * @param httpReadTimeoutMs The HTTP read timeout in milliseconds, 096 * zero means timeout determined by the 097 * underlying HTTP client. 098 */ 099 public DefaultEntityStatementRetriever(final int httpConnectTimeoutMs, 100 final int httpReadTimeoutMs) { 101 this.httpConnectTimeoutMs = httpConnectTimeoutMs; 102 this.httpReadTimeoutMs = httpReadTimeoutMs; 103 } 104 105 106 /** 107 * Returns the configured HTTP connect timeout. 108 * 109 * @return The configured HTTP connect timeout in milliseconds, zero 110 * means timeout determined by the underlying HTTP client. 111 */ 112 public int getHTTPConnectTimeout() { 113 return httpConnectTimeoutMs; 114 } 115 116 117 /** 118 * Returns the configured HTTP read timeout. 119 * 120 * @return The configured HTTP read timeout in milliseconds, zero 121 * means timeout determined by the underlying HTTP client. 122 */ 123 public int getHTTPReadTimeout() { 124 return httpReadTimeoutMs; 125 } 126 127 128 void applyTimeouts(final HTTPRequest httpRequest) { 129 httpRequest.setConnectTimeout(httpConnectTimeoutMs); 130 httpRequest.setReadTimeout(httpReadTimeoutMs); 131 } 132 133 134 @Override 135 public EntityStatement fetchEntityConfiguration(final EntityID target) 136 throws ResolveException { 137 138 FederationEntityConfigurationRequest request = new FederationEntityConfigurationRequest(target); 139 HTTPRequest httpRequest = request.toHTTPRequest(); 140 applyTimeouts(httpRequest); 141 142 record(httpRequest); 143 144 HTTPResponse httpResponse; 145 try { 146 httpResponse = httpRequest.send(); 147 } catch (IOException e) { 148 throw new ResolveException("Couldn't retrieve entity configuration for " + httpRequest.getURL() + ": " + e.getMessage(), e); 149 } 150 151 if (StringUtils.isNotBlank(target.toURI().getPath()) && HTTPResponse.SC_NOT_FOUND == httpResponse.getStatusCode()) { 152 // We have a path in the entity ID URL, try infix strategy 153 request = new FederationEntityConfigurationRequest(target, WellKnownPathComposeStrategy.INFIX); 154 httpRequest = request.toHTTPRequest(); 155 applyTimeouts(httpRequest); 156 157 record(httpRequest); 158 159 try { 160 httpResponse = httpRequest.send(); 161 } catch (IOException e) { 162 throw new ResolveException("Couldn't retrieve entity configuration for " + httpRequest.getURL() + ": " + e.getMessage(), e); 163 } 164 } 165 166 FederationEntityConfigurationResponse response; 167 try { 168 response = FederationEntityConfigurationResponse.parse(httpResponse); 169 } catch (ParseException e) { 170 throw new ResolveException("Error parsing entity configuration response from " + httpRequest.getURL() + ": " + e.getMessage(), e); 171 } 172 173 if (! response.indicatesSuccess()) { 174 ErrorObject errorObject = response.toErrorResponse().getErrorObject(); 175 throw new ResolveException("Entity configuration error response from " + httpRequest.getURL() + ": " + 176 errorObject.getHTTPStatusCode() + 177 (errorObject.getCode() != null ? " " + errorObject.getCode() : ""), 178 errorObject); 179 } 180 181 return response.toSuccessResponse().getEntityStatement(); 182 } 183 184 185 @Override 186 public EntityStatement fetchEntityStatement(final URI federationAPIEndpoint, final EntityID issuer, final EntityID subject) 187 throws ResolveException { 188 189 FetchEntityStatementRequest request = new FetchEntityStatementRequest(federationAPIEndpoint, issuer, subject); 190 HTTPRequest httpRequest = request.toHTTPRequest(); 191 applyTimeouts(httpRequest); 192 193 record(httpRequest); 194 195 HTTPResponse httpResponse; 196 try { 197 httpResponse = httpRequest.send(); 198 } catch (IOException e) { 199 throw new ResolveException("Couldn't fetch entity statement from " + issuer + " at " + federationAPIEndpoint + ": " + e.getMessage(), e); 200 } 201 202 FetchEntityStatementResponse response; 203 try { 204 response = FetchEntityStatementResponse.parse(httpResponse); 205 } catch (ParseException e) { 206 throw new ResolveException("Error parsing entity statement response from " + issuer + " at " + federationAPIEndpoint + ": " + e.getMessage(), e); 207 } 208 209 if (! response.indicatesSuccess()) { 210 ErrorObject errorObject = response.toErrorResponse().getErrorObject(); 211 throw new ResolveException("Entity statement error response from " + issuer + " at " + federationAPIEndpoint + ": " + 212 errorObject.getHTTPStatusCode() + 213 (errorObject.getCode() != null ? " " + errorObject.getCode() : ""), 214 errorObject); 215 } 216 217 return response.toSuccessResponse().getEntityStatement(); 218 } 219 220 221 private void record(final HTTPRequest httpRequest) { 222 223 URI uri = null; 224 if (httpRequest.getQuery() == null) { 225 uri = httpRequest.getURI(); 226 } else { 227 try { 228 uri = new URI(httpRequest.getURL() + "?" + httpRequest.getQuery()); 229 } catch (URISyntaxException e) { 230 // ignore 231 } 232 } 233 234 recordedRequests.add(uri); 235 } 236 237 238 /** 239 * Returns the running list of the recorded HTTP requests. 240 * 241 * @return The HTTP request URIs (with query parameters), empty if 242 * none. 243 */ 244 public List<URI> getRecordedRequests() { 245 return recordedRequests; 246 } 247}