In OAuth 2.0 Resource Server mode, LuciadFusion validates a bearer token on every incoming request. Clients obtain a token from the identity provider and include it in each request they send to the server. The LuciadFusion Studio web application uses the Authorization Code + PKCE flow for this.

For an explanation of how this mode compares to OAuth 2.0 Login, see OAuth 2.0 authentication in LuciadFusion.

Prerequisites

The prerequisites below describe what LuciadFusion requires from your identity provider. For general client setup such as enabling the Authorization Code flow or assigning roles to users, refer to your identity provider’s documentation.

Configure OAuth 2.0 Resource Server

Set the authentication type

In the LuciadFusion configuration file, set fusion.security.authenticationTypes to oauth2ResourceServer:

fusion.security:
  authenticationTypes:
    - oauth2ResourceServer

When using OAuth 2.0 authentication, leave the fusion.security.authenticationManager property empty.

oauth2ResourceServer must be the only entry in fusion.security.authenticationTypes. It can’t be combined with other authentication types.

For more information about the LuciadFusion configuration file, see Configuring LuciadFusion Studio.

Configure the LuciadFusion Studio OIDC client

LuciadFusion Studio uses a built-in OIDC client to authenticate the user against the identity provider and obtain a bearer token. Configure it using the fusion.studio.security.oidc-client.* properties. The full list of available properties is documented in application-fusion.production-template.yml.

fusion:
  studio:
    security:
      oidc-client:
        authority: https://<keycloak-host>/realms/<your-realm>
        client-id: <your-client-id>

Configure token validation

LuciadFusion needs to know how to validate the bearer tokens it receives. You provide this information by setting standard Spring Boot OAuth 2.0 Resource Server properties. Refer to the Spring Security OAuth 2.0 Resource Server documentation for the full list of available properties and customization beans.

LuciadFusion supports two token formats:

  • JWT tokens: are validated locally, making request processing faster.

  • opaque tokens: require LuciadFusion to contact the identity provider for every request, but support immediate token revocation.

JWT tokens example

Supply the issuer URI so that LuciadFusion can fetch the identity provider’s OIDC discovery document to verify token signatures:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://<keycloak-host>/realms/<your-realm>

Alternatively, if your identity provider does not expose a discovery document, specify the JWK Set URI directly:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: https://<keycloak-host>/realms/<your-realm>/protocol/openid-connect/certs

Opaque tokens example

Supply the introspection URI and client credentials so that LuciadFusion can validate tokens by contacting the identity provider:

spring:
  security:
    oauth2:
      resourceserver:
        opaquetoken:
          introspection-uri: https://<keycloak-host>/realms/<your-realm>/protocol/openid-connect/token/introspect
          client-id: <your-introspection-client-id>
          client-secret: <your-introspection-client-secret>

This client is only used to authenticate the introspection request itself. It is not necessarily the same as the OIDC client configured above for the Authorization Code + PKCE flow. Depending on how your IdP is set up, these might need to be different clients with different client IDs.

Customize JWT

Configure the username

By default, the sub claim is used as the username, which is often an opaque UUID rather than a human-readable name. If you want to use a different claim instead, such as preferred_username, provide a custom JwtAuthenticationConverter bean. Check your IdP’s documentation for the correct claim name in your setup. See the Spring Security JWT documentation for more details.

@Configuration
public class OAuth2ResourceServerConfiguration {

  @Bean
  public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setPrincipalClaimName(StandardClaimNames.PREFERRED_USERNAME);
    return converter;
  }
}

To make LuciadFusion pick up this configuration, add its package to the scan path:

fusion.config.additionalScanPackages: <your.package>

Role mapping

LuciadFusion supports role-based access control. If you use this, provide a custom JwtAuthenticationConverter bean to map identity provider roles to LuciadFusion roles. If you also need to configure the username, you can combine both in the same bean. See the Spring Security JWT documentation for more details.

This example handles Keycloak, which places realm roles under the realm_access.roles claim:

@Configuration
public class OAuth2ResourceServerConfiguration {

  @Bean
  public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(jwt -> {
      Map<String, Object> realmAccess = jwt.getClaim("realm_access");
      if (realmAccess != null) {
        List<String> roles = (List<String>) realmAccess.get("roles");
        if (roles != null) {
          return roles.stream()
                      .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                      .collect(Collectors.toList());
        }
      }
      return List.of();
    });
    return converter;
  }
}

To make LuciadFusion pick up this configuration, add its package to the scan path:

fusion.config.additionalScanPackages: <your.package>

Customize opaque tokens

Configure the username

By default, the sub claim is used as the username, which is often an opaque UUID rather than a human-readable name. If you want to use a different claim instead, such as preferred_username, provide a custom OpaqueTokenIntrospector bean. Check your IdP’s documentation for the correct claim name in your setup. See the Spring Security opaque token documentation for more details.

@Configuration
public class OAuth2ResourceServerConfiguration {

  @Bean
  public OpaqueTokenIntrospector introspector() {
    OpaqueTokenIntrospector delegate = SpringOpaqueTokenIntrospector
        .withIntrospectionUri("https://<keycloak-host>/realms/<your-realm>/protocol/openid-connect/token/introspect")
        .clientId("<your-introspection-client-id>")
        .clientSecret("<your-introspection-client-secret>")
        .build();
    return token -> {
      OAuth2AuthenticatedPrincipal principal = delegate.introspect(token);
      String name = principal.getAttribute("preferred_username");
      return new DefaultOAuth2AuthenticatedPrincipal(name, principal.getAttributes(),
                                                     List.copyOf(principal.getAuthorities()));
    };
  }
}

To make LuciadFusion pick up this configuration, add its package to the scan path:

fusion.config.additionalScanPackages: <your.package>

Role mapping

LuciadFusion supports role-based access control. If you use this, provide a custom OpaqueTokenIntrospector bean to map identity provider roles to LuciadFusion roles. If you also need to configure the username, you can combine both in the same bean. See the Spring Security opaque token documentation for more details.

This example handles Keycloak, which places realm roles under the realm_access.roles claim:

@Configuration
public class OAuth2ResourceServerConfiguration {

  @Bean
  public OpaqueTokenIntrospector introspector() {
    OpaqueTokenIntrospector delegate = SpringOpaqueTokenIntrospector
        .withIntrospectionUri("https://<keycloak-host>/realms/<your-realm>/protocol/openid-connect/token/introspect")
        .clientId("<your-introspection-client-id>")
        .clientSecret("<your-introspection-client-secret>")
        .build();
    return token -> {
      OAuth2AuthenticatedPrincipal principal = delegate.introspect(token);
      Collection<GrantedAuthority> authorities = null;
      Map<String, Object> realmAccess = principal.getAttribute("realm_access");
      if (realmAccess != null) {
        List<String> roles = (List<String>) realmAccess.get("roles");
        if (roles != null) {
          authorities = roles.stream()
                              .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                              .collect(Collectors.toList());
        }
      }
      return new DefaultOAuth2AuthenticatedPrincipal(principal.getName(), principal.getAttributes(), authorities);
    };
  }
}

To make LuciadFusion pick up this configuration, add its package to the scan path:

fusion.config.additionalScanPackages: <your.package>