Serverless architectures are an easy new way to build scalable backend applications, without the hassle or cost of operating your own servers. With the proliferation of hosted cloud services, whether for compute, file storage, or databases, developers can link essential services together without the need to build a traditional REST API for their mobile and frontend applications. This architecture also allows developers to move logic directly into frontend clients, which can speed up prototyping of new apps and features.
Stormpath helps developers manage and authenticate users for your web applications, and works great with serverless architectures. With authentication from a hosted service like Stormpath, independent services can verify the identity of the end user and properly authorize them to perform various actions, ensuring that the developer has control. With the release of the Stormpath Client API, Stormpath enables frontend and mobile clients to authenticate in a consistent manner. Serverless code can still use the same SDKs developers enjoy with other backend architectures.
In this post, we’ll walk you through an application we built with a serverless architecture, and talk about the design thinking behind it.
Building Stormpath Notes with a Serverless Architecture
To demonstrate what you can do with Stormpath in a serverless environment, we’ve re-built Stormpath Notes, a note-taking app that we built for the iOS and Android launch tutorial. Users can log in, edit, and save their personal notes. Instead of building a REST API to power the backend, we’ve built it in a serverless environment within AWS.
Here’s a demo of the app in action:
Check out our serverless example code on GitHub
We’ve used the following tools to build Stormpath Notes:
- AWS Lambda
- AWS DynamoDB
- AWS Identity & Access Management
- AWS iOS SDK
- Stormpath Node.js SDK
- Stormpath iOS SDK
Serverless Application Architecture
If we were building this backend the normal way (and we did! Check out this tutorial), we’d build a REST API with endpoints for registration, login, retrieving, and saving your notes.
In a serverless environment, instead of building an register and login endpoint, Stormpath powers the authentication. The Stormpath SDK in our iOS app powers registration and authentication for end users. DynamoDB directly stores user notes, with AWS Identity and Access Management (IAM) to authorize users to read and write to the DynamoDB table.
This allows us to build a cheap and scalable backend that requires no maintenance to run!
DynamoDB Setup
DynamoDB is a NoSQL database; it stores an arbitrary JSON blob against a set of keys. We’ll use those JSON blobs to store the end user’s notes. The Stormpath user account’s href
, or its identifier, is the primary key for the table. This ensures that we have a unique entry for each user that we can use to store data.
AWS Identity and Access Management Configuration
IAM enables us to build a serverless backend by properly authorizing access to end users. IAM uses the concept of both users and roles to authorize actions. IAM Users represent who is performing an operation, whether an actual end-user, administrator, or automated service. IAM Roles represent a set of permissions assigned to a user, which can be used to perform an action.
A user called stormpath-notes-unauthenticated
represents the iOS application making calls to AWS, when not associated with a specific end user. This allows us to authorize access to a subset of our Lambda functions.
Next, the our Lambda function runs under the stormpath-notes-lambda
role. This role allows Lambda to issue temporary credentials for end users.
Last, a role called stormpath-notes-authenticated
authorizes end user access to our Stormpath Notes DynamoDB table.
Set Up Lambda to Generate Temporary Credentials
With IAM set up, we can create a Lambda function to validate Stormpath access tokens, and issue temporary credentials to our end users. While you can see the full code here, here’s a brief overview of how this function works.
We begin by using the Stormpath Node.js SDK to validate the Stormpath access token. After it’s validated, we issue temporary credentials against the stormpath-notes-authenticated
role, and further restrict them with an additional IAM policy. The policy statement looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{ "Sid": "AllowAccessToOnlyItemsMatchingUserID", "Effect": "Allow", "Action": [ "dynamodb:GetItem", "dynamodb:UpdateItem" ], "Resource": [ "arn:aws:dynamodb:us-east-1:569073598720:table/stormpath-notes" ], "Condition": { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": [ accountHref ] } } } |
All IAM policies have three components: action, resource, and condition. In this policy, we restrict the credentials to dynamodb:GetItem
and dynamodb:UpdateItem
, which do what they sound like they do. We apply this policy against the arn:aws:dynamodb:us-east-1:569073598720:table/stormpath-notes
table that we created in DynamoDB. Then, we add an additional condition, which is that the dynamodb:LeadingKeys
has to ForAllValues:StringEquals
the user account’s href, or its unique identifier.
Last, we need to pick a role for this Lambda function. Lambda, when spinning up our function, automatically sets up credentials with a specific IAM role. This is the stormpath-notes-lambda
role we discussed earlier.
Note: Stormpath is building in support for OpenID Connect, which will make the token exchange process discussed here even easier. Once that feature is available, we would be able to set up Stormpath as an OpenID Connect identity provider in IAM, eliminating the need for this Lambda endpoint
Configure IAM Policies to Authorize Access to AWS Resources
With all of the resources created and defined, here’s how we have the trust relationships set up between all of the IAM roles.
For the stormpath-notes-unauthenticated
users, we give them access to our Lambda function with this AWS policy:
1 2 3 4 5 6 7 8 9 10 11 |
{ "Sid": "Stmt1486684275000", "Effect": "Allow", "Action": [ "lambda:InvokeFunction" ], "Resource": [ "arn:aws:lambda:us-east-1:569073598720:function:stormpath-authorizer" ] } |
Then, the stormpath-notes-authenticated
role allows it to access our DynamoDB table, and get / update items.
1 2 3 4 5 6 7 8 9 |
{ "Effect": "Allow", "Action": [ "dynamodb:GetItem", "dynamodb:UpdateItem" ], "Resource": "arn:aws:dynamodb:us-east-1:569073598720:table/stormpath-notes" } |
Last, the stormpath-notes-lambda
role allows it to request temporary credentials for the stormpath-notes-authenticated
role:
1 2 3 4 5 6 |
{ "Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "arn:aws:iam::569073598720:role/stormpath-notes-authenticated" } |
iOS App
The app is based on the Stormpath Notes demo built in last year’s tutorial. We’re using both the Stormpath SDK, as well as the DynamoDB and Lambda SDKs. Check out the iOS project on GitHub. Try compiling and running it — it should automatically hit the backend we have set up!
To use the Stormpath and AWS SDKs, we’ve used Cocoapods to add the following dependencies:
1 2 3 4 |
pod 'Stormpath', '~> 3.0' pod 'AWSDynamoDB', '~> 2.5' pod 'AWSLambda', '~> 2.5' |
When looking through the code, you’ll want to pay attention to two key parts of this project:
APIClient
– class that wraps the Stormpath and AWS SDKs.AppAWSCredentialsProvider
– stores the auth tokens for accessing AWS and its current state.
Let’s take a look at how we built the iOS app in more depth.
Providing AWS Credentials to the AWS SDK
The AWS SDK defines the AWSCredentialsProvider
protocol, which we implemented to provide the correct credentials to AWS. The implementation is fairly straightforward, with this basic class signature:
1 2 3 4 5 6 7 |
class AppAWSCredentialsProvider { let unauthenticatedCredentials: AWSCredentials var authenticatedCredentials: AWSCredentials? func credentials() -> AWSTask<AWSCredentials> } |
By default, unauthenticatedCredentials
should be a set of credentials representing the stormpath-notes-unauthenticated
role. By creating a set of credentials in IAM, we can set this to:
1 2 |
AWSCredentials(accessKey: "AKIAI2BRME6TDHDWHBEQ", secretKey: "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", sessionKey: nil, expiration: nil) |
Note: this might seem a bit insecure because we are embedding a secret key in the app. However, when we created this account in IAM, we limited the scope to a specific Lambda function, stormpath-authorizer
. Because it’s limited to a specific function, it’s almost as if we built that lambda function as an unauthenticated API endpoint. This set of API credentials are very limited in scope, and are OK to embed in the app.
Authenticated credentials will be set if we are authenticated by the stormpath-authorizer
function. Thus, the credentials()
function returns the current set of credentials. This allows the CredentialsProvider to choose between the correct API keys / token for accessing AWS.
In addition, we add an authenticate(accessToken: String)
function to this credentials provider. This function takes a Stormpath access token, hits Lambda, and exchanges it for a set of AWS credentials.
1 2 3 4 5 6 7 8 9 10 11 |
func authenticate(accessToken: String) -> AWSTask<AnyObject> { let lambdaInvoker = AWSLambdaInvoker.default() return lambdaInvoker.invokeFunction("stormpath-authorizer", jsonObject: ["accessToken": accessToken]).continueWith { (task) -> Any? in if let credentialsJSON = (task.result as? [String: Any])?["Credentials"] { self.authenticatedCredentials = AWSCredentials(json: credentialsJSON) } return nil } } |
Login With Stormpath
Since we’re using Stormpath to handle authentication, a login
method in APIClient
handles username/password based login.
After calling the Stormpath SDK and getting an access token, we then call the credentials provider’s authenticate
method to exchange the Stormpath access token for an AWS access token.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
Stormpath.sharedSession.login(username: username, password: password) { (success, error) in guard success else { callback?(error) return } // Get the AWS Credentials Provider let credentialsProvider = AWSServiceManager.default().defaultServiceConfiguration.credentialsProvider as? AppAWSCredentialsProvider // Allow the AWS Credentials Provider to authenticate with the Stormpath access token & get a user-scoped access token. credentialsProvider?.authenticate(accessToken: Stormpath.sharedSession.accessToken!).continueWith { (task) -> Any? in // Populate user fields Stormpath.sharedSession.me { account, error in self.account = account DispatchQueue.main.async { callback?(error) } } return nil } } |
Get Note from DynamoDB
To use the DynamoDB SDK, we need to create a model for how our data in Dynamo looks like. To do so is really simple! A Note
class extends from AWSDynamoDBObjectModel
and defines the fields we’ll be using:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Note: AWSDynamoDBObjectModel { // Primary key is the account resource var accountHref: String? // Text of the note var text: String? class func dynamoDBTableName() -> String { return "stormpath-notes" } class func hashKeyAttribute() -> String { return "accountHref" } } |
With an authenticated session, we can get a note from DynamoDB:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Query DynamoDB for a note with the user's href as the primary key AWSDynamoDBObjectMapper.default().load(Note.self, hashKey: account.href.absoluteString, rangeKey: nil) .continueWith { task -> Any? in let dynamoNote = task.result as? Note self.notes = dynamoNote?.text ?? "This is your notebook. Edit this to start saving your notes!" DispatchQueue.main.async { callback?(nil) } return nil } |
And save a note to DynamoDB:
1 2 3 4 5 6 7 8 |
// Create the DynamoDB model let note = Note()! note.accountHref = account.href.absoluteString note.text = notes // Save to DynamoDB AWSDynamoDBObjectMapper.default().save(note) |
And that’s it! Serverless authentication with Stormpath, plus AWS’s services, allow you to quickly and easily draft up an app with little work.
And if we want to add more? It’s easy. For instance, we could add a file upload feature with Amazon S3. In a similar fashion, we would create a S3 bucket, and add additional permissions to the policy generated by our Lambda function. Then, using the AWS SDK, we would implement file upload in our iOS app, and viola! Uploads! Building an application with a serverless architecture is a fast, fun, and rewarding way to architect the backend of your next application. We suggest you try it out!
What’s next?
Check out the code! — read the full code behind this app on GitHub
Try building this as a REST API — Serverless architectures are great, but for higher degrees of maintainability, it’s nice to know how to build your own REST API. Learn how to build this same backend, but with a REST API using Node.js
Learn how to build the app! — We did a brief overview of how to build the API client for this app, and use the Stormpath and AWS SDKs. If you’d like to learn how to build this app (but more in depth), check out this tutorial.
Learn more about Stormpath — Stormpath is free to use, and can help your team write a secure, scalable application without worrying about the nitty gritty details of authentication, authorization, and user security. Sign up for a forever-free developer account today!
Talk with us — We’re proud of the exceptional level of support we provide our community, and would love to hear from you about your project! Please don’t hesitate to contact us at [email protected], file an issue against one of our GitHub projects, or leave a comment below! Or, follow us @EdwardStarcraft and @goStormpath!