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.URI; 022 023import net.jcip.annotations.Immutable; 024import net.minidev.json.JSONObject; 025 026import com.nimbusds.common.contenttype.ContentType; 027import com.nimbusds.jose.JWSObject; 028import com.nimbusds.jwt.JWTClaimsSet; 029import com.nimbusds.jwt.SignedJWT; 030import com.nimbusds.oauth2.sdk.ParseException; 031import com.nimbusds.oauth2.sdk.ProtectedResourceRequest; 032import com.nimbusds.oauth2.sdk.SerializeException; 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; 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), sections 085 * 2 and 3.1. 086 * </ul> 087 */ 088@Immutable 089public class ClientRegistrationRequest extends ProtectedResourceRequest { 090 091 092 /** 093 * The client metadata. 094 */ 095 private final ClientMetadata metadata; 096 097 098 /** 099 * The optional software statement. 100 */ 101 private final SignedJWT softwareStatement; 102 103 104 /** 105 * Creates a new client registration request. 106 * 107 * @param uri The URI of the client registration endpoint. May 108 * be {@code null} if the {@link #toHTTPRequest()} 109 * method will not be used. 110 * @param metadata The client metadata. Must not be {@code null} and 111 * must specify one or more redirection URIs. 112 * @param accessToken An OAuth 2.0 Bearer access token for the request, 113 * {@code null} if none. 114 */ 115 public ClientRegistrationRequest(final URI uri, 116 final ClientMetadata metadata, 117 final BearerAccessToken accessToken) { 118 119 this(uri, metadata, null, accessToken); 120 } 121 122 123 /** 124 * Creates a new client registration request with an optional software 125 * statement. 126 * 127 * @param uri The URI of the client registration 128 * endpoint. May be {@code null} if the 129 * {@link #toHTTPRequest()} method will not be 130 * used. 131 * @param metadata The client metadata. Must not be 132 * {@code null} and must specify one or more 133 * redirection URIs. 134 * @param softwareStatement Optional software statement, as a signed 135 * JWT with an {@code iss} claim; {@code null} 136 * if not specified. 137 * @param accessToken An OAuth 2.0 Bearer access token for the 138 * request, {@code null} if none. 139 */ 140 public ClientRegistrationRequest(final URI uri, 141 final ClientMetadata metadata, 142 final SignedJWT softwareStatement, 143 final BearerAccessToken accessToken) { 144 145 super(uri, accessToken); 146 147 if (metadata == null) 148 throw new IllegalArgumentException("The client metadata must not be null"); 149 150 this.metadata = metadata; 151 152 153 if (softwareStatement != null) { 154 155 if (softwareStatement.getState() == JWSObject.State.UNSIGNED) { 156 throw new IllegalArgumentException("The software statement JWT must be signed"); 157 } 158 159 JWTClaimsSet claimsSet; 160 161 try { 162 claimsSet = softwareStatement.getJWTClaimsSet(); 163 164 } catch (java.text.ParseException e) { 165 166 throw new IllegalArgumentException("The software statement is not a valid signed JWT: " + e.getMessage()); 167 } 168 169 if (claimsSet.getIssuer() == null) { 170 171 // http://tools.ietf.org/html/rfc7591#section-2.3 172 throw new IllegalArgumentException("The software statement JWT must contain an 'iss' claim"); 173 } 174 175 } 176 177 this.softwareStatement = softwareStatement; 178 } 179 180 181 /** 182 * Gets the associated client metadata. 183 * 184 * @return The client metadata. 185 */ 186 public ClientMetadata getClientMetadata() { 187 188 return metadata; 189 } 190 191 192 /** 193 * Gets the software statement. 194 * 195 * @return The software statement, as a signed JWT with an {@code iss} 196 * claim; {@code null} if not specified. 197 */ 198 public SignedJWT getSoftwareStatement() { 199 200 return softwareStatement; 201 } 202 203 204 @Override 205 public HTTPRequest toHTTPRequest() { 206 207 if (getEndpointURI() == null) 208 throw new SerializeException("The endpoint URI is not specified"); 209 210 HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, getEndpointURI()); 211 212 if (getAccessToken() != null) { 213 httpRequest.setAuthorization(getAccessToken().toAuthorizationHeader()); 214 } 215 216 httpRequest.setEntityContentType(ContentType.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.setBody(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 return new ClientRegistrationRequest(httpRequest.getURI(), metadata, stmt, accessToken); 281 } catch (IllegalArgumentException e) { 282 throw new ParseException(e.getMessage(), e); 283 } 284 } 285}