keycloak-uncached
Changes
integration/js/src/main/resources/keycloak.js 227(+174 -53)
testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java 5(+3 -2)
testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java 2(+1 -1)
Details
diff --git a/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java b/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java
index 328916e..35085c7 100755
--- a/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java
+++ b/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java
@@ -342,6 +342,17 @@ public class KeycloakUriBuilder {
}
/**
+ * Set fragment, but not encode it. It assumes that given fragment was already properly encoded
+ *
+ * @param fragment
+ * @return
+ */
+ public KeycloakUriBuilder encodedFragment(String fragment) {
+ this.fragment = fragment;
+ return this;
+ }
+
+ /**
* Only replace path params in path of URI. This changes state of URIBuilder.
*
* @param name
diff --git a/core/src/main/java/org/keycloak/representations/RefreshToken.java b/core/src/main/java/org/keycloak/representations/RefreshToken.java
index 39c7c46..0300673 100755
--- a/core/src/main/java/org/keycloak/representations/RefreshToken.java
+++ b/core/src/main/java/org/keycloak/representations/RefreshToken.java
@@ -28,6 +28,7 @@ public class RefreshToken extends AccessToken {
this.subject = token.subject;
this.issuedFor = token.issuedFor;
this.sessionState = token.sessionState;
+ this.nonce = token.nonce;
if (token.realmAccess != null) {
realmAccess = token.realmAccess.clone();
}
diff --git a/events/api/src/main/java/org/keycloak/events/Details.java b/events/api/src/main/java/org/keycloak/events/Details.java
index b9c5338..a995d7b 100755
--- a/events/api/src/main/java/org/keycloak/events/Details.java
+++ b/events/api/src/main/java/org/keycloak/events/Details.java
@@ -11,6 +11,7 @@ public interface Details {
String CODE_ID = "code_id";
String REDIRECT_URI = "redirect_uri";
String RESPONSE_TYPE = "response_type";
+ String RESPONSE_MODE = "response_mode";
String AUTH_TYPE = "auth_type";
String AUTH_METHOD = "auth_method";
String IDENTITY_PROVIDER = "identity_provider";
diff --git a/examples/js-console/src/main/webapp/index.html b/examples/js-console/src/main/webapp/index.html
index 2cca0f8..8c7713f 100644
--- a/examples/js-console/src/main/webapp/index.html
+++ b/examples/js-console/src/main/webapp/index.html
@@ -106,7 +106,13 @@
event('Auth Logout');
};
- keycloak.init().success(function(authenticated) {
+ // Flow can be changed to 'implicit' or 'hybrid', but then client must enable implicit flow too in admin console
+ var initOptions = {
+ responseMode: 'fragment',
+ flow: 'standard'
+ };
+
+ keycloak.init(initOptions).success(function(authenticated) {
output('Init Success (' + (authenticated ? 'Authenticated' : 'Not Authenticated') + ')');
}).error(function() {
output('Init Error');
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
index 21175a0..ffe8328 100644
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -176,7 +176,8 @@ failedLogout=Logout failed
unknownLoginRequesterMessage=Unknown login requester
loginRequesterNotEnabledMessage=Login requester not enabled
bearerOnlyMessage=Bearer-only applications are not allowed to initiate browser login
-standardFlowDisabledMessage=Client is not allowed to initiate browser login because standard flow is disabled for the client.
+standardFlowDisabledMessage=Client is not allowed to initiate browser login with given response_type. Standard flow is disabled for the client.
+implicitFlowDisabledMessage=Client is not allowed to initiate browser login with given response_type. Implicit flow is disabled for the client.
invalidRedirectUriMessage=Invalid redirect uri
unsupportedNameIdFormatMessage=Unsupported NameIDFormat
invlidRequesterMessage=Invalid requester
integration/js/src/main/resources/keycloak.js 227(+174 -53)
diff --git a/integration/js/src/main/resources/keycloak.js b/integration/js/src/main/resources/keycloak.js
index 09e0d86..8f587f0 100755
--- a/integration/js/src/main/resources/keycloak.js
+++ b/integration/js/src/main/resources/keycloak.js
@@ -36,8 +36,41 @@
if (initOptions.onLoad === 'login-required') {
kc.loginRequired = true;
}
+
+ if (initOptions.responseMode) {
+ if (initOptions.responseMode === 'query' || initOptions.responseMode === 'fragment') {
+ kc.responseMode = initOptions.responseMode;
+ } else {
+ throw 'Invalid value for responseMode';
+ }
+ }
+
+ if (initOptions.flow) {
+ switch (initOptions.flow) {
+ case 'standard':
+ kc.responseType = 'code';
+ break;
+ case 'implicit':
+ kc.responseType = 'id_token token refresh_token';
+ break;
+ case 'hybrid':
+ kc.responseType = 'code id_token token refresh_token';
+ break;
+ default:
+ throw 'Invalid value for flow';
+ }
+ }
+ }
+
+ if (!kc.responseMode) {
+ kc.responseMode = 'fragment';
+ }
+ if (!kc.responseType) {
+ kc.responseType = 'code';
}
+ console.log('responseMode=' + kc.responseMode + ', responseType=' + kc.responseType);
+
var promise = createPromise();
var initPromise = createPromise();
@@ -132,13 +165,14 @@
kc.createLoginUrl = function(options) {
var state = createUUID();
+ var nonce = createUUID();
var redirectUri = adapter.redirectUri(options);
if (options && options.prompt) {
redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'prompt=' + options.prompt;
}
- sessionStorage.oauthState = JSON.stringify({ state: state, redirectUri: encodeURIComponent(redirectUri) });
+ sessionStorage.oauthState = JSON.stringify({ state: state, nonce: nonce, redirectUri: encodeURIComponent(redirectUri) });
var action = 'auth';
if (options && options.action == 'register') {
@@ -150,7 +184,9 @@
+ '?client_id=' + encodeURIComponent(kc.clientId)
+ '&redirect_uri=' + encodeURIComponent(redirectUri)
+ '&state=' + encodeURIComponent(state)
- + '&response_type=code';
+ + '&nonce=' + encodeURIComponent(nonce)
+ + '&response_mode=' + encodeURIComponent(kc.responseMode)
+ + '&response_type=' + encodeURIComponent(kc.responseType);
if (options && options.prompt) {
url += '&prompt=' + encodeURIComponent(options.prompt);
@@ -394,6 +430,8 @@
var error = oauth.error;
var prompt = oauth.prompt;
+ var timeLocal = new Date().getTime();
+
if (code) {
var params = 'code=' + code + '&grant_type=authorization_code';
var url = getRealmUrl() + '/protocol/openid-connect/token';
@@ -412,20 +450,12 @@
req.withCredentials = true;
- var timeLocal = new Date().getTime();
-
req.onreadystatechange = function() {
if (req.readyState == 4) {
if (req.status == 200) {
- timeLocal = (timeLocal + new Date().getTime()) / 2;
var tokenResponse = JSON.parse(req.responseText);
- setToken(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token']);
-
- kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat;
-
- kc.onAuthSuccess && kc.onAuthSuccess();
- promise && promise.setSuccess();
+ authSuccess(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'])
} else {
kc.onAuthError && kc.onAuthError();
promise && promise.setError();
@@ -441,7 +471,31 @@
} else {
promise && promise.setSuccess();
}
+ } else if (oauth.access_token || oauth.id_token || oauth.refresh_token) {
+ authSuccess(oauth.access_token, oauth.refresh_token, oauth.id_token);
}
+
+
+ function authSuccess(accessToken, refreshToken, idToken) {
+ timeLocal = (timeLocal + new Date().getTime()) / 2;
+
+ setToken(accessToken, refreshToken, idToken);
+
+ if ((kc.tokenParsed && kc.tokenParsed.nonce != oauth.storedNonce) ||
+ (kc.refreshTokenParsed && kc.refreshTokenParsed.nonce != oauth.storedNonce) ||
+ (kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce)) {
+
+ console.log('invalid nonce!');
+ kc.clearToken();
+ promise && promise.setError();
+ } else {
+ kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat;
+
+ kc.onAuthSuccess && kc.onAuthSuccess();
+ promise && promise.setSuccess();
+ }
+ }
+
}
function loadConfig(url) {
@@ -597,53 +651,21 @@
}
function parseCallback(url) {
- if (url.indexOf('?') != -1) {
- var oauth = {};
+ var oauth = new CallbackParser(url, kc.responseMode).parseUri();
- oauth.newUrl = url.split('?')[0];
- var paramString = url.split('?')[1];
- var fragIndex = paramString.indexOf('#');
- if (fragIndex != -1) {
- paramString = paramString.substring(0, fragIndex);
- }
- var params = paramString.split('&');
- for (var i = 0; i < params.length; i++) {
- var p = params[i].split('=');
- switch (decodeURIComponent(p[0])) {
- case 'code':
- oauth.code = p[1];
- break;
- case 'error':
- oauth.error = p[1];
- break;
- case 'state':
- oauth.state = decodeURIComponent(p[1]);
- break;
- case 'redirect_fragment':
- oauth.fragment = decodeURIComponent(p[1]);
- break;
- case 'prompt':
- oauth.prompt = p[1];
- break;
- default:
- oauth.newUrl += (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + p[0] + '=' + p[1];
- break;
- }
- }
+ var sessionState = sessionStorage.oauthState && JSON.parse(sessionStorage.oauthState);
- var sessionState = sessionStorage.oauthState && JSON.parse(sessionStorage.oauthState);
+ if (sessionState && (oauth.code || oauth.error || oauth.access_token || oauth.id_token) && oauth.state && oauth.state == sessionState.state) {
+ delete sessionStorage.oauthState;
- if (sessionState && (oauth.code || oauth.error) && oauth.state && oauth.state == sessionState.state) {
- delete sessionStorage.oauthState;
+ oauth.redirectUri = sessionState.redirectUri;
+ oauth.storedNonce = sessionState.nonce;
- oauth.redirectUri = sessionState.redirectUri;
-
- if (oauth.fragment) {
- oauth.newUrl += '#' + oauth.fragment;
- }
-
- return oauth;
+ if (oauth.fragment) {
+ oauth.newUrl += '#' + oauth.fragment;
}
+
+ return oauth;
}
}
@@ -907,6 +929,105 @@
throw 'invalid adapter type: ' + type;
}
+
+
+ var CallbackParser = function(uriToParse, responseMode) {
+ if (!(this instanceof CallbackParser)) {
+ return new CallbackParser(uriToParse, responseMode);
+ }
+ var parser = this;
+
+ var initialParse = function() {
+ var baseUri = null;
+ var queryString = null;
+ var fragmentString = null;
+
+ var questionMarkIndex = uriToParse.indexOf("?");
+ var fragmentIndex = uriToParse.indexOf("#", questionMarkIndex + 1);
+ if (questionMarkIndex == -1 && fragmentIndex == -1) {
+ baseUri = uriToParse;
+ } else if (questionMarkIndex != -1) {
+ baseUri = uriToParse.substring(0, questionMarkIndex);
+ queryString = uriToParse.substring(questionMarkIndex + 1);
+ if (fragmentIndex != -1) {
+ fragmentIndex = queryString.indexOf("#");
+ fragmentString = queryString.substring(fragmentIndex + 1);
+ queryString = queryString.substring(0, fragmentIndex);
+ }
+ } else {
+ baseUri = uriToParse.substring(0, fragmentIndex);
+ fragmentString = uriToParse.substring(fragmentIndex + 1);
+ }
+
+ return { baseUri: baseUri, queryString: queryString, fragmentString: fragmentString };
+ }
+
+ var parseParams = function(paramString) {
+ var result = {};
+ var params = paramString.split('&');
+ for (var i = 0; i < params.length; i++) {
+ var p = params[i].split('=');
+ var paramName = decodeURIComponent(p[0]);
+ var paramValue = decodeURIComponent(p[1]);
+ result[paramName] = paramValue;
+ }
+ return result;
+ }
+
+ var handleQueryParam = function(paramName, paramValue, oauth) {
+ var supportedOAuthParams = [ 'code', 'error', 'state' ];
+
+ for (var i = 0 ; i< supportedOAuthParams.length ; i++) {
+ if (paramName === supportedOAuthParams[i]) {
+ oauth[paramName] = paramValue;
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ parser.parseUri = function() {
+ var parsedUri = initialParse();
+
+ var queryParams = {};
+ if (parsedUri.queryString) {
+ queryParams = parseParams(parsedUri.queryString);
+ }
+
+ var oauth = { newUrl: parsedUri.baseUri };
+ for (var param in queryParams) {
+ switch (param) {
+ case 'redirect_fragment':
+ oauth.fragment = queryParams[param];
+ break;
+ case 'prompt':
+ oauth.prompt = queryParams[param];
+ break;
+ default:
+ if (responseMode != 'query' || !handleQueryParam(param, queryParams[param], oauth)) {
+ oauth.newUrl += (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + param + '=' + queryParams[param];
+ }
+ break;
+ }
+ }
+
+ if (responseMode === 'fragment') {
+ var fragmentParams = {};
+ if (parsedUri.fragmentString) {
+ fragmentParams = parseParams(parsedUri.fragmentString);
+ }
+ for (var param in fragmentParams) {
+ oauth[param] = fragmentParams[param];
+ }
+ }
+
+ console.log("OAUTH: ");
+ console.log(oauth);
+ return oauth;
+ }
+ }
+
}
if ( typeof module === "object" && module && typeof module.exports === "object" ) {
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index b1d4162..710c77d 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -470,7 +470,8 @@ public class AuthenticationProcessor {
LoginProtocol protocol = getSession().getProvider(LoginProtocol.class, getClientSession().getAuthMethod());
protocol.setRealm(getRealm())
.setHttpHeaders(getHttpRequest().getHttpHeaders())
- .setUriInfo(getUriInfo());
+ .setUriInfo(getUriInfo())
+ .setEventBuilder(event);
Response response = protocol.sendError(getClientSession(), Error.CANCELLED_BY_USER);
forceChallenge(response);
}
@@ -808,7 +809,7 @@ public class AuthenticationProcessor {
public Response finishAuthentication() {
event.success();
RealmModel realm = clientSession.getRealm();
- return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, connection);
+ return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, connection, event);
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index 250de12..4b95d56 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -1,19 +1,11 @@
package org.keycloak.protocol.oidc.endpoints;
-import java.util.List;
-
import javax.ws.rs.GET;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
-import org.jboss.resteasy.spi.HttpRequest;
-import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationProcessor;
-import org.keycloak.common.ClientConnection;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
@@ -24,12 +16,12 @@ import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.IdentityProviderModel;
-import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.AuthorizationEndpointBase;
-import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.services.ErrorPageException;
import org.keycloak.services.Urls;
@@ -55,11 +47,13 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
private ClientSessionModel clientSession;
private Action action;
+ private OIDCResponseType parsedResponseType;
private String clientId;
private String redirectUri;
private String redirectUriParam;
private String responseType;
+ private String responseMode;
private String state;
private String scope;
private String loginHint;
@@ -80,6 +74,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM);
responseType = params.getFirst(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
+ responseMode = params.getFirst(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
redirectUriParam = params.getFirst(OIDCLoginProtocol.REDIRECT_URI_PARAM);
state = params.getFirst(OIDCLoginProtocol.STATE_PARAM);
scope = params.getFirst(OIDCLoginProtocol.SCOPE_PARAM);
@@ -90,8 +85,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
checkSsl();
checkRealm();
- checkClient();
checkResponseType();
+ checkClient();
checkRedirectUri();
createClientSession();
@@ -172,11 +167,17 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
throw new ErrorPageException(session, Messages.BEARER_ONLY);
}
- if (!client.isStandardFlowEnabled()) {
+ if ((parsedResponseType.hasResponseType(OIDCResponseType.CODE) || parsedResponseType.hasResponseType(OIDCResponseType.NONE)) && !client.isStandardFlowEnabled()) {
event.error(Errors.NOT_ALLOWED);
throw new ErrorPageException(session, Messages.STANDARD_FLOW_DISABLED);
}
+ if ((parsedResponseType.hasResponseType(OIDCResponseType.TOKEN) || parsedResponseType.hasResponseType(OIDCResponseType.ID_TOKEN) || parsedResponseType.hasResponseType(OIDCResponseType.REFRESH_TOKEN))
+ && !client.isImplicitFlowEnabled()) {
+ event.error(Errors.NOT_ALLOWED);
+ throw new ErrorPageException(session, Messages.IMPLICIT_FLOW_DISABLED);
+ }
+
session.getContext().setClient(client);
}
@@ -192,14 +193,32 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
event.detail(Details.RESPONSE_TYPE, responseType);
- if (responseType.equals(OAuth2Constants.CODE)) {
+ try {
+ parsedResponseType = OIDCResponseType.parse(responseType);
if (action == null) {
action = Action.CODE;
}
- } else {
+ } catch (IllegalArgumentException iae) {
+ logger.error(iae.getMessage());
event.error(Errors.INVALID_REQUEST);
throw new ErrorPageException(session, Messages.INVALID_PARAMETER, OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
}
+
+ try {
+ OIDCResponseMode parsedResponseMode = OIDCResponseMode.parse(responseMode, parsedResponseType);
+ event.detail(Details.RESPONSE_MODE, parsedResponseMode.toString().toLowerCase());
+
+ // Disallowed by OIDC specs
+ if (parsedResponseType.isImplicitOrHybridFlow() && parsedResponseMode == OIDCResponseMode.QUERY) {
+ logger.error("Response_mode 'query' not allowed for implicit or hybrid flow");
+ event.error(Errors.INVALID_REQUEST);
+ throw new ErrorPageException(session, Messages.INVALID_PARAMETER, OIDCLoginProtocol.RESPONSE_MODE_PARAM);
+ }
+
+ } catch (IllegalArgumentException iae) {
+ event.error(Errors.INVALID_REQUEST);
+ throw new ErrorPageException(session, Messages.INVALID_PARAMETER, OIDCLoginProtocol.RESPONSE_MODE_PARAM);
+ }
}
private void checkRedirectUri() {
@@ -228,6 +247,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
if (loginHint != null) clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, loginHint);
if (prompt != null) clientSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, prompt);
if (idpHint != null) clientSession.setNote(AdapterConstants.KC_IDP_HINT, idpHint);
+ if (responseMode != null) clientSession.setNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, responseMode);
}
private Response buildAuthorizationCodeAuthorizationResponse() {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index c790661..fe358e0 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -327,7 +327,7 @@ public class TokenEndpoint {
}
public Response buildResourceOwnerPasswordCredentialsGrant() {
- event.detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, "token");
+ event.detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD);
if (client.isConsentRequired()) {
event.error(Errors.CONSENT_DENIED);
@@ -393,7 +393,7 @@ public class TokenEndpoint {
throw new ErrorResponseException("unauthorized_client", "Client not enabled to retrieve service account", Response.Status.UNAUTHORIZED);
}
- event.detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH);
+ event.detail(Details.RESPONSE_TYPE, OAuth2Constants.CLIENT_CREDENTIALS);
UserModel clientUser = session.users().getUserByServiceAccountClient(client);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
index b9d55db..76c9794 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
@@ -33,6 +33,10 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.RestartLoginCookie;
+import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder;
+import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
+import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.ResourceAdminManager;
@@ -62,6 +66,8 @@ public class OIDCLoginProtocol implements LoginProtocol {
public static final String LOGOUT_REDIRECT_URI = "OIDC_LOGOUT_REDIRECT_URI";
public static final String ISSUER = "iss";
+ public static final String RESPONSE_MODE_PARAM = "response_mode";
+
private static final Logger log = Logger.getLogger(OIDCLoginProtocol.class);
protected KeycloakSession session;
@@ -74,6 +80,9 @@ public class OIDCLoginProtocol implements LoginProtocol {
protected EventBuilder event;
+ protected OIDCResponseType responseType;
+ protected OIDCResponseMode responseMode;
+
public OIDCLoginProtocol(KeycloakSession session, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, EventBuilder event) {
this.session = session;
this.realm = realm;
@@ -86,6 +95,15 @@ public class OIDCLoginProtocol implements LoginProtocol {
}
+ private void setupResponseTypeAndMode(ClientSessionModel clientSession) {
+ String responseType = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
+ String responseMode = clientSession.getNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
+ this.responseType = OIDCResponseType.parse(responseType);
+ this.responseMode = OIDCResponseMode.parse(responseMode, this.responseType);
+ this.event.detail(Details.RESPONSE_TYPE, responseType);
+ this.event.detail(Details.RESPONSE_MODE, this.responseMode.toString().toLowerCase());
+ }
+
@Override
public OIDCLoginProtocol setSession(KeycloakSession session) {
this.session = session;
@@ -116,32 +134,70 @@ public class OIDCLoginProtocol implements LoginProtocol {
return this;
}
+
@Override
public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) {
ClientSessionModel clientSession = accessCode.getClientSession();
+ setupResponseTypeAndMode(clientSession);
+
String redirect = clientSession.getRedirectUri();
+ OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode);
String state = clientSession.getNote(OIDCLoginProtocol.STATE_PARAM);
- accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN.name());
- UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.CODE, accessCode.getCode());
log.debugv("redirectAccessCode: state: {0}", state);
if (state != null)
- redirectUri.queryParam(OAuth2Constants.STATE, state);
- Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
+ redirectUri.addParam(OAuth2Constants.STATE, state);
- return location.build();
+ // Standard or hybrid flow
+ if (responseType.hasResponseType(OIDCResponseType.CODE)) {
+ accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN.name());
+ redirectUri.addParam(OAuth2Constants.CODE, accessCode.getCode());
+ }
+
+ // Implicit or hybrid flow
+ if (responseType.hasResponseType(OIDCResponseType.TOKEN) || responseType.hasResponseType(OIDCResponseType.ID_TOKEN) || responseType.hasResponseType(OIDCResponseType.REFRESH_TOKEN)) {
+ TokenManager tokenManager = new TokenManager();
+ AccessTokenResponse res = tokenManager.responseBuilder(realm, clientSession.getClient(), event, session, userSession, clientSession)
+ .generateAccessToken()
+ .generateRefreshToken()
+ .generateIDToken()
+ .build();
+
+ if (responseType.hasResponseType(OIDCResponseType.ID_TOKEN)) {
+ redirectUri.addParam("id_token", res.getIdToken());
+ }
+
+ if (responseType.hasResponseType(OIDCResponseType.TOKEN)) {
+ redirectUri.addParam("access_token", res.getToken());
+ redirectUri.addParam("token_type", res.getTokenType());
+ redirectUri.addParam("session-state", res.getSessionState());
+ redirectUri.addParam("expires_in", String.valueOf(res.getExpiresIn()));
+ }
+
+ // Not OIDC standard, but supported
+ if (responseType.hasResponseType(OIDCResponseType.REFRESH_TOKEN)) {
+ redirectUri.addParam("refresh_token", res.getRefreshToken());
+ redirectUri.addParam("refresh_expires_in", String.valueOf(res.getRefreshExpiresIn()));
+ }
+
+ redirectUri.addParam("not-before-policy", String.valueOf(res.getNotBeforePolicy()));
+ }
+
+ return redirectUri.build();
}
+
@Override
public Response sendError(ClientSessionModel clientSession, Error error) {
+ setupResponseTypeAndMode(clientSession);
+
String redirect = clientSession.getRedirectUri();
String state = clientSession.getNote(OIDCLoginProtocol.STATE_PARAM);
- UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, translateError(error));
+ OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode).addParam(OAuth2Constants.ERROR, translateError(error));
if (state != null)
- redirectUri.queryParam(OAuth2Constants.STATE, state);
+ redirectUri.addParam(OAuth2Constants.STATE, state);
session.sessions().removeClientSession(realm, clientSession);
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
- Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
- return location.build();
+ return redirectUri.build();
}
private String translateError(Error error) {
@@ -161,10 +217,8 @@ public class OIDCLoginProtocol implements LoginProtocol {
@Override
public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
- if (!(clientSession.getClient() instanceof ClientModel))
- return;
- ClientModel app = clientSession.getClient();
- new ResourceAdminManager(session).logoutClientSession(uriInfo.getRequestUri(), realm, app, clientSession);
+ ClientModel client = clientSession.getClient();
+ new ResourceAdminManager(session).logoutClientSession(uriInfo.getRequestUri(), realm, client, clientSession);
}
@Override
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index 304e6af..b2966c0 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -52,7 +52,7 @@ import java.util.Map;
import java.util.Set;
/**
- * Stateful object that creates tokens and manages oauth access codes
+ * Stateless object that creates tokens and manages oauth access codes
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java
new file mode 100644
index 0000000..06cc3cc
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java
@@ -0,0 +1,149 @@
+package org.keycloak.protocol.oidc.utils;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.keycloak.common.util.Encode;
+import org.keycloak.common.util.KeycloakUriBuilder;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class OIDCRedirectUriBuilder {
+
+ protected final KeycloakUriBuilder uriBuilder;
+
+ protected OIDCRedirectUriBuilder(KeycloakUriBuilder uriBuilder) {
+ this.uriBuilder = uriBuilder;
+ }
+
+ public abstract OIDCRedirectUriBuilder addParam(String paramName, String paramValue);
+ public abstract Response build();
+
+
+ public static OIDCRedirectUriBuilder fromUri(String baseUri, OIDCResponseMode responseMode) {
+ KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(baseUri);
+
+ switch (responseMode) {
+ case QUERY: return new QueryRedirectUriBuilder(uriBuilder);
+ case FRAGMENT: return new FragmentRedirectUriBuilder(uriBuilder);
+ case FORM_POST: return new FormPostRedirectUriBuilder(uriBuilder);
+ }
+
+ throw new IllegalStateException("Not possible to end here");
+ }
+
+
+ // Impl subclasses
+
+
+ // http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
+ public static class QueryRedirectUriBuilder extends OIDCRedirectUriBuilder {
+
+ protected QueryRedirectUriBuilder(KeycloakUriBuilder uriBuilder) {
+ super(uriBuilder);
+ }
+
+ @Override
+ public OIDCRedirectUriBuilder addParam(String paramName, String paramValue) {
+ uriBuilder.queryParam(paramName, paramValue);
+ return this;
+ }
+
+ @Override
+ public Response build() {
+ URI redirectUri = uriBuilder.build();
+ Response.ResponseBuilder location = Response.status(302).location(redirectUri);
+ return location.build();
+ }
+ }
+
+
+ // http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
+ public static class FragmentRedirectUriBuilder extends OIDCRedirectUriBuilder {
+
+ private StringBuilder fragment;
+
+ protected FragmentRedirectUriBuilder(KeycloakUriBuilder uriBuilder) {
+ super(uriBuilder);
+ }
+
+ @Override
+ public OIDCRedirectUriBuilder addParam(String paramName, String paramValue) {
+ String param = paramName + "=" + Encode.encodeQueryParam(paramValue);
+ if (fragment == null) {
+ fragment = new StringBuilder(param);
+ } else {
+ fragment.append("&").append(param);
+ }
+ return this;
+ }
+
+ @Override
+ public Response build() {
+ if (fragment != null) {
+ uriBuilder.encodedFragment(fragment.toString());
+ }
+ URI redirectUri = uriBuilder.build();
+
+ Response.ResponseBuilder location = Response.status(302).location(redirectUri);
+ return location.build();
+ }
+
+ }
+
+
+ // http://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html
+ public static class FormPostRedirectUriBuilder extends OIDCRedirectUriBuilder {
+
+ private Map<String, String> params = new HashMap<>();
+
+ protected FormPostRedirectUriBuilder(KeycloakUriBuilder uriBuilder) {
+ super(uriBuilder);
+ }
+
+ @Override
+ public OIDCRedirectUriBuilder addParam(String paramName, String paramValue) {
+ params.put(paramName, Encode.encodeQueryParam(paramValue));
+ return this;
+ }
+
+ @Override
+ public Response build() {
+ StringBuilder builder = new StringBuilder();
+ URI redirectUri = uriBuilder.build();
+
+ builder.append("<HTML>");
+ builder.append(" <HEAD>");
+ builder.append(" <TITLE>OIDC Form_Post Response</TITLE>");
+ builder.append(" </HEAD>");
+ builder.append(" <BODY Onload=\"document.forms[0].submit()\">");
+
+ builder.append(" <FORM METHOD=\"POST\" ACTION=\"" + redirectUri.toString() + "\">");
+
+ for (Map.Entry<String, String> param : params.entrySet()) {
+ builder.append(" <INPUT TYPE=\"HIDDEN\" NAME=\"").append(param.getKey())
+ .append("\" VALUE=\"").append(param.getValue()).append("\" />");
+ }
+
+ builder.append(" <NOSCRIPT>");
+ builder.append(" <P>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue .</P>");
+ builder.append(" <INPUT name=\"continue\" TYPE=\"SUBMIT\" VALUE=\"CONTINUE\" />");
+ builder.append(" </NOSCRIPT>");
+ builder.append(" </FORM>");
+ builder.append(" </BODY>");
+ builder.append("</HTML>");
+
+ return Response.status(Response.Status.OK)
+ .type(MediaType.TEXT_HTML_TYPE)
+ .entity(builder.toString()).build();
+ }
+
+ }
+
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseMode.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseMode.java
new file mode 100644
index 0000000..c255ccc
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseMode.java
@@ -0,0 +1,25 @@
+package org.keycloak.protocol.oidc.utils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public enum OIDCResponseMode {
+
+ QUERY, FRAGMENT, FORM_POST;
+
+ public static OIDCResponseMode parse(String responseMode, OIDCResponseType responseType) {
+ if (responseMode == null) {
+ return getDefaultResponseMode(responseType);
+ } else {
+ return Enum.valueOf(OIDCResponseMode.class, responseMode.toUpperCase());
+ }
+ }
+
+ private static OIDCResponseMode getDefaultResponseMode(OIDCResponseType responseType) {
+ if (responseType.isImplicitOrHybridFlow()) {
+ return OIDCResponseMode.FRAGMENT;
+ } else {
+ return OIDCResponseMode.QUERY;
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseType.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseType.java
new file mode 100644
index 0000000..5c96de0
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseType.java
@@ -0,0 +1,72 @@
+package org.keycloak.protocol.oidc.utils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OIDCResponseType {
+
+ public static final String CODE = OIDCLoginProtocol.CODE_PARAM;
+ public static final String TOKEN = "token";
+ public static final String ID_TOKEN = "id_token";
+ public static final String REFRESH_TOKEN = "refresh_token"; // Not officially supported by OIDC
+ public static final String NONE = "none";
+
+ private static final List<String> ALLOWED_RESPONSE_TYPES = Arrays.asList(CODE, TOKEN, ID_TOKEN, REFRESH_TOKEN, NONE);
+
+ private final List<String> responseTypes;
+
+
+ private OIDCResponseType(List<String> responseTypes) {
+ this.responseTypes = responseTypes;
+ }
+
+
+ public static OIDCResponseType parse(String responseTypeParam) {
+ if (responseTypeParam == null) {
+ throw new IllegalStateException("response_type is null");
+ }
+
+ String[] responseTypes = responseTypeParam.trim().split(" ");
+ List<String> allowedTypes = new ArrayList<>();
+ for (String current : responseTypes) {
+ if (ALLOWED_RESPONSE_TYPES.contains(current)) {
+ allowedTypes.add(current);
+ } else {
+ throw new IllegalStateException("Unsupported response_type: " + responseTypeParam);
+ }
+ }
+ return new OIDCResponseType(allowedTypes);
+ }
+
+
+ public boolean hasResponseType(String responseType) {
+ return responseTypes.contains(responseType);
+ }
+
+
+ public boolean isImplicitOrHybridFlow() {
+ return hasResponseType(TOKEN) || hasResponseType(ID_TOKEN) || hasResponseType(REFRESH_TOKEN);
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ boolean first = true;
+ for (String responseType : responseTypes) {
+ if (!first) {
+ builder.append(" ");
+ } else {
+ first = false;
+ }
+ builder.append(responseType);
+ }
+ return builder.toString();
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index c820e4d..0131b6d 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -380,7 +380,8 @@ public class AuthenticationManager {
public static Response redirectAfterSuccessfulFlow(KeycloakSession session, RealmModel realm, UserSessionModel userSession,
ClientSessionModel clientSession,
- HttpRequest request, UriInfo uriInfo, ClientConnection clientConnection) {
+ HttpRequest request, UriInfo uriInfo, ClientConnection clientConnection,
+ EventBuilder event) {
Cookie sessionCookie = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_SESSION_COOKIE);
if (sessionCookie != null) {
@@ -407,7 +408,8 @@ public class AuthenticationManager {
LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod());
protocol.setRealm(realm)
.setHttpHeaders(request.getHttpHeaders())
- .setUriInfo(uriInfo);
+ .setUriInfo(uriInfo)
+ .setEventBuilder(event);
RestartLoginCookie.expireRestartCookie(realm, clientConnection, uriInfo);
return protocol.authenticated(userSession, new ClientSessionCode(realm, clientSession));
@@ -429,7 +431,7 @@ public class AuthenticationManager {
}
event.success();
RealmModel realm = clientSession.getRealm();
- return redirectAfterSuccessfulFlow(session, realm , userSession, clientSession, request, uriInfo, clientConnection);
+ return redirectAfterSuccessfulFlow(session, realm , userSession, clientSession, request, uriInfo, clientConnection, event);
}
@@ -522,9 +524,11 @@ public class AuthenticationManager {
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
protocol.setRealm(context.getRealm())
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
- .setUriInfo(context.getUriInfo());
+ .setUriInfo(context.getUriInfo())
+ .setEventBuilder(event);
+ Response response = protocol.sendError(context.getClientSession(), Error.CONSENT_DENIED);
event.error(Errors.REJECTED_BY_USER);
- return protocol.sendError(context.getClientSession(), Error.CONSENT_DENIED);
+ return response;
}
else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
clientSession.setNote(CURRENT_REQUIRED_ACTION, model.getProviderId());
diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java
index 6b22606..ca1db96 100755
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -112,6 +112,8 @@ public class Messages {
public static final String STANDARD_FLOW_DISABLED = "standardFlowDisabledMessage";
+ public static final String IMPLICIT_FLOW_DISABLED = "implicitFlowDisabledMessage";
+
public static final String INVALID_REDIRECT_URI = "invalidRedirectUriMessage";
public static final String UNSUPPORTED_NAME_ID_FORMAT = "unsupportedNameIdFormatMessage";
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index bc83df5..36e4c6c 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -59,6 +59,8 @@ import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
@@ -546,7 +548,7 @@ public class LoginActionsService {
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processConsent(final MultivaluedMap<String, String> formData) {
- event.event(EventType.LOGIN).detail(Details.RESPONSE_TYPE, "code");
+ event.event(EventType.LOGIN);
if (!checkSsl()) {
@@ -561,38 +563,28 @@ public class LoginActionsService {
return ErrorPage.error(session, Messages.INVALID_ACCESS_CODE);
}
ClientSessionModel clientSession = accessCode.getClientSession();
- event.detail(Details.CODE_ID, clientSession.getId());
- String redirect = clientSession.getRedirectUri();
+ initEvent(clientSession);
+
UserSessionModel userSession = clientSession.getUserSession();
UserModel user = userSession.getUser();
ClientModel client = clientSession.getClient();
- event.client(client)
- .user(user)
- .detail(Details.RESPONSE_TYPE, "code")
- .detail(Details.REDIRECT_URI, redirect);
-
- event.detail(Details.AUTH_METHOD, userSession.getAuthMethod());
- event.detail(Details.USERNAME, userSession.getLoginUsername());
- if (userSession.isRememberMe()) {
- event.detail(Details.REMEMBER_ME, "true");
- }
-
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true);
event.error(Errors.INVALID_CODE);
return ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE);
}
- event.session(userSession);
if (formData.containsKey("cancel")) {
LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod());
protocol.setRealm(realm)
.setHttpHeaders(headers)
- .setUriInfo(uriInfo);
+ .setUriInfo(uriInfo)
+ .setEventBuilder(event);
+ Response response = protocol.sendError(clientSession, Error.CONSENT_DENIED);
event.error(Errors.REJECTED_BY_USER);
- return protocol.sendError(clientSession, Error.CONSENT_DENIED);
+ return response;
}
UserConsentModel grantedConsent = user.getConsentByClient(client.getId());
@@ -613,7 +605,7 @@ public class LoginActionsService {
event.detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED);
event.success();
- return authManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, clientConnection);
+ return authManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, clientConnection, event);
}
@Path("email-verification")
@@ -727,22 +719,27 @@ public class LoginActionsService {
}
private void initEvent(ClientSessionModel clientSession) {
+ UserSessionModel userSession = clientSession.getUserSession();
+
+ String responseType = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
+ if (responseType == null) {
+ responseType = "code";
+ }
+ String respMode = clientSession.getNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
+ OIDCResponseMode responseMode = OIDCResponseMode.parse(respMode, OIDCResponseType.parse(responseType));
+
event.event(EventType.LOGIN).client(clientSession.getClient())
- .user(clientSession.getUserSession().getUser())
- .session(clientSession.getUserSession().getId())
+ .user(userSession.getUser())
+ .session(userSession.getId())
.detail(Details.CODE_ID, clientSession.getId())
.detail(Details.REDIRECT_URI, clientSession.getRedirectUri())
.detail(Details.USERNAME, clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME))
- .detail(Details.RESPONSE_TYPE, "code");
-
- UserSessionModel userSession = clientSession.getUserSession();
-
- if (userSession != null) {
- event.detail(Details.AUTH_METHOD, userSession.getAuthMethod());
- event.detail(Details.USERNAME, userSession.getLoginUsername());
- if (userSession.isRememberMe()) {
- event.detail(Details.REMEMBER_ME, "true");
- }
+ .detail(Details.AUTH_METHOD, userSession.getAuthMethod())
+ .detail(Details.USERNAME, userSession.getLoginUsername())
+ .detail(Details.RESPONSE_TYPE, responseType)
+ .detail(Details.RESPONSE_MODE, responseMode.toString().toLowerCase());
+ if (userSession.isRememberMe()) {
+ event.detail(Details.REMEMBER_ME, "true");
}
}
@@ -827,9 +824,14 @@ public class LoginActionsService {
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
protocol.setRealm(context.getRealm())
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
- .setUriInfo(context.getUriInfo());
- event.detail(Details.CUSTOM_REQUIRED_ACTION, action).error(Errors.REJECTED_BY_USER);
- return protocol.sendError(context.getClientSession(), Error.CONSENT_DENIED);
+ .setUriInfo(context.getUriInfo())
+ .setEventBuilder(event);
+
+ event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
+ Response response = protocol.sendError(context.getClientSession(), Error.CONSENT_DENIED);
+ event.error(Errors.REJECTED_BY_USER);
+ return response;
+
}
throw new RuntimeException("Unreachable");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
index 74c70ea..84e724b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
@@ -8,6 +8,7 @@ import org.junit.Assert;
import org.junit.rules.TestRule;
import org.junit.runners.model.Statement;
import org.keycloak.Config;
+import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.events.admin.AdminEvent;
@@ -134,7 +135,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
return expect(EventType.CLIENT_LOGIN)
.detail(Details.CODE_ID, isCodeId())
.detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
- .detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH)
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)
.removeDetail(Details.CODE_ID)
.session(isUUID());
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java
index f704553..4690308 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java
@@ -218,7 +218,7 @@ public class CustomFlowTest {
.client(clientId)
.user(userId)
.session(accessToken.getSessionState())
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.USERNAME, login)
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GroupTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GroupTest.java
index 97250c4..a5f2388 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GroupTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GroupTest.java
@@ -7,6 +7,7 @@ import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
+import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.GroupResource;
import org.keycloak.admin.client.resource.RealmResource;
@@ -256,7 +257,7 @@ public class GroupTest {
.client(clientId)
.user(userId)
.session(accessToken.getSessionState())
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.USERNAME, login)
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
index d3a47e0..5287189 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
@@ -30,6 +30,7 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
@@ -164,8 +165,8 @@ public class AuthorizationCodeTest {
UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl());
b.replaceQueryParam(OAuth2Constants.RESPONSE_TYPE, "token");
driver.navigate().to(b.build().toURL());
- assertEquals("Invalid parameter: response_type", errorPage.getError());
- events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().detail(Details.RESPONSE_TYPE, "token").assertEvent();
+ assertEquals("Client is not allowed to initiate browser login with given response_type. Implicit flow is disabled for the client.", errorPage.getError());
+ events.expectLogin().error(Errors.NOT_ALLOWED).user((String) null).session((String) null).clearDetails().detail(Details.RESPONSE_TYPE, OIDCResponseType.TOKEN).assertEvent();
}
private void assertCode(String expectedCodeId, String actualCode) {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
index 537a297..898066a 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
@@ -189,7 +189,7 @@ public class ClientAuthSignedJWTTest {
events.expectLogin()
.client("client2")
.session(accessToken.getSessionState())
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.USERNAME, "test-user@localhost")
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java
index 6c205ad..d333f86 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java
@@ -40,6 +40,7 @@ import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
import java.io.IOException;
+import java.net.URL;
/**
* @author <a href="mailto:vrockai@redhat.com">Viliam Rockai</a>
@@ -174,7 +175,10 @@ public class OAuthRedirectUriTest {
OAuthClient.AuthorizationCodeResponse response = oauth.doLogin("test-user@localhost", "password");
Assert.assertNotNull(response.getCode());
- Assert.assertTrue(driver.getCurrentUrl().startsWith("http://localhost:8081/app?code="));
+ URL url = new URL(driver.getCurrentUrl());
+ Assert.assertTrue(url.toString().startsWith("http://localhost:8081/app"));
+ Assert.assertTrue(url.getQuery().contains("code="));
+ Assert.assertTrue(url.getQuery().contains("state="));
}
@Test
@@ -192,7 +196,11 @@ public class OAuthRedirectUriTest {
OAuthClient.AuthorizationCodeResponse response = oauth.doLogin("test-user@localhost", "password");
Assert.assertNotNull(response.getCode());
- Assert.assertTrue(driver.getCurrentUrl().startsWith("http://localhost:8081/app?key=value&code="));
+ URL url = new URL(driver.getCurrentUrl());
+ Assert.assertTrue(url.toString().startsWith("http://localhost:8081/app"));
+ Assert.assertTrue(url.getQuery().contains("key=value"));
+ Assert.assertTrue(url.getQuery().contains("state="));
+ Assert.assertTrue(url.getQuery().contains("code="));
}
@Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
index 884d9f0..a4b2855 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
@@ -319,7 +319,7 @@ public class OfflineTokenTest {
.client("offline-client")
.user(userId)
.session(token.getSessionState())
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, token.getId())
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
@@ -361,7 +361,7 @@ public class OfflineTokenTest {
.client("offline-client")
.user(userId)
.session(token.getSessionState())
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, token.getId())
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
index 74e3d38..b64a683 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
@@ -6,6 +6,7 @@ import org.apache.http.impl.client.DefaultHttpClient;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
+import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
@@ -93,7 +94,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
.client(clientId)
.user(userId)
.session(accessToken.getSessionState())
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.USERNAME, login)
@@ -129,7 +130,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
events.expectLogin()
.client("resource-owner")
.session(accessToken.getSessionState())
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.removeDetail(Details.CODE_ID)
@@ -285,7 +286,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
events.expectLogin()
.client("resource-owner")
.session((String) null)
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.removeDetail(Details.CODE_ID)
.removeDetail(Details.REDIRECT_URI)
.removeDetail(Details.CONSENT)
@@ -307,7 +308,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
.client("resource-owner")
.user((String) null)
.session((String) null)
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.USERNAME, "invalid")
.removeDetail(Details.CODE_ID)
.removeDetail(Details.REDIRECT_URI)