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