KEYCLOAK-1886 Added cors headers to errors in token endpoint

This commit is contained in:
Stian Thorgersen 2017-11-09 15:42:18 +01:00 committed by Stian Thorgersen
parent 04ad634986
commit 4295f4ec31
6 changed files with 261 additions and 108 deletions

View file

@ -62,6 +62,7 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
@ -140,6 +141,8 @@ public class TokenEndpoint {
private String grantType;
private Cors cors;
public TokenEndpoint(TokenManager tokenManager, RealmModel realm, EventBuilder event) {
this.tokenManager = tokenManager;
this.realm = realm;
@ -148,6 +151,8 @@ public class TokenEndpoint {
@POST
public Response processGrantRequest() {
cors = Cors.add(request).auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS);
formParams = request.getDecodedFormParameters();
grantType = formParams.getFirst(OIDCLoginProtocol.GRANT_TYPE_PARAM);
@ -191,13 +196,13 @@ public class TokenEndpoint {
private void checkSsl() {
if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "HTTPS required", Response.Status.FORBIDDEN);
throw new CorsErrorResponseException(cors.allowAllOrigins(), OAuthErrorException.INVALID_REQUEST, "HTTPS required", Response.Status.FORBIDDEN);
}
}
private void checkRealm() {
if (!realm.isEnabled()) {
throw new ErrorResponseException("access_denied", "Realm not enabled", Response.Status.FORBIDDEN);
throw new CorsErrorResponseException(cors.allowAllOrigins(), "access_denied", "Realm not enabled", Response.Status.FORBIDDEN);
}
}
@ -206,8 +211,10 @@ public class TokenEndpoint {
client = clientAuth.getClient();
clientAuthAttributes = clientAuth.getClientAuthAttributes();
cors.allowedOrigins(uriInfo, client);
if (client.isBearerOnly()) {
throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Bearer-only not allowed", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_CLIENT, "Bearer-only not allowed", Response.Status.BAD_REQUEST);
}
@ -215,7 +222,7 @@ public class TokenEndpoint {
private void checkGrantType() {
if (grantType == null) {
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Missing form parameter: " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Missing form parameter: " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST);
}
if (grantType.equals(OAuth2Constants.AUTHORIZATION_CODE)) {
@ -235,7 +242,7 @@ public class TokenEndpoint {
action = Action.TOKEN_EXCHANGE;
} else {
throw new ErrorResponseException(Errors.INVALID_REQUEST, "Invalid " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, Errors.INVALID_REQUEST, "Invalid " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST);
}
event.detail(Details.GRANT_TYPE, grantType);
@ -245,7 +252,7 @@ public class TokenEndpoint {
String code = formParams.getFirst(OAuth2Constants.CODE);
if (code == null) {
event.error(Errors.INVALID_CODE);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST);
}
ClientSessionCode.ParseResult<AuthenticatedClientSessionModel> parseResult = ClientSessionCode.parseResult(code, session, realm, event, AuthenticatedClientSessionModel.class);
@ -259,57 +266,57 @@ public class TokenEndpoint {
event.error(Errors.INVALID_CODE);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Code not valid", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Code not valid", Response.Status.BAD_REQUEST);
}
AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
if (parseResult.isExpiredToken()) {
event.error(Errors.EXPIRED_CODE);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Code is expired", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Code is expired", Response.Status.BAD_REQUEST);
}
UserSessionModel userSession = clientSession.getUserSession();
if (userSession == null) {
event.error(Errors.USER_SESSION_NOT_FOUND);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "User session not found", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "User session not found", Response.Status.BAD_REQUEST);
}
UserModel user = userSession.getUser();
if (user == null) {
event.error(Errors.USER_NOT_FOUND);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "User not found", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "User not found", Response.Status.BAD_REQUEST);
}
event.user(userSession.getUser());
if (!user.isEnabled()) {
event.error(Errors.USER_DISABLED);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "User disabled", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "User disabled", Response.Status.BAD_REQUEST);
}
String redirectUri = clientSession.getNote(OIDCLoginProtocol.REDIRECT_URI_PARAM);
String formParam = formParams.getFirst(OAuth2Constants.REDIRECT_URI);
if (redirectUri != null && !redirectUri.equals(formParam)) {
event.error(Errors.INVALID_CODE);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Incorrect redirect_uri", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Incorrect redirect_uri", Response.Status.BAD_REQUEST);
}
if (!client.getClientId().equals(clientSession.getClient().getClientId())) {
event.error(Errors.INVALID_CODE);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Auth error", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Auth error", Response.Status.BAD_REQUEST);
}
if (!client.isStandardFlowEnabled()) {
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Client not allowed to exchange code", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Client not allowed to exchange code", Response.Status.BAD_REQUEST);
}
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
event.error(Errors.USER_SESSION_NOT_FOUND);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Session not active", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Session not active", Response.Status.BAD_REQUEST);
}
// https://tools.ietf.org/html/rfc7636#section-4.6
@ -327,7 +334,7 @@ public class TokenEndpoint {
if (codeChallenge != null && codeVerifier == null) {
logger.warnf("PKCE code verifier not specified, authUserId = %s, authUsername = %s", authUserId, authUsername);
event.error(Errors.CODE_VERIFIER_MISSING);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "PKCE code verifier not specified", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "PKCE code verifier not specified", Response.Status.BAD_REQUEST);
}
if (codeChallenge != null) {
@ -336,16 +343,16 @@ public class TokenEndpoint {
if (!isValidPkceCodeVerifier(codeVerifier)) {
logger.infof("PKCE invalid code verifier");
event.error(Errors.INVALID_CODE_VERIFIER);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "PKCE invalid code verifier", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "PKCE invalid code verifier", Response.Status.BAD_REQUEST);
}
logger.debugf("PKCE supporting Client, codeVerifier = %s", codeVerifier);
String codeVerifierEncoded = codeVerifier;
try {
// https://tools.ietf.org/html/rfc7636#section-4.2
// plain or S256
if (codeChallengeMethod != null && codeChallengeMethod.equals(OAuth2Constants.PKCE_METHOD_S256)) {
logger.debugf("PKCE codeChallengeMethod = %s", codeChallengeMethod);
logger.debugf("PKCE codeChallengeMethod = %s", codeChallengeMethod);
codeVerifierEncoded = generateS256CodeChallenge(codeVerifier);
} else {
logger.debug("PKCE codeChallengeMethod is plain");
@ -354,12 +361,12 @@ public class TokenEndpoint {
} catch (Exception nae) {
logger.infof("PKCE code verification failed, not supported algorithm specified");
event.error(Errors.PKCE_VERIFICATION_FAILED);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "PKCE code verification failed, not supported algorithm specified", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "PKCE code verification failed, not supported algorithm specified", Response.Status.BAD_REQUEST);
}
if (!codeChallenge.equals(codeVerifierEncoded)) {
logger.warnf("PKCE verification failed. authUserId = %s, authUsername = %s", authUserId, authUsername);
event.error(Errors.PKCE_VERIFICATION_FAILED);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "PKCE verification failed", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "PKCE verification failed", Response.Status.BAD_REQUEST);
} else {
logger.debugf("PKCE verification success. codeVerifierEncoded = %s, codeChallenge = %s", codeVerifierEncoded, codeChallenge);
}
@ -384,13 +391,13 @@ public class TokenEndpoint {
event.success();
return Cors.add(request, Response.ok(res).type(MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
return cors.builder(Response.ok(res).type(MediaType.APPLICATION_JSON_TYPE)).build();
}
public Response refreshTokenGrant() {
String refreshToken = formParams.getFirst(OAuth2Constants.REFRESH_TOKEN);
if (refreshToken == null) {
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "No refresh token", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "No refresh token", Response.Status.BAD_REQUEST);
}
AccessTokenResponse res;
@ -408,12 +415,12 @@ public class TokenEndpoint {
} catch (OAuthErrorException e) {
logger.trace(e.getMessage(), e);
event.error(Errors.INVALID_TOKEN);
throw new ErrorResponseException(e.getError(), e.getDescription(), Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, e.getError(), e.getDescription(), Response.Status.BAD_REQUEST);
}
event.success();
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).build();
}
private void updateClientSession(AuthenticatedClientSessionModel clientSession) {
@ -453,12 +460,12 @@ public class TokenEndpoint {
if (!client.isDirectAccessGrantsEnabled()) {
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Client not allowed for direct access grants", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Client not allowed for direct access grants", Response.Status.BAD_REQUEST);
}
if (client.isConsentRequired()) {
event.error(Errors.CONSENT_DENIED);
throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
}
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
@ -485,7 +492,7 @@ public class TokenEndpoint {
UserModel user = authSession.getAuthenticatedUser();
if (user.getRequiredActions() != null && user.getRequiredActions().size() > 0) {
event.error(Errors.RESOLVE_REQUIRED_ACTIONS);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Invalid user credentials", Response.Status.UNAUTHORIZED);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Invalid user credentials", Response.Status.UNAUTHORIZED);
}
@ -509,21 +516,21 @@ public class TokenEndpoint {
event.success();
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).build();
}
public Response clientCredentialsGrant() {
if (client.isBearerOnly()) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException(OAuthErrorException.UNAUTHORIZED_CLIENT, "Bearer-only client not allowed to retrieve service account", Response.Status.UNAUTHORIZED);
throw new CorsErrorResponseException(cors, OAuthErrorException.UNAUTHORIZED_CLIENT, "Bearer-only client not allowed to retrieve service account", Response.Status.UNAUTHORIZED);
}
if (client.isPublicClient()) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException(OAuthErrorException.UNAUTHORIZED_CLIENT, "Public client not allowed to retrieve service account", Response.Status.UNAUTHORIZED);
throw new CorsErrorResponseException(cors, OAuthErrorException.UNAUTHORIZED_CLIENT, "Public client not allowed to retrieve service account", Response.Status.UNAUTHORIZED);
}
if (!client.isServiceAccountsEnabled()) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException(OAuthErrorException.UNAUTHORIZED_CLIENT, "Client not enabled to retrieve service account", Response.Status.UNAUTHORIZED);
throw new CorsErrorResponseException(cors, OAuthErrorException.UNAUTHORIZED_CLIENT, "Client not enabled to retrieve service account", Response.Status.UNAUTHORIZED);
}
UserModel clientUser = session.users().getServiceAccount(client);
@ -541,7 +548,7 @@ public class TokenEndpoint {
if (!clientUser.isEnabled()) {
event.error(Errors.USER_DISABLED);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User '" + clientUsername + "' disabled", Response.Status.UNAUTHORIZED);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "User '" + clientUsername + "' disabled", Response.Status.UNAUTHORIZED);
}
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
@ -578,7 +585,7 @@ public class TokenEndpoint {
event.success();
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).build();
}
public Response tokenExchange() {
@ -605,7 +612,7 @@ public class TokenEndpoint {
} catch (JWSInputException e) {
event.detail(Details.REASON, "unable to parse jwt subject_token");
event.error(Errors.INVALID_TOKEN);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token type, must be access token", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_TOKEN, "Invalid token type, must be access token", Response.Status.BAD_REQUEST);
}
}
@ -619,7 +626,7 @@ public class TokenEndpoint {
if (subjectTokenType != null && !subjectTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
event.detail(Details.REASON, "subject_token supports access tokens only");
event.error(Errors.INVALID_TOKEN);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token type, must be access token", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_TOKEN, "Invalid token type, must be access token", Response.Status.BAD_REQUEST);
}
@ -627,7 +634,7 @@ public class TokenEndpoint {
if (authResult == null) {
event.detail(Details.REASON, "subject_token validation failure");
event.error(Errors.INVALID_TOKEN);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.BAD_REQUEST);
}
tokenUser = authResult.getUser();
@ -647,7 +654,7 @@ public class TokenEndpoint {
// We always returned access denied to avoid username fishing
event.detail(Details.REASON, "requested_subject not found");
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
}
@ -658,7 +665,7 @@ public class TokenEndpoint {
if (!AdminPermissions.evaluator(session, realm, auth).users().canImpersonate(requestedUser)) {
event.detail(Details.REASON, "subject not allowed to impersonate");
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
}
} else {
@ -667,13 +674,13 @@ public class TokenEndpoint {
if (client.isPublicClient()) {
event.detail(Details.REASON, "public clients not allowed");
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
}
if (!AdminPermissions.management(session, realm).users().canClientImpersonate(client, requestedUser)) {
event.detail(Details.REASON, "client not allowed to impersonate");
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
}
}
@ -707,22 +714,22 @@ public class TokenEndpoint {
if (providerModel == null) {
event.detail(Details.REASON, "unknown requested_issuer");
event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid issuer", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Invalid issuer", Response.Status.BAD_REQUEST);
}
IdentityProvider provider = IdentityBrokerService.getIdentityProvider(session, realm, requestedIssuer);
if (!(provider instanceof ExchangeTokenToIdentityProviderToken)) {
event.detail(Details.REASON, "exchange unsupported by requested_issuer");
event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Issuer does not support token exchange", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Issuer does not support token exchange", Response.Status.BAD_REQUEST);
}
if (!AdminPermissions.management(session, realm).idps().canExchangeTo(client, providerModel)) {
event.detail(Details.REASON, "client not allowed to exchange for requested_issuer");
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
}
Response response = ((ExchangeTokenToIdentityProviderToken)provider).exchangeFromToken(uriInfo, event, client, targetUserSession, targetUser, formParams);
return Cors.add(request, Response.fromResponse(response)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
return cors.builder(Response.fromResponse(response)).build();
}
@ -733,7 +740,7 @@ public class TokenEndpoint {
} else if (!requestedTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE) && !requestedTokenType.equals(OAuth2Constants.REFRESH_TOKEN_TYPE)) {
event.detail(Details.REASON, "requested_token_type unsupported");
event.error(Errors.INVALID_REQUEST);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "requested_token_type unsupported", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "requested_token_type unsupported", Response.Status.BAD_REQUEST);
}
ClientModel targetClient = client;
@ -745,13 +752,13 @@ public class TokenEndpoint {
if (targetClient.isConsentRequired()) {
event.detail(Details.REASON, "audience requires consent");
event.error(Errors.CONSENT_DENIED);
throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
}
if (!targetClient.equals(client) && !AdminPermissions.management(session, realm).clients().canExchangeTo(client, targetClient)) {
event.detail(Details.REASON, "client not allowed to exchange to audience");
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
}
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
@ -788,7 +795,7 @@ public class TokenEndpoint {
event.success();
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).build();
}
public Response exchangeExternalToken(String issuer, String subjectToken) {
@ -811,17 +818,17 @@ public class TokenEndpoint {
if (externalIdp == null) {
event.error(Errors.INVALID_ISSUER);
throw new ErrorResponseException(Errors.INVALID_ISSUER, "Invalid " + OAuth2Constants.SUBJECT_ISSUER + " parameter", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, Errors.INVALID_ISSUER, "Invalid " + OAuth2Constants.SUBJECT_ISSUER + " parameter", Response.Status.BAD_REQUEST);
}
if (!AdminPermissions.management(session, realm).idps().canExchangeTo(client, externalIdpModel)) {
event.detail(Details.REASON, "client not allowed to exchange subject_issuer");
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
}
BrokeredIdentityContext context = externalIdp.exchangeExternal(event, formParams);
if (context == null) {
event.error(Errors.INVALID_ISSUER);
throw new ErrorResponseException(Errors.INVALID_ISSUER, "Invalid " + OAuth2Constants.SUBJECT_ISSUER + " parameter", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, Errors.INVALID_ISSUER, "Invalid " + OAuth2Constants.SUBJECT_ISSUER + " parameter", Response.Status.BAD_REQUEST);
}
UserModel user = importUserFromExternalIdentity(context);
@ -884,14 +891,14 @@ public class TokenEndpoint {
UserModel existingUser = session.users().getUserByEmail(context.getEmail(), realm);
if (existingUser != null) {
event.error(Errors.FEDERATED_IDENTITY_EXISTS);
throw new ErrorResponseException(Errors.INVALID_TOKEN, "User already exists", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, Errors.INVALID_TOKEN, "User already exists", Response.Status.BAD_REQUEST);
}
}
UserModel existingUser = session.users().getUserByUsername(username, realm);
if (existingUser != null) {
event.error(Errors.FEDERATED_IDENTITY_EXISTS);
throw new ErrorResponseException(Errors.INVALID_TOKEN, "User already exists", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, Errors.INVALID_TOKEN, "User already exists", Response.Status.BAD_REQUEST);
}
@ -922,12 +929,12 @@ public class TokenEndpoint {
} else {
if (!user.isEnabled()) {
event.error(Errors.USER_DISABLED);
throw new ErrorResponseException(Errors.INVALID_TOKEN, "Invalid Token", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, Errors.INVALID_TOKEN, "Invalid Token", Response.Status.BAD_REQUEST);
}
if (realm.isBruteForceProtected()) {
if (session.getProvider(BruteForceProtector.class).isTemporarilyDisabled(session, realm, user)) {
event.error(Errors.USER_TEMPORARILY_DISABLED);
throw new ErrorResponseException(Errors.INVALID_TOKEN, "Invalid Token", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, Errors.INVALID_TOKEN, "Invalid Token", Response.Status.BAD_REQUEST);
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.services.resources.Cors;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class CorsErrorResponseException extends WebApplicationException {
private final Cors cors;
private final String error;
private final String errorDescription;
private final Response.Status status;
public CorsErrorResponseException(Cors cors, String error, String errorDescription, Response.Status status) {
this.cors = cors;
this.error = error;
this.errorDescription = errorDescription;
this.status = status;
}
@Override
public Response getResponse() {
OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(error, errorDescription);
Response.ResponseBuilder builder = Response.status(status).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE);
return cors.builder(builder).build();
}
}

View file

@ -17,6 +17,7 @@
package org.keycloak.services.resources;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@ -82,6 +83,11 @@ public class Cors {
return new Cors(request);
}
public Cors builder(ResponseBuilder builder) {
this.builder = builder;
return this;
}
public Cors preflight() {
preflight = true;
return this;
@ -92,6 +98,11 @@ public class Cors {
return this;
}
public Cors allowAllOrigins() {
allowedOrigins = Collections.singleton(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD);
return this;
}
public Cors allowedOrigins(UriInfo uriInfo, ClientModel client) {
if (client != null) {
allowedOrigins = WebOriginsUtils.resolveValidWebOrigins(uriInfo, client);

View file

@ -131,6 +131,7 @@ public class OAuthClient {
private String codeVerifier;
private String codeChallenge;
private String codeChallengeMethod;
private String origin;
public class LogoutUrlBuilder {
private final UriBuilder b = OIDCLoginProtocolService.logoutUrl(UriBuilder.fromUri(baseUrl));
@ -193,6 +194,7 @@ public class OAuthClient {
codeVerifier = null;
codeChallenge = null;
codeChallengeMethod = null;
origin = null;
}
public AuthorizationEndpointResponse doLogin(String username, String password) {
@ -268,6 +270,9 @@ public class OAuthClient {
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE));
if (origin != null) {
post.addHeader("Origin", origin);
}
if (code != null) {
parameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
}
@ -562,6 +567,9 @@ public class OAuthClient {
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
if (origin != null) {
post.addHeader("Origin", origin);
}
if (refreshToken != null) {
parameters.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
}
@ -873,6 +881,10 @@ public class OAuthClient {
this.codeChallengeMethod = codeChallengeMethod;
return this;
}
public OAuthClient origin(String origin) {
this.origin = origin;
return this;
}
public static class AuthorizationEndpointResponse {
@ -956,10 +968,18 @@ public class OAuthClient {
private String error;
private String errorDescription;
private Map<String, String> headers;
public AccessTokenResponse(CloseableHttpResponse response) throws Exception {
try {
statusCode = response.getStatusLine().getStatusCode();
headers = new HashMap<>();
for (Header h : response.getAllHeaders()) {
headers.put(h.getName(), h.getValue());
}
Header[] contentTypeHeaders = response.getHeaders("Content-Type");
String contentType = (contentTypeHeaders != null && contentTypeHeaders.length > 0) ? contentTypeHeaders[0].getValue() : null;
if (!"application/json".equals(contentType)) {
@ -1033,6 +1053,10 @@ public class OAuthClient {
public String getScope() {
return scope;
}
public Map<String, String> getHeaders() {
return headers;
}
}
public PublicKey getRealmPublicKey(String realm) {
@ -1059,4 +1083,4 @@ public class OAuthClient {
}
}
}

View file

@ -1,51 +0,0 @@
package org.keycloak.testsuite.oauth;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class PreflightRequestTest extends AbstractKeycloakTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@Override
public void beforeAbstractKeycloakTest() throws Exception {
super.beforeAbstractKeycloakTest();
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation testRealmRep = new RealmRepresentation();
testRealmRep.setId(TEST);
testRealmRep.setRealm(TEST);
testRealmRep.setEnabled(true);
testRealms.add(testRealmRep);
}
@Test
public void preflightRequest() throws Exception {
CloseableHttpResponse response = oauth.doPreflightRequest();
String[] methods = response.getHeaders("Access-Control-Allow-Methods")[0].getValue().split(", ");
Set allowedMethods = new HashSet(Arrays.asList(methods));
assertEquals(2, allowedMethods.size());
assertTrue(allowedMethods.containsAll(Arrays.asList("POST", "OPTIONS")));
}
}

View file

@ -0,0 +1,111 @@
package org.keycloak.testsuite.oauth;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.UserBuilder;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class TokenEndpointCorsTest extends AbstractKeycloakTest {
private static final String VALID_CORS_URL = "http://localtest.me:8180";
private static final String INVALID_CORS_URL = "http://invalid.localtest.me:8180";
@Rule
public AssertEvents events = new AssertEvents(this);
@Override
public void beforeAbstractKeycloakTest() throws Exception {
super.beforeAbstractKeycloakTest();
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
realm.getClients().add(ClientBuilder.create().redirectUris(VALID_CORS_URL + "/realms/master/app").addWebOrigin(VALID_CORS_URL).id("test-app2").clientId("test-app2").publicClient().build());
testRealms.add(realm);
}
@Test
public void preflightRequest() throws Exception {
CloseableHttpResponse response = oauth.doPreflightRequest();
String[] methods = response.getHeaders("Access-Control-Allow-Methods")[0].getValue().split(", ");
Set allowedMethods = new HashSet(Arrays.asList(methods));
assertEquals(2, allowedMethods.size());
assertTrue(allowedMethods.containsAll(Arrays.asList("POST", "OPTIONS")));
}
@Test
public void accessTokenCorsRequest() throws Exception {
oauth.realm("test");
oauth.clientId("test-app2");
oauth.redirectUri(VALID_CORS_URL + "/realms/master/app");
oauth.doLogin("test-user@localhost", "password");
// Token request
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
oauth.origin(VALID_CORS_URL);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
assertEquals(200, response.getStatusCode());
assertCors(response);
// Refresh request
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), null);
assertEquals(200, response.getStatusCode());
assertCors(response);
// Invalid origin
oauth.origin(INVALID_CORS_URL);
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "password");
assertEquals(200, response.getStatusCode());
assertNotCors(response);
oauth.origin(VALID_CORS_URL);
// No session
oauth.openLogout();
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), null);
assertEquals(400, response.getStatusCode());
assertCors(response);
assertEquals("invalid_grant", response.getError());
assertEquals("Session not active", response.getErrorDescription());
}
private static void assertCors(OAuthClient.AccessTokenResponse response) {
assertEquals("true", response.getHeaders().get("Access-Control-Allow-Credentials"));
assertEquals(VALID_CORS_URL, response.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals("Access-Control-Allow-Methods", response.getHeaders().get("Access-Control-Expose-Headers"));
}
private static void assertNotCors(OAuthClient.AccessTokenResponse response) {
assertNull(response.getHeaders().get("Access-Control-Allow-Credentials"));
assertNull(response.getHeaders().get("Access-Control-Allow-Origin"));
assertNull(response.getHeaders().get("Access-Control-Expose-Headers"));
}
}