Skip to content

Commit aa02920

Browse files
committed
First commit, working Recurse.com OAuth for ONE user with Oslo
0 parents  commit aa02920

6 files changed

+1953
-0
lines changed

.gitignore

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.DS_Store
2+
.idea
3+
*.log
4+
tmp/
5+
6+
*.tern-port
7+
node_modules/
8+
npm-debug.log*
9+
yarn-debug.log*
10+
yarn-error.log*
11+
*.tsbuildinfo
12+
.npm
13+
.eslintcache
14+
*.env

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Following [Lucia Getting Started in Express tutorial](https://lucia-auth.com/getting-started/express)
2+
3+
Node >=20 required (or see notes about Oslo installation for Node <20 [here](https://oslo.js.org))
4+
5+
```sh
6+
npm install;
7+
```
8+
9+
Get your Client ID and Client Secret by going to https://recurse.com/settings/apps, and click 'Create OAuth Application'. Use `http://localhost:3001/myOauth2RedirectUri` as the Redirect URI.
10+
11+
```sh
12+
node --env-file=config.env index.js
13+
```

config.env.template

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
OAUTH_CLIENT_ID=<your client id>
2+
OAUTH_CLIENT_SECRET=<your client secret>

index.js

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { OAuth2Client, generateState } from "oslo/oauth2";
2+
import { OAuth2RequestError } from "oslo/oauth2";
3+
import express from "express";
4+
5+
const app = express();
6+
const port = process.env.PORT || 3001;
7+
const authorizeEndpoint = "https://recurse.com/oauth/authorize";
8+
// TODO P.B. found this required `www` though authorize doesn't.
9+
const tokenEndpoint = "https://www.recurse.com/oauth/token";
10+
11+
// DO NOT COMMIT
12+
// From https://www.recurse.com/settings/apps
13+
const clientId = process.env.OAUTH_CLIENT_ID;
14+
const clientSecret = process.env.OAUTH_CLIENT_SECRET;
15+
16+
console.log(clientId, clientSecret);
17+
18+
const client = new OAuth2Client(clientId, authorizeEndpoint, tokenEndpoint, {
19+
redirectURI: `http://localhost:${port}/myOauth2RedirectUri`,
20+
});
21+
22+
// TODO: Use Lucia to put this in user's session
23+
const newUserSession = () => ({
24+
state: null,
25+
refresh_token: null,
26+
access_token: null,
27+
});
28+
29+
let userSession = newUserSession();
30+
31+
const page = ({ body, title }) => `
32+
<!DOCTYPE html>
33+
<html lang="en">
34+
<head>
35+
<title>${title}</title>
36+
<meta name="viewport" content="width=device-width, initial-scale=1" />
37+
<link rel="shortcut icon" type="image/png" href="favicon.png" />
38+
</head>
39+
<body>
40+
${body}
41+
</body>
42+
</html>
43+
`;
44+
45+
app.get("/", async (req, res) => {
46+
let authenticated = false;
47+
if (userSession.refresh_token) {
48+
try {
49+
// Again the oslo docs are wrong, or at least inspecific.
50+
// Source don't lie, though! https://github.com/pilcrowOnPaper/oslo/blob/main/src/oauth2/index.ts#L76
51+
const { access_token, refresh_token } = await client.refreshAccessToken(
52+
userSession.refresh_token,
53+
{
54+
credentials: clientSecret,
55+
authenticateWith: "request_body",
56+
},
57+
);
58+
59+
userSession.access_token = access_token;
60+
userSession.refresh_token = refresh_token;
61+
62+
authenticated = true;
63+
} catch (e) {
64+
if (e instanceof OAuth2RequestError) {
65+
// see https://www.rfc-editor.org/rfc/rfc6749#section-5.2
66+
const { request, message, description } = e;
67+
68+
throw e;
69+
}
70+
// unknown error
71+
throw e;
72+
}
73+
}
74+
const body = `
75+
<h1>Recurse OAuth Example with Oslo</h1>
76+
<p>${
77+
authenticated
78+
? 'You\'re logged in already - <a href="/logout">logout</a>'
79+
: '<a href="/getAuthorizationUrl">Authorize</a>'
80+
}
81+
</p>
82+
`;
83+
res.send(
84+
page({
85+
title: "Homepage",
86+
body,
87+
}),
88+
);
89+
});
90+
91+
app.get("/logout", async (req, res) => {
92+
userSession = newUserSession();
93+
94+
res.redirect("/");
95+
});
96+
97+
app.get("/getAuthorizationUrl", async (req, res) => {
98+
userSession.state = generateState();
99+
const url = await client.createAuthorizationURL({
100+
state: userSession.state,
101+
scope: ["user:email"],
102+
});
103+
res.redirect(url);
104+
});
105+
106+
app.get("/myOauth2RedirectUri", async (req, res) => {
107+
const { state, code } = req.query;
108+
109+
if (!userSession.state || !state || userSession.state !== state) {
110+
// TODO: Don't crash the server!
111+
throw new Error("State didn't match");
112+
}
113+
114+
try {
115+
// NOTE: This is different from the Oslo OAuth2 docs, they use camel case
116+
const { access_token, refresh_token } =
117+
await client.validateAuthorizationCode(code, {
118+
credentials: clientSecret,
119+
authenticateWith: "request_body",
120+
});
121+
122+
userSession.access_token = access_token;
123+
userSession.refresh_token = refresh_token;
124+
125+
res.redirect("/");
126+
return;
127+
} catch (e) {
128+
if (e instanceof OAuth2RequestError) {
129+
// see https://www.rfc-editor.org/rfc/rfc6749#section-5.2
130+
const { request, message, description } = e;
131+
132+
throw e;
133+
}
134+
// unknown error
135+
throw e;
136+
}
137+
});
138+
139+
//
140+
// Final 404/5XX handlers
141+
//
142+
app.use(function (err, req, res, next) {
143+
console.error("5XX", err, req, next);
144+
res.status(err?.status || 500);
145+
146+
res.send("5XX");
147+
});
148+
149+
app.use(function (req, res) {
150+
res.status(404);
151+
res.send("4XX");
152+
});
153+
const listener = app.listen(port, () => {
154+
console.log(`Server is available at http://localhost:${port}`);
155+
});
156+
157+
// So I can kill from local terminal with Ctrl-c
158+
// From https://github.com/strongloop/node-foreman/issues/118#issuecomment-475902308
159+
process.on("SIGINT", () => {
160+
listener.close(() => {
161+
process.exit(0);
162+
});
163+
});

0 commit comments

Comments
 (0)