| title | Providers |
|---|---|
| permalink | /providers |
Providers are used to fetch account resources from their persistence layer or source, such as a third party API or a storage layer like MongoDB, Redis, or MySQL, for instance.
There are a couple built-in providers that you can take advantage of, or you can use a service as your provider.
The built-in providers allow you to use configuration instead of actually coding your own solutions.
Each built-in provider packaged with this bundle allows you configure an option for logging a user
out when the password changes. To use this option, specify change_password_logout: true
inside your provider config.
The In-Memory provider allows you to use your config file as the persistence layer. The username, password, and user roles are all configured inside the provider configuration file, so you do not need any other database or file storage.
security:
providers:
admin_memory_provider:
memory:
users:
super_admin:
password: super-admin-password
roles: [ROLE_USER, ROLE_ADMIN, ROLE_SUPER_ADMIN]
admin:
password: admin-password
roles: [ROLE_USER, ROLE_ADMIN]
front_memory_provider:
memory:
users:
moderator:
password: moderator-password
roles: [ROLE_USER, ROLE_MODERATOR]
In the above example, you can see the password is specified in plain text. You would do this if
you are using the 'text' encryptor. Although, it's advised that you use a salt / pepper, and base64
encode it, if this is your approach. The username is the object key, super_admin, admin and
moderator, in this case.
You can specify as many groups of in-memory users as you want, and you can specify as many users in each group as you want. You might separate them into groups so that you can take advantage of provider chains, but you can very well just put all users into one group.
You are not restricted to using the text encryptor. You can use any registered encryptor that you
wish. The catch is that you need to encrypt the password yourself and store the encrypted value in
the config file. You can use the conga security:encrypt command to do this.
The users that get resolved from the In-Memory provider will be an instance of
@conga/framework-security:security/user/InMemoryUser. This is so that you can configure the
in-memory user's encryption strategy separately from other user instances. Below is an example
using the bcrypt encryptor with the InMemoryUser.
security:
encryption:
# the object key, 'in_memory_user', can be any name you want
in_memory_user:
# the path to the user class (this must point to InMemoryUser)
path: @conga/framework-security:security/user/InMemoryUser
# the algorithm you want to use
algorithm: bcrypt
# bcrypt salt-rounds config
saltRounds: 50
# application-wide pepper
pepper: abc123!=!321CBA
You can have as many users with different roles and passwords as you wish, but they have to use the same encryption strategy, as they are all of the same InMemoryUser class.
After you set up your encryptor, you can generate an encrypted password with the following example:
conga security:encrypt --path @conga/framework-security:security/user/InMemoryUser --value admin-password
Then copy / paste this password under your user's password property, in your security configuration.
The bass provider allows you to use any bass adapter as your persistence layer. Each collection (or database table) will have its own document (model), and so each separate user type you want to configure should have its own encryption strategy defined.
The bass provider requires that you specify the login and secret field on your document, so that the provider knows how to access them. Currently, methods are not supported, so provide the public property name so the provider can reference them correctly.
security:
# these are examples, you can use any variation you want
encryption:
administrator:
path: project-bundle:model/Administrator
algorithm: bcrypt
saltRounds: 50
pepper: TYHJK4%^&238765rtghydjk # application-wide pepper
user:
path: project-bundle:model/User
algorithm: sha512
pepper: KJHGTYU45678^%$aSFFAf! # application-wide pepper
fields:
salt: encryptionSalt # grab the salt from the User document so each user has a unique salt!
secret: encryptionSecret # grab the hmac secret from the User document so each user has a uniuqe secret!
providers:
admin_user_provider:
bass:
document: project-bundle:model/Administrator # the document path
login: username # the login / username field in the document
secret: password # the password field in the document
change_password_logout: true # make sure users are logged out on password change
website_user_provider:
bass:
document: project-bundle:model/User # the document path
login: email # the login / username field in the document
secret: password # the password field in the document
change_password_logout: true # make sure users are logged out on password change
In the above configuration, you can see that we have configured two different documents, each with their own encryption strategy.
The storage engine / persistence layer can now be managed from within your bass configuration, for whichever adapters these documents are registered under (you might have more than one!).
The encrypted passwords live in the row matching the login / secret for each document, inside the database. For that reason, among so many others, it's important that your login / secret combination is unique for each user.
The chain provider simply allows you to chain multiple providers together. During authentication, it will try each provider in the chain, one by one, in series, until it finds one that it can use, or until it has tried them all.
security:
providers:
# chain the adminstrator access providers together
admin_chain: [admin_memory_provider, admin_user_provider]
# chain the front-end access providers together
frontend_chain: [front_memory_provider, website_user_provider, my_provider]
# chain everything together (uses nested chains!)
chain: [admin_chain, frontend_chain]
You can use your own service as your provider, if you wish. The only catch is that your service
must be an instance of a class that inherits from
@conga/framework-security:security/provider/AbstractProvider.
const { AbstractProvider } = require('@conga/framework-security').Provider;
class MyProviderService extends AbstractProvider {
/**
* Our example injects the service container
* @param {Container} container The service container
**/
constructor(container) {
this.container = container;
}
/**
* See if this provider supports a given resource
* @param {AuthResource|*} resource The resource to check for
* @returns {boolean}
*/
supportsResource(resource) {
// you must implement this method
}
/**
* Get the resource that needs to be authenticated
* @param {*} credentials The secret password / api key, etc.
* @returns {Promise}
*/
getResource(credentials) {
// you must implement this method
}
/**
* Refresh an existing resource
* @param {AuthResource|*} resource
* @returns {Promise}
*/
refreshResource(resource) {
// you must implement this method
}
}
module.exports = MyProviderService;
services:
my_provider_service:
constructor: my-bundle:service/MyProviderService
arguments: ['@service_container']
security:
providers:
my_provider: '@my_provider_service'
If you want to provide your own configurable provider, like the in-memory provider, or the bass provider, you can do that. You will need to register your provider with a service tag, and provide a method that will be used to check if your provider matches the security configuration.
You might want to do something like this if you are building a bundle for others to use!
You need to create a provider config class that extends AbstractProviderConfig, and tag it in your service definition, so that conga-security can register it.
const AbstractProviderConfig = require('conga-security').Provider.AbstractProviderConfig;
const MyProvider = require('./provider');
class MyProviderConfig extends AbstractProviderConfig {
constructor(container) {
// if you need it, you can inject the container into your provider configuration
this.container = container;
}
// abstract method you are required to provide
supportsConfig(config) {
// 'bundle_provider' is what we are checking for to see if our provider should be used
// you can check for ANYTHING that can be configured in your config
return config.bundle_provider instanceof Object;
}
// abstract method you are required to provide
useConfig(config) {
return new MyProvider(config);
}
}
module.exports = MyProviderConfig;
You need to create a provider class that extends AbstractProvider. You don't need to register this
anywhere, your provider config class will return it when useConfig is called.
const AbstractProvider = require('conga-security').Provider.AbstractProvider;
class MyProvider extends AbstractProvider {
// abstract method you are required to provide
getResource(credentials) {
// this is where you would fetch your resource (user) from a persistence layer or API, etc.
// return a promise that resolves an AuthResource type
// this example simply returns an AuthUser, which extends AuthResource, with custom roles
const { username, password } = credentials;
return Promise.resolve(new AuthUser(username, password, ['ROLE_CUSTOM']));
}
}
module.exports = MyProvider;
You need to register your provider config with a service definition and tag it with
security.firewall.provider.
services:
bundle.provider:
constructor: my-bundle:security/provider-config
arguments: ["@service_container"]
tags:
- { name: security.firewall.provider }
Then you can use your provider by configuring it in your security section in
app/config/config.yml.
security:
firewall:
my.firewall:
route: ^/foo/bar
authenticator: my.authenticator
provider: my.provider
providers:
my.provider:
bundle_provider:
foo: asdfasdf
bar: asdfasdf