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.openid.connect.sdk;
019
020
021import java.util.Collections;
022import java.util.HashSet;
023import java.util.Set;
024
025import net.jcip.annotations.Immutable;
026
027import com.nimbusds.common.contenttype.ContentType;
028import com.nimbusds.oauth2.sdk.ErrorObject;
029import com.nimbusds.oauth2.sdk.ErrorResponse;
030import com.nimbusds.oauth2.sdk.ParseException;
031import com.nimbusds.oauth2.sdk.http.HTTPResponse;
032import com.nimbusds.oauth2.sdk.token.AccessTokenType;
033import com.nimbusds.oauth2.sdk.token.BearerTokenError;
034import com.nimbusds.oauth2.sdk.token.DPoPTokenError;
035import com.nimbusds.oauth2.sdk.token.TokenSchemeError;
036import com.nimbusds.oauth2.sdk.util.StringUtils;
037
038
039/**
040 * UserInfo error response.
041 *
042 * <p>Standard OAuth 2.0 Bearer Token errors:
043 *
044 * <ul>
045 *     <li>{@link com.nimbusds.oauth2.sdk.token.BearerTokenError#MISSING_TOKEN}
046 *     <li>{@link com.nimbusds.oauth2.sdk.token.BearerTokenError#INVALID_REQUEST}
047 *     <li>{@link com.nimbusds.oauth2.sdk.token.BearerTokenError#INVALID_TOKEN}
048 *     <li>{@link com.nimbusds.oauth2.sdk.token.BearerTokenError#INSUFFICIENT_SCOPE}
049 * </ul>
050 *
051 * <p>Example HTTP response:
052 *
053 * <pre>
054 * HTTP/1.1 401 Unauthorized
055 * WWW-Authenticate: Bearer realm="example.com",
056 *                   error="invalid_token",
057 *                   error_description="The access token expired"
058 * </pre>
059 *
060 * <p>Related specifications:
061 *
062 * <ul>
063 *     <li>OpenID Connect Core 1.0, section 5.3.3.
064 *     <li>OAuth 2.0 Bearer Token Usage (RFC 6750), section 3.1.
065 *     <li>OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer
066 *         (DPoP) (draft-ietf-oauth-dpop-03), section 7.
067 * </ul>
068 */
069@Immutable
070public class UserInfoErrorResponse
071        extends UserInfoResponse
072        implements ErrorResponse {
073
074
075        /**
076         * Gets the standard errors for a UserInfo error response.
077         *
078         * @return The standard errors, as a read-only set.
079         */
080        public static Set<BearerTokenError> getStandardErrors() {
081                
082                Set<BearerTokenError> stdErrors = new HashSet<>();
083                stdErrors.add(BearerTokenError.MISSING_TOKEN);
084                stdErrors.add(BearerTokenError.INVALID_REQUEST);
085                stdErrors.add(BearerTokenError.INVALID_TOKEN);
086                stdErrors.add(BearerTokenError.INSUFFICIENT_SCOPE);
087
088                return Collections.unmodifiableSet(stdErrors);
089        }
090
091
092        /**
093         * The underlying error.
094         */
095        private final ErrorObject error;
096
097
098        /**
099         * Creates a new UserInfo error response. No OAuth 2.0 token error /
100         * general error object is specified.
101         */
102        private UserInfoErrorResponse() {
103
104                error = null;
105        }
106        
107
108        /**
109         * Creates a new UserInfo error response indicating a bearer token
110         * error.
111         *
112         * @param error The OAuth 2.0 bearer token error. Should match one of 
113         *              the {@link #getStandardErrors standard errors} for a 
114         *              UserInfo error response. Must not be {@code null}.
115         */
116        public UserInfoErrorResponse(final BearerTokenError error) {
117
118                this((ErrorObject) error);
119        }
120        
121
122        /**
123         * Creates a new UserInfo error response indicating a DPoP token error.
124         *
125         * @param error The OAuth 2.0 DPoP token error. Should match one of
126         *              the {@link #getStandardErrors standard errors} for a
127         *              UserInfo error response. Must not be {@code null}.
128         */
129        public UserInfoErrorResponse(final DPoPTokenError error) {
130
131                this((ErrorObject) error);
132        }
133        
134        
135        /**
136         * Creates a new UserInfo error response indicating a general error.
137         *
138         * @param error The error. Must not be {@code null}.
139         */
140        public UserInfoErrorResponse(final ErrorObject error) {
141                
142                if (error == null)
143                        throw new IllegalArgumentException("The error must not be null");
144                
145                this.error = error;
146        }
147
148
149        @Override
150        public boolean indicatesSuccess() {
151
152                return false;
153        }
154
155
156        @Override
157        public ErrorObject getErrorObject() {
158
159                return error;
160        }
161
162
163        /**
164         * Returns the HTTP response for this UserInfo error response.
165         *
166         * <p>Example HTTP response:
167         *
168         * <pre>
169         * HTTP/1.1 401 Unauthorized
170         * WWW-Authenticate: Bearer realm="example.com",
171         *                   error="invalid_token",
172         *                   error_description="The access token expired"
173         * </pre>
174         *
175         * @return The HTTP response matching this UserInfo error response.
176         */
177        @Override
178        public HTTPResponse toHTTPResponse() {
179
180                HTTPResponse httpResponse;
181
182                if (error != null && error.getHTTPStatusCode() > 0) {
183                        httpResponse = new HTTPResponse(error.getHTTPStatusCode());
184                } else {
185                        httpResponse = new HTTPResponse(HTTPResponse.SC_BAD_REQUEST);
186                }
187
188                // Add the WWW-Authenticate header
189                if (error instanceof TokenSchemeError) {
190                        httpResponse.setWWWAuthenticate(((TokenSchemeError) error).toWWWAuthenticateHeader());
191                } else if (error != null){
192                        httpResponse.setEntityContentType(ContentType.APPLICATION_JSON);
193                        httpResponse.setContent(error.toJSONObject().toJSONString());
194                }
195
196                return httpResponse;
197        }
198
199
200        /**
201         * Parses a UserInfo error response from the specified HTTP response
202         * {@code WWW-Authenticate} header.
203         *
204         * @param wwwAuth The {@code WWW-Authenticate} header value to parse. 
205         *                Must not be {@code null}.
206         *
207         * @return The UserInfo error response.
208         *
209         * @throws ParseException If the {@code WWW-Authenticate} header value 
210         *                        couldn't be parsed to a UserInfo error 
211         *                        response.
212         */
213        public static UserInfoErrorResponse parse(final String wwwAuth)
214                throws ParseException {
215
216                BearerTokenError error = BearerTokenError.parse(wwwAuth);
217
218                return new UserInfoErrorResponse(error);
219        }
220        
221        
222        /**
223         * Parses a UserInfo error response from the specified HTTP response.
224         *
225         * <p>Note: The HTTP status code is not checked for matching the error
226         * code semantics.
227         *
228         * @param httpResponse The HTTP response to parse. Its status code must
229         *                     not be 200 (OK). Must not be {@code null}.
230         *
231         * @return The UserInfo error response.
232         *
233         * @throws ParseException If the HTTP response couldn't be parsed to a 
234         *                        UserInfo error response.
235         */
236        public static UserInfoErrorResponse parse(final HTTPResponse httpResponse)
237                throws ParseException {
238                
239                httpResponse.ensureStatusCodeNotOK();
240
241                String wwwAuth = httpResponse.getWWWAuthenticate();
242                
243                if (StringUtils.isNotBlank(wwwAuth)) {
244                        
245                        if (wwwAuth.toLowerCase().startsWith(AccessTokenType.BEARER.getValue().toLowerCase())) {
246                                
247                                // Bearer token error?
248                                try {
249                                        BearerTokenError bte = BearerTokenError.parse(wwwAuth);
250                                        
251                                        return new UserInfoErrorResponse(
252                                                new BearerTokenError(
253                                                        bte.getCode(),
254                                                        bte.getDescription(),
255                                                        httpResponse.getStatusCode(), // override HTTP status code
256                                                        bte.getURI(),
257                                                        bte.getRealm(),
258                                                        bte.getScope()));
259                                } catch (ParseException e) {
260                                        // Ignore parse exception for WWW-auth header and continue
261                                }
262                                
263                        } else if (wwwAuth.toLowerCase().startsWith(AccessTokenType.DPOP.getValue().toLowerCase())) {
264                                
265                                // Bearer token error?
266                                try {
267                                        DPoPTokenError dte = DPoPTokenError.parse(wwwAuth);
268                                        
269                                        return new UserInfoErrorResponse(
270                                                new DPoPTokenError(
271                                                        dte.getCode(),
272                                                        dte.getDescription(),
273                                                        httpResponse.getStatusCode(), // override HTTP status code
274                                                        dte.getURI(),
275                                                        dte.getRealm(),
276                                                        dte.getScope(),
277                                                        dte.getJWSAlgorithms()));
278                                } catch (ParseException e) {
279                                        // Ignore parse exception for WWW-auth header and continue
280                                }
281                        }
282                }
283                
284                // Other error?
285                return new UserInfoErrorResponse(ErrorObject.parse(httpResponse));
286        }
287}