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