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 com.nimbusds.common.contenttype.ContentType;
022import com.nimbusds.jose.JWSObject;
023import com.nimbusds.jwt.JWTClaimsSet;
024import com.nimbusds.jwt.SignedJWT;
025import com.nimbusds.oauth2.sdk.ParseException;
026import com.nimbusds.oauth2.sdk.ProtectedResourceRequest;
027import com.nimbusds.oauth2.sdk.SerializeException;
028import com.nimbusds.oauth2.sdk.http.HTTPRequest;
029import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
030import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
031import com.nimbusds.oauth2.sdk.util.StringUtils;
032import net.jcip.annotations.Immutable;
033import net.minidev.json.JSONObject;
034
035import java.net.URI;
036import java.util.Objects;
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)
085 * </ul>
086 */
087@Immutable
088public class ClientRegistrationRequest extends ProtectedResourceRequest {
089
090
091        /**
092         * The client metadata.
093         */
094        private final ClientMetadata metadata;
095
096
097        /**
098         * The optional software statement.
099         */
100        private final SignedJWT softwareStatement;
101
102
103        /**
104         * Creates a new client registration request.
105         *
106         * @param endpoint    The URI of the client registration endpoint. May
107         *                    be {@code null} if the {@link #toHTTPRequest()}
108         *                    method is not going to be used.
109         * @param metadata    The client metadata. Must not be {@code null} and 
110         *                    must specify one or more redirection URIs.
111         * @param accessToken An OAuth 2.0 Bearer access token for the request, 
112         *                    {@code null} if none.
113         */
114        public ClientRegistrationRequest(final URI endpoint,
115                                         final ClientMetadata metadata, 
116                                         final BearerAccessToken accessToken) {
117
118                this(endpoint, metadata, null, accessToken);
119        }
120
121
122        /**
123         * Creates a new client registration request with an optional software
124         * statement.
125         *
126         * @param endpoint          The URI of the client registration
127         *                          endpoint. May be {@code null} if the
128         *                          {@link #toHTTPRequest()} method is not
129         *                          going to be used.
130         * @param metadata          The client metadata. Must not be
131         *                          {@code null} and must specify one or more
132         *                          redirection URIs.
133         * @param softwareStatement Optional software statement, as a signed
134         *                          JWT with an {@code iss} claim; {@code null}
135         *                          if not specified.
136         * @param accessToken       An OAuth 2.0 Bearer access token for the
137         *                          request, {@code null} if none.
138         */
139        public ClientRegistrationRequest(final URI endpoint,
140                                         final ClientMetadata metadata,
141                                         final SignedJWT softwareStatement,
142                                         final BearerAccessToken accessToken) {
143
144                super(endpoint, accessToken);
145                this.metadata = Objects.requireNonNull(metadata);
146
147
148                if (softwareStatement != null) {
149
150                        if (softwareStatement.getState() == JWSObject.State.UNSIGNED) {
151                                throw new IllegalArgumentException("The software statement JWT must be signed");
152                        }
153
154                        JWTClaimsSet claimsSet;
155
156                        try {
157                                claimsSet = softwareStatement.getJWTClaimsSet();
158
159                        } catch (java.text.ParseException e) {
160
161                                throw new IllegalArgumentException("The software statement is not a valid signed JWT: " + e.getMessage());
162                        }
163
164                        if (claimsSet.getIssuer() == null) {
165
166                                // http://tools.ietf.org/html/rfc7591#section-2.3
167                                throw new IllegalArgumentException("The software statement JWT must contain an 'iss' claim");
168                        }
169
170                }
171
172                this.softwareStatement = softwareStatement;
173        }
174
175
176        /**
177         * Gets the associated client metadata.
178         *
179         * @return The client metadata.
180         */
181        public ClientMetadata getClientMetadata() {
182
183                return metadata;
184        }
185
186
187        /**
188         * Gets the software statement.
189         *
190         * @return The software statement, as a signed JWT with an {@code iss}
191         *         claim; {@code null} if not specified.
192         */
193        public SignedJWT getSoftwareStatement() {
194
195                return softwareStatement;
196        }
197
198
199        @Override
200        public HTTPRequest toHTTPRequest() {
201                
202                if (getEndpointURI() == null)
203                        throw new SerializeException("The endpoint URI is not specified");
204
205                HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, getEndpointURI());
206
207                if (getAccessToken() != null) {
208                        httpRequest.setAuthorization(getAccessToken().toAuthorizationHeader());
209                }
210
211                httpRequest.setEntityContentType(ContentType.APPLICATION_JSON);
212
213                JSONObject content = metadata.toJSONObject();
214
215                if (softwareStatement != null) {
216
217                        // Signed state check done in constructor
218                        content.put("software_statement", softwareStatement.serialize());
219                }
220
221                httpRequest.setBody(content.toString());
222
223                return httpRequest;
224        }
225
226
227        /**
228         * Parses a client registration request from the specified HTTP POST 
229         * request.
230         *
231         * @param httpRequest The HTTP request. Must not be {@code null}.
232         *
233         * @return The client registration request.
234         *
235         * @throws ParseException If the HTTP request couldn't be parsed to a 
236         *                        client registration request.
237         */
238        public static ClientRegistrationRequest parse(final HTTPRequest httpRequest)
239                throws ParseException {
240
241                httpRequest.ensureMethod(HTTPRequest.Method.POST);
242
243                // Get the JSON object content
244                JSONObject jsonObject = httpRequest.getBodyAsJSONObject();
245
246                // Extract the software statement if any
247                SignedJWT stmt = null;
248
249                if (jsonObject.containsKey("software_statement")) {
250
251                        try {
252                                stmt = SignedJWT.parse(JSONObjectUtils.getString(jsonObject, "software_statement"));
253
254                        } catch (java.text.ParseException e) {
255
256                                throw new ParseException("Invalid software statement JWT: " + e.getMessage());
257                        }
258
259                        // Prevent the JWT from appearing in the metadata
260                        jsonObject.remove("software_statement");
261                }
262
263                // Parse the client metadata
264                ClientMetadata metadata = ClientMetadata.parse(jsonObject);
265
266                // Parse the optional bearer access token
267                BearerAccessToken accessToken = null;
268                
269                String authzHeaderValue = httpRequest.getAuthorization();
270                
271                if (StringUtils.isNotBlank(authzHeaderValue))
272                        accessToken = BearerAccessToken.parse(authzHeaderValue);
273                
274                try {
275                        return new ClientRegistrationRequest(httpRequest.getURI(), metadata, stmt, accessToken);
276                } catch (IllegalArgumentException e) {
277                        throw new ParseException(e.getMessage(), e);
278                }
279        }
280}