admin rest api example

This commit is contained in:
Bill Burke 2014-05-23 18:20:55 -04:00
parent 29070cec77
commit e4232c73c6
31 changed files with 556 additions and 36 deletions

View file

@ -10,6 +10,7 @@ public interface ServiceUrlConstants {
public static final String TOKEN_SERVICE_ACCESS_CODE_PATH = "/realms/{realm-name}/tokens/access/codes"; public static final String TOKEN_SERVICE_ACCESS_CODE_PATH = "/realms/{realm-name}/tokens/access/codes";
public static final String TOKEN_SERVICE_REFRESH_PATH = "/realms/{realm-name}/tokens/refresh"; public static final String TOKEN_SERVICE_REFRESH_PATH = "/realms/{realm-name}/tokens/refresh";
public static final String TOKEN_SERVICE_LOGOUT_PATH = "/realms/{realm-name}/tokens/logout"; public static final String TOKEN_SERVICE_LOGOUT_PATH = "/realms/{realm-name}/tokens/logout";
public static final String TOKEN_SERVICE_DIRECT_GRANT_PATH = "/realms/{realm-name}/tokens/grants/access";
public static final String ACCOUNT_SERVICE_PATH = "/realms/{realm-name}/account"; public static final String ACCOUNT_SERVICE_PATH = "/realms/{realm-name}/account";
public static final String REALM_INFO_PATH = "/realms/{realm-name}"; public static final String REALM_INFO_PATH = "/realms/{realm-name}";

View file

@ -27,6 +27,9 @@ public class AccessTokenResponse {
@JsonProperty("not-before-policy") @JsonProperty("not-before-policy")
protected int notBeforePolicy; protected int notBeforePolicy;
@JsonProperty("session-state")
protected String sessionState;
public String getToken() { public String getToken() {
return token; return token;
} }
@ -74,4 +77,12 @@ public class AccessTokenResponse {
public void setNotBeforePolicy(int notBeforePolicy) { public void setNotBeforePolicy(int notBeforePolicy) {
this.notBeforePolicy = notBeforePolicy; this.notBeforePolicy = notBeforePolicy;
} }
public String getSessionState() {
return sessionState;
}
public void setSessionState(String sessionState) {
this.sessionState = sessionState;
}
} }

View file

@ -17,6 +17,7 @@ public class OAuthClientRepresentation {
protected ClaimRepresentation claims; protected ClaimRepresentation claims;
protected Integer notBefore; protected Integer notBefore;
protected Boolean publicClient; protected Boolean publicClient;
protected Boolean directGrantsOnly;
public String getId() { public String getId() {
@ -98,4 +99,12 @@ public class OAuthClientRepresentation {
public void setPublicClient(Boolean publicClient) { public void setPublicClient(Boolean publicClient) {
this.publicClient = publicClient; this.publicClient = publicClient;
} }
public Boolean isDirectGrantsOnly() {
return directGrantsOnly;
}
public void setDirectGrantsOnly(Boolean directGrantsOnly) {
this.directGrantsOnly = directGrantsOnly;
}
} }

View file

@ -6,6 +6,7 @@ The following examples requires Wildfly 8.0.0, JBoss EAP 6.x, or JBoss AS 7.1.1.
* Transferring identity and role mappings via a special bearer token (Skeleton Key Token). * Transferring identity and role mappings via a special bearer token (Skeleton Key Token).
* Bearer token authentication and authorization of JAX-RS services * Bearer token authentication and authorization of JAX-RS services
* Obtaining bearer tokens via the OAuth2 protocol * Obtaining bearer tokens via the OAuth2 protocol
* Interact with the Keycloak Admin REST Api
There are multiple WAR projects. These will all run on the same WildFly instance, but pretend each one is running on a different There are multiple WAR projects. These will all run on the same WildFly instance, but pretend each one is running on a different
machine on the network or Internet. machine on the network or Internet.
@ -13,6 +14,7 @@ machine on the network or Internet.
* **customer-app-js** A pure HTML/Javascript application that does remote login using OAuth2 browser redirects with the auth server * **customer-app-js** A pure HTML/Javascript application that does remote login using OAuth2 browser redirects with the auth server
* **customer-app-cli** A pure CLI application that does remote login using OAuth2 browser redirects with the auth server * **customer-app-cli** A pure CLI application that does remote login using OAuth2 browser redirects with the auth server
* **product-app** A WAR application that does remote login using OAuth2 browser redirects with the auth server * **product-app** A WAR application that does remote login using OAuth2 browser redirects with the auth server
* **admin-access-app** A WAR application that does remote REST login to admin console to obtain a list of realm roles from Admin REST API
* **database-service** JAX-RS services authenticated by bearer tokens only. The customer and product app invoke on it to get data * **database-service** JAX-RS services authenticated by bearer tokens only. The customer and product app invoke on it to get data
* **third-party** Simple WAR that obtain a bearer token using OAuth2 using browser redirects to the auth-server. * **third-party** Simple WAR that obtain a bearer token using OAuth2 using browser redirects to the auth-server.
* **third-party-cdi** Simple CDI/JSF WAR that obtain a bearer token using OAuth2 using browser redirects to the auth-server. * **third-party-cdi** Simple CDI/JSF WAR that obtain a bearer token using OAuth2 using browser redirects to the auth-server.
@ -184,6 +186,16 @@ The CLI example has two alternative methods for login. When a browser is availab
temporary web server on a free port. If a browser is not available the URL to login is displayed on the CLI. The user can copy this URL to another computer that has a browser available. The code temporary web server on a free port. If a browser is not available the URL to login is displayed on the CLI. The user can copy this URL to another computer that has a browser available. The code
is displayed to the user after login and the user has to copy this code back to the application. is displayed to the user after login and the user has to copy this code back to the application.
Step 8: Admin REST API
----------------------------------
Keycloak has a Admin REST API. This example shows an application making a remove direct login to Keycloak to obtain a token
then using that token to access the Admin REST API.
[http://localhost:8080/admin-access](http://localhost:8080/admin-access)
If you are already logged in, you will not be asked for a username and password, but you will be redirected to
an oauth grant page. This page asks you if you want to grant certain permissions to the third-part app.
Admin Console Admin Console
========================== ==========================

View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<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/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.0-beta-1-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.keycloak.example.demo</groupId>
<artifactId>admin-access-example</artifactId>
<packaging>war</packaging>
<name>Admin Access Example</name>
<description/>
<repositories>
<repository>
<id>jboss</id>
<name>jboss repo</name>
<url>http://repository.jboss.org/nexus/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${keycloak.apache.httpcomponents.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>admin-access</finalName>
<plugins>
<plugin>
<groupId>org.jboss.as.plugins</groupId>
<artifactId>jboss-as-maven-plugin</artifactId>
<version>7.4.Final</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,151 @@
package org.keycloak.example;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.OAuth2Constants;
import org.keycloak.ServiceUrlConstants;
import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.util.BasicAuthHelper;
import org.keycloak.util.JsonSerialization;
import org.keycloak.util.KeycloakUriBuilder;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class AdminClient {
static class TypedList extends ArrayList<RoleRepresentation> {
}
public static class Failure extends Exception {
private int status;
public Failure(int status) {
this.status = status;
}
public int getStatus() {
return status;
}
}
public static AccessTokenResponse getToken() throws IOException {
HttpClient client = new HttpClientBuilder()
.disableTrustManager().build();
try {
HttpPost post = new HttpPost(KeycloakUriBuilder.fromUri("http://localhost:8080/auth")
.path(ServiceUrlConstants.TOKEN_SERVICE_DIRECT_GRANT_PATH).build("demo"));
List <NameValuePair> formparams = new ArrayList <NameValuePair>();
formparams.add(new BasicNameValuePair("username", "admin"));
formparams.add(new BasicNameValuePair("password", "password"));
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, "admin-client"));
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
HttpResponse response = client.execute(post);
int status = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
if (status != 200) {
throw new IOException("Bad status: " + status);
}
if (entity == null) {
throw new IOException("No Entity");
}
InputStream is = entity.getContent();
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
int c;
while ((c = is.read()) != -1) {
os.write(c);
}
byte[] bytes = os.toByteArray();
String json = new String(bytes);
try {
return JsonSerialization.readValue(json, AccessTokenResponse.class);
} catch (IOException e) {
throw new IOException(json, e);
}
} finally {
try {
is.close();
} catch (IOException ignored) {
}
}
} finally {
client.getConnectionManager().shutdown();
}
}
public static void logout(AccessTokenResponse res) throws IOException {
HttpClient client = new HttpClientBuilder()
.disableTrustManager().build();
try {
HttpGet get = new HttpGet(KeycloakUriBuilder.fromUri("http://localhost:8080/auth")
.path(ServiceUrlConstants.TOKEN_SERVICE_LOGIN_PATH)
.queryParam("session-state", res.getSessionState())
.build("demo"));
HttpResponse response = client.execute(get);
HttpEntity entity = response.getEntity();
if (entity == null) {
return;
}
InputStream is = entity.getContent();
if (is != null) is.close();
} finally {
client.getConnectionManager().shutdown();
}
}
public static List<RoleRepresentation> getRealmRoles(AccessTokenResponse res) throws Failure {
HttpClient client = new HttpClientBuilder()
.disableTrustManager().build();
try {
HttpGet get = new HttpGet("http://localhost:8080/auth/admin/realms/demo/roles");
get.addHeader("Authorization", "Bearer " + res.getToken());
try {
HttpResponse response = client.execute(get);
if (response.getStatusLine().getStatusCode() != 200) {
throw new Failure(response.getStatusLine().getStatusCode());
}
HttpEntity entity = response.getEntity();
InputStream is = entity.getContent();
try {
return JsonSerialization.readValue(is, TypedList.class);
} finally {
is.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
} finally {
client.getConnectionManager().shutdown();
}
}
}

View file

@ -0,0 +1,9 @@
<jboss-deployment-structure>
<deployment>
<dependencies>
<!-- the Demo code uses classes in these modules. These are optional to import if you are not using
Apache Http Client or the HttpClientBuilder that comes with the adapter core -->
<module name="org.apache.httpcomponents"/>
</dependencies>
</deployment>
</jboss-deployment-structure>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
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-app_3_0.xsd"
version="3.0">
<module-name>admin-access</module-name>
</web-app>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
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-app_3_0.xsd"
version="3.0">
<module-name>admin-access</module-name>
</web-app>

View file

@ -0,0 +1,30 @@
<%@ page import="org.keycloak.representations.idm.RoleRepresentation" %>
<%@ page import="org.keycloak.example.AdminClient" %>
<%@ page import="org.keycloak.representations.AccessTokenResponse" %>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1" %>
<html>
<head>
<title>Admin Interface</title>
</head>
<body bgcolor="#E3F6CE">
<h2>List of Realm Roles from Admin REST API Call</h2>
<%
java.util.List<RoleRepresentation> list = null;
try {
AccessTokenResponse res = AdminClient.getToken();
list = AdminClient.getRealmRoles(res);
AdminClient.logout(res);
} catch (AdminClient.Failure failure) {
out.println("There was a failure processing request. You either didn't configure Keycloak properly");
out.println("Status from database service invocation was: " + failure.getStatus());
return;
}
for (RoleRepresentation role : list) {
out.print("<p>");
out.print(role.getName());
out.println("</p>");
}
%></body>
</html>

View file

@ -0,0 +1,67 @@
package org.keycloak.example;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.util.JsonSerialization;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class AdminClient {
static class TypedList extends ArrayList<RoleRepresentation> {
}
public static class Failure extends Exception {
private int status;
public Failure(int status) {
this.status = status;
}
public int getStatus() {
return status;
}
}
public static List<RoleRepresentation> getRealmRoles(HttpServletRequest req) throws Failure {
KeycloakSecurityContext session = (KeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName());
HttpClient client = new HttpClientBuilder()
.disableTrustManager().build();
try {
HttpGet get = new HttpGet("http://localhost:8080/auth/admin/realms/demo/roles");
get.addHeader("Authorization", "Bearer " + session.getTokenString());
try {
HttpResponse response = client.execute(get);
if (response.getStatusLine().getStatusCode() != 200) {
throw new Failure(response.getStatusLine().getStatusCode());
}
HttpEntity entity = response.getEntity();
InputStream is = entity.getContent();
try {
return JsonSerialization.readValue(is, TypedList.class);
} finally {
is.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
} finally {
client.getConnectionManager().shutdown();
}
}
}

View file

@ -1,3 +1,5 @@
<%@ page import="org.keycloak.representations.idm.RoleRepresentation" %>
<%@ page import="org.keycloak.example.AdminClient" %>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" <%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1" %> pageEncoding="ISO-8859-1" %>
<html> <html>
@ -8,5 +10,24 @@
<h1>Customer Admin Interface</h1> <h1>Customer Admin Interface</h1>
User <b><%=request.getUserPrincipal().getName()%> User <b><%=request.getUserPrincipal().getName()%>
</b> made this request. </b> made this request.
</body> <p>
</p>
<h2>Admin REST To Get Role List of Realm</h2>
<%
java.util.List<RoleRepresentation> list = null;
try {
list = AdminClient.getRealmRoles(request);
} catch (AdminClient.Failure failure) {
out.println("There was a failure processing request. You either didn't configure Keycloak properly");
out.println("Status from database service invocation was: " + failure.getStatus());
return;
}
for (RoleRepresentation role : list) {
out.print("<p>");
out.print(role.getName());
out.println("</p>");
}
%></body>
</html> </html>

View file

@ -38,6 +38,7 @@
<module>customer-app-cli</module> <module>customer-app-cli</module>
<module>customer-app-js</module> <module>customer-app-js</module>
<module>product-app</module> <module>product-app</module>
<module>admin-access-app</module>
<module>angular-product-app</module> <module>angular-product-app</module>
<module>database-service</module> <module>database-service</module>
<module>third-party</module> <module>third-party</module>

View file

@ -4,6 +4,7 @@
"accessTokenLifespan": 3000, "accessTokenLifespan": 3000,
"accessCodeLifespan": 10, "accessCodeLifespan": 10,
"accessCodeLifespanUserAction": 6000, "accessCodeLifespanUserAction": 6000,
"passwordCredentialGrantAllowed": true,
"sslNotRequired": true, "sslNotRequired": true,
"registrationAllowed": false, "registrationAllowed": false,
"social": false, "social": false,
@ -22,6 +23,17 @@
{ "type" : "password", { "type" : "password",
"value" : "password" } "value" : "password" }
] ]
},
{
"username" : "admin",
"enabled": true,
"email" : "admin@admin.com",
"firstName": "Admin",
"lastName": "Burke",
"credentials" : [
{ "type" : "password",
"value" : "password" }
]
} }
], ],
"roles" : { "roles" : {
@ -40,6 +52,10 @@
{ {
"username": "bburke@redhat.com", "username": "bburke@redhat.com",
"roles": ["user"] "roles": ["user"]
},
{
"username": "admin",
"roles": ["user","admin"]
} }
], ],
"scopeMappings": [ "scopeMappings": [
@ -123,6 +139,13 @@
"http://localhost:8080/oauth-client-cdi/*" "http://localhost:8080/oauth-client-cdi/*"
], ],
"secret": "password" "secret": "password"
},
{
"name": "admin-client",
"enabled": true,
"publicClient": true,
"directGrantsOnly": true
} }
], ],
"applicationRoleMappings": { "applicationRoleMappings": {
@ -131,7 +154,22 @@
"username": "bburke@redhat.com", "username": "bburke@redhat.com",
"roles": ["manage-account"] "roles": ["manage-account"]
} }
],
"realm-management": [
{
"username": "admin",
"roles": ["realm-admin"]
}
]
},
"applicationScopeMappings": {
"realm-management": [
{
"client": "admin-client",
"roles": ["realm-admin"]
}
] ]
} }
} }

View file

@ -198,7 +198,7 @@ module.controller('ApplicationInstallationCtrl', function($scope, realm, applica
module.controller('ApplicationDetailCtrl', function($scope, realm, application, Application, $location, Dialog, Notifications) { module.controller('ApplicationDetailCtrl', function($scope, realm, application, Application, $location, Dialog, Notifications) {
console.log('ApplicationDetailCtrl'); console.log('ApplicationDetailCtrl');
$scope.clientTypes = [ $scope.accessTypes = [
"confidential", "confidential",
"public", "public",
"bearer-only" "bearer-only"
@ -208,28 +208,27 @@ module.controller('ApplicationDetailCtrl', function($scope, realm, application,
$scope.create = !application.name; $scope.create = !application.name;
if (!$scope.create) { if (!$scope.create) {
$scope.application= angular.copy(application); $scope.application= angular.copy(application);
$scope.clientType = $scope.clientTypes[0]; $scope.accessType = $scope.accessTypes[0];
if (application.bearerOnly) { if (application.bearerOnly) {
$scope.clientType = $scope.clientTypes[2]; $scope.accessType = $scope.accessTypes[2];
} else if (application.publicClient) { } else if (application.publicClient) {
$scope.clientType = $scope.clientTypes[1]; $scope.accessType = $scope.accessTypes[1];
} }
} else { } else {
$scope.application = { enabled: true }; $scope.application = { enabled: true };
$scope.application.webOrigins = []; $scope.application.webOrigins = [];
$scope.application.redirectUris = []; $scope.application.redirectUris = [];
$scope.clientType = $scope.clientTypes[0]; $scope.accessType = $scope.accessTypes[0];
} }
$scope.changeClientType = function() { $scope.changeAccessType = function() {
console.log('Client Type: ' + $scope.clientType); if ($scope.accessType == "confidential") {
if ($scope.clientType == "confidential") {
$scope.application.bearerOnly = false; $scope.application.bearerOnly = false;
$scope.application.publicClient = false; $scope.application.publicClient = false;
} else if ($scope.clientType == "public") { } else if ($scope.accessType == "public") {
$scope.application.bearerOnly = false; $scope.application.bearerOnly = false;
$scope.application.publicClient = true; $scope.application.publicClient = true;
} else if ($scope.clientType == "bearer-only") { } else if ($scope.accessType == "bearer-only") {
$scope.application.bearerOnly = true; $scope.application.bearerOnly = true;
$scope.application.publicClient = false; $scope.application.publicClient = false;
} }

View file

@ -77,16 +77,15 @@ module.controller('OAuthClientDetailCtrl', function($scope, realm, oauth, OAuthC
$scope.realm = realm; $scope.realm = realm;
$scope.create = !oauth.id; $scope.create = !oauth.id;
$scope.clientTypes = [ $scope.accessTypes = [
"confidential", "confidential",
"public" "public"
]; ];
$scope.changeClientType = function() { $scope.changeAccessType = function() {
console.log('Client Type: ' + $scope.clientType); if ($scope.accessType == "confidential") {
if ($scope.clientType == "confidential") {
$scope.oauth.publicClient = false; $scope.oauth.publicClient = false;
} else if ($scope.clientType == "public") { } else if ($scope.accessType == "public") {
$scope.oauth.publicClient = true; $scope.oauth.publicClient = true;
} }
}; };
@ -94,15 +93,15 @@ module.controller('OAuthClientDetailCtrl', function($scope, realm, oauth, OAuthC
if (!$scope.create) { if (!$scope.create) {
$scope.oauth= angular.copy(oauth); $scope.oauth= angular.copy(oauth);
$scope.clientType = $scope.clientTypes[0]; $scope.accessType = $scope.accessTypes[0];
if (oauth.publicClient) { if (oauth.publicClient) {
$scope.clientType = $scope.clientTypes[1]; $scope.accessType = $scope.accessTypes[1];
} }
} else { } else {
$scope.oauth = { enabled: true }; $scope.oauth = { enabled: true };
$scope.oauth.webOrigins = []; $scope.oauth.webOrigins = [];
$scope.oauth.redirectUris = []; $scope.oauth.redirectUris = [];
$scope.clientType = $scope.clientTypes[0]; $scope.accessType = $scope.accessTypes[0];
} }
$scope.$watch(function() { $scope.$watch(function() {
@ -133,7 +132,7 @@ module.controller('OAuthClientDetailCtrl', function($scope, realm, oauth, OAuthC
} }
$scope.save = function() { $scope.save = function() {
if (!$scope.oauth.redirectUris || $scope.oauth.redirectUris.length == 0) { if (!$scope.oauth.directGrantsOnly && (!$scope.oauth.redirectUris || $scope.oauth.redirectUris.length == 0)) {
Notifications.error("You must specify at least one redirect uri"); Notifications.error("You must specify at least one redirect uri");
} else { } else {
if ($scope.create) { if ($scope.create) {

View file

@ -42,13 +42,13 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label" for="clientType">Client Type</label> <label class="col-sm-2 control-label" for="accessType">Access Type</label>
<div class="col-sm-4"> <div class="col-sm-4">
<div class="select-kc"> <div class="select-kc">
<select id="clientType" <select id="accessType"
ng-change="changeClientType()" ng-change="changeAccessType()"
ng-model="clientType" ng-model="accessType"
ng-options="cType for cType in clientTypes"> ng-options="aType for aType in accessTypes">
</select> </select>
</div> </div>
</div> </div>

View file

@ -41,18 +41,24 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label" for="clientType">Client Type</label> <label class="col-sm-2 control-label" for="accessType">Access Type</label>
<div class="col-sm-4"> <div class="col-sm-4">
<div class="select-kc"> <div class="select-kc">
<select id="clientType" <select id="accessType"
ng-change="changeClientType()" ng-change="changeAccessType()"
ng-model="clientType" ng-model="accessType"
ng-options="cType for cType in clientTypes"> ng-options="aType for aType in accessTypes">
</select> </select>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group clearfix block">
<label class="col-sm-2 control-label" for="directGrantsOnly">Direct Grants Only</label>
<div class="col-sm-4">
<input ng-model="oauth.directGrantsOnly" name="directGrantsOnly" id="directGrantsOnly" onoffswitch />
</div>
</div>
<div class="form-group" data-ng-hide="oauth.directGrantsOnly">
<label class="col-sm-2 control-label" for="newRedirectUri">Redirect URI <span class="required" data-ng-show="create">*</span></label> <label class="col-sm-2 control-label" for="newRedirectUri">Redirect URI <span class="required" data-ng-show="create">*</span></label>
<div class="col-sm-4 multiple" ng-repeat="redirectUri in oauth.redirectUris"> <div class="col-sm-4 multiple" ng-repeat="redirectUri in oauth.redirectUris">
<div class="input-group kc-item-deletable"> <div class="input-group kc-item-deletable">
@ -75,7 +81,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group" data-ng-hide="create">
<label class="col-sm-2 control-label" for="newWebOrigin">Web Origin</label> <label class="col-sm-2 control-label" for="newWebOrigin">Web Origin</label>
<div class="col-sm-4 multiple" ng-repeat="webOrigin in oauth.webOrigins"> <div class="col-sm-4 multiple" ng-repeat="webOrigin in oauth.webOrigins">
<div class="input-group kc-item-deletable"> <div class="input-group kc-item-deletable">

View file

@ -67,7 +67,7 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="passwordCredentialGrantAllowed" class="col-sm-2 control-label">Password Credential Grant</label> <label for="passwordCredentialGrantAllowed" class="col-sm-2 control-label">Direct Grant API</label>
<div class="col-sm-4"> <div class="col-sm-4">
<input ng-model="realm.passwordCredentialGrantAllowedpasswordCredentialGrantAllowed" name="passwordCredentialGrantAllowed" id="passwordCredentialGrantAllowed" onoffswitch /> <input ng-model="realm.passwordCredentialGrantAllowedpasswordCredentialGrantAllowed" name="passwordCredentialGrantAllowed" id="passwordCredentialGrantAllowed" onoffswitch />
</div> </div>

View file

@ -54,6 +54,9 @@ public interface ClientModel {
boolean isPublicClient(); boolean isPublicClient();
void setPublicClient(boolean flag); void setPublicClient(boolean flag);
boolean isDirectGrantsOnly();
void setDirectGrantsOnly(boolean flag);
RealmModel getRealm(); RealmModel getRealm();
/** /**

View file

@ -4,4 +4,13 @@ package org.keycloak.models.entities;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class OAuthClientEntity extends ClientEntity { public class OAuthClientEntity extends ClientEntity {
protected boolean directGrantsOnly;
public boolean isDirectGrantsOnly() {
return directGrantsOnly;
}
public void setDirectGrantsOnly(boolean directGrantsOnly) {
this.directGrantsOnly = directGrantsOnly;
}
} }

View file

@ -88,6 +88,16 @@ public class ApplicationAdapter extends ClientAdapter implements ApplicationMode
applicationEntity.setBearerOnly(only); applicationEntity.setBearerOnly(only);
} }
@Override
public boolean isDirectGrantsOnly() {
return false; // applications can't be grant only
}
@Override
public void setDirectGrantsOnly(boolean flag) {
// applications can't be grant only
}
@Override @Override
public RoleModel getRole(String name) { public RoleModel getRole(String name) {
TypedQuery<ApplicationRoleEntity> query = em.createNamedQuery("getAppRoleByName", ApplicationRoleEntity.class); TypedQuery<ApplicationRoleEntity> query = em.createNamedQuery("getAppRoleByName", ApplicationRoleEntity.class);

View file

@ -18,7 +18,7 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class ClientAdapter implements ClientModel { public abstract class ClientAdapter implements ClientModel {
protected ClientEntity entity; protected ClientEntity entity;
protected RealmModel realm; protected RealmModel realm;
protected EntityManager em; protected EntityManager em;

View file

@ -15,8 +15,11 @@ import java.util.Set;
*/ */
public class OAuthClientAdapter extends ClientAdapter implements OAuthClientModel { public class OAuthClientAdapter extends ClientAdapter implements OAuthClientModel {
protected final OAuthClientEntity oAuthClientEntity;
public OAuthClientAdapter(RealmModel realm, OAuthClientEntity entity, EntityManager em) { public OAuthClientAdapter(RealmModel realm, OAuthClientEntity entity, EntityManager em) {
super(realm, entity, em); super(realm, entity, em);
oAuthClientEntity = entity;
} }
@Override @Override
@ -24,4 +27,14 @@ public class OAuthClientAdapter extends ClientAdapter implements OAuthClientMode
entity.setName(id); entity.setName(id);
} }
@Override
public boolean isDirectGrantsOnly() {
return oAuthClientEntity.isDirectGrantsOnly();
}
@Override
public void setDirectGrantsOnly(boolean flag) {
oAuthClientEntity.setDirectGrantsOnly(flag);
}
} }

View file

@ -15,4 +15,13 @@ import javax.persistence.NamedQuery;
}) })
@Entity @Entity
public class OAuthClientEntity extends ClientEntity { public class OAuthClientEntity extends ClientEntity {
protected boolean directGrantsOnly;
public boolean isDirectGrantsOnly() {
return directGrantsOnly;
}
public void setDirectGrantsOnly(boolean directGrantsOnly) {
this.directGrantsOnly = directGrantsOnly;
}
} }

View file

@ -97,6 +97,17 @@ public class ApplicationAdapter extends ClientAdapter<MongoApplicationEntity> im
updateMongoEntity(); updateMongoEntity();
} }
@Override
public boolean isDirectGrantsOnly() {
return false; // applications can't be grant only
}
@Override
public void setDirectGrantsOnly(boolean flag) {
// applications can't be grant only
}
@Override @Override
public RoleAdapter getRole(String name) { public RoleAdapter getRole(String name) {
DBObject query = new QueryBuilder() DBObject query = new QueryBuilder()

View file

@ -18,7 +18,7 @@ import org.keycloak.models.mongo.keycloak.entities.MongoUserSessionEntity;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class ClientAdapter<T extends MongoIdentifiableEntity> extends AbstractMongoAdapter<T> implements ClientModel { public abstract class ClientAdapter<T extends MongoIdentifiableEntity> extends AbstractMongoAdapter<T> implements ClientModel {
protected final T clientEntity; protected final T clientEntity;
private final RealmAdapter realm; private final RealmAdapter realm;

View file

@ -19,4 +19,14 @@ public class OAuthClientAdapter extends ClientAdapter<MongoOAuthClientEntity> im
getMongoEntity().setName(id); getMongoEntity().setName(id);
updateMongoEntity(); updateMongoEntity();
} }
@Override
public boolean isDirectGrantsOnly() {
return getMongoEntity().isDirectGrantsOnly();
}
@Override
public void setDirectGrantsOnly(boolean flag) {
getMongoEntity().setDirectGrantsOnly(flag);
}
} }

View file

@ -51,6 +51,7 @@ public class OAuthClientManager {
if (rep.getName() != null) model.setClientId(rep.getName()); if (rep.getName() != null) model.setClientId(rep.getName());
if (rep.isEnabled() != null) model.setEnabled(rep.isEnabled()); if (rep.isEnabled() != null) model.setEnabled(rep.isEnabled());
if (rep.isPublicClient() != null) model.setPublicClient(rep.isPublicClient()); if (rep.isPublicClient() != null) model.setPublicClient(rep.isPublicClient());
if (rep.isDirectGrantsOnly() != null) model.setDirectGrantsOnly(rep.isDirectGrantsOnly());
if (rep.getClaims() != null) { if (rep.getClaims() != null) {
ClaimManager.setClaims(model, rep.getClaims()); ClaimManager.setClaims(model, rep.getClaims());
} }
@ -84,6 +85,7 @@ public class OAuthClientManager {
rep.setName(model.getClientId()); rep.setName(model.getClientId());
rep.setEnabled(model.isEnabled()); rep.setEnabled(model.isEnabled());
rep.setPublicClient(model.isPublicClient()); rep.setPublicClient(model.isPublicClient());
rep.setDirectGrantsOnly(model.isDirectGrantsOnly());
Set<String> redirectUris = model.getRedirectUris(); Set<String> redirectUris = model.getRedirectUris();
if (redirectUris != null) { if (redirectUris != null) {
rep.setRedirectUris(new LinkedList<String>(redirectUris)); rep.setRedirectUris(new LinkedList<String>(redirectUris));

View file

@ -453,6 +453,7 @@ public class TokenManager {
String encodedToken = new JWSBuilder().jsonContent(accessToken).rsa256(realm.getPrivateKey()); String encodedToken = new JWSBuilder().jsonContent(accessToken).rsa256(realm.getPrivateKey());
res.setToken(encodedToken); res.setToken(encodedToken);
res.setTokenType("bearer"); res.setTokenType("bearer");
res.setSessionState(accessToken.getSessionState());
if (accessToken.getExpiration() != 0) { if (accessToken.getExpiration() != 0) {
res.setExpiresIn(accessToken.getExpiration() - Time.currentTime()); res.setExpiresIn(accessToken.getExpiration() - Time.currentTime());
} }

View file

@ -754,7 +754,11 @@ public class TokenService {
} }
if ( (client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) { if ( (client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) {
audit.error(Errors.NOT_ALLOWED); audit.error(Errors.NOT_ALLOWED);
return oauth.forwardToSecurityFailure("Bearer-only applications are not allowed to initiate login"); return oauth.forwardToSecurityFailure("Bearer-only applications are not allowed to initiate browser login");
}
if (client.isDirectGrantsOnly()) {
audit.error(Errors.NOT_ALLOWED);
return oauth.forwardToSecurityFailure("direct-grants-only clients are not allowed to initiate browser login");
} }
redirect = verifyRedirectUri(uriInfo, redirect, realm, client); redirect = verifyRedirectUri(uriInfo, redirect, realm, client);
if (redirect == null) { if (redirect == null) {