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}