Create a Registration API in 15 minutes
Allowing users to register in your app is one of those important things few knows how to actually implement correctly. It requires knowledge about a whole range of complex things, few software developers have time to study.
In the video below I am demonstrating how to use Magic's No-Code and Low-Code features to create it from scratch, in 15 minutes.
The point about the above process is that it relies upon workflows and software development automation, utilising No-Code and Low-Code software development concepts, or "composables" if you wish.
This allows us to create software 1 million times faster than the average software developer, because out of 1,000,000 things you need to think about, 999,995 of those things are already handled in Magic. Still, because of Magic's workflows and actions, you don't have to sacrifice neither speed, security, nor flexibility - Because each action is an atomic building block, allowing you to chain and orchestrate actions together any ways you see fit.
This allows you to use these actions and orchestrate them together to meet your exact requirements. Never again do you have to answer "We don't have that features" - Simply because as delivery speed increases orders of magnitudes, the software development axiom fundamentally changes. But that's a topic for another article.
Let's go through what's required when creating a registration API to understand the complexity of the task, and what I actually did in those 15 minutes in the above YouTube video.
User registration concepts
When creating a user registration workflow there's a lot of things you'll have to consider to make your system secure and well functioning to prevent your system from entering an invalid state. We need to consider bots, security, data quality, avoiding inserting partial or invalid data, etc. Below are the concepts we have to think about as we proceed.
Double optin
User registration requires two endpoints; one endpoint allowing the user to register, and another endpoint that is sent to the user as a hyperlink he or she can click to verify his or her email address. The second endpoint's verifies that a "secret" parameter sent to the user's email address was correctly supplied before we accept the user as a "real user", and this secret must be impossible to guess.
This logic makes it impossible for users to register without supplying our system with a valid email address. This is often referred to as "double optin registration", and ensures you've got a real email address for every single valid user in your system.
reCAPTCHA
Another security measure such an endpoint would typically employ is reCAPTCHA to avoid having malicious actors invoking it thousands of times with random garbage to fill up your database with invalid users. reCAPTCHA ensures the invocation came from an actual human being, and not from a script created by some malicious actor. This further adds to the security of our system and ensures that a human being registered and not some bot or script.
By combining reCAPTCHA with double optin you have two guarantees.
- A human being registered
- The human being gave you a real email address he or she controls
This increases the quality of the data you end up with in your system during registration.
Unique emails and usernames
In addition we typically don't want to allow for the same email address to be used multiple times for creating multiple users. This implies that before we create our user, we'll need to check our existing users to see if the specified email address has been previously used for registering a user in our system.
In addition we want to ensure that the username is available before we allow for the user to register. This prevents the same username from being used multiple times, which obviously would be a bad thing. Typically this is automatic since most systems saves usernames as unique columns in some database - However, if we implement our own logic here, we can create more useful error messages, propagating to the client with intelligent information, allowing him or her to understand what's wrong when the system fails.
A message such as "UNIQUE constraint violation on xyz" isn't particularly informative for the end user. It's much better to provide the user with "Username already taken".
Storing passwords securely
Passwords should never be stored in plain text in a database. I'm not going to go through all the arguments here, but basically you want to store passwords as hashed, and you want to use slow hashing, with record based salts. Luckily for us, this is an out of the box feature in Magic we don't need to think about. For those interested in the details, Magic is using blowfish hashing with individual per-record based salts, preventing an entire axiom of security vulnerabilities - But that's besides the point for this article.
All you need to know is that Magic is using the same password storage functionality as Linux, the CIA, and the NSA. It's rock solid!
If you're using something else than Magic, you need to think about this - However, we're using Magic, and this is an out of the box feature we don't need to think about.
Validate input
In addition to the above we want to enforce on the server side that the user actually provided values for each argument our system must have to function correctly. This prevents our workflows from partially executing, resulting in that our system enters an invalid state for parts of its data. In magic this is done by using a mandatory validator, that ensures our arguments are specified, and if not, throws an exception.
We also want to make sure the user provided a real email address before we start our workflow, to prevent creating a partial user based upon erronous data. This is done using an email validator. Notice, this doesn't prevent the user from providing an email address he or she doesn't own - But combined with our above double optin logic, this creates a highly fault tolerant system, almost impossible to feed with invalid data.
Typically the above is done on the client side, but as we all know, client side code might have bugs, allowing the system to be invoked with invalid data. For these reasons we want to repeat this logic also on the server, regardless of whether or not we implemented it on the client side.
When you create software, you want your solutions to be as close to atomic as possible. The above ensures almost 100% perfect atomicity, where only if all arguments are valid, changes are persisted and state changes.
Allowing for state changes in your system because of "bad data", results in that your system and its database becomes a "big junkyard" after a couple of years.
Our endpoints
With the above explanation we can now type out what arguments our endpoints requires. Our two endpoints requires the following arguments.
register.post.hl
- username
- password
- name
- recaptcha_token
verify-email.get.hl
- username
- hashed username
Registration flow
With all of the above explained, we can now proceed to actually create our registration endpoint's logic. The following flow is how a registration typically works.
User registers with username, password, name, and email
- Validate reCAPTCHA value
- Ensure required arguments were given
- Ensure email is an email address and not something else
- Make sure username is available
- Create user, making sure we store password as BlowFish hashed with individual per-record based salts
- Associates user with two extra fields; name and email
- Send email with double optin link
- Getting config value for magic:auth:secret
- Concatenating username with auth secret
- Hashing result of concatenated strings
- Send email to user with link to another HTTP GET endpoint that verifies the hash with username and hash value as QUERY parameters
User clicks links in email
- Verifying hash
- Getting config value for magic:auth:secret
- Concatenating username with auth secret
- Hashing result of concatenated strings
- Verify result of above hash equals QUERY parameter hash, and if not, throw an exception (conditional action)
- Add user to guest role
- Redirect user to some static welcome URL
The code
The above is rock solid registration API logic, perfectly encapsulating almost everything you need to consider as you create a registration API. Still, I have not seen a single system in my professional life (besides from Magic), implemented using the above best practices.
The way we built it in the above YouTube video was almost "drag'n'drop". Still, the code encapsulates orders of magnitudes better code than most systems you've ever worked on as a professional software developer. In fact, providing "registration services" is an entire axiom of SaaS business company ideas. I know at least a handful of companies providing the above as their single value proposition because of how easy it is to mess this up.
Below you can see the whole code required to accomplish the above.
register.post.hl
/*
* Registers a user in your cool app!
*
* Very cool!
*/
.arguments
username:string
password:string
name:string
email:string
recaptcha_token:string
/*
* Validates the specified reCAPTCHA token.
*
* This action will validate the specified reCAPTCHA token and throw an exception if validation fails,
* unless the user is authenticated and belongs to the root role.
*/
execute:magic.workflows.actions.execute
name:recaptcha
filename:/misc/workflows/actions/security/recaptcha.hl
arguments
recaptcha_token:x:@.arguments/*/recaptcha_token
threshold:decimal:0.3
/*
* Ensures the spcified [args] was given.
*
* Will iterate through each [args] specified and verify it exists, and if not, the action will
* throw an exception with a descriptive text explaining what argument was missing.
*/
execute:magic.workflows.actions.execute
name:validators-mandatory
filename:/misc/workflows/actions/security/validators-mandatory.hl
arguments
args
username:x:@.arguments/*/username
password:x:@.arguments/*/password
name:x:@.arguments/*/name
email:x:@.arguments/*/email
/*
* Ensures the spcified [emails] are valid emails.
*
* Will iterate through each [emails] specified and verifying these, this action will
* throw an exception with a descriptive text explaining what argument was not a valid email.
*/
execute:magic.workflows.actions.execute
name:validators-email
filename:/misc/workflows/actions/security/validators-email.hl
arguments
emails
email:x:@.arguments/*/email
/*
* Returns true if the specified username is available.
*
* Optionally apply [throw] as true, at which point the action will throw an exception if
* the username is already taken.
*/
execute:magic.workflows.actions.execute
name:username-available
filename:/modules/registration/workflows/actions/username-available.hl
arguments
username:x:@.arguments/*/username
throw:bool:true
/*
* Registers the specified [username] as a user in the system, with the specified [password].
*
* Notice, [password] will be hashed using blowfish hashing.
*/
execute:magic.workflows.actions.execute
name:users-create
filename:/modules/registration/workflows/actions/users-create.hl
arguments
username:x:@.arguments/*/username
password:x:@.arguments/*/password
/*
* Associates the specified [username] with the specified [extra] fields.
*
* If extra field already exists for user, it will be changed - If not, it will be inserted.
*/
execute:magic.workflows.actions.execute
name:extras-upsert
filename:/modules/registration/workflows/actions/extras-upsert.hl
arguments
username:x:@.arguments/*/username
extra
name:x:@.arguments/*/name
email:x:@.arguments/*/email
/*
* Returns the specified [key] configuration setting.
*
* Notice, to traverse into for instance magic.foo.bar, you'll have to colon separate
* your path as follows "magic:foo:bar".
*/
execute:magic.workflows.actions.execute
name:config
filename:/misc/workflows/actions/misc/config.hl
arguments
key:"magic:auth:secret"
/*
* Joins the specified [values] into a single string.
*
* Notice, the [separator] is inserted between each string. If no [separator] argument is provided,
* the strings will simply be concatenated instead.
*/
execute:magic.workflows.actions.execute
name:strings-join
filename:/misc/workflows/actions/strings/strings-join.hl
arguments
values
.:x:@.arguments/*/username
.:x:--/execute/=config/*/value
separator:,
/*
* Creates a hash of the specified [input].
*
* Use [algorithm] to override the default hashing algorithm used. The default hashing algorithm
* is SHA 256.
*/
execute:magic.workflows.actions.execute
name:hash
filename:/misc/workflows/actions/misc/hash.hl
arguments
input:x:--/execute/=strings-join/*/result
algorithm:sha256
// Becomes the body of our email
.body:@"Jo dude! Click the link below to confirm your email address.
https://thomastest-team.us.ainiro.io/magic/modules/my-cool-app/verify?username=[username]&token=[token]"
/*
* Replaces the specified [source] string's [what] occurencies with [with].
*
* Returns the result as [result].
*/
execute:magic.workflows.actions.execute
name:strings-replace-username
filename:/misc/workflows/actions/strings/strings-replace.hl
arguments
source:x:@.body
what:[username]
with:x:@.arguments/*/username
/*
* Replaces the specified [source] string's [what] occurencies with [with].
*
* Returns the result as [result].
*/
execute:magic.workflows.actions.execute
name:strings-replace
filename:/misc/workflows/actions/strings/strings-replace.hl
arguments
source:x:--/execute/=strings-replace-username/*/result
what:[token]
with:x:--/execute/=hash/*/result
/*
* Sends an email to the specified [name]/[email] recipient, with the specified [subject] and [body].
*
* Optionally supply [from] and [from-email] as name/email sender. If you don't supply from, this action
* will use the default sender settings from your configuration.
*/
execute:magic.workflows.actions.execute
name:email
filename:/misc/workflows/actions/misc/email.hl
arguments
html:bool:false
name:x:@.arguments/*/name
email:x:@.arguments/*/email
subject:Please verify your email address
body:x:--/execute/=strings-replace/*/result
return
result:success
verify.get.hl
/*
* Allows the user to verify his or her email address
*
* Very cool app!
*/
.arguments
token:string
username:string
/*
* Returns the specified [key] configuration setting.
*
* Notice, to traverse into for instance magic.foo.bar, you'll have to colon separate
* your path as follows "magic:foo:bar".
*/
execute:magic.workflows.actions.execute
name:config
filename:/misc/workflows/actions/misc/config.hl
arguments
key:"magic:auth:secret"
/*
* Joins the specified [values] into a single string.
*
* Notice, the [separator] is inserted between each string. If no [separator] argument is provided,
* the strings will simply be concatenated instead.
*/
execute:magic.workflows.actions.execute
name:strings-join
filename:/misc/workflows/actions/strings/strings-join.hl
arguments
values
.:x:@.arguments/*/username
.:x:--/execute/=config/*/value
separator:,
/*
* Creates a hash of the specified [input].
*
* Use [algorithm] to override the default hashing algorithm used. The default hashing algorithm
* is SHA 256.
*/
execute:magic.workflows.actions.execute
name:hash
filename:/misc/workflows/actions/misc/hash.hl
arguments
input:x:--/execute/=strings-join/*/result
algorithm:sha256
/*
* Executes the specified [action] if the specified [condition] is true.
*
* Will pass in all arguments specified to the action. The [condition] can be two different
* values or expressions, and the [comparison] can be eq, neq, mt, mte, lt, lte, or some other
* comparison operator.
*
* The action will use the [comparison] to compare the [lhs] and [rhs] values/expressions.
*/
execute:magic.workflows.actions.execute
name:execute-action-if
filename:/misc/workflows/actions/misc/execute-action-if.hl
arguments
lhs:x:--/execute/=hash/*/result
comparison:neq
rhs:x:@.arguments/*/token
action:/misc/workflows/actions/misc/error.hl
arguments
message:Bogus token, go home!!
status:400
public:true
/*
* Associates the specified [username] with the specified [roles] roles.
*
* Notice, does not remove roles from user, only adds roles. This action will also throw
* if you try to associate a user with a role the user is already associated with.
*/
execute:magic.workflows.actions.execute
name:roles-add
filename:/modules/registration/workflows/actions/roles-add.hl
arguments
roles
.:guest
username:x:@.arguments/*/username
// Redirects the client to the specified [url].
execute:magic.workflows.actions.execute
name:http-redirect
filename:/misc/workflows/actions/http/http-redirect.hl
arguments
url:"https://ainiro.io"
Bonus code
If you in addition need an authenticate API endpoint, I have included it below too. I didn't create this in the YouTube video, but it literally took me 1 minute. This implies that I spent 16 minutes in total delivering a registration and authentication API backend module.
/*
* Authenticates the user
*
* Very cool app!!
*/
.arguments
username:string
password:string
/*
* Authenticates the user with the specified [username] and [password].
*
* Returns a JWT Bearer token that can be used for consecutive requests authorizing user according
* to his or her roles.
*/
execute:magic.workflows.actions.execute
name:authenticate
filename:/modules/registration/workflows/actions/authenticate.hl
arguments
username:x:@.arguments/*/username
password:x:@.arguments/*/password
// Returns the result of your last action.
return-nodes:x:@execute/*
Conclusion
In total the solution contains 303 lines of code. Divided by 16 minutes, that becomes 18 lines of code per minute. Almost one line of code per second. If I hadn't created the YouTube video at the same time I created the code, I could probably have tripled my speed, implying one line of code per second, building the entire solution in 5 minutes.
This becomes 604,800 lines of code per month, assuming I code for 40 hours per week
Realising the industry average is 325 lines of code per month, this implies with Magic you can in theory become 1,860 times more productive - Ignoring whether or not LOC is a good measurement of productivity.
Of course, measuring productivity by number of lines of code per minute is probably not a very good metric. However, the above is highly useful code, arguably production ready as is, and I have seen senior software developers mess with the above for months without being able to implement it correctly.
But don't believe me, go ask one of your team's senior devs how much time he or she needs to implement user registration, based upon RBAC, all industry best practices, and try to give him the task. Then tell him you need an administration GUI to be able to administrate your users and roles.
Even a super human software developer would easily spend weeks, if not months delivering the above! Yet again, I did it live on YouTube, and I did it in 15 minutes!
In addition, most online tutorials you'll find about registration APIs will happily skip the most important parts, with a TODO hidden somewhere in a comment, saying stuff like "TODO: Implement security". The above have almost no TODOs. I could have provided a password validator maybe, to ensure the length of passwords was at least 12 character or something - But that's literally the only TODO for the above code.
In the above YouTube video I arguably created a "perfect" solution in 15 minutes. I think that's cool 😎