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.SignedJWT;
017import com.nimbusds.jwt.ReadOnlyJWTClaimsSet;
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
074 *         (draft-ietf-oauth-dyn-reg-18), sections 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                        ReadOnlyJWTClaimsSet 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/draft-ietf-oauth-dyn-reg-18#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                throws SerializeException {
196                
197                if (getEndpointURI() == null)
198                        throw new SerializeException("The endpoint URI is not specified");
199
200                URL endpointURL;
201
202                try {
203                        endpointURL = getEndpointURI().toURL();
204
205                } catch (MalformedURLException e) {
206
207                        throw new SerializeException(e.getMessage(), e);
208                }
209        
210                HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, endpointURL);
211
212                if (getAccessToken() != null) {
213                        httpRequest.setAuthorization(getAccessToken().toAuthorizationHeader());
214                }
215
216                httpRequest.setContentType(CommonContentTypes.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                        URI endpointURI = httpRequest.getURL().toURI();
281
282                        return new ClientRegistrationRequest(endpointURI, metadata, stmt, accessToken);
283
284                } catch (URISyntaxException | IllegalArgumentException e) {
285
286                        throw new ParseException(e.getMessage(), e);
287                }
288        }
289}