Skip to content

Commit

Permalink
Merge branch 'release/0.2.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
foo4u committed May 19, 2015
2 parents 178cacd + 639160e commit 19e791a
Show file tree
Hide file tree
Showing 17 changed files with 1,897 additions and 3 deletions.
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,40 @@
Provides projects that may be useful when implementing Spring Security with [Keycloak].
These project may add auxiliary behavior or have yet to be merged into the Keycloak project.

## Keycloak Spring Security Authentication
## Modules

### Keycloak Spring Security Authentication

Provides an extension to Keycloak's Spring Security adapter that enables the authenticated
principal to be loaded from a Spring Security user detail service.

#### Usage

[ ![Download](https://api.bintray.com/packages/smartling/release/keycloak-spring-security-auth/images/download.svg) ](https://bintray.com/smartling/release/keycloak-spring-security-auth/_latestVersion)

```
<dependency>
<groupId>com.smartling.keycloak.extras</groupId>
<artifactId>keycloak-spring-security-auth</artifactId>
<version>0.1.0-RELEASE</version>
</dependency>
```

## Using Smartling Keycloak Extras

Until the extracts are included in JCenter, add the Smrartling OSS [release repository][repo]
to your build:

```
<repositories>
<repository>
<id>smartling-oss-release</id>
<url>https://dl.bintray.com/smartling/release</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>
```

[keycloak]: http://keycloak.org
[repo]: https://dl.bintray.com/smartling/release
8 changes: 7 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ buildscript {
}
dependencies {
classpath "com.netflix.nebula:nebula-project-plugin:2.2.+"
classpath "com.netflix.nebula:gradle-extra-configurations-plugin:2.2.+"
classpath "com.netflix.nebula:nebula-bintray-plugin:2.2.+"
}
}

group = 'com.smartling.keycloak.extras'
description = 'Smartling Keycloak Extras'
version = '0.1.0' + (project.hasProperty('release') && project.ext.release ? '-RELEASE' : '-SNAPSHOT')
version = '0.2.0' + (project.hasProperty('release') && project.ext.release ? '-RELEASE' : '-SNAPSHOT')

ext {
springVersion = '3.2.6.RELEASE'
Expand All @@ -42,6 +43,7 @@ project('keycloak-spring-security-auth') {

subprojects {
apply plugin: "nebula.nebula-project"
apply plugin: "provided-base"
apply plugin: "nebula.nebula-bintray"
apply plugin: 'java'

Expand All @@ -60,6 +62,10 @@ subprojects {
dependencies {
compile "org.slf4j:slf4j-api:1.7.7"
compile "org.keycloak:keycloak-spring-security-adapter:${keycloakVersion}"
provided "javax.servlet:javax.servlet-api:3.0.1"

testRuntime "org.jboss.logging:jboss-logging:3.2.1.Final"
testRuntime "com.fasterxml.jackson.core:jackson-databind:2.5.3"

testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-all:1.10.19'
Expand Down
9 changes: 8 additions & 1 deletion circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@ machine:
java:
version: oraclejdk7

dependencies:
cache_directories:
- ../.gradle/wrapper/dists

test:
override:
- ./gradlew test
post:
- mkdir -p $CIRCLE_TEST_REPORTS/junit/
- find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} $CIRCLE_TEST_REPORTS/junit/ \;

deployment:
snapshot:
branch: develop
commands:
- ./gradlew clean
- ./gradlew clean
release:
branch: master
commands:
Expand Down
24 changes: 24 additions & 0 deletions install-keycloak.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash
#
# Script to install Keycloak on a CI server.
#

set -u # Fail on unset variables
set -e # Fail if any command fails

DIST="keycloak-1.2.1.Smartling-SNAPSHOT"
ARCHIVE="${DIST}.tar.gz"

if [ ! -e ${DIST} ]; then
wget https://s3.amazonaws.com/keycloak-server/${ARCHIVE}
tar xzf ${ARCHIVE}
fi

cp spring-demo-realm.json /tmp

cd ${DIST}
(./bin/standalone.sh -Dkeycloak.migration.action=import \
-Dkeycloak.migration.provider=singleFile \
-Dkeycloak.migration.file=/tmp/spring-demo-realm.json \
-Dkeycloak.migration.strategy=OVERWRITE_EXISTING 2>&1 > /tmp/keyloak.log &) &

Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright 2015 Smartling, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.keycloak.adapters.springsecurity.authentication;

import org.keycloak.KeycloakPrincipal;
import org.keycloak.VerificationException;
import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.KeycloakAccount;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean;
import org.keycloak.adapters.springsecurity.account.KeycloakRole;
import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount;
import org.keycloak.adapters.springsecurity.service.DirectAccessGrantService;
import org.keycloak.adapters.springsecurity.token.DirectAccessGrantToken;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.util.Assert;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;

/**
* {@link AuthenticationProvider} implementing the OAuth2 resource owner password credentials
* grant for clients secured by Keycloak.
*
* <p>
* The resource owner password credentials grant type is suitable in
* cases where the resource owner has a trust relationship with the
* client, such as the device operating system or a highly privileged
* application.
* </p>
*
* @author <a href="mailto:[email protected]">Scott Rossillo</a>
*/
public class DirectAccessGrantAuthenticationProvider implements AuthenticationProvider {

private AdapterDeploymentContextBean adapterDeploymentContextBean;
private DirectAccessGrantService directAccessGrantService;
private GrantedAuthoritiesMapper grantedAuthoritiesMapper = null;

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
RefreshableKeycloakSecurityContext context;
KeycloakAuthenticationToken token;
Collection<? extends GrantedAuthority> authorities;

try {
context = directAccessGrantService.login(username, password);
authorities = this.createGrantedAuthorities(context);
token = new KeycloakAuthenticationToken(createAccount(context), authorities);
} catch (VerificationException e) {
throw new BadCredentialsException("Unable to validate token", e);
} catch (Exception e) {
throw new AuthenticationServiceException("Error authenticating with Keycloak server", e);
}

return token;
}

private KeycloakAccount createAccount(RefreshableKeycloakSecurityContext context) {
Assert.notNull(context);
Set<String> roles = AdapterUtils.getRolesFromSecurityContext(context);
KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal =
AdapterUtils.createPrincipal(adapterDeploymentContextBean.getDeployment(), context);

return new SimpleKeycloakAccount(principal, roles, context);
}

private Collection<? extends GrantedAuthority> createGrantedAuthorities(RefreshableKeycloakSecurityContext context) {
List<KeycloakRole> grantedAuthorities = new ArrayList<>();

for (String role : AdapterUtils.getRolesFromSecurityContext(context)) {
grantedAuthorities.add(new KeycloakRole(role));
}

return mapAuthorities(Collections.unmodifiableList(grantedAuthorities));
}

private Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
if (grantedAuthoritiesMapper == null) {
return authorities;
}
return grantedAuthoritiesMapper.mapAuthorities(authorities);
}


@Override
public boolean supports(Class<?> authentication) {
return DirectAccessGrantToken.class.isAssignableFrom(authentication)
|| UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}

@Required
public void setAdapterDeploymentContextBean(AdapterDeploymentContextBean adapterDeploymentContextBean) {
this.adapterDeploymentContextBean = adapterDeploymentContextBean;
}

@Required
public void setDirectAccessGrantService(DirectAccessGrantService directAccessGrantService) {
this.directAccessGrantService = directAccessGrantService;
}

/**
* Set the optional {@link GrantedAuthoritiesMapper} for this {@link AuthenticationProvider}.
*
* @param grantedAuthoritiesMapper the <code>GrantedAuthoritiesMapper</code> to use
*/
public void setGrantedAuthoritiesMapper(GrantedAuthoritiesMapper grantedAuthoritiesMapper) {
this.grantedAuthoritiesMapper = grantedAuthoritiesMapper;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2015 Smartling, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.keycloak.adapters.springsecurity.authentication;

import org.keycloak.KeycloakPrincipal;
import org.keycloak.adapters.KeycloakAccount;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.keycloak.adapters.springsecurity.token.KeycloakUserDetailsAuthenticationToken;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.util.Assert;

import java.security.Principal;

/**
* Provides a {@link DirectAccessGrantAuthenticationProvider} capable of
* swapping the Keycloak principal with a {@link UserDetails user details} principal.
*
* <p>
* The supplied {@link UserDetailsService} is consulted using the Keycloak
* access token's principal attribute as the username.
* </p>
* <p>
* The original Keycloak principal is available from the {@link KeycloakAuthenticationToken}:
* <pre>
* KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) SecurityContextHolder.getContext().getAuthentication());
* KeycloakAccount account = token.getAccount();
* Principal = account.getPrincipal();
* </pre>
* </p>
*
* @author <a href="mailto:[email protected]">Scott Rossillo</a>
*
* @see UserDetailsService#loadUserByUsername
* @see KeycloakUserDetailsAuthenticationToken
*/
public class DirectAccessGrantUserDetailsAuthenticationProvider extends DirectAccessGrantAuthenticationProvider {

private UserDetailsService userDetailsService;

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {

KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) super.authenticate(authentication);
String username;
UserDetails userDetails;

if (token == null) {
return null;
}

username = this.resolveUsername(token);
userDetails = userDetailsService.loadUserByUsername(username);

return new KeycloakUserDetailsAuthenticationToken(userDetails, token.getAccount(), token.getAuthorities());
}

/**
* Returns the username from the given {@link KeycloakAuthenticationToken}. By default, this method
* resolves the username from the token's {@link KeycloakPrincipal}'s name. This value can be controlled
* via <code>keycloak.json</code>'s
* <a href="http://docs.jboss.org/keycloak/docs/1.2.0.CR1/userguide/html/ch08.html#adapter-config"><code>principal-attribute</code></a>.
* For more fine-grained username resolution, override this method.
*
* @param token the {@link KeycloakAuthenticationToken} from which to extract the username
*
* @return the username to use when loading a user from the this provider's {@link UserDetailsService}.
*
* @see UserDetailsService#loadUserByUsername
* @see KeycloakAccount#getPrincipal
*/
protected String resolveUsername(KeycloakAuthenticationToken token) {

Assert.notNull(token, "KeycloakAuthenticationToken required");
Assert.notNull(token.getAccount(), "KeycloakAuthenticationToken.getAccount() cannot be return null");
KeycloakAccount account = token.getAccount();
Principal principal = account.getPrincipal();

return principal.getName();
}

@Required
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
Loading

0 comments on commit 19e791a

Please sign in to comment.