MIT Weblab笔记 - Authentication

Introduction

Setting up account and authentication involves achieving two goals:

  • Initial login
  • Staying login when making requests or refreshing pages

Initial Login

User send username and password to the server, however, it’s not safe to directly pass them as JSON. Using Google Authentication can help us manage the account.

Session

  • When a user logs in, the server creates a session and assigns a unique session ID to the user.
  • The session ID is stored on the client-side in a cookie, while the actual session data corresponding to the session ID is stored on the server
  • On each subsequent request, the server uses the session ID from the cookie to retrieve the session data and identify the user.

JWT (Json Web Token)

JWTs do not require server-side storage of session data.

  • When a user logs in, the server generates a JWT containing the user’s information and sends it to the client.
  • Browser puts JWT in local storage. The client includes the JWT for subsequent requests.
  • The server verifies the token’s signature to authenticate the user.

How to Manage Login?

Sample Image

Login (Frontend)

At Frontend:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import GoogleLogin from "react-google-login";

// This identifies your web application to Google's authentication service
const GOOGLE_CLIENT_ID = "your-google-client-id";

<GoogleLogin
clientId={GOOGLE_CLIENT_ID}
buttonText="Login"
onSuccess={handleLogin}
onFailure={(err) => console.log(err)}
className="NavBar-link NavBar-login"
/>

const handleLogin = (res) => {
console.log(res);
setLoggedIn(true);
const userToken = res.tokenObj.id_token;
post("/api/login", { token: userToken }).then((user) => {
console.log(user);
});
};
  • When user clicks the login button, it triggers google login process. (GoogleLogin component from the react-google-login package).
  • onSuccess={handleLogin}: Triggers handleLogin function when loggin in successgully, passing response from google as the parameter.
response
  • Inside handleLogin, It extracts the id_token from the response by userToken = res.tokenObj.id_token and sends it to the server via a POST request to the /api/login endpoint.

Login (Backend)

Define user schema for MongoDB:

1
2
3
4
5
6
7
8
9
10
// User.js
const mongoose = require("mongoose");

const UserSchema = new mongoose.Schema({
name: String,
googleid: String,
});

// compile model from schema
module.exports = mongoose.model("user", UserSchema);

The frontend passes the userToken to backend through post request.
Handling /api/login Post request by calling login function in the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const { OAuth2Client } = require("google-auth-library");
const User = require("./models/user"); // Import User model

const CLIENT_ID = "your-google-client-id";
const client = new OAuth2Client(CLIENT_ID);

function login(req, res) {
verify(req.body.token)
.then((user) => getOrCreateUser(user))
.then((user) => {
console.log(`Logged in as ${user.name}`);

// persist user in the session
req.session.user = user;
res.send(user);
})
.catch((err) => {
console.log(`Failed to log in: ${err}`);
res.status(401).send({ err });
});
}

function verify(token) {
return client
.verifyIdToken({
idToken: token,
audience: CLIENT_ID,
})
.then((ticket) => ticket.getPayload());
}

function getOrCreateUser(user) {
// the "sub" field means "subject", which is a unique identifier for each user
return User.findOne({ googleid: user.sub }).then((existingUser) => {
if (existingUser) return existingUser;

const newUser = new User({
name: user.name,
googleid: user.sub,
});

return newUser.save();
});
}
  • The login function calls the verify function to validate the token with Google.
  • The login function then calls getOrCreateUser. This function checks if the user already exists in the database using the googleid. If the user exists, it returns the existing user; if not, it creates a new user in the database.
  • Then, the login function stores the user information in the session by req.session.user = user.
  • sends the user data back to the client.

req.session and authenticating following requests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// In server.js
const session = require("express-session");

app.use(
session({
secret: "session-secret",
resave: false,
saveUninitialized: false,
})
);

const auth = require('./auth');
app.use(auth.populateCurrentUser);

// In auth.js
function populateCurrentUser(req, res, next) {
req.user = req.session.user;
next();
}
  • app.use(session({...})):

    • This middleware sets up and manages sessions in an Express application.
    • It uses a server-side storage mechanism to store session data.
    • It creates a session ID, which is stored in a cookie on the client side.
  • Previously, we do req.session.user = user, the following happens:

    • The express-session middleware stores the user object in the session data associated with the current session on server side.
    • The session middleware ensures that a cookie containing the session ID is sent to the client’s browser.
    • For every subsequent request, the browser automatically includes the session ID cookie in the request headers. The session middleware parses the session ID from the client’s cookie, retrieves the session data, and attaches it to req.session. As a result, req.session is automatically filled by the user document that we previously stored in session.
  • populateCurrentUser middleware: The populateCurrentUser middleware sets req.user to the user data from req.session.user for each following request.

Stay Logged In At Frontend

Now, since the session ID is in my browser’s cookie, everytime I make a request, as long as the cookies exist, my req.user will be set as the user that I previously logged in.
To let frontend know that I am currently logged in, we can send a get request to backend, then backend checks req.user and sends back.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// At Frontend:
useEffect(() => {
get("/api/whoami").then((user) => {
if (user._id) {
setLoggedIn(true);
}
})
}, []);

// At Backend:
router.get("/whoami", (req, res) => {
if (req.user) {
res.send(req.user);
} else {
res.send({});
}
});

MIT Weblab笔记 - Authentication
https://thiefcat.github.io/2024/07/18/MIT-Weblab/account/
Author
小贼猫
Posted on
July 18, 2024
Licensed under