Skelpo User Manager Service
The Skelpo User Service is an application micro-service written using Swift and Vapor, a server side Swift framework. This micro-service can be integrated into most applications to handle the app's users and authentication. It is designed to be easily customizable, whether that is adding additional data points to the user, getting more data with the authentication payload, or creating more routes.
Please note: This is not a project that you should just use out-of-the-box. Rather it should serve as a template in your own custom micro-service infrastructure. You may be able to use the service as is for your project, but by no means expect it to cover everything you want by default.
Clone down the repo and create a MySQL database called
~$ mysql -u root -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 138 Server version: 5.7.21 Homebrew Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> CREATE DATABASE service_users;
Any of the values for the database configuration can be modified as desired.
The configuration for the database use environment variables for the credentials, name, and host of the database:
DATABASE_HOSTNAME: The host of the service. If you are running MySQL locally, this will most likely be
DATABASE_USER: The owner of the database, most likely
rootor your user.
DATABASE_PASSWORDThe password for the database. If you don't have a password for the database, you don't need to create this env var.
DATABASE_DB: The name of the database.
The names of the environment variables are the same ones used by Vapor Cloud, so you should be able to connect to a hosted database without an issue.
You will also need to create an environment variable named
JWT_SECRET with the
n value of the JWK to verify access tokens. This service also signs the access tokens, so you will need another environment variable (called
USER_JWT_D by default, but that can be changed) that contains the
d value of the JWK.
This service uses SendGrid to send account verification and password reset emails. The service accesses your API key through another environment variable called
SENDGRID_API_KEY. Set that and you should be good to go.
You can run the service and access its routes through localhost!
There are few other things to note when configuring the service:
There is a global constant called
configure.swiftfile. By default, it is set to
false, which means the user can login and start using the service right away. If you set it to
true, it requires the user to confirm with an email that is sent to them before they can authenticate with the service.
There is a
JWTDataConfigservice registered in the
configure(_:_:_:_)function. The objects stored in this service (of type
RemoteDataClient) are used to get data outside the service that needs to be stored in the access token payload.
filtersproperty is an array of the keys to sub-objects in the JSON returned from the remote service. This works with arrays, so if you have an array of objets, all with an
idvalue, you can use
["id"], and get an array of the
The authentication allowed by this configuration is a bit constrained at this time. It uses an access token generated by the User Service to authenticate with other services, so you can only authenticate with services that use your User Service. The access token that is passed through will only ever contain the basic payload and then will have the additional data added before being returned from the service's authentication route.
When the data is retrieved from the outside service, the JSON is added to the access token payload with the JSON value fetch as the value and the service name as the key.
To authenticate in a separate service a request that contains an access token from the user service, you will need to setup the JWTVapor package in your project. You can reference the code the register it in the User service configuration if you need help setting it up.
Once you have the JWTVapor provider configured, you will need to add a middleware to your protected routes to verify the access token. If you just want to verify the access token and get the payload, the JWTMIddleware package's
JWTVerificationMiddleware can be used. The JWTMiddleware package also adds a
.payload helper method to the
Request class so you can get the access token's payload.
The other option for middleware is the
JWTAuthenticatableMiddleware that also comes with the JWTMiddleware package. It handles authentication with a certain type (i.e.
User) that acts as an owner for the rest of the service's models.
To add custom routes to your user service, create a controller in the
Sources/App/Controllers directory. You can make it a
RouteCollection, or have the route registration work some other way. After you have created all your routes, you can register it in
Sources/Configuration/router.swift to the
router object passed into the
routes(_:) function. If you want the routes to be protected so the client needs to have an access token, You can create a route group with
The routes for the service all start with a wildcard path element. This allows you to run multiple different versions of your API (with paths
/v2/..., etc.) on any given cloud provider using a load balancer to figure out where to send the request to so we get the proper API version, while at the same time letting us ignore the version number. We don't need to know if it is correct or not. The load balancer takes care of that.
You can add additional properties to the
User model if you want, though if the service is already running, they will have to be optional (unless you want to set the values of the rows in the database, but that is beyond the scope of this document).
Add the property to the model. If you want it to be in the user JSON, then add it to the
UserResponse struct also.
user database table is connected to another table, called
attributes. An attribute's row contains the ID of the user that owns it, a key that is unique to the user, and a value stored as a string.
When working in the service, if you want to interact with a user's attributes, there are several methods available:
/// Create a query that gets all the attributes belonging to a user. func attributes(on connection: DatabaseConnectable)throws -> QueryBuilder<Attribute, Attribute> /// Creates a dictionary where the key is the attribute's key and the value is the attribute's text. func attributesMap(on connection: DatabaseConnectable)throws -> Future<[String:String]> /// Creates a profile attribute for the user. /// /// - parameters: /// - key: A public identifier for the attribute. /// - text: The value of the attribute. func createAttribute(_ key: String, text: String, on connection: DatabaseConnectable)throws -> Future<Attribute> /// Removed the attribute from the user based on its key. func removeAttribute(key: String, on connection: DatabaseConnectable)throws -> Future<Void> /// Remove the attribute from the user based on its database ID. func removeAttribute(id: Int, on connection: DatabaseConnectable)throws -> Future<Void>
If you want to access the user's attributes through an API endpoint, there are the following routes available:
GET /*/users/attributes: Gets all the attributes of the currently authenticated user. This route does not require any parameters.
POST /*/users/attributesCreates a new attribute, or sets the value of an existing value.
This route requires
attributeKeyparameters in the request body. If an attribute already exists with the key passed in, then the value will be changed to the one passed in. Otherwise, a new attribute will be created.
DELETE /*/users/attributes: Deletes an attribute from a user.
This route requires either an
attributeKeyparameter in the request body to identify the attribute to delete.
The easiest way to start the service is by using the Dockerfile. The following command could be used as it is, we highly recommend however that you customize your setup to your needs.
# Note that you still need to setup the database before this step. docker build . -t users docker run -e JWT_SECRET='n-value-from-jwt' \ -e DATABASE_HOSTNAME='localhost' \ -e DATABASE_USER='users_service' \ -e DATABASE_PASSWORD='users_service' \ -e DATABASE_DB='users_service' \ -e USER_JWT_D='d-value-from-jwt' -p 8080:8080 users
If you want to run the server without docker the following summary of ENV variables that are needed may be helpful:
export JWT_SECRET='n-value-from-jwt' export DATABASE_HOSTNAME='localhost' export DATABASE_USER='users_service' export DATABASE_PASSWORD='users_service' export DATABASE_DB='users_service' export USER_JWT_D='d-value-from-jwt'
More Information about JWT and JWKS
You'll notice that this project uses JWT and JWKS for authentication. If you are unfamiliar with the concept the following two links will provide you with an overview:
Todo & Roadmap
The following features will come at some point soon:
- Admin-Functions: List of all users, Editing of other users based on permission level, Adding new users as an admin
- (open for suggestions)
Contribution & License
This project is published under MIT and is free for any kind of use. Contributions in forms of PRs are more than welcome, please keep the following simple rules:
- Keep it generic: This user manager should be equally usable for shops, blogs, apps, websites, enterprise systems or any other user system.
- Less is more: There are many features that could be added. Most of them are better suited for additional services though (like e.g. customer related fields etc.)
- Improvements are always great and so are alternatives: If you want this to run with MongoDB, Elastic or anything else - by all means feel free to contribute. Improvements are always great!
Help us keep the lights on
0.1.5 - Jul 13, 2018
The Name Doesn't Always Equal the Use
Updated JWTVapor version to 0.11.1, to use more appropriately named JWT env vars on configuration.
0.1.4 - Jul 9, 2018
Permission to Restriction
JWTMIddleware 0.8.0, replacing
0.1.3 - Jun 19, 2018
Breaking Changes! Fluent 3 RC-3!
QueryBuilder's first generic type is now required to be a database type, not a model type. For some reason you have to manually conform enum types to
ReflectDecodable now, so that is taken care of. And lastly, filtering to longer requires a
try statement, so those where removed.
0.1.2 - May 9, 2018
To Sync or Not to Sync?
You should only use
syncGet for small pieces of data and only once in a route handler. I broke this rule and was caught (almost) red-handed. I have made amends for this.
0.1.1 - May 9, 2018
The Patch to Start all Patches
Use a lot a environment variables and fix the content of the SendGrid email.