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>
<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>
</dependency>
<dependency>

View file

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

View file

@ -12,8 +12,8 @@
<!-- <select class="nav pull-left" ng-options="r.name for r in current.realms"></select> -->
</div>
<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/saas/saas-register.jsp">Register</a></li>
<li><a href="/auth-server/rest/saas/login">Login</a></li>
<li><a href="/auth-server/rest/saas/registrations">Register</a></li>
</ul>
<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

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>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-sdk-html</artifactId>
<name>Keycloak HTML SDK</name>
<artifactId>keycloak-forms</artifactId>
<name>Keycloak Forms</name>
<description />
<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.util.HashMap;
@ -8,36 +29,42 @@ import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.faces.application.FacesMessage;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.faces.context.FacesContext;
import javax.imageio.spi.ServiceRegistry;
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.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
public class LoginBean {
public class FormsBean {
private RealmModel realm;
private String name;
private String loginUrl;
private String loginAction;
private String socialLoginUrl;
private UriBuilder socialLoginUrlBuilder;
private String registrationUrl;
private String username;
private String registrationAction;
private List<RequiredCredential> requiredCredentials;
private List<Property> hiddenProperties;
private List<SocialProvider> providers;
private String theme;
@ -48,28 +75,53 @@ public class LoginBean {
private String error;
private String errorDetails;
private String view;
private Map<String, String> formData;
@PostConstruct
public void init() {
FacesContext ctx = FacesContext.getCurrentInstance();
HttpServletRequest request = (HttpServletRequest) ctx.getExternalContext().getRequest();
realm = (RealmModel) request.getAttribute(RealmModel.class.getName());
if (RealmModel.DEFAULT_REALM.equals(realm.getName())) {
realm = (RealmModel) request.getAttribute(FormFlows.REALM);
boolean saas = RealmModel.DEFAULT_REALM.equals(realm.getName());
if (saas) {
name = "Keycloak";
} else {
name = realm.getName();
}
loginAction = ((URI) request.getAttribute("KEYCLOAK_LOGIN_ACTION")).toString();
registrationUrl = ((URI) request.getAttribute("KEYCLOAK_REGISTRATION_PAGE")).toString();
socialLoginUrl = ((URI) request.getAttribute("KEYCLOAK_SOCIAL_LOGIN")).toString();
view = ctx.getViewRoot().getViewId();
view = view.substring(view.lastIndexOf('/') + 1, view.lastIndexOf('.'));
UriBuilder b = UriBuilder.fromUri(request.getRequestURI()).replaceQuery(request.getQueryString())
.replacePath(request.getContextPath()).path("rest");
URI baseURI = b.build();
username = (String) request.getAttribute("username");
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();
addHiddenProperties(request, "client_id", "scope", "state", "redirect_uri");
addFormData(request);
addSocialProviders();
addErrors(request);
@ -98,6 +150,10 @@ public class LoginBean {
return name;
}
public String getLoginUrl() {
return loginUrl;
}
public String getLoginAction() {
return loginAction;
}
@ -106,14 +162,22 @@ public class LoginBean {
return error;
}
public List<Property> getHiddenProperties() {
return hiddenProperties;
public String getErrorDetails() {
return errorDetails;
}
public Map<String, String> getFormData() {
return formData;
}
public List<RequiredCredential> getRequiredCredentials() {
return requiredCredentials;
}
public String getView() {
return view;
}
public String getTheme() {
return theme;
}
@ -126,8 +190,8 @@ public class LoginBean {
return registrationUrl;
}
public String getUsername() {
return username;
public String getRegistrationAction() {
return registrationAction;
}
public boolean isSocial() {
@ -139,12 +203,14 @@ public class LoginBean {
return realm.isRegistrationAllowed();
}
private void addHiddenProperties(HttpServletRequest request, String... names) {
hiddenProperties = new LinkedList<Property>();
for (String name : names) {
Object v = request.getAttribute(name);
if (v != null) {
hiddenProperties.add(new Property(name, (String) v));
private void addFormData(HttpServletRequest request) {
formData = new HashMap<String, String>();
@SuppressWarnings("unchecked")
MultivaluedMap<String, String> t = (MultivaluedMap<String, String>) request.getAttribute(FormFlows.DATA);
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) {
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 {
@ -236,13 +312,7 @@ public class LoginBean {
}
public String getLoginUrl() {
StringBuilder sb = new StringBuilder();
sb.append(socialLoginUrl);
sb.append("?provider_id=" + id);
for (Property p : hiddenProperties) {
sb.append("&" + p.getName() + "=" + p.getValue());
}
return sb.toString();
return socialLoginUrlBuilder.replaceQueryParam("provider_id", id).build().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"
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>

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">
<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">
<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

@ -3,4 +3,11 @@
@IMPORT url("css/forms.css");
@IMPORT url("css/zocial/zocial.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>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Log in to #{login.name}</title>
<link href="#{login.themeConfig['styles']}" rel="stylesheet" />
<title>#{messages.logInTo} #{forms.name}</title>
<link href="#{forms.themeConfig['styles']}" rel="stylesheet" />
<style>
body {
background-image: url("#{login.themeConfig['background']}");
background-image: url("#{forms.themeConfig['background']}");
}
</style>
</h:head>
<h:body class="rcue-login-register #{bodyClass}">
<h:panelGroup rendered="#{not empty login.themeConfig['logo']}">
<h1><a href="#" title="Go to the home page"><img src="#{login.themeConfig['logo']}" alt="Logo" /></a></h1>
<h:panelGroup rendered="#{not empty forms.themeConfig['logo']}">
<h1><a href="#" title="Go to the home page"><img src="#{forms.themeConfig['logo']}" alt="Logo" /></a></h1>
</h:panelGroup>
<div class="content">
<h2><ui:insert name="header" /></h2>
<div class="background-area">
<div class="form-area #{login.social ? 'social' : ''} clearfix">
<div class="form-area #{forms.social ? 'social' : ''} clearfix">
<section class="app-form">
<h3>Application login area</h3>
<h:panelGroup rendered="#{not empty login.error}">
<div class="feedback error bottom-left show"><p><strong>#{login.error}</strong></p></div>
<h:panelGroup rendered="#{not empty forms.error}">
<div class="feedback error bottom-left show">
<p>
<strong>#{forms.error}</strong><br/>
#{forms.errorDetails}
</p>
</div>
</h:panelGroup>
<ui:insert name="form" />
</section>
<h:panelGroup rendered="#{login.social}">
<h:panelGroup rendered="#{forms.social}">
<section class="social-login">
<span>or</span>
<h3>Social login area</h3>
<p>Log In with</p>
<p>#{messages.logInWith}</p>
<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>
</ui:repeat>
</ul>
@ -48,17 +53,17 @@
<section class="info-area">
<h3>Info area</h3>
<ui:insert name="info" />
<ul>
<li><strong>Domain:</strong> 10.0.0.1</li>
<li><strong>Zone:</strong> Live</li>
<li><strong>Appliance:</strong> Yep</li>
</ul>
<!-- <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>
<h:panelGroup rendered="#{login.themeConfig['displayPoweredBy']}">
<p class="powered"><a href="#">Powered by Keycloak</a></p>
<h:panelGroup rendered="#{forms.themeConfig['displayPoweredBy']}">
<p class="powered"><a href="#">#{messages.poweredByKeycloak}</a></p>
</h:panelGroup>
</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>examples</module>
<module>social</module>
<module>sdk-html</module>
<module>forms</module>
<!--<module>ui</module> -->
</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.setSslNotRequired(rep.isSslNotRequired());
newRealm.setCookieLoginAllowed(rep.isCookieLoginAllowed());
newRealm.setRegistrationAllowed(rep.isRegistrationAllowed());
if (rep.getPrivateKey() == null || rep.getPublicKey() == null) {
generateRealmKeys(newRealm);
} 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.setInput(true);
PASSWORD.setSecret(true);
PASSWORD.setFormLabel("Password");
PASSWORD.setFormLabel("password");
map.put(PASSWORD.getType(), PASSWORD);
TOTP = new RequiredCredentialModel();
TOTP.setType(CredentialRepresentation.TOTP);
TOTP.setInput(true);
TOTP.setSecret(false);
TOTP.setFormLabel("Authenticator Code");
TOTP.setFormLabel("authenticatorCode");
map.put(TOTP.getType(), TOTP);
CLIENT_CERT = new RequiredCredentialModel();
CLIENT_CERT.setType(CredentialRepresentation.CLIENT_CERT);
CLIENT_CERT.setInput(false);
CLIENT_CERT.setSecret(false);
CLIENT_CERT.setFormLabel("Client Certificate");
CLIENT_CERT.setFormLabel("clientCertificate");
map.put(CLIENT_CERT.getType(), CLIENT_CERT);
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();
this.factory = f;
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(SaasService.class);
}

View file

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

View file

@ -43,11 +43,8 @@ public class RealmsResource {
protected TokenManager tokenManager;
protected SocialRequestManager socialRequestManager;
public RealmsResource(TokenManager tokenManager, SocialRequestManager socialRequestManager) {
public RealmsResource(TokenManager tokenManager) {
this.tokenManager = tokenManager;
this.socialRequestManager = socialRequestManager;
}
public static UriBuilder realmBaseUrl(UriInfo uriInfo) {
@ -56,7 +53,7 @@ public class RealmsResource {
@Path("{realm}/tokens")
public TokenService getTokenService(final @PathParam("realm") String id) {
return new Transaction(false) {
return new Transaction<TokenService>(false) {
@Override
protected TokenService callImpl() {
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}")
public PublicRealmResource getRealmResource(final @PathParam("realm") String id) {
return new Transaction(false) {
return new Transaction<PublicRealmResource>(false) {
@Override
protected PublicRealmResource callImpl() {
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.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.RoleModel;
import org.keycloak.services.models.UserCredentialModel;
import org.keycloak.services.models.UserModel;
import org.keycloak.services.resources.admin.RealmsAdminResource;
import org.keycloak.services.resources.flows.Flows;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.net.URI;
import java.util.StringTokenizer;
@ -39,8 +42,6 @@ public class SaasService {
@Context
HttpResponse response;
protected String saasLoginPath = "/saas/saas-login.jsp";
protected String saasRegisterPath = "/saas/saas-register.jsp";
protected String adminPath = "/saas/admin/index.html";
protected AuthenticationManager authManager = new AuthenticationManager();
@ -78,12 +79,13 @@ public class SaasService {
@NoCache
public Response keepalive(final @Context HttpHeaders headers) {
logger.info("keepalive");
return new Transaction() {
return new Transaction<Response>() {
@Override
public Response callImpl() {
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.defaultRealm();
if (realm == null) throw new NotFoundException();
if (realm == null)
throw new NotFoundException();
UserModel user = authManager.authenticateSaasIdentityCookie(realm, uriInfo, headers);
if (user == null) {
return Response.status(401).build();
@ -99,12 +101,13 @@ public class SaasService {
@Produces("application/json")
@NoCache
public Response whoAmI(final @Context HttpHeaders headers) {
return new Transaction() {
return new Transaction<Response>() {
@Override
public Response callImpl() {
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.defaultRealm();
if (realm == null) throw new NotFoundException();
if (realm == null)
throw new NotFoundException();
UserModel user = authManager.authenticateSaasIdentityCookie(realm, uriInfo, headers);
if (user == null) {
return Response.status(401).build();
@ -119,7 +122,7 @@ public class SaasService {
@Produces("application/javascript")
@NoCache
public String isLoggedIn(final @Context HttpHeaders headers) {
return new Transaction() {
return new Transaction<String>() {
@Override
public String callImpl() {
logger.info("WHOAMI Javascript start.");
@ -139,7 +142,6 @@ public class SaasService {
}.call();
}
public static UriBuilder contextRoot(UriInfo uriInfo) {
return UriBuilder.fromUri(uriInfo.getBaseUri()).replacePath("/auth-server");
}
@ -150,12 +152,13 @@ public class SaasService {
@Path("admin/realms")
public RealmsAdminResource getRealmsAdmin(@Context final HttpHeaders headers) {
return new Transaction(false) {
return new Transaction<RealmsAdminResource>(false) {
@Override
protected RealmsAdminResource callImpl() {
RealmManager realmManager = new RealmManager(session);
RealmModel saasRealm = realmManager.defaultRealm();
if (saasRealm == null) throw new NotFoundException();
if (saasRealm == null)
throw new NotFoundException();
UserModel admin = authManager.authenticateSaasIdentity(saasRealm, uriInfo, headers);
if (admin == null) {
throw new NotAuthorizedException("Bearer");
@ -170,7 +173,7 @@ public class SaasService {
}.call();
}
@Path("loginPage.html")
@Path("login")
@GET
@NoCache
public void loginPage() {
@ -180,7 +183,24 @@ public class SaasService {
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.defaultRealm();
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();
}
@ -195,12 +215,12 @@ public class SaasService {
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.defaultRealm();
authManager.expireSaasIdentityCookie(uriInfo);
forwardToLoginForm(realm);
Flows.forms(realm, request).forwardToLogin();
}
}.run();
}
@Path("logout-cookie")
@GET
@NoCache
@ -214,30 +234,18 @@ public class SaasService {
}.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")
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processLogin(final MultivaluedMap<String, String> formData) {
logger.info("processLogin start");
return new Transaction() {
return new Transaction<Response>() {
@Override
protected Response callImpl() {
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.defaultRealm();
if (realm == null) throw new NotFoundException();
if (realm == null)
throw new NotFoundException();
if (!realm.isEnabled()) {
throw new NotImplementedYetException();
@ -246,29 +254,27 @@ public class SaasService {
UserModel user = realm.getUser(username);
if (user == null) {
logger.info("Not Authenticated! Incorrect user name");
request.setAttribute("KEYCLOAK_LOGIN_ERROR_MESSAGE", "Incorrect user name.");
forwardToLoginForm(realm);
return null;
return Flows.forms(realm, request).setError(Messages.INVALID_USER).setFormData(formData)
.forwardToLogin();
}
if (!user.isEnabled()) {
logger.info("NAccount is disabled, contact admin.");
request.setAttribute("KEYCLOAK_LOGIN_ERROR_MESSAGE", "Account is disabled, contact admin.");
forwardToLoginForm(realm);
return null;
logger.info("Account is disabled, contact admin.");
return Flows.forms(realm, request).setError(Messages.ACCOUNT_DISABLED)
.setFormData(formData).forwardToLogin();
}
boolean authenticated = authManager.authenticateForm(realm, user, formData);
if (!authenticated) {
logger.info("Not Authenticated! Invalid credentials");
request.setAttribute("KEYCLOAK_LOGIN_ERROR_MESSAGE", "Invalid credentials.");
forwardToLoginForm(realm);
return null;
return Flows.forms(realm, request).setError(Messages.INVALID_PASSWORD).setFormData(formData)
.forwardToLogin();
}
NewCookie cookie = authManager.createSaasIdentityCookie(realm, user, uriInfo);
return Response.status(302)
.cookie(cookie)
.location(contextRoot(uriInfo).path(adminPath).build()).build();
return Response.status(302).cookie(cookie).location(contextRoot(uriInfo).path(adminPath).build()).build();
}
}.call();
}
@ -277,7 +283,7 @@ public class SaasService {
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response register(final UserRepresentation newUser) {
return new Transaction() {
return new Transaction<Response>() {
@Override
protected Response callImpl() {
RealmManager realmManager = new RealmManager(session);
@ -295,24 +301,24 @@ public class SaasService {
@Path("registrations")
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processRegister(final @FormParam("name") String fullname,
final @FormParam("email") String email,
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() {
public Response processRegister(final MultivaluedMap<String, String> formData) {
return new Transaction<Response>() {
@Override
protected Response callImpl() {
RealmManager realmManager = new RealmManager(session);
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();
newUser.setUsername(username);
newUser.setEmail(email);
newUser.setUsername(formData.getFirst("username"));
newUser.setEmail(formData.getFirst("email"));
String fullname = formData.getFirst("name");
if (fullname != null) {
StringTokenizer tokenizer = new StringTokenizer(fullname, " ");
StringBuffer first = null;
@ -330,16 +336,16 @@ public class SaasService {
last = token;
}
}
if (first == null) first = new StringBuffer();
if (first == null)
first = new StringBuffer();
newUser.setFirstName(first.toString());
newUser.setLastName(last);
}
newUser.credential(CredentialRepresentation.PASSWORD, password);
newUser.credential(CredentialRepresentation.PASSWORD, formData.getFirst("password"));
UserModel user = registerMe(defaultRealm, newUser);
if (user == null) {
request.setAttribute("KEYCLOAK_LOGIN_ERROR_MESSAGE", "Username already exists.");
request.forward(saasRegisterPath);
return null;
return Flows.forms(defaultRealm, request).setError(Messages.USERNAME_EXISTS)
.setFormData(formData).forwardToRegistration();
}
NewCookie cookie = authManager.createSaasIdentityCookie(defaultRealm, user, uriInfo);
@ -348,7 +354,6 @@ public class SaasService {
}.call();
}
protected UserModel registerMe(RealmModel defaultRealm, UserRepresentation newUser) {
if (!defaultRealm.isEnabled()) {
throw new ForbiddenException();
@ -376,5 +381,32 @@ public class SaasService {
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.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@ -32,20 +31,25 @@ import java.util.Map.Entry;
import javax.imageio.spi.ServiceRegistry;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
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.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.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.AuthRequest;
import org.keycloak.social.RequestDetails;
@ -59,52 +63,69 @@ import org.keycloak.social.SocialUser;
/**
* @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
private HttpHeaders headers;
protected UriInfo uriInfo;
@Context
private UriInfo uriInfo;
protected HttpHeaders headers;
public static UriBuilder socialServiceBaseUrl(UriInfo uriInfo) {
UriBuilder base = uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getSocialService");
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");
}
@Context
private HttpRequest request;
private SocialRequestManager socialRequestManager;
public SocialService(RealmModel realm, TokenManager tokenManager, SocialRequestManager socialRequestManager) {
super(realm, tokenManager);
private TokenManager tokenManager;
private AuthenticationManager authManager = new AuthenticationManager();
public SocialResource(TokenManager tokenManager, SocialRequestManager socialRequestManager) {
this.tokenManager = tokenManager;
this.socialRequestManager = socialRequestManager;
}
@GET
@Path("callback")
public Response callback() throws URISyntaxException {
return new Transaction() {
return new Transaction<Response>() {
protected Response callImpl() {
Map<String, String[]> queryParams = getQueryParams();
RequestDetails requestData = getRequestDetails(queryParams);
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 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);
AuthCallback callback = new AuthCallback(requestData.getSocialAttributes(), queryParams);
@ -114,25 +135,7 @@ public class SocialService extends AbstractLoginService {
socialUser = provider.processCallback(config, callback);
} catch (SocialProviderException e) {
logger.warn("Failed to process social callback", e);
securityFailureForward("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;
return oauth.forwardToSecurityFailure("Failed to process social callback");
}
// 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());
// 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()) {
securityFailureForward("Your account is not enabled.");
return null;
return oauth.forwardToSecurityFailure("Your account is not enabled.");
}
String scope = requestData.getClientAttributes().get("scope");
String state = requestData.getClientAttributes().get("state");
String redirectUri = requestData.getClientAttributes().get("redirectUri");
return processAccessCode(scope, state, redirectUri, client, user);
return oauth.processAccessCode(scope, state, redirectUri, client, user);
}
}.call();
}
@GET
@Path("providers")
@Produces(MediaType.APPLICATION_JSON)
public List<SocialProvider> getProviders() {
List<SocialProvider> providers = new LinkedList<SocialProvider>();
Iterator<SocialProvider> itr = ServiceRegistry.lookupProviders(SocialProvider.class);
while (itr.hasNext()) {
providers.add(itr.next());
@Path("{realm}/login")
public Response redirectToProviderAuth(@PathParam("realm") final String realmId,
@QueryParam("provider_id") final String providerId, @QueryParam("client_id") final String clientId,
@QueryParam("scope") final String scope, @QueryParam("state") final String state,
@QueryParam("redirect_uri") final String redirectUri) {
SocialProvider provider = getProvider(providerId);
if (provider == null) {
return Flows.pages(request).forwardToSecurityFailure("Social provider not found");
}
return providers;
}
@GET
@Path("login")
public Response redirectToProviderAuth(@QueryParam("provider_id") final String providerId,
@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");
String secret = System.getProperty("keycloak.social." + providerId + ".secret");
String callbackUri = Urls.socialCallback(uriInfo.getBaseUri()).toString();
String key = System.getProperty("keycloak.social." + providerId + ".key");
String secret = System.getProperty("keycloak.social." + providerId + ".secret");
String callbackUri = callbackUrl(uriInfo).build(realm.getId()).toString();
SocialProviderConfig config = new SocialProviderConfig(key, secret, callbackUri);
SocialProviderConfig config = new SocialProviderConfig(key, secret, callbackUri);
try {
AuthRequest authRequest = provider.getAuthUrl(config);
try {
AuthRequest authRequest = provider.getAuthUrl(config);
RequestDetails socialRequest = RequestDetailsBuilder.create(providerId)
.putSocialAttributes(authRequest.getAttributes()).putClientAttribute("realmId", realmId)
.putClientAttribute("clientId", clientId).putClientAttribute("scope", scope)
.putClientAttribute("state", state).putClientAttribute("redirectUri", redirectUri).build();
RequestDetails socialRequest = RequestDetailsBuilder.create(providerId)
.putSocialAttributes(authRequest.getAttributes()).putClientAttribute("clientId", clientId)
.putClientAttribute("scope", scope).putClientAttribute("state", state)
.putClientAttribute("redirectUri", redirectUri).build();
socialRequestManager.addRequest(authRequest.getId(), socialRequest);
socialRequestManager.addRequest(authRequest.getId(), socialRequest);
return Response.status(Status.FOUND).location(authRequest.getAuthUri()).build();
} catch (Throwable t) {
logger.error("Failed to redirect to social auth", t);
securityFailureForward("Failed to redirect to social auth");
return null;
}
}
}.call();
return Response.status(Status.FOUND).location(authRequest.getAuthUri()).build();
} catch (Throwable t) {
return Flows.pages(request).forwardToSecurityFailure("Failed to redirect to social auth");
}
}
private RequestDetails getRequestDetails(Map<String, String[]> queryParams) {
@ -251,9 +237,4 @@ public class SocialService extends AbstractLoginService {
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.jwt.JsonSerialization;
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.SkeletonKeyToken;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.RoleModel;
import org.keycloak.services.models.UserCredentialModel;
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.ForbiddenException;
import javax.ws.rs.GET;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.POST;
@ -25,40 +34,55 @@ import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class TokenService extends AbstractLoginService {
public class TokenService {
protected static final Logger logger = Logger.getLogger(TokenService.class);
protected RealmModel realm;
protected TokenManager tokenManager;
protected AuthenticationManager authManager = new AuthenticationManager();
@Context
protected Providers providers;
@Context
protected SecurityContext securityContext;
@Context
protected UriInfo uriInfo;
@Context
protected HttpHeaders headers;
@Context
HttpRequest request;
@Context
HttpResponse response;
private ResourceAdminManager resourceAdminManager = new ResourceAdminManager();
public TokenService(RealmModel realm, TokenManager tokenManager) {
super(realm, tokenManager);
this.realm = realm;
this.tokenManager = tokenManager;
}
public static UriBuilder tokenServiceBaseUrl(UriInfo uriInfo) {
UriBuilder base = uriInfo.getBaseUriBuilder()
.path(RealmsResource.class).path(RealmsResource.class, "getTokenService");
UriBuilder base = uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getTokenService");
return base;
}
@ -89,13 +113,12 @@ public class TokenService extends AbstractLoginService {
return tokenServiceBaseUrl(uriInfo).path(TokenService.class, "processOAuth");
}
@Path("grants/identity-token")
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
public Response grantIdentityToken(final MultivaluedMap<String, String> form) {
return new Transaction() {
return new Transaction<Response>() {
protected Response callImpl() {
String username = form.getFirst(AuthenticationManager.FORM_USERNAME);
if (username == null) {
@ -128,7 +151,7 @@ public class TokenService extends AbstractLoginService {
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
public Response grantAccessToken(final MultivaluedMap<String, String> form) {
return new Transaction() {
return new Transaction<Response>() {
protected Response callImpl() {
String username = form.getFirst(AuthenticationManager.FORM_USERNAME);
if (username == null) {
@ -159,49 +182,126 @@ public class TokenService extends AbstractLoginService {
@Path("auth/request/login")
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processLogin(final MultivaluedMap<String, String> formData) {
return new Transaction() {
public Response processLogin(@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>() {
protected Response callImpl() {
String clientId = formData.getFirst("client_id");
String scopeParam = formData.getFirst("scope");
String state = formData.getFirst("state");
String redirect = formData.getFirst("redirect_uri");
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
if (!realm.isEnabled()) {
securityFailureForward("Realm not enabled.");
return null;
return oauth.forwardToSecurityFailure("Realm not enabled.");
}
UserModel client = realm.getUser(clientId);
if (client == null) {
securityFailureForward("Unknown login requester.");
return null;
return oauth.forwardToSecurityFailure("Unknown login requester.");
}
if (!client.isEnabled()) {
securityFailureForward("Login requester not enabled.");
return null;
return oauth.forwardToSecurityFailure("Login requester not enabled.");
}
String username = formData.getFirst("username");
UserModel user = realm.getUser(username);
if (user == null) {
logger.error("Incorrect user name.");
request.setAttribute("KEYCLOAK_LOGIN_ERROR_MESSAGE", "Incorrect user name.");
forwardToLoginForm(redirect, clientId, scopeParam, state);
return null;
return Flows.forms(realm, request).setError(Messages.INVALID_USER).setFormData(formData)
.forwardToLogin();
}
if (!user.isEnabled()) {
securityFailureForward("Your account is not enabled.");
return null;
return oauth.forwardToSecurityFailure("Your account is not enabled.");
}
boolean authenticated = authManager.authenticateForm(realm, user, formData);
if (!authenticated) {
logger.error("Authentication failed");
request.setAttribute("username", username);
request.setAttribute("KEYCLOAK_LOGIN_ERROR_MESSAGE", "Invalid credentials.");
forwardToLoginForm(redirect, clientId, scopeParam, state);
return null;
return Flows.forms(realm, request).setError(Messages.INVALID_PASSWORD).setFormData(formData)
.forwardToLogin();
}
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();
}
@ -210,7 +310,7 @@ public class TokenService extends AbstractLoginService {
@POST
@Produces("application/json")
public Response accessCodeToToken(final MultivaluedMap<String, String> formData) {
return new Transaction() {
return new Transaction<Response>() {
protected Response callImpl() {
logger.info("accessRequest <---");
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();
}
JWSInput input = new JWSInput(code, providers);
boolean verifiedCode = false;
try {
@ -270,7 +369,8 @@ public class TokenService extends AbstractLoginService {
Map<String, String> res = new HashMap<String, String>();
res.put("error", "invalid_grant");
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);
AccessCodeEntry accessCode = tokenManager.pullAccessCode(key);
@ -278,25 +378,29 @@ public class TokenService extends AbstractLoginService {
Map<String, String> res = new HashMap<String, String>();
res.put("error", "invalid_grant");
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()) {
Map<String, String> res = new HashMap<String, String>();
res.put("error", "invalid_grant");
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()) {
Map<String, String> res = new HashMap<String, String>();
res.put("error", "invalid_grant");
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())) {
Map<String, String> res = new HashMap<String, String>();
res.put("error", "invalid_grant");
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");
AccessTokenResponse res = accessTokenResponse(realm.getPrivateKey(), accessCode.getToken());
@ -313,9 +417,7 @@ public class TokenService extends AbstractLoginService {
} catch (Exception e) {
throw new RuntimeException(e);
}
String encodedToken = new JWSBuilder()
.content(tokenBytes)
.rsa256(privateKey);
String encodedToken = new JWSBuilder().content(tokenBytes).rsa256(privateKey);
return accessTokenResponse(token, encodedToken);
}
@ -334,25 +436,25 @@ public class TokenService extends AbstractLoginService {
@Path("login")
@GET
public Response loginPage(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() {
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()) {
securityFailureForward("Realm not enabled");
oauth.forwardToSecurityFailure("Realm not enabled");
return null;
}
UserModel client = realm.getUser(clientId);
if (client == null) {
securityFailureForward("Unknown login requester.");
oauth.forwardToSecurityFailure("Unknown login requester.");
transaction.rollback();
return null;
}
if (!client.isEnabled()) {
securityFailureForward("Login requester not enabled.");
oauth.forwardToSecurityFailure("Login requester not enabled.");
transaction.rollback();
session.close();
return null;
@ -362,7 +464,7 @@ public class TokenService extends AbstractLoginService {
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.");
oauth.forwardToSecurityFailure("Login requester not allowed to request login.");
transaction.rollback();
session.close();
return null;
@ -371,11 +473,42 @@ public class TokenService extends AbstractLoginService {
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
if (user != null) {
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 null;
return Flows.forms(realm, request).forwardToLogin();
}
}.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();
}
@ -384,7 +517,7 @@ public class TokenService extends AbstractLoginService {
@GET
@NoCache
public Response logout(final @QueryParam("redirect_uri") String redirectUri) {
return new Transaction() {
return new Transaction<Response>() {
protected Response callImpl() {
// todo do we care if anybody can trigger this?
@ -404,8 +537,10 @@ public class TokenService extends AbstractLoginService {
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processOAuth(final MultivaluedMap<String, String> formData) {
return new Transaction() {
return new Transaction<Response>() {
protected Response callImpl() {
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
String code = formData.getFirst("code");
JWSInput input = new JWSInput(code, providers);
boolean verifiedCode = false;
@ -415,16 +550,12 @@ public class TokenService extends AbstractLoginService {
logger.debug("Failed to verify signature", ignored);
}
if (!verifiedCode) {
securityFailureForward("Illegal access code.");
session.close();
return null;
return oauth.forwardToSecurityFailure("Illegal access code.");
}
String key = input.readContent(String.class);
AccessCodeEntry accessCodeEntry = tokenManager.getAccessCode(key);
if (accessCodeEntry == null) {
securityFailureForward("Unknown access code.");
session.close();
return null;
return oauth.forwardToSecurityFailure("Unknown access code.");
}
String redirect = accessCodeEntry.getRedirectUri();
@ -434,21 +565,45 @@ public class TokenService extends AbstractLoginService {
return redirectAccessDenied(redirect, state);
}
return redirectAccessCode(accessCodeEntry, state, redirect);
return oauth.redirectAccessCode(accessCodeEntry, state, redirect);
}
}.call();
}
protected Response redirectAccessDenied(String redirect, String state) {
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());
return location.build();
}
@Override
protected Logger getLogger() {
return logger;
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

@ -13,7 +13,7 @@ import javax.ws.rs.core.Application;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class Transaction {
public class Transaction<T> {
protected KeycloakSession session;
protected KeycloakTransaction transaction;
protected boolean closeSession;
@ -83,7 +83,7 @@ public class Transaction {
}
}
protected <T> T callImpl() {
protected T callImpl() {
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
*
*/
public <T> T call() {
public T call() {
boolean wasActive = transaction.isActive();
if (!wasActive) transaction.begin();
try {

View file

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

View file

@ -42,7 +42,7 @@ public class ApplicationsResource {
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public List<ApplicationRepresentation> getResources() {
return new Transaction() {
return new Transaction<List<ApplicationRepresentation>>() {
@Override
protected List<ApplicationRepresentation> callImpl() {
List<ApplicationRepresentation> rep = new ArrayList<ApplicationRepresentation>();
@ -59,7 +59,7 @@ public class ApplicationsResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response createResource(final @Context UriInfo uriInfo, final ApplicationRepresentation rep) {
return new Transaction() {
return new Transaction<Response>() {
@Override
protected Response callImpl() {
ResourceManager resourceManager = new ResourceManager(new RealmManager(session));
@ -71,7 +71,7 @@ public class ApplicationsResource {
@Path("{id}")
public ApplicationResource getResource(final @PathParam("id") String id) {
return new Transaction(false) {
return new Transaction<ApplicationResource>(false) {
@Override
protected ApplicationResource callImpl() {
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.UserRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.UserManager;
import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.RoleModel;
import org.keycloak.services.models.UserModel;
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.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -45,7 +49,7 @@ public class RealmAdminResource {
@NoCache
@Produces("application/json")
public RealmRepresentation getRealm() {
return new Transaction() {
return new Transaction<RealmRepresentation>() {
@Override
protected RealmRepresentation callImpl() {
return new RealmManager(session).toRepresentation(realm);
@ -58,15 +62,14 @@ public class RealmAdminResource {
@GET
@NoCache
@Produces("application/json")
public List<RoleRepresentation> queryRoles() {
return new Transaction() {
public List<RoleRepresentation> getRoles() {
return new Transaction<List<RoleRepresentation>>() {
@Override
protected List<RoleRepresentation> callImpl() {
List<RoleModel> roleModels = realm.getRoles();
List<RoleRepresentation> roles = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : roleModels) {
RoleRepresentation role = new RoleRepresentation(roleModel.getName(), roleModel.getDescription());
role.setId(roleModel.getId());
roles.add(role);
}
return roles;
@ -92,7 +95,7 @@ public class RealmAdminResource {
@NoCache
@Produces("application/json")
public RoleRepresentation getRole(final @PathParam("id") String id) {
return new Transaction() {
return new Transaction<RoleRepresentation>() {
@Override
protected RoleRepresentation callImpl() {
RoleModel roleModel = realm.getRoleById(id);
@ -129,7 +132,7 @@ public class RealmAdminResource {
@POST
@Consumes("application/json")
public Response createRole(final @Context UriInfo uriInfo, final RoleRepresentation rep) {
return new Transaction() {
return new Transaction<Response>() {
@Override
protected Response callImpl() {
if (realm.getRole(rep.getName()) != null) {
@ -151,85 +154,10 @@ public class RealmAdminResource {
@GET
@NoCache
@Produces("application/json")
public List<UserRepresentation> queryUsers(final @Context UriInfo uriInfo) {
return new Transaction() {
@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();
public List<UserRepresentation> getUsers() {
return null;
}
@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
@Produces("application/json")
public List<RealmRepresentation> getRealms() {
return new Transaction() {
return new Transaction<List<RealmRepresentation>>() {
@Override
protected List<RealmRepresentation> callImpl() {
logger.info(("getRealms()"));
@ -81,7 +81,7 @@ public class RealmsAdminResource {
@Consumes("application/json")
public Response importRealm(@Context final UriInfo uriInfo, final RealmRepresentation rep) {
logger.info("importRealm: " + rep.getRealm());
return new Transaction() {
return new Transaction<Response>() {
@Override
protected Response callImpl() {
RealmManager realmManager = new RealmManager(session);
@ -96,7 +96,7 @@ public class RealmsAdminResource {
@Path("{id}")
public RealmAdminResource getRealmAdmin(@Context final HttpHeaders headers,
@PathParam("id") final String id) {
return new Transaction(false) {
return new Transaction<RealmAdminResource>(false) {
@Override
protected RealmAdminResource callImpl() {
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();
Assert.assertEquals(1, creds.size());
RequiredCredentialModel cred = creds.get(0);
Assert.assertEquals("Password", cred.getFormLabel());
Assert.assertEquals("password", cred.getFormLabel());
UserModel user = realm.getUser("loginclient");
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;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@XmlRootElement
public interface SocialProvider {
String getId();
@XmlTransient
AuthRequest getAuthUrl(SocialProviderConfig config) throws SocialProviderException;
String getRequestIdParamName();
String getName();
@XmlTransient
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>google</module>
<module>twitter</module>
<module>facebook</module>
</modules>
</project>

View file

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