Skip to content

Commit

Permalink
Merge pull request #260 from xenit-eu/ACC-1450-delegated-authmanager
Browse files Browse the repository at this point in the history
ACC-1459: authentication for delegated access tokens
  • Loading branch information
vierbergenlars authored Jul 10, 2024
2 parents 3ae09cd + ed27711 commit 4aed0e3
Show file tree
Hide file tree
Showing 14 changed files with 378 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.contentgrid.gateway.runtime.security.authority;

import com.contentgrid.gateway.runtime.security.jwt.ContentGridClaimNames;
import com.contentgrid.gateway.security.authority.Actor;
import com.contentgrid.gateway.security.authority.DelegatedAuthenticationDetailsGrantedAuthority;
import com.contentgrid.gateway.security.jwt.issuer.encrypt.TextEncryptorFactory;
import com.nimbusds.jwt.JWTClaimsSet;
import java.text.ParseException;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.security.oauth2.jwt.Jwt;

@RequiredArgsConstructor
public class ExtensionDelegationGrantedAuthorityConverter implements
Converter<Jwt, Collection<GrantedAuthority>> {

private final TextEncryptorFactory encryptorFactory;
private final Converter<ClaimAccessor, Actor> actorConverter;

@Override
public Collection<GrantedAuthority> convert(Jwt source) {
var principal = actorConverter.convert(decryptClaims(source.getClaimAsString(ContentGridClaimNames.RESTRICT_PRINCIPAL_CLAIMS)));
if (principal == null) {
return null;
}
var actor = actorConverter.convert(() -> source.getClaimAsMap(ContentGridClaimNames.ACT));
if (actor == null) {
return null;
}
return List.of(new DelegatedAuthenticationDetailsGrantedAuthority(principal, actor));
}

@SneakyThrows(ParseException.class)
private ClaimAccessor decryptClaims(String encryptedClaims) {
var decryptedClaimsString = encryptorFactory.newEncryptor().decrypt(encryptedClaims);
var claimSet = JWTClaimsSet.parse(decryptedClaimsString);
return claimSet::toJSONObject;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
@ConfigurationProperties("contentgrid.gateway.runtime-platform.external-issuers")
public class RuntimePlatformExternalIssuerProperties {

private OidcIssuerProperties extensionDelegation = new OidcIssuerProperties();
private OidcIssuerProperties extensionSystem = new OidcIssuerProperties();

@Data
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
package com.contentgrid.gateway.runtime.security.bearer;

import com.contentgrid.gateway.runtime.application.ApplicationId;
import com.contentgrid.gateway.runtime.routing.ApplicationIdRequestResolver;
import com.contentgrid.gateway.runtime.security.authority.ClaimUtil;
import com.contentgrid.gateway.runtime.security.authority.ExtensionDelegationGrantedAuthorityConverter;
import com.contentgrid.gateway.runtime.security.bearer.RuntimePlatformExternalIssuerProperties.OidcIssuerProperties;
import com.contentgrid.gateway.runtime.security.jwt.ContentGridAudiences;
import com.contentgrid.gateway.security.authority.Actor;
import com.contentgrid.gateway.security.authority.Actor.ActorType;
import com.contentgrid.gateway.security.authority.ActorConverter;
import com.contentgrid.gateway.security.authority.ActorConverterType;
import com.contentgrid.gateway.security.authority.UserGrantedAuthorityConverter;
import com.contentgrid.gateway.security.bearer.DynamicJwtAuthenticationManagerResolver;
import com.contentgrid.gateway.security.bearer.IssuerGatedJwtAuthenticationManager;
import com.contentgrid.gateway.security.bearer.PostValidatingJwtAuthenticationManager;
import com.contentgrid.gateway.security.bearer.ReactiveJwtDecoderBuilder;
import com.contentgrid.gateway.security.oidc.ReactiveClientRegistrationIdResolver;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.function.Predicate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -44,8 +51,32 @@
public class RuntimePlatformOAuth2ResourceServerConfiguration {

@Bean
ActorConverter runtimeUserActorConverter() {
return new ActorConverter(iss -> true, ActorType.USER, ClaimUtil::userClaims);
@ActorConverterType(ActorType.USER)
ActorConverter runtimeUserActorConverter(
RuntimePlatformExternalIssuerProperties externalIssuerProperties
) {
var nonUserIssuers = new HashSet<String>();
nonUserIssuers.add(externalIssuerProperties.getExtensionSystem().getIssuer());
nonUserIssuers.add(externalIssuerProperties.getExtensionDelegation().getIssuer());

return new ActorConverter(Predicate.not(nonUserIssuers::contains), ActorType.USER, ClaimUtil::userClaims);
}

@Bean
@ActorConverterType(ActorType.EXTENSION)
ActorConverter runtimeExtensionActorConverter(
RuntimePlatformExternalIssuerProperties externalIssuerProperties
) {
var converter = new ActorConverter(
Predicate.isEqual(externalIssuerProperties.getExtensionSystem().getIssuer()),
ActorType.EXTENSION,
ClaimUtil::extensionSystemClaims
);

// Extensions can have nested actors
converter.setParentActorConverter(converter);

return converter;
}

@Bean
Expand All @@ -54,14 +85,19 @@ Customizer<OAuth2ResourceServerSpec> configureRuntimeJwtAuthenticationManagerRes
ReactiveClientRegistrationIdResolver registrationIdResolver,
ReactiveClientRegistrationRepository clientRegistrationRepository,
RuntimePlatformExternalIssuerProperties externalIssuerProperties,
Converter<ClaimAccessor, Actor> userActorConverter
@ActorConverterType(ActorType.USER)
Converter<ClaimAccessor, Actor> userActorConverter,
@ActorConverterType(ActorType.EXTENSION)
Converter<ClaimAccessor, Actor> extensionActorConverter,
@Autowired(required = false) ExtensionDelegationGrantedAuthorityConverter extensionDelegationGrantedAuthorityConverter
) {
return spec -> {
// The general idea here is: there are 2 different issuers that are trusted for different ways of authenticating
// The general idea here is: there are 3 different issuers that are trusted for different ways of authenticating
// 1. A normal, application-specific, issuer; used directly for user authentication (this one is resolved by the DynamicJwtAuthenticationManagerResolver)
// 2. A shared issuer for extension system authentication: used for extension authentication with a service account (extensionSystemAuthenticationManager)
// 3. A shared issuer for delegated authentication by an extension: used for authenticating an extension on-behalf-of the user (extensionDelegationAuthenticationManager)
//
// For the shared issuers, we only want to create one shared AuthenticationManager that can be shared across all applications.
// For these shared issuers, we only want to create one shared AuthenticationManager that can be shared across all applications.
// However, for shared issuers, we also MUST verify the 'aud' claim, so it is only acceptable for the application that the token was
// issued for. If the audience is not checked, users for different applications (or organizations) would be able to log in to the application.
//
Expand All @@ -75,13 +111,11 @@ Customizer<OAuth2ResourceServerSpec> configureRuntimeJwtAuthenticationManagerRes

var extensionSystemAuthenticationManager = createAuthenticationManager(
externalIssuerProperties.getExtensionSystem(),
new UserGrantedAuthorityConverter(
new ActorConverter(
iss -> true,
ActorType.EXTENSION,
ClaimUtil::extensionSystemClaims
)
)
new UserGrantedAuthorityConverter(extensionActorConverter)
);
var extensionDelegationAuthenticationManager = createAuthenticationManager(
externalIssuerProperties.getExtensionDelegation(),
extensionDelegationGrantedAuthorityConverter
);
resolver.setAuthenticationManagerConfigurer(authenticationManager -> {
var authenticationConverter = new ReactiveJwtAuthenticationConverter();
Expand All @@ -91,18 +125,35 @@ Customizer<OAuth2ResourceServerSpec> configureRuntimeJwtAuthenticationManagerRes
});
resolver.setPostProcessor((authenticationManager, webExchange) ->
applicationIdResolver.resolveApplicationId(webExchange)
.<Mono<ReactiveAuthenticationManager>>map(applicationId -> Mono.just(new DelegatingReactiveAuthenticationManager(
new PostValidatingJwtAuthenticationManager<>(
new JwtClaimValidator<List<String>>(JwtClaimNames.AUD, aud -> aud != null && aud.contains("contentgrid:application:"+applicationId.getValue())),
extensionSystemAuthenticationManager
),
authenticationManager
)))
.map(applicationId -> Mono.<ReactiveAuthenticationManager>just(
new DelegatingReactiveAuthenticationManager(
createApplicationSpecificExtensionAuthenticationManager(
applicationId,
extensionSystemAuthenticationManager,
extensionDelegationAuthenticationManager
),
authenticationManager
))
)
.orElse(Mono.just(authenticationManager))
);
spec.authenticationManagerResolver(resolver);
};
}

private static PostValidatingJwtAuthenticationManager<List<String>> createApplicationSpecificExtensionAuthenticationManager(
ApplicationId applicationId,
ReactiveAuthenticationManager extensionSystemAuthenticationManager,
ReactiveAuthenticationManager extensionDelegationAuthenticationManager
) {
return new PostValidatingJwtAuthenticationManager<>(
new JwtClaimValidator<List<String>>(JwtClaimNames.AUD,
aud -> aud != null && aud.contains(ContentGridAudiences.application(applicationId))),
new DelegatingReactiveAuthenticationManager(
extensionSystemAuthenticationManager,
extensionDelegationAuthenticationManager
)
);
}

@Bean
Expand Down Expand Up @@ -137,5 +188,4 @@ private static ReactiveAuthenticationManager createAuthenticationManager(
jwtAuthenticationManager
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.contentgrid.gateway.runtime.security.jwt;

import com.contentgrid.gateway.runtime.application.ApplicationId;
import lombok.experimental.UtilityClass;

@UtilityClass
public class ContentGridAudiences {

/**
* Audience for the 'authentication' endpoint
* @see <a href="https://github.com/xenit-eu/contentgrid-system-design/blob/main/specs/automation-extension-authentication.md#client-facing-token-exchange">Automation extension authentication spec</a>
*/
public static final String SYSTEM_ENDPOINT_AUTHENTICATION = systemEndpoint("authentication");

public static String systemEndpoint(String endpointId) {
return "contentgrid:system:endpoints:"+endpointId;
}

/**
* Audience for an application
*
* @see <a href="https://github.com/xenit-eu/contentgrid-system-design/blob/main/specs/automation-extension-authentication.md#gateway-extension">Automation extension authentication spec</a>
*/
public static String application(ApplicationId applicationId) {
return "contentgrid:application:"+applicationId.getValue();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.contentgrid.gateway.runtime.security.jwt;

import lombok.experimental.UtilityClass;

@UtilityClass
public class ContentGridClaimNames {

/**
* Contains encrypted claims of the principal in a delegated authentication token
* @see <a href="https://github.com/xenit-eu/contentgrid-system-design/blob/main/specs/automation-extension-authentication.md#additional-jwt-claims">Automation extension authentication spec</a>
*/
public static final String RESTRICT_PRINCIPAL_CLAIMS = "restrict:principal_claims";

/**
* The application ID ({@link com.contentgrid.gateway.runtime.application.ApplicationId}) for which the token is valid
* @see <a href="https://github.com/xenit-eu/contentgrid-system-design/blob/main/specs/automation-extension-authentication.md#additional-jwt-claims">Automation extension authentication spec</a>
*/
public static final String CONTEXT_APPLICATION_ID = "context:application:id";

/**
* All domain names belonging to the application for which the token is valid
* @see <a href="https://github.com/xenit-eu/contentgrid-system-design/blob/main/specs/automation-extension-authentication.md#additional-jwt-claims">Automation extension authentication spec</a>
*/
public static final String CONTEXT_APPLICATION_DOMAINS = "context:application:domains";

/**
* Contains the claims of the actor in a delegated authentication token
* @see <a href="https://www.rfc-editor.org/rfc/rfc8693.html#name-act-actor-claim">RFC8693</a>
*/
public static final String ACT = "act";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.contentgrid.gateway.runtime.application.ApplicationId;
import com.contentgrid.gateway.runtime.config.ApplicationConfigurationRepository;
import com.contentgrid.gateway.runtime.security.jwt.ContentGridAudiences;
import com.contentgrid.gateway.runtime.security.jwt.ContentGridClaimNames;
import com.contentgrid.gateway.security.authority.AuthenticationDetails;
import com.contentgrid.gateway.runtime.web.ContentGridAppRequestWebFilter;
import com.contentgrid.gateway.security.jwt.issuer.JwtClaimsResolver;
Expand Down Expand Up @@ -31,13 +33,13 @@ public Mono<JWTClaimsSet> resolveAdditionalClaims(ServerWebExchange exchange, Au

var claimsBuilder = new JWTClaimsSet.Builder();

claimsBuilder.audience("contentgrid:system:endpoints:authentication");
claimsBuilder.claim("context:application:id", applicationId.toString());
claimsBuilder.audience(ContentGridAudiences.SYSTEM_ENDPOINT_AUTHENTICATION);
claimsBuilder.claim(ContentGridClaimNames.CONTEXT_APPLICATION_ID, applicationId.toString());
if (applicationConfiguration != null) {
claimsBuilder.claim("context:application:domains", applicationConfiguration.getDomains());
claimsBuilder.claim(ContentGridClaimNames.CONTEXT_APPLICATION_DOMAINS, applicationConfiguration.getDomains());
}
try {
claimsBuilder.claim("restrict:principal_claims",
claimsBuilder.claim(ContentGridClaimNames.RESTRICT_PRINCIPAL_CLAIMS,
principalClaimsEncryptor.newEncryptor()
.encrypt(createFromClaimAccessor(authenticationDetails.getPrincipal().getClaims()))
);
Expand Down
Loading

0 comments on commit 4aed0e3

Please sign in to comment.