JesseObrien / fake-iot

Fake IoT Project

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Initial Design

JesseObrien opened this issue · comments

I'll be building a front-end client using React js and an http API server using Go. The trade offs and design descriptions are in this header. There are diagrams of the system and each bit below. I'll be following this project standards repo as a standard layout for the project.

User & Password
I'm going to hard code the username & password in the code. The password will be a string hashed using the bcrypt hashing algorithm. In a normal flow you'd create these on user sign up and store these in a table. You'd then look them up when the user sends the username/password login request and validate the password matches hash.

Authentication
When the user sends a login request, I'll send back a JWT for the UI to put in the Authorization header as a Bearer token. I'm not going to implement the entire OAuth2 flow. Normally you'd redirect the user, obtain a grant and exchange the grant for an access token (JWT) that includes claims for the user. Each request will have the JWT validated through middleware.

Account Types
I'll be hardcoding an account in the accounts table. It will contain the type of account. Type will be "basic" by default. It will change to "enterprise" once the user clicks the upgrade button. I'll hard code the login limits (100, 1000) per account type in the code. In a normal system we'd have these values stored in the table so they'd be customizable per account.

Metrics
I'll be storing the login metrics in a table (account_logins). To get the count, I'll be querying with a COUNT(*) on the table. To do it properly, I'd create a materialised view that would reflect those aggregates on a per account basis whenever data is written and query that directly with the account id.

Each metric consumed will write to the database, then emit a login.count.updated event to a go channel where the websocket will be waiting to consume and send it to the UI. Normally we'd use some kind of message bus for this (Kafka, etc) but a channel will do in this instance. I'm going to re-use the same event for all front-end updates. In a nominal situation I'd break events out into stateless actions ( account.login, account.{id}.upgraded, etc) and react to those appropriately with different handlers. When consuming the metrics from the fakeiot http client, I'll be checking the count of current logins against the plan and will not be incrementing the counter if the plan limit has been reached.

Database Tables

account_logins
---
account_id string (uuidv4)
user_id string (uuidv4)
timestamp timestamp
accounts
---
id string (uuidv4)
user_id string (uuidv4)
type string (enum)

Design Diagrams

dashboard-login

consumption-path

showing-logins

upgrade-account

@JesseObrien

  • How do you plan to implement logout?
  • How do you plan to hand CSRF?

@JesseObrien

  • How do you plan to implement logout?
  • How do you plan to hand CSRF?

Good questions @russjones

Logout
JWT are not revocable by nature, so at best I can remove the current token from memory and not send the authorization header if there is no token. If the token is expired, a 401 Unauthorized would be sent back to the user. This obviously assumes someone hasn't pulled the token out of memory and is still attempting to use it after the "log out" happens from the browser perspective. To deter that, in a real OAuth flow the JWT would be issued with a short expiry time on it (say 5-15 minutes) and a refresh token would be attached and exchanged in order to get a new token. We could revoke the refresh token on the server side to eliminate that exchange taking place. If the window of expiry on JWT is deemed to insecure we could fall back to OAuth2 token/refresh tokens which are revocable any time, or even revert to http basic auth. It depends on the scenario with load balancers and other bits in terms of having required user data move through the system.

CSRF
I won't be rolling my own CSRF implementation. I'll be using gorilla's implementation to embed the token in the front-end so it can be sent as a header and validated by the middleware wrapper for the HTTP server.

XSS
There won't be any user-editable or render-able content on the page and using react with JSX will automatically escape any embedding that would happen regardless. I'm not saying to completely ignore it, but in the case of this project there should be no concern that XSS will happen.

  • how are fakeiot requests authenticated?
  • what's the fakeiot response when the plan limit is reached
  • can multiple users (using the same account) view the dashboard page with live updates in parallel? specifically, how will that work with the Go channel logic?

JWT are not revocable by nature, so at best I can remove the current token from memory and not send the authorization header if there is no token. If the token is expired, a 401 Unauthorized would be sent back to the user. This obviously assumes someone hasn't pulled the token out of memory and is still attempting to use it after the "log out" happens from the browser perspective. To deter that, in a real OAuth flow the JWT would be issued with a short expiry time on it (say 5-15 minutes) and a refresh token would be attached and exchanged in order to get a new token. We could revoke the refresh token on the server side to eliminate that exchange taking place. If the window of expiry on JWT is deemed to insecure we could fall back to OAuth2 token/refresh tokens which are revocable any time, or even revert to http basic auth. It depends on the scenario with load balancers and other bits in terms of having required user data move through the system.

All reasonable answers when using JWTs.

However, since logout is a requirement, and the above will get quite complex to implement, how about just using opaque tokens to do session management on the backend?

Once you call the logout endpoint, delete the token on the backend, send updated Set-Cookie header and you're done.

What do you think?

  • how are fakeiot requests authenticated?
  • what's the fakeiot response when the plan limit is reached
  • can multiple users (using the same account) view the dashboard page with live updates in parallel? specifically, how will that work with the Go channel logic?

fakeiot request auth
I'll have the http back-end looking for an environment variable with the API key in it, something like FAKIOT_API_KEY. That key will be passed via the cli flags to make sure the client is authenticated properly.

fakeiot plan limit reached
This one I'm a bit unsure of. My thinking is that when the plan limit is reached, users under that account wouldn't be able to log in anymore. That being the case, the metrics coming in wouldn't be login metrics any longer, they'd be login attempt failed or account login limit reached metrics. If the client is going to keep sending the metrics even though the plan limit has been reached, I would probably send a bad request with the reason that the plan limit has been exceeded. On the flip side, I don't like considering any data lost, so I could have a separate table to store them in until the plan limit is reached and unlock that data once the user upgrades.

multiple users on the same account
In the case of multiple users for the same account I'd register a new channel for each user when they connect to the web socket. I'd store a new user's channel in a map with the account id as the root, so using the example below. That way any user in that account can receive the same updates. Assuming the websocket func reading from the chan interface{} can do some type juggling and send out the correct event with what each is reading from the channel.

// Assume this type exists
type UserChannel struct {
  user_id string
  updates chan interface{}
}

// Assume this global channel store exists
var userSockets map[string]UserChannel

/// *socket connected with account_id and user_id sent*

// defer a func to remove the channel with that user id from the userSockets on disconnect
userSockets[account_id] = UserChannel{user_id, make(chan interface{})}

JWT are not revocable by nature, so at best I can remove the current token from memory and not send the authorization header if there is no token. If the token is expired, a 401 Unauthorized would be sent back to the user. This obviously assumes someone hasn't pulled the token out of memory and is still attempting to use it after the "log out" happens from the browser perspective. To deter that, in a real OAuth flow the JWT would be issued with a short expiry time on it (say 5-15 minutes) and a refresh token would be attached and exchanged in order to get a new token. We could revoke the refresh token on the server side to eliminate that exchange taking place. If the window of expiry on JWT is deemed to insecure we could fall back to OAuth2 token/refresh tokens which are revocable any time, or even revert to http basic auth. It depends on the scenario with load balancers and other bits in terms of having required user data move through the system.

All reasonable answers when using JWTs.

However, since logout is a requirement, and the above will get quite complex to implement, how about just using opaque tokens to do session management on the backend?

Once you call the logout endpoint, delete the token on the backend, send updated Set-Cookie header and you're done.

What do you think?

Yep just using a generated token stored on the server and setting the cookies manually would be fine for this. I could also use something like gorilla sessions to manage the cookies.

Yep just using a generated token stored on the server and setting the cookies manually would be fine for this. I could also use something like gorilla sessions to manage the cookies.

Either way is is fine with me.

@JesseObrien what npm dependencies (aside React library) are you planning to use for the challenge and why?

@JesseObrien what npm dependencies (aside React library) are you planning to use for the challenge and why?

@alex-kovoy

Dependencies

  • axios
  • websocket

Rationale

  • For requests I'll be using axios because I'm familiar with it. I prefer it over window.fetch. I've used it across multiple react/vue and nodejs projects.
  • For the websockets I'll be using the basic websocket package. I don't need anything fancy like socket.io, although socket does provide fallbacks and a generally nicer abstraction over websockets.
  • For two screens I think I'll forego using a router package and just render the two different components based on the logged in state. If there we more routes I would use the react-router package as I've used it many times on projects and know it well.
  • The progress bar bit looks like it'll just use the width property on it to show the updated progress so I won't need a CSS framework to handle that.

@JesseObrien sounds good!

Btw, just curious why not vanilla websockets?

In the case of multiple users for the same account I'd register a new channel for each user when they connect to the web socket.

@JesseObrien sounds good. It may get tricky when doing fan-out of an event to multiple user channels, especially when handling disconnects or slow consumers. But this is implementation details we can think through later.

@JesseObrien sounds good!

Btw, just curious why not vanilla websockets?

@alex-kovoy I was used to using it from building nodejs apps, but from doing a bit more reading it looks like the browser websocket is just fine to use and the websocket package has some C++ bindings in it that won't be useful in the browser.

@JesseObrien sounds good. It may get tricky when doing fan-out of an event to multiple user channels, especially when handling disconnects or slow consumers. But this is implementation details we can think through later.

@awly 💯 I agree, it's not optimal. At minimum it'll need a lock around the map when reading/writing which slows it all down as well. Ultimately it would be nice if there was a way to bake multicast channels into the language, but that's wishful thinking. Using a message bus solves for x here nicely.