Compare commits

..

9 Commits

Author SHA1 Message Date
Vasili Karaev 27ba862dde add oauth2 token expiration date fix 2020-06-05 17:53:36 +03:00
Vasili Karaev 78e6d09bce fix cas version 2020-06-05 17:52:36 +03:00
Vasili Karaev 75f7f05f82 fix upstream conflicts 2020-04-30 19:38:38 +03:00
Vasili Karaev 624c905af8 add module dependencies 2020-04-30 19:37:46 +03:00
Misagh Moayyed 2fc996f8c5
adjust logs and search 2020-04-30 13:13:26 +04:30
Misagh Moayyed 607c7b3a01
update tasks 2020-04-30 13:07:20 +04:30
Misagh Moayyed f941ad4359
update jib 2020-04-30 12:52:11 +04:30
Misagh Moayyed f8f1ca5297
update boot 2020-03-26 17:49:38 +04:30
Vasili Karaev 3b7c40b74f cas: fix version 2020-03-19 19:31:09 +03:00
4 changed files with 326 additions and 13 deletions

View File

@ -20,7 +20,7 @@ buildscript {
dependencies {
classpath "de.undercouch:gradle-download-task:${project.gradleDownloadTaskVersion}"
classpath "org.springframework.boot:spring-boot-gradle-plugin:${project.springBootVersion}"
classpath "gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:${project.jibVersion}"
classpath "com.google.cloud.tools:jib-gradle-plugin:${project.jibVersion}"
classpath "io.freefair.gradle:maven-plugin:${project.gradleMavenPluginVersion}"
classpath "io.freefair.gradle:lombok-plugin:${project.gradleLombokPluginVersion}"
}
@ -70,8 +70,21 @@ apply from: rootProject.file("gradle/springboot.gradle")
apply from: rootProject.file("gradle/dockerjib.gradle")
dependencies {
// Other CAS dependencies/modules may be listed here...
// implementation "org.apereo.cas:cas-server-support-json-service-registry:${casServerVersion}"
// Module dependencies
compile "org.apereo.cas:cas-server-support-ldap:${project.'cas.version'}"
compile "org.apereo.cas:cas-server-support-reports:${project.'cas.version'}"
compile "org.apereo.cas:cas-server-support-yaml-service-registry:${project.'cas.version'}"
compile "org.apereo.cas:cas-server-support-oauth-webflow:${project.'cas.version'}"
compile "org.apereo.cas:cas-server-support-redis-ticket-registry:${project.'cas.version'}"
// Api dependencies
compile "org.apereo.cas:cas-server-core-util-api:${project.'cas.version'}"
compile "org.apereo.cas:cas-server-core-api-services:${project.'cas.version'}"
compile "org.apereo.cas:cas-server-core-services-api:${project.'cas.version'}"
compile "org.apereo.cas:cas-server-core-authentication-api:${project.'cas.version'}"
compile "org.apereo.cas:cas-server-support-oauth-api:${project.'cas.version'}"
compile "org.apereo.cas:cas-server-support-oauth-core-api:${project.'cas.version'}"
compile "org.apereo.cas:cas-server-support-oauth-services:${project.'cas.version'}"
}
tasks.findByName("jibDockerBuild")
@ -108,4 +121,4 @@ idea {
downloadJavadoc = true
downloadSources = true
}
}
}

View File

@ -1,5 +1,5 @@
# Versions
cas.version=6.2.0-SNAPSHOT
cas.version=6.2.0-RC2
springBootVersion=2.2.5.RELEASE
# Use -jetty, -undertow to other containers
@ -7,13 +7,13 @@ springBootVersion=2.2.5.RELEASE
appServer=-tomcat
executable=false
tomcatVersion=9.0.33
tomcatVersion=9.0.34
group=org.apereo.cas
sourceCompatibility=11
targetCompatibility=11
jibVersion=2.1.0
jibVersion=2.2.0
# Location of the downloaded CAS shell JAR
shellDir=build/libs

View File

@ -33,7 +33,9 @@ project.ext."tomcatDirectory" = tomcatDirectory
def explodedDir = "${buildDir}/cas"
def explodedResourcesDir = "${buildDir}/cas-resources"
def resourceJarName = "cas-server-webapp-resources"
def resourcesJarName = "cas-server-webapp-resources"
def templateViewsJarName = "cas-server-support-thymeleaf"
task copyCasConfiguration(type: Copy, group: "build", description: "Copy the CAS configuration from this project to /etc/cas/config") {
from "etc/cas/config"
@ -47,12 +49,22 @@ task explodeWarOnly(type: Copy, group: "build", description: "Explodes the CAS w
dependsOn 'build'
from zipTree("build/libs/${casWebApplicationBinaryName}")
into explodedDir
doLast {
println "Exploded WAR into ${explodedDir}"
}
}
task explodeWar(type: Copy, group: "build", description: "Explodes the CAS archive and resources jar from the CAS web application archive") {
dependsOn explodeWarOnly
from zipTree("${explodedDir}/WEB-INF/lib/${resourceJarName}-${casServerVersion}.jar")
from zipTree("${explodedDir}/WEB-INF/lib/${templateViewsJarName}-${casServerVersion}.jar")
into explodedResourcesDir
from zipTree("${explodedDir}/WEB-INF/lib/${resourcesJarName}-${casServerVersion}.jar")
into explodedResourcesDir
doLast {
println "Exploded WAR resources into ${explodedResourcesDir}"
}
}
task run(group: "build", description: "Run the CAS web application in embedded container mode") {
@ -222,9 +234,11 @@ task listTemplateViews(group: "build", description: "List all CAS views") {
fileTree(explodedResourcesDir).matching {
include "**/*.html"
}
.collect { it.name }
.toSorted()
.each { println it }
.collect {
return it.path.replace(explodedResourcesDir, "")
}
.toSorted()
.each { println it }
}
}
@ -300,7 +314,11 @@ task getResource(group: "build", description: "Fetch a CAS resource and move it
return
}
if (results.size() > 1) {
println "Multiple resources found matching ${resourceName}: ${results}"
println "Multiple resources found matching ${resourceName}:\n"
results.each {
println "\t-" + it.path.replace(explodedResourcesDir, "")
}
println "\nNarrow down your search criteria and try again."
return
}

View File

@ -0,0 +1,282 @@
package org.apereo.cas.support.oauth.web.response.accesstoken;
import org.apereo.cas.authentication.DefaultAuthenticationBuilder;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.configuration.support.Beans;
import org.apereo.cas.support.oauth.OAuth20Constants;
import org.apereo.cas.support.oauth.OAuth20ResponseTypes;
import org.apereo.cas.support.oauth.validator.token.device.InvalidOAuth20DeviceTokenException;
import org.apereo.cas.support.oauth.validator.token.device.ThrottledOAuth20DeviceUserCodeApprovalException;
import org.apereo.cas.support.oauth.validator.token.device.UnapprovedOAuth20DeviceUserCodeException;
import org.apereo.cas.support.oauth.web.response.accesstoken.ext.AccessTokenRequestDataHolder;
import org.apereo.cas.ticket.Ticket;
import org.apereo.cas.ticket.TicketGrantingTicket;
import org.apereo.cas.ticket.TicketState;
import org.apereo.cas.ticket.accesstoken.OAuth20AccessToken;
import org.apereo.cas.ticket.accesstoken.OAuth20AccessTokenFactory;
import org.apereo.cas.ticket.code.OAuth20Code;
import org.apereo.cas.ticket.device.OAuth20DeviceToken;
import org.apereo.cas.ticket.device.OAuth20DeviceTokenFactory;
import org.apereo.cas.ticket.device.OAuth20DeviceUserCode;
import org.apereo.cas.ticket.refreshtoken.OAuth20RefreshToken;
import org.apereo.cas.ticket.refreshtoken.OAuth20RefreshTokenFactory;
import org.apereo.cas.ticket.registry.TicketRegistry;
import org.apereo.cas.util.function.FunctionUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.transaction.annotation.Transactional;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.LinkedHashSet;
/**
* This is {@link OAuth20DefaultTokenGenerator}.
*
* @author Misagh Moayyed
* @since 5.2.0
*/
@Transactional(transactionManager = "ticketTransactionManager")
@Slf4j
@RequiredArgsConstructor
public class OAuth20DefaultTokenGenerator implements OAuth20TokenGenerator {
/**
* The Access token factory.
*/
protected final OAuth20AccessTokenFactory accessTokenFactory;
/**
* The device token factory.
*/
protected final OAuth20DeviceTokenFactory deviceTokenFactory;
/**
* The refresh token factory.
*/
protected final OAuth20RefreshTokenFactory refreshTokenFactory;
/**
* The Ticket registry.
*/
protected final TicketRegistry ticketRegistry;
/**
* CAS configuration settings.
*/
protected final CasConfigurationProperties casProperties;
@Override
public OAuth20TokenGeneratedResult generate(final AccessTokenRequestDataHolder holder) {
if (OAuth20ResponseTypes.DEVICE_CODE.equals(holder.getResponseType())) {
return generateAccessTokenOAuthDeviceCodeResponseType(holder);
}
val pair = generateAccessTokenOAuthGrantTypes(holder);
return generateAccessTokenResult(holder, pair);
}
/**
* Generate access token OAuth device code response type OAuth token generated result.
*
* @param holder the holder
* @return the OAuth token generated result
*/
protected OAuth20TokenGeneratedResult generateAccessTokenOAuthDeviceCodeResponseType(final AccessTokenRequestDataHolder holder) {
val deviceCode = holder.getDeviceCode();
if (StringUtils.isNotBlank(deviceCode)) {
val deviceCodeTicket = getDeviceTokenFromTicketRegistry(deviceCode);
val deviceUserCode = getDeviceUserCodeFromRegistry(deviceCodeTicket);
if (deviceUserCode.isUserCodeApproved()) {
this.ticketRegistry.deleteTicket(deviceCode);
val deviceResult = AccessTokenRequestDataHolder.builder()
.service(holder.getService())
.authentication(holder.getAuthentication())
.registeredService(holder.getRegisteredService())
.ticketGrantingTicket(holder.getTicketGrantingTicket())
.grantType(holder.getGrantType())
.scopes(new LinkedHashSet<>(0))
.responseType(holder.getResponseType())
.generateRefreshToken(holder.getRegisteredService() != null && holder.isGenerateRefreshToken())
.build();
val ticketPair = generateAccessTokenOAuthGrantTypes(deviceResult);
return generateAccessTokenResult(deviceResult, ticketPair);
}
if (deviceCodeTicket.getLastTimeUsed() != null) {
val interval = Beans.newDuration(casProperties.getAuthn().getOauth().getDeviceToken().getRefreshInterval()).getSeconds();
val shouldSlowDown = deviceCodeTicket.getLastTimeUsed().plusSeconds(interval).isAfter(ZonedDateTime.now(ZoneOffset.UTC));
if (shouldSlowDown) {
throw new ThrottledOAuth20DeviceUserCodeApprovalException(deviceCodeTicket.getId());
}
}
deviceCodeTicket.update();
this.ticketRegistry.updateTicket(deviceCodeTicket);
throw new UnapprovedOAuth20DeviceUserCodeException(deviceCodeTicket.getId());
}
val deviceTokens = createDeviceTokensInTicketRegistry(holder);
return OAuth20TokenGeneratedResult.builder()
.responseType(holder.getResponseType())
.registeredService(holder.getRegisteredService())
.deviceCode(deviceTokens.getLeft().getId())
.userCode(deviceTokens.getValue().getId())
.build();
}
private OAuth20DeviceUserCode getDeviceUserCodeFromRegistry(final OAuth20DeviceToken deviceCodeTicket) {
val userCode = this.ticketRegistry.getTicket(deviceCodeTicket.getUserCode(), OAuth20DeviceUserCode.class);
if (userCode == null) {
throw new InvalidOAuth20DeviceTokenException(deviceCodeTicket.getUserCode());
}
if (userCode.isExpired()) {
this.ticketRegistry.deleteTicket(userCode.getId());
throw new InvalidOAuth20DeviceTokenException(deviceCodeTicket.getUserCode());
}
return userCode;
}
private OAuth20DeviceToken getDeviceTokenFromTicketRegistry(final String deviceCode) {
val deviceCodeTicket = this.ticketRegistry.getTicket(deviceCode, OAuth20DeviceToken.class);
if (deviceCodeTicket == null) {
throw new InvalidOAuth20DeviceTokenException(deviceCode);
}
if (deviceCodeTicket.isExpired()) {
this.ticketRegistry.deleteTicket(deviceCode);
throw new InvalidOAuth20DeviceTokenException(deviceCode);
}
return deviceCodeTicket;
}
private Pair<OAuth20DeviceToken, OAuth20DeviceUserCode> createDeviceTokensInTicketRegistry(final AccessTokenRequestDataHolder holder) {
val deviceToken = deviceTokenFactory.createDeviceCode(holder.getService());
val deviceUserCode = deviceTokenFactory.createDeviceUserCode(deviceToken);
addTicketToRegistry(deviceToken);
addTicketToRegistry(deviceUserCode);
return Pair.of(deviceToken, deviceUserCode);
}
/**
* Generate access token OAuth grant types pair.
*
* @param holder the holder
* @return the pair
*/
protected Pair<OAuth20AccessToken, OAuth20RefreshToken> generateAccessTokenOAuthGrantTypes(final AccessTokenRequestDataHolder holder) {
val clientId = holder.getRegisteredService().getClientId();
val authn = DefaultAuthenticationBuilder
.newInstance(holder.getAuthentication())
.setAuthenticationDate(ZonedDateTime.now(ZoneOffset.UTC))
.addAttribute(OAuth20Constants.GRANT_TYPE, holder.getGrantType().toString())
.addAttribute(OAuth20Constants.SCOPE, holder.getScopes())
.addAttribute(OAuth20Constants.CLIENT_ID, clientId)
.addAttribute(OAuth20Constants.CLAIMS, holder.getClaims())
.build();
val ticketGrantingTicket = holder.getTicketGrantingTicket();
val accessToken = this.accessTokenFactory.create(holder.getService(),
authn, ticketGrantingTicket, holder.getScopes(),
clientId, holder.getClaims());
addTicketToRegistry(accessToken, ticketGrantingTicket);
updateOAuthCode(holder);
val refreshToken = FunctionUtils.doIf(holder.isGenerateRefreshToken(),
() -> generateRefreshToken(holder),
() -> {
return null;
}).get();
return Pair.of(accessToken, refreshToken);
}
/**
* Update OAuth code.
*
* @param holder the holder
*/
protected void updateOAuthCode(final AccessTokenRequestDataHolder holder) {
if (holder.getToken() instanceof OAuth20Code) {
val codeState = TicketState.class.cast(holder.getToken());
codeState.update();
if (holder.getToken().isExpired()) {
this.ticketRegistry.deleteTicket(holder.getToken().getId());
} else {
this.ticketRegistry.updateTicket(holder.getToken());
}
this.ticketRegistry.updateTicket(holder.getTicketGrantingTicket());
}
}
/**
* Add ticket to registry.
*
* @param ticket the ticket
* @param ticketGrantingTicket the ticket granting ticket
*/
protected void addTicketToRegistry(final Ticket ticket, final TicketGrantingTicket ticketGrantingTicket) {
this.ticketRegistry.addTicket(ticket);
if (ticketGrantingTicket != null) {
this.ticketRegistry.updateTicket(ticketGrantingTicket);
}
}
/**
* Add ticket to registry.
*
* @param ticket the ticket
*/
protected void addTicketToRegistry(final Ticket ticket) {
addTicketToRegistry(ticket, null);
}
/**
* Generate refresh token.
*
* @param responseHolder the response holder
* @return the refresh token
*/
protected OAuth20RefreshToken generateRefreshToken(final AccessTokenRequestDataHolder responseHolder) {
val refreshToken = this.refreshTokenFactory.create(responseHolder.getService(),
responseHolder.getAuthentication(),
responseHolder.getTicketGrantingTicket(),
responseHolder.getScopes(),
responseHolder.getClientId(),
responseHolder.getClaims());
addTicketToRegistry(refreshToken, responseHolder.getTicketGrantingTicket());
if (responseHolder.isExpireOldRefreshToken()) {
expireOldRefreshToken(responseHolder);
}
return refreshToken;
}
private void expireOldRefreshToken(final AccessTokenRequestDataHolder responseHolder) {
val oldRefreshToken = responseHolder.getToken();
oldRefreshToken.markTicketExpired();
ticketRegistry.deleteTicket(oldRefreshToken);
}
private static OAuth20TokenGeneratedResult generateAccessTokenResult(final AccessTokenRequestDataHolder holder,
final Pair<OAuth20AccessToken, OAuth20RefreshToken> pair) {
return OAuth20TokenGeneratedResult.builder()
.registeredService(holder.getRegisteredService())
.accessToken(pair.getKey())
.refreshToken(pair.getValue())
.grantType(holder.getGrantType())
.responseType(holder.getResponseType())
.build();
}
}