More design adjustments

This commit is contained in:
Gabriel Cardoso 2013-10-21 18:04:29 -02:00
commit 953b1b2a8c
92 changed files with 1515 additions and 445 deletions

View file

@ -131,8 +131,8 @@ body {
}
.header.rcue .navbar.primary .nav > li .select-rcue select option {
background-color: #fff;
color: black;
padding: 4px 10px;
color: #333;
padding: 0.36363636363636em 0.90909090909091em;
}
.header.rcue .navbar.primary .nav > li a#refresh {
border: none;
@ -313,10 +313,6 @@ body {
top: 70px;
width: 877.5px;
}
/* Page: Realm Users */
.realm-users caption {
display: none;
}
table thead tr th {
font-size: 1.1em;
}
@ -332,6 +328,9 @@ table thead tr:first-child th {
table a:hover {
color: #0099D3;
}
table + .feedback.inline.warning {
margin-top: 1em;
}
.advanced-search-comp {
position: relative;
display: inline-block;

View file

@ -68,8 +68,8 @@ body {
option {
background-color: #fff;
color: black;
padding: 4px 10px;
color: #333;
padding: 0.36363636363636em 0.90909090909091em;
}
}
}
@ -376,13 +376,6 @@ body {
width: 877.5px;
}
/* Page: Realm Users */
.realm-users caption {
display: none;
}
table {
thead tr {
@ -408,6 +401,10 @@ table {
color: #0099D3;
}
+ .feedback.inline.warning {
margin-top: 1em;
}
}
.advanced-search-comp {

View file

@ -55,6 +55,13 @@ textarea {
padding: 0.45em 0.545454545454545em;
height: auto;
}
.input-below {
clear: both;
display: inline-block;
margin-left: 10.9090909090909em;
margin-top: 0.45454545454545em;
padding-left: 3.63636363636364em;
}
input[type="button"],
button,
a.button {
@ -654,6 +661,9 @@ input[type="email"].tiny {
border-bottom: 1px solid #a7d7f1;
color: #4d5258;
}
.select2-container-multi .select2-choices {
width: 40em;
}
.input-group input + .select-rcue {
border-radius: 0 2px 2px 0;
border-left: 0;

View file

@ -56,6 +56,14 @@ textarea {
height: auto;
}
.input-below {
clear: both;
display: inline-block;
margin-left: 10.9090909090909em;
margin-top: 0.45454545454545em;
padding-left: 3.63636363636364em;
}
input[type="button"],
button,
a.button {
@ -795,6 +803,10 @@ input[type="email"] {
}
}
.select2-container-multi .select2-choices {
width: 40em;
}
.input-group input + .select-rcue {
border-radius: 0 2px 2px 0;
border-left: 0;

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 B

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="1000px" height="11px" viewBox="0 0 1000 11" enable-background="new 0 0 1000 11" xml:space="preserve">
<path fill="#E4F1E1" d="M1000,0c0,1.104-0.896,2-2,2H38l-8,9l-8-9H2C0.896,2,0,1.104,0,0"/>
<path fill="#4B9E39" d="M999,0c0,0.551-0.448,1-1,1H38h-0.449l-0.298,0.335L30,9.495l-7.253-8.159L22.449,1H22H2
C1.449,1,1,0.551,1,0 M0,0c0,1.104,0.896,2,2,2h20l8,9l8-9h960c1.104,0,2-0.896,2-2"/>
</svg>

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="17px" height="17px" viewBox="0 0 17 17" enable-background="new 0 0 17 17" xml:space="preserve">
<path fill="#58A846" d="M12.524,4.654l-5.472,5.893L4.57,8.022L3.145,9.425l3.222,3.276C6.555,12.893,6.811,13,7.079,13
c0.006,0,0.013,0,0.019,0c0.273-0.005,0.535-0.122,0.72-0.325L14,6.003L12.524,4.654z"/>
</svg>

After

Width:  |  Height:  |  Size: 691 B

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="svg7384" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="18px" height="17px" viewBox="0 0 18 17" enable-background="new 0 0 18 17" xml:space="preserve">
<g>
<path fill="#EE7700" d="M17.077,12.955L10.745,1.39c-0.518-0.838-1.271-1.343-2.064-1.387L8.572,0C7.773,0,7.057,0.478,6.54,1.364
L0.295,12.951c-0.41,0.738-0.391,1.695,0.047,2.439c0.381,0.646,1.002,1.02,1.703,1.02H15.2c0.77,0,1.498-0.436,1.902-1.137
C17.521,14.551,17.507,13.701,17.077,12.955z M1.61,13.672L7.847,2.099c0.131-0.224,0.396-0.598,0.75-0.598l0,0
c0.295,0.016,0.621,0.27,0.853,0.643l6.32,11.546c0.213,0.369,0.135,0.658,0.033,0.834c-0.133,0.23-0.377,0.387-0.604,0.387H2.045
c-0.203,0-0.336-0.154-0.411-0.281C1.492,14.387,1.428,14,1.61,13.672z"/>
<path fill="#EE7700" d="M8.732,10.672c0.527,0,1.007-0.486,1-1.014V5.72c0.018-0.544-0.486-1.048-1.031-1.031
C8.648,4.696,8.595,4.706,8.544,4.72c-0.459,0.087-0.82,0.533-0.812,1v3.938C7.725,10.186,8.205,10.672,8.732,10.672z"/>
<rect x="7.732" y="11.658" fill="#EE7700" width="1.999" height="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="324px" height="400px" viewBox="0 0 324 400" enable-background="new 0 0 324 400" xml:space="preserve">
<rect x="6.001" opacity="0.07" fill="#FFFFFF" enable-background="new " width="0.997" height="190"/>
<rect x="6" y="209" opacity="0.07" fill="#FFFFFF" enable-background="new " width="1" height="191"/>
<g opacity="0.15">
<path fill="#FFFFFF" d="M6.501,200.066c0,1.047-0.264,1.864-0.791,2.452S4.454,203.4,3.524,203.4c-0.574,0-1.084-0.135-1.529-0.404
c-0.445-0.27-0.789-0.656-1.031-1.16c-0.242-0.504-0.363-1.094-0.363-1.77c0-1.047,0.262-1.862,0.785-2.446
c0.523-0.584,1.25-0.876,2.18-0.876c0.898,0,1.612,0.299,2.142,0.896C6.238,198.237,6.501,199.047,6.501,200.066z M1.608,200.066
c0,0.821,0.164,1.446,0.492,1.875s0.811,0.645,1.447,0.645c0.636,0,1.12-0.214,1.45-0.643c0.33-0.428,0.495-1.053,0.495-1.877
c0-0.816-0.165-1.437-0.495-1.86s-0.817-0.636-1.462-0.636c-0.637,0-1.117,0.209-1.441,0.627
C1.77,198.615,1.608,199.238,1.608,200.066z"/>
<path fill="#FFFFFF" d="M11.136,196.744c0.285,0,0.541,0.023,0.768,0.07l-0.135,0.902c-0.266-0.059-0.5-0.088-0.703-0.088
c-0.52,0-0.964,0.211-1.333,0.633c-0.369,0.422-0.554,0.947-0.554,1.576v3.445H8.206v-6.422h0.803l0.111,1.189h0.047
c0.238-0.418,0.525-0.74,0.861-0.967C10.364,196.855,10.733,196.744,11.136,196.744z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -64,8 +64,8 @@
</div>
<div class="alert-container" data-ng-show="notification" data-ng-click="notification = null">
<div class="alert alert-{{notification.type}}">{{notification.message}}</div>
<div class="feedback-aligner" data-ng-show="notification" data-ng-click="notification = null">
<div class="alert alert-{{notification.type}}"><strong>{{notification.message}}</strong></div> <!-- Needs to be <div class="feedback success"> to have the original design -->
</div>
<div id="wrap">

View file

@ -196,6 +196,20 @@ module.controller('ApplicationDetailCtrl', function($scope, realm, application,
}
}, true);
$scope.deleteWebOrigin = function(index) {
$scope.application.webOrigins.splice(index, 1);
}
$scope.addWebOrigin = function() {
$scope.application.webOrigins.push($scope.newWebOrigin);
$scope.newWebOrigin = "";
}
$scope.deleteRedirectUri = function(index) {
$scope.application.redirectUris.splice(index, 1);
}
$scope.addRedirectUri = function() {
$scope.application.redirectUris.push($scope.newRedirectUri);
$scope.newRedirectUri = "";
}
$scope.save = function() {
if ($scope.applicationForm.$valid) {
@ -208,7 +222,7 @@ module.controller('ApplicationDetailCtrl', function($scope, realm, application,
var l = headers().location;
var id = l.substring(l.lastIndexOf("/") + 1);
$location.url("/realms/" + realm.id + "/applications/" + id);
Notifications.success("Created application");
Notifications.success("The application has been created.");
});
} else {
Application.update({

View file

@ -104,7 +104,7 @@ module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, $ht
}
}
$location.url("/realms/" + id);
Notifications.success("Created realm");
Notifications.success("The realm has been created.");
});
});
} else {
@ -122,7 +122,7 @@ module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, $ht
}
});
$location.url("/realms/" + id);
Notifications.success("Saved changes to realm");
Notifications.success("Your changes have been saved to the realm.");
});
}
} else {
@ -300,7 +300,7 @@ module.controller('RoleDetailCtrl', function($scope, realm, role, Role, $locatio
}, $scope.role, function() {
$scope.changed = false;
role = angular.copy($scope.role);
Notifications.success("Saved changes to role");
Notifications.success("Your changes have been saved to the role.");
});
}
};

View file

@ -151,7 +151,7 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, $locatio
user = angular.copy($scope.user);
$location.url("/realms/" + realm.id + "/users/" + $scope.user.username);
Notifications.success("Created user");
Notifications.success("The user has been created.");
});
} else {
User.update({
@ -160,7 +160,7 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, $locatio
}, $scope.user, function () {
$scope.changed = false;
user = angular.copy($scope.user);
Notifications.success("Saved changes to user");
Notifications.success("Your changes have been saved to the user.");
});
}

View file

@ -37,7 +37,7 @@
required>
</div>
</div>
<div class="form-group">
<div class="form-actions">
<button type="submit" data-ng-click="changePassword()" class="primary" ng-show="password != null">Save
</button>
</div>
@ -53,7 +53,7 @@
</button>
</div>
</div>
<div class="form-group">
<div class="form-actions">
<label></label>
<button type="submit" data-ng-click="changeTotp()" class="primary" ng-show="totp != null">Save
</button>

View file

@ -72,6 +72,36 @@
data-ng-model="application.adminUrl">
</div>
</div>
<div class="form-group">
<label for="newWebOrigin" class="control-label">Web Origin</label>
<div class="controls">
<div ng-repeat="webOrigin in application.webOrigins" class="item-deletable">
<input class="input-small" type="text" data-ng-class="{'input-below':!$first}"
name="webOrigin" id="webOrigin" data-ng-model="webOrigin" readonly />
<button type="button" data-ng-click="deleteWebOrigin($index)" class="btn-delete">
Delete</button>
</div>
<input class="input-small" type="text" name="newWebOrigin" id="newWebOrigin"
placeholder="New Web Origin..." data-ng-model="newWebOrigin"
data-ng-class="{'input-below':application.webOrigins.length}" />
<button data-ng-click="addWebOrigin()" ng-show="newWebOrigin.length > 0">Add</button>
</div>
</div>
<div class="form-group">
<label for="newRedirectUri" class="control-label">Redirect URI</label>
<div class="controls">
<div ng-repeat="redirectUri in application.redirectUris" class="item-deletable">
<input class="input-small" type="text" data-ng-class="{'input-below':!$first}"
name="redirectUri" id="redirectUri" data-ng-model="redirectUri" readonly />
<button type="button" data-ng-click="deleteRedirectUri($index)" class="btn-delete">
Delete</button>
</div>
<input class="input-small" type="text" name="newRedirectUri" id="newRedirectUri"
placeholder="New Redirect URI..." data-ng-model="newRedirectUri"
data-ng-class="{'input-below':application.redirectUris.length}" />
<button data-ng-click="addRedirectUri()" ng-show="newRedirectUri.length > 0">Add</button>
</div>
</div>
</fieldset>
<div class="form-actions" data-ng-show="create">
<button type="submit" data-ng-click="save()" class="primary">Save

View file

@ -46,8 +46,10 @@
<label for="description">Description </label>
<div class="controls">
<input type="text" id="description" name="description" data-ng-model="role.description" autofocus
required>
<textarea rows="5" cols="50" id="description" name="description" data-ng-model="role.description" required></textarea>
<!-- Replaced by the textarea above <input type="text" id="description" name="description" data-ng-model="role.description" autofocus
required> -->
</div>
</div>
</fieldset>

View file

@ -21,21 +21,21 @@
<form name="realmForm" novalidate>
<fieldset class="border-top">
<div class="form-group clearfix">
<label for="user" class="control-label">Required User Credentials</label>
<label for="user" class="control-label two-lines">Required User Credentials</label>
<div class="controls">
<input id="user" type="text" ui-select2="userCredentialOptions" ng-model="realm.requiredCredentials" placeholder="Type a role and enter">
</div>
</div>
<div class="form-group clearfix">
<label for="application" class="control-label">Required Application Credentials</label>
<label for="application" class="control-label two-lines">Required Application Credentials</label>
<div class="controls">
<input id="application" type="text" ui-select2="userCredentialOptions" ng-model="realm.requiredApplicationCredentials" placeholder="Type a role and enter">
</div>
</div>
<div class="form-group clearfix">
<label for="oauth" class="control-label">Required OAuth Credentials</label>
<label for="oauth" class="control-label two-lines">Required OAuth Credentials</label>
<div class="controls">
<input id="oauth" type="text" ui-select2="userCredentialOptions" ng-model="realm.requiredOAuthClientCredentials" placeholder="Type a role and enter">

View file

@ -75,6 +75,20 @@
</label>
</div>
</div>
<div class="form-group clearfix block">
<label for="accountManagement" class="control-label two-lines">User account management</label>
<div class="onoffswitch">
<input type="checkbox" data-ng-model="realm.accountManagement" class="onoffswitch-checkbox"
name="accountManagement" id="accountManagement">
<label for="accountManagement" class="onoffswitch-label">
<span class="onoffswitch-inner">
<span class="onoffswitch-active">ON</span>
<span class="onoffswitch-inactive">OFF</span>
</span>
<span class="onoffswitch-switch"></span>
</label>
</div>
</div>
<div class="form-group clearfix block">
<label for="requireSsl" class="control-label">Require SSL</label>
<div class="onoffswitch">

View file

@ -42,7 +42,6 @@
<div class="controls">
<textarea rows="5" cols="50" id="description" name="description" data-ng-model="role.description" required></textarea>
<!-- Replaced by the textarea above <input type="text" id="description" name="description" data-ng-model="role.description" required> -->
</div>
</div>
</fieldset>

View file

@ -22,7 +22,14 @@ import java.net.URI;
public class JaxrsOAuthClient extends AbstractOAuthClient {
protected static final Logger logger = Logger.getLogger(JaxrsOAuthClient.class);
public Response redirect(UriInfo uriInfo, String redirectUri) {
return redirect(uriInfo, redirectUri, null);
}
public Response redirect(UriInfo uriInfo, String redirectUri, String path) {
String state = getStateCode();
if (path != null) {
state += "#" + path;
}
URI url = UriBuilder.fromUri(authUrl)
.queryParam("client_id", clientId)
@ -58,7 +65,7 @@ public class JaxrsOAuthClient extends AbstractOAuthClient {
return uriInfo.getQueryParameters().getFirst("code");
}
public void checkStateCookie(UriInfo uriInfo, HttpHeaders headers) {
public String checkStateCookie(UriInfo uriInfo, HttpHeaders headers) {
Cookie stateCookie = headers.getCookies().get(stateCookieName);
if (stateCookie == null) throw new BadRequestException("state cookie not set");
String state = uriInfo.getQueryParameters().getFirst("state");
@ -66,5 +73,10 @@ public class JaxrsOAuthClient extends AbstractOAuthClient {
if (!state.equals(stateCookie.getValue())) {
throw new BadRequestException("state parameter invalid");
}
if (state.indexOf('#') != -1) {
return state.substring(state.indexOf('#') + 1);
} else {
return null;
}
}
}

View file

@ -21,6 +21,7 @@ public class ApplicationRepresentation {
protected List<UserRoleMappingRepresentation> roleMappings;
protected List<ScopeMappingRepresentation> scopeMappings;
protected List<String> redirectUris;
protected List<String> webOrigins;
public String getSelf() {
return self;
@ -155,4 +156,12 @@ public class ApplicationRepresentation {
public void setRedirectUris(List<String> redirectUris) {
this.redirectUris = redirectUris;
}
public List<String> getWebOrigins() {
return webOrigins;
}
public void setWebOrigins(List<String> webOrigins) {
this.webOrigins = webOrigins;
}
}

View file

@ -16,6 +16,7 @@ public class RealmRepresentation {
protected Integer accessCodeLifespan;
protected Integer accessCodeLifespanUserAction;
protected Boolean enabled;
protected Boolean accountManagement;
protected Boolean sslNotRequired;
protected Boolean cookieLoginAllowed;
protected Boolean registrationAllowed;
@ -101,6 +102,14 @@ public class RealmRepresentation {
this.enabled = enabled;
}
public Boolean isAccountManagement() {
return accountManagement;
}
public void setAccountManagement(Boolean accountManagement) {
this.accountManagement = accountManagement;
}
public Boolean isSslNotRequired() {
return sslNotRequired;
}

View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<script src="keycloak.js"></script>
</head>
<body>
<script>
keycloak.init({
clientId : '57572475438.apps.googleusercontent.com',
clientSecret : 'xyfsPS9maRTz5fj0pOxf0zjD'
});
if (keycloak.authenticated) {
document.write('<h2>Token</h2><pre>' + keycloak.token + '</pre>');
document.write('<h2>Token info</h2><pre>' + JSON.stringify(keycloak.tokenInfo, undefined, 4) + '</pre>');
document.write('<h2>Profile</h2><pre>' + JSON.stringify(keycloak.profile(true), undefined, 4) + '</pre>');
document.write('<h2>Contacts</h2><pre>' + keycloak.contacts(true) + '</pre>');
} else {
document.write('<a href="#" id="login" onclick="keycloak.login()">Login</a>');
}
</script>
</body>
</html>

View file

@ -0,0 +1,139 @@
window.keycloak = (function () {
var kc = {};
var config = {
clientId: null,
clientSecret: null
};
kc.init = function (c) {
for (var prop in config) {
if (c[prop]) {
config[prop] = c[prop];
}
if (!config[prop]) {
throw new Error(prop + ' not defined');
}
}
loadToken();
if (kc.token) {
kc.user = kc.tokenInfo.user_id;
kc.authenticated = true;
} else {
kc.authenticated = false;
kc.user = null;
}
}
kc.login = function () {
var clientId = encodeURIComponent(config.clientId);
var redirectUri = encodeURIComponent(window.location.href);
var state = encodeURIComponent(createUUID());
var scope = encodeURIComponent('https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/plus.login');
var url = 'https://accounts.google.com/o/oauth2/auth?response_type=token&client_id=' + clientId + '&redirect_uri=' + redirectUri
+ '&state=' + state + '&scope=' + scope;
sessionStorage.state = state;
window.location.href = url;
}
function parseToken(token) {
return JSON.parse(atob(token.split('.')[1]));
}
kc.profile = function(header) {
var url = 'https://www.googleapis.com/oauth2/v1/userinfo'
if (!header) {
url = url + '?access_token=' + kc.token;
}
var http = new XMLHttpRequest();
http.open('GET', url, false);
if (header) {
http.setRequestHeader('Authorization', 'Bearer ' + kc.token);
}
http.send();
if (http.status == 200) {
return JSON.parse(http.responseText);
}
}
kc.contacts = function(header) {
var url = 'https://www.googleapis.com/plus/v1/people/me';
if (!header) {
url = url + '?access_token=' + kc.token;
}
var http = new XMLHttpRequest();
http.open('GET', url, false);
if (header) {
http.setRequestHeader('Authorization', 'Bearer ' + kc.token);
}
http.send();
if (http.status == 200) {
return http.responseText;
}
}
return kc;
function loadToken() {
var params = {}
var queryString = location.hash.substring(1)
var regex = /([^&=]+)=([^&]*)/g, m;
while (m = regex.exec(queryString)) {
params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
}
var token = params['access_token'];
var state = params['state'];
if (token && state === sessionStorage.state) {
window.history.replaceState({}, document.title, location.protocol + "//" + location.host + location.pathname);
kc.token = token;
var url = 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' + token;
var http = new XMLHttpRequest();
http.open('GET', url, false);
http.send();
if (http.status == 200) {
kc.tokenInfo = JSON.parse(http.responseText);
}
}
return undefined;
}
function getQueryParam(name) {
console.debug(window.location.hash);
var params = window.location.hash.substring(1).split('&');
for (var i = 0; i < params.length; i++) {
var p = params[i].split('=');
if (decodeURIComponent(p[0]) == name) {
return p[1];
}
}
}
function createUUID() {
var s = [];
var hexDigits = '0123456789abcdef';
for (var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = '4';
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
s[8] = s[13] = s[18] = s[23] = '-';
var uuid = s.join('');
return uuid;
}
})();

View file

@ -0,0 +1,222 @@
<<<<<<< Updated upstream
window.keycloak = (function() {
var kc = {};
var config = null;
kc.init = function(c) {
config = c;
var token = getTokenFromCode();
if (token) {
var t = parseToken(token);
kc.user = t.prn;
kc.authenticated = true;
} else {
kc.authenticated = false;
}
}
kc.login = function() {
var clientId = encodeURIComponent(config.clientId);
var redirectUri = encodeURIComponent(window.location.href);
var state = encodeURIComponent(createUUID());
var realm = encodeURIComponent(config.realm);
var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/login?response_type=code&client_id=' + clientId + '&redirect_uri=' + redirectUri
+ '&state=' + state;
window.location.href = url;
}
return kc;
function parseToken(token) {
return JSON.parse(atob(token.split('.')[1]));
}
function getTokenFromCode() {
var code = getQueryParam('code');
if (code) {
window.history.replaceState({}, document.title, location.protocol + "//" + location.host + location.pathname);
var clientId = encodeURIComponent(config.clientId);
var clientSecret = encodeURIComponent(config.clientSecret);
var realm = encodeURIComponent(config.realm);
var params = 'code=' + code + '&client_id=' + config.clientId + '&password=' + config.clientSecret;
var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/access/codes'
var http = new XMLHttpRequest();
http.open('POST', url, false);
http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
http.send(params);
if (http.status == 200) {
return JSON.parse(http.responseText)['access_token'];
}
}
return undefined;
}
function getQueryParam(name) {
var params = window.location.search.substring(1).split('&');
for ( var i = 0; i < params.length; i++) {
var p = params[i].split('=');
if (decodeURIComponent(p[0]) == name) {
return p[1];
}
}
}
function createUUID() {
var s = [];
var hexDigits = '0123456789abcdef';
for ( var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = '4';
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
s[8] = s[13] = s[18] = s[23] = '-';
var uuid = s.join('');
return uuid;
}
=======
window.keycloak = (function () {
var kc = {};
var config = {
baseUrl : null,
clientId : null,
clientSecret: null,
realm: null
};
kc.init = function (c) {
for (var prop in config) {
if (c[prop]) {
config[prop] = c[prop];
}
if (!config[prop]) {
throw new Error(prop + 'not defined');
}
}
var token = getTokenFromCode();
if (token) {
var t = parseToken(token);
kc.user = t.prn;
kc.authenticated = true;
} else {
kc.authenticated = false;
}
}
kc.login = function () {
var clientId = encodeURIComponent(config.clientId);
var redirectUri = encodeURIComponent(window.location.href);
var realm = encodeURIComponent(config.realm);
var state = encodeURIComponent(createUUID());
var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/login?response_type=code&client_id=' + clientId + '&redirect_uri=' + redirectUri
+ '&state=' + state;
sessionStorage.state = state;
window.location.href = url;
}
return kc;
function parseToken(token) {
var t = base64Decode(token.split('.')[1]);
return JSON.parse(t);
}
function getTokenFromCode() {
var code = getQueryParam('code');
var state = getQueryParam('state');
if (code) {
if (state && state === sessionStorage.state) {
window.history.replaceState({}, document.title, location.protocol + "//" + location.host + location.pathname);
var clientId = encodeURIComponent(config.clientId);
var clientSecret = encodeURIComponent(config.clientSecret);
var realm = encodeURIComponent(config.realm);
var params = 'code=' + code + '&client_id=' + clientId + '&password=' + clientSecret;
var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/access/codes'
var http = new XMLHttpRequest();
http.open('POST', url, false);
http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
http.send(params);
if (http.status == 200) {
return JSON.parse(http.responseText)['access_token'];
}
}
}
return undefined;
}
function getQueryParam(name) {
var params = window.location.search.substring(1).split('&');
for (var i = 0; i < params.length; i++) {
var p = params[i].split('=');
if (decodeURIComponent(p[0]) == name) {
return p[1];
}
}
}
function createUUID() {
var s = [];
var hexDigits = '0123456789abcdef';
for (var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = '4';
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
s[8] = s[13] = s[18] = s[23] = '-';
var uuid = s.join('');
return uuid;
}
function base64Decode(data) {
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var o1, o2, o3, h1, h2, h3, h4, bits, i = 0,
ac = 0,
dec = "",
tmp_arr = [];
if (!data) {
return data;
}
data += '';
do {
h1 = b64.indexOf(data.charAt(i++));
h2 = b64.indexOf(data.charAt(i++));
h3 = b64.indexOf(data.charAt(i++));
h4 = b64.indexOf(data.charAt(i++));
bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;
o1 = bits >> 16 & 0xff;
o2 = bits >> 8 & 0xff;
o3 = bits & 0xff;
if (h3 == 64) {
tmp_arr[ac++] = String.fromCharCode(o1);
} else if (h4 == 64) {
tmp_arr[ac++] = String.fromCharCode(o1, o2);
} else {
tmp_arr[ac++] = String.fromCharCode(o1, o2, o3);
}
} while (i < data.length);
dec = tmp_arr.join('');
return dec;
}
>>>>>>> Stashed changes
})();

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<script src="http://code.jquery.com/jquery-2.0.3.js"></script>
</head>
<body>
<script>
$.ajax('https://baas.kinvey.com/appdata/kid_PVD-jo1HqO');
</script>
</body>
</html>

View file

@ -0,0 +1,60 @@
{
"id": "test",
"realm": "test",
"enabled": true,
"tokenLifespan": 300,
"accessCodeLifespan": 10,
"accessCodeLifespanUserAction": 600,
"sslNotRequired": true,
"cookieLoginAllowed": true,
"registrationAllowed": true,
"resetPasswordAllowed": true,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ],
"requiredApplicationCredentials": [ "password" ],
"requiredOAuthClientCredentials": [ "password" ],
"defaultRoles": [ "user" ],
"users" : [
{
"username" : "test-user@localhost",
"enabled": true,
"email" : "test-user@localhost",
"credentials" : [
{ "type" : "password",
"value" : "password" }
]
}
],
"roles": [
{
"name": "user",
"description": "Have User privileges"
},
{
"name": "admin",
"description": "Have Administrator privileges"
}
],
"roleMappings": [
{
"username": "test-user@localhost",
"roles": ["user"]
}
],
"applications": [
{
"name": "test-app",
"enabled": true,
"adminUrl": "http://localhost:8081/app/logout",
"useRealmMappings": true,
"webOrigins": [ "http://localhost", "http://localhost:8000", "http://localhost:8080" ],
"credentials": [
{
"type": "password",
"value": "password"
}
]
}
]
}

View file

@ -10,13 +10,14 @@
clientId : 'test-app',
clientSecret : 'password',
baseUrl : 'http://localhost:8081/auth-server',
realm : 'test'
realm : 'test',
redirectUri : 'http://localhost/js'
});
if (keycloak.authenticated) {
document.write('User: ' + keycloak.user);
} else {
document.write('<a href="#" id="login" onclick="keycloak.login()">Login</a>');
document.write('<a href="#" id="login" onclick="keycloak.login(location.hash)">Login</a>');
}
</script>

View file

@ -1,120 +1,124 @@
window.keycloak = (function() {
var kc = {};
var config = null;
window.keycloak = (function () {
var kc = {};
var config = {
baseUrl: null,
clientId: null,
clientSecret: null,
realm: null,
redirectUri: null
};
kc.init = function(c) {
config = c;
kc.init = function (c) {
for (var prop in config) {
if (c[prop]) {
config[prop] = c[prop];
}
var token = getTokenFromCode();
if (token) {
var t = parseToken(token);
kc.user = t.prn;
kc.authenticated = true;
} else {
kc.authenticated = false;
}
}
if (!config[prop]) {
throw new Error(prop + 'not defined');
}
}
kc.login = function() {
var clientId = encodeURIComponent(config.clientId);
var redirectUri = encodeURIComponent(window.location.href);
var state = encodeURIComponent(createUUID());
var realm = encodeURIComponent(config.realm);
var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/login?response_type=code&client_id=' + clientId + '&redirect_uri=' + redirectUri
+ '&state=' + state;
window.location.href = url;
}
processCallback();
}
return kc;
kc.login = function () {
window.location.href = getLoginUrl();
}
function parseToken(token) {
var t = base64Decode(token.split('.')[1]);
return JSON.parse(t);
}
return kc;
function getTokenFromCode() {
var code = getQueryParam('code');
if (code) {
window.history.replaceState({}, document.title, location.protocol + "//" + location.host + location.pathname);
function getLoginUrl(fragment) {
var state = createUUID();
if (fragment) {
state += '#' + fragment;
}
sessionStorage.state = state;
var url = config.baseUrl + '/rest/realms/' + encodeURIComponent(config.realm) + '/tokens/login?response_type=code&client_id='
+ encodeURIComponent(config.clientId) + '&redirect_uri=' + encodeURIComponent(config.redirectUri) + '&state=' + encodeURIComponent(state);
return url;
}
var clientId = encodeURIComponent(config.clientId);
var clientSecret = encodeURIComponent(config.clientSecret);
var realm = encodeURIComponent(config.realm);
function parseToken(token) {
return JSON.parse(atob(token.split('.')[1]));
}
var params = 'code=' + code + '&client_id=' + config.clientId + '&password=' + config.clientSecret;
var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/access/codes'
function processCallback() {
var code = getQueryParam('code');
var error = getQueryParam('error');
var state = getQueryParam('state');
var http = new XMLHttpRequest();
http.open('POST', url, false);
http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
if (!(code || error)) {
return false;
}
http.send(params);
if (http.status == 200) {
return JSON.parse(http.responseText)['access_token'];
}
}
return undefined;
}
if (state != sessionStorage.state) {
console.error('Invalid state');
return true;
}
function getQueryParam(name) {
var params = window.location.search.substring(1).split('&');
for ( var i = 0; i < params.length; i++) {
var p = params[i].split('=');
if (decodeURIComponent(p[0]) == name) {
return p[1];
}
}
}
if (code) {
console.info('Received code');
function createUUID() {
var s = [];
var hexDigits = '0123456789abcdef';
for ( var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = '4';
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
s[8] = s[13] = s[18] = s[23] = '-';
var uuid = s.join('');
return uuid;
}
var clientId = encodeURIComponent(config.clientId);
var clientSecret = encodeURIComponent(config.clientSecret);
var realm = encodeURIComponent(config.realm);
function base64Decode (data) {
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var o1, o2, o3, h1, h2, h3, h4, bits, i = 0,
ac = 0,
dec = "",
tmp_arr = [];
var params = 'code=' + code + '&client_id=' + clientId + '&password=' + clientSecret;
var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/access/codes'
if (!data) {
return data;
}
var http = new XMLHttpRequest();
http.open('POST', url, false);
http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
data += '';
http.send(params);
if (http.status == 200) {
kc.token = JSON.parse(http.responseText)['access_token'];
kc.tokenParsed = parseToken(kc.token);
kc.authenticated = true;
kc.user = kc.tokenParsed.prn;
do {
h1 = b64.indexOf(data.charAt(i++));
h2 = b64.indexOf(data.charAt(i++));
h3 = b64.indexOf(data.charAt(i++));
h4 = b64.indexOf(data.charAt(i++));
console.info('Authenticated');
}
bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;
updateLocation(state);
return true;
} else if (error) {
console.info('Error ' + error);
updateLocation(state);
return true;
}
}
o1 = bits >> 16 & 0xff;
o2 = bits >> 8 & 0xff;
o3 = bits & 0xff;
function updateLocation(state) {
var fragment = '';
if (state && state.indexOf('#') != -1) {
fragment = state.substr(state.indexOf('#'));
}
if (h3 == 64) {
tmp_arr[ac++] = String.fromCharCode(o1);
} else if (h4 == 64) {
tmp_arr[ac++] = String.fromCharCode(o1, o2);
} else {
tmp_arr[ac++] = String.fromCharCode(o1, o2, o3);
}
} while (i < data.length);
window.history.replaceState({}, document.title, location.protocol + "//" + location.host + location.pathname + fragment);
}
dec = tmp_arr.join('');
function getQueryParam(name) {
var params = window.location.search.substring(1).split('&');
for (var i = 0; i < params.length; i++) {
var p = params[i].split('=');
if (decodeURIComponent(p[0]) == name) {
return p[1];
}
}
}
return dec;
}
function createUUID() {
var s = [];
var hexDigits = '0123456789abcdef';
for (var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = '4';
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
s[8] = s[13] = s[18] = s[23] = '-';
var uuid = s.join('');
return uuid;
}
})();

View file

@ -8,6 +8,7 @@
"sslNotRequired": true,
"cookieLoginAllowed": true,
"registrationAllowed": true,
"accountManagement": true,
"resetPasswordAllowed": true,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
@ -48,6 +49,7 @@
"enabled": true,
"adminUrl": "http://localhost:8081/app/logout",
"useRealmMappings": true,
"webOrigins": [ "http://localhost", "http://localhost:8000", "http://localhost:8080" ],
"credentials": [
{
"type": "password",

View file

@ -26,18 +26,18 @@ import org.keycloak.services.resources.flows.FormFlows;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ErrorBean {
public class MessageBean {
private String summary;
private FormFlows.ErrorType type;
private FormFlows.MessageType type;
// Message is considered ERROR by default
public ErrorBean(String summary) {
this(summary, FormFlows.ErrorType.ERROR);
public MessageBean(String summary) {
this(summary, FormFlows.MessageType.ERROR);
}
public ErrorBean(String summary, FormFlows.ErrorType type) {
public MessageBean(String summary, FormFlows.MessageType type) {
this.summary = summary;
this.type = type;
}
@ -47,15 +47,15 @@ public class ErrorBean {
}
public boolean isSuccess(){
return FormFlows.ErrorType.SUCCESS.equals(this.type);
return FormFlows.MessageType.SUCCESS.equals(this.type);
}
public boolean isWarning(){
return FormFlows.ErrorType.WARNING.equals(this.type);
return FormFlows.MessageType.WARNING.equals(this.type);
}
public boolean isError(){
return FormFlows.ErrorType.ERROR.equals(this.type);
return FormFlows.MessageType.ERROR.equals(this.type);
}
}

View file

@ -1,70 +0,0 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.keycloak.forms;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jboss.resteasy.logging.Logger;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@WebServlet(urlPatterns = "/forms/qrcode")
public class QRServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Logger log = Logger.getLogger(QRServlet.class);
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String[] size = req.getParameter("size").split("x");
int width = Integer.parseInt(size[0]);
int height = Integer.parseInt(size[1]);
String contents = req.getParameter("contents");
try {
QRCodeWriter writer = new QRCodeWriter();
BitMatrix bitMatrix = writer.encode(contents, BarcodeFormat.QR_CODE, width, height);
MatrixToImageWriter.writeToStream(bitMatrix, "png", resp.getOutputStream());
resp.setContentType("image/png");
} catch (Exception e) {
log.warn("Failed to generate qr code", e);
resp.sendError(500);
}
}
}

View file

@ -78,7 +78,7 @@ public class TotpBean {
public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException {
String contents = URLEncoder.encode("otpauth://totp/keycloak?secret=" + totpSecretEncoded, "utf-8");
return contextUrl + "/forms/qrcode" + "?size=246x246&contents=" + contents;
return contextUrl + "/rest/qrcode" + "?size=246x246&contents=" + contents;
}
public UserBean getUser() {

View file

@ -124,6 +124,10 @@ public class UrlBean {
return Urls.accountTotpRemove(baseURI, realm.getId()).toString();
}
public String getLogoutUrl() {
return Urls.accountLogout(baseURI, realm.getId()).toString();
}
public String getLoginPasswordResetUrl() {
return Urls.loginPasswordReset(baseURI, realm.getId()).toString();
}

View file

@ -32,7 +32,7 @@ import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.jboss.resteasy.logging.Logger;
import org.keycloak.forms.ErrorBean;
import org.keycloak.forms.MessageBean;
import org.keycloak.forms.LoginBean;
import org.keycloak.forms.OAuthGrantBean;
import org.keycloak.forms.RealmBean;
@ -69,8 +69,7 @@ public class FormServiceImpl implements FormService {
commandMap.put(Pages.TOTP, new CommandTotp());
commandMap.put(Pages.LOGIN_CONFIG_TOTP, new CommandTotp());
commandMap.put(Pages.LOGIN_TOTP, new CommandLoginTotp());
commandMap.put(Pages.LOGIN_VERIFY_EMAIL, new CommandLoginTotp());
commandMap.put(Pages.ERROR, new CommandError());
commandMap.put(Pages.LOGIN_VERIFY_EMAIL, new CommandVerifyEmail());
commandMap.put(Pages.OAUTH_GRANT, new CommandOAuthGrant());
}
@ -82,8 +81,8 @@ public class FormServiceImpl implements FormService {
Map<String, Object> attributes = new HashMap<String, Object>();
if (dataBean.getError() != null){
attributes.put("message", new ErrorBean(dataBean.getError(), dataBean.getErrorType()));
if (dataBean.getMessage() != null){
attributes.put("message", new MessageBean(dataBean.getMessage(), dataBean.getMessageType()));
}
RealmBean realm = new RealmBean(dataBean.getRealm());
@ -161,9 +160,6 @@ public class FormServiceImpl implements FormService {
private class CommandLoginTotp implements Command {
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
if (dataBean.getError() != null){
attributes.put("error", new ErrorBean(dataBean.getError()));
}
RealmBean realm = new RealmBean(dataBean.getRealm());
@ -206,10 +202,6 @@ public class FormServiceImpl implements FormService {
private class CommandLogin implements Command {
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
if (dataBean.getError() != null){
attributes.put("error", new ErrorBean(dataBean.getError()));
}
RealmBean realm = new RealmBean(dataBean.getRealm());
attributes.put("realm", realm);
@ -230,9 +222,6 @@ public class FormServiceImpl implements FormService {
private class CommandRegister implements Command {
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
if (dataBean.getError() != null){
attributes.put("error", new ErrorBean(dataBean.getError()));
}
RealmBean realm = new RealmBean(dataBean.getRealm());
@ -252,14 +241,6 @@ public class FormServiceImpl implements FormService {
}
}
private class CommandError implements Command {
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
if (dataBean.getError() != null){
attributes.put("error", new ErrorBean(dataBean.getError()));
}
}
}
private class CommandOAuthGrant implements Command {
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
@ -274,6 +255,20 @@ public class FormServiceImpl implements FormService {
}
}
private class CommandVerifyEmail implements Command {
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean) {
RealmBean realm = new RealmBean(dataBean.getRealm());
attributes.put("realm", realm);
UrlBean url = new UrlBean(realm, dataBean.getBaseURI());
url.setSocialRegistration(dataBean.getSocialRegistration());
attributes.put("url", url);
}
}
private interface Command {
public void exec(Map<String, Object> attributes, FormServiceDataBean dataBean);
}

View file

@ -291,9 +291,6 @@ a.zocial:before {
.rcue-login-register.reset .background-area .section.app-form {
width: 43.2em;
}
.rcue-login-register.reset .feedback {
left: 35.7em;
}
.rcue-login-register.oauth .form-actions {
margin-bottom: 0;

View file

@ -12,7 +12,7 @@
<#elseif section = "form">
<p class="instruction">Something happened and we could not process your request.</p>
<p id="error-summary" class="instruction second">${error.summary}</p>
<p id="error-summary" class="instruction second">${message.summary}</p>
<#elseif section = "info" >

View file

@ -8,11 +8,6 @@
Google Authenticator Setup
<#elseif section = "feedback">
<div class="feedback warning show">
<p><strong>Your account is not enabled because you need to set up the Google Authenticator.</strong><br>Please follow the steps below.</p>
</div>
<#elseif section = "form">
<div id="form">

View file

@ -11,15 +11,6 @@
<#elseif section = "form">
<div id="form">
<#if message?has_content>
<#if message.success>
<div class="feedback success bottom-left show"><p><strong>${rb.getString('successHeader')}</strong> ${rb.getString(message.summary)}</p></div>
</#if>
<#if message.error>
<div class="feedback error bottom-left show"><p><strong>${rb.getString('errorHeader')}</strong><br/>${rb.getString(message.summary)}</p></div>
</#if>
</#if>
<p class="instruction">${rb.getString('emailInstruction')}</p>
<form action="${url.loginPasswordResetUrl}" method="post">
<div>

View file

@ -8,11 +8,6 @@
Email verification
<#elseif section = "feedback">
<div class="feedback warning show">
<p><strong>Your account is not enabled because you need to verify your email.</strong><br>Please follow the steps below.</p>
</div>
<#elseif section = "form">
<div class="app-form">

View file

@ -17,7 +17,11 @@
<body class="rcue-login-register ${bodyClass}">
<div class="feedback-aligner">
<#nested "feedback">
<#if message?has_content && message.warning>
<div class="feedback warning show">
<p><strong>${rb.getString('actionWarningHeader')} ${rb.getString(message.summary)}</strong><br/>${rb.getString('actionFollow')}</p>
</div>
</#if>
</div>
<#if (template.themeConfig.logo)?has_content>
<h1>
@ -33,18 +37,26 @@
<div class="background-area">
<div class="form-area clearfix">
<div class="section app-form">
<#if !isErrorPage && message?has_content>
<#if message.error>
<div class="feedback error bottom-left show">
<p>
<strong id="loginError">${rb.getString(message.summary)}</strong><br/>${rb.getString('emailErrorInfo')}
</p>
</div>
<#elseif message.success>
<div class="feedback success bottom-left show">
<p>
<strong>${rb.getString('successHeader')}</strong> ${rb.getString(message.summary)}
</p>
</div>
</#if>
</#if>
<h3>Application login area</h3>
<#nested "form">
</div>
<#if !isErrorPage && error?has_content>
<div class="feedback error bottom-left show">
<p>
<strong id="loginError">${rb.getString(error.summary)}</strong>
</p>
</div>
</#if>
<div class="section info-area">
<h3>Info area</h3>
<#nested "info">

View file

@ -31,10 +31,10 @@
<div class="form-area ${(realm.social)?string('social','')} clearfix">
<div class="section app-form">
<h3>Application login area</h3>
<#if error?has_content>
<#if message?has_content && message.error>
<div class="feedback error bottom-left show">
<p>
<strong id="loginError">${rb.getString(error.summary)}</strong><br/>${rb.getString('emailErrorInfo')}
<strong id="loginError">${rb.getString(message.summary)}</strong><br/>${rb.getString('emailErrorInfo')}
</p>
</div>
</#if>

View file

@ -54,6 +54,9 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="icon-user">Icon: user</span>
${user.firstName!''} ${user.lastName!''}</a>
</li>
<li>
<a href="${url.logoutUrl}">Logout</a>
</li>
</ul>
</div>
</div>

View file

@ -31,6 +31,7 @@ missingLastName=Please specify last name
missingEmail=Please specify email
missingUsername=Please specify username
missingPassword=Please specify password
notMatchPassword=Passwords don't match
missingTotp=Please specify authenticator code
invalidPasswordExisting=Invalid existing password
@ -43,6 +44,12 @@ successTotpRemoved=Google authenticator removed.
usernameExists=Username already exists
error=A system error has occured, contact admin
actionWarningHeader=Your account is not enabled.
actionTotpWarning=You need to set up the Google Authenticator to activate your account.
actionProfileWarning=You need to update your user profile to activate your account.
actionPasswordWarning=You need to change your password to activate your account.
actionEmailWarning=You need to verify your email address to activate your account.
actionFollow=Please follow the steps below.
successHeader=Success!
errorHeader=Error!

View file

@ -11,4 +11,6 @@ public interface Constants {
String APPLICATION_ROLE = "KEYCLOAK_APPLICATION";
String IDENTITY_REQUESTER_ROLE = "KEYCLOAK_IDENTITY_REQUESTER";
String WILDCARD_ROLE = "*";
String ACCOUNT_MANAGEMENT_APPLICATION = "Account Management";
}

View file

@ -35,6 +35,14 @@ public interface UserModel {
void removeRequiredAction(RequiredAction action);
Set<String> getWebOrigins();
void setWebOrigins(Set<String> webOrigins);
void addWebOrigin(String webOrigin);
void removeWebOrigin(String webOrigin);
Set<String> getRedirectUris();
void setRedirectUris(Set<String> redirectUris);

View file

@ -22,6 +22,7 @@ public class UserAdapter implements UserModel {
private static final String REQUIRED_ACTIONS_ATTR = "requiredActions";
private static final String REDIRECT_URIS = "redirectUris";
private static final String WEB_ORIGINS = "webOrigins";
protected User user;
protected IdentityManager idm;
@ -161,6 +162,26 @@ public class UserAdapter implements UserModel {
removeFromAttributeSet(REDIRECT_URIS, redirectUri);
}
@Override
public Set<String> getWebOrigins() {
return getAttributeSet(WEB_ORIGINS);
}
@Override
public void setWebOrigins(Set<String> webOrigins) {
setAttributeSet(WEB_ORIGINS, webOrigins);
}
@Override
public void addWebOrigin(String webOrigin) {
addToAttributeSet(WEB_ORIGINS, webOrigin);
}
@Override
public void removeWebOrigin(String webOrigin) {
removeFromAttributeSet(WEB_ORIGINS, webOrigin);
}
@Override
public boolean isTotp() {
Attribute<Boolean> a = user.getAttribute(KEYCLOAK_TOTP_ATTR);

View file

@ -192,6 +192,11 @@
<artifactId>jboss-logging</artifactId>
<version>${jboss.logging.version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>

View file

@ -165,6 +165,10 @@
<artifactId>de.flapdoodle.embed.mongo</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>

View file

@ -44,9 +44,9 @@ public interface FormService {
private RealmModel realm;
private UserModel userModel;
private String error;
private String message;
private FormFlows.ErrorType errorType;
private FormFlows.MessageType messageType;
private MultivaluedMap<String, String> formData;
private URI baseURI;
@ -81,11 +81,11 @@ public interface FormService {
private String contextPath;
public FormServiceDataBean(RealmModel realm, UserModel userModel, MultivaluedMap<String, String> formData, String error){
public FormServiceDataBean(RealmModel realm, UserModel userModel, MultivaluedMap<String, String> formData, String message){
this.realm = realm;
this.userModel = userModel;
this.formData = formData;
this.error = error;
this.message = message;
}
public URI getBaseURI() {
@ -96,12 +96,12 @@ public interface FormService {
this.baseURI = baseURI;
}
public String getError() {
return error;
public String getMessage() {
return message;
}
public void setError(String error) {
this.error = error;
public void setMessage(String message) {
this.message = message;
}
public MultivaluedMap<String, String> getFormData() {
@ -128,12 +128,12 @@ public interface FormService {
this.userModel = userModel;
}
public FormFlows.ErrorType getErrorType() {
return errorType;
public FormFlows.MessageType getMessageType() {
return messageType;
}
public void setErrorType(FormFlows.ErrorType errorType) {
this.errorType = errorType;
public void setMessageType(FormFlows.MessageType messageType) {
this.messageType = messageType;
}
/* OAuth Part */

View file

@ -42,6 +42,11 @@ public class ApplicationManager {
resourceUser.addRedirectUri(redirectUri);
}
}
if (resourceRep.getWebOrigins() != null) {
for (String webOrigin : resourceRep.getWebOrigins()) {
resourceUser.addWebOrigin(webOrigin);
}
}
realm.grantRole(resourceUser, loginRole);
@ -97,6 +102,11 @@ public class ApplicationManager {
if (redirectUris != null) {
resource.getApplicationUser().setRedirectUris(new HashSet<String>(redirectUris));
}
List<String> webOrigins = rep.getWebOrigins();
if (webOrigins != null) {
resource.getApplicationUser().setWebOrigins(new HashSet<String>(webOrigins));
}
}
public ApplicationRepresentation toRepresentation(ApplicationModel applicationModel) {
@ -113,6 +123,11 @@ public class ApplicationManager {
rep.setRedirectUris(new LinkedList<String>(redirectUris));
}
Set<String> webOrigins = applicationModel.getApplicationUser().getWebOrigins();
if (webOrigins != null) {
rep.setWebOrigins(new LinkedList<String>(webOrigins));
}
return rep;
}

View file

@ -13,6 +13,7 @@ import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.resources.AccountService;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.resources.SaasService;
@ -61,6 +62,11 @@ public class AuthenticationManager {
return createLoginCookie(realm, user, cookieName, cookiePath);
}
public NewCookie createAccountIdentityCookie(RealmModel realm, UserModel user, URI uri) {
String cookieName = AccountService.ACCOUNT_IDENTITY_COOKIE;
String cookiePath = uri.getPath();
return createLoginCookie(realm, user, cookieName, cookiePath);
}
protected NewCookie createLoginCookie(RealmModel realm, UserModel user, String cookieName, String cookiePath) {
SkeletonKeyToken identityToken = createIdentityToken(realm, user.getLoginName());
@ -99,6 +105,11 @@ public class AuthenticationManager {
expireCookie(SaasService.SAAS_IDENTITY_COOKIE, cookiePath);
}
public void expireAccountIdentityCookie(URI uri) {
String cookiePath = uri.getPath();
expireCookie(AccountService.ACCOUNT_IDENTITY_COOKIE, cookiePath);
}
public void expireCookie(String cookieName, String path) {
HttpResponse response = ResteasyProviderFactory.getContextData(HttpResponse.class);
if (response == null) {
@ -120,6 +131,11 @@ public class AuthenticationManager {
return authenticateIdentityCookie(realm, uriInfo, headers, cookieName);
}
public UserModel authenticateAccountIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) {
String cookieName = AccountService.ACCOUNT_IDENTITY_COOKIE;
return authenticateIdentityCookie(realm, uriInfo, headers, cookieName);
}
public UserModel authenticateSaasIdentity(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) {
UserModel user = authenticateSaasIdentityCookie(realm, uriInfo, headers);
if (user != null) return user;

View file

@ -90,6 +90,36 @@ public class RealmManager {
if (rep.getDefaultRoles() != null) {
realm.updateDefaultRoles(rep.getDefaultRoles());
}
if (rep.isAccountManagement()) {
enableAccountManagement(realm);
} else {
disableAccountManagement(realm);
}
}
private void enableAccountManagement(RealmModel realm) {
ApplicationModel application = realm.getApplicationById(Constants.ACCOUNT_MANAGEMENT_APPLICATION);
if (application == null) {
application = realm.addApplication(Constants.ACCOUNT_MANAGEMENT_APPLICATION);
UserCredentialModel password = new UserCredentialModel();
password.setType(UserCredentialModel.PASSWORD);
password.setValue(UUID.randomUUID().toString()); // just a random password as we'll never access it
realm.updateCredential(application.getApplicationUser(), password);
RoleModel applicationRole = realm.getRole(Constants.APPLICATION_ROLE);
realm.grantRole(application.getApplicationUser(), applicationRole);
}
application.setEnabled(true);
}
private void disableAccountManagement(RealmModel realm) {
ApplicationModel application = realm.getApplicationNameMap().get(Constants.ACCOUNT_MANAGEMENT_APPLICATION);
if (application != null) {
application.setEnabled(false); // TODO Should we delete the application instead?
}
}
public RealmModel importRealm(RealmRepresentation rep, UserModel realmCreator) {
@ -214,6 +244,10 @@ public class RealmManager {
}
}
}
if (rep.isAccountManagement() != null && rep.isAccountManagement()) {
enableAccountManagement(newRealm);
}
}
public void createRole(RealmModel newRealm, RoleRepresentation roleRep) {
@ -370,6 +404,9 @@ public class RealmManager {
rep.setAccessCodeLifespan(realm.getAccessCodeLifespan());
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
ApplicationModel accountManagementApplication = realm.getApplicationNameMap().get(Constants.ACCOUNT_MANAGEMENT_APPLICATION);
rep.setAccountManagement(accountManagementApplication != null && accountManagementApplication.isEnabled());
List<RoleModel> defaultRoles = realm.getDefaultRoles();
if (defaultRoles.size() > 0) {
String[] d = new String[defaultRoles.size()];

View file

@ -37,16 +37,20 @@ public class ResourceAdminManager {
}
protected boolean logoutResource(RealmModel realm, ApplicationModel resource, String user, ResteasyClient client) {
LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), System.currentTimeMillis() / 1000 + 30, resource.getName(), user);
String token = new TokenManager().encodeToken(realm, adminAction);
Form form = new Form();
form.param("token", token);
String managementUrl = resource.getManagementUrl();
logger.info("logout user: " + user + " resource: " + resource.getName() + " url" + managementUrl);
Response response = client.target(managementUrl).queryParam("action", "logout").request().post(Entity.form(form));
boolean success = response.getStatus() == 204;
response.close();
return success;
if (managementUrl != null) {
LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), System.currentTimeMillis() / 1000 + 30, resource.getName(), user);
String token = new TokenManager().encodeToken(realm, adminAction);
Form form = new Form();
form.param("token", token);
logger.info("logout user: " + user + " resource: " + resource.getName() + " url" + managementUrl);
Response response = client.target(managementUrl).queryParam("action", "logout").request().post(Entity.form(form));
boolean success = response.getStatus() == 204;
response.close();
return success;
} else {
return false;
}
}
}

View file

@ -44,6 +44,8 @@ public class Messages {
public static final String MISSING_PASSWORD = "missingPassword";
public static final String NOTMATCH_PASSWORD = "notMatchPassword";
public static final String MISSING_USERNAME = "missingUsername";
public static final String MISSING_TOTP = "missingTotp";
@ -52,6 +54,14 @@ public class Messages {
public static final String USERNAME_EXISTS = "usernameExists";
public static final String ACTION_WARN_TOTP = "actionTotpWarning";
public static final String ACTION_WARN_PROFILE = "actionProfileWarning";
public static final String ACTION_WARN_PASSWD = "actionPasswordWarning";
public static final String ACTION_WARN_EMAIL = "actionEmailWarning";
public static final String ERROR = "error";
}

View file

@ -21,37 +21,35 @@
*/
package org.keycloak.services.resources;
import java.net.URI;
import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.jose.jws.JWSInput;
import org.jboss.resteasy.jose.jws.crypto.RSAProvider;
import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.AbstractOAuthClient;
import org.keycloak.jaxrs.JaxrsOAuthClient;
import org.keycloak.models.*;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.email.EmailSender;
import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.services.resources.flows.Flows;
import org.keycloak.services.resources.flows.FormFlows;
import org.keycloak.services.resources.flows.Pages;
import org.keycloak.services.resources.flows.Urls;
import org.keycloak.services.validation.Validation;
import org.picketlink.idm.credential.util.TimeBasedOTP;
@ -60,6 +58,10 @@ import org.picketlink.idm.credential.util.TimeBasedOTP;
*/
public class AccountService {
private static final Logger logger = Logger.getLogger(AccountService.class);
public static final String ACCOUNT_IDENTITY_COOKIE = "KEYCLOAK_ACCOUNT_IDENTITY";
private RealmModel realm;
@Context
@ -72,37 +74,64 @@ public class AccountService {
private UriInfo uriInfo;
@Context
protected Providers providers;
private Providers providers;
protected AuthenticationManager authManager = new AuthenticationManager();
private AuthenticationManager authManager = new AuthenticationManager();
private ApplicationModel application;
private TokenManager tokenManager;
public AccountService(RealmModel realm, TokenManager tokenManager) {
public AccountService(RealmModel realm, ApplicationModel application, TokenManager tokenManager) {
this.realm = realm;
this.application = application;
this.tokenManager = tokenManager;
}
private Response forwardToPage(String path, String template) {
UserModel user = getUser(false);
if (user != null) {
return Flows.forms(realm, request, uriInfo).setUser(user).forwardToForm(template);
} else {
return login(path);
}
}
@Path("")
@GET
public Response accountPage() {
return forwardToPage(null, Pages.ACCOUNT);
}
@Path("social")
@GET
public Response socialPage() {
return forwardToPage("social", Pages.SOCIAL);
}
@Path("totp")
@GET
public Response totpPage() {
return forwardToPage("totp", Pages.TOTP);
}
@Path("password")
@GET
public Response passwordPage() {
return forwardToPage("password", Pages.PASSWORD);
}
@Path("access")
@GET
public Response accessPage() {
UserModel user = getUserFromAuthManager();
if (user != null) {
return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccess();
} else {
return Response.status(Status.FORBIDDEN).build();
}
return forwardToPage("access", Pages.ACCESS);
}
@Path("")
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processAccountUpdate(final MultivaluedMap<String, String> formData) {
UserModel user = getUserFromAuthManager();
if (user == null) {
return Response.status(Status.FORBIDDEN).build();
}
UserModel user = getUser(true);
user.setFirstName(formData.getFirst("firstName"));
user.setLastName(formData.getFirst("lastName"));
user.setEmail(formData.getFirst("email"));
@ -113,9 +142,9 @@ public class AccountService {
@Path("totp-remove")
@GET
public Response processTotpRemove() {
UserModel user = getUserFromAuthManager();
UserModel user = getUser(true);
user.setTotp(false);
return Flows.forms(realm, request, uriInfo).setError("successTotpRemoved").setErrorType(FormFlows.ErrorType.SUCCESS)
return Flows.forms(realm, request, uriInfo).setError("successTotpRemoved").setErrorType(FormFlows.MessageType.SUCCESS)
.setUser(user).forwardToTotp();
}
@ -123,36 +152,26 @@ public class AccountService {
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processTotpUpdate(final MultivaluedMap<String, String> formData) {
UserModel user = getUserFromAuthManager();
if (user == null) {
return Response.status(Status.FORBIDDEN).build();
}
FormFlows forms = Flows.forms(realm, request, uriInfo);
UserModel user = getUser(true);
String totp = formData.getFirst("totp");
String totpSecret = formData.getFirst("totpSecret");
String error = null;
FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user);
if (Validation.isEmpty(totp)) {
error = Messages.MISSING_TOTP;
return forms.setError(Messages.MISSING_TOTP).forwardToTotp();
} else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) {
error = Messages.INVALID_TOTP;
}
if (error != null) {
return forms.setError(error).setUser(user).forwardToTotp();
return forms.setError(Messages.INVALID_TOTP).forwardToTotp();
}
UserCredentialModel credentials = new UserCredentialModel();
credentials.setType(CredentialRepresentation.TOTP);
credentials.setValue(formData.getFirst("totpSecret"));
credentials.setValue(totpSecret);
realm.updateCredential(user, credentials);
user.setTotp(true);
return Flows.forms(realm, request, uriInfo).setError("successTotp").setErrorType(FormFlows.ErrorType.SUCCESS)
return Flows.forms(realm, request, uriInfo).setError("successTotp").setErrorType(FormFlows.MessageType.SUCCESS)
.setUser(user).forwardToTotp();
}
@ -160,10 +179,7 @@ public class AccountService {
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processPasswordUpdate(final MultivaluedMap<String, String> formData) {
UserModel user = getUserFromAuthManager();
if (user == null) {
return Response.status(Status.FORBIDDEN).build();
}
UserModel user = getUser(true);
FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user);
@ -172,18 +188,17 @@ public class AccountService {
String passwordConfirm = formData.getFirst("password-confirm");
if (Validation.isEmpty(passwordNew)) {
forms.setError(Messages.MISSING_PASSWORD).forwardToPassword();
return forms.setError(Messages.MISSING_PASSWORD).forwardToPassword();
} else if (!passwordNew.equals(passwordConfirm)) {
forms.setError(Messages.INVALID_PASSWORD_CONFIRM).forwardToPassword();
return forms.setError(Messages.INVALID_PASSWORD_CONFIRM).forwardToPassword();
}
if (Validation.isEmpty(password)) {
forms.setError(Messages.MISSING_PASSWORD).forwardToPassword();
return forms.setError(Messages.MISSING_PASSWORD).forwardToPassword();
} else if (!realm.validatePassword(user, password)) {
forms.setError(Messages.INVALID_PASSWORD_EXISTING).forwardToPassword();
return forms.setError(Messages.INVALID_PASSWORD_EXISTING).forwardToPassword();
}
UserCredentialModel credentials = new UserCredentialModel();
credentials.setType(CredentialRepresentation.PASSWORD);
credentials.setValue(passwordNew);
@ -193,50 +208,112 @@ public class AccountService {
return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword();
}
@Path("")
@Path("login-redirect")
@GET
public Response accountPage() {
UserModel user = getUserFromAuthManager();
if (user != null) {
return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccount();
} else {
return Response.status(Status.FORBIDDEN).build();
public Response loginRedirect(@QueryParam("code") String code,
@QueryParam("state") String state,
@QueryParam("error") String error,
@Context HttpHeaders headers) {
try {
if (error != null) {
logger.debug("error from oauth");
throw new ForbiddenException("error");
}
if (!realm.isEnabled()) {
logger.debug("realm not enabled");
throw new ForbiddenException();
}
UserModel client = application.getApplicationUser();
if (!client.isEnabled() || !application.isEnabled()) {
logger.debug("account management app not enabled");
throw new ForbiddenException();
}
if (code == null) {
logger.debug("code not specified");
throw new BadRequestException();
}
if (state == null) {
logger.debug("state not specified");
throw new BadRequestException();
}
String path = new JaxrsOAuthClient().checkStateCookie(uriInfo, headers);
JWSInput input = new JWSInput(code, providers);
boolean verifiedCode = false;
try {
verifiedCode = RSAProvider.verify(input, realm.getPublicKey());
} catch (Exception ignored) {
logger.debug("Failed to verify signature", ignored);
}
if (!verifiedCode) {
logger.debug("unverified access code");
throw new BadRequestException();
}
String key = input.readContent(String.class);
AccessCodeEntry accessCode = tokenManager.pullAccessCode(key);
if (accessCode == null) {
logger.debug("bad access code");
throw new BadRequestException();
}
if (accessCode.isExpired()) {
logger.debug("access code expired");
throw new BadRequestException();
}
if (!accessCode.getToken().isActive()) {
logger.debug("access token expired");
throw new BadRequestException();
}
if (!accessCode.getRealm().getId().equals(realm.getId())) {
logger.debug("bad realm");
throw new BadRequestException();
}
if (!client.getLoginName().equals(accessCode.getClient().getLoginName())) {
logger.debug("bad client");
throw new BadRequestException();
}
UriBuilder redirectBuilder = Urls.accountBase(uriInfo.getBaseUri());
if (path != null) {
redirectBuilder.path(path);
}
URI redirectUri = redirectBuilder.build(realm.getId());
NewCookie cookie = authManager.createAccountIdentityCookie(realm, accessCode.getUser(), Urls.accountBase(uriInfo.getBaseUri()).build(realm.getId()));
return Response.status(302).cookie(cookie).location(redirectUri).build();
} finally {
authManager.expireCookie(AbstractOAuthClient.OAUTH_TOKEN_REQUEST_STATE, uriInfo.getAbsolutePath().getPath());
}
}
@Path("social")
@Path("logout")
@GET
public Response socialPage() {
UserModel user = getUserFromAuthManager();
if (user != null) {
return Flows.forms(realm, request, uriInfo).setUser(user).forwardToSocial();
} else {
return Response.status(Status.FORBIDDEN).build();
}
public Response logout() {
// TODO Should use single-sign out via TokenService
URI baseUri = Urls.accountBase(uriInfo.getBaseUri()).build(realm.getId());
authManager.expireIdentityCookie(realm, uriInfo);
authManager.expireAccountIdentityCookie(baseUri);
return Response.status(302).location(baseUri).build();
}
@Path("totp")
@GET
public Response totpPage() {
UserModel user = getUserFromAuthManager();
if (user != null) {
return Flows.forms(realm, request, uriInfo).setUser(user).forwardToTotp();
} else {
return Response.status(Status.FORBIDDEN).build();
}
private Response login(String path) {
JaxrsOAuthClient oauth = new JaxrsOAuthClient();
String authUrl = Urls.realmLoginPage(uriInfo.getBaseUri(), realm.getId()).toString();
oauth.setAuthUrl(authUrl);
oauth.setClientId(Constants.ACCOUNT_MANAGEMENT_APPLICATION);
URI accountUri = Urls.accountPageBuilder(uriInfo.getBaseUri()).path(AccountService.class, "loginRedirect").build(realm.getId());
oauth.setStateCookiePath(accountUri.getPath());
return oauth.redirect(uriInfo, accountUri.toString(), path);
}
@Path("password")
@GET
public Response passwordPage() {
UserModel user = getUserFromAuthManager();
if (user == null) {
return Response.status(Status.FORBIDDEN).build();
private UserModel getUser(boolean required) {
UserModel user = authManager.authenticateAccountIdentityCookie(realm, uriInfo, headers);
if (user == null && required) {
throw new ForbiddenException();
}
return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword();
}
private UserModel getUserFromAuthManager() {
return authManager.authenticateIdentityCookie(realm, uriInfo, headers);
return user;
}
}

View file

@ -0,0 +1,43 @@
package org.keycloak.services.resources;
import java.util.Set;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import org.jboss.resteasy.spi.HttpRequest;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class Cors {
private HttpRequest request;
private ResponseBuilder response;
private Set<String> allowedOrigins;
public Cors(HttpRequest request, ResponseBuilder response) {
this.request = request;
this.response = response;
}
public static Cors add(HttpRequest request, ResponseBuilder response) {
return new Cors(request, response);
}
public Cors allowedOrigins(Set<String> allowedOrigins) {
this.allowedOrigins = allowedOrigins;
return this;
}
public Response build() {
String origin = request.getHttpHeaders().getHeaderString("Origin");
if (origin == null || allowedOrigins == null || (!allowedOrigins.contains(origin))) {
return response.build();
}
response.header("Access-Control-Allow-Origin", origin);
return response.build();
}
}

View file

@ -50,6 +50,7 @@ public class KeycloakApplication extends Application {
singletons.add(new SaasService(tokenManager));
singletons.add(new SocialResource(tokenManager, new SocialRequestManager()));
classes.add(SkeletonKeyContextResolver.class);
classes.add(QRCodeResource.class);
}
protected KeycloakSessionFactory createSessionFactory() {

View file

@ -0,0 +1,52 @@
package org.keycloak.services.resources;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import javax.servlet.ServletException;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import java.io.IOException;
import java.io.OutputStream;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@Path("/qrcode")
public class QRCodeResource {
@GET
@Produces("image/png")
public Response createQrCode(@QueryParam("contents") String contents, @QueryParam("size") String size) throws ServletException, IOException, WriterException {
int width = 256;
int height = 256;
if (size != null) {
String[] s = size.split("x");
width = Integer.parseInt(s[0]);
height = Integer.parseInt(s[1]);
}
if (contents == null) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
QRCodeWriter writer = new QRCodeWriter();
final BitMatrix bitMatrix = writer.encode(contents, BarcodeFormat.QR_CODE, width, height);
StreamingOutput stream = new StreamingOutput() {
@Override
public void write(OutputStream os) throws IOException,
WebApplicationException {
MatrixToImageWriter.writeToStream(bitMatrix, "png", os);
}
};
return Response.ok(stream).build();
}
}

View file

@ -1,6 +1,8 @@
package org.keycloak.services.resources;
import org.jboss.resteasy.logging.Logger;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.Constants;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.TokenManager;
import org.keycloak.models.KeycloakSession;
@ -66,7 +68,14 @@ public class RealmsResource {
logger.debug("realm not found");
throw new NotFoundException();
}
AccountService accountService = new AccountService(realm, tokenManager);
ApplicationModel application = realm.getApplicationNameMap().get(Constants.ACCOUNT_MANAGEMENT_APPLICATION);
if (application == null || !application.isEnabled()) {
logger.debug("account management not enabled");
throw new NotFoundException();
}
AccountService accountService = new AccountService(realm, application, tokenManager);
resourceContext.initResource(accountService);
return accountService;
}

View file

@ -88,6 +88,12 @@ public class RequiredActionsService {
}
UserModel user = getUser(accessCode);
String error = Validation.validateUpdateProfileForm(formData);
if (error != null) {
return Flows.forms(realm, request, uriInfo).setError(error).forwardToAction(RequiredAction.UPDATE_PROFILE);
}
user.setFirstName(formData.getFirst("firstName"));
user.setLastName(formData.getFirst("lastName"));
user.setEmail(formData.getFirst("email"));
@ -121,7 +127,7 @@ public class RequiredActionsService {
UserCredentialModel credentials = new UserCredentialModel();
credentials.setType(CredentialRepresentation.TOTP);
credentials.setValue(formData.getFirst("totpSecret"));
credentials.setValue(totpSecret);
realm.updateCredential(user, credentials);
user.setTotp(true);
@ -146,15 +152,14 @@ public class RequiredActionsService {
UserModel user = getUser(accessCode);
String password = formData.getFirst("password");
String passwordNew = formData.getFirst("password-new");
String passwordConfirm = formData.getFirst("password-confirm");
FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user);
if (Validation.isEmpty(passwordNew)) {
forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
return forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
} else if (!passwordNew.equals(passwordConfirm)) {
forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
return forms.setError(Messages.NOTMATCH_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
}
UserCredentialModel credentials = new UserCredentialModel();
@ -257,7 +262,7 @@ public class RequiredActionsService {
new EmailSender().sendPasswordReset(user, realm, accessCode, uriInfo);
return Flows.forms(realm, request, uriInfo).setError("emailSent").setErrorType(FormFlows.ErrorType.SUCCESS)
return Flows.forms(realm, request, uriInfo).setError("emailSent").setErrorType(FormFlows.MessageType.SUCCESS)
.forwardToPasswordReset();
}

View file

@ -415,7 +415,8 @@ public class TokenService {
}
logger.info("accessRequest SUCCESS");
AccessTokenResponse res = accessTokenResponse(realm.getPrivateKey(), accessCode.getToken());
return Response.ok(res).build();
return Cors.add(request, Response.ok(res)).allowedOrigins(client.getWebOrigins()).build();
}
protected AccessTokenResponse accessTokenResponse(PrivateKey privateKey, SkeletonKeyToken token) {

View file

@ -34,6 +34,7 @@ import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.services.messages.Messages;
import org.picketlink.idm.model.sample.Realm;
import javax.imageio.spi.ServiceRegistry;
@ -58,8 +59,8 @@ public class FormFlows {
// TODO refactor/rename "error" to "message" everywhere where it makes sense
private String error;
public static enum ErrorType {SUCCESS, WARNING, ERROR};
private ErrorType errorType;
public static enum MessageType {SUCCESS, WARNING, ERROR};
private MessageType messageType = MessageType.ERROR;
private MultivaluedMap<String, String> formData;
@ -79,16 +80,17 @@ public class FormFlows {
}
public Response forwardToAction(RequiredAction action) {
switch (action) {
case CONFIGURE_TOTP:
return forwardToForm(Pages.LOGIN_CONFIG_TOTP);
return forwardToActionForm(Pages.LOGIN_CONFIG_TOTP, Messages.ACTION_WARN_TOTP);
case UPDATE_PROFILE:
return forwardToForm(Pages.LOGIN_UPDATE_PROFILE);
return forwardToActionForm(Pages.LOGIN_UPDATE_PROFILE, Messages.ACTION_WARN_PROFILE);
case UPDATE_PASSWORD:
return forwardToForm(Pages.LOGIN_UPDATE_PASSWORD);
return forwardToActionForm(Pages.LOGIN_UPDATE_PASSWORD, Messages.ACTION_WARN_PASSWD);
case VERIFY_EMAIL:
new EmailSender().sendEmailVerification(userModel, realm, accessCode, uriInfo);
return forwardToForm(Pages.LOGIN_VERIFY_EMAIL);
return forwardToActionForm(Pages.LOGIN_VERIFY_EMAIL, Messages.ACTION_WARN_EMAIL);
default:
return Response.serverError().build();
}
@ -103,7 +105,6 @@ public class FormFlows {
}
private Response forwardToForm(String template, FormService.FormServiceDataBean formDataBean) {
formDataBean.setErrorType(errorType == null ? ErrorType.ERROR : errorType);
// Getting URI needed by form processing service
ResteasyUriInfo uriInfo = request.getUri();
@ -140,11 +141,24 @@ public class FormFlows {
return Response.status(200).entity("form provider not found").build();
}
private Response forwardToForm(String template) {
public Response forwardToForm(String template) {
FormService.FormServiceDataBean formDataBean = new FormService.FormServiceDataBean(realm, userModel, formData, error);
return forwardToForm(template, formDataBean);
formDataBean.setMessageType(messageType);
return forwardToForm(template, formDataBean);
}
private Response forwardToActionForm(String template, String warningSummary) {
// If no other message is set, notify user about required action in the warning window
// so it's clear that this is a req. action form not a login form
if (error == null){
messageType = MessageType.WARNING;
error = warningSummary;
}
return forwardToForm(template);
}
public Response forwardToLogin() {
@ -202,8 +216,8 @@ public class FormFlows {
return this;
}
public FormFlows setErrorType(ErrorType errorType) {
this.errorType = errorType;
public FormFlows setErrorType(MessageType errorType) {
this.messageType = errorType;
return this;
}

View file

@ -35,12 +35,16 @@ public class Urls {
return accountBase(baseUri).path(AccountService.class, "accessPage").build(realmId);
}
private static UriBuilder accountBase(URI baseUri) {
public static UriBuilder accountBase(URI baseUri) {
return realmBase(baseUri).path(RealmsResource.class, "getAccountService");
}
public static URI accountPage(URI baseUri, String realmId) {
return accountBase(baseUri).path(AccountService.class, "accountPage").build(realmId);
return accountPageBuilder(baseUri).build(realmId);
}
public static UriBuilder accountPageBuilder(URI baseUri) {
return accountBase(baseUri).path(AccountService.class, "accountPage");
}
public static URI accountPasswordPage(URI baseUri, String realmId) {
@ -59,6 +63,10 @@ public class Urls {
return accountBase(baseUri).path(AccountService.class, "processTotpRemove").build(realmId);
}
public static URI accountLogout(URI baseUri, String realmId) {
return accountBase(baseUri).path(AccountService.class, "logout").build(realmId);
}
public static URI loginActionUpdatePassword(URI baseUri, String realmId) {
return requiredActionsBase(baseUri).path(RequiredActionsService.class, "updatePassword").build(realmId);
}

View file

@ -38,6 +38,22 @@ public class Validation {
return null;
}
public static String validateUpdateProfileForm(MultivaluedMap<String, String> formData) {
if (isEmpty(formData.getFirst("firstName"))) {
return Messages.MISSING_FIRST_NAME;
}
if (isEmpty(formData.getFirst("lastName"))) {
return Messages.MISSING_LAST_NAME;
}
if (isEmpty(formData.getFirst("email"))) {
return Messages.MISSING_EMAIL;
}
return null;
}
public static boolean isEmpty(String s) {
return s == null || s.length() == 0;
}

View file

@ -1,23 +1,20 @@
package org.keycloak.test;
import java.util.Iterator;
import java.util.List;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.*;
import org.keycloak.representations.idm.ApplicationRepresentation;
import org.keycloak.services.managers.ApplicationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.KeycloakApplication;
import java.util.Iterator;
import java.util.List;
import static org.junit.Assert.assertNotNull;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -49,6 +46,9 @@ public class ApplicationModelTest extends AbstractKeycloakServerTest {
application.getApplicationUser().addRedirectUri("redirect-1");
application.getApplicationUser().addRedirectUri("redirect-2");
application.getApplicationUser().addWebOrigin("origin-1");
application.getApplicationUser().addWebOrigin("origin-2");
application.updateApplication();
}
@ -85,6 +85,7 @@ public class ApplicationModelTest extends AbstractKeycloakServerTest {
UserModel euser = expected.getApplicationUser();
Assert.assertTrue(euser.getRedirectUris().containsAll(auser.getRedirectUris()));
Assert.assertTrue(euser.getWebOrigins().containsAll(auser.getWebOrigins()));
}
public static void assertEquals(List<RoleModel> expected, List<RoleModel> actual) {

View file

@ -53,11 +53,34 @@ public class UserModelTest extends AbstractKeycloakServerTest {
user.addRequiredAction(RequiredAction.CONFIGURE_TOTP);
user.addRequiredAction(RequiredAction.UPDATE_PASSWORD);
user.addWebOrigin("origin-1");
user.addWebOrigin("origin-2");
UserModel persisted = manager.getRealm(realm.getId()).getUser("user");
assertEquals(user, persisted);
}
@Test
public void webOriginSetTest() {
RealmModel realm = manager.createRealm("original");
UserModel user = realm.addUser("user");
Assert.assertTrue(user.getWebOrigins().isEmpty());
user.addWebOrigin("origin-1");
Assert.assertEquals(1, user.getWebOrigins().size());
user.addWebOrigin("origin-2");
Assert.assertEquals(2, user.getWebOrigins().size());
user.removeWebOrigin("origin-2");
Assert.assertEquals(1, user.getWebOrigins().size());
user.removeWebOrigin("origin-1");
Assert.assertTrue(user.getWebOrigins().isEmpty());
}
@Test
public void testUserRequiredActions() throws Exception {
RealmModel realm = manager.createRealm("original");
@ -102,7 +125,7 @@ public class UserModelTest extends AbstractKeycloakServerTest {
Assert.assertEquals(expected.getLastName(), actual.getLastName());
Assert.assertArrayEquals(expected.getRedirectUris().toArray(), actual.getRedirectUris().toArray());
Assert.assertArrayEquals(expected.getRequiredActions().toArray(), actual.getRequiredActions().toArray());
Assert.assertArrayEquals(expected.getWebOrigins().toArray(), actual.getWebOrigins().toArray());
}
public static void assertEquals(List<RoleModel> expected, List<RoleModel> actual) {

View file

@ -101,6 +101,10 @@
<groupId>org.picketlink</groupId>
<artifactId>picketlink-config</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>

View file

@ -0,0 +1,5 @@
log4j.rootLogger=debug, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] %m%n

View file

@ -44,8 +44,8 @@ import org.openqa.selenium.WebDriver;
*/
public class RequiredActionUpdateProfileTest {
@ClassRule
public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() {
@Rule
public KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
@ -83,4 +83,50 @@ public class RequiredActionUpdateProfileTest {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
}
@Test
public void updateProfileMissingFirstName() {
loginPage.open();
loginPage.login("test-user@localhost", "password");
updateProfilePage.assertCurrent();
updateProfilePage.update("", "New last", "new@email.com");
updateProfilePage.assertCurrent();
Assert.assertEquals("Please specify first name", updateProfilePage.getError());
}
@Test
public void updateProfileMissingLastName() {
loginPage.open();
loginPage.login("test-user@localhost", "password");
updateProfilePage.assertCurrent();
updateProfilePage.update("New first", "", "new@email.com");
updateProfilePage.assertCurrent();
Assert.assertEquals("Please specify last name", updateProfilePage.getError());
}
@Test
public void updateProfileMissingEmail() {
loginPage.open();
loginPage.login("test-user@localhost", "password");
updateProfilePage.assertCurrent();
updateProfilePage.update("New first", "New last", "");
updateProfilePage.assertCurrent();
Assert.assertEquals("Please specify email", updateProfilePage.getError());
}
}

View file

@ -21,11 +21,7 @@
*/
package org.keycloak.testsuite.forms;
import org.junit.After;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.*;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.models.RealmModel;
@ -97,13 +93,12 @@ public class AccountTest {
@Test
public void changePassword() {
loginPage.open();
changePasswordPage.open();
loginPage.login("test-user@localhost", "password");
changePasswordPage.open();
changePasswordPage.changePassword("password", "new-password", "new-password");
oauth.openLogout();
changePasswordPage.logout();
loginPage.open();
loginPage.login("test-user@localhost", "password");
@ -118,10 +113,8 @@ public class AccountTest {
@Test
public void changeProfile() {
loginPage.open();
loginPage.login("test-user@localhost", "password");
profilePage.open();
loginPage.login("test-user@localhost", "password");
Assert.assertEquals("", profilePage.getFirstName());
Assert.assertEquals("", profilePage.getLastName());
@ -136,10 +129,8 @@ public class AccountTest {
@Test
public void setupTotp() {
loginPage.open();
loginPage.login("test-user@localhost", "password");
totpPage.open();
loginPage.login("test-user@localhost", "password");
Assert.assertTrue(totpPage.isCurrent());

View file

@ -123,7 +123,7 @@ public class ResetPasswordTest {
resetPasswordPage.assertCurrent();
Assert.assertNotEquals("Success!", resetPasswordPage.getMessage());
Assert.assertEquals("Error!", resetPasswordPage.getMessage());
Assert.assertEquals("Invalid username or email.", resetPasswordPage.getMessage());
}
@Test
@ -138,7 +138,7 @@ public class ResetPasswordTest {
resetPasswordPage.assertCurrent();
Assert.assertNotEquals("Success!", resetPasswordPage.getMessage());
Assert.assertEquals("Error!", resetPasswordPage.getMessage());
Assert.assertEquals("Invalid username or email.", resetPasswordPage.getMessage());
}
}

View file

@ -0,0 +1,42 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.keycloak.testsuite.pages;
import org.junit.Assert;
import org.keycloak.testsuite.rule.WebResource;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public abstract class AbstractAccountPage extends AbstractPage {
@FindBy(linkText = "Logout")
private WebElement logoutLink;
public void logout() {
logoutLink.click();
}
}

View file

@ -28,7 +28,7 @@ import org.openqa.selenium.WebDriver;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public abstract class Page {
public abstract class AbstractPage {
@WebResource
protected WebDriver driver;

View file

@ -28,7 +28,7 @@ import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AccountPasswordPage extends Page {
public class AccountPasswordPage extends AbstractAccountPage {
private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/test/account/password";

View file

@ -28,7 +28,7 @@ import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AccountTotpPage extends Page {
public class AccountTotpPage extends AbstractAccountPage {
private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/test/account/totp";

View file

@ -28,7 +28,7 @@ import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AccountUpdateProfilePage extends Page {
public class AccountUpdateProfilePage extends AbstractAccountPage {
private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/test/account";

View file

@ -25,7 +25,7 @@ package org.keycloak.testsuite.pages;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AppPage extends Page {
public class AppPage extends AbstractPage {
private String baseUrl = "http://localhost:8081/app";

View file

@ -23,14 +23,13 @@ package org.keycloak.testsuite.pages;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.rule.WebResource;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ErrorPage extends Page {
public class ErrorPage extends AbstractPage {
@WebResource
protected OAuthClient oauth;

View file

@ -27,7 +27,7 @@ import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LoginConfigTotpPage extends Page {
public class LoginConfigTotpPage extends AbstractPage {
@FindBy(id = "totpSecret")
private WebElement totpSecret;

View file

@ -30,7 +30,7 @@ import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LoginPage extends Page {
public class LoginPage extends AbstractPage {
@WebResource
protected OAuthClient oauth;

View file

@ -27,7 +27,7 @@ import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LoginPasswordResetPage extends Page {
public class LoginPasswordResetPage extends AbstractPage {
@FindBy(id = "username")
private WebElement usernameInput;

View file

@ -27,7 +27,7 @@ import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LoginPasswordUpdatePage extends Page {
public class LoginPasswordUpdatePage extends AbstractPage {
@FindBy(id = "password-new")
private WebElement newPasswordInput;

View file

@ -28,7 +28,7 @@ import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LoginTotpPage extends Page {
public class LoginTotpPage extends AbstractPage {
@FindBy(id = "totp")
private WebElement totpInput;

View file

@ -27,7 +27,7 @@ import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LoginUpdateProfilePage extends Page {
public class LoginUpdateProfilePage extends AbstractPage {
@FindBy(id = "firstName")
private WebElement firstNameInput;

View file

@ -27,7 +27,7 @@ import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class OAuthGrantPage extends Page {
public class OAuthGrantPage extends AbstractPage {
@FindBy(css = "input[name=\"accept\"]")
private WebElement acceptButton;

View file

@ -27,7 +27,7 @@ import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class RegisterPage extends Page {
public class RegisterPage extends AbstractPage {
@FindBy(id = "firstName")
private WebElement firstNameInput;
@ -85,6 +85,22 @@ public class RegisterPage extends Page {
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
}
public String getFirstName() {
return firstNameInput.getAttribute("value");
}
public String getLastName() {
return lastNameInput.getAttribute("value");
}
public String getEmail() {
return emailInput.getAttribute("value");
}
public String getUsername() {
return usernameInput.getAttribute("value");
}
public boolean isCurrent() {
return driver.getTitle().equals("Register with test");
}

View file

@ -29,7 +29,7 @@ import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:vrockai@redhat.com">Viliam Rockai</a>
*/
public class VerifyEmailPage extends Page {
public class VerifyEmailPage extends AbstractPage {
@WebResource
protected OAuthClient oauth;

View file

@ -25,7 +25,7 @@ import java.lang.reflect.Field;
import org.junit.rules.ExternalResource;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.Page;
import org.keycloak.testsuite.pages.AbstractPage;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
@ -78,7 +78,7 @@ public class WebRule extends ExternalResource {
Class<?> type = f.getType();
if (type.equals(WebDriver.class)) {
set(f, o, driver);
} else if (Page.class.isAssignableFrom(type)) {
} else if (AbstractPage.class.isAssignableFrom(type)) {
set(f, o, getPage(f.getType()));
} else if (type.equals(OAuthClient.class)) {
set(f, o, oauth);

View file

@ -35,6 +35,7 @@ import org.keycloak.testsuite.OAuthClient.AccessTokenResponse;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup;
import org.keycloak.testsuite.rule.WebResource;
@ -68,6 +69,9 @@ public class SocialLoginTest {
@WebResource
protected LoginPage loginPage;
@WebResource
protected RegisterPage registerPage;
@WebResource
protected OAuthClient oauth;
@ -97,4 +101,41 @@ public class SocialLoginTest {
Assert.assertTrue(token.getRealmAccess().isUserInRole("user"));
}
@Test
public void registerRequired() {
keycloakRule.configure(new KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
appRealm.setAutomaticRegistrationAfterSocialLogin(false);
}
});
try {
loginPage.open();
loginPage.clickSocial("dummy");
driver.findElement(By.id("username")).sendKeys("dummy-user-reg");
driver.findElement(By.id("submit")).click();
registerPage.isCurrent();
Assert.assertEquals("", registerPage.getFirstName());
Assert.assertEquals("", registerPage.getLastName());
Assert.assertEquals("dummy-user-reg@dummy-social", registerPage.getEmail());
Assert.assertEquals("dummy-user-reg", registerPage.getUsername());
registerPage.register("Dummy", "User", "dummy-user-reg@dummy-social", "dummy-user-reg", "password", "password");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
} finally {
keycloakRule.configure(new KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
appRealm.setAutomaticRegistrationAfterSocialLogin(true);
}
});
}
}
}

View file

@ -8,6 +8,7 @@
"sslNotRequired": true,
"cookieLoginAllowed": true,
"registrationAllowed": true,
"accountManagement": true,
"resetPasswordAllowed": true,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",