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}