Skip to content

Commit

Permalink
KEYCLOAK-1129 Implicit flow and Hybrid flow support
Browse files Browse the repository at this point in the history
  • Loading branch information
mposolda committed Nov 27, 2015
1 parent 8d2e4c0 commit ef80b64
Show file tree
Hide file tree
Showing 25 changed files with 621 additions and 139 deletions.
Expand Up @@ -341,6 +341,17 @@ public KeycloakUriBuilder fragment(String fragment) throws IllegalArgumentExcept
return this; 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. * Only replace path params in path of URI. This changes state of URIBuilder.
* *
Expand Down
Expand Up @@ -28,6 +28,7 @@ public RefreshToken(AccessToken token) {
this.subject = token.subject; this.subject = token.subject;
this.issuedFor = token.issuedFor; this.issuedFor = token.issuedFor;
this.sessionState = token.sessionState; this.sessionState = token.sessionState;
this.nonce = token.nonce;
if (token.realmAccess != null) { if (token.realmAccess != null) {
realmAccess = token.realmAccess.clone(); realmAccess = token.realmAccess.clone();
} }
Expand Down
1 change: 1 addition & 0 deletions events/api/src/main/java/org/keycloak/events/Details.java
Expand Up @@ -11,6 +11,7 @@ public interface Details {
String CODE_ID = "code_id"; String CODE_ID = "code_id";
String REDIRECT_URI = "redirect_uri"; String REDIRECT_URI = "redirect_uri";
String RESPONSE_TYPE = "response_type"; String RESPONSE_TYPE = "response_type";
String RESPONSE_MODE = "response_mode";
String AUTH_TYPE = "auth_type"; String AUTH_TYPE = "auth_type";
String AUTH_METHOD = "auth_method"; String AUTH_METHOD = "auth_method";
String IDENTITY_PROVIDER = "identity_provider"; String IDENTITY_PROVIDER = "identity_provider";
Expand Down
8 changes: 7 additions & 1 deletion examples/js-console/src/main/webapp/index.html
Expand Up @@ -106,7 +106,13 @@ <h2>Events</h2>
event('Auth Logout'); 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') + ')'); output('Init Success (' + (authenticated ? 'Authenticated' : 'Not Authenticated') + ')');
}).error(function() { }).error(function() {
output('Init Error'); output('Init Error');
Expand Down
Expand Up @@ -176,7 +176,8 @@ failedLogout=Logout failed
unknownLoginRequesterMessage=Unknown login requester unknownLoginRequesterMessage=Unknown login requester
loginRequesterNotEnabledMessage=Login requester not enabled loginRequesterNotEnabledMessage=Login requester not enabled
bearerOnlyMessage=Bearer-only applications are not allowed to initiate browser login 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 invalidRedirectUriMessage=Invalid redirect uri
unsupportedNameIdFormatMessage=Unsupported NameIDFormat unsupportedNameIdFormatMessage=Unsupported NameIDFormat
invlidRequesterMessage=Invalid requester invlidRequesterMessage=Invalid requester
Expand Down
227 changes: 174 additions & 53 deletions integration/js/src/main/resources/keycloak.js
Expand Up @@ -36,8 +36,41 @@
if (initOptions.onLoad === 'login-required') { if (initOptions.onLoad === 'login-required') {
kc.loginRequired = true; 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 promise = createPromise();


var initPromise = createPromise(); var initPromise = createPromise();
Expand Down Expand Up @@ -132,13 +165,14 @@


kc.createLoginUrl = function(options) { kc.createLoginUrl = function(options) {
var state = createUUID(); var state = createUUID();
var nonce = createUUID();


var redirectUri = adapter.redirectUri(options); var redirectUri = adapter.redirectUri(options);
if (options && options.prompt) { if (options && options.prompt) {
redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'prompt=' + 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'; var action = 'auth';
if (options && options.action == 'register') { if (options && options.action == 'register') {
Expand All @@ -150,7 +184,9 @@
+ '?client_id=' + encodeURIComponent(kc.clientId) + '?client_id=' + encodeURIComponent(kc.clientId)
+ '&redirect_uri=' + encodeURIComponent(redirectUri) + '&redirect_uri=' + encodeURIComponent(redirectUri)
+ '&state=' + encodeURIComponent(state) + '&state=' + encodeURIComponent(state)
+ '&response_type=code'; + '&nonce=' + encodeURIComponent(nonce)
+ '&response_mode=' + encodeURIComponent(kc.responseMode)
+ '&response_type=' + encodeURIComponent(kc.responseType);


if (options && options.prompt) { if (options && options.prompt) {
url += '&prompt=' + encodeURIComponent(options.prompt); url += '&prompt=' + encodeURIComponent(options.prompt);
Expand Down Expand Up @@ -394,6 +430,8 @@
var error = oauth.error; var error = oauth.error;
var prompt = oauth.prompt; var prompt = oauth.prompt;


var timeLocal = new Date().getTime();

if (code) { if (code) {
var params = 'code=' + code + '&grant_type=authorization_code'; var params = 'code=' + code + '&grant_type=authorization_code';
var url = getRealmUrl() + '/protocol/openid-connect/token'; var url = getRealmUrl() + '/protocol/openid-connect/token';
Expand All @@ -412,20 +450,12 @@


req.withCredentials = true; req.withCredentials = true;


var timeLocal = new Date().getTime();

req.onreadystatechange = function() { req.onreadystatechange = function() {
if (req.readyState == 4) { if (req.readyState == 4) {
if (req.status == 200) { if (req.status == 200) {
timeLocal = (timeLocal + new Date().getTime()) / 2;


var tokenResponse = JSON.parse(req.responseText); var tokenResponse = JSON.parse(req.responseText);
setToken(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token']); authSuccess(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();
} else { } else {
kc.onAuthError && kc.onAuthError(); kc.onAuthError && kc.onAuthError();
promise && promise.setError(); promise && promise.setError();
Expand All @@ -441,7 +471,31 @@
} else { } else {
promise && promise.setSuccess(); 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) { function loadConfig(url) {
Expand Down Expand Up @@ -597,53 +651,21 @@
} }


function parseCallback(url) { function parseCallback(url) {
if (url.indexOf('?') != -1) { var oauth = new CallbackParser(url, kc.responseMode).parseUri();
var oauth = {};


oauth.newUrl = url.split('?')[0]; var sessionState = sessionStorage.oauthState && JSON.parse(sessionStorage.oauthState);
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;


if (sessionState && (oauth.code || oauth.error) && oauth.state && oauth.state == sessionState.state) { oauth.redirectUri = sessionState.redirectUri;
delete sessionStorage.oauthState; oauth.storedNonce = sessionState.nonce;


oauth.redirectUri = sessionState.redirectUri; if (oauth.fragment) {

oauth.newUrl += '#' + oauth.fragment;
if (oauth.fragment) {
oauth.newUrl += '#' + oauth.fragment;
}

return oauth;
} }

return oauth;
} }
} }


Expand Down Expand Up @@ -907,6 +929,105 @@


throw 'invalid adapter type: ' + type; 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" ) { if ( typeof module === "object" && module && typeof module.exports === "object" ) {
Expand Down
Expand Up @@ -470,7 +470,8 @@ public void cancelLogin() {
LoginProtocol protocol = getSession().getProvider(LoginProtocol.class, getClientSession().getAuthMethod()); LoginProtocol protocol = getSession().getProvider(LoginProtocol.class, getClientSession().getAuthMethod());
protocol.setRealm(getRealm()) protocol.setRealm(getRealm())
.setHttpHeaders(getHttpRequest().getHttpHeaders()) .setHttpHeaders(getHttpRequest().getHttpHeaders())
.setUriInfo(getUriInfo()); .setUriInfo(getUriInfo())
.setEventBuilder(event);
Response response = protocol.sendError(getClientSession(), Error.CANCELLED_BY_USER); Response response = protocol.sendError(getClientSession(), Error.CANCELLED_BY_USER);
forceChallenge(response); forceChallenge(response);
} }
Expand Down Expand Up @@ -808,7 +809,7 @@ public void evaluateRequiredActionTriggers() {
public Response finishAuthentication() { public Response finishAuthentication() {
event.success(); event.success();
RealmModel realm = clientSession.getRealm(); 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);


} }


Expand Down

0 comments on commit ef80b64

Please sign in to comment.