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