Compare commits
9 Commits
master
...
feature/in
| Author | SHA1 | Date |
|---|---|---|
|
|
27ba862dde | |
|
|
78e6d09bce | |
|
|
75f7f05f82 | |
|
|
624c905af8 | |
|
|
2fc996f8c5 | |
|
|
607c7b3a01 | |
|
|
f941ad4359 | |
|
|
f8f1ca5297 | |
|
|
3b7c40b74f |
21
build.gradle
21
build.gradle
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue