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