At Notarize, we want to bring trust to some of life’s most important transactions. We know that security is at the foundation of this trust – that businesses and consumers alike expect us to maintain security standards and best practices to keep their information safe.
This is why we’re constantly refining the Notarize platform to not only meet the challenges of today, but to prepare for what’s ahead.
The Notarize API team recently worked on a project to improve our authentication flows and improve security around our use of cookies. We’re sharing what we learned and how it allows us to offer a stronger, safer, more consistent signing experience moving forward.
What is a website cookie?
Let’s start with the basics: What is a website cookie, and what is its purpose? A cookie is a piece of textual information in the form of a name and value pair that is stored in your browser’s memory.
The concept of a cookie is often compared to a coat check ticket. When you first visit a website, the website instructs your browser to store a cookie. This would be like getting a token when you first drop off your coat. Later, when you visit the same website with the same browser, the website uses the cookie stored in your browser to “remember” something about your last visit, the same way your coat check ticket uniquely links you to your coat.
Your browser can store many cookies for multiple purposes: some examples are session management, targeted advertisements, and suggested items when shopping.
Note: Notarize does not use cookies for marketing or tracking people’s activities on other sites.
While the most important part of a cookie is its name and value, there are also attributes that configure how it can be used. Each attribute has its own security implications.
The HTTPOnly attribute that the Notarize team focused on, if set to true, ensures that the cookie cannot be read by JavaScript’s cookie API. We also touched on the domain attribute, which specifies which hosts have access to the cookie.
How Notarize used cookies in the past
In order to talk about the changes we made, we should first address that our app has always used cookies to manage user session persistence.
When a user logged in from a web client, the client made a login request to our server, which authenticated the user and returned a JSON Web Token (JWT) as a response. The web client would then store this JWT in a cookie.
Anytime the client sent an authenticated request to our API, it read the JWT from the cookie and included it in the Authorization HTTP header. Our servers then read this Authorization HTTP header to access the token and authenticate the user. When a user logged out from the web client, the client deleted the cookie.
And while mobile clients didn’t use cookies, they still stored the JWT from the server response and included it as an Authorization HTTP header in requests.
As we began to introduce Security Assertion Markup Language (SAML) into our login flow, we noticed a few things:
- The responsibility of authentication was distributed across the client and server. We would authenticate the user on the server side, but then the client was responsible for setting and reading the auth cookie.
- Because our client needed to check the cookie, we could not set the HTTPOnly attribute on it.
- Because our web clients need access to the cookies, we had to set the domain of the cookie to .notarize.com, which resulted in this cookie being included in requests to any site that ends with “.notarize.com,” many of which didn’t even need access -- so we were sending unnecessary session information to services like CloudFront.
Based on these concerns, we decided to move all cookie management to the server, which would allow us to set the HTTPOnly attribute to true. As an added benefit, the domain would be limited strictly to our API service URL.
How Notarize uses cookies now
Moving all our cookie management to the server required the following changes:
- On login, the client still makes a request to the server. The server still generates the JWT as usual, but it also creates a cookie with that JWT, and includes that cookie in the “Set-Cookie” header in the response. The browser automatically sets any cookies that are in that header and include them in all future requests that are being sent to valid recipients of that cookie. Since mobile clients don’t have browsers, they have to read and store the cookie and ensure they include it in any future requests.
- In any request to the server, the server looks at the cookie header to retrieve the Authorization cookie and reads the JWT to authenticate the user.
- When logging out, clients make a call to the server so the cookie can be deleted.
Within our app, this change required us to remove web client dependencies on reading the cookie to know whether a user was logged in, and to modify our mobile clients to work with this new flow.
Our efforts were worth it though, since we were able to solve the problems we had with our previous approach: a cleaner separation of auth responsibilities, using the HTTPOnly flag for added security against XSS, and restricting our domain to prevent unnecessary information transmission over the wire. Overall, we made a huge improvement to our application by creating a more secure browsing experience for everyone who uses it.
Note: If you’re thinking about using cookies for authentication, it’s important to have CORS and CSRF protection to prevent activities from malicious sites from using your cookies against you -- you can find great resources to help you with this on the OWASP Cheat Sheet Series.