Week 09: State management in React, Auth Filip Kaštovský Outline What is state management? Why do we need it? History of state management in React State segregation Modern state management What is state management? Why do we need it? We don't!* What is state management? Why do we need it? A way to manage the state of your application (duh) State is data that is used by your application State management is needed because: State is shared between components State is updated by multiple components State is updated by external sources (API, user input) History of state management in React Pre-redux era (< 2016) React's self contained components were an unexplored concept Nobody knew what they were doing Lifting state up was a common way to share state between components God components Prop drilling Emerging patterns: Flux architecture Redux (2016 - 2020) Created by Dan Abramov and Andrew Clark A simplification of the Flux architecture, Redux introduces a single store that is the source of truth for the entire application State is immutable and can only be updated by dispatching actions Actions are processed by reducers, which update the state Redux (2016 - 2020) type Action = | { type: "increment" } | { type: "decrement" } | { type: "set"; payload: number }; type State = { count: number }; function counterReducer(state = State, action: Action) { switch (action.type) { case "increment": return { count: state.count + 1 }; case "decrement": return { count: state.count - 1 }; case "set": return { count: action.payload }; default: return state; } } Redux (2016 - 2020) Reducers cannot be async -> a new layer before reducers is introduced: redux middleware in an era without async/await, async stuff was painful redux-thunk , redux-saga , redux-observable A lot of boilerplate Everything is in one place (store) Immutable updates for nested objects Heavy! Fundamentally changes how you write your app Large ecosystem of libraries and tools for almost everything React Context API (> 2019) React's built-in state management solution Grew to popularity with hooks Declare a context and insert it into the component tree: const MyContext = React.createContext(defaultValue); Anything inside of value can be accessed via a useContext(MyContext) hook React Context API (> 2019) You can put useState values and functions in the context, sharing them between components const [state, setState] = useState(initialState); const value = useMemo(() => ({ state, setState }), [state]); But, remember how react re-renders components? The above is a very bad idea for global state management Sidetrack: useReducer useReducer is a hook that is similar to useState , but it allows you to manage more complex state uses the same reducer pattern as redux const [state, dispatch] = useReducer(reducer, initialState); const reducer = (state: State, action: Action) => { switch (action.type) { case "increment": return { count: state.count + 1 }; case "decrement": return { count: state.count - 1 }; case "set": return { count: action.payload }; default: return state; } }; Antipattern: Poor mans redux You can combine useReducer and useContext to create a poor mans redux in like 10 lines of code DO not do this, this is not a replacement for redux (or any other state management solution) Change of perspective: State segregation Rather then treating all of our application state as global and one big pile of data handled by a generic manager, use specialized tools for specialized tasks There is no need to reinvent the wheel! Form data? Use react-hook-form API? Use Tanstack Query Routing? Use react-router Local state? Use useState / useReducer Local state across multiple components? Use useContext ??? What else do we need to track of in an app? State segregation There is nothing left! We have covered all of the state management needs of our application. Almost... (theme, current user, etc.) Most of the time, you will not need a global state management solution anymore Modern state management Ideal state is: handle all state updates outside of React (it is bad at it, wants to re-render everything) only notify and update components that are interested in the state change Composable state (atoms) Composable state (atoms) Originally a react core team's idea, now implemented as Recoil const countState = atom({ key: "countState", default: 0, }); function Counter() { const [count, setCount] = useRecoilState(countState); return
{count}
; } Recoil is a bloated library for what it provides, jotai is a much better implementation Signals Observer pattern for state management, introduced by solid-js By far the most performant solution for state management! Backported to react as @preact/signals-react Auth Outline Access control AuthN vs AuthZ Common auth patterns Auth in Express Auth on the frontend Security considerations Access control Only allow access to resources to authorized users Different users have different permissions User level access: only the resource owner can access the resource Role-based access control: users are assigned roles, roles have permissions Rules, policies, etc... AuthN vs AuthZ Authentication (AuthN) is the process of verifying the identity of a user Authorization (AuthZ) is the process of verifying that the user has the necessary permissions to access a resource Easy to mix up, but they are different things AuthN and AuthZ are often handled together (Auth / AA) by an application Common auth patterns Basic auth Username and password Sent in the Authorization header Base64 encoded Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== Not secure, use encrypted connections Some clients allow encoding the password in the URL smtp://username:password@server:587 Token based auth A client authenticates with a server and receives a token The token is then used for subsequent requests SessionID Create a unique session ID for each successful authentication Store it a database Send it to the client Client sends it back with each request Overhead of storing sessions 'somewhere', stateful API keys Used for authenticating against an external API Can be of any format Generally considered insecure ? Principle of least privilege Rotate keys often JWT JSON Web Token Self-contained, signed token Contains claims (data) about the user Cryptographically signed with a given expiry jwt.io Invalidation/revoking OAuth2/OpenID Connect OAuth2 is an authorization framework OpenID Connect is an identity layer on top of OAuth2 Used for single sign-on (SSO), "social login" Allows third-party applications to access resources on behalf of a user Requires multiple token exchanges for security Complex, but very powerful Pattern: Federated auth Dedicating a separate service for authentication and authorization Allows for more flexibility and scalability Allows multiple services to authenticate against the same service! In case of a breach, it is more difficult to access all the users' data Allows you to use an off the shelf solution, reducing the risk of introducing vulnerabilities Auth in Express passport.js is a middleware for Express that handles session management app.post("/login/password", passport.authenticate("local")); middleware strategies sessions Strategies A strategy is a way to authenticate a user Over 500 strategies available! For local username/password authentication: npm install passport-local OpenID Connect: npm install passport-openidconnect Sessions Passport also contains connectors for session management express-session app.use( session({ secret, }) ); app.use(passport.session()); internally uses cookies But wait? Where do I store the token for token-based auth? HTTP cookies both sides (client and server) are allowed to read and write them (most of the time) Usually the server sets a cookie with the token Logout can be done by deleting the cookie setting the cookie as httpOnly prevents client-side JS from reading it Headers The token can be sent in the Authorization header (just as with basic auth) Authorization: Bearer This header is then read by the server, but has to be sent by the client. Here, the client has to manage the token Local storage, session storage... Security considerations XSS Cross-Site Scripting Attacker injects malicious scripts into a website and can get access to cookies, session tokens, etc. CSRF Cross-Site Request Forgery Forces authenticated users to submit a request to a Web application against which they are currently authenticated Mitigation: CSRF tokens Password storage and validation Never store passwords in plaintext! Always use a secure hashing algorithm (argon2, scrypt, bcrypt) Salting Hashing: const hash = await argon2.hash(..); try { if (await argon2.verify("", "password")) { // password match } else { // password did not match } } catch (err) { // internal failure } Rate limiting Prevents brute force attacks Limits the number of requests a user can make in a given time frame Protects underlying infrastructure For sensitive endpoints, require the user to solve a challenge (captcha) to prevent automated attacks The above doesn't work well anymore (AI) Web Application Firewalls Generally a paid service. Filters out malicious traffic. WAF uses a set of heuristics and rules to determine if a request is malicious, can prompt the user to solve a challenge if the target is a website. OWASP Open Web Application Security Project A community that produces freely-available articles, methodologies, documentation, tools, and technologies in the field of web application security OWASP Top 10 Thanks for listening! Questions?