KEYCLOAK-1129 Implicit flow and Hybrid flow support
This commit is contained in:
parent
8d2e4c0316
commit
ef80b64d1c
25 changed files with 623 additions and 141 deletions
|
@ -341,6 +341,17 @@ public class KeycloakUriBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
if (sessionState && (oauth.code || oauth.error || oauth.access_token || oauth.id_token) && oauth.state && oauth.state == sessionState.state) {
|
||||
delete sessionStorage.oauthState;
|
||||
|
||||
oauth.redirectUri = sessionState.redirectUri;
|
||||
oauth.storedNonce = sessionState.nonce;
|
||||
|
||||
if (oauth.fragment) {
|
||||
oauth.newUrl += '#' + oauth.fragment;
|
||||
}
|
||||
|
||||
var sessionState = sessionStorage.oauthState && JSON.parse(sessionStorage.oauthState);
|
||||
|
||||
if (sessionState && (oauth.code || oauth.error) && oauth.state && oauth.state == sessionState.state) {
|
||||
delete sessionStorage.oauthState;
|
||||
|
||||
oauth.redirectUri = sessionState.redirectUri;
|
||||
|
||||
if (oauth.fragment) {
|
||||
oauth.newUrl += '#' + oauth.fragment;
|
||||
}
|
||||
|
||||
return oauth;
|
||||
}
|
||||
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" ) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 $
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue