001package com.nimbusds.oauth2.sdk.client; 002 003 004import java.net.MalformedURLException; 005import java.net.URI; 006import java.net.URISyntaxException; 007import java.net.URL; 008 009import net.jcip.annotations.Immutable; 010 011import org.apache.commons.lang3.StringUtils; 012 013import net.minidev.json.JSONObject; 014 015import com.nimbusds.jose.JWSObject; 016import com.nimbusds.jwt.JWTClaimsSet; 017import com.nimbusds.jwt.SignedJWT; 018 019import com.nimbusds.oauth2.sdk.ParseException; 020import com.nimbusds.oauth2.sdk.ProtectedResourceRequest; 021import com.nimbusds.oauth2.sdk.SerializeException; 022import com.nimbusds.oauth2.sdk.http.CommonContentTypes; 023import com.nimbusds.oauth2.sdk.http.HTTPRequest; 024import com.nimbusds.oauth2.sdk.token.BearerAccessToken; 025import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; 026 027 028/** 029 * Client registration request. 030 * 031 * <p>Example HTTP request: 032 * 033 * <pre> 034 * POST /register HTTP/1.1 035 * Content-Type: application/json 036 * Accept: application/json 037 * Authorization: Bearer ey23f2.adfj230.af32-developer321 038 * Host: server.example.com 039 * 040 * { 041 * "redirect_uris" : [ "https://client.example.org/callback", 042 * "https://client.example.org/callback2" ], 043 * "client_name" : "My Example Client", 044 * "client_name#ja-Jpan-JP" : "\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u540D", 045 * "token_endpoint_auth_method" : "client_secret_basic", 046 * "scope" : "read write dolphin", 047 * "logo_uri" : "https://client.example.org/logo.png", 048 * "jwks_uri" : "https://client.example.org/my_public_keys.jwks" 049 * } 050 * </pre> 051 * 052 * <p>Example HTTP request with a software statement: 053 * 054 * <pre> 055 * POST /register HTTP/1.1 056 * Content-Type: application/json 057 * Accept: application/json 058 * Host: server.example.com 059 * 060 * { 061 * "redirect_uris" : [ "https://client.example.org/callback", 062 * "https://client.example.org/callback2" ], 063 * "software_statement" : "eyJhbGciOiJFUzI1NiJ9.eyJpc3Mi[...omitted for brevity...]", 064 * "scope" : "read write", 065 * "example_extension_parameter" : "example_value" 066 * } 067 * 068 * </pre> 069 * 070 * <p>Related specifications: 071 * 072 * <ul> 073 * <li>OAuth 2.0 Dynamic Client Registration Protocol (RFC 7591), sections 074 * 2 and 3.1. 075 * </ul> 076 */ 077@Immutable 078public class ClientRegistrationRequest extends ProtectedResourceRequest { 079 080 081 /** 082 * The client metadata. 083 */ 084 private final ClientMetadata metadata; 085 086 087 /** 088 * The optional software statement. 089 */ 090 private final SignedJWT softwareStatement; 091 092 093 /** 094 * Creates a new client registration request. 095 * 096 * @param uri The URI of the client registration endpoint. May 097 * be {@code null} if the {@link #toHTTPRequest()} 098 * method will not be used. 099 * @param metadata The client metadata. Must not be {@code null} and 100 * must specify one or more redirection URIs. 101 * @param accessToken An OAuth 2.0 Bearer access token for the request, 102 * {@code null} if none. 103 */ 104 public ClientRegistrationRequest(final URI uri, 105 final ClientMetadata metadata, 106 final BearerAccessToken accessToken) { 107 108 this(uri, metadata, null, accessToken); 109 } 110 111 112 /** 113 * Creates a new client registration request with an optional software 114 * statement. 115 * 116 * @param uri The URI of the client registration 117 * endpoint. May be {@code null} if the 118 * {@link #toHTTPRequest()} method will not be 119 * used. 120 * @param metadata The client metadata. Must not be 121 * {@code null} and must specify one or more 122 * redirection URIs. 123 * @param softwareStatement Optional software statement, as a signed 124 * JWT with an {@code iss} claim; {@code null} 125 * if not specified. 126 * @param accessToken An OAuth 2.0 Bearer access token for the 127 * request, {@code null} if none. 128 */ 129 public ClientRegistrationRequest(final URI uri, 130 final ClientMetadata metadata, 131 final SignedJWT softwareStatement, 132 final BearerAccessToken accessToken) { 133 134 super(uri, accessToken); 135 136 if (metadata == null) 137 throw new IllegalArgumentException("The client metadata must not be null"); 138 139 this.metadata = metadata; 140 141 142 if (softwareStatement != null) { 143 144 if (softwareStatement.getState() == JWSObject.State.UNSIGNED) { 145 throw new IllegalArgumentException("The software statement JWT must be signed"); 146 } 147 148 JWTClaimsSet claimsSet; 149 150 try { 151 claimsSet = softwareStatement.getJWTClaimsSet(); 152 153 } catch (java.text.ParseException e) { 154 155 throw new IllegalArgumentException("The software statement is not a valid signed JWT: " + e.getMessage()); 156 } 157 158 if (claimsSet.getIssuer() == null) { 159 160 // http://tools.ietf.org/html/rfc7591#section-2.3 161 throw new IllegalArgumentException("The software statement JWT must contain an 'iss' claim"); 162 } 163 164 } 165 166 this.softwareStatement = softwareStatement; 167 } 168 169 170 /** 171 * Gets the associated client metadata. 172 * 173 * @return The client metadata. 174 */ 175 public ClientMetadata getClientMetadata() { 176 177 return metadata; 178 } 179 180 181 /** 182 * Gets the software statement. 183 * 184 * @return The software statement, as a signed JWT with an {@code iss} 185 * claim; {@code null} if not specified. 186 */ 187 public SignedJWT getSoftwareStatement() { 188 189 return softwareStatement; 190 } 191 192 193 @Override 194 public HTTPRequest toHTTPRequest() { 195 196 if (getEndpointURI() == null) 197 throw new SerializeException("The endpoint URI is not specified"); 198 199 URL endpointURL; 200 201 try { 202 endpointURL = getEndpointURI().toURL(); 203 204 } catch (MalformedURLException e) { 205 206 throw new SerializeException(e.getMessage(), e); 207 } 208 209 HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, endpointURL); 210 211 if (getAccessToken() != null) { 212 httpRequest.setAuthorization(getAccessToken().toAuthorizationHeader()); 213 } 214 215 httpRequest.setContentType(CommonContentTypes.APPLICATION_JSON); 216 217 JSONObject content = metadata.toJSONObject(); 218 219 if (softwareStatement != null) { 220 221 // Signed state check done in constructor 222 content.put("software_statement", softwareStatement.serialize()); 223 } 224 225 httpRequest.setQuery(content.toString()); 226 227 return httpRequest; 228 } 229 230 231 /** 232 * Parses a client registration request from the specified HTTP POST 233 * request. 234 * 235 * @param httpRequest The HTTP request. Must not be {@code null}. 236 * 237 * @return The client registration request. 238 * 239 * @throws ParseException If the HTTP request couldn't be parsed to a 240 * client registration request. 241 */ 242 public static ClientRegistrationRequest parse(final HTTPRequest httpRequest) 243 throws ParseException { 244 245 httpRequest.ensureMethod(HTTPRequest.Method.POST); 246 247 // Get the JSON object content 248 JSONObject jsonObject = httpRequest.getQueryAsJSONObject(); 249 250 // Extract the software statement if any 251 SignedJWT stmt = null; 252 253 if (jsonObject.containsKey("software_statement")) { 254 255 try { 256 stmt = SignedJWT.parse(JSONObjectUtils.getString(jsonObject, "software_statement")); 257 258 } catch (java.text.ParseException e) { 259 260 throw new ParseException("Invalid software statement JWT: " + e.getMessage()); 261 } 262 263 // Prevent the JWT from appearing in the metadata 264 jsonObject.remove("software_statement"); 265 } 266 267 // Parse the client metadata 268 ClientMetadata metadata = ClientMetadata.parse(jsonObject); 269 270 // Parse the optional bearer access token 271 BearerAccessToken accessToken = null; 272 273 String authzHeaderValue = httpRequest.getAuthorization(); 274 275 if (StringUtils.isNotBlank(authzHeaderValue)) 276 accessToken = BearerAccessToken.parse(authzHeaderValue); 277 278 try { 279 URI endpointURI = httpRequest.getURL().toURI(); 280 281 return new ClientRegistrationRequest(endpointURI, metadata, stmt, accessToken); 282 283 } catch (URISyntaxException | IllegalArgumentException e) { 284 285 throw new ParseException(e.getMessage(), e); 286 } 287 } 288}