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