feat(docs): add multiton docs and fix the custom handler service

Signed-off-by: Juan Pablo Garcia Ripa <sarabadu@gmail.com>
This commit is contained in:
Juan Pablo Garcia Ripa
2025-03-24 10:22:43 +01:00
parent 7d96f52c41
commit 9377fbfb0f
12 changed files with 93 additions and 48 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
'@backstage/backend-defaults': minor
---
Add a new `externalTokenHandlerDecoratorServiceRef` to allow custom external token validations
Add a new `externalTokenHandlersServiceRef` to allow custom external token validations
+12 -10
View File
@@ -414,7 +414,7 @@ Each entry has one or more of the following fields:
## Adding custom or logic for validation and issuing of tokens
The `pluginTokenHandlerDecoratorServiceRef` and `externalTokenHandlerDecoratorServiceRef` can be used to decorate the existing token handler without having to re-implement the entire `AuthService` implementation.
The `pluginTokenHandlerDecoratorServiceRef` and `externalTokenHandlersServiceRef` can be used to extend the existing token handler without having to re-implement the entire `AuthService` implementation.
This is particularly useful when you want to add additional logic to the handler, such as logging or metrics or custom token validation.
### PluginTokenHandler decoration
@@ -446,25 +446,27 @@ const decoratedPluginTokenHandler = createServiceFactory({
### ExternalTokenHandler decoration
The `externalTokenHandlerDecoratorServiceRef` can be used to decorate the default ExternalTokenHandler used for verify tokens from external callers.
The `externalTokenHandlersServiceRef` can be used to add custom external token handlers to the default implementation.
The `ExternalTokenHandler` interface has one methods:
The returned object should be a map of token custom types and their handler factories. The object keys will be matched to the configured `externalAccess` type in the app-config, calling the factory function with the config object for that type. Custom token handlers should implement the `TokenHandler` interface, which provides methods for verifying tokens.
- `verifyToken`: This method is used to verify a token. It takes in the token as an argument and returns a promise that resolves to an object containing the subject of the token and an optional limited user token.
For example, to add a custom token handler for a type called 'custom':
```ts
import {
ExternalTokenHandler,
externalTokenHandlerDecoratorServiceRef,
TokenHandler,
externalTokenHandlersServiceRef,
} from '@backstage/backend-defaults/auth';
import { Config } from '@backstage/config';
import { createServiceFactory } from '@backstage/backend-plugin-api';
const decoratedPluginTokenHandler = createServiceFactory({
service: externalTokenHandlerDecoratorServiceRef,
const customExternalTokenHandlers = createServiceFactory({
service: externalTokenHandlersServiceRef,
deps: {},
async factory() {
return (defaultImplementation: ExternalTokenHandler) =>
new CustomTokenHandler(defaultImplementation);
return {
custom: (config: Config) => new CustomTokenHandler(config);
};
},
});
```
@@ -223,6 +223,35 @@ export const customFooServiceFactory = createServiceFactory({
This allows you to provide more advanced options for the service implementation that couldn't be expressed through static configuration. It also gives users of the service implementation access to other services through dependency injection, which can be useful for their customizations.
## Multiton
By default the service reference will point to a singleton instance of the service. This meand if a new service factory uses this reference will override the previous one. This is the most common use-case, but in some cases you may want to have multiple instances of the same service.
For some services, is desirable to extend the functionality instead of overriding it. For example, some services could have many handler to address a specific event, and you may want to add a new handler instead of overriding the previous one. In this case, you can use the `multiton` option when creating the service reference:
```ts
// example-service-ref.ts
import { createServiceRef } from '@backstage/backend-plugin-api';
export interface FooService {
foo(options: FooOptions): Promise<FooResult>;
}
export const fooServiceRef = createServiceRef<FooService>({
id: 'example.foo',
multiton: true, // this service ref will be an array of instances
});
```
When adding this serviceRef ad dependency to a factory, the factory will receive an array of instances instead of a single instance:
```ts
deps: {fooServices: fooServiceRef},
factory(fooServices) {
// fooServices is an array of instances
return new Bar(fooServices);
},
```
## Service Factory Options Pattern
:::note Note
+26 -15
View File
@@ -5,9 +5,16 @@
```ts
import { AuthService } from '@backstage/backend-plugin-api';
import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { ServiceFactory } from '@backstage/backend-plugin-api';
import { ServiceRef } from '@backstage/backend-plugin-api';
// @public (undocumented)
export type AccessRestrictionsMap = Map<
string, // plugin ID
BackstagePrincipalAccessRestrictions
>;
// @public
export const authServiceFactory: ServiceFactory<
AuthService,
@@ -16,22 +23,12 @@ export const authServiceFactory: ServiceFactory<
>;
// @public
export interface ExternalTokenHandler {
// (undocumented)
verifyToken(token: string): Promise<
| {
subject: string;
accessRestrictions?: BackstagePrincipalAccessRestrictions;
}
| undefined
>;
}
// @public
export const externalTokenHandlerDecoratorServiceRef: ServiceRef<
(defaultImplementation: ExternalTokenHandler) => ExternalTokenHandler,
export const externalTokenHandlersServiceRef: ServiceRef<
{
[configKey: string]: (config: Config) => TokenHandler;
},
'plugin',
'singleton'
'multiton'
>;
// @public
@@ -64,5 +61,19 @@ export const pluginTokenHandlerDecoratorServiceRef: ServiceRef<
'singleton'
>;
// @public
export interface TokenHandler {
// (undocumented)
add?(options: Config): TokenHandler;
// (undocumented)
verifyToken(token: string): Promise<
| {
subject: string;
allAccessRestrictions?: AccessRestrictionsMap;
}
| undefined
>;
}
// (No @packageDocumentation comment for this package)
```
@@ -31,7 +31,7 @@ import { setupServer } from 'msw/node';
import { toInternalBackstageCredentials } from './helpers';
import { PluginTokenHandler } from './plugin/PluginTokenHandler';
import { createServiceFactory } from '@backstage/backend-plugin-api';
import { AccessRestriptionsMap, TokenHandler } from './external/types';
import { AccessRestrictionsMap, TokenHandler } from './external/types';
import { Config } from '@backstage/config';
// import { ExternalTokenHandler } from './external/ExternalTokenHandler';
// import { TokenHandler } from './external/types';
@@ -512,7 +512,7 @@ describe('authServiceFactory', () => {
async verifyToken(token: string): Promise<
| {
subject: string;
allAccessRestrictions?: AccessRestriptionsMap;
allAccessRestrictions?: AccessRestrictionsMap;
}
| undefined
> {
@@ -59,12 +59,6 @@ export const externalTokenHandlersServiceRef = createServiceRef<{
}>({
id: 'core.auth.externalTokenHandlers',
multiton: true,
// defaultFactory: async service =>
// createServiceFactory({
// service,
// deps: {},
// factory: async () => {},
// }),
});
/**
@@ -15,7 +15,7 @@
*/
import { Config } from '@backstage/config';
import { AccessRestriptionsMap } from './types';
import { AccessRestrictionsMap } from './types';
/**
* Parses and returns the `accessRestrictions` configuration from an
@@ -25,12 +25,12 @@ import { AccessRestriptionsMap } from './types';
*/
export function readAccessRestrictionsFromConfig(
externalAccessEntryConfig: Config,
): AccessRestriptionsMap | undefined {
): AccessRestrictionsMap | undefined {
const configs =
externalAccessEntryConfig.getOptionalConfigArray('accessRestrictions') ??
[];
const result: AccessRestriptionsMap = new Map();
const result: AccessRestrictionsMap = new Map();
for (const config of configs) {
const validKeys = ['plugin', 'permission', 'permissionAttribute'];
for (const key of config.keys()) {
@@ -20,7 +20,7 @@ import {
readAccessRestrictionsFromConfig,
readStringOrStringArrayFromConfig,
} from './helpers';
import { AccessRestriptionsMap, TokenHandler } from './types';
import { AccessRestrictionsMap, TokenHandler } from './types';
/**
* Handles `type: jwks` access.
@@ -35,7 +35,7 @@ export class JWKSHandler implements TokenHandler {
subjectPrefix?: string;
url: URL;
jwks: JWTVerifyGetKey;
allAccessRestrictions?: AccessRestriptionsMap;
allAccessRestrictions?: AccessRestrictionsMap;
}> = [];
add(config: Config) {
@@ -17,7 +17,7 @@
import { Config } from '@backstage/config';
import { base64url, decodeJwt, decodeProtectedHeader, jwtVerify } from 'jose';
import { readAccessRestrictionsFromConfig } from './helpers';
import { AccessRestriptionsMap, TokenHandler } from './types';
import { AccessRestrictionsMap, TokenHandler } from './types';
/**
* Handles `type: legacy` access.
@@ -29,7 +29,7 @@ export class LegacyTokenHandler implements TokenHandler {
key: Uint8Array;
result: {
subject: string;
allAccessRestrictions?: AccessRestriptionsMap;
allAccessRestrictions?: AccessRestrictionsMap;
};
}>();
@@ -52,7 +52,7 @@ export class LegacyTokenHandler implements TokenHandler {
#doAdd(
secret: string,
subject: string,
allAccessRestrictions?: AccessRestriptionsMap,
allAccessRestrictions?: AccessRestrictionsMap,
) {
if (!secret.match(/^\S+$/)) {
throw new Error('Illegal secret, must be a valid base64 string');
@@ -16,7 +16,7 @@
import { Config } from '@backstage/config';
import { readAccessRestrictionsFromConfig } from './helpers';
import { AccessRestriptionsMap, TokenHandler } from './types';
import { AccessRestrictionsMap, TokenHandler } from './types';
const MIN_TOKEN_LENGTH = 8;
@@ -30,7 +30,7 @@ export class StaticTokenHandler implements TokenHandler {
string,
{
subject: string;
allAccessRestrictions?: AccessRestriptionsMap;
allAccessRestrictions?: AccessRestrictionsMap;
}
>();
@@ -17,17 +17,25 @@
import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
export type AccessRestriptionsMap = Map<
/**
* @public
*/
export type AccessRestrictionsMap = Map<
string, // plugin ID
BackstagePrincipalAccessRestrictions
>;
/**
* @public
* This interface is used to handle external tokens.
* It is used by the auth service to verify tokens and extract the subject.
*/
export interface TokenHandler {
add?(options: Config): TokenHandler;
verifyToken(token: string): Promise<
| {
subject: string;
allAccessRestrictions?: AccessRestriptionsMap;
allAccessRestrictions?: AccessRestrictionsMap;
}
| undefined
>;
@@ -17,9 +17,10 @@
export {
authServiceFactory,
pluginTokenHandlerDecoratorServiceRef,
externalTokenHandlerDecoratorServiceRef,
externalTokenHandlersServiceRef,
} from './authServiceFactory';
export type { ExternalTokenHandler } from './external/ExternalTokenHandler';
export type { TokenHandler } from './external/types';
export type { AccessRestrictionsMap } from './external/types';
export type { PluginTokenHandler } from './plugin/PluginTokenHandler';