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