Token Procedure Plugins

The Curity Security Token Server uses token procedures to issue tokens (see Issuing OAuth and OpenId Connect Tokens). These procedures can be defined as JavaScript scripts, described on Token procedures, or as SDK plugins, as documented in this section.

A token procedure plugin is an SDK plugin providing one or more procedures. Token procedures provided via plugins are specific to an endpoint (e.g. token endpoint) and to a flow on that endpoint (e.g. authorization code grant), as illustrated in the following table:

Table 35 token procedure types
Endpoint Flow Procedure Type
Authorization Endpoint Authorization Code AuthorizeCodeTokenProcedure
Authorization Endpoint Hybrid OpenIdAuthorizeEndpointHybridTokenProcedure
Authorization Endpoint Implicit AuthorizeImplicitTokenProcedure
Device Authorization Endpoint Device Authorization DeviceAuthorizationTokenProcedure
Introspection Endpoint Introspection IntrospectionTokenProcedure
Introspection Endpoint Introspection using JWT IntrospectionApplicationJwtTokenProcedure
Token Endpoint Assertion Token AssertionTokenProcedure
Token Endpoint Authorization Code AuthorizationCodeTokenProcedure
Token Endpoint CIBA BackchannelAuthenticationTokenProcedure
Token Endpoint Client Credentials ClientCredentialsTokenProcedure
Token Endpoint Device Code DeviceCodeTokenProcedure
Token Endpoint Refresh Token RefreshTokenProcedure
Token Endpoint Resource Owner Password Credential RopcTokenProcedure
Token Endpoint Token Exchange TokenExchangeTokenProcedure
Token Endpoint Pre-Authorized Code[1] PreAuthorizedCodeTokenProcedure
Assisted Token Endpoint Assisted Token AssistedTokenProcedure
UserInfo Endpoint UserInfo OpenIdUserInfoTokenProcedure

A token plugin can provide zero or one procedure for each one of the above types.

Configuring and using Token Procedure Plugins

Token plugins are configured on a token service profile, more specifically on the token procedure plugins list.

The following two figures illustrate listing and adding a token procedure plugin.

../_images/token-procedure-plugin-list.jpg

Fig. 168 Listing token procedure plugins.

../_images/token-procedure-plugin-add.jpg

Fig. 169 Adding a token procedure plugin.

After a token procedure plugin is configured on a token service profile then it can be used by an endpoint of that same token service profile, by setting the plugin identifier (e.g. example-procedure-plugin-1) on the endpoint configuration (e.g. authorization endpoint configuration).

The following figure illustrates using the procedure provided by the example-procedure-plugin-1 on the authorization endpoint and for the authorization code flow.

../_images/token-procedure-plugin-associate.jpg

Fig. 170 Using a procedure provided by a token procedure plugin on an endpoint.

Note that this association is only possible if the token procedure plugin contains internally a token procedure of the type required by the endpoint and flow being configured. Note also that it is not necessary to select which internal procedure to use from the plugin, since a token procedure plugin can contain at most one procedure of each type.

Developing Token Procedure Plugins

A token procedure plugin is an SDK plugin and the Curity Plugins SDK contains documentation with an overview on how the plugin system works. For reference information of the Java types, use Plugins SDK Javadocs.

A token procedure plugin is described by a descriptor class, implementing the TokenProcedurePluginDescriptor interface, and overriding a get method for each token procedure type that is supported by the plugin. For instance, a token procedure plugin providing a token procedure usable on the authorization endpoint for the authorization code flow, must:

  • Contain a class implementing the AuthorizeCodeTokenProcedure interface.
  • In the descriptor class, override the getOAuthAuthorizeEndpointCodeTokenProcedure method so that it returns the Class instance for the class implementing the AuthorizeCodeTokenProcedure interface.

A token procedure provided by a plugin is a class implementing one of the base interfaces (e.g.``AuthorizeCodeTokenProcedure``), containing a single run method, receiving a context and returning a ResponseModel. The returned response model is used by Curity Identity Server to build the endpoint response. The context, which has a different type per procedure type, provides data and services required to produce the endpoints response map, namely issuing tokens.

The following class exemplifies a token procedure for the authorization code flow of the token endpoint. Notice the use of default data and token issuers, provided by the context, to create the different tokens required for the response.

Listing 155 Example token procedure for the authorization code flow of the token endpoint, written using the Kotlin language.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class ExampleAuthorizationCodeTokenProcedure(
    private val config: ExampleTokenProcedurePluginConfiguration,
) : AuthorizationCodeTokenProcedure {

    override fun run(context: AuthorizationCodeTokenProcedurePluginContext) {
        try {
            val defaultDelegationData = context.defaultDelegationData
            val delegation = context.delegationIssuer.issue(defaultDelegationData)

            val defaultAccessTokenData = context.defaultAccessTokenData
            val accessToken = context.accessTokenIssuer.issue(defaultAccessTokenData, delegation)
                ?: throw TokenIssuerException("AccessToken must always be issued, but wasn't.")

            val refreshToken = context.refreshTokenIssuer.issue(context.defaultRefreshTokenData, delegation)

            val idToken = if (context is OpenIdConnectAuthorizationCodeTokenProcedurePluginContext) {
                var idTokenData = context.defaultIdTokenData
                if (idTokenData == null) {
                    null
                } else {
                    idTokenData = IdTokenAttributes.of(idTokenData.with(
                        Attribute.of(
                            "at_hash", context.idTokenIssuer.atHash(accessToken)
                        )
                    ))
                    context.idTokenIssuer.issue(idTokenData, delegation)
                }
            } else {
                null
            }

            val result = mutableMapOf<String, Any>(
                "access_token" to accessToken,
                "refresh_token" to refreshToken,
                "scope" to defaultAccessTokenData.scope,
                "token_type" to "bearer",
                "expires_in" to Duration.between(Instant.now(), defaultAccessTokenData.expires).seconds,
            )
            if (idToken != null) {
                result["id_token"] = idToken
            }
            if (context.accessTokenClaimNames != null) {
                result["claims"] = context.accessTokenClaimNames
            }

            return ResponseModel.mapResponseModel(result)
        } catch (ex: TokenIssuerException) {
            throw config.getExceptionFactory().internalServerException(ErrorCode.TOKEN_ISSUANCE_ERROR, ex.message)
        }
    }

The reference documentation for the TokenProcedure, present in the plugins SDK Javadocs <https://curity.io/docs/idsvr-java-plugin-sdk/latest>, contains information about all the available procedure types, as well as the context classes that they may receive.

Using Custom Token Issuers

It’s possible to define custom token issuers and then use them in token procedure plugins. The steps below highlight what needs to be done.

  1. Add a getter for the issuer to the plugin’s configuration interface.

    Listing 156 Example token procedure plugin configuration with a getter for a custom access token issuer.
    1
    2
    3
    interface TokenProcedurePluginConfiguration : Configuration {
        fun getMyIssuer(): AccessTokenIssuer
    }
    
  2. Use the custom token issuer in the token procedure plugin (via the configuration getter).

    Listing 157 Example of using a custom access token issuer in a token procedure plugin.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    class CustomClientCredentialsTokenProcedure(
        private val config: TokenProcedurePluginConfiguration,
    ) : ClientCredentialsTokenProcedure {
    
        override fun run(context: ClientCredentialsTokenProcedurePluginContext): ResponseModel {
            val delegation = context.delegationIssuer.issue(context.defaultDelegationData)
            val customAccessTokenIssuer = config.getMyIssuer()
            val accessToken = customAccessTokenIssuer.issue(context.defaultAccessTokenData, delegation)
            return ResponseModel.mapResponseModel(mapOf("access_token" to accessToken))
        }
    }
    
  3. Configure a custom access token issuer in the server configuration under a token profile.

    Listing 158 Example of a configuration for a custom access token issuer.
    1
    2
    3
    4
    5
    6
    7
    8
    <custom-token-issuer>
        <id>CustomAccessTokenIssuer</id>
        <issuer-type>opaque</issuer-type>
        <purpose-type>access_token</purpose-type>
        <data-sources>
            <tokens-data-source-id>DefaultHSQLDB</tokens-data-source-id>
        </data-sources>
    </custom-token-issuer>
    

    See Issuing OAuth and OpenId Connect Tokens for more information about custom token issuers configuration.

  4. Configure the custom token issuer for a token procedure plugin instance.

    Listing 159 Configuring a custom token issuer in a plugin instance.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <profiles xmlns="https://curity.se/ns/conf/base">
        <profile>
            <id>oauth-dev</id>
            <type xmlns:as="https://curity.se/ns/conf/profile/oauth">as:oauth-service</type>
            <settings>
                <authorization-server xmlns="https://curity.se/ns/conf/profile/oauth">
                    <token-procedure-plugins>
                        <token-procedure-plugin>
                            <id>custom-token-procedure-plugin</id>
                            <custom-token-procedure-plugin xmlns="https://curity.se/ns/ext-conf/custom-token-procedure-plugin">
                                <my-issuer>
                                    <id>CustomAccessTokenIssuer</id>
                                </my-issuer>
                            </custom-token-procedure-plugin>
                        </token-procedure-plugin>
                    </token-procedure-plugins>
                </authorization-server>
            </settings>
        </profile>
    </profiles>
    

    Note how the element name my-issuer matches the getter name getMyIssuer() in the plugin’s configuration (declared in the step 1 above). A token procedure plugin can only use issuers defined in the same token profile.

As can be seen above, a custom token issuer can be accessed via the plugin’s configuration interface (using the plugin’s service injection mechanism). This is different from script token procedures where custom token issuers are accessed using the token procedure’s context object that is provided to the procedure’s function as the only parameter (e.g. context.getAccessTokenIssuer("CustomAccessTokenIssuer")). All of the issuer interfaces present in the SDK’s se.curity.identityserver.sdk.service.issuer package can be injected into token procedure plugins using the above mechanism.

There are two issuer types that are treated differently: DelegationIssuer and DeviceCodeIssuer. It’s not possible to create a custom version of these two issuers, however a default DelegationIssuer and a default DeviceCodeIssuer is always created for each token profile. Injecting these issuers into a token procedure plugin always results in the default issuer being provided. Since it’s not possible to configure a custom DelegationIssuer or DeviceCodeIssuer, using them in a token procedure plugin doesn’t require executing steps 3 and 4 from the above. It’s enough to just add a getter to the plugin’s configuration interface and then use the issuer in the token procedure’s run method.

Note that default issuers are always available via the plugin’s context object (similarly to how they’re available in script token procedures). They may also be obtained using the @DefaultService annotation mechanism.

Using Custom Token Introspecters

Similarly to token issuers it’s also possible to inject and use token introspecters in token procedure plugins. The steps below highlight what needs to be done.

  1. Add a getter for the introspecter to the plugin’s configuration interface.

    Listing 160 Example token procedure plugin configuration with a getter for an access token introspecter.
    1
    2
    3
    interface TokenProcedurePluginConfiguration : Configuration {
        fun getMyIntrospecter(): AccessTokenIntrospecter
    }
    
  2. Use the token introspecter in the token procedure plugin (via the configuration getter).

    Listing 161 Example of using an access token introspecter in a token procedure plugin.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class CustomClientCredentialsTokenProcedure(
        private val config: TokenProcedurePluginConfiguration,
    ) : ClientCredentialsTokenProcedure {
    
        override fun run(context: ClientCredentialsTokenProcedurePluginContext): ResponseModel {
            val presentedTokenValue = context.request.getFormParameterValueOrError("access-token")
            val accessTokenIntrospecter = config.getMyIntrospecter()
            var accessTokenAttributes = accessTokenIntrospecter.introspect(presentedTokenValue).orElseThrow()
    
            // manipulate access token attributes (i.e. change some claims)
            accessTokenAttributes = AccessTokenAttributes.of(
                accessTokenAttributes.with(Attribute.of("extra_claim", "extra claim value"))
            )
            val delegation = context.delegationIssuer.issue(context.defaultDelegationData)
            val accessToken = context.accessTokenIssuer.issue(accessTokenAttributes, delegation)
    
            return ResponseModel.mapResponseModel(mapOf("new_access_token" to accessToken))
        }
    }
    
  3. Configure an access token introspecter in the server configuration under a token profile.

    Listing 162 Example of a configuration for an access token introspecter.
    1
    2
    3
    4
    5
    6
    7
    8
    <custom-token-issuer>
        <id>CustomAccessTokenIntrospecter</id>
        <issuer-type>opaque</issuer-type>
        <purpose-type>access_token</purpose-type>
        <data-sources>
            <tokens-data-source-id>DefaultHSQLDB</tokens-data-source-id>
        </data-sources>
    </custom-token-issuer>
    

    See Issuing OAuth and OpenId Connect Tokens for more information about custom token issuers configuration. Note: In the server’s configuration, introspecters and issuers are the same thing. So in order to use an introspecter, a token issuer needs to be configured.

  4. Configure the token introspecter for a token procedure plugin instance.

    Listing 163 Configuring a token introspecter in a plugin instance.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <profiles xmlns="https://curity.se/ns/conf/base">
        <profile>
            <id>oauth-dev</id>
            <type xmlns:as="https://curity.se/ns/conf/profile/oauth">as:oauth-service</type>
            <settings>
                <authorization-server xmlns="https://curity.se/ns/conf/profile/oauth">
                    <token-procedure-plugins>
                        <token-procedure-plugin>
                            <id>custom-token-procedure-plugin</id>
                            <custom-token-procedure-plugin xmlns="https://curity.se/ns/ext-conf/custom-token-procedure-plugin">
                                <my-introspecter>
                                    <id>CustomAccessTokenIntrospecter</id>
                                </my-introspecter>
                            </custom-token-procedure-plugin>
                        </token-procedure-plugin>
                    </token-procedure-plugins>
                </authorization-server>
            </settings>
        </profile>
    </profiles>
    

    Note how the element name my-introspecter matches the getter name getMyIntrospecter() in the plugin’s configuration (declared in the step 1 above). A token procedure plugin can only use introspecters defined in the same token profile.

It’s important to understand that from the server’s configuration perspective, both custom token issuers and token introspecters are the same thing. In order to inject an introspecter, a token issuer has to be configured and then referenced from the token procedure plugin’s configuration, as can be seen in steps 3 and 4 above. The same configured issuer can be injected into a token procedure plugin as an issuer (e.g. using the AccessTokenIssuer type) and as an introspecter (e.g. using the AccessTokenIntrospecter type). A general rule is that an introspecter can introspect tokens issued by a custom issuer having the same id.

Footnotes

[1]The support for the pre-authorized code flow is experimental. See related documentation for more information.