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