Upgrade OAuth setup to v3
Update database
You can continue using the keys table but we recommend creating a dedicated table for storing OAuth accounts, as shown in the database migration guides.
Replace OAuth integration
The OAuth integration has been replaced with Arctic, which provides everything the integration did without Lucia-specific APIs. It supports all the OAuth providers that the integration supported.
npm install arctic
You can initialize the providers without passing the Lucia instance and it does not accept scopes.
import { GitHub } from "arctic";
export const githubAuth = new GitHub(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET);
export const googleAuth = new Google(
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
"http://localhost:3000/login/github/callback"
);
Create authorization URL
createAuthorizationURL()
replaces getAuthorizationUrl()
. State and code verifier must generated on your side.
import { generateState, generateCodeVerifier } from "arctic";
// generate state
const state = generateState();
// pass state (and code verifier for PKCE)
// returns the authorization url only
const authorizationURL = await githubAuth.createAuthorizationURL(state, {
scopes: ["email"] // pass scopes here instead
});
setCookie("github_oauth_state", state, {
secure: true, // set to false in localhost
path: "/",
httpOnly: true,
maxAge: 60 * 10 // 10 min
});
// redirect to authorization url
Validate callback
The state
check stays the same.
validateAuthorizationCode()
replaces validateCallback()
. Instead of returning tokens, users, and database methods, it just returns tokens. Use the access token to get the user, then check if the user is already registered and create a new user if they aren't.
You now have to create users and manage OAuth accounts by yourself.
import { generateId } from "lucia";
// check for state
// ...
// only returns tokens
const tokens = await githubAuth.validateAuthorizationCode(code);
// use the access token to get the user
const githubUser = await githubAuth.getUser(tokens.accessToken);
const existingAccount = await db
.table("oauth_account")
.where("provider_id", "=", "github")
.where("provider_user_id", "=", githubUser.id)
.get();
if (existingAccount) {
// simplified `createSession()` - second param for session attributes
const session = await lucia.createSession(existingUser.id, {});
// `createSessionCookie()` now takes a session ID instead of the entire session object
const sessionCookie = lucia.createSessionCookie(session.id);
// set session cookie as usual (using `Response` as example)
return new Response(null, {
status: 302,
headers: {
Location: "/",
"Set-Cookie": sessionCookie.serialize()
}
});
}
// v2 IDs have a length of 15
const userId = generateId(15);
await db.beginTransaction();
// create user manually
await db.table("user").insert({
id: userId,
username: github.login
});
// store oauth account
await db.table("oauth_account").insert({
provider_id: "github",
provider_user_id: githubUser.id,
user_id: userId
});
await db.commit();
// simplified `createSession()` - second param for session attributes
const session = await lucia.createSession(userId, {});
// `createSessionCookie()` now takes a session ID instead of the entire session object
const sessionCookie = lucia.createSessionCookie(session.id);
// set session cookie as usual (using `Response` as example)
return new Response(null, {
status: 302,
headers: {
Location: "/",
"Set-Cookie": sessionCookie.serialize()
}
});
Error handling
Error handling has improved with v3. validateAuthorizationCode()
throws an OAuth2RequestError
, which includes proper error messages and descriptions.
try {
const tokens = await githubAuth.validateAuthorizationCode(code);
// ...
} catch (e) {
console.log(e);
if (e instanceof OAuth2RequestError) {
// bad verification code, invalid credentials, etc
return new Response(null, {
status: 400
});
}
return new Response(null, {
status: 500
});
}