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.SignedJWT; 017import com.nimbusds.jwt.ReadOnlyJWTClaimsSet; 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 074 * (draft-ietf-oauth-dyn-reg-17), sections 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 ReadOnlyJWTClaimsSet 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/draft-ietf-oauth-dyn-reg-17#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 throws SerializeException { 196 197 if (getEndpointURI() == null) 198 throw new SerializeException("The endpoint URI is not specified"); 199 200 URL endpointURL; 201 202 try { 203 endpointURL = getEndpointURI().toURL(); 204 205 } catch (MalformedURLException e) { 206 207 throw new SerializeException(e.getMessage(), e); 208 } 209 210 HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, endpointURL); 211 212 if (getAccessToken() != null) { 213 httpRequest.setAuthorization(getAccessToken().toAuthorizationHeader()); 214 } 215 216 httpRequest.setContentType(CommonContentTypes.APPLICATION_JSON); 217 218 JSONObject content = metadata.toJSONObject(); 219 220 if (softwareStatement != null) { 221 222 // Signed state check done in constructor 223 content.put("software_statement", softwareStatement.serialize()); 224 } 225 226 httpRequest.setQuery(content.toString()); 227 228 return httpRequest; 229 } 230 231 232 /** 233 * Parses a client registration request from the specified HTTP POST 234 * request. 235 * 236 * @param httpRequest The HTTP request. Must not be {@code null}. 237 * 238 * @return The client registration request. 239 * 240 * @throws ParseException If the HTTP request couldn't be parsed to a 241 * client registration request. 242 */ 243 public static ClientRegistrationRequest parse(final HTTPRequest httpRequest) 244 throws ParseException { 245 246 httpRequest.ensureMethod(HTTPRequest.Method.POST); 247 248 // Get the JSON object content 249 JSONObject jsonObject = httpRequest.getQueryAsJSONObject(); 250 251 // Extract the software statement if any 252 SignedJWT stmt = null; 253 254 if (jsonObject.containsKey("software_statement")) { 255 256 try { 257 stmt = SignedJWT.parse(JSONObjectUtils.getString(jsonObject, "software_statement")); 258 259 } catch (java.text.ParseException e) { 260 261 throw new ParseException("Invalid software statement JWT: " + e.getMessage()); 262 } 263 264 // Prevent the JWT from appearing in the metadata 265 jsonObject.remove("software_statement"); 266 } 267 268 // Parse the client metadata 269 ClientMetadata metadata = ClientMetadata.parse(jsonObject); 270 271 // Parse the optional bearer access token 272 BearerAccessToken accessToken = null; 273 274 String authzHeaderValue = httpRequest.getAuthorization(); 275 276 if (StringUtils.isNotBlank(authzHeaderValue)) 277 accessToken = BearerAccessToken.parse(authzHeaderValue); 278 279 try { 280 URI endpointURI = httpRequest.getURL().toURI(); 281 282 return new ClientRegistrationRequest(endpointURI, metadata, stmt, accessToken); 283 284 } catch (URISyntaxException | IllegalArgumentException e) { 285 286 throw new ParseException(e.getMessage(), e); 287 } 288 } 289}