Two plugins, one for ServiceStack and one for IdentityServer3 that allow Vault to be used as a common Client Secret data store.
- IdentityServer3.Contrib.Vault.ClientSecretStore - Contains a plugin to retrieve Client and Scope secrets from Vault.
- ServiceStack.Authentication.IdentityServer.Vault - Contains the IdentityServerVaultAuthFeature which is just an extension of the IdentityServerAuthFeature plugin
IdentityServer, in order to authenticate a Client (where a Client is an API / Website / Web app etc) requires a Client Id and a Client Secret be passed from the Client to the Identity Server instance. Both the Client and the IdentityServer instance need to have access to these secrets but also must keep these secrets safe. The Identity Server instance must be able to confirm that the Client Secret it receives is valid for the Client Id. Vault will act as a data store for these Client Secrets where both the Client and IdentityServer instance can request these secrets. This removes the responsiblity of keeping these secrets safe, and in sync from the Client and IdentityServer to Vault.
Vault is assumed to have been setup with the following backends:
In addition, the following vault configurations must have been set up:
-
AppId/UserId tokens have been created for Service Stack Client
- Create AppId
- Create UserId
- Map UserId to AppId
-
AppId/UserId tokens have been created for IdentityServer
- Create AppId
- Create UserId
- Map UserId to AppId
-
A Transit Secret Backend Encryption Key has been created for the Client Id
See here for information on how to configure Vault for the steps below:
First add the following package to the Identity Server instance:
PM> Install-Package IdentityServer3.Contrib.Vault.ClientSecretStore
Add the following to the OWIN startup class of your IdentityServer instance:
public void Configuration(IAppBuilder app)
{
var factory = new IdentityServerServiceFactory()
...
factory.RegisterClientDataStore(new Registration<IClientDataStore>(resolver =>
new InMemoryClientDataStore(Clients.Get())) // Replace with implementation of IClientDataStore
); // NOTE!!!: Not IClientStore
factory.RegisterScopeDataStore(new Registration<IScopeDataStore>(resolver =>
new InMemoryScopeDataStore(Scopes.Get())) // Replace with implementation of IScopeDataStore
); // NOTE!!!: Not IScopeStore
factory.AddVaultClientSecretStore(
new VaultClientSecretStoreAppIdOptions
{
VaultUrl = "http://127.0.0.1:8200", // The ip address and port that vault is on
AppId = "146a3d05-2042-4855-93ba-1b122e70eb6d", // The IdentityServer AppId for AppId Authentication Backend
UserId = "976c1095-a7b4-4b6f-8cd8-d71d860c6a31" // The IdentityServer UserId for AppId Authentication Backend
});
...
}
Add the following package to the ServiceStack instance:
PM> Install-Package IdentityServer3.Contrib.Vault.ClientSecretStore
Add the following to your AppHost:
public class AppHost : AppSelfHostBase
{
...
public override void Configure(Container container)
{
...
AppSettings.SetUserAuthProvider() // Service Stack Identity Server Configuraiton
.SetAuthRealm("http://localhost:5000/")
.SetClientId("clientid ....")
.SetScopes("openid etc ....");
Plugins.Add(new IdentityServerAuthFeature());
Plugins.Add(new IdentityServerVaultAuthFeature // IdentityServer Vault Secret Store
{
VaultAppId = "f8a5a40f-ecd9-43da-a009-82f180e1ef84", // The IdentityServer AppId for AppId Authentication Backend
VaultUserId = "27ded1df-7aca-40ba-a825-cc9bf5cb7f88" // The IdentityServer UserId for AppId Authentication Backend
});
...
}
}
Provided all the external configuration is correct that should be enough to use Vault as the Client Secret Store.
Both IdentityServer and our Client require access to Vault to retrieve a secret. Vault supports a number of methods of authentication using an Auth Backend. Possible methods of authentication are:
- AppId - a unique combination of both an App ID and a User ID token. Both must be unique and the combination of the two determines the privileges in Vault.
- GitHub - authentication is done via a GitHub Api Token
- LDAP - ldap user & password credentials
- Token - a single token to authenticate
- Username / Password - a username and password combination Depending on the Auth Backend chosen, there may be configuration required to "mount the backend".
When a Client authenticates against Vault they are issued with a Vault Auth token which must be in the http request header of any subsequent requests (the "X-Vault-Token" header).
Currently only the AppId is supported but further backends may be supported at a later date.
It is assumed that a list of secrets has been created and stored in Vault for the Client. The Client will retrieve one of these secrets for passing to IdentityServer.
Note: Storing a list of secrets and selecting one at random allows secrets to be rotated / added / removed by an external secret management process that would in turn make it harder to guess and reduces the time in which a secret is valid (replay attacks etc).
Vault provides a generic secret backend that can be used to store the secrets for the client using an api endpoint e.g. /secret/services/client-secrets/0cb13ff8-184e-42f7-9c89-76515df1fee0 Where the GUID is the Client ID for the service. IdentityServer will know where to retrieve the Client Secret from as it is given the Client ID as part of the request.
As the Client will be transmitting the Client Secret out into the wild, the Vault Transit Backend can be used to encrypt the Client Secret such that IdentityServer can also use Vault to decrypt the Client Secret. Neither the Client(s) nor IdentityServer need to worry about sharing encryption / decryption libraries and public / private keys.
An external process would be responsible for creating a transit key endpoint that can be used by both the Client and IdentityServer so that the Client would encrypt the secret using the api endpoint: /transit/encrypt/0cb13ff8-184e-42f7-9c89-76515df1fee0
IdentityServer would be able to decrypt the secret using the api endpoint: /transit/decrypt/0cb13ff8-184e-42f7-9c89-76515df1fee0
Where the GUID is the Client ID. IdentityServer will know how to decrypt as it is given the Client ID is part of the request.
When the Client authenticates with IdentityServer, it passes the Client ID and the encrypted Client Secret.
IdentityServer provides an IClientStore and an IScopeStore interface for retrieving Client and Scope data, including the secrets for these.
The IdentityServer plugin injects new versions of these, IClientDataStore and IScopeDataStore which are workarounds for the IdentityServer IOC Container.
IdentityServer validates the Client Secret using the following steps:
- Decrypt the Client Secret using the Vault Transit Backend
- Retrieve the list of Secrets stored for the Client
- Compare the decrypted secret against the list of secrets If IdentityServer is able to match the decrypted secret against the list of secrets from Vault then it authenticated the Client Secret. It would then continue as normal with client / scope validation etc.