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