This commit is contained in:
Bill Burke 2013-08-15 09:37:41 -04:00
commit 13a93fb776
73 changed files with 1498 additions and 701 deletions

View file

@ -47,7 +47,12 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-sdk-html</artifactId> <artifactId>keycloak-social-facebook</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-forms</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency> <dependency>

View file

@ -5,6 +5,7 @@
"accessCodeLifespan": 10, "accessCodeLifespan": 10,
"sslNotRequired": true, "sslNotRequired": true,
"cookieLoginAllowed": true, "cookieLoginAllowed": true,
"registrationAllowed": true,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", "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", "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ], "requiredCredentials": [ "password" ],

View file

@ -12,8 +12,8 @@
<!-- <select class="nav pull-left" ng-options="r.name for r in current.realms"></select> --> <!-- <select class="nav pull-left" ng-options="r.name for r in current.realms"></select> -->
</div> </div>
<ul class="nav pull-right" data-ng-hide="auth.loggedIn"> <ul class="nav pull-right" data-ng-hide="auth.loggedIn">
<li><a href="/auth-server/rest/saas/loginPage.html">Login</a></li> <li><a href="/auth-server/rest/saas/login">Login</a></li>
<li><a href="/auth-server/saas/saas-register.jsp">Register</a></li> <li><a href="/auth-server/rest/saas/registrations">Register</a></li>
</ul> </ul>
<ul class="nav pull-right" data-ng-show="auth.loggedIn"> <ul class="nav pull-right" data-ng-show="auth.loggedIn">
<li class="divider-vertical-left dropdown"><a data-toggle="dropdown" class="dropdown-toggle" href="#"><i <li class="divider-vertical-left dropdown"><a data-toggle="dropdown" class="dropdown-toggle" href="#"><i

View file

@ -1,88 +0,0 @@
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%><!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Register with Keycloak</title>
<link rel="stylesheet" href="<%=application.getContextPath()%>/saas/css/reset.css">
<link rel="stylesheet" type="text/css" href="<%=application.getContextPath()%>/saas/css/base.css">
<link rel="stylesheet" type="text/css" href="<%=application.getContextPath()%>/saas/css/forms.css">
<link rel="stylesheet" type="text/css" href="<%=application.getContextPath()%>/saas/css/zocial/zocial.css">
<link rel="stylesheet" type="text/css" href="<%=application.getContextPath()%>/saas/css/login-screen.css">
<link rel="stylesheet" type="text/css" href='http://fonts.googleapis.com/css?family=Open+Sans:400,300,300italic,400italic,600,600italic,700,700italic,800,800italic'>
</head>
<body class="rcue-login-register register">
<h1><a href="#" title="Go to the home page"><img src="<%=application.getContextPath()%>/saas/img/red-hat-logo.png" alt="Red Hat logo"></a></h1>
<div class="content">
<h2>Register with <strong>Keycloak</strong></h2>
<div class="background-area">
<div class="form-area social clearfix">
<section class="app-form">
<h3>Application login area</h3>
<form action="<%=application.getContextPath()%>/rest/saas/registrations" method="POST">
<%
String errorMessage = (String)request.getAttribute("KEYCLOAK_LOGIN_ERROR_MESSAGE");
if (errorMessage != null) { %>
<div class="feedback feedback-error">
<p><font color="red"><%=errorMessage%></font></p>
</div>
<% } %>
<p class="subtitle">All fields required</p>
<div>
<label for="name">Full name</label><input type="text" id="name" name="name" autofocus>
</div>
<div>
<label for="email">Email</label><input type="email" id="email" name="email">
</div>
<div>
<label for="username">Username</label><input type="text" id="username" name="username">
</div>
<div>
<label for="password">Password</label><input type="password" id="password" placeholder="At least 6 characters" name="password">
</div>
<div>
<label for="password-confirm" class="two-lines">Password confirmation</label><input type="password" id="password-confirm" name="password-confirm">
</div>
<div class="aside-btn">
<p>By registering you agree to the <a href="#">Terms of Service</a> and the <a href="#">Privacy Policy</a>.</p>
</div>
<input type="submit" value="Register">
</form>
</section>
<section class="social-login">
<span>or</span>
<h3>Social login area</h3>
<p>Log In with</p>
<ul>
<li>
<a href="#" class="zocial facebook">
<span class="text">Facebook</span>
</a>
</li>
<li>
<a href="#" class="zocial googleplus">
<span class="text">Google</span>
</a>
</li>
<li>
<a href="#" class="zocial twitter">
<span class="text">Twitter</span>
</a>
</li>
</ul>
</section>
<section class="info-area">
<h3>Info area</h3>
<p>Already have an account? <a href="<%=application.getContextPath()%>/rest/saas/loginPage.html">Log in</a>.</p>
<ul>
<li><strong>Domain:</strong> 10.0.0.1</li>
<li><strong>Zone:</strong> Live</li>
<li><strong>Appliance:</strong> Yep</li>
</ul>
</section>
</div>
</div>
</div>
</body>
</html>

View file

@ -8,8 +8,8 @@
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-sdk-html</artifactId> <artifactId>keycloak-forms</artifactId>
<name>Keycloak HTML SDK</name> <name>Keycloak Forms</name>
<description /> <description />
<dependencies> <dependencies>

View file

@ -1,4 +1,25 @@
package org.keycloak.sdk; /*
* 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.net.URI; import java.net.URI;
import java.util.HashMap; import java.util.HashMap;
@ -8,36 +29,42 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.faces.application.FacesMessage;
import javax.faces.bean.ManagedBean; import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped; import javax.faces.bean.RequestScoped;
import javax.faces.context.FacesContext; import javax.faces.context.FacesContext;
import javax.imageio.spi.ServiceRegistry; import javax.imageio.spi.ServiceRegistry;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriBuilder;
import org.keycloak.services.models.RealmModel; import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.RequiredCredentialModel; import org.keycloak.services.models.RequiredCredentialModel;
import org.keycloak.services.resources.flows.FormFlows;
import org.keycloak.services.resources.flows.Urls;
@ManagedBean(name = "login") /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ManagedBean(name = "forms")
@RequestScoped @RequestScoped
public class LoginBean { public class FormsBean {
private RealmModel realm; private RealmModel realm;
private String name; private String name;
private String loginUrl;
private String loginAction; private String loginAction;
private String socialLoginUrl; private UriBuilder socialLoginUrlBuilder;
private String registrationUrl; private String registrationUrl;
private String username; private String registrationAction;
private List<RequiredCredential> requiredCredentials; private List<RequiredCredential> requiredCredentials;
private List<Property> hiddenProperties;
private List<SocialProvider> providers; private List<SocialProvider> providers;
private String theme; private String theme;
@ -48,28 +75,53 @@ public class LoginBean {
private String error; private String error;
private String errorDetails;
private String view;
private Map<String, String> formData;
@PostConstruct @PostConstruct
public void init() { public void init() {
FacesContext ctx = FacesContext.getCurrentInstance(); FacesContext ctx = FacesContext.getCurrentInstance();
HttpServletRequest request = (HttpServletRequest) ctx.getExternalContext().getRequest(); HttpServletRequest request = (HttpServletRequest) ctx.getExternalContext().getRequest();
realm = (RealmModel) request.getAttribute(RealmModel.class.getName()); realm = (RealmModel) request.getAttribute(FormFlows.REALM);
if (RealmModel.DEFAULT_REALM.equals(realm.getName())) { boolean saas = RealmModel.DEFAULT_REALM.equals(realm.getName());
if (saas) {
name = "Keycloak"; name = "Keycloak";
} else { } else {
name = realm.getName(); name = realm.getName();
} }
loginAction = ((URI) request.getAttribute("KEYCLOAK_LOGIN_ACTION")).toString(); view = ctx.getViewRoot().getViewId();
registrationUrl = ((URI) request.getAttribute("KEYCLOAK_REGISTRATION_PAGE")).toString(); view = view.substring(view.lastIndexOf('/') + 1, view.lastIndexOf('.'));
socialLoginUrl = ((URI) request.getAttribute("KEYCLOAK_SOCIAL_LOGIN")).toString();
username = (String) request.getAttribute("username"); UriBuilder b = UriBuilder.fromUri(request.getRequestURI()).replaceQuery(request.getQueryString())
.replacePath(request.getContextPath()).path("rest");
URI baseURI = b.build();
if (saas) {
loginUrl = Urls.saasLoginPage(baseURI).toString();
loginAction = Urls.saasLoginAction(baseURI).toString();
registrationUrl = Urls.saasRegisterPage(baseURI).toString();
registrationAction = Urls.saasRegisterAction(baseURI).toString();
} else {
loginUrl = Urls.realmLoginPage(baseURI, realm.getId()).toString();
loginAction = Urls.realmLoginAction(baseURI, realm.getId()).toString();
registrationUrl = Urls.realmRegisterPage(baseURI, realm.getId()).toString();
registrationAction = Urls.realmRegisterAction(baseURI, realm.getId()).toString();
}
socialLoginUrlBuilder = UriBuilder.fromUri(Urls.socialRedirectToProviderAuth(baseURI, realm.getId()));
addRequiredCredentials(); addRequiredCredentials();
addHiddenProperties(request, "client_id", "scope", "state", "redirect_uri"); addFormData(request);
addSocialProviders(); addSocialProviders();
addErrors(request); addErrors(request);
@ -98,6 +150,10 @@ public class LoginBean {
return name; return name;
} }
public String getLoginUrl() {
return loginUrl;
}
public String getLoginAction() { public String getLoginAction() {
return loginAction; return loginAction;
} }
@ -106,14 +162,22 @@ public class LoginBean {
return error; return error;
} }
public List<Property> getHiddenProperties() { public String getErrorDetails() {
return hiddenProperties; return errorDetails;
}
public Map<String, String> getFormData() {
return formData;
} }
public List<RequiredCredential> getRequiredCredentials() { public List<RequiredCredential> getRequiredCredentials() {
return requiredCredentials; return requiredCredentials;
} }
public String getView() {
return view;
}
public String getTheme() { public String getTheme() {
return theme; return theme;
} }
@ -126,8 +190,8 @@ public class LoginBean {
return registrationUrl; return registrationUrl;
} }
public String getUsername() { public String getRegistrationAction() {
return username; return registrationAction;
} }
public boolean isSocial() { public boolean isSocial() {
@ -139,12 +203,14 @@ public class LoginBean {
return realm.isRegistrationAllowed(); return realm.isRegistrationAllowed();
} }
private void addHiddenProperties(HttpServletRequest request, String... names) { private void addFormData(HttpServletRequest request) {
hiddenProperties = new LinkedList<Property>(); formData = new HashMap<String, String>();
for (String name : names) {
Object v = request.getAttribute(name); @SuppressWarnings("unchecked")
if (v != null) { MultivaluedMap<String, String> t = (MultivaluedMap<String, String>) request.getAttribute(FormFlows.DATA);
hiddenProperties.add(new Property(name, (String) v)); if (t != null) {
for (String k : t.keySet()) {
formData.put(k, t.getFirst(k));
} }
} }
} }
@ -169,7 +235,17 @@ public class LoginBean {
} }
private void addErrors(HttpServletRequest request) { private void addErrors(HttpServletRequest request) {
error = (String) request.getAttribute("KEYCLOAK_LOGIN_ERROR_MESSAGE"); error = (String) request.getAttribute(FormFlows.ERROR_MESSAGE);
if (error != null) {
if (view.equals("login")) {
errorDetails = error;
error = "Login failed";
} else if (view.equals("register")) {
errorDetails = error;
error = "Registration failed";
}
}
} }
public class Property { public class Property {
@ -236,13 +312,7 @@ public class LoginBean {
} }
public String getLoginUrl() { public String getLoginUrl() {
StringBuilder sb = new StringBuilder(); return socialLoginUrlBuilder.replaceQueryParam("provider_id", id).build().toString();
sb.append(socialLoginUrl);
sb.append("?provider_id=" + id);
for (Property p : hiddenProperties) {
sb.append("&" + p.getName() + "=" + p.getValue());
}
return sb.toString();
} }
} }

View file

@ -3,6 +3,11 @@
xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd "> xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd ">
<application>
<resource-bundle>
<base-name>org.keycloak.forms.messages</base-name>
<var>messages</var>
</resource-bundle>
</application>
</faces-config> </faces-config>

View file

@ -1,2 +1,2 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:include xmlns:ui="http://java.sun.com/jsf/facelets" src="theme/#{login.theme}/login.xhtml" /> <ui:include xmlns:ui="http://java.sun.com/jsf/facelets" src="theme/#{forms.theme}/login.xhtml" />

View file

@ -1,2 +1,2 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:include xmlns:ui="http://java.sun.com/jsf/facelets" src="theme/#{login.theme}/register.xhtml" /> <ui:include xmlns:ui="http://java.sun.com/jsf/facelets" src="theme/#{forms.theme}/register.xhtml" />

View file

@ -0,0 +1,34 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jstl/core" template="template.xhtml">
<ui:define name="header">Log in to <strong>#{forms.name}</strong></ui:define>
<ui:define name="form">
<form action="#{forms.loginAction}" method="post">
<div>
<label for="username">#{messages.username}</label>
<input id="username" name="username" value="#{forms.formData['username']}" type="text" />
</div>
<ui:repeat var="c" value="#{forms.requiredCredentials}">
<div>
<label for="#{c.name}">#{messages[c.label]}</label> <input id="#{c.name}" name="#{c.name}" type="#{c.inputType}" />
</div>
</ui:repeat>
<div class="aside-btn">
<!-- <input type="checkbox" id="remember" /><label for="remember">Remember Username</label> -->
<!-- <p>Forgot <a href="#">Username</a> or <a href="#">Password</a>?</p> -->
</div>
<input type="submit" value="Log In" />
</form>
</ui:define>
<ui:define name="info">
<h:panelGroup rendered="#{forms.registrationAllowed}">
<p>#{messages.noAccount} <a href="#{forms.registrationUrl}">#{messages.register}</a>.</p>
</h:panelGroup>
</ui:define>
</ui:composition>

View file

@ -0,0 +1,44 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jstl/core" template="template.xhtml">
<ui:param name="bodyClass" value="register" />
<ui:define name="header">#{messages.registerWith} <strong>#{forms.name}</strong></ui:define>
<ui:define name="form">
<form action="#{forms.registrationAction}" method="post">
<p class="subtitle">#{messages.allRequired}</p>
<div>
<label for="name">#{messages.fullName}</label>
<input type="text" id="name" name="name" value="#{forms.formData['name']}" />
</div>
<div>
<label for="email">#{messages.email}</label>
<input type="text" id="email" name="email" value="#{forms.formData['email']}" />
</div>
<div>
<label for="username">#{messages.username}</label>
<input type="text" id="username" name="username" value="#{forms.formData['username']}" />
</div>
<div>
<label for="password">#{messages.password}</label>
<input type="password" id="password" name="password" />
</div>
<div>
<label for="password-confirm">#{messages.passwordConfirm}</label>
<input type="password" id="password-confirm" name="password-confirm" />
</div>
<div class="aside-btn">
<p>By registering you agree to the <a href="#">Terms of Service</a> and the <a href="#">Privacy Policy</a>.</p>
</div>
<input type="submit" value="Register" />
</form>
</ui:define>
<ui:define name="info">
<p>#{messages.alreadyHaveAccount} <a href="#{forms.loginUrl}">#{messages.logIn}</a>.</p>
</ui:define>
</ui:composition>

View file

@ -4,3 +4,10 @@
@IMPORT url("css/zocial/zocial.css"); @IMPORT url("css/zocial/zocial.css");
@IMPORT url("css/login-register.css"); @IMPORT url("css/login-register.css");
@IMPORT url("http://fonts.googleapis.com/css?family=Open+Sans:400,300,300italic,400italic,600,600italic,700,700italic,800,800italic"); @IMPORT url("http://fonts.googleapis.com/css?family=Open+Sans:400,300,300italic,400italic,600,600italic,700,700italic,800,800italic");
.zocial.google {
background-color: #dd4b39 !important;
}
.zocial.google:before {
content: "+" !important;
}

View file

@ -3,42 +3,47 @@
<h:head> <h:head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Log in to #{login.name}</title> <title>#{messages.logInTo} #{forms.name}</title>
<link href="#{login.themeConfig['styles']}" rel="stylesheet" /> <link href="#{forms.themeConfig['styles']}" rel="stylesheet" />
<style> <style>
body { body {
background-image: url("#{login.themeConfig['background']}"); background-image: url("#{forms.themeConfig['background']}");
} }
</style> </style>
</h:head> </h:head>
<h:body class="rcue-login-register #{bodyClass}"> <h:body class="rcue-login-register #{bodyClass}">
<h:panelGroup rendered="#{not empty login.themeConfig['logo']}"> <h:panelGroup rendered="#{not empty forms.themeConfig['logo']}">
<h1><a href="#" title="Go to the home page"><img src="#{login.themeConfig['logo']}" alt="Logo" /></a></h1> <h1><a href="#" title="Go to the home page"><img src="#{forms.themeConfig['logo']}" alt="Logo" /></a></h1>
</h:panelGroup> </h:panelGroup>
<div class="content"> <div class="content">
<h2><ui:insert name="header" /></h2> <h2><ui:insert name="header" /></h2>
<div class="background-area"> <div class="background-area">
<div class="form-area #{login.social ? 'social' : ''} clearfix"> <div class="form-area #{forms.social ? 'social' : ''} clearfix">
<section class="app-form"> <section class="app-form">
<h3>Application login area</h3> <h3>Application login area</h3>
<h:panelGroup rendered="#{not empty login.error}"> <h:panelGroup rendered="#{not empty forms.error}">
<div class="feedback error bottom-left show"><p><strong>#{login.error}</strong></p></div> <div class="feedback error bottom-left show">
<p>
<strong>#{forms.error}</strong><br/>
#{forms.errorDetails}
</p>
</div>
</h:panelGroup> </h:panelGroup>
<ui:insert name="form" /> <ui:insert name="form" />
</section> </section>
<h:panelGroup rendered="#{login.social}"> <h:panelGroup rendered="#{forms.social}">
<section class="social-login"> <section class="social-login">
<span>or</span> <span>or</span>
<h3>Social login area</h3> <h3>Social login area</h3>
<p>Log In with</p> <p>#{messages.logInWith}</p>
<ul> <ul>
<ui:repeat var="p" value="#{login.providers}"> <ui:repeat var="p" value="#{forms.providers}">
<li><a href="#{p.loginUrl}" class="zocial #{p.id}"> <span class="text">#{p.name}</span></a></li> <li><a href="#{p.loginUrl}" class="zocial #{p.id}"> <span class="text">#{p.name}</span></a></li>
</ui:repeat> </ui:repeat>
</ul> </ul>
@ -48,17 +53,17 @@
<section class="info-area"> <section class="info-area">
<h3>Info area</h3> <h3>Info area</h3>
<ui:insert name="info" /> <ui:insert name="info" />
<ul> <!-- <ul> -->
<li><strong>Domain:</strong> 10.0.0.1</li> <!-- <li><strong>Domain:</strong> 10.0.0.1</li> -->
<li><strong>Zone:</strong> Live</li> <!-- <li><strong>Zone:</strong> Live</li> -->
<li><strong>Appliance:</strong> Yep</li> <!-- <li><strong>Appliance:</strong> Yep</li> -->
</ul> <!-- </ul> -->
</section> </section>
</div> </div>
</div> </div>
<h:panelGroup rendered="#{login.themeConfig['displayPoweredBy']}"> <h:panelGroup rendered="#{forms.themeConfig['displayPoweredBy']}">
<p class="powered"><a href="#">Powered by Keycloak</a></p> <p class="powered"><a href="#">#{messages.poweredByKeycloak}</a></p>
</h:panelGroup> </h:panelGroup>
</div> </div>

View file

@ -0,0 +1,32 @@
logIn=Log in
logInTo=Log in to
logInWith=Log in with
noAccount=No account?
register=Register
registerWith=Register with
allRequired=All fields are required
alreadyHaveAccount=Already have an account?
poweredByKeycloak=Powered by Keycloak
username=Username
fullName=Full name
email=Email
password=Password
passwordConfirm=Password confirmation
authenticatorCode=Authenticator Code
clientCertificate=Client Certificate
invalidUser=Invalid username or password
invalidPassword=Invalid username or password
accountDisabled=Account is disabled, contact admin
missingName=Please specify full name
missingEmail=Please specify email
missingUsername=Please specify username
missingPassword=Please specify password
invalidPasswordConfirm=Password confirmation doesn't match
usernameExists=Username already exists

View file

@ -58,7 +58,7 @@
<module>integration</module> <module>integration</module>
<module>examples</module> <module>examples</module>
<module>social</module> <module>social</module>
<module>sdk-html</module> <module>forms</module>
<!--<module>ui</module> --> <!--<module>ui</module> -->
</modules> </modules>

View file

@ -1,37 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jstl/core" template="template.xhtml">
<ui:define name="header">Log in to <strong>#{login.name}</strong></ui:define>
<ui:define name="form">
<form action="#{login.loginAction}" method="post">
<div>
<label for="username">Username</label> <input id="username" name="username" value="#{login.username}" type="text" />
</div>
<ui:repeat var="c" value="#{login.requiredCredentials}">
<div>
<label for="#{c.name}">#{c.label}</label> <input id="#{c.name}" name="#{c.name}" type="#{c.inputType}" />
</div>
</ui:repeat>
<ui:repeat var="p" value="#{login.hiddenProperties}">
<input name="#{p.name}" value="#{p.value}" type="hidden" />
</ui:repeat>
<div class="aside-btn">
<input type="checkbox" id="remember" /><label for="remember">Remember Username</label>
<p>Forgot <a href="#">Username</a> or <a href="#">Password</a>?</p>
</div>
<input type="submit" value="Log In" />
</form>
</ui:define>
<ui:define name="info">
<h:panelGroup rendered="#{login.registrationAllowed}">
<p>No account? <a href="#{login.registrationUrl}">Register</a>.</p>
</h:panelGroup>
</ui:define>
</ui:composition>

View file

@ -1,43 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jstl/core" template="template.xhtml">
<ui:param name="bodyClass" value="register" />
<ui:define name="header">Register with <strong>#{login.name}</strong></ui:define>
<ui:define name="form">
<form action="#{login.registerAction}" method="post">
<div>
<label for="name">Full name</label>
<input type="text" id="name" />
</div>
<div>
<label for="email">Email</label>
<input type="email" id="email" />
</div>
<div>
<label for="username">Username</label>
<input type="text" id="username" />
</div>
<div>
<label for="password">Password</label>
<input type="password" id="password" />
</div>
<ui:repeat var="p" value="#{login.hiddenProperties}">
<input name="#{p.name}" value="#{p.value}" type="hidden" />
</ui:repeat>
<div class="aside-btn">
<p>By registering you agree to the <a href="#">Terms of Service</a> and the <a href="#">Privacy Policy</a>.</p>
</div>
<input type="submit" value="Register" />
</form>
</ui:define>
<ui:define name="info">
<p>Already have an account? <a href="realm-login.html">Log in</a>.</p>
</ui:define>
</ui:composition>

View file

@ -103,6 +103,7 @@ public class RealmManager {
newRealm.setAccessCodeLifespan(rep.getAccessCodeLifespan()); newRealm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
newRealm.setSslNotRequired(rep.isSslNotRequired()); newRealm.setSslNotRequired(rep.isSslNotRequired());
newRealm.setCookieLoginAllowed(rep.isCookieLoginAllowed()); newRealm.setCookieLoginAllowed(rep.isCookieLoginAllowed());
newRealm.setRegistrationAllowed(rep.isRegistrationAllowed());
if (rep.getPrivateKey() == null || rep.getPublicKey() == null) { if (rep.getPrivateKey() == null || rep.getPublicKey() == null) {
generateRealmKeys(newRealm); generateRealmKeys(newRealm);
} else { } else {

View file

@ -0,0 +1,47 @@
/*
* 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.services.messages;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class Messages {
public static final String ACCOUNT_DISABLED = "accountDisabled";
public static final String INVALID_PASSWORD = "invalidPassword";
public static final String INVALID_PASSWORD_CONFIRM = "invalidPasswordConfirm";
public static final String INVALID_USER = "invalidUser";
public static final String MISSING_EMAIL = "missingEmail";
public static final String MISSING_NAME = "missingName";
public static final String MISSING_PASSWORD = "missingPassword";
public static final String MISSING_USERNAME = "missingUsername";
public static final String USERNAME_EXISTS = "usernameExists";
}

View file

@ -62,19 +62,19 @@ public class RequiredCredentialModel {
PASSWORD.setType(CredentialRepresentation.PASSWORD); PASSWORD.setType(CredentialRepresentation.PASSWORD);
PASSWORD.setInput(true); PASSWORD.setInput(true);
PASSWORD.setSecret(true); PASSWORD.setSecret(true);
PASSWORD.setFormLabel("Password"); PASSWORD.setFormLabel("password");
map.put(PASSWORD.getType(), PASSWORD); map.put(PASSWORD.getType(), PASSWORD);
TOTP = new RequiredCredentialModel(); TOTP = new RequiredCredentialModel();
TOTP.setType(CredentialRepresentation.TOTP); TOTP.setType(CredentialRepresentation.TOTP);
TOTP.setInput(true); TOTP.setInput(true);
TOTP.setSecret(false); TOTP.setSecret(false);
TOTP.setFormLabel("Authenticator Code"); TOTP.setFormLabel("authenticatorCode");
map.put(TOTP.getType(), TOTP); map.put(TOTP.getType(), TOTP);
CLIENT_CERT = new RequiredCredentialModel(); CLIENT_CERT = new RequiredCredentialModel();
CLIENT_CERT.setType(CredentialRepresentation.CLIENT_CERT); CLIENT_CERT.setType(CredentialRepresentation.CLIENT_CERT);
CLIENT_CERT.setInput(false); CLIENT_CERT.setInput(false);
CLIENT_CERT.setSecret(false); CLIENT_CERT.setSecret(false);
CLIENT_CERT.setFormLabel("Client Certificate"); CLIENT_CERT.setFormLabel("clientCertificate");
map.put(CLIENT_CERT.getType(), CLIENT_CERT); map.put(CLIENT_CERT.getType(), CLIENT_CERT);
BUILT_IN = Collections.unmodifiableMap(map); BUILT_IN = Collections.unmodifiableMap(map);
} }

View file

@ -1,112 +0,0 @@
package org.keycloak.services.resources;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.keycloak.services.JspRequestParameters;
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.models.RealmModel;
import org.keycloak.services.models.RoleModel;
import org.keycloak.services.models.UserModel;
import java.net.URI;
public abstract class AbstractLoginService {
@Context
protected UriInfo uriInfo;
@Context
protected HttpHeaders headers;
@Context
HttpRequest request;
@Context
HttpResponse response;
public final static String securityFailurePath = "/saas/securityFailure.jsp";
public final static String loginFormPath = "/sdk/login.xhtml";
public final static String oauthFormPath = "/saas/oauthGrantForm.jsp";
protected RealmModel realm;
protected TokenManager tokenManager;
protected AuthenticationManager authManager = new AuthenticationManager();
public AbstractLoginService(RealmModel realm, TokenManager tokenManager) {
this.realm = realm;
this.tokenManager = tokenManager;
}
protected Response processAccessCode(String scopeParam, String state, String redirect, UserModel client, UserModel user) {
RoleModel resourceRole = realm.getRole(RealmManager.RESOURCE_ROLE);
RoleModel identityRequestRole = realm.getRole(RealmManager.IDENTITY_REQUESTER_ROLE);
boolean isResource = realm.hasRole(client, resourceRole);
if (!isResource && !realm.hasRole(client, identityRequestRole)) {
securityFailureForward("Login requester not allowed to request login.");
return null;
}
AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user);
getLogger().info("processAccessCode: isResource: " + isResource);
getLogger().info("processAccessCode: go to oauth page?: "
+ (!isResource && (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested()
.size() > 0)));
if (!isResource
&& (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested().size() > 0)) {
oauthGrantPage(accessCode, client);
return null;
}
return redirectAccessCode(accessCode, state, redirect);
}
protected Response redirectAccessCode(AccessCodeEntry accessCode, String state, String redirect) {
String code = accessCode.getCode();
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("code", code);
getLogger().info("redirectAccessCode: state: " + state);
if (state != null)
redirectUri.queryParam("state", state);
Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
if (realm.isCookieLoginAllowed()) {
location.cookie(authManager.createLoginCookie(realm, accessCode.getUser(), uriInfo));
}
return location.build();
}
protected void securityFailureForward(String message) {
getLogger().error(message);
request.setAttribute(JspRequestParameters.KEYCLOAK_SECURITY_FAILURE_MESSAGE, message);
request.forward(securityFailurePath);
}
protected void forwardToLoginForm(String redirect, String clientId, String scopeParam, String state) {
request.setAttribute(RealmModel.class.getName(), realm);
request.setAttribute("KEYCLOAK_LOGIN_ACTION", TokenService.processLoginUrl(uriInfo).build(realm.getId()));
request.setAttribute("KEYCLOAK_SOCIAL_LOGIN", SocialService.redirectToProviderAuthUrl(uriInfo).build(realm.getId()));
request.setAttribute("KEYCLOAK_REGISTRATION_PAGE", URI.create("not-implemented-yet"));
// RESTEASY eats the form data, so we send via an attribute
request.setAttribute("redirect_uri", redirect);
request.setAttribute("client_id", clientId);
request.setAttribute("scope", scopeParam);
request.setAttribute("state", state);
request.forward(loginFormPath);
}
protected void oauthGrantPage(AccessCodeEntry accessCode, UserModel client) {
request.setAttribute("realmRolesRequested", accessCode.getRealmRolesRequested());
request.setAttribute("resourceRolesRequested", accessCode.getResourceRolesRequested());
request.setAttribute("client", client);
request.setAttribute("action", TokenService.processOAuthUrl(uriInfo).build(realm.getId()).toString());
request.setAttribute("code", accessCode.getCode());
request.forward(oauthFormPath);
}
protected abstract Logger getLogger();
}

View file

@ -49,7 +49,11 @@ public class KeycloakApplication extends Application {
KeycloakSessionFactory f = createSessionFactory(); KeycloakSessionFactory f = createSessionFactory();
this.factory = f; this.factory = f;
classes.add(KeycloakSessionCleanupFilter.class); classes.add(KeycloakSessionCleanupFilter.class);
singletons.add(new RealmsResource(new TokenManager(), new SocialRequestManager()));
TokenManager tokenManager = new TokenManager();
singletons.add(new RealmsResource(tokenManager));
singletons.add(new SocialResource(tokenManager, new SocialRequestManager()));
classes.add(SkeletonKeyContextResolver.class); classes.add(SkeletonKeyContextResolver.class);
classes.add(SaasService.class); classes.add(SaasService.class);
} }

View file

@ -41,7 +41,7 @@ public class PublicRealmResource {
@NoCache @NoCache
@Produces("application/json") @Produces("application/json")
public PublishedRealmRepresentation getRealm(@PathParam("realm") String id) { public PublishedRealmRepresentation getRealm(@PathParam("realm") String id) {
return new Transaction() { return new Transaction<PublishedRealmRepresentation>() {
protected PublishedRealmRepresentation callImpl() { protected PublishedRealmRepresentation callImpl() {
return realmRep(realm, uriInfo); return realmRep(realm, uriInfo);
} }
@ -53,7 +53,7 @@ public class PublicRealmResource {
@Path("html") @Path("html")
@Produces("text/html") @Produces("text/html")
public String getRealmHtml(@PathParam("realm") String id) { public String getRealmHtml(@PathParam("realm") String id) {
return new Transaction() { return new Transaction<String>() {
protected String callImpl() { protected String callImpl() {
StringBuffer html = new StringBuffer(); StringBuffer html = new StringBuffer();

View file

@ -43,11 +43,8 @@ public class RealmsResource {
protected TokenManager tokenManager; protected TokenManager tokenManager;
protected SocialRequestManager socialRequestManager; public RealmsResource(TokenManager tokenManager) {
public RealmsResource(TokenManager tokenManager, SocialRequestManager socialRequestManager) {
this.tokenManager = tokenManager; this.tokenManager = tokenManager;
this.socialRequestManager = socialRequestManager;
} }
public static UriBuilder realmBaseUrl(UriInfo uriInfo) { public static UriBuilder realmBaseUrl(UriInfo uriInfo) {
@ -56,7 +53,7 @@ public class RealmsResource {
@Path("{realm}/tokens") @Path("{realm}/tokens")
public TokenService getTokenService(final @PathParam("realm") String id) { public TokenService getTokenService(final @PathParam("realm") String id) {
return new Transaction(false) { return new Transaction<TokenService>(false) {
@Override @Override
protected TokenService callImpl() { protected TokenService callImpl() {
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);
@ -73,27 +70,9 @@ public class RealmsResource {
} }
@Path("{realm}/social")
public SocialService getSocialService(final @PathParam("realm") String id) {
return new Transaction(false) {
@Override
protected SocialService callImpl() {
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.getRealm(id);
if (realm == null) {
logger.debug("realm not found");
throw new NotFoundException();
}
SocialService socialService = new SocialService(realm, tokenManager, socialRequestManager);
resourceContext.initResource(socialService);
return socialService;
}
}.call();
}
@Path("{realm}") @Path("{realm}")
public PublicRealmResource getRealmResource(final @PathParam("realm") String id) { public PublicRealmResource getRealmResource(final @PathParam("realm") String id) {
return new Transaction(false) { return new Transaction<PublicRealmResource>(false) {
@Override @Override
protected PublicRealmResource callImpl() { protected PublicRealmResource callImpl() {
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);

View file

@ -9,14 +9,17 @@ import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.models.RealmModel; import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.RoleModel; import org.keycloak.services.models.RoleModel;
import org.keycloak.services.models.UserCredentialModel; import org.keycloak.services.models.UserCredentialModel;
import org.keycloak.services.models.UserModel; import org.keycloak.services.models.UserModel;
import org.keycloak.services.resources.admin.RealmsAdminResource; import org.keycloak.services.resources.admin.RealmsAdminResource;
import org.keycloak.services.resources.flows.Flows;
import javax.ws.rs.*; import javax.ws.rs.*;
import javax.ws.rs.core.*; import javax.ws.rs.core.*;
import java.net.URI; import java.net.URI;
import java.util.StringTokenizer; import java.util.StringTokenizer;
@ -39,8 +42,6 @@ public class SaasService {
@Context @Context
HttpResponse response; HttpResponse response;
protected String saasLoginPath = "/saas/saas-login.jsp";
protected String saasRegisterPath = "/saas/saas-register.jsp";
protected String adminPath = "/saas/admin/index.html"; protected String adminPath = "/saas/admin/index.html";
protected AuthenticationManager authManager = new AuthenticationManager(); protected AuthenticationManager authManager = new AuthenticationManager();
@ -78,12 +79,13 @@ public class SaasService {
@NoCache @NoCache
public Response keepalive(final @Context HttpHeaders headers) { public Response keepalive(final @Context HttpHeaders headers) {
logger.info("keepalive"); logger.info("keepalive");
return new Transaction() { return new Transaction<Response>() {
@Override @Override
public Response callImpl() { public Response callImpl() {
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.defaultRealm(); RealmModel realm = realmManager.defaultRealm();
if (realm == null) throw new NotFoundException(); if (realm == null)
throw new NotFoundException();
UserModel user = authManager.authenticateSaasIdentityCookie(realm, uriInfo, headers); UserModel user = authManager.authenticateSaasIdentityCookie(realm, uriInfo, headers);
if (user == null) { if (user == null) {
return Response.status(401).build(); return Response.status(401).build();
@ -99,12 +101,13 @@ public class SaasService {
@Produces("application/json") @Produces("application/json")
@NoCache @NoCache
public Response whoAmI(final @Context HttpHeaders headers) { public Response whoAmI(final @Context HttpHeaders headers) {
return new Transaction() { return new Transaction<Response>() {
@Override @Override
public Response callImpl() { public Response callImpl() {
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.defaultRealm(); RealmModel realm = realmManager.defaultRealm();
if (realm == null) throw new NotFoundException(); if (realm == null)
throw new NotFoundException();
UserModel user = authManager.authenticateSaasIdentityCookie(realm, uriInfo, headers); UserModel user = authManager.authenticateSaasIdentityCookie(realm, uriInfo, headers);
if (user == null) { if (user == null) {
return Response.status(401).build(); return Response.status(401).build();
@ -119,7 +122,7 @@ public class SaasService {
@Produces("application/javascript") @Produces("application/javascript")
@NoCache @NoCache
public String isLoggedIn(final @Context HttpHeaders headers) { public String isLoggedIn(final @Context HttpHeaders headers) {
return new Transaction() { return new Transaction<String>() {
@Override @Override
public String callImpl() { public String callImpl() {
logger.info("WHOAMI Javascript start."); logger.info("WHOAMI Javascript start.");
@ -139,7 +142,6 @@ public class SaasService {
}.call(); }.call();
} }
public static UriBuilder contextRoot(UriInfo uriInfo) { public static UriBuilder contextRoot(UriInfo uriInfo) {
return UriBuilder.fromUri(uriInfo.getBaseUri()).replacePath("/auth-server"); return UriBuilder.fromUri(uriInfo.getBaseUri()).replacePath("/auth-server");
} }
@ -150,12 +152,13 @@ public class SaasService {
@Path("admin/realms") @Path("admin/realms")
public RealmsAdminResource getRealmsAdmin(@Context final HttpHeaders headers) { public RealmsAdminResource getRealmsAdmin(@Context final HttpHeaders headers) {
return new Transaction(false) { return new Transaction<RealmsAdminResource>(false) {
@Override @Override
protected RealmsAdminResource callImpl() { protected RealmsAdminResource callImpl() {
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);
RealmModel saasRealm = realmManager.defaultRealm(); RealmModel saasRealm = realmManager.defaultRealm();
if (saasRealm == null) throw new NotFoundException(); if (saasRealm == null)
throw new NotFoundException();
UserModel admin = authManager.authenticateSaasIdentity(saasRealm, uriInfo, headers); UserModel admin = authManager.authenticateSaasIdentity(saasRealm, uriInfo, headers);
if (admin == null) { if (admin == null) {
throw new NotAuthorizedException("Bearer"); throw new NotAuthorizedException("Bearer");
@ -170,7 +173,7 @@ public class SaasService {
}.call(); }.call();
} }
@Path("loginPage.html") @Path("login")
@GET @GET
@NoCache @NoCache
public void loginPage() { public void loginPage() {
@ -180,7 +183,24 @@ public class SaasService {
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.defaultRealm(); RealmModel realm = realmManager.defaultRealm();
authManager.expireSaasIdentityCookie(uriInfo); authManager.expireSaasIdentityCookie(uriInfo);
forwardToLoginForm(realm);
Flows.forms(realm, request).forwardToLogin();
}
}.run();
}
@Path("registrations")
@GET
@NoCache
public void registerPage() {
new Transaction() {
@Override
protected void runImpl() {
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.defaultRealm();
authManager.expireSaasIdentityCookie(uriInfo);
Flows.forms(realm, request).forwardToRegistration();
} }
}.run(); }.run();
} }
@ -195,12 +215,12 @@ public class SaasService {
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.defaultRealm(); RealmModel realm = realmManager.defaultRealm();
authManager.expireSaasIdentityCookie(uriInfo); authManager.expireSaasIdentityCookie(uriInfo);
forwardToLoginForm(realm);
Flows.forms(realm, request).forwardToLogin();
} }
}.run(); }.run();
} }
@Path("logout-cookie") @Path("logout-cookie")
@GET @GET
@NoCache @NoCache
@ -214,30 +234,18 @@ public class SaasService {
}.run(); }.run();
} }
public final static String loginFormPath = "/sdk/login.xhtml";
protected void forwardToLoginForm(RealmModel realm) {
request.setAttribute(RealmModel.class.getName(), realm);
URI action = uriInfo.getBaseUriBuilder().path(SaasService.class).path(SaasService.class, "processLogin").build();
URI register = contextRoot(uriInfo).path(saasRegisterPath).build();
request.setAttribute("KEYCLOAK_LOGIN_ACTION", action);
request.setAttribute("KEYCLOAK_REGISTRATION_PAGE", register);
request.setAttribute("KEYCLOAK_SOCIAL_LOGIN", SocialService.redirectToProviderAuthUrl(uriInfo).build(realm.getId()));
request.forward(loginFormPath);
}
@Path("login") @Path("login")
@POST @POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processLogin(final MultivaluedMap<String, String> formData) { public Response processLogin(final MultivaluedMap<String, String> formData) {
logger.info("processLogin start"); logger.info("processLogin start");
return new Transaction() { return new Transaction<Response>() {
@Override @Override
protected Response callImpl() { protected Response callImpl() {
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.defaultRealm(); RealmModel realm = realmManager.defaultRealm();
if (realm == null) throw new NotFoundException(); if (realm == null)
throw new NotFoundException();
if (!realm.isEnabled()) { if (!realm.isEnabled()) {
throw new NotImplementedYetException(); throw new NotImplementedYetException();
@ -246,29 +254,27 @@ public class SaasService {
UserModel user = realm.getUser(username); UserModel user = realm.getUser(username);
if (user == null) { if (user == null) {
logger.info("Not Authenticated! Incorrect user name"); logger.info("Not Authenticated! Incorrect user name");
request.setAttribute("KEYCLOAK_LOGIN_ERROR_MESSAGE", "Incorrect user name.");
forwardToLoginForm(realm); return Flows.forms(realm, request).setError(Messages.INVALID_USER).setFormData(formData)
return null; .forwardToLogin();
} }
if (!user.isEnabled()) { if (!user.isEnabled()) {
logger.info("NAccount is disabled, contact admin."); logger.info("Account is disabled, contact admin.");
request.setAttribute("KEYCLOAK_LOGIN_ERROR_MESSAGE", "Account is disabled, contact admin.");
forwardToLoginForm(realm); return Flows.forms(realm, request).setError(Messages.ACCOUNT_DISABLED)
return null; .setFormData(formData).forwardToLogin();
} }
boolean authenticated = authManager.authenticateForm(realm, user, formData); boolean authenticated = authManager.authenticateForm(realm, user, formData);
if (!authenticated) { if (!authenticated) {
logger.info("Not Authenticated! Invalid credentials"); logger.info("Not Authenticated! Invalid credentials");
request.setAttribute("KEYCLOAK_LOGIN_ERROR_MESSAGE", "Invalid credentials.");
forwardToLoginForm(realm); return Flows.forms(realm, request).setError(Messages.INVALID_PASSWORD).setFormData(formData)
return null; .forwardToLogin();
} }
NewCookie cookie = authManager.createSaasIdentityCookie(realm, user, uriInfo); NewCookie cookie = authManager.createSaasIdentityCookie(realm, user, uriInfo);
return Response.status(302) return Response.status(302).cookie(cookie).location(contextRoot(uriInfo).path(adminPath).build()).build();
.cookie(cookie)
.location(contextRoot(uriInfo).path(adminPath).build()).build();
} }
}.call(); }.call();
} }
@ -277,7 +283,7 @@ public class SaasService {
@POST @POST
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public Response register(final UserRepresentation newUser) { public Response register(final UserRepresentation newUser) {
return new Transaction() { return new Transaction<Response>() {
@Override @Override
protected Response callImpl() { protected Response callImpl() {
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);
@ -295,24 +301,24 @@ public class SaasService {
@Path("registrations") @Path("registrations")
@POST @POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processRegister(final @FormParam("name") String fullname, public Response processRegister(final MultivaluedMap<String, String> formData) {
final @FormParam("email") String email, return new Transaction<Response>() {
final @FormParam("username") String username,
final @FormParam("password") String password,
final @FormParam("password-confirm") String confirm) {
if (!password.equals(confirm)) {
request.setAttribute("KEYCLOAK_LOGIN_ERROR_MESSAGE", "Password confirmation doesn't match.");
request.forward(saasRegisterPath);
return null;
}
return new Transaction() {
@Override @Override
protected Response callImpl() { protected Response callImpl() {
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);
RealmModel defaultRealm = realmManager.defaultRealm(); RealmModel defaultRealm = realmManager.defaultRealm();
String error = validateRegistrationForm(formData);
if (error != null) {
return Flows.forms(defaultRealm, request).setError(error).setFormData(formData)
.forwardToRegistration();
}
UserRepresentation newUser = new UserRepresentation(); UserRepresentation newUser = new UserRepresentation();
newUser.setUsername(username); newUser.setUsername(formData.getFirst("username"));
newUser.setEmail(email); newUser.setEmail(formData.getFirst("email"));
String fullname = formData.getFirst("name");
if (fullname != null) { if (fullname != null) {
StringTokenizer tokenizer = new StringTokenizer(fullname, " "); StringTokenizer tokenizer = new StringTokenizer(fullname, " ");
StringBuffer first = null; StringBuffer first = null;
@ -330,16 +336,16 @@ public class SaasService {
last = token; last = token;
} }
} }
if (first == null) first = new StringBuffer(); if (first == null)
first = new StringBuffer();
newUser.setFirstName(first.toString()); newUser.setFirstName(first.toString());
newUser.setLastName(last); newUser.setLastName(last);
} }
newUser.credential(CredentialRepresentation.PASSWORD, password); newUser.credential(CredentialRepresentation.PASSWORD, formData.getFirst("password"));
UserModel user = registerMe(defaultRealm, newUser); UserModel user = registerMe(defaultRealm, newUser);
if (user == null) { if (user == null) {
request.setAttribute("KEYCLOAK_LOGIN_ERROR_MESSAGE", "Username already exists."); return Flows.forms(defaultRealm, request).setError(Messages.USERNAME_EXISTS)
request.forward(saasRegisterPath); .setFormData(formData).forwardToRegistration();
return null;
} }
NewCookie cookie = authManager.createSaasIdentityCookie(defaultRealm, user, uriInfo); NewCookie cookie = authManager.createSaasIdentityCookie(defaultRealm, user, uriInfo);
@ -348,7 +354,6 @@ public class SaasService {
}.call(); }.call();
} }
protected UserModel registerMe(RealmModel defaultRealm, UserRepresentation newUser) { protected UserModel registerMe(RealmModel defaultRealm, UserRepresentation newUser) {
if (!defaultRealm.isEnabled()) { if (!defaultRealm.isEnabled()) {
throw new ForbiddenException(); throw new ForbiddenException();
@ -376,5 +381,32 @@ public class SaasService {
return user; return user;
} }
private String validateRegistrationForm(MultivaluedMap<String, String> formData) {
if (isEmpty(formData.getFirst("name"))) {
return Messages.MISSING_NAME;
}
if (isEmpty(formData.getFirst("email"))) {
return Messages.MISSING_EMAIL;
}
if (isEmpty(formData.getFirst("username"))) {
return Messages.MISSING_USERNAME;
}
if (isEmpty(formData.getFirst("password"))) {
return Messages.MISSING_PASSWORD;
}
if (!formData.getFirst("password").equals(formData.getFirst("password-confirm"))) {
return Messages.INVALID_PASSWORD_CONFIRM;
}
return null;
}
private boolean isEmpty(String s) {
return s == null || s.length() == 0;
}
} }

View file

@ -24,7 +24,6 @@ package org.keycloak.services.resources;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
@ -32,20 +31,25 @@ import java.util.Map.Entry;
import javax.imageio.spi.ServiceRegistry; import javax.imageio.spi.ServiceRegistry;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.Produces; import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.TokenManager; import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.models.RealmModel; import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.RoleModel;
import org.keycloak.services.models.UserModel; import org.keycloak.services.models.UserModel;
import org.keycloak.services.resources.flows.Flows;
import org.keycloak.services.resources.flows.OAuthFlows;
import org.keycloak.services.resources.flows.Urls;
import org.keycloak.social.AuthCallback; import org.keycloak.social.AuthCallback;
import org.keycloak.social.AuthRequest; import org.keycloak.social.AuthRequest;
import org.keycloak.social.RequestDetails; import org.keycloak.social.RequestDetails;
@ -59,52 +63,69 @@ import org.keycloak.social.SocialUser;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class SocialService extends AbstractLoginService { @Path("/social")
public class SocialResource {
private static final Logger logger = Logger.getLogger(SocialService.class); protected static Logger logger = Logger.getLogger(SocialResource.class);
@Context @Context
private HttpHeaders headers; protected UriInfo uriInfo;
@Context @Context
private UriInfo uriInfo; protected HttpHeaders headers;
public static UriBuilder socialServiceBaseUrl(UriInfo uriInfo) { @Context
UriBuilder base = uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getSocialService"); private HttpRequest request;
return base;
}
public static UriBuilder redirectToProviderAuthUrl(UriInfo uriInfo) {
return socialServiceBaseUrl(uriInfo).path(SocialService.class, "redirectToProviderAuth");
}
public static UriBuilder callbackUrl(UriInfo uriInfo) {
return socialServiceBaseUrl(uriInfo).path(SocialService.class, "callback");
}
private SocialRequestManager socialRequestManager; private SocialRequestManager socialRequestManager;
public SocialService(RealmModel realm, TokenManager tokenManager, SocialRequestManager socialRequestManager) { private TokenManager tokenManager;
super(realm, tokenManager);
private AuthenticationManager authManager = new AuthenticationManager();
public SocialResource(TokenManager tokenManager, SocialRequestManager socialRequestManager) {
this.tokenManager = tokenManager;
this.socialRequestManager = socialRequestManager; this.socialRequestManager = socialRequestManager;
} }
@GET @GET
@Path("callback") @Path("callback")
public Response callback() throws URISyntaxException { public Response callback() throws URISyntaxException {
return new Transaction() { return new Transaction<Response>() {
protected Response callImpl() { protected Response callImpl() {
Map<String, String[]> queryParams = getQueryParams(); Map<String, String[]> queryParams = getQueryParams();
RequestDetails requestData = getRequestDetails(queryParams); RequestDetails requestData = getRequestDetails(queryParams);
SocialProvider provider = getProvider(requestData.getProviderId()); SocialProvider provider = getProvider(requestData.getProviderId());
String realmId = requestData.getClientAttribute("realmId");
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.getRealm(realmId);
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
if (!realm.isEnabled()) {
return oauth.forwardToSecurityFailure("Realm not enabled.");
}
if (!realm.isEnabled()) {
return oauth.forwardToSecurityFailure("Realm not enabled.");
}
String clientId = requestData.getClientAttributes().get("clientId");
UserModel client = realm.getUser(clientId);
if (client == null) {
return oauth.forwardToSecurityFailure("Unknown login requester.");
}
if (!client.isEnabled()) {
return oauth.forwardToSecurityFailure("Login requester not enabled.");
}
String key = System.getProperty("keycloak.social." + requestData.getProviderId() + ".key"); String key = System.getProperty("keycloak.social." + requestData.getProviderId() + ".key");
String secret = System.getProperty("keycloak.social." + requestData.getProviderId() + ".secret"); String secret = System.getProperty("keycloak.social." + requestData.getProviderId() + ".secret");
String callbackUri = callbackUrl(uriInfo).build(realm.getId()).toString(); String callbackUri = Urls.socialCallback(uriInfo.getBaseUri()).toString();
SocialProviderConfig config = new SocialProviderConfig(key, secret, callbackUri); SocialProviderConfig config = new SocialProviderConfig(key, secret, callbackUri);
AuthCallback callback = new AuthCallback(requestData.getSocialAttributes(), queryParams); AuthCallback callback = new AuthCallback(requestData.getSocialAttributes(), queryParams);
@ -114,25 +135,7 @@ public class SocialService extends AbstractLoginService {
socialUser = provider.processCallback(config, callback); socialUser = provider.processCallback(config, callback);
} catch (SocialProviderException e) { } catch (SocialProviderException e) {
logger.warn("Failed to process social callback", e); logger.warn("Failed to process social callback", e);
securityFailureForward("Failed to process social callback"); return oauth.forwardToSecurityFailure("Failed to process social callback");
return null;
}
if (!realm.isEnabled()) {
securityFailureForward("Realm not enabled.");
return null;
}
String clientId = requestData.getClientAttributes().get("clientId");
UserModel client = realm.getUser(clientId);
if (client == null) {
securityFailureForward("Unknown login requester.");
return null;
}
if (!client.isEnabled()) {
securityFailureForward("Login requester not enabled.");
return null;
} }
// TODO Lookup user based on attribute for provider id - this is so a user can have a friendly username + link a // TODO Lookup user based on attribute for provider id - this is so a user can have a friendly username + link a
@ -145,72 +148,55 @@ public class SocialService extends AbstractLoginService {
user.setAttribute(provider.getId() + ".id", socialUser.getId()); user.setAttribute(provider.getId() + ".id", socialUser.getId());
// TODO Grant default roles for realm when available // TODO Grant default roles for realm when available
realm.grantRole(user, realm.getRole("user")); RoleModel defaultRole = realm.getRole("user");
realm.grantRole(user, defaultRole);
} }
if (!user.isEnabled()) { if (!user.isEnabled()) {
securityFailureForward("Your account is not enabled."); return oauth.forwardToSecurityFailure("Your account is not enabled.");
return null;
} }
String scope = requestData.getClientAttributes().get("scope"); String scope = requestData.getClientAttributes().get("scope");
String state = requestData.getClientAttributes().get("state"); String state = requestData.getClientAttributes().get("state");
String redirectUri = requestData.getClientAttributes().get("redirectUri"); String redirectUri = requestData.getClientAttributes().get("redirectUri");
return processAccessCode(scope, state, redirectUri, client, user); return oauth.processAccessCode(scope, state, redirectUri, client, user);
} }
}.call(); }.call();
} }
@GET @GET
@Path("providers") @Path("{realm}/login")
@Produces(MediaType.APPLICATION_JSON) public Response redirectToProviderAuth(@PathParam("realm") final String realmId,
public List<SocialProvider> getProviders() { @QueryParam("provider_id") final String providerId, @QueryParam("client_id") final String clientId,
List<SocialProvider> providers = new LinkedList<SocialProvider>(); @QueryParam("scope") final String scope, @QueryParam("state") final String state,
Iterator<SocialProvider> itr = ServiceRegistry.lookupProviders(SocialProvider.class); @QueryParam("redirect_uri") final String redirectUri) {
while (itr.hasNext()) { SocialProvider provider = getProvider(providerId);
providers.add(itr.next()); if (provider == null) {
return Flows.pages(request).forwardToSecurityFailure("Social provider not found");
} }
return providers;
}
@GET String key = System.getProperty("keycloak.social." + providerId + ".key");
@Path("login") String secret = System.getProperty("keycloak.social." + providerId + ".secret");
public Response redirectToProviderAuth(@QueryParam("provider_id") final String providerId, String callbackUri = Urls.socialCallback(uriInfo.getBaseUri()).toString();
@QueryParam("client_id") final String clientId, @QueryParam("scope") final String scope,
@QueryParam("state") final String state, @QueryParam("redirect_uri") final String redirectUri) {
return new Transaction() {
protected Response callImpl() {
SocialProvider provider = getProvider(providerId);
if (provider == null) {
securityFailureForward("Social provider not found");
return null;
}
String key = System.getProperty("keycloak.social." + providerId + ".key"); SocialProviderConfig config = new SocialProviderConfig(key, secret, callbackUri);
String secret = System.getProperty("keycloak.social." + providerId + ".secret");
String callbackUri = callbackUrl(uriInfo).build(realm.getId()).toString();
SocialProviderConfig config = new SocialProviderConfig(key, secret, callbackUri); try {
AuthRequest authRequest = provider.getAuthUrl(config);
try { RequestDetails socialRequest = RequestDetailsBuilder.create(providerId)
AuthRequest authRequest = provider.getAuthUrl(config); .putSocialAttributes(authRequest.getAttributes()).putClientAttribute("realmId", realmId)
.putClientAttribute("clientId", clientId).putClientAttribute("scope", scope)
.putClientAttribute("state", state).putClientAttribute("redirectUri", redirectUri).build();
RequestDetails socialRequest = RequestDetailsBuilder.create(providerId) socialRequestManager.addRequest(authRequest.getId(), socialRequest);
.putSocialAttributes(authRequest.getAttributes()).putClientAttribute("clientId", clientId)
.putClientAttribute("scope", scope).putClientAttribute("state", state)
.putClientAttribute("redirectUri", redirectUri).build();
socialRequestManager.addRequest(authRequest.getId(), socialRequest); return Response.status(Status.FOUND).location(authRequest.getAuthUri()).build();
} catch (Throwable t) {
return Response.status(Status.FOUND).location(authRequest.getAuthUri()).build(); return Flows.pages(request).forwardToSecurityFailure("Failed to redirect to social auth");
} catch (Throwable t) { }
logger.error("Failed to redirect to social auth", t);
securityFailureForward("Failed to redirect to social auth");
return null;
}
}
}.call();
} }
private RequestDetails getRequestDetails(Map<String, String[]> queryParams) { private RequestDetails getRequestDetails(Map<String, String[]> queryParams) {
@ -251,9 +237,4 @@ public class SocialService extends AbstractLoginService {
return queryParams; return queryParams;
} }
@Override
protected Logger getLogger() {
return logger;
}
} }

View file

@ -6,18 +6,27 @@ import org.jboss.resteasy.jose.jws.JWSInput;
import org.jboss.resteasy.jose.jws.crypto.RSAProvider; import org.jboss.resteasy.jose.jws.crypto.RSAProvider;
import org.jboss.resteasy.jwt.JsonSerialization; import org.jboss.resteasy.jwt.JsonSerialization;
import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.SkeletonKeyToken; import org.keycloak.representations.SkeletonKeyToken;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.managers.TokenManager; import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.models.RealmModel; import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.RoleModel; import org.keycloak.services.models.RoleModel;
import org.keycloak.services.models.UserCredentialModel;
import org.keycloak.services.models.UserModel; import org.keycloak.services.models.UserModel;
import org.keycloak.services.resources.flows.Flows;
import org.keycloak.services.resources.flows.OAuthFlows;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.NotAuthorizedException; import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.POST; import javax.ws.rs.POST;
@ -25,40 +34,55 @@ import javax.ws.rs.Path;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers; import javax.ws.rs.ext.Providers;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.StringTokenizer;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class TokenService extends AbstractLoginService { public class TokenService {
protected static final Logger logger = Logger.getLogger(TokenService.class); protected static final Logger logger = Logger.getLogger(TokenService.class);
protected RealmModel realm;
protected TokenManager tokenManager;
protected AuthenticationManager authManager = new AuthenticationManager();
@Context @Context
protected Providers providers; protected Providers providers;
@Context @Context
protected SecurityContext securityContext; protected SecurityContext securityContext;
@Context
protected UriInfo uriInfo;
@Context
protected HttpHeaders headers;
@Context
HttpRequest request;
@Context
HttpResponse response;
private ResourceAdminManager resourceAdminManager = new ResourceAdminManager(); private ResourceAdminManager resourceAdminManager = new ResourceAdminManager();
public TokenService(RealmModel realm, TokenManager tokenManager) { public TokenService(RealmModel realm, TokenManager tokenManager) {
super(realm, tokenManager); this.realm = realm;
this.tokenManager = tokenManager;
} }
public static UriBuilder tokenServiceBaseUrl(UriInfo uriInfo) { public static UriBuilder tokenServiceBaseUrl(UriInfo uriInfo) {
UriBuilder base = uriInfo.getBaseUriBuilder() UriBuilder base = uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getTokenService");
.path(RealmsResource.class).path(RealmsResource.class, "getTokenService");
return base; return base;
} }
@ -89,13 +113,12 @@ public class TokenService extends AbstractLoginService {
return tokenServiceBaseUrl(uriInfo).path(TokenService.class, "processOAuth"); return tokenServiceBaseUrl(uriInfo).path(TokenService.class, "processOAuth");
} }
@Path("grants/identity-token") @Path("grants/identity-token")
@POST @POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response grantIdentityToken(final MultivaluedMap<String, String> form) { public Response grantIdentityToken(final MultivaluedMap<String, String> form) {
return new Transaction() { return new Transaction<Response>() {
protected Response callImpl() { protected Response callImpl() {
String username = form.getFirst(AuthenticationManager.FORM_USERNAME); String username = form.getFirst(AuthenticationManager.FORM_USERNAME);
if (username == null) { if (username == null) {
@ -128,7 +151,7 @@ public class TokenService extends AbstractLoginService {
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response grantAccessToken(final MultivaluedMap<String, String> form) { public Response grantAccessToken(final MultivaluedMap<String, String> form) {
return new Transaction() { return new Transaction<Response>() {
protected Response callImpl() { protected Response callImpl() {
String username = form.getFirst(AuthenticationManager.FORM_USERNAME); String username = form.getFirst(AuthenticationManager.FORM_USERNAME);
if (username == null) { if (username == null) {
@ -159,49 +182,126 @@ public class TokenService extends AbstractLoginService {
@Path("auth/request/login") @Path("auth/request/login")
@POST @POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processLogin(final MultivaluedMap<String, String> formData) { public Response processLogin(@QueryParam("client_id") final String clientId, @QueryParam("scope") final String scopeParam,
return new Transaction() { @QueryParam("state") final String state, @QueryParam("redirect_uri") final String redirect,
final MultivaluedMap<String, String> formData) {
return new Transaction<Response>() {
protected Response callImpl() { protected Response callImpl() {
String clientId = formData.getFirst("client_id"); OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
String scopeParam = formData.getFirst("scope");
String state = formData.getFirst("state");
String redirect = formData.getFirst("redirect_uri");
if (!realm.isEnabled()) { if (!realm.isEnabled()) {
securityFailureForward("Realm not enabled."); return oauth.forwardToSecurityFailure("Realm not enabled.");
return null;
} }
UserModel client = realm.getUser(clientId); UserModel client = realm.getUser(clientId);
if (client == null) { if (client == null) {
securityFailureForward("Unknown login requester."); return oauth.forwardToSecurityFailure("Unknown login requester.");
return null;
} }
if (!client.isEnabled()) { if (!client.isEnabled()) {
securityFailureForward("Login requester not enabled."); return oauth.forwardToSecurityFailure("Login requester not enabled.");
return null;
} }
String username = formData.getFirst("username"); String username = formData.getFirst("username");
UserModel user = realm.getUser(username); UserModel user = realm.getUser(username);
if (user == null) { if (user == null) {
logger.error("Incorrect user name."); logger.error("Incorrect user name.");
request.setAttribute("KEYCLOAK_LOGIN_ERROR_MESSAGE", "Incorrect user name.");
forwardToLoginForm(redirect, clientId, scopeParam, state); return Flows.forms(realm, request).setError(Messages.INVALID_USER).setFormData(formData)
return null; .forwardToLogin();
} }
if (!user.isEnabled()) { if (!user.isEnabled()) {
securityFailureForward("Your account is not enabled."); return oauth.forwardToSecurityFailure("Your account is not enabled.");
return null;
} }
boolean authenticated = authManager.authenticateForm(realm, user, formData); boolean authenticated = authManager.authenticateForm(realm, user, formData);
if (!authenticated) { if (!authenticated) {
logger.error("Authentication failed"); logger.error("Authentication failed");
request.setAttribute("username", username);
request.setAttribute("KEYCLOAK_LOGIN_ERROR_MESSAGE", "Invalid credentials."); return Flows.forms(realm, request).setError(Messages.INVALID_PASSWORD).setFormData(formData)
forwardToLoginForm(redirect, clientId, scopeParam, state); .forwardToLogin();
return null;
} }
return processAccessCode(scopeParam, state, redirect, client, user); return oauth.processAccessCode(scopeParam, state, redirect, client, user);
}
}.call();
}
@Path("registrations")
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processRegister(@QueryParam("client_id") final String clientId,
@QueryParam("scope") final String scopeParam, @QueryParam("state") final String state,
@QueryParam("redirect_uri") final String redirect, final MultivaluedMap<String, String> formData) {
return new Transaction<Response>() {
@Override
protected Response callImpl() {
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
if (!realm.isEnabled()) {
return oauth.forwardToSecurityFailure("Realm not enabled");
}
UserModel client = realm.getUser(clientId);
if (client == null) {
return oauth.forwardToSecurityFailure("Unknown login requester.");
}
if (!client.isEnabled()) {
return oauth.forwardToSecurityFailure("Login requester not enabled.");
}
if (!realm.isRegistrationAllowed()) {
return oauth.forwardToSecurityFailure("Registration not allowed");
}
String error = validateRegistrationForm(formData);
if (error != null) {
return Flows.forms(realm, request).setError(error).setFormData(formData).forwardToRegistration();
}
String username = formData.getFirst("username");
UserModel user = realm.getUser(username);
if (user != null) {
return Flows.forms(realm, request).setError(Messages.USERNAME_EXISTS).setFormData(formData)
.forwardToRegistration();
}
user = realm.addUser(username);
String fullname = formData.getFirst("name");
if (fullname != null) {
StringTokenizer tokenizer = new StringTokenizer(fullname, " ");
StringBuffer first = null;
String last = "";
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
if (tokenizer.hasMoreTokens()) {
if (first == null) {
first = new StringBuffer();
} else {
first.append(" ");
}
first.append(token);
} else {
last = token;
}
}
if (first == null)
first = new StringBuffer();
user.setFirstName(first.toString());
user.setLastName(last);
}
user.setEmail(formData.getFirst("email"));
UserCredentialModel credentials = new UserCredentialModel();
credentials.setType(CredentialRepresentation.PASSWORD);
credentials.setValue(formData.getFirst("password"));
realm.updateCredential(user, credentials);
// TODO Grant default roles for realm when available
RoleModel defaultRole = realm.getRole("user");
realm.grantRole(user, defaultRole);
return processLogin(clientId, scopeParam, state, redirect, formData);
} }
}.call(); }.call();
} }
@ -210,7 +310,7 @@ public class TokenService extends AbstractLoginService {
@POST @POST
@Produces("application/json") @Produces("application/json")
public Response accessCodeToToken(final MultivaluedMap<String, String> formData) { public Response accessCodeToToken(final MultivaluedMap<String, String> formData) {
return new Transaction() { return new Transaction<Response>() {
protected Response callImpl() { protected Response callImpl() {
logger.info("accessRequest <---"); logger.info("accessRequest <---");
if (!realm.isEnabled()) { if (!realm.isEnabled()) {
@ -258,7 +358,6 @@ public class TokenService extends AbstractLoginService {
return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(); return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build();
} }
JWSInput input = new JWSInput(code, providers); JWSInput input = new JWSInput(code, providers);
boolean verifiedCode = false; boolean verifiedCode = false;
try { try {
@ -270,7 +369,8 @@ public class TokenService extends AbstractLoginService {
Map<String, String> res = new HashMap<String, String>(); Map<String, String> res = new HashMap<String, String>();
res.put("error", "invalid_grant"); res.put("error", "invalid_grant");
res.put("error_description", "Unable to verify code signature"); res.put("error_description", "Unable to verify code signature");
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res).build(); return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
.build();
} }
String key = input.readContent(String.class); String key = input.readContent(String.class);
AccessCodeEntry accessCode = tokenManager.pullAccessCode(key); AccessCodeEntry accessCode = tokenManager.pullAccessCode(key);
@ -278,25 +378,29 @@ public class TokenService extends AbstractLoginService {
Map<String, String> res = new HashMap<String, String>(); Map<String, String> res = new HashMap<String, String>();
res.put("error", "invalid_grant"); res.put("error", "invalid_grant");
res.put("error_description", "Code not found"); res.put("error_description", "Code not found");
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res).build(); return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
.build();
} }
if (accessCode.isExpired()) { if (accessCode.isExpired()) {
Map<String, String> res = new HashMap<String, String>(); Map<String, String> res = new HashMap<String, String>();
res.put("error", "invalid_grant"); res.put("error", "invalid_grant");
res.put("error_description", "Code is expired"); res.put("error_description", "Code is expired");
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res).build(); return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
.build();
} }
if (!accessCode.getToken().isActive()) { if (!accessCode.getToken().isActive()) {
Map<String, String> res = new HashMap<String, String>(); Map<String, String> res = new HashMap<String, String>();
res.put("error", "invalid_grant"); res.put("error", "invalid_grant");
res.put("error_description", "Token expired"); res.put("error_description", "Token expired");
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res).build(); return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
.build();
} }
if (!client.getLoginName().equals(accessCode.getClient().getLoginName())) { if (!client.getLoginName().equals(accessCode.getClient().getLoginName())) {
Map<String, String> res = new HashMap<String, String>(); Map<String, String> res = new HashMap<String, String>();
res.put("error", "invalid_grant"); res.put("error", "invalid_grant");
res.put("error_description", "Auth error"); res.put("error_description", "Auth error");
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res).build(); return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
.build();
} }
logger.info("accessRequest SUCCESS"); logger.info("accessRequest SUCCESS");
AccessTokenResponse res = accessTokenResponse(realm.getPrivateKey(), accessCode.getToken()); AccessTokenResponse res = accessTokenResponse(realm.getPrivateKey(), accessCode.getToken());
@ -313,9 +417,7 @@ public class TokenService extends AbstractLoginService {
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
String encodedToken = new JWSBuilder() String encodedToken = new JWSBuilder().content(tokenBytes).rsa256(privateKey);
.content(tokenBytes)
.rsa256(privateKey);
return accessTokenResponse(token, encodedToken); return accessTokenResponse(token, encodedToken);
} }
@ -334,25 +436,25 @@ public class TokenService extends AbstractLoginService {
@Path("login") @Path("login")
@GET @GET
public Response loginPage(final @QueryParam("response_type") String responseType, public Response loginPage(final @QueryParam("response_type") String responseType,
final @QueryParam("redirect_uri") String redirect, final @QueryParam("redirect_uri") String redirect, final @QueryParam("client_id") String clientId,
final @QueryParam("client_id") String clientId, final @QueryParam("scope") String scopeParam, final @QueryParam("state") String state) {
final @QueryParam("scope") String scopeParam, return new Transaction<Response>() {
final @QueryParam("state") String state) {
return new Transaction() {
protected Response callImpl() { protected Response callImpl() {
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
if (!realm.isEnabled()) { if (!realm.isEnabled()) {
securityFailureForward("Realm not enabled"); oauth.forwardToSecurityFailure("Realm not enabled");
return null; return null;
} }
UserModel client = realm.getUser(clientId); UserModel client = realm.getUser(clientId);
if (client == null) { if (client == null) {
securityFailureForward("Unknown login requester."); oauth.forwardToSecurityFailure("Unknown login requester.");
transaction.rollback(); transaction.rollback();
return null; return null;
} }
if (!client.isEnabled()) { if (!client.isEnabled()) {
securityFailureForward("Login requester not enabled."); oauth.forwardToSecurityFailure("Login requester not enabled.");
transaction.rollback(); transaction.rollback();
session.close(); session.close();
return null; return null;
@ -362,7 +464,7 @@ public class TokenService extends AbstractLoginService {
RoleModel identityRequestRole = realm.getRole(RealmManager.IDENTITY_REQUESTER_ROLE); RoleModel identityRequestRole = realm.getRole(RealmManager.IDENTITY_REQUESTER_ROLE);
boolean isResource = realm.hasRole(client, resourceRole); boolean isResource = realm.hasRole(client, resourceRole);
if (!isResource && !realm.hasRole(client, identityRequestRole)) { if (!isResource && !realm.hasRole(client, identityRequestRole)) {
securityFailureForward("Login requester not allowed to request login."); oauth.forwardToSecurityFailure("Login requester not allowed to request login.");
transaction.rollback(); transaction.rollback();
session.close(); session.close();
return null; return null;
@ -371,11 +473,42 @@ public class TokenService extends AbstractLoginService {
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers); UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
if (user != null) { if (user != null) {
logger.info(user.getLoginName() + " already logged in."); logger.info(user.getLoginName() + " already logged in.");
return processAccessCode(scopeParam, state, redirect, client, user); return oauth.processAccessCode(scopeParam, state, redirect, client, user);
} }
forwardToLoginForm(redirect, clientId, scopeParam, state); return Flows.forms(realm, request).forwardToLogin();
return null; }
}.call();
}
@Path("registrations")
@GET
public Response registerPage(final @QueryParam("response_type") String responseType,
final @QueryParam("redirect_uri") String redirect, final @QueryParam("client_id") String clientId,
final @QueryParam("scope") String scopeParam, final @QueryParam("state") String state) {
return new Transaction<Response>() {
protected Response callImpl() {
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
if (!realm.isEnabled()) {
return oauth.forwardToSecurityFailure("Realm not enabled");
}
UserModel client = realm.getUser(clientId);
if (client == null) {
return oauth.forwardToSecurityFailure("Unknown login requester.");
}
if (!client.isEnabled()) {
return oauth.forwardToSecurityFailure("Login requester not enabled.");
}
if (!realm.isRegistrationAllowed()) {
return oauth.forwardToSecurityFailure("Registration not allowed");
}
authManager.expireIdentityCookie(realm, uriInfo);
return Flows.forms(realm, request).forwardToRegistration();
} }
}.call(); }.call();
} }
@ -384,7 +517,7 @@ public class TokenService extends AbstractLoginService {
@GET @GET
@NoCache @NoCache
public Response logout(final @QueryParam("redirect_uri") String redirectUri) { public Response logout(final @QueryParam("redirect_uri") String redirectUri) {
return new Transaction() { return new Transaction<Response>() {
protected Response callImpl() { protected Response callImpl() {
// todo do we care if anybody can trigger this? // todo do we care if anybody can trigger this?
@ -404,8 +537,10 @@ public class TokenService extends AbstractLoginService {
@POST @POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processOAuth(final MultivaluedMap<String, String> formData) { public Response processOAuth(final MultivaluedMap<String, String> formData) {
return new Transaction() { return new Transaction<Response>() {
protected Response callImpl() { protected Response callImpl() {
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
String code = formData.getFirst("code"); String code = formData.getFirst("code");
JWSInput input = new JWSInput(code, providers); JWSInput input = new JWSInput(code, providers);
boolean verifiedCode = false; boolean verifiedCode = false;
@ -415,16 +550,12 @@ public class TokenService extends AbstractLoginService {
logger.debug("Failed to verify signature", ignored); logger.debug("Failed to verify signature", ignored);
} }
if (!verifiedCode) { if (!verifiedCode) {
securityFailureForward("Illegal access code."); return oauth.forwardToSecurityFailure("Illegal access code.");
session.close();
return null;
} }
String key = input.readContent(String.class); String key = input.readContent(String.class);
AccessCodeEntry accessCodeEntry = tokenManager.getAccessCode(key); AccessCodeEntry accessCodeEntry = tokenManager.getAccessCode(key);
if (accessCodeEntry == null) { if (accessCodeEntry == null) {
securityFailureForward("Unknown access code."); return oauth.forwardToSecurityFailure("Unknown access code.");
session.close();
return null;
} }
String redirect = accessCodeEntry.getRedirectUri(); String redirect = accessCodeEntry.getRedirectUri();
@ -434,21 +565,45 @@ public class TokenService extends AbstractLoginService {
return redirectAccessDenied(redirect, state); return redirectAccessDenied(redirect, state);
} }
return redirectAccessCode(accessCodeEntry, state, redirect); return oauth.redirectAccessCode(accessCodeEntry, state, redirect);
} }
}.call(); }.call();
} }
protected Response redirectAccessDenied(String redirect, String state) { protected Response redirectAccessDenied(String redirect, String state) {
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("error", "access_denied"); UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("error", "access_denied");
if (state != null) redirectUri.queryParam("state", state); if (state != null)
redirectUri.queryParam("state", state);
Response.ResponseBuilder location = Response.status(302).location(redirectUri.build()); Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
return location.build(); return location.build();
} }
@Override private String validateRegistrationForm(MultivaluedMap<String, String> formData) {
protected Logger getLogger() { if (isEmpty(formData.getFirst("name"))) {
return logger; return Messages.MISSING_NAME;
}
if (isEmpty(formData.getFirst("email"))) {
return Messages.MISSING_EMAIL;
}
if (isEmpty(formData.getFirst("username"))) {
return Messages.MISSING_USERNAME;
}
if (isEmpty(formData.getFirst("password"))) {
return Messages.MISSING_PASSWORD;
}
if (!formData.getFirst("password").equals(formData.getFirst("password-confirm"))) {
return Messages.INVALID_PASSWORD_CONFIRM;
}
return null;
}
private boolean isEmpty(String s) {
return s == null || s.length() == 0;
} }
} }

View file

@ -13,7 +13,7 @@ import javax.ws.rs.core.Application;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class Transaction { public class Transaction<T> {
protected KeycloakSession session; protected KeycloakSession session;
protected KeycloakTransaction transaction; protected KeycloakTransaction transaction;
protected boolean closeSession; protected boolean closeSession;
@ -83,7 +83,7 @@ public class Transaction {
} }
} }
protected <T> T callImpl() { protected T callImpl() {
return null; return null;
} }
@ -91,7 +91,7 @@ public class Transaction {
* Will not begin or end a transaction or close a session if the transaction was already active when called * Will not begin or end a transaction or close a session if the transaction was already active when called
* *
*/ */
public <T> T call() { public T call() {
boolean wasActive = transaction.isActive(); boolean wasActive = transaction.isActive();
if (!wasActive) transaction.begin(); if (!wasActive) transaction.begin();
try { try {

View file

@ -36,7 +36,7 @@ public class ApplicationResource {
@PUT @PUT
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public void update(final ApplicationRepresentation rep) { public void update(final ApplicationRepresentation rep) {
new Transaction() { new Transaction<Void>() {
@Override @Override
protected void runImpl() { protected void runImpl() {
ResourceManager resourceManager = new ResourceManager(new RealmManager(session)); ResourceManager resourceManager = new ResourceManager(new RealmManager(session));
@ -50,7 +50,7 @@ public class ApplicationResource {
@NoCache @NoCache
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public ApplicationRepresentation getResource(final @PathParam("id") String id) { public ApplicationRepresentation getResource(final @PathParam("id") String id) {
return new Transaction() { return new Transaction<ApplicationRepresentation>() {
@Override @Override
protected ApplicationRepresentation callImpl() { protected ApplicationRepresentation callImpl() {
ResourceManager resourceManager = new ResourceManager(new RealmManager(session)); ResourceManager resourceManager = new ResourceManager(new RealmManager(session));

View file

@ -42,7 +42,7 @@ public class ApplicationsResource {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@NoCache @NoCache
public List<ApplicationRepresentation> getResources() { public List<ApplicationRepresentation> getResources() {
return new Transaction() { return new Transaction<List<ApplicationRepresentation>>() {
@Override @Override
protected List<ApplicationRepresentation> callImpl() { protected List<ApplicationRepresentation> callImpl() {
List<ApplicationRepresentation> rep = new ArrayList<ApplicationRepresentation>(); List<ApplicationRepresentation> rep = new ArrayList<ApplicationRepresentation>();
@ -59,7 +59,7 @@ public class ApplicationsResource {
@POST @POST
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public Response createResource(final @Context UriInfo uriInfo, final ApplicationRepresentation rep) { public Response createResource(final @Context UriInfo uriInfo, final ApplicationRepresentation rep) {
return new Transaction() { return new Transaction<Response>() {
@Override @Override
protected Response callImpl() { protected Response callImpl() {
ResourceManager resourceManager = new ResourceManager(new RealmManager(session)); ResourceManager resourceManager = new ResourceManager(new RealmManager(session));
@ -71,7 +71,7 @@ public class ApplicationsResource {
@Path("{id}") @Path("{id}")
public ApplicationResource getResource(final @PathParam("id") String id) { public ApplicationResource getResource(final @PathParam("id") String id) {
return new Transaction(false) { return new Transaction<ApplicationResource>(false) {
@Override @Override
protected ApplicationResource callImpl() { protected ApplicationResource callImpl() {
ApplicationModel applicationModel = realm.getApplicationById(id); ApplicationModel applicationModel = realm.getApplicationById(id);

View file

@ -6,21 +6,25 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.UserManager;
import org.keycloak.services.models.RealmModel; import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.RoleModel; import org.keycloak.services.models.RoleModel;
import org.keycloak.services.models.UserModel; import org.keycloak.services.models.UserModel;
import org.keycloak.services.resources.Transaction; import org.keycloak.services.resources.Transaction;
import javax.ws.rs.*; import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -45,7 +49,7 @@ public class RealmAdminResource {
@NoCache @NoCache
@Produces("application/json") @Produces("application/json")
public RealmRepresentation getRealm() { public RealmRepresentation getRealm() {
return new Transaction() { return new Transaction<RealmRepresentation>() {
@Override @Override
protected RealmRepresentation callImpl() { protected RealmRepresentation callImpl() {
return new RealmManager(session).toRepresentation(realm); return new RealmManager(session).toRepresentation(realm);
@ -58,15 +62,14 @@ public class RealmAdminResource {
@GET @GET
@NoCache @NoCache
@Produces("application/json") @Produces("application/json")
public List<RoleRepresentation> queryRoles() { public List<RoleRepresentation> getRoles() {
return new Transaction() { return new Transaction<List<RoleRepresentation>>() {
@Override @Override
protected List<RoleRepresentation> callImpl() { protected List<RoleRepresentation> callImpl() {
List<RoleModel> roleModels = realm.getRoles(); List<RoleModel> roleModels = realm.getRoles();
List<RoleRepresentation> roles = new ArrayList<RoleRepresentation>(); List<RoleRepresentation> roles = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : roleModels) { for (RoleModel roleModel : roleModels) {
RoleRepresentation role = new RoleRepresentation(roleModel.getName(), roleModel.getDescription()); RoleRepresentation role = new RoleRepresentation(roleModel.getName(), roleModel.getDescription());
role.setId(roleModel.getId());
roles.add(role); roles.add(role);
} }
return roles; return roles;
@ -92,7 +95,7 @@ public class RealmAdminResource {
@NoCache @NoCache
@Produces("application/json") @Produces("application/json")
public RoleRepresentation getRole(final @PathParam("id") String id) { public RoleRepresentation getRole(final @PathParam("id") String id) {
return new Transaction() { return new Transaction<RoleRepresentation>() {
@Override @Override
protected RoleRepresentation callImpl() { protected RoleRepresentation callImpl() {
RoleModel roleModel = realm.getRoleById(id); RoleModel roleModel = realm.getRoleById(id);
@ -129,7 +132,7 @@ public class RealmAdminResource {
@POST @POST
@Consumes("application/json") @Consumes("application/json")
public Response createRole(final @Context UriInfo uriInfo, final RoleRepresentation rep) { public Response createRole(final @Context UriInfo uriInfo, final RoleRepresentation rep) {
return new Transaction() { return new Transaction<Response>() {
@Override @Override
protected Response callImpl() { protected Response callImpl() {
if (realm.getRole(rep.getName()) != null) { if (realm.getRole(rep.getName()) != null) {
@ -151,85 +154,10 @@ public class RealmAdminResource {
@GET @GET
@NoCache @NoCache
@Produces("application/json") @Produces("application/json")
public List<UserRepresentation> queryUsers(final @Context UriInfo uriInfo) { public List<UserRepresentation> getUsers() {
return new Transaction() { return null;
@Override
protected List<UserRepresentation> callImpl() {
logger.info("queryUsers");
Map<String, String> params = new HashMap<String, String>();
MultivaluedMap<String,String> queryParameters = uriInfo.getQueryParameters();
for (String key : queryParameters.keySet()) {
logger.info(" " + key + "=" + queryParameters.getFirst(key));
params.put(key, queryParameters.getFirst(key));
}
List<UserModel> userModels = realm.queryUsers(params);
List<UserRepresentation> users = new ArrayList<UserRepresentation>();
for (UserModel userModel : userModels) {
users.add(UserManager.toRepresentation(userModel));
}
logger.info(" resultSet: " + users.size());
return users;
}
}.call();
} }
@Path("users/{loginName}")
@GET
@NoCache
@Produces("application/json")
public UserRepresentation getUser(final @PathParam("loginName") String loginName) {
return new Transaction() {
@Override
protected UserRepresentation callImpl() {
UserModel userModel = realm.getUser(loginName);
if (userModel == null) {
throw new NotFoundException();
}
return UserManager.toRepresentation(userModel);
}
}.call();
}
@Path("users")
@POST
@NoCache
@Consumes("application/json")
public Response createUser(final @Context UriInfo uriInfo, final UserRepresentation rep) {
return new Transaction() {
@Override
protected Response callImpl() {
if (realm.getUser(rep.getUsername()) != null) {
return Response.status(Response.Status.FOUND).build();
}
rep.setCredentials(null); // don't allow credential creation
UserManager userManager = new UserManager();
UserModel userModel = userManager.createUser(realm, rep);
return Response.created(uriInfo.getAbsolutePathBuilder().path(userModel.getLoginName()).build()).build();
}
}.call();
}
@Path("users/{loginName}")
@PUT
@NoCache
@Consumes("application/json")
public void updateUser(final @PathParam("loginName") String loginName, final UserRepresentation rep) {
new Transaction() {
@Override
protected void runImpl() {
UserModel userModel = realm.getUser(loginName);
if (userModel == null) {
throw new NotFoundException();
}
UserManager userManager = new UserManager();
userManager.updateUserAsAdmin(userModel, rep);
}
}.run();
}

View file

@ -54,7 +54,7 @@ public class RealmsAdminResource {
@NoCache @NoCache
@Produces("application/json") @Produces("application/json")
public List<RealmRepresentation> getRealms() { public List<RealmRepresentation> getRealms() {
return new Transaction() { return new Transaction<List<RealmRepresentation>>() {
@Override @Override
protected List<RealmRepresentation> callImpl() { protected List<RealmRepresentation> callImpl() {
logger.info(("getRealms()")); logger.info(("getRealms()"));
@ -81,7 +81,7 @@ public class RealmsAdminResource {
@Consumes("application/json") @Consumes("application/json")
public Response importRealm(@Context final UriInfo uriInfo, final RealmRepresentation rep) { public Response importRealm(@Context final UriInfo uriInfo, final RealmRepresentation rep) {
logger.info("importRealm: " + rep.getRealm()); logger.info("importRealm: " + rep.getRealm());
return new Transaction() { return new Transaction<Response>() {
@Override @Override
protected Response callImpl() { protected Response callImpl() {
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);
@ -96,7 +96,7 @@ public class RealmsAdminResource {
@Path("{id}") @Path("{id}")
public RealmAdminResource getRealmAdmin(@Context final HttpHeaders headers, public RealmAdminResource getRealmAdmin(@Context final HttpHeaders headers,
@PathParam("id") final String id) { @PathParam("id") final String id) {
return new Transaction(false) { return new Transaction<RealmAdminResource>(false) {
@Override @Override
protected RealmAdminResource callImpl() { protected RealmAdminResource callImpl() {
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);

View file

@ -0,0 +1,52 @@
/*
* 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.services.resources.flows;
import javax.ws.rs.core.UriInfo;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.models.RealmModel;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class Flows {
private Flows() {
}
public static PageFlows pages(HttpRequest request) {
return new PageFlows(request);
}
public static FormFlows forms(RealmModel realm, HttpRequest request) {
return new FormFlows(realm, request);
}
public static OAuthFlows oauth(RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager,
TokenManager tokenManager) {
return new OAuthFlows(realm, request, uriInfo, authManager, tokenManager);
}
}

View file

@ -0,0 +1,85 @@
/*
* 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.services.resources.flows;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.services.models.RealmModel;
import org.picketlink.idm.model.sample.Realm;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FormFlows {
public static final String REALM = Realm.class.getName();
public static final String ERROR_MESSAGE = "KEYCLOAK_FORMS_ERROR_MESSAGE";
public static final String DATA = "KEYCLOAK_FORMS_DATA";
private MultivaluedMap<String, String> formData;
private String error;
private RealmModel realm;
private HttpRequest request;
FormFlows(RealmModel realm, HttpRequest request) {
this.realm = realm;
this.request = request;
}
public FormFlows setFormData(MultivaluedMap<String, String> formData) {
this.formData = formData;
return this;
}
public FormFlows setError(String error) {
this.error = error;
return this;
}
public Response forwardToLogin() {
return forwardToForm(Pages.LOGIN);
}
public Response forwardToRegistration() {
return forwardToForm(Pages.REGISTER);
}
private Response forwardToForm(String form) {
request.setAttribute(REALM, realm);
if (error != null) {
request.setAttribute(ERROR_MESSAGE, error);
}
if (formData != null) {
request.setAttribute(DATA, formData);
}
request.forward(form);
return null;
}
}

View file

@ -0,0 +1,114 @@
/*
* 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.services.resources.flows;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
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.models.RealmModel;
import org.keycloak.services.models.RoleModel;
import org.keycloak.services.models.UserModel;
import org.keycloak.services.resources.TokenService;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class OAuthFlows {
private static final Logger log = Logger.getLogger(OAuthFlows.class);
private RealmModel realm;
private HttpRequest request;
private UriInfo uriInfo;
private AuthenticationManager authManager;
private TokenManager tokenManager;
OAuthFlows(RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager,
TokenManager tokenManager) {
this.realm = realm;
this.request = request;
this.uriInfo = uriInfo;
this.authManager = authManager;
this.tokenManager = tokenManager;
}
public Response redirectAccessCode(AccessCodeEntry accessCode, String state, String redirect) {
String code = accessCode.getCode();
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("code", code);
log.info("redirectAccessCode: state: " + state);
if (state != null)
redirectUri.queryParam("state", state);
Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
if (realm.isCookieLoginAllowed()) {
location.cookie(authManager.createLoginCookie(realm, accessCode.getUser(), uriInfo));
}
return location.build();
}
public Response processAccessCode(String scopeParam, String state, String redirect, UserModel client, UserModel user) {
RoleModel resourceRole = realm.getRole(RealmManager.RESOURCE_ROLE);
RoleModel identityRequestRole = realm.getRole(RealmManager.IDENTITY_REQUESTER_ROLE);
boolean isResource = realm.hasRole(client, resourceRole);
if (!isResource && !realm.hasRole(client, identityRequestRole)) {
return forwardToSecurityFailure("Login requester not allowed to request login.");
}
AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user);
log.info("processAccessCode: isResource: " + isResource);
log.info("processAccessCode: go to oauth page?: "
+ (!isResource && (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested()
.size() > 0)));
if (!isResource
&& (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested().size() > 0)) {
return oauthGrantPage(accessCode, client);
}
return redirectAccessCode(accessCode, state, redirect);
}
public Response oauthGrantPage(AccessCodeEntry accessCode, UserModel client) {
request.setAttribute("realmRolesRequested", accessCode.getRealmRolesRequested());
request.setAttribute("resourceRolesRequested", accessCode.getResourceRolesRequested());
request.setAttribute("client", client);
request.setAttribute("action", TokenService.processOAuthUrl(uriInfo).build(realm.getId()).toString());
request.setAttribute("code", accessCode.getCode());
request.forward(Pages.OAUTH_GRANT);
return null;
}
public Response forwardToSecurityFailure(String message) {
return Flows.pages(request).forwardToSecurityFailure(message);
}
}

View file

@ -0,0 +1,52 @@
/*
* 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.services.resources.flows;
import javax.ws.rs.core.Response;
import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.services.JspRequestParameters;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class PageFlows {
private static final Logger log = Logger.getLogger(PageFlows.class);
private HttpRequest request;
PageFlows(HttpRequest request) {
this.request = request;
}
public Response forwardToSecurityFailure(String message) {
log.error(message);
request.setAttribute(JspRequestParameters.KEYCLOAK_SECURITY_FAILURE_MESSAGE, message);
request.forward(Pages.SECURITY_FAILURE);
return null;
}
}

View file

@ -0,0 +1,37 @@
/*
* 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.services.resources.flows;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class Pages {
public final static String LOGIN = "/sdk/login.xhtml";
public final static String OAUTH_GRANT = "/saas/oauthGrantForm.jsp";
public final static String REGISTER = "/sdk/register.xhtml";
public final static String SECURITY_FAILURE = "/saas/securityFailure.jsp";
}

View file

@ -0,0 +1,95 @@
/*
* 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.services.resources.flows;
import java.net.URI;
import javax.ws.rs.core.UriBuilder;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.resources.SaasService;
import org.keycloak.services.resources.SocialResource;
import org.keycloak.services.resources.TokenService;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class Urls {
private static UriBuilder realmBase(URI baseUri) {
return UriBuilder.fromUri(baseUri).path(RealmsResource.class);
}
private static UriBuilder tokenBase(URI baseUri) {
return realmBase(baseUri).path(RealmsResource.class, "getTokenService");
}
public static URI realmLoginAction(URI baseUri, String realmId) {
return tokenBase(baseUri).path(TokenService.class, "processLogin").build(realmId);
}
public static URI realmLoginPage(URI baseUri, String realmId) {
return tokenBase(baseUri).path(TokenService.class, "loginPage").build(realmId);
}
public static URI realmRegisterAction(URI baseUri, String realmId) {
return tokenBase(baseUri).path(TokenService.class, "processRegister").build(realmId);
}
public static URI realmRegisterPage(URI baseUri, String realmId) {
return tokenBase(baseUri).path(TokenService.class, "registerPage").build(realmId);
}
private static UriBuilder saasBase(URI baseUri) {
return UriBuilder.fromUri(baseUri).path(SaasService.class);
}
public static URI saasLoginAction(URI baseUri) {
return saasBase(baseUri).path(SaasService.class, "processLogin").build();
}
public static URI saasLoginPage(URI baseUri) {
return saasBase(baseUri).path(SaasService.class, "loginPage").build();
}
public static URI saasRegisterAction(URI baseUri) {
return saasBase(baseUri).path(SaasService.class, "processRegister").build();
}
public static URI saasRegisterPage(URI baseUri) {
return saasBase(baseUri).path(SaasService.class, "registerPage").build();
}
private static UriBuilder socialBase(URI baseUri) {
return UriBuilder.fromUri(baseUri).path(SocialResource.class);
}
public static URI socialCallback(URI baseUri) {
return socialBase(baseUri).path(SocialResource.class, "callback").build();
}
public static URI socialRedirectToProviderAuth(URI baseUri, String realmId) {
return socialBase(baseUri).path(SocialResource.class, "redirectToProviderAuth")
.build(realmId);
}
}

View file

@ -71,7 +71,7 @@ public class ImportTest {
List<RequiredCredentialModel> creds = realm.getRequiredCredentials(); List<RequiredCredentialModel> creds = realm.getRequiredCredentials();
Assert.assertEquals(1, creds.size()); Assert.assertEquals(1, creds.size());
RequiredCredentialModel cred = creds.get(0); RequiredCredentialModel cred = creds.get(0);
Assert.assertEquals("Password", cred.getFormLabel()); Assert.assertEquals("password", cred.getFormLabel());
UserModel user = realm.getUser("loginclient"); UserModel user = realm.getUser("loginclient");
Assert.assertNotNull(user); Assert.assertNotNull(user);

59
social/README.md Normal file
View file

@ -0,0 +1,59 @@
Keycloak social
===============
This document describes how to configure social providers for Keycloak. At the moment social providers are configured globally using system properties. These can either be passed using '-D' when starting the application server or added to the standalone.xml file, for example:
<system-properties>
<property name="keycloak.social.facebook.key" value="<facebook key>"/>
<property name="keycloak.social.facebook.secret" value="<facebook secret>"/>
<property name="keycloak.social.google.key" value="<google key>"/>
<property name="keycloak.social.google.secret" value="<google secret>"/>
<property name="keycloak.social.twitter.key" value="<twitter key>"/>
<property name="keycloak.social.twitter.secret" value="<twitter secret>"/>
</system-properties>
Social provides implementations for Facebook, Google and Twitter.
Configure Facebook
------------------
Open https://developers.facebook.com/apps. Click on Create New App
Use any app name that you'd like, click Continue
Select Disabled for Sandbox Mode
Under Select how your app integrates with Facebook select Website with Facebook login. Fill in the form with the following values:
* Site URL: http://<HOSTNAME>[<PORT>]/auth-server/rest/social/callback
Click on Save changes. Use the value of App ID as the value of the system property "keycloak.social.facebook.key", and the value of App Secret as the value of "keycloak.social.facebook.secret".
Configure Google
----------------
Open https://code.google.com/apis/console/. From the drop-down menu select Create.
Use any name that you'd like, click Create Project, select API Access and click on Create an OAuth 2.0 client ID.
Use any product name you'd like and leave the other fields empty, then click Next. On the next page select Web application as the application type. Click more options next> to Your site or hostname. Fill in the form with the following values:
* Authorized Redirect URIs: http://<HOSTNAME>[<PORT>]/auth-server/rest/social/callback
Click on Create client ID. Use the value of Client ID as the value of the system property "keycloak.social.google.key", and the value of Client secret as the value of "keycloak.social.google.secret".
Configure Twitter
-----------------
Open https://dev.twitter.com/apps. Click on Create a new application.
Fill in name, description and website. Leave Callback URL empty!
Agree to the rules, fill in the captcha and click on Create your Twitter application.
Now click on Settings and tick the box Allow this application to be used to Sign in with Twitter, and click on Update this Twitter application's settings.
Finally click on Details. Use the value of Client key as the value of the system property "keycloak.social.twitter.key", and the value of Client secret as the value of "keycloak.social.twitter.secret".

View file

@ -21,25 +21,20 @@
*/ */
package org.keycloak.social; package org.keycloak.social;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
@XmlRootElement
public interface SocialProvider { public interface SocialProvider {
String getId(); String getId();
@XmlTransient
AuthRequest getAuthUrl(SocialProviderConfig config) throws SocialProviderException; AuthRequest getAuthUrl(SocialProviderConfig config) throws SocialProviderException;
String getRequestIdParamName(); String getRequestIdParamName();
String getName(); String getName();
@XmlTransient
SocialUser processCallback(SocialProviderConfig config, AuthCallback callback) throws SocialProviderException; SocialUser processCallback(SocialProviderConfig config, AuthCallback callback) throws SocialProviderException;
} }

34
social/facebook/pom.xml Normal file
View file

@ -0,0 +1,34 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>keycloak-social-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.0-alpha-1</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>keycloak-social-facebook</artifactId>
<name>Keycloak Social Facebook</name>
<description/>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-social-core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,145 @@
package org.keycloak.social.facebook;
import java.net.URI;
import java.util.UUID;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.keycloak.social.AuthCallback;
import org.keycloak.social.AuthRequest;
import org.keycloak.social.AuthRequestBuilder;
import org.keycloak.social.SocialProvider;
import org.keycloak.social.SocialProviderConfig;
import org.keycloak.social.SocialProviderException;
import org.keycloak.social.SocialUser;
/**
* Social provider for Facebook
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class FacebookProvider implements SocialProvider {
private static final String AUTHENTICATION_ENDPOINT_URL = "https://graph.facebook.com/oauth/authorize";
private static final String ACCESS_TOKEN_ENDPOINT_URL = "https://graph.facebook.com/oauth/access_token";
private static final String PROFILE_ENDPOINT_URL = "https://graph.facebook.com/me";
private static final String DEFAULT_RESPONSE_TYPE = "code";
private static final String DEFAULT_SCOPE = "email";
@Override
public String getId() {
return "facebook";
}
@Override
public AuthRequest getAuthUrl(SocialProviderConfig config) throws SocialProviderException {
String state = UUID.randomUUID().toString();
AuthRequestBuilder b = AuthRequestBuilder.create(state, AUTHENTICATION_ENDPOINT_URL).setQueryParam("client_id", config.getKey())
.setQueryParam("response_type", DEFAULT_RESPONSE_TYPE).setQueryParam("scope", DEFAULT_SCOPE)
.setQueryParam("redirect_uri", config.getCallbackUrl()).setQueryParam("state", state);
b.setAttribute("state", state);
return b.build();
}
@Override
public String getRequestIdParamName() {
return "state";
}
@Override
public String getName() {
return "Facebook";
}
@Override
public SocialUser processCallback(SocialProviderConfig config, AuthCallback callback) throws SocialProviderException {
String code = callback.getQueryParam(DEFAULT_RESPONSE_TYPE);
try {
if (!callback.getQueryParam("state").equals(callback.getAttribute("state"))) {
throw new SocialProviderException("Invalid state");
}
ResteasyClient client = new ResteasyClientBuilder()
.hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.ANY).build();
String accessToken = loadAccessToken(code, config, client);
FacebookUser facebookUser = loadUser(accessToken, client);
SocialUser socialUser = new SocialUser(facebookUser.getId());
socialUser.setEmail(facebookUser.getEmail());
socialUser.setLastName(facebookUser.getLastName());
socialUser.setFirstName(facebookUser.getFirstName());
return socialUser;
} catch (SocialProviderException spe) {
throw spe;
} catch (Exception e) {
throw new SocialProviderException(e);
}
}
protected String loadAccessToken(String code, SocialProviderConfig config, ResteasyClient client) throws SocialProviderException {
Form form = new Form();
form.param("grant_type", "authorization_code")
.param("code", code)
.param("client_id", config.getKey())
.param("client_secret", config.getSecret())
.param("redirect_uri", config.getCallbackUrl());
Response response = client.target(ACCESS_TOKEN_ENDPOINT_URL).request().post(Entity.form(form));
if (response.getStatus() != 200) {
String errorTokenResponse = response.readEntity(String.class);
throw new SocialProviderException("Access token request to Facebook failed. Status: " + response.getStatus() + ", response: " + errorTokenResponse);
}
String accessTokenResponse = response.readEntity(String.class);
return parseParameter(accessTokenResponse, "access_token");
}
protected FacebookUser loadUser(String accessToken, ResteasyClient client) throws SocialProviderException {
URI userDetailsUri = UriBuilder.fromUri(PROFILE_ENDPOINT_URL)
.queryParam("access_token", accessToken)
.queryParam("fields", "id,name,username,first_name,last_name,email")
.build();
Response response = client.target(userDetailsUri).request()
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
.get();
if (response.getStatus() != 200) {
String errorTokenResponse = response.readEntity(String.class);
throw new SocialProviderException("Request to Facebook for obtaining user failed. Status: " + response.getStatus() + ", response: " + errorTokenResponse);
}
return response.readEntity(FacebookUser.class);
}
// Parses value of given parameter from input string like "my_param=abcd&another_param=xyz"
private String parseParameter(String input, String paramName) {
int start = input.indexOf(paramName + "=");
if (start != -1) {
input = input.substring(start + paramName.length() + 1);
int end = input.indexOf("&");
return end==-1 ? input : input.substring(0, end);
} else {
throw new IllegalArgumentException("Parameter " + paramName + " not available in response " + input);
}
}
}

View file

@ -0,0 +1,77 @@
package org.keycloak.social.facebook;
import org.codehaus.jackson.annotate.JsonProperty;
/**
* Wrap info about user from Facebook
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class FacebookUser {
@JsonProperty("id")
private String id;
@JsonProperty("first_name")
private String firstName;
@JsonProperty("last_name")
private String lastName;
@JsonProperty("username")
private String username;
@JsonProperty("name")
private String name;
@JsonProperty("email")
private String email;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}

View file

@ -0,0 +1 @@
org.keycloak.social.facebook.FacebookProvider

View file

@ -17,6 +17,7 @@
<module>core</module> <module>core</module>
<module>google</module> <module>google</module>
<module>twitter</module> <module>twitter</module>
<module>facebook</module>
</modules> </modules>
</project> </project>

View file

@ -49,7 +49,7 @@ public class TwitterProvider implements SocialProvider {
Twitter twitter = new TwitterFactory().getInstance(); Twitter twitter = new TwitterFactory().getInstance();
twitter.setOAuthConsumer(request.getKey(), request.getSecret()); twitter.setOAuthConsumer(request.getKey(), request.getSecret());
RequestToken requestToken = twitter.getOAuthRequestToken(); RequestToken requestToken = twitter.getOAuthRequestToken(request.getCallbackUrl());
return AuthRequestBuilder.create(requestToken.getToken(), requestToken.getAuthenticationURL()) return AuthRequestBuilder.create(requestToken.getToken(), requestToken.getAuthenticationURL())
.setAttribute("token", requestToken.getToken()).setAttribute("tokenSecret", requestToken.getTokenSecret()) .setAttribute("token", requestToken.getToken()).setAttribute("tokenSecret", requestToken.getTokenSecret())