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-11), 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}