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 com.nimbusds.common.contenttype.ContentType;
022import com.nimbusds.oauth2.sdk.ErrorObject;
023import com.nimbusds.oauth2.sdk.ErrorResponse;
024import com.nimbusds.oauth2.sdk.ParseException;
025import com.nimbusds.oauth2.sdk.http.HTTPResponse;
026import com.nimbusds.oauth2.sdk.token.AccessTokenType;
027import com.nimbusds.oauth2.sdk.token.BearerTokenError;
028import com.nimbusds.oauth2.sdk.token.DPoPTokenError;
029import com.nimbusds.oauth2.sdk.token.TokenSchemeError;
030import com.nimbusds.oauth2.sdk.util.StringUtils;
031import net.jcip.annotations.Immutable;
032
033import java.util.Collections;
034import java.util.HashSet;
035import java.util.Objects;
036import java.util.Set;
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
064 *     <li>OAuth 2.0 Bearer Token Usage (RFC 6750)
065 *     <li>OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer
066 *         (DPoP) (RFC 9449)
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                this.error = Objects.requireNonNull(error);
143        }
144
145
146        @Override
147        public boolean indicatesSuccess() {
148
149                return false;
150        }
151
152
153        @Override
154        public ErrorObject getErrorObject() {
155
156                return error;
157        }
158
159
160        /**
161         * Returns the HTTP response for this UserInfo error response.
162         *
163         * <p>Example HTTP response:
164         *
165         * <pre>
166         * HTTP/1.1 401 Unauthorized
167         * WWW-Authenticate: Bearer realm="example.com",
168         *                   error="invalid_token",
169         *                   error_description="The access token expired"
170         * </pre>
171         *
172         * @return The HTTP response matching this UserInfo error response.
173         */
174        @Override
175        public HTTPResponse toHTTPResponse() {
176
177                HTTPResponse httpResponse;
178
179                if (error != null && error.getHTTPStatusCode() > 0) {
180                        httpResponse = new HTTPResponse(error.getHTTPStatusCode());
181                } else {
182                        httpResponse = new HTTPResponse(HTTPResponse.SC_BAD_REQUEST);
183                }
184
185                // Add the WWW-Authenticate header
186                if (error instanceof TokenSchemeError) {
187                        httpResponse.setWWWAuthenticate(((TokenSchemeError) error).toWWWAuthenticateHeader());
188                } else if (error != null){
189                        httpResponse.setEntityContentType(ContentType.APPLICATION_JSON);
190                        httpResponse.setBody(error.toJSONObject().toJSONString());
191                }
192
193                return httpResponse;
194        }
195
196
197        /**
198         * Parses a UserInfo error response from the specified HTTP response
199         * {@code WWW-Authenticate} header.
200         *
201         * @param wwwAuth The {@code WWW-Authenticate} header value to parse. 
202         *                Must not be {@code null}.
203         *
204         * @return The UserInfo error response.
205         *
206         * @throws ParseException If the {@code WWW-Authenticate} header value 
207         *                        couldn't be parsed to a UserInfo error 
208         *                        response.
209         */
210        public static UserInfoErrorResponse parse(final String wwwAuth)
211                throws ParseException {
212
213                BearerTokenError error = BearerTokenError.parse(wwwAuth);
214
215                return new UserInfoErrorResponse(error);
216        }
217        
218        
219        /**
220         * Parses a UserInfo error response from the specified HTTP response.
221         *
222         * <p>Note: The HTTP status code is not checked for matching the error
223         * code semantics.
224         *
225         * @param httpResponse The HTTP response to parse. Its status code must
226         *                     not be 200 (OK). Must not be {@code null}.
227         *
228         * @return The UserInfo error response.
229         *
230         * @throws ParseException If the HTTP response couldn't be parsed to a 
231         *                        UserInfo error response.
232         */
233        public static UserInfoErrorResponse parse(final HTTPResponse httpResponse)
234                throws ParseException {
235                
236                httpResponse.ensureStatusCodeNotOK();
237
238                String wwwAuth = httpResponse.getWWWAuthenticate();
239                
240                if (StringUtils.isNotBlank(wwwAuth)) {
241                        
242                        if (wwwAuth.toLowerCase().startsWith(AccessTokenType.BEARER.getValue().toLowerCase())) {
243                                
244                                // Bearer token error?
245                                try {
246                                        BearerTokenError bte = BearerTokenError.parse(wwwAuth);
247                                        
248                                        return new UserInfoErrorResponse(
249                                                new BearerTokenError(
250                                                        bte.getCode(),
251                                                        bte.getDescription(),
252                                                        httpResponse.getStatusCode(), // override HTTP status code
253                                                        bte.getURI(),
254                                                        bte.getRealm(),
255                                                        bte.getScope()));
256                                } catch (ParseException e) {
257                                        // Ignore parse exception for WWW-auth header and continue
258                                }
259                                
260                        } else if (wwwAuth.toLowerCase().startsWith(AccessTokenType.DPOP.getValue().toLowerCase())) {
261                                
262                                // Bearer token error?
263                                try {
264                                        DPoPTokenError dte = DPoPTokenError.parse(wwwAuth);
265                                        
266                                        return new UserInfoErrorResponse(
267                                                new DPoPTokenError(
268                                                        dte.getCode(),
269                                                        dte.getDescription(),
270                                                        httpResponse.getStatusCode(), // override HTTP status code
271                                                        dte.getURI(),
272                                                        dte.getRealm(),
273                                                        dte.getScope(),
274                                                        dte.getJWSAlgorithms()));
275                                } catch (ParseException e) {
276                                        // Ignore parse exception for WWW-auth header and continue
277                                }
278                        }
279                }
280                
281                // Other error?
282                return new UserInfoErrorResponse(ErrorObject.parse(httpResponse));
283        }
284}