Giter VIP home page Giter VIP logo

usermanager's Introduction

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.

Getting Started

MySQL

Clone down the repo and create a MySQL database called service_users:

~$ 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 localhost.
  • DATABASE_USER: The owner of the database, most likely root or your user.
  • DATABASE_PASSWORD The 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.

JWT

You will also need to create an environment variable named JWT_PUBLIC 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 JWT_SECRET by default, but that can be changed) that contains the d value of the JWK.

Email

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!

Additional Configuration

There are few other things to note when configuring the service:

  1. There is a global constant called emailConfirmation in the configure.swift file. 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.

  2. There is a JWTDataConfig service 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.

    The filters property 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 id value, you can use ["id"], and get an array of the ids.

    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.

Authentication

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.

Routes

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 JWTAuthenticatableMiddlware<User> middleware.

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 /v1/..., /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.

User

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.

Attributes

The 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/attributes Creates a new attribute, or sets the value of an existing value.

    This route requires attributeText and attributeKey parameters 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 attributeId or attributeKey parameter in the request body to identify the attribute to delete.

Easy Start

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_PUBLIC='n-value-from-jwt' \
-e DATABASE_HOSTNAME='localhost' \
-e DATABASE_USER='users_service' \
-e DATABASE_PASSWORD='users_service' \
-e DATABASE_DB='users_service' \
-e JWT_SECRET='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_PUBLIC='n-value-from-jwt'
export DATABASE_HOSTNAME='localhost'
export DATABASE_USER='users_service'
export DATABASE_PASSWORD='users_service'
export DATABASE_DB='users_service' 
export JWT_SECRET='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!

usermanager's People

Contributors

calebkleveter avatar dean151 avatar frivas avatar grundoon avatar proggeramlug avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

usermanager's Issues

PEM_read_bio_PrivateKey - Unexpectedly found nil while unwrapping an Optional value

Hi,

Could you please help with generating right JWT_PUBLIC and JWT_SECRET?

I'm generating RSA key:
openssl genrsa -out rsa-1024.pem 1024

Then I'm using script from here - #12 and getting N and D

N:
4BuKaDkZohgQLHs0UnWslEP6K7hv9woHOP7jLN/c+YVVcsZjBVQ6IdkLEm6L9E+bENDSAdyVNdQOAAz6gcsSoQzmvNQ5HFpqdnOUH0Qnv4o0fQiAJUalO7y5O0ON9nHvKQRaOnifSPlg9l7ZDwNttz5w7K+FVkmNqHKrtmSVkQU=
D:
AJDffOPPNdDheWMn6jT8OSpHn2uv+UFbzDt+GmGh/fmXxhtCQDTrtAC5jDzIFdVDuNxwAbKOZC69/KbvvOk227zmxtlWbZeFUptwd/bEHbID061E27bQ6VOQl63kCb3WO4f5efXxNvLvxI/fFJX9t9ICJC73n1C+wteXIOzN0Cep

But it doesn't work like that.

image

Build Error

AuthController.swift:142:83: error: use of unresolved identifier 'signer'
let refreshJWT = try JWT(from: refreshToken, verifiedUsing: signer.signer)
screen shot 2019-01-09 at 1 26 21 pm

Admin-Functions

We need functions that are only available to admin users. These include:

  • A list of all users with all their attributes
  • Edit a user and all their attributes
  • Add a new user

Sendgrid integration not quite functional

@calebkleveter test with this send grid key: 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

If you need anything else let me know. Also make sure it uses the localizations properly.

Note: I've made the emailFrom & emailUrl come from the environment variables as well.

GPDR Routes

Creates routes where a user can view and delete all their data:

GET /user/data
DELETE /user/data

Not clear how register (and login) works

Hey @proggeramlug, I tried to walk through register and login method implementations and came up with some questions:

  1. why register returns UserSuccessResponse which doesn't contain any session information (JWT token)
  2. why login requires a request to be authenticated? usually in mobile apps client sends username/pass to get a JWT token.

Run production-tests

We need to be sure this actually does what is says.

I plan to run some local tests and the replace a vapor-2 instance of this service that is used in production. Once this is confirmed we can go public.

Errors are not very informative

When sending an empty POST request to */users/register I'm getting:

{
  "error": true,
  "reason": "Value of type 'String' required for key ''."
}

Make User registraion public/private

A setting (like the emailconfirmation one) shoul indicate whether or not the public can register themselves. If so the users/register route needs to be protected, if not it can be public.

Allow other services to integrate

Just like in the vapor 2 version, we need the option for other services like teams, billing etc. to infiltrate their information into the JWT token.

Update Documentation for Vapor 3

The README documentation is outdated, with instructions on the JSON config files and other things that have been removed in Vapor 3. The README should be updated for the new platform.

Swift Tools

Hi,

I have been trouble getting the docker build command to work. I am repeatedly receiving the error /root/vapor: error: package at '/root/vapor' requires a minimum Swift tools version of 5.0.0 (currently 4.2.0). I upgraded to the latest Xcode 11 Beta and selected the latest tools using sudo xcode-select -s /Applications/Xcode-beta.app/Contents/Developer. Running swiftenv I can see that the selected swift tools are 5.0 at the repository.

Any help would be greatly appreciated!

I ran the following command replacing the JWT_PUBLIC and JWT_SECRET values with generated rsa public and private keys,

docker build . -t users
docker run -e JWT_PUBLIC='n-value-from-jwt' \
-e DATABASE_HOSTNAME='localhost' \
-e DATABASE_USER='users_service' \
-e DATABASE_PASSWORD='users_service' \
-e DATABASE_DB='users_service' \
-e JWT_SECRET='d-value-from-jwt' -p 8080:8080 users

Here are the logs from running the getting started command.

Sending build context to Docker daemon  120.8MB
Step 1/25 : FROM swift:4.2
 ---> ce1fe933c297
Step 2/25 : ARG ENVIRONMENT
 ---> Using cache
 ---> 52c9704beb14
Step 3/25 : ENV ENVIRONMENT ${ENVIRONMENT:-production}
 ---> Using cache
 ---> 525c39d43606
Step 4/25 : ENV DEBIAN_FRONTEND noninteractive
 ---> Using cache
 ---> 6a1dcf17f6cf
Step 5/25 : ENV TZ=Europe/Berlin
 ---> Using cache
 ---> be3ab5a1e6fc
Step 6/25 : ENV TERM xterm
 ---> Using cache
 ---> 509a1f6fd08d
Step 7/25 : RUN apt-get update && apt-get -y install wget lsb-release apt-transport-https
 ---> Using cache
 ---> 1119f8522a86
Step 8/25 : RUN wget -q https://repo.vapor.codes/apt/keyring.gpg -O- | apt-key add -
 ---> Using cache
 ---> 7025dc9a6736
Step 9/25 : RUN echo "deb https://repo.vapor.codes/apt $(lsb_release -sc) main" | tee /etc/apt/sources.list.d/vapor.list
 ---> Using cache
 ---> 4f2e17d46633
Step 10/25 : RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
 ---> Using cache
 ---> f2a799ef60eb
Step 11/25 : USER root
 ---> Using cache
 ---> 150ff5afb19f
Step 12/25 : RUN apt-get update && apt-get install
 ---> Using cache
 ---> e6fea8edc4c2
Step 13/25 : RUN mkdir /root/vapor
 ---> Using cache
 ---> c0b471d1d3d0
Step 14/25 : ADD . /root/vapor
 ---> Using cache
 ---> 48103aec8142
Step 15/25 : WORKDIR /root/vapor
 ---> Using cache
 ---> a0af880d3b4f
Step 16/25 : RUN cd /root/vapor && rm -rf .build
 ---> Using cache
 ---> 8c070f77c9a0
Step 17/25 : RUN swift package update
 ---> Running in d7df9175b541
/root/vapor: error: package at '/root/vapor' requires a minimum Swift tools version of 5.0.0 (currently 4.2.0)
The command '/bin/sh -c swift package update' returned a non-zero code: 1
Unable to find image 'users:latest' locally
docker: Error response from daemon: pull access denied for users, repository does not exist or may require 'docker login'.

Thanks!

error: swift build

screen shot 2019-01-02 at 12 50 57 pm

UserManager/Sources/App/Controllers/AdminController.swift:17:13: error: generic type 'PermissionsMiddleware' specialized with too many typeparameters (got 2, but expected 1)
            PermissionsMiddleware<UserStatus, Payload>(allowed: [.admin]),
            ^                    ~~~~~~~~~~~~~~~~~~~~~
JWTMiddleware.PermissionsMiddleware:1:20: note: generic type 'PermissionsMiddleware' declared here
final public class PermissionsMiddleware<Payload> : Middleware where Payload : PermissionedUserPayload {
                   ^
UserManager/Sources/App/Controllers/AuthController.swift:22:39: error: generic type 'PermissionsMiddleware' specialized with too many type parameters (got 2, but expected 1)
        let restricted = auth.grouped(PermissionsMiddleware<UserStatus, Payload>(allowed: [.admin]))
                                      ^                    ~~~~~~~~~~~~~~~~~~~~~
JWTMiddleware.PermissionsMiddleware:1:20: note: generic type 'PermissionsMiddleware' declared here
final public class PermissionsMiddleware<Payload> : Middleware where Payload : PermissionedUserPayload {
                   ^
error: terminated(1): 

JWT

Could you please explain how to generate this N and D values from JWT? I used JWT with NodeJS but never faced with a manual configuration of those parameters.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.