@axinom/mosaic-id-guard
The mosaic-id-guard
package provides the required middleware and utility related to
securing GraphQL endpoints and messages, security permissions, authentication and authorization.
These utilities can be used for authentication purposes anywhere within the Mosaic framework.
The exported interfaces, functions, and types are described below.
Interfaces
This section describes the exported interfaces through mosaic-id-guard
. These
interfaces are used to enforce the data structures required in the library.
AuthenticationConfig
This interface describes the information required to access the Identity Service. This
is used in utility methods, such as setupManagementAuthentication
and setupEndUserAuthentication
.
interface AuthenticationConfig {
authEndpoint: string;
tenantId: string;
environmentId: string;
}
EnvironmentInfo
This interface describes a specific environment in Mosaic using tenantId
and environmentId
.
interface EnvironmentInfo {
tenantId: string;
environmentId: string;
}
SubjectType
SubjectType
is an enumeration that is used to identify different types of authenticated tokens. A property of this type named subjectType
is available in every Mosaic user token.
enum SubjectType {
ManagedServiceAccount = 'ManagedServiceAccount',
ServiceAccount = 'ServiceAccount',
UserAccount = 'UserAccount',
ImpersonatedUserAccount = 'ImpersonatedUserAccount',
SuperUserAccount = 'SuperUserAccount',
EnvironmentAdminAccount = 'EnvironmentAdminAccount',
EndUserAccount = 'EndUserAccount',
EndUserApplication = 'EndUserApplication',
}
GenericAuthenticatedSubject
This interface abstracts common properties that are included in a JWT. This is a private interface.
interface GenericAuthenticatedSubject extends EnvironmentInfo {
iat: number;
exp: number;
aud: string;
iss: string;
sub: string;
name: string;
email?: string;
subjectType: SubjectType;
}
AuthenticatedManagementSubject
This interface describes information related to an authenticated management user/service account. An instance of this object is constructed by parsing the values extracted from the JWT.
interface AuthenticatedManagementSubject
extends GenericAuthenticatedSubject {
permissions: {
[key: string]: string[];
};
tags?: string[];
[key: string]: unknown;
}
AuthenticatedEndUser
This interface describes information related to an authenticated end-user application user. An instance of this object is constructed by parsing the values extracted from the JWT created by ax-user-service.
interface AuthenticatedEndUser extends GenericAuthenticatedSubject {
applicationId: string;
sessionId: string;
profileId: string;
extensions: unknown;
}
ManagementAuthenticationContext
This interface describes the properties related to a management user authentication. It has an
AuthenticatedManagementSubject
if the authentication was successful or an AxGuardErrorInfo
object if the authentication failed.
interface ManagementAuthenticationContext {
subject?: AuthenticatedManagementSubject;
authErrorInfo?: AxGuardErrorInfo;
}
AuthenticatedManagementRequest
This is an extended type of the Express req object
with the additional property authContext
which is of type ManagementAuthenticationContext
.
interface AuthenticatedManagementRequest extends Request {
authContext: ManagementAuthenticationContext;
}
EndUserAuthenticationContext
This interface describes the properties related to end-user authentication. It has an
AuthenticatedEndUser
if the authentication was successful or an AxGuardErrorInfo
object if the authentication failed.
interface EndUserAuthenticationContext {
subject?: AuthenticatedEndUser;
authErrorInfo?: AxGuardErrorInfo;
}
AuthenticatedEndUserRequest
This is an extended type of the Express req object
with the additional property authContext
which is of type EndUserAuthenticationContext
.
interface AuthenticatedEndUserRequest extends Request {
authContext: EndUserAuthenticationContext;
}
PermissionDefinition
This interface is used to construct an object containing permission names along with the GraphQL operations that belong to a permission.
This structure can store an array of Permission
objects, and have some configuration properties specially when
it comes to GraphQL operations.
- anonymousGqlOperations - Operations defined under this property is ignored while checking for authorization.
- ignoredGqlOperations - Operations defined under this property are always disabled and the
EnforceStrictPermissions
plugin will not produce any warnings in the logs. This option can be used when a developer is certain that a GraphQL operation must not be available in the API and therefore explicitly defines it as an ignored operations, so the default behavior of warning about it in the logging will be turned off.
interface Permission {
/**
* Key of the permission.
* @property
*/
key: string;
/**
* Title of the permission.
* @property
*/
title: string;
/**
* True for only permission is used for manage services.
* @property
*/
usedByManagedServiceOnly?: boolean;
/**
* True for only permission is used for development.
* @property
*/
usedForDevelopment?: boolean;
/**
* List of GraphQL operations guarded by this Permission.
* @property
*/
gqlOperations?: readonly string[];
}
interface PermissionDefinition {
gqlOptions?: {
/**
* Any operations defined under ANONYMOUS will be ignored while checking for Authorization.
* @property
*/
anonymousGqlOperations?: string[];
/**
* Any operations defined under IGNORE will not be logged as disabled operations
* as they are known to be intentionally removed.
* @property
*/
ignoredGqlOperations?: string[];
};
/**
* Array of permissions owned by the service. These permissions will be available to be granted to
* User Roles and Service Accounts.
* @property
*/
permissions: readonly Permission[];
}
Permission Naming Convention
The permissions for the managed services have a naming convention as follows:
- Key - <PLURAL_ENTITY>_<ACTION>
- Title - <Plural Entity>: Action
It is advised to follow a similar naming convention when defining customizable service permissions to have better consistency, but is not enforced in anyway.
An example permission definition using this naming convention can be seen below:
export const permissionDefinition: PermissionDefinition = {
gqlOptions: {
anonymousGqlOperations: [M.someOperationWhichCanBeCalledWithoutAnyToken],
},
permissions: [
{
key: 'MOVIES_VIEW',
title: 'Movies: View',
gqlOperations: [...MoviesReadOperations],
},
{
key: 'MOVIES_EDIT',
title: 'Movies: Edit',
gqlOperations: [...MoviesReadOperations, ...MoviesMutateOperations],
}
]
}
Middleware
setupManagementAuthentication
function setupManagementAuthentication(
app: Express,
guardRoutes: string[],
authParams: string | AuthenticationConfig,
): void;
This is the Express middleware
that is used by Mosaic services to secure any http endpoints they expose that are related to management users. It validates
the Bearer token embedded in the http request header and authorizes the request to
use the routes passed as an argument. If a Bearer token is not present for an endpoint
guarded by this middleware it throws an AccessTokenRequired
error.
In the validation process, it extracts the JWT, verifies its authenticity and sets
the authContext
of the AuthenticatedManagementRequest
. AuthenticatedManagementRequest
is an extended type of the Express req object
with an additional authContext
property. The authContext
is an object that carries
information related to the authenticated subject of type AuthenticatedManagementSubject
.
Usage
The setupManagementAuthentication
middleware is called in the index.ts
file of Mosaic
services that require the authentication functionality.
setupManagementAuthentication(app, ['/graphql'], authenticationConfig);
The middleware function takes the three following arguments:
- Express app object (
app
) - Guard routes (
['/graphql']
) - A
string
or anAuthenticationConfig
object (authParams
)
If a string
is passed as the authParams
argument, it should contain the ID Service Auth Endpoint.
If it is an instance of AuthenticationConfig
, it should contain ID Service connection information. In case of non-managed services, it is mandatory to pass an AuthenticationConfig
object with the correct tenant and environment IDs.
Since a regular Mosaic backend only exposes GraphQL endpoints, usually, the setupManagementAuthentication
middleware is only registered for the /graphql
endpoint. However, that does not
restrict an application developer from using the middleware to secure any other
endpoints that the application might expose.
Errors
This middleware may throw errors with the following codes while validating the JWT:
AccessTokenExpired
- This error is thrown if the passed JWT has expiredSigningKeyNotFound
- This error is thrown if a valid signing key is not found to verify the token. This may happen if the signing key used to sign the token is revoked.JwksError
- This error is thrown if an error occurs while trying to fetch the signing keys from the JWKS endpoint. One of the reasons this might happen is because the library cannot find a signing key for thekid
in the JWT, because the keys were rotated and the user is still trying to access using a JWT signed using the old key. Signing in again to the application may resolve the error.IdentityServiceNotAccessible
- This error is thrown if there is a network level error while trying to access the ID Service for verification. i.e.ECONNREFUSED
AccessTokenVerificationFailed
- This error is thrown if the error does not belong to any of the above categories.
setupEndUserAuthentication
function setupEndUserAuthentication(
app: Express,
guardRoutes: string[],
authParams: string | AuthenticationConfig,
): void;
This is the Express middleware
that is used by Mosaic services to secure any http endpoints they expose that are related
to end-user related services. It expects a bearer token embedded in the http
request header signed by the user service. The function validates the bearer token and
authorizes the request to use the routes passed as an argument. If a Bearer token is not
present for an endpoint guarded by this middleware it throws an AccessTokenRequired
error.
In the validation process, it extracts the JWT, verifies its authenticity and sets
the authContext
of the AuthenticatedEndUserRequest
. AuthenticatedEndUserRequest
is an extended type of the Express req object
with an additional authContext
property. The authContext
is an object that carries
information related to the authenticated subject of type AuthenticatedEndUser
.
Usage
The setupEndUserAuthentication
middleware is called in the index.ts
file of end-user facing
Mosaic services that require the authentication functionality.
setupEndUserAuthentication(app, ['/graphql'], config.userServiceAuthEndpoint);
The middleware function takes the following three arguments:
- Express app object (
app
) - Guard routes (
['/graphql']
) - A
string
or anAuthenticationConfig
object (authParams
)
If a string
is passed as the authParams
argument, it should contain the User Service Auth Endpoint.
If it is an instance of AuthenticationConfig
, it should contain User Service connection information. In case of non-managed services, it is mandatory to pass an AuthenticationConfig
object with the correct tenant and environment IDs.
In practice, this is called before the call to the setupPostGraphile
middleware.
Since a regular Mosaic backend only exposes GraphQL endpoints, usually, the setupEndUserAuthentication
middleware is only registered for the /graphql
endpoint. However, that does not
restrict an application developer from using the middleware to secure any other
endpoints that the application might expose.
Errors
This middleware may throw the following errors while validating the JWT:
AccessTokenExpired
- This error is thrown if the passed JWT has expiredSigningKeyNotFound
- This error is thrown if a valid signing key is not found to verify the token. This may happen if the signing key used to sign the token is revoked.JwksError
- This error is thrown if an error occurs while trying to fetch the signing keys from the JWKS endpoint. One of the reasons this might happen is because the library cannot find a signing key for thekid
in the JWT, because the keys were rotated and the user is still trying to access using a JWT signed using the old key. Signing in again to the application may resolve the error.UserServiceNotAccessible
- This error is thrown if there is a network level error while trying to access the User Service for verification. i.e.ECONNREFUSED
AccessTokenVerificationFailed
- This error is thrown if the error does not belong to any of the above categories.
setupManagementGQLSubscriptionAuthentication
setupManagementGQLSubscriptionAuthentication = (
authParams: string | AuthenticationConfig,
): Middleware<Request, Response>[];
This returns an array of Express middleware that is used by Mosaic services for setting up authentication for GraphQL subscriptions that are related to management services.
The middleware returned by this function first extracts the bearer token from the request, parses the token
to verify it and attaches it to the authContext
property of the request as an
ManagementAuthenticationContext
. If the authErrorInfo
property has a value
then that error is thrown.
Usage
setupManagementGQLSubscriptionAuthentication
should be passed as websocketMiddleware
when setting up web socket server using setupHttpServerWithWebsockets()
.
const httpServer = setupHttpServerWithWebsockets(
app,
logger,
setupManagementGQLSubscriptionAuthentication(
config.idServiceAuthEndpointUrl,
),
);
This function takes a single argument of either a string which contains the URL of the ID Service auth endpoint
or an instance of AuthenticationConfig
containing ID Service connection information.
Errors
This middleware may throw an AxGuardError
if the token verification fails.
setupEndUserGQLSubscriptionAuthentication
setupEndUserGQLSubscriptionAuthentication = (
authParams: string | AuthenticationConfig,
): Middleware<Request, Response>[];
This returns an array of Express middleware that is used by Mosaic services for setting up authentication for GraphQL subscriptions that are related to end-user applications.
The middleware returned by this function first extracts the bearer token from the request, parses the token
to verify it and attaches it to the authContext
property of the request as an
EndUserAuthenticationContext
. If the authErrorInfo
property has a value
then that error is thrown.
Usage
setupEndUserGQLSubscriptionAuthentication
should be passed as websocketMiddleware
when setting up web socket server using setupHttpServerWithWebsockets()
.
const httpServer = setupHttpServerWithWebsockets(
app,
logger,
setupEndUserGQLSubscriptionAuthentication(
config.userServiceAuthEndpointUrl,
),
);
This function takes a single argument of either a string which contains the URL of the User Service auth endpoint
or an instance of AuthenticationConfig
containing User Service connection information.
Errors
This middleware may throw an AxGuardError
if the token verification fails.
Functions
The following section describes the functions that are exported through this library as well as their usages.
getAuthenticatedManagementSubject
This function validates the specified token in the context of a given environment and derives an AuthenticatedManagementSubject
object (which contains a list of the permissions provided in the token).
getAuthenticatedManagementSubject = async (
token: string,
authConfig: AuthenticationConfig,
): Promise<AuthenticatedManagementSubject>
getAuthenticatedEndUser
This function can be used to verify and derive an AuthenticatedEndUser
object from a JWT.
getAuthenticatedEndUser = async (
token: string,
authParams: string | AuthenticationConfig,
): Promise<AuthenticatedEndUser>
getManagementAuthenticationContext
This function accepts an http request and an AuthenticationConfig
object and returns the ManagementAuthenticationContext
for that request.
getManagementAuthenticationContext = async (
req: Request,
authConfig?: AuthenticationConfig,
): Promise<ManagementAuthenticationContext>
getEndUserAuthenticationContext
This function accepts an http request and an AuthenticationConfig
object and returns the EndUserAuthenticationContext
for that request.
getEndUserAuthenticationContext = async (
req: Request,
authParams?: string | AuthenticationConfig,
): Promise<EndUserAuthenticationContext>
PostGraphile Plug-ins
The PostGraphile plug-ins are used to extend the GraphQL schema that is generated by PostGraphile.
AxGuardPlugin
The AxGuardPlugin
is a Postgraphile wrapper plug-in
which should be added to the appendPlugins
array in the PostGraphile options
object when setting up PostGraphile. This plug-in takes care of the authorization
functionality and makes sure that the JWT has the required permissions to access
the given GraphQL resource. The authorization logic applies to queries, mutations
and subscriptions.
In addition to performing authorization logic, this plugin generates a permission-definition.json
file
when the configuration is set to development
. This file contains the Permission Definition for the
service. If this file needs to be generated, the AxGuard plugin must be provided with two additional
arguments:
config
- The configuration object of the service.permissionDefinitionJsonPath
- The path to thepermission-definition.json
file.
Setting up PostGraphile
The PostGraphile options can be set up in two ways:
- Construct the PostGraphile options as mentioned here.
- Use the
PostgraphileOptionsBuilder
exposed via@axinom/mosaic-service-common
to construct the PostGraphile options object.
Usage
Extra code has been removed from the examples below for readability. All
examples use the PostgraphileOptionsBuilder
to construct the PostGraphile options.
The plug-in is added to the PostGraphile options as shown below.
PostgraphileOptionsBuilder(config.isDev, config.graphqlGuiEnabled)
.addPlugins(
AxGuardPlugin(config, './src/generated/security/permission-definition.json'),
)
.build();
There are a few prerequisites for the AxGuardPlugin
to work, which need to be
set in the PostGraphile options.
The AuthenticatedManagementSubject
or AuthenticatedEndUser
must be set in the ExtendedGraphQLContext
.
It can be done as shown below.
PostgraphileOptionsBuilder(config.isDev, config.graphqlGuiEnabled)
.setAdditionalGraphQLContextFromRequest(async (req) => {
const { subject, authErrorInfo } = await getManagementAuthenticationContext(
req,
ManagementAuthenticationContext,
);
const extendedRequest = req as Request & { token: string };
return {
subject,
authErrorInfo,
};
})
.build();
The serviceId
and permissionDefinition
properties must be set in the PostGraphile
options. The permissionDefinition
object is of the type PermissionDefinition
,
which contains the mapping of permissions to GraphQL operations.
The following code snippet describes how serviceId
and permissionDefinition
are set.
PostgraphileOptionsBuilder(config.isDev, config.graphqlGuiEnabled)
.addGraphileBuildOptions({
serviceId: config.serviceId,
permissionDefinition: permissionDefinition,
})
.build();
AxGuardPlugin is a wrapper plug-in. For any GraphQL request, after a request is
authenticated through setupManagementAuthentication
or setupEndUserAuthentication
middleware,
this plug-in comes into action and performs the required authorization tasks. The
plug-in is executed before the rest of the request is processed.
When performing the authorization, it excludes any operations that are defined under
anonymousGqlOperations
in the permissionDefinition
. Then, it extracts the permissions attached
to the AuthenticatedManagementSubject
object and validates against the permissionDefinition
object to check if the required permissions are present.
Errors
If the required permissions are not present in the AuthenticatedManagementSubject
or AuthenticatedEndUser
to perform
the requested GraphQL operation, the plug-in throws the UserNotAuthorized
error.
EnforceStrictPermissionsPlugin
The EnforceStrictPermissionsPlugin
omits all GraphQL operations from being exposed
that are not assigned to a permission in the respective PermissionDefinition
file.
This excludes any operations defined under anonymousGqlOperations
.
Usage
This plug-in requires the permissionDefinition
property in the PostGraphile options
to be set (see above).
Then, the plug-in must added to the appendPlugins
array in the PostGraphile options,
so it could be enabled.
PostgraphileOptionsBuilder(config.isDev, config.graphqlGuiEnabled)
.addPlugins(
EnforceStrictPermissionsPlugin,
)
.build();
When the service is started, this plug-in logs any disabled operations and mapped operations that do not exist.
Classes
ManagedServiceGuardedMessageHandler
The ManagedServiceGuardedMessageHandler
is an abstract class that can be used to build message
handlers. When extended from ManagedServiceGuardedMessageHandler
, it changes the MessageInfo
parameter to an AuthenticatedManagementSubjectMessageInfo
object, which contains a verified JWT subject.
In addition, it validates if the required set of permissions defined for the message handler are present.
Usage
Any message handler that requires authorization must be extended from ManagedServiceGuardedMessageHandler
.
In the constructor, a call to super()
must be made, defining permissions that are
required for the message handler to function.
As a practice/pattern, it is recommended that a service-specific class is defined extending from the
ManagedServiceGuardedMessageHandler
and that this class is used for handler creation.
// Service specific class
abstract class MediaManagedServiceGuardedMessageHandler<
TContent
> extends ManagedServiceGuardedMessageHandler<TContent> {
constructor(
messagingKey: string,
permissions: Permission[],
protected readonly config: Config,
overrides?: SubscriptionConfig,
middleware: OnMessageMiddleware[] = [],
) {
super(
messagingKey,
permissions,
config.serviceId,
{
tenantId: config.tenantId,
environmentId: config.environmentId,
authEndpoint: config.idServiceAuthEndpointUrl,
},
overrides,
middleware,
);
}
}
// Message handler extending from MediaManagedServiceGuardedMessageHandler
class PublishEntityCommandHandler extends MediaManagedServiceGuardedMessageHandler<
PublishEntityCommand
> {
private readonly logger;
constructor(
protected readonly broker: Broker,
private readonly dbPool: Pool,
config: Config,
) {
super(
// Defining permissions required to execute the message handler
MediaMessagingSettings.PublishEntity.messageType,
[
'ADMIN',
'COLLECTIONS_EDIT',
'MOVIES_EDIT',
'SETTINGS_EDIT',
'TVSHOWS_EDIT',
],
config,
);
this.logger = new Logger(config, 'PublishEntityCommandHandler');
}
async onMessage(
payload: PublishEntityCommand,
messageInfo: MessageInfo<PublishEntityCommand>,
): Promise<void> {
//....
}
}