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