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}