Started support for installed applications

This commit is contained in:
Stian Thorgersen 2014-03-03 09:28:38 +00:00
parent ec309e7e02
commit 87aaaf0b06
13 changed files with 171 additions and 99 deletions

View file

@ -0,0 +1,19 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout; section>
<#if section = "title">
<#if code.success>
Success code=${code.code}
<#else>
Error error=${code.error}
</#if>
<#elseif section = "form">
<div id="kc-code">
<#if code.success>
<p>Please copy this code and paste it into your application:</p>
<textarea id="code">${code.code}</textarea>
<#else>
<p>${code.error}</p>
</#if>
</div>
</#if>
</@layout.registrationLayout>

View file

@ -1,33 +0,0 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayInfo=true; section>
<#if section = "title">
${rb.emailUsernameForgotHeader}
<#elseif section = "header">
${rb.emailUsernameForgotHeader}
<#elseif section = "form">
<form id="kc-username-reminder-form" class="${properties.kcFormClass!}" action="${url.loginUsernameReminderUrl}" method="post">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="email" class="${properties.kcLabelClass!}">${rb.email}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="email" name="email" class="${properties.kcInputClass!}" />
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
<div class="${properties.kcFormOptionsWrapperClass!}">
<span><a href="${url.loginUrl}">${rb.backToLogin}</a></span>
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<input class="btn btn-primary btn-lg" type="submit" value="${rb.submit}"/>
</div>
</div>
</form>
<#elseif section = "info" >
${rb.emailUsernameInstruction}
</#if>
</@layout.registrationLayout>

View file

@ -121,6 +121,12 @@ ol#kc-totp-settings li:first-of-type {
width: 50%;
}
/* Code */
#kc-code textarea {
width: 100%;
height: 8em;
}
/* Social */
#kc-social-providers ul {

View file

@ -27,6 +27,8 @@ public interface LoginForms {
public Response createOAuthGrant();
public Response createCode();
public LoginForms setAccessCode(String accessCodeId, String accessCode);
public LoginForms setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested);

View file

@ -5,6 +5,6 @@ package org.keycloak.login;
*/
public enum LoginFormsPages {
LOGIN, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_VERIFY_EMAIL, OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, LOGIN_USERNAME_REMINDER, REGISTER, ERROR, LOGIN_UPDATE_PROFILE;
LOGIN, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_VERIFY_EMAIL, OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, REGISTER, ERROR, LOGIN_UPDATE_PROFILE, CODE;
}

View file

@ -8,6 +8,7 @@ import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeLoader;
import org.keycloak.login.LoginForms;
import org.keycloak.login.LoginFormsPages;
import org.keycloak.login.freemarker.model.CodeBean;
import org.keycloak.login.freemarker.model.LoginBean;
import org.keycloak.login.freemarker.model.MessageBean;
import org.keycloak.login.freemarker.model.OAuthGrantBean;
@ -178,6 +179,9 @@ public class FreeMarkerLoginForms implements LoginForms {
case OAUTH_GRANT:
attributes.put("oauth", new OAuthGrantBean(accessCode, client, realmRolesRequested, resourceRolesRequested));
break;
case CODE:
attributes.put("code", new CodeBean(accessCode, messageType == MessageType.ERROR ? message : null));
break;
}
try {
@ -197,10 +201,6 @@ public class FreeMarkerLoginForms implements LoginForms {
return createResponse(LoginFormsPages.LOGIN_RESET_PASSWORD);
}
public Response createUsernameReminder() {
return createResponse(LoginFormsPages.LOGIN_USERNAME_REMINDER);
}
public Response createLoginTotp() {
return createResponse(LoginFormsPages.LOGIN_TOTP);
}
@ -218,6 +218,11 @@ public class FreeMarkerLoginForms implements LoginForms {
return createResponse(LoginFormsPages.OAUTH_GRANT);
}
@Override
public Response createCode() {
return createResponse(LoginFormsPages.CODE);
}
public FreeMarkerLoginForms setError(String message) {
this.message = message;
this.messageType = MessageType.ERROR;

View file

@ -29,6 +29,8 @@ public class Templates {
return "error.ftl";
case LOGIN_UPDATE_PROFILE:
return "login-update-profile.ftl";
case CODE:
return "code.ftl";
default:
throw new IllegalArgumentException();
}

View file

@ -0,0 +1,27 @@
package org.keycloak.login.freemarker.model;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class CodeBean {
private final String code;
private final String error;
public CodeBean(String code, String error) {
this.code = code;
this.error = error;
}
public boolean isSuccess() {
return code != null && error == null;
}
public String getCode() {
return code;
}
public String getError() {
return error;
}
}

View file

@ -5,7 +5,7 @@ var Keycloak = function (options) {
return new Keycloak(options);
}
var instance = this;
var kc = this;
if (!options.url) {
var scripts = document.getElementsByTagName('script');
@ -33,7 +33,7 @@ var Keycloak = function (options) {
throw 'clientSecret missing';
}
this.init = function (successCallback, errorCallback) {
kc.init = function (successCallback, errorCallback) {
if (window.oauth.callback) {
delete sessionStorage.oauthToken;
processCallback(successCallback, errorCallback);
@ -44,50 +44,50 @@ var Keycloak = function (options) {
} else if (options.onload) {
switch (options.onload) {
case 'login-required' :
window.location = createLoginUrl(true);
window.location = kc.createLoginUrl(true);
break;
case 'check-sso' :
window.location = createLoginUrl(false);
window.location = kc.createLoginUrl(false);
break;
}
}
}
this.login = function () {
window.location.href = createLoginUrl(true);
kc.login = function () {
window.location.href = kc.createLoginUrl(true);
}
this.logout = function () {
kc.logout = function () {
setToken(undefined);
window.location.href = createLogoutUrl();
window.location.href = kc.createLogoutUrl();
}
this.hasRealmRole = function (role) {
var access = this.realmAccess;
kc.hasRealmRole = function (role) {
var access = kc.realmAccess;
return access && access.roles.indexOf(role) >= 0 || false;
}
this.hasResourceRole = function (role, resource) {
if (!this.resourceAccess) {
kc.hasResourceRole = function (role, resource) {
if (!kc.resourceAccess) {
return false;
}
var access = this.resourceAccess[resource || options.clientId];
var access = kc.resourceAccess[resource || options.clientId];
return access && access.roles.indexOf(role) >= 0 || false;
}
this.loadUserProfile = function (success, error) {
var url = getRealmUrl() + '/account';
kc.loadUserProfile = function (success, error) {
var url = kc.getRealmUrl() + '/account';
var req = new XMLHttpRequest();
req.open('GET', url, true);
req.setRequestHeader('Accept', 'application/json');
req.setRequestHeader('Authorization', 'bearer ' + this.token);
req.setRequestHeader('Authorization', 'bearer ' + kc.token);
req.onreadystatechange = function () {
if (req.readyState == 4) {
if (req.status == 200) {
instance.profile = JSON.parse(req.responseText);
success && success(instance.profile)
kc.profile = JSON.parse(req.responseText);
success && success(kc.profile)
} else {
var response = { status: req.status, statusText: req.status };
if (req.responseText) {
@ -108,22 +108,22 @@ var Keycloak = function (options) {
* @param successCallback
* @param errorCallback
*/
this.onValidAccessToken = function(successCallback, errorCallback) {
if (!this.tokenParsed) {
kc.onValidAccessToken = function(successCallback, errorCallback) {
if (!kc.tokenParsed) {
console.log('no token');
errorCallback();
return;
}
var currTime = new Date().getTime() / 1000;
if (currTime > this.tokenParsed['exp']) {
if (!this.refreshToken) {
if (currTime > kc.tokenParsed['exp']) {
if (!kc.refreshToken) {
console.log('no refresh token');
errorCallback();
return;
}
console.log('calling refresh');
var params = 'grant_type=refresh_token&' + 'refresh_token=' + this.refreshToken;
var url = getRealmUrl() + '/tokens/refresh';
var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken;
var url = kc.getRealmUrl() + '/tokens/refresh';
var req = new XMLHttpRequest();
req.open('POST', url, true, options.clientId, options.clientSecret);
@ -134,8 +134,8 @@ var Keycloak = function (options) {
if (req.status == 200) {
console.log('Refresh Success');
var tokenResponse = JSON.parse(req.responseText);
this.refreshToken = tokenResponse['refresh_token'];
setToken(tokenResponse['access_token'], successCallback);
kc.refreshToken = tokenResponse['refresh_token'];
kc.setToken(tokenResponse['access_token'], successCallback);
} else {
console.log('error on refresh HTTP invoke: ' + req.status);
errorCallback && errorCallback({ authenticated: false, status: req.status, statusText: req.statusText });
@ -150,7 +150,7 @@ var Keycloak = function (options) {
}
function getRealmUrl() {
kc.getRealmUrl = function() {
return options.url + '/auth/rest/realms/' + encodeURIComponent(options.realm);
}
@ -161,7 +161,7 @@ var Keycloak = function (options) {
if (code) {
var params = 'code=' + code;
var url = getRealmUrl() + '/tokens/access/codes';
var url = kc.getRealmUrl() + '/tokens/access/codes';
var req = new XMLHttpRequest();
req.open('POST', url, true, options.clientId, options.clientSecret);
@ -171,8 +171,8 @@ var Keycloak = function (options) {
if (req.readyState == 4) {
if (req.status == 200) {
var tokenResponse = JSON.parse(req.responseText);
instance.refreshToken = tokenResponse['refresh_token'];
setToken(tokenResponse['access_token'], successCallback);
kc.refreshToken = tokenResponse['refresh_token'];
kc.setToken(tokenResponse['access_token'], successCallback);
} else {
errorCallback && errorCallback({ authenticated: false, status: req.status, statusText: req.statusText });
}
@ -189,33 +189,33 @@ var Keycloak = function (options) {
}
}
function setToken(token, successCallback) {
kc.setToken = function(token, successCallback) {
if (token) {
sessionStorage.oauthToken = token;
window.oauth.token = token;
instance.token = token;
kc.token = token;
instance.tokenParsed = JSON.parse(atob(token.split('.')[1]));
instance.authenticated = true;
instance.username = instance.tokenParsed.sub;
instance.realmAccess = instance.tokenParsed.realm_access;
instance.resourceAccess = instance.tokenParsed.resource_access;
kc.tokenParsed = JSON.parse(atob(token.split('.')[1]));
kc.authenticated = true;
kc.username = kc.tokenParsed.sub;
kc.realmAccess = kc.tokenParsed.realm_access;
kc.resourceAccess = kc.tokenParsed.resource_access;
setTimeout(function() {
successCallback && successCallback({ authenticated: instance.authenticated, username: instance.username });
successCallback && successCallback({ authenticated: kc.authenticated, username: kc.username });
}, 0);
} else {
delete sessionStorage.oauthToken;
delete window.oauth.token;
delete instance.token;
delete kc.token;
}
}
function createLoginUrl(prompt) {
kc.createLoginUrl = function(prompt) {
var state = createUUID();
sessionStorage.oauthState = state;
var url = getRealmUrl()
var url = kc.getRealmUrl()
+ '/tokens/login'
+ '?client_id=' + encodeURIComponent(options.clientId)
+ '&redirect_uri=' + getEncodedRedirectUri()
@ -229,17 +229,22 @@ var Keycloak = function (options) {
return url;
}
function createLogoutUrl() {
var url = getRealmUrl()
kc.createLogoutUrl = function() {
var url = kc.getRealmUrl()
+ '/tokens/logout'
+ '?redirect_uri=' + getEncodedRedirectUri();
return url;
}
function getEncodedRedirectUri() {
var url = (location.protocol + '//' + location.hostname + (location.port && (':' + location.port)) + location.pathname);
if (location.hash) {
url += '?redirect_fragment=' + encodeURIComponent(location.hash.substring(1));
var url;
if (options.redirectUri) {
url = options.redirectUri;
} else {
url = (location.protocol + '//' + location.hostname + (location.port && (':' + location.port)) + location.pathname);
if (location.hash) {
url += '?redirect_fragment=' + encodeURIComponent(location.hash.substring(1));
}
}
return encodeURI(url);
}

View file

@ -11,4 +11,6 @@ public interface Constants {
String INTERNAL_ROLE = "KEYCLOAK_";
String ACCOUNT_MANAGEMENT_APP = "account";
String INSTALLED_APP_URN = "urn:ietf:wg:oauth:2.0:oob";
}

View file

@ -38,6 +38,7 @@ import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.resources.TokenService;
import javax.ws.rs.Path;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
@ -79,24 +80,32 @@ public class OAuthFlows {
public Response redirectAccessCode(AccessCodeEntry accessCode, String state, String redirect, boolean rememberMe) {
String code = accessCode.getCode();
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("code", code);
log.debug("redirectAccessCode: state: {0}", state);
if (state != null)
redirectUri.queryParam("state", state);
Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
Cookie remember = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_REMEMBER_ME);
rememberMe = rememberMe || remember != null;
location.cookie(authManager.createLoginCookie(realm, accessCode.getUser(), uriInfo, rememberMe));
return location.build();
if (Constants.INSTALLED_APP_URN.equals(redirect)) {
return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), code).createCode();
} else {
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("code", code);
log.debug("redirectAccessCode: state: {0}", state);
if (state != null)
redirectUri.queryParam("state", state);
Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
Cookie remember = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_REMEMBER_ME);
rememberMe = rememberMe || remember != null;
location.cookie(authManager.createLoginCookie(realm, accessCode.getUser(), uriInfo, rememberMe));
return location.build();
}
}
public Response redirectError(ClientModel client, String error, String state, String redirect) {
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("error", error);
if (state != null) {
redirectUri.queryParam("state", state);
if (Constants.INSTALLED_APP_URN.equals(redirect)) {
return Flows.forms(realm, request, uriInfo).setError(error).createCode();
} else {
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("error", error);
if (state != null) {
redirectUri.queryParam("state", state);
}
return Response.status(302).location(redirectUri.build()).build();
}
return Response.status(302).location(redirectUri.build()).build();
}
public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user) {

View file

@ -38,6 +38,8 @@ import org.json.JSONObject;
import org.junit.Assert;
import org.keycloak.RSATokenVerifier;
import org.keycloak.VerificationException;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.AccessScope;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.UserRepresentation;
@ -156,6 +158,12 @@ public class OAuthClient {
}
}
public void verifyCode(String code) {
if (!RSAProvider.verify(new JWSInput(code), realmPublicKey)) {
throw new RuntimeException("Failed to verify code");
}
}
public String getClientId() {
return clientId;
}

View file

@ -26,8 +26,8 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.OAuthClient.AuthorizationCodeResponse;
@ -36,6 +36,7 @@ import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import java.io.IOException;
@ -73,6 +74,21 @@ public class AuthorizationCodeTest {
Assert.assertNotNull(response.getCode());
Assert.assertEquals("mystate", response.getState());
Assert.assertNull(response.getError());
oauth.verifyCode(response.getCode());
}
@Test
public void authorizationRequestInstalledApp() throws IOException {
oauth.redirectUri(Constants.INSTALLED_APP_URN);
oauth.doLogin("test-user@localhost", "password");
String title = driver.getTitle();
Assert.assertTrue(title.startsWith("Success code="));
String code = driver.findElement(By.id("code")).getText();
oauth.verifyCode(code);
}
@Test
@ -94,6 +110,8 @@ public class AuthorizationCodeTest {
Assert.assertTrue(response.isRedirected());
Assert.assertNotNull(response.getCode());
oauth.verifyCode(response.getCode());
}
@Test
@ -104,6 +122,8 @@ public class AuthorizationCodeTest {
Assert.assertNotNull(response.getCode());
Assert.assertNull(response.getState());
Assert.assertNull(response.getError());
oauth.verifyCode(response.getCode());
}
}