How to Implement Custom Authentication Providers Using TokenDao and GatewayAuthorizationFilter in Apache Linkis

To implement custom authentication providers in Linkis, create a custom TokenDao implementation to fetch tokens from your data source, implement a TokenService (or extend CachedTokenService) to handle validation logic, and expose it as a Spring @Primary bean so GatewaySpringConfiguration injects it into the TokenAuthentication static entry point.

Apache Linkis provides a modular token-based authentication flow through its Spring Cloud Gateway. By leveraging the TokenDao interface and GatewayAuthorizationFilter, you can plug in custom authentication providers without modifying core gateway code. This guide walks through implementing custom TokenDao and TokenService implementations to integrate external token stores.

Understanding the Linkis Token Authentication Architecture

Linkis authenticates requests through a token-based flow wired into the Spring Cloud Gateway. The core components include:

Component Role Main Source File
TokenDao Low-level data-access object that reads token data from persistence linkis-spring-cloud-services/linkis-service-gateway/linkis-gateway-authentication/src/main/java/org/apache/linkis/gateway/authentication/dao/TokenDao.java
TokenEntity POJO representing a token record (name, sign, allowed users/hosts) linkis-spring-cloud-services/linkis-service-gateway/linkis-gateway-authentication/src/main/java/org/apache/linkis/gateway/authentication/entity/TokenEntity.java
TokenService High-level service API used by the gateway with methods like doAuth and isTokenValid linkis-spring-cloud-services/linkis-service-gateway/linkis-gateway-authentication/src/main/scala/org/apache/linkis/gateway/authentication/service/TokenService.scala
CachedTokenService Default TokenService implementation that loads tokens via TokenDao and caches them with Guava linkis-spring-cloud-services/linkis-service-gateway/linkis-gateway-authentication/src/main/scala/org/apache/linkis/gateway/authentication/service/CachedTokenService.scala
TokenAuthentication Static object consulted by the gateway filter; delegates to the TokenService bean linkis-spring-cloud-services/linkis-service-gateway/linkis-gateway-core/src/main/scala/org/apache/linkis/gateway/security/token/TokenAuthentication.scala
GatewayAuthorizationFilter Global Spring Cloud Gateway filter that runs every request and executes security filters linkis-spring-cloud-services/linkis-service-gateway/linkis-spring-cloud-gateway/src/main/java/org/apache/linkis/gateway/springcloud/http/GatewayAuthorizationFilter.java
GatewaySpringConfiguration Spring configuration that injects the TokenService implementation into TokenAuthentication linkis-spring-cloud-services/linkis-service-gateway/linkis-gateway-core/src/main/scala/org/apache/linkis/gateway/config/GatewaySpringConfiguration.scala

How the Default Authentication Flow Works

  1. Incoming request reaches GatewayAuthorizationFilter.
  2. The filter asks SecurityFilter whether the request carries a token via TokenAuthentication.isTokenRequest.
  3. If a token is present, TokenAuthentication.tokenAuth is invoked.
  4. TokenAuthentication delegates to the singleton TokenService (CachedTokenService by default) which:
    • Looks up the token record via TokenDao.
    • Performs validation (expiry, allowed users, allowed hosts).
    • Throws TokenAuthException if any check fails.
  5. On success, the token's owner is written into the GatewayContext via GatewaySSOUtils.setLoginUser.

Because the DAO and service are Spring beans, you can substitute them with your own implementation, creating a custom authentication provider without touching core gateway code.

Creating a Custom TokenDao Implementation

The TokenDao interface defines the contract for loading token data. To implement a custom provider, create a new DAO that reads tokens from your chosen source (e.g., external REST API, LDAP, or a different database).

// src/main/scala/com/yourcorp/linkis/auth/dao/CustomTokenDao.scala
package com.yourcorp.linkis.auth.dao

import org.apache.linkis.gateway.authentication.entity.TokenEntity
import org.apache.linkis.gateway.authentication.dao.TokenDao
import org.apache.ibatis.annotations.Param
import org.springframework.stereotype.Repository
import org.apache.http.impl.client.HttpClients
import org.apache.http.client.methods.HttpGet

@Repository
class CustomTokenDao extends TokenDao {

  private val httpClient = HttpClients.createDefault()
  private val baseUrl = "https://auth.mycorp.com/api/tokens"

  override def selectTokenByName(@Param("tokenName") tokenName: String): TokenEntity = {
    val request = new HttpGet(s"$baseUrl/name/$tokenName")
    val response = httpClient.execute(request)
    try {
      val json = scala.io.Source.fromInputStream(response.getEntity.getContent).mkString
      parseJsonToEntity(json)
    } finally {
      response.close()
    }
  }

  override def selectTokenBySign(@Param("tokenSign") tokenSign: String): TokenEntity = {
    // Implementation for sign-based lookup
    val request = new HttpGet(s"$baseUrl/sign/$tokenSign")
    // ... similar implementation
    null
  }

  private def parseJsonToEntity(json: String): TokenEntity = {
    // Parse JSON and map to TokenEntity fields
    val entity = new TokenEntity()
    // ... mapping logic
    entity
  }
}

Key implementation points:

  • Annotate the class with @Repository so Spring can inject it.
  • Implement all methods declared in TokenDao (typically selectTokenByName and selectTokenBySign).
  • Return TokenEntity objects populated with token metadata (name, signature, allowed users, allowed hosts, expiry).

Implementing a Custom TokenService

The TokenService interface defines high-level authentication operations. You can either implement the interface directly or extend CachedTokenService if you only need to modify specific behaviors (e.g., removing caching or adding custom validation rules).

Extending CachedTokenService for Custom Logic

If you want to bypass the Guava cache and always query your custom DAO, or add additional validation steps:

// src/main/scala/com/yourcorp/linkis/auth/service/CustomTokenService.scala
package com.yourcorp.linkis.auth.service

import org.apache.linkis.gateway.authentication.service.TokenService
import org.apache.linkis.gateway.authentication.bo.{Token, User}
import org.apache.linkis.gateway.authentication.entity.TokenEntity
import org.apache.linkis.gateway.authentication.exception.TokenAuthException
import org.apache.linkis.gateway.authentication.errorcode.LinkisGwAuthenticationErrorCodeSummary._
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import org.springframework.context.annotation.Primary

@Service
@Primary
class CustomTokenService extends TokenService {

  @Autowired
  private var tokenDao: com.yourcorp.linkis.auth.dao.CustomTokenDao = _

  private def loadToken(tokenName: String): Token = {
    val entity = tokenDao.selectTokenByName(tokenName)
    if (entity == null) {
      throw new TokenAuthException(INVALID_TOKEN.getErrorCode, INVALID_TOKEN.getErrorDesc)
    }
    convertToToken(entity)
  }

  private def convertToEntity(token: Token): TokenEntity = {
    // Conversion logic from Token BO to TokenEntity
    null
  }

  private def convertToToken(entity: TokenEntity): Token = {
    // Conversion logic from TokenEntity to Token BO
    new Token() {
      override def getTokenName: String = entity.getTokenName
      override def getBusinessOwner: String = entity.getBusinessOwner
      override def getCreateTime: java.util.Date = entity.getCreateTime
      override def getUpdateTime: java.util.Date = entity.getUpdateTime
      override def getExpireTime: java.util.Date = entity.getExpireTime
      override def getAllowedUsers: String = entity.getAllowedUsers
      override def getAllowedHosts: String = entity.getAllowedHosts
      override def isStale: Boolean = {
        if (getExpireTime == null) false
        else getExpireTime.before(new java.util.Date())
      }
      override def isUserLegal(user: String): Boolean = {
        if (getAllowedUsers == null || getAllowedUsers.isEmpty) true
        else getAllowedUsers.split(",").contains(user)
      }
      override def isHostLegal(host: String): Boolean = {
        if (getAllowedHosts == null || getAllowedHosts.isEmpty) true
        else getAllowedHosts.split(",").contains(host)
      }
    }
  }

  override def doAuth(tokenName: String, userName: String, host: String): Boolean = {
    val token = loadToken(tokenName)
    if (token.isStale) {
      throw new TokenAuthException(TOKEN_IS_STALE.getErrorCode, TOKEN_IS_STALE.getErrorDesc)
    }
    if (!token.isUserLegal(userName)) {
      throw new TokenAuthException(ILLEGAL_TOKENUSER.getErrorCode, ILLEGAL_TOKENUSER.getErrorDesc)
    }
    if (!token.isHostLegal(host)) {
      throw new TokenAuthException(ILLEGAL_TOKENHOST.getErrorCode, ILLEGAL_TOKENHOST.getErrorDesc)
    }
    true
  }

  override def isTokenValid(tokenName: String): Boolean = {
    try {
      val token = loadToken(tokenName)
      !token.isStale
    } catch {
      case _: Exception => false
    }
  }

  override def isTokenAcceptableWithUser(tokenName: String, userName: String): Boolean = {
    try {
      val token = loadToken(tokenName)
      !token.isStale && token.isUserLegal(userName)
    } catch {
      case _: Exception => false
    }
  }

  override def getTokenByName(tokenName: String): Token = loadToken(tokenName)

  override def getTokenByUserToken(tokenName: String, tokenSign: String): Token = {
    // Implementation for sign-based lookup if needed
    loadToken(tokenName)
  }
}

Wiring the Custom Service with Spring

GatewaySpringConfiguration obtains the TokenService bean via field injection and registers it with the static TokenAuthentication object. To ensure your implementation is used, mark it with @Primary:

@Service
@Primary
class CustomTokenService extends TokenService {
  // ... implementation
}

Alternatively, exclude the default CachedTokenService from component scanning or override it explicitly in a @Configuration class:

@Configuration
class AuthConfiguration {
  
  @Bean
  @Primary
  def tokenService(customDao: CustomTokenDao): TokenService = {
    new CustomTokenService(customDao)
  }
}

The @PostConstruct method in GatewaySpringConfiguration automatically calls TokenAuthentication.setTokenService(tokenService), ensuring your custom logic handles all token authentication requests.

Extending Validation with LinkisPreFilter

For additional request-level validation that runs before token authentication—such as IP allowlisting or rate limiting—implement the LinkisPreFilter interface. GatewayAuthorizationFilter automatically iterates over all registered pre-filters (around line 159 in the source), aborting the request if any filter returns false.

// src/main/scala/com/yourcorp/linkis/filter/IpAllowListFilter.scala
package com.yourcorp.linkis.filter

import org.apache.linkis.gateway.http.GatewayContext
import org.apache.linkis.gateway.security.LinkisPreFilter
import org.springframework.stereotype.Component

@Component
class IpAllowListFilter extends LinkisPreFilter {
  
  private val allowedIps = Set("10.0.0.1", "10.0.0.2", "192.168.1.100")
  
  override def name: String = "IpAllowListFilter"
  
  override def doFilter(gatewayContext: GatewayContext): Boolean = {
    val clientIp = gatewayContext.getRequest.getRequestRealIpAddr()
    if (allowedIps.contains(clientIp)) {
      true
    } else {
      gatewayContext.getResponse.setStatusCode(403)
      gatewayContext.getResponse.write("Access denied: IP not in allowlist")
      false
    }
  }
}

Register the filter as a Spring @Component or manually add it via LinkisPreFilter.addFilter() in a configuration class. The filter executes before token validation, allowing you to reject requests based on custom business rules.

Testing Your Custom Authentication Provider

Once deployed, test the custom provider by sending requests with the Token-Key and Token-User headers:

curl -H "Token-Key: abcdef123456" \
     -H "Token-User: alice" \
     http://gateway.example.com/api/v1/engineconn/submit

The GatewayAuthorizationFilter extracts these headers and delegates to TokenAuthentication.tokenAuth, which invokes your CustomTokenService.doAuth method. If your DAO returns a valid TokenEntity for abcdef123456, and the token permits user alice from the requesting host, the request proceeds to the target service. Otherwise, the gateway returns a 401 or 403 error based on the specific validation failure.

Summary

  • TokenDao provides the data access contract for token storage; implement this interface to read from external systems like REST APIs or LDAP.
  • TokenService defines the authentication contract; extend CachedTokenService or implement the interface directly to customize validation logic, caching behavior, or error handling.
  • Spring Integration requires marking your TokenService with @Primary (or excluding the default bean) so GatewaySpringConfiguration injects it into the static TokenAuthentication entry point.
  • LinkisPreFilter offers an extension point for pre-authentication checks such as IP allowlisting or rate limiting, executed by GatewayAuthorizationFilter before token validation.
  • All customization happens without modifying GatewayAuthorizationFilter or core gateway classes, preserving Linkis's modular architecture while supporting enterprise authentication requirements.

Frequently Asked Questions

How does GatewayAuthorizationFilter interact with custom TokenService implementations?

GatewayAuthorizationFilter acts as the global entry point for all gateway requests. When it detects a token-based request (via TokenAuthentication.isTokenRequest), it delegates to TokenAuthentication.tokenAuth, which internally calls the singleton TokenService bean. Because GatewaySpringConfiguration sets this bean via TokenAuthentication.setTokenService(tokenService) during application startup, any custom implementation you provide (marked with @Primary) automatically handles all token validation requests without requiring changes to the filter itself.

Can I use multiple authentication sources simultaneously with TokenDao?

The TokenDao interface is designed as a single contract per TokenService instance. However, you can implement a composite DAO that aggregates multiple sources internally. For example, your selectTokenByName method could first query a local cache, then fall back to a REST API, and finally check an LDAP directory. Since CachedTokenService (or your custom TokenService) calls the DAO methods, the service remains unaware of the underlying data federation, allowing you to support multiple authentication sources transparently.

What is the difference between implementing TokenService directly versus extending CachedTokenService?

Extending CachedTokenService is appropriate when you want to retain the default Guava-based caching behavior but modify specific aspects like validation rules or error handling. CachedTokenService already implements the full TokenService contract, including cache management and DAO delegation. Implementing TokenService directly gives you complete control over the authentication lifecycle—including whether to use caching at all, how to handle concurrent requests, and custom exception mapping. Use direct implementation when the default caching strategy conflicts with your requirements (such as needing real-time token revocation).

How do I troubleshoot when my custom authentication provider is not being invoked?

First, verify that your TokenService bean is annotated with @Primary to ensure it overrides the default CachedTokenService in the Spring context. Check the application logs during startup for the GatewaySpringConfiguration initialization; you should see the TokenAuthentication.setTokenService call completing without errors. If the gateway still uses default authentication, confirm that your custom DAO is correctly returning TokenEntity objects and not throwing exceptions that trigger fallback behavior. Finally, enable debug logging for org.apache.linkis.gateway to trace the request flow through GatewayAuthorizationFilter and verify that TokenAuthentication.isTokenRequest is detecting your token headers correctly.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →