001/* 002 * oauth2-oidc-sdk 003 * 004 * Copyright 2012-2021, 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.assurance.evidences.attachment; 019 020 021import java.io.IOException; 022import java.net.URI; 023import java.security.NoSuchAlgorithmException; 024import java.util.Objects; 025 026import net.jcip.annotations.Immutable; 027import net.minidev.json.JSONObject; 028 029import com.nimbusds.jose.util.Base64; 030import com.nimbusds.oauth2.sdk.ParseException; 031import com.nimbusds.oauth2.sdk.http.HTTPRequest; 032import com.nimbusds.oauth2.sdk.http.HTTPResponse; 033import com.nimbusds.oauth2.sdk.token.BearerAccessToken; 034import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; 035import com.nimbusds.oauth2.sdk.util.StringUtils; 036 037 038/** 039 * External attachment. Provides a {@link #retrieveContent method} to retrieve 040 * the remote content and verify its digest. 041 * 042 * <p>Related specifications: 043 * 044 * <ul> 045 * <li>OpenID Connect for Identity Assurance 1.0, section 5.1.2.2. 046 * </ul> 047 */ 048@Immutable 049public class ExternalAttachment extends Attachment { 050 051 052 /** 053 * The attachment URL. 054 */ 055 private final URI url; 056 057 058 /** 059 * Optional access token of type Bearer for retrieving the attachment. 060 */ 061 private final BearerAccessToken accessToken; 062 063 064 /** 065 * Number of seconds until the attachment becomes unavailable and / or 066 * the access token becomes invalid. Zero or negative is not specified. 067 */ 068 private final long expiresIn; 069 070 071 /** 072 * The cryptographic digest. 073 */ 074 private final Digest digest; 075 076 077 /** 078 * Creates a new external attachment. 079 * 080 * @param url The attachment URL. Must not be {@code null}. 081 * @param accessToken Optional access token of type Bearer for 082 * retrieving the attachment, {@code null} if none. 083 * @param expiresIn Number of seconds until the attachment becomes 084 * unavailable and / or the access token becomes 085 * invalid. Zero or negative if not specified. 086 * @param digest The cryptographic digest for the document 087 * content. Must not be {@code null}. 088 * @param description The description, {@code null} if not specified. 089 */ 090 public ExternalAttachment(final URI url, 091 final BearerAccessToken accessToken, 092 final long expiresIn, 093 final Digest digest, 094 final String description) { 095 super(AttachmentType.EXTERNAL, description); 096 097 Objects.requireNonNull(url); 098 this.url = url; 099 100 this.accessToken = accessToken; 101 102 this.expiresIn = expiresIn; 103 104 Objects.requireNonNull(digest); 105 this.digest = digest; 106 } 107 108 109 /** 110 * Returns the attachment URL. 111 * 112 * @return The attachment URL. 113 */ 114 public URI getURL() { 115 return url; 116 } 117 118 119 /** 120 * Returns the optional access token of type Bearer for retrieving the 121 * attachment. 122 * 123 * @return The bearer access token, {@code null} if not specified. 124 */ 125 public BearerAccessToken getBearerAccessToken() { 126 return accessToken; 127 } 128 129 130 /** 131 * Returns the number of seconds until the attachment becomes 132 * unavailable and / or the access token becomes invalid. 133 * 134 * @return The number of seconds until the attachment becomes 135 * unavailable and / or the access token becomes invalid. Zero 136 * or negative if not specified. 137 */ 138 public long getExpiresIn() { 139 return expiresIn; 140 } 141 142 143 /** 144 * Returns the cryptographic digest for the document content. 145 * 146 * @return The cryptographic digest. 147 */ 148 public Digest getDigest() { 149 return digest; 150 } 151 152 153 /** 154 * Retrieves the external attachment content and verifies its digest. 155 * 156 * @param httpConnectTimeout The HTTP connect timeout, in milliseconds. 157 * Zero implies no timeout. Must not be 158 * negative. 159 * @param httpReadTimeout The HTTP response read timeout, in 160 * milliseconds. Zero implies no timeout. 161 * Must not be negative. 162 * 163 * @return The retrieved content. 164 * 165 * @throws IOException If retrieval of the content failed. 166 * @throws NoSuchAlgorithmException If the hash algorithm for the 167 * digest isn't supported. 168 * @throws DigestMismatchException If the computed digest for the 169 * retrieved document doesn't match 170 * the expected. 171 */ 172 public Content retrieveContent(final int httpConnectTimeout, final int httpReadTimeout) 173 throws IOException, NoSuchAlgorithmException, DigestMismatchException { 174 175 HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.GET, getURL()); 176 if (getBearerAccessToken() != null) { 177 httpRequest.setAuthorization(getBearerAccessToken().toAuthorizationHeader()); 178 } 179 httpRequest.setConnectTimeout(httpConnectTimeout); 180 httpRequest.setReadTimeout(httpReadTimeout); 181 182 HTTPResponse httpResponse = httpRequest.send(); 183 try { 184 httpResponse.ensureStatusCode(200); 185 } catch (ParseException e) { 186 throw new IOException(e.getMessage(), e); 187 } 188 189 if (httpResponse.getEntityContentType() == null) { 190 throw new IOException("Missing Content-Type header in HTTP response: " + url); 191 } 192 193 if (StringUtils.isBlank(httpResponse.getContent())) { 194 throw new IOException("The HTTP response has no content: " + url); 195 } 196 197 // Trim whitespace to ensure digest gets computed over base64 text only 198 Base64 contentBase64 = new Base64(httpResponse.getContent().trim()); 199 200 if (! getDigest().matches(contentBase64)) { 201 throw new DigestMismatchException("The computed " + digest.getHashAlgorithm() + " digest for the retrieved content doesn't match the expected: " + getURL()); 202 } 203 204 return new Content(httpResponse.getEntityContentType(), contentBase64, getDescriptionString()); 205 } 206 207 208 @Override 209 public JSONObject toJSONObject() { 210 211 JSONObject jsonObject = super.toJSONObject(); 212 213 jsonObject.put("url", getURL().toString()); 214 if (getBearerAccessToken() != null) { 215 jsonObject.put("access_token", getBearerAccessToken().getValue()); 216 } 217 if (expiresIn > 0) { 218 jsonObject.put("expires_in", getExpiresIn()); 219 } 220 jsonObject.put("digest", getDigest().toJSONObject()); 221 return jsonObject; 222 } 223 224 225 @Override 226 public boolean equals(Object o) { 227 if (this == o) return true; 228 if (!(o instanceof ExternalAttachment)) return false; 229 if (!super.equals(o)) return false; 230 ExternalAttachment that = (ExternalAttachment) o; 231 return getExpiresIn() == that.getExpiresIn() && 232 url.equals(that.url) && 233 Objects.equals(accessToken, that.accessToken) && 234 getDigest().equals(that.getDigest()); 235 } 236 237 238 @Override 239 public int hashCode() { 240 return Objects.hash(super.hashCode(), url, accessToken, getExpiresIn(), getDigest()); 241 } 242 243 244 /** 245 * Parses an external attachment from the specified JSON object. 246 * 247 * @param jsonObject The JSON object. Must not be {@code null}. 248 * 249 * @return The external attachment. 250 * 251 * @throws ParseException If parsing failed. 252 */ 253 public static ExternalAttachment parse(final JSONObject jsonObject) 254 throws ParseException { 255 256 URI url = JSONObjectUtils.getURI(jsonObject, "url"); 257 258 long expiresIn = 0; 259 if (jsonObject.get("expires_in") != null) { 260 261 expiresIn = JSONObjectUtils.getLong(jsonObject, "expires_in"); 262 263 if (expiresIn < 1) { 264 throw new ParseException("The expires_in parameter must be a positive integer"); 265 } 266 } 267 268 BearerAccessToken accessToken = null; 269 if (jsonObject.get("access_token") != null) { 270 271 String tokenValue = JSONObjectUtils.getString(jsonObject, "access_token"); 272 273 if (expiresIn > 0) { 274 accessToken = new BearerAccessToken(tokenValue, expiresIn, null); 275 } else { 276 accessToken = new BearerAccessToken(tokenValue); 277 } 278 } 279 280 String description = JSONObjectUtils.getString(jsonObject, "desc", null); 281 282 Digest digest = Digest.parse(JSONObjectUtils.getJSONObject(jsonObject, "digest")); 283 284 return new ExternalAttachment(url, accessToken, expiresIn, digest, description); 285 } 286}