@rxstack/security
v0.8.0
Published
RxStack Security Module
Downloads
89
Readme
RxStack Security Module
The Security module provides facilities for authenticating user requests using JSON Web Token (JWT)
but also allows you to implement your own authentication strategies.
Installation
npm install @rxstack/security --save
Documentation
- Setup
- Configurations
- SecretLoader
- Token Extractors
- User Providers
- Password Encoders
- Authentication
- Working with tokens
- Local Authentication
- Token Encoder
- Token Manager
- Refresh Token Manager
Setup
Security
module needs to be configured and registered in the application
. Let's create the application:
import {Application, ApplicationOptions} from '@rxstack/core';
import { SecurityModule } from '@rxstack/security';
export const SECURITY_APP_OPTIONS: ApplicationOptions = {
imports: [
// ...
SecurityModule.configure({
local_authentication: true, // defaults to false
token_extractors: {
query_parameter: {
enabled: true,
name: 'bearer' // query string param name
},
authorization_header: {
enabled: true,
name: 'authorization', // header name
prefix: 'Bearer'
}
},
ttl: 300,
default_issuer: 'default',
secret_configurations: [
{
issuer: 'default',
secret: 'my_secret',
signature_algorithm: 'HS512',
}
]
})
],
servers: [
// ...
],
providers: [
// ...
]
};
Configurations
The module accepts the following options
local_authentication
: allows you to authenticate users with username and password, facility to refresh jwt token, authenticate via sockets and logout from the application. Defaults tofalse
.token_extractors
: extracts the token fromquery string
orheader
query_parameter
- extracts the token from query string parameterauthorization_header
-extracts the token from http header
ttl
- token validity in seconds. Defaults to300
refresh_token_ttl
- the time in seconds token could be refreshed. Default to(60 * 60 * 24)
user_identity_field
- name of the property in the decoded payload which is used to look up the user.default_issuer
- default name of the issuer, used to load the secret keysecret_configurations
- an array of secret configurations:secret
- string token orRsa
objectsignature_algorithm
- algorithm used in jwtissuer
- issuer name
For more information, please check jsonwebtoken
Secret Loader
An array of configurations are passed to secret_configurations
option. Each option creates a SecretLoader
service instance
which is responsible to load ssh keys or string secret.
You need to configure at least one secret and set it as default issuer.
SecretLoader
options:
secret
- value should be a string orRsa
objectRsa
public_key
- path to public key, used to decode the tokenprivate_key
- path to private key, used to sign the token, needed if local authentication is enabled.passphrase
- used alongside private key.
signature_algorithm
- algorithm used in jwt. Defaults toRS512
issuer
- name of the issuer, used to load the secret key
Token Extractors
Token Extractors are responsible to extract the token from Request
object. There are two build-in extractors:
HeaderTokenExtractor
Extracts the token from header, accepts the following configurations:
enabled
- whether is enabled or notprefix
- token prefix, defaults toBearer
name
- header name, default toauthorization
QueryParameterTokenExtractor
Extracts the token from query string parameter, accepts the following configurations:
enabled
- whether is enabled or notname
parameter name, default tobearer
Custom Token Extractor
You can create your own extractor by implementing TokenExtractorInterface
.
@Injectable()
export class MyCustomTokenExtractor implements TokenExtractorInterface {
static readonly EXTRACTOR_NAME = 'my_custom_extractor';
constructor(private config: SecurityConfiguration) { }
extract(request: Request): string {
// extract the token somehow
}
getName(): string {
return MyCustomTokenExtractor.EXTRACTOR_NAME;
}
}
and you need to register it in the application providers:
import {TOKEN_EXTRACTOR_REGISTRY} from '@rxstack/security';
{
// ...
providers: [
{ provide: TOKEN_EXTRACTOR_REGISTRY, useClass: MyCustomTokenExtractor, multi: true }
]
}
User Providers
UserProviderManager
service is responsible for loading the requested user from a storage. It iterates through all registered user providers
and return
the user or throws an UserNotFoundException
.
const user = await injector.get(UserProviderManager).loadUserByUsername('admin');
There are several build-in user providers but none of them are enabled by default.
UserProvider services are registered as
multi providers
usingUSER_PROVIDER_REGISTRY
.
InMemoryUserProvider
InMemoryUserProvider
allows you to load users from configurations.
import { UserInterface } from '@rxstack/core';
import { USER_PROVIDER_REGISTRY, InMemoryUserProvider } from '@rxstack/security';
// ...
providers: [
{
provide: USER_PROVIDER_REGISTRY,
useFactory: () => {
return new InMemoryUserProvider<UserInterface>(
[
{
username: 'admin',
password: 'admin',
roles: ['ROLE_ADMIN']
},
{
username: 'user',
password: 'user',
roles: ['ROLE_USER']
}
],
(data: UserInterface) => new UserWithEncoder(data.username, data.password, data.roles)
);
},
deps: [],
multi: true
},
]
const user = await injector.get(UserProviderManager).loadUserByUsername('admin');
Provider uses
UserWithEncoder
, you can read more about encoders here.
PayloadUserProvider
PayloadUserProvider
allows you to load users from token payload without using any storage.
import { UserInterface, User } from '@rxstack/core';
import { USER_PROVIDER_REGISTRY } from '@rxstack/security';
// ...
providers: [
{
provide: USER_PROVIDER_REGISTRY,
useFactory: () => {
return new PayloadProvider<UserInterface>(
(data: any) => new User(data.username, null, data.roles)
);
},
deps: [],
multi: true
},
]
const user = await injector.get(UserProviderManager)
.get('payload')
.loadUserByUsername('admin', {'username': 'admin', 'password': null,'roles': ['ROLE_ADMIN']});
Custom UserProvider
You can create a custom user provider by implementing UserProviderInterface
:
export class MyCustomUserProvider implements UserProviderInterface {
constructor(private db: Connection) { }
async loadUserByUsername(username: string, payload?: any): Promise<UserInterface> {
// load user from anywhere
const user = await this.db.findOneByUsername(username);
if (!user) throw new UserNotFoundException(username);
return user;
}
// unique provider name
getName(): string {
return 'my-custom-provider';
}
}
then register it in the application providers:
providers: [
{
provide: USER_PROVIDER_REGISTRY,
useFactory: (db: Connection) => {
return new MyCustomUserProvider(db);
},
deps: [Connection],
multi: true
},
]
And we're done.
Password Encoders
EncoderFactory
service is responsible to retrieve a registered password encoders by user or encoder name.
const user: UserInterface;
// by user
const encoder: PasswordEncoderInterface = injector.get(EncoderFactory).getEncoder(user);
// by encoder name
const encoder: PasswordEncoderInterface = injector.get(EncoderFactory).get('some-encoder');
When getting the encoder by user then BcryptPasswordEncoder
is used by default.
To change that you need to implement EncoderAwareInterface
in the user class:
import {EncoderAwareInterface, User, PlainTextPasswordEncoder} from '@rxstack/security';
export class UserWithEncoder extends User implements EncoderAwareInterface {
getEncoderName(): string {
return PlainTextPasswordEncoder.ENCODER_NAME;
}
}
From now on UserWithEncoder
will use PlainTextPasswordEncoder
;
Build-in encoders
There are two build-in encodes both enabled by default:
BcryptPasswordEncoder
: uses bcrypt and it is the default encoder.PlainTextPasswordEncoder
: does not encode anything, it just returns the value as it is.
Usage
All encoder methods are asynchronous. Let's see how we can use them:
const encoder; // .. get it from somewhere
// encodes the plain password
const encodedPassword: string = await encoder.encodePassword('my-password');
// compare encoded password against the plain one
const isPasswordValid: boolean = await encoder.isPasswordValid(encodedPassword, 'my-password');
Custom encoders
You can easily create a custom encoder by implementing PasswordEncoderInterface
:
import {Injectable} from 'injection-js';
import {PasswordEncoderInterface} from '@rxstack/security';
@Injectable()
export class MyEncoder implements PasswordEncoderInterface {
static readonly ENCODER_NAME = 'my-encoder';
async encodePassword(raw: string): Promise<string> {
const encrypted = '...';
return encrypted;
}
async isPasswordValid(encoded: string, raw: string): Promise<boolean> {
const decrypted = '...';
return decrypted === raw;
}
getName(): string {
return MyEncoder.ENCODER_NAME;
}
}
then you need to register it in the application providers:
providers: [
{
provide: PASSWORD_ENCODER_REGISTRY,
useClass: MyEncoder,
multi: true
},
]
That's it. Now you can use your encoder.
Authentication
When a request points to a secured area TokenExtractorListener
extract the raw token from the current Request
object
then AuthenticationTokenListener
validates the given token, and returns an authenticated token if valid.
AuthenticationProviderManager
AuthenticationProviderManager
receives several authentication providers, each supporting a different type of token.
const token: TokenInterface = '...';
// returns authenticated token or throws AuthenticationException
await injector.get(AuthenticationProviderManager).authenticate(token);
You can get a registered authentication provider by name:
const tokenAuthenticationProvider = injector.get(AuthenticationProviderManager)
.get(TokenAuthenticationProvider.PROVIDER_NAME)
AuthenticationProviderManager
also dispatches two types of events:
AuthenticationEvents.AUTHENTICATION_SUCCESS
: dispatched when token is successfully authenticated.AuthenticationEvents.AUTHENTICATION_FAILURE
: dispatched only if exception is instance ofAuthenticationException
import {Observe} from '@rxstack/async-event-dispatcher';
import {AuthenticationEvents, AuthenticationEvent, AuthenticationFailureEvent} from '@rxstack/security';
import {Injectable} from 'injection-js';
@Injectable()
export class AuthListener {
@Observe(AuthenticationEvents.AUTHENTICATION_SUCCESS)
async onAuthenticationSuccess(event: AuthenticationEvent): Promise<void> {
// do something
}
@Observe(AuthenticationEvents.AUTHENTICATION_FAILURE)
async onAuthenticationFailure(event: AuthenticationFailureEvent): Promise<void> {
// do something
}
}
Make sure you register the listener in the application providers.
Authentication Providers
Each provider (since it implements AuthenticationProviderInterface) has a method support() by which
the AuthenticationProviderManager
can determine if it supports the given token.
If this is the case, the manager then calls the provider's method authenticate().
This method should return an authenticated token or throw an AuthenticationException
(or any other exception extending it).
There are two authentication providers enabled by default.
TokenAuthenticationProvider
: It will attempt to authenticate a user based on a jwt token.
// extracted jwt token
const rawToken = '...';
// construct token object
const token = new Token(rawToken);
// get token authentication provider from the manager
const tokenAuthenticationProvider: AuthenticationProviderInterface = '...';
// authenticate the token or throws exception
const authenticatedToken = await tokenAuthenticationProvider.authenticate(token);
UserPasswordAuthenticationProvider
: It will attempt to authenticate a user based on username and password.
// construct token object
const token = new UsernameAndPasswordToken('admin', 'my-password');
// get user-password authentication provider
const userPasswordAuthenticationProvider: AuthenticationProviderInterface = '...';
// authenticate the token or throws exception
const authenticatedToken = await userPasswordAuthenticationProvider.authenticate(token);
Custom Authentication Provider
Creating a custom authentication system is not an easy task, here are the steps you need to follow:
- The token represents the user authentication data present in the request. First, you'll create your token class. This will allow the passing of all relevant information to your authentication provider:
import {AbstractToken} from '@rxstack/security';
export class MyCustomToken extends AbstractToken {
constructor(private apiKey: string) {
super();
}
getUsername(): string {
return this.user ? this.user.username : null;
}
getCredentials(): string {
return this.apiKey;
}
}
The
MyCustomToken
class extendsAbstractToken
class, which provides basic token functionality. Implement theTokenInterface
on any class to use as a token.
- The authentication provider will do the verification of the
MyCustomToken
:
import {Injectable} from 'injection-js';
import {
UserProviderManager, AuthenticationProviderInterface, TokenManagerInterface
} from '@rxstack/security';
import {TokenInterface, UserInterface} from '@rxstack/core';
@Injectable()
export class MyCustomAuthenticationProvider implements AuthenticationProviderInterface {
static readonly PROVIDER_NAME = 'my-custom-provider';
constructor(private userProvider: UserProviderManager) { }
async authenticate(token: TokenInterface): Promise<TokenInterface> {
const payload = await this.getPayload(token);
const user = await this.getUserFromPayload(payload);
token.setUser(user);
token.setAuthenticated(true);
token.setFullyAuthenticated(true);
return token;
}
getName(): string {
return TokenAuthenticationProvider.PROVIDER_NAME;
}
support(token: TokenInterface): boolean {
return (token instanceof MyCustomToken);
}
private async getPayload(token: TokenInterface): Promise<Object> {
// extract somehow the payload from token
return {
// some useful information
username: "moderator"
};
}
private async getUserFromPayload(payload: Object): Promise<UserInterface> {
return this.userProvider.loadUserByUsername(payload['username'], payload);
}
}
Let's register the authentication provider in the application:
providers: [
{
provide: AUTH_PROVIDER_REGISTRY,
useFactory: (userProvider: UserProviderManager) => {
return new MyCustomAuthenticationProvider(userProvider);
},
deps: [UserProviderManager],
multi: true
},
]
Let's see it into action:
const myCustomToken = new MyCustomToken('my-api-key-extracted-from-somewhere');
const authenticateToken = await injector.get(AuthenticationProviderManager).authenticate(myCustomToken);
MyCustomToken
is supported only byMyCustomAuthenticationProvider
.
As a compete guide you can use the build-in token authentication system:
Token
: token classTokenAuthenticationProvider
: authentication provider that supportsToken
classTokenExtractorListener
: extracts the raw token and setsToken
object in theRequest
.AuthenticationTokenListener
: authenticates the token
Working with tokens
Token contains information about user and how he was authenticated. Each token implements TokenInterface
Token is available only in the
Request
object.
import {Http, Request, Response, WebSocket} from '@rxstack/core';
import {Injectable} from 'injection-js';
import {UnauthorizedException} from '@rxstack/exceptions';
@Injectable()
export class MyController {
@Http('GET', '/secured', 'secured')
@WebSocket('secured')
async securedAction(request: Request): Promise<Response> {
// only authenticated users can access it
if (!request.token.hasRole('ROLE_ADMIN')) {
throw new UnauthorizedException();
}
// user is authorized to access that action
return new Response();
}
@Http('GET', '/not-secured', 'not_secured')
@WebSocket('not_secured')
async notSecuredAction(request: Request): Promise<Response> {
// only not authenticated users can access it
if (request.token.isAuthenticated()) {
throw new UnauthorizedException();
}
// do something
return new Response();
}
}
There are different types of tokens.
AnonymousToken
: used for not authenticated users.UsernameAndPasswordToken
: used with local authenticationToken
: used with api token
There are few important methods:
getUser()
- retrieves the current user ornull
isAuthenticated()
- whether user is authenticated or notisFullyAuthenticated()
- when token expires then it can be refreshed, after refreshed, user is not fully authenticated any more. It is useful for specific actions like changing password or payments. In that case you can force the user to re-authenticate again.getRoles()
- retrieves user roleshasRole('ROLE_MODERATOR')
- checks whether user has a specific rolegetUsername()
- retrieves the username of current user ornull
Local Authentication
If enabled it allows users to authenticate via HTTP
using username
and password
.
Under the hood it uses SecurityController
which has the following actions:
SecurityController.loginAction
Allows user to generate a token via HTTP
using username
and password
.
Using CURL
curl -X POST \
http://localhost:3000/security/login \
-H 'accept: application/json' \
-H 'content-type: application/json' \
-d '{
"username": "admin",
"password": "admin"
}'
On success with status code 200:
{
token: 'generated-token',
refreshToken: 'c16fefba1911e414762bb66372bc4bbc'
}
An
AuthenticationEvents.LOGIN_SUCCESS
will be dispatched.
On failure status code 401
SecurityController.refreshTokenAction
Allows user to refresh the token via HTTP
.
Using CURL
curl -X POST \
http://localhost:3000/security/refresh-token \
-H 'accept: application/json' \
-H 'content-type: application/json' \
-d '{
"refreshToken": "c16fefba1911e414762bb66372bc4bbc"
}'
On success will return the same response as loginAction
.
An
AuthenticationEvents.REFRESH_TOKEN_SUCCESS
will be dispatched.
On failure status code 401 (token is expired) or 404 (token not found)
SecurityController.logoutAction
Allows user to invalidate the refresh token
Using CURL
curl -X POST \
http://localhost:3000/security/logout \
-H 'accept: application/json' \
-d '{
"refreshToken": "c16fefba1911e414762bb66372bc4bbc"
}'
If refreshToken
is found then it is disabled and status code 204 is returned, otherwise status code 404
An
AuthenticationEvents.LOGOUT_SUCCESS
will be dispatched.
SecurityController.authenticateAction
Allows users to authenticate via WebSocket
.
Once user is connected to the socket server he can authenticate:
const io = require('socket.io-client');
const defaultNs = io('http://localhost:4000');
defaultNs.emit('security_authenticate', {'params': {'bearer': 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9......'}}, function (response: any) {
// should return status code 204 (on success) or 401 (on failure)
});
An
AuthenticationEvents.SOCKET_AUTHENTICATION_SUCCESS
will be dispatched.
SecurityController.unauthenticateAction
Allows user to unauthenticate via WebSocket
. It will destroy token in the Request
object.
const io = require('socket.io-client');
const defaultNs = io('http://localhost:4000');
defaultNs.emit('security_unauthenticate', null, function (response: any) {
// should return status code 204 (on success) or 401 (on failure)
});
It should return status code 204
or 403
on failure (id user was not previously authenticated)
An
AuthenticationEvents.SOCKET_UNAUTHENTICATION_SUCCESS
will be dispatched.
Local Authentication Events:
Each Security
controller action dispatches an event. Here is a event listener example:
import {AuthenticationEvents, AuthenticationRequestEvent} from '@rxstack/security';
import {Observe} from '@rxstack/async-event-dispatcher';
import {Injectable} from 'injection-js';
@Injectable()
export class AuthListener {
@Observe(AuthenticationEvents.LOGIN_SUCCESS)
async onLogin(event: AuthenticationRequestEvent): Promise<void> {
// do something
}
@Observe(AuthenticationEvents.LOGOUT_SUCCESS)
async onLogout(event: AuthenticationRequestEvent): Promise<void> {
// do something
}
@Observe(AuthenticationEvents.REFRESH_TOKEN_SUCCESS)
async onRefreshToken(event: AuthenticationRequestEvent): Promise<void> {
// do something
}
@Observe(AuthenticationEvents.SOCKET_AUTHENTICATION_SUCCESS)
async onSocketAuthentication(event: AuthenticationRequestEvent): Promise<void> {
// do something
}
@Observe(AuthenticationEvents.SOCKET_UNAUTHENTICATION_SUCCESS)
async onSocketUnAuthentication(event: AuthenticationRequestEvent): Promise<void> {
// do something
}
}
Make sure you register the listener in the application providers.
Token Encoder
TokenEncoder
service is responsible for encoding and decoding the JSON Web Token (JWT).
Under the hood it uses jsonwebtoken.
There are two async methods:
encode(payload)
- encodes provided payloaddecode(encodedToken)
- decodes the encoded token
If you want to replace JWT with any other token based authentication then you should create your own token encoder and replace the current one.
import {TokenEncoderInterface} from '@rxstack/security';
import {Injectable} from 'injection-js';
@Injectable()
export class MyTokenEncoder implements TokenEncoderInterface {
async encode(payload: Object): Promise<string> {
return 'encoded-token';
}
async decode(token: string): Promise<Object> {
return 'decoded-token';
}
}
then you need to register it in the application providers. The new one will replace the old one:
import {TOKEN_ENCODER} from '@rxstack/security';
providers: [
{
provide: TOKEN_ENCODER,
useClass: MyTokenEncoder
}
]
Token Manager
TokenManager
service is responsible to create a token from UserInterface
object and decode a token.
There are two async methods:
create(user)
- creates a token fromUser
objectdecode(encodedToken)
- decodes the encoded token
TokenManager
dispatches several events while creating and decoding.
the security.token.created
event
the event is used to modify the payload before encoded.
import {AsyncEventDispatcher} from '@rxstack/async-event-dispatcher';
import {TokenManagerEvents, TokenPayloadEvent} from '@rxstack/security';
// get dispatcher
const dispatcher: AsyncEventDispatcher;
dispatcher.addListener(TokenManagerEvents.TOKEN_CREATED, async (event: TokenPayloadEvent): Promise<void> => {
const user = event.user;
// add a new property to the payload
event.payload['new_prop'] = 'value';
});
The security.token.encoded
event
the event is used to replace the generated token.
import {AsyncEventDispatcher} from '@rxstack/async-event-dispatcher';
import {TokenManagerEvents, TokenEncodedEvent} from '@rxstack/security';
// get dispatcher
const dispatcher: AsyncEventDispatcher;
dispatcher.addListener(TokenManagerEvents.TOKEN_ENCODED, async (event: TokenEncodedEvent): Promise<void> => {
const user = event.user;
event.rawToken = 'new token';
});
The security.token.decoded
event
the event is used to modify the payload or mark token as invalid.
import {AsyncEventDispatcher} from '@rxstack/async-event-dispatcher';
import {TokenManagerEvents, TokenDecodedEvent} from '@rxstack/security';
// get dispatcher
const dispatcher: AsyncEventDispatcher;
dispatcher.addListener(TokenManagerEvents.TOKEN_DECODED, async (event: TokenDecodedEvent): Promise<void> => {
event.payload['new_prop'] = 'new value';
// this will mark token as invalid and stop propagation to other listeners
event.markAsInvalid();
});
If you want to replace TokenManager
then you should create a service which implements TokenManagerInterface
and replace the current one.
Note: check implementation of
TokenManager
and how events are dispatched
import {TokenManagerInterface} from '@rxstack/security';
import {Injectable} from 'injection-js';
@Injectable()
export class MyTokenManager implements TokenManagerInterface {
async create(user: UserInterface): Promise<string> {
// dispatch the security.token.created event
// dispatch the security.token.encoded event
return 'encoded-token';
}
async decode(token: string): Promise<Object> {
// dispatch the security.token.decoded event
return 'decoded-token';
}
}
then you need to register it in the application providers. The new one will replace the old one:
import {TOKEN_MANAGER} from '@rxstack/security';
providers: [
{
provide: TOKEN_MANAGER,
useClass: MyTokenManager
}
]
Refresh Token Manager
RefreshTokenManager
is responsible for refreshing and validating the actual token by passing an unique identifier.
By default InMemoryRefreshTokenManager
is enabled but it has some drawbacks because it stores keys in memory
of the application instance. You can easily replace it with your own which implement redis for example.
import {AbstractRefreshTokenManager, RefreshTokenInterface} from '@rxstack/security';
import {Injectable} from 'injection-js';
@Injectable()
export class MyRefreshTokenManager extends AbstractRefreshTokenManager {
async persist(refreshToken: RefreshTokenInterface): Promise<RefreshTokenInterface> {
// persist token in the storage
}
async get(identifier: string): Promise<RefreshTokenInterface> {
// retrieve the token from the storage
}
async clear(): Promise<void> {
// removes all persisted tokens
}
}
then you need to register it in the application providers. The new one will replace the old one:
import {REFRESH_TOKEN_MANAGER} from '@rxstack/security';
providers: [
{
provide: REFRESH_TOKEN_MANAGER,
useClass: MyRefreshTokenManager
}
]
License
Licensed under the MIT license.