Skip to content

Commit 190c49b

Browse files
committed
feat: implement OAuth login with Google and Discord\n\nImplements OAuth 2.0 authentication with Discord and Google to simplify and speed up user access to Puter. This feature provides a seamless alternative to traditional registration, improving both user experience and security.\n\nChanges include:\n- Added OAuth configuration to backend config\n- Created OAuth service and router endpoints\n- Updated database schema to store OAuth user data\n- Added OAuth buttons to login and signup UI\n- Added OAuth provider icons\n\nResolves #1220\n\nai: true
1 parent 9180261 commit 190c49b

File tree

11 files changed

+698
-0
lines changed

11 files changed

+698
-0
lines changed

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@
5151
"javascript-time-ago": "^2.5.11",
5252
"json-colorizer": "^3.0.1",
5353
"open": "^10.1.0",
54+
"passport": "^0.7.0",
55+
"passport-discord": "^0.1.4",
56+
"passport-google-oauth20": "^2.0.0",
5457
"sharp": "^0.33.5",
5558
"sharp-bmp": "^0.1.5",
5659
"sharp-ico": "^0.1.5",

src/backend/src/boot/default_config.js

+24
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,30 @@ module.exports = {
2626
protocol: 'http',
2727
contact_email: '[email protected]',
2828

29+
// OAuth configuration
30+
oauth: {
31+
// Enable/disable OAuth providers
32+
enabled: true,
33+
34+
// Google OAuth configuration
35+
google: {
36+
enabled: false, // Set to true to enable Google OAuth
37+
clientID: 'YOUR_GOOGLE_CLIENT_ID',
38+
clientSecret: 'YOUR_GOOGLE_CLIENT_SECRET',
39+
callbackURL: '/auth/google/callback',
40+
scope: ['profile', 'email']
41+
},
42+
43+
// Discord OAuth configuration
44+
discord: {
45+
enabled: false, // Set to true to enable Discord OAuth
46+
clientID: 'YOUR_DISCORD_CLIENT_ID',
47+
clientSecret: 'YOUR_DISCORD_CLIENT_SECRET',
48+
callbackURL: '/auth/discord/callback',
49+
scope: ['identify', 'email']
50+
}
51+
},
52+
2953
services: {
3054
database: {
3155
engine: 'sqlite',
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright (C) 2024-present Puter Technologies Inc.
3+
*
4+
* This file is part of Puter.
5+
*
6+
* Puter is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published
8+
* by the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
/**
21+
* Migration to add OAuth-related fields to the user table
22+
*/
23+
module.exports = {
24+
/**
25+
* Apply the migration
26+
* @param {Object} db - Database connection
27+
*/
28+
async up(db) {
29+
await db.write(`
30+
ALTER TABLE user
31+
ADD COLUMN oauth_provider VARCHAR(50) NULL,
32+
ADD COLUMN oauth_id VARCHAR(255) NULL,
33+
ADD COLUMN oauth_data TEXT NULL,
34+
ADD INDEX idx_oauth_provider_id (oauth_provider, oauth_id)
35+
`);
36+
},
37+
38+
/**
39+
* Undo the migration
40+
* @param {Object} db - Database connection
41+
*/
42+
async down(db) {
43+
await db.write(`
44+
ALTER TABLE user
45+
DROP COLUMN oauth_provider,
46+
DROP COLUMN oauth_id,
47+
DROP COLUMN oauth_data,
48+
DROP INDEX idx_oauth_provider_id
49+
`);
50+
}
51+
};

src/backend/src/routers/auth/oauth.js

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright (C) 2024-present Puter Technologies Inc.
3+
*
4+
* This file is part of Puter.
5+
*
6+
* Puter is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published
8+
* by the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
"use strict";
20+
21+
const express = require('express');
22+
const router = new express.Router();
23+
const passport = require('passport');
24+
const config = require('../../config');
25+
const { get_taskbar_items } = require('../../helpers');
26+
const { Context } = require('../../util/context');
27+
28+
// Helper function to handle OAuth callback
29+
const handleOAuthCallback = async (req, res) => {
30+
if (!req.user) {
31+
return res.redirect('/login?error=oauth_failed');
32+
}
33+
34+
try {
35+
const svc_oauth = req.services.get('oauth');
36+
const { token, user } = await svc_oauth.createOAuthSession(req.user, { req });
37+
38+
// Set cookie
39+
res.cookie(config.cookie_name, token, {
40+
sameSite: 'none',
41+
secure: true,
42+
httpOnly: true,
43+
});
44+
45+
// Redirect to success page or main app
46+
return res.redirect('/');
47+
} catch (error) {
48+
console.error('OAuth callback error:', error);
49+
return res.redirect('/login?error=oauth_session_failed');
50+
}
51+
};
52+
53+
// Only enable OAuth routes if OAuth is enabled in config
54+
if (config.oauth?.enabled) {
55+
// Google OAuth routes
56+
if (config.oauth?.google?.enabled) {
57+
router.get('/auth/google', passport.authenticate('google', {
58+
scope: config.oauth.google.scope
59+
}));
60+
61+
router.get('/auth/google/callback',
62+
passport.authenticate('google', {
63+
failureRedirect: '/login?error=google_auth_failed',
64+
session: false
65+
}),
66+
handleOAuthCallback
67+
);
68+
}
69+
70+
// Discord OAuth routes
71+
if (config.oauth?.discord?.enabled) {
72+
router.get('/auth/discord', passport.authenticate('discord', {
73+
scope: config.oauth.discord.scope
74+
}));
75+
76+
router.get('/auth/discord/callback',
77+
passport.authenticate('discord', {
78+
failureRedirect: '/login?error=discord_auth_failed',
79+
session: false
80+
}),
81+
handleOAuthCallback
82+
);
83+
}
84+
85+
// Route to get current OAuth providers status
86+
router.get('/oauth/providers', (req, res) => {
87+
const providers = {};
88+
89+
if (config.oauth?.google?.enabled) {
90+
providers.google = true;
91+
}
92+
93+
if (config.oauth?.discord?.enabled) {
94+
providers.discord = true;
95+
}
96+
97+
if (config.oauth?.github?.enabled) {
98+
providers.github = true;
99+
}
100+
101+
return res.json({ providers });
102+
});
103+
}
104+
105+
module.exports = router;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (C) 2024-present Puter Technologies Inc.
3+
*
4+
* This file is part of Puter.
5+
*
6+
* Puter is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published
8+
* by the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
"use strict";
20+
const BaseService = require("./BaseService");
21+
const passport = require('passport');
22+
23+
/**
24+
* @class OAuthAPIService
25+
* @extends BaseService
26+
*
27+
* The OAuthAPIService class is responsible for integrating OAuth authentication routes
28+
* into the web server for the Puter application. It registers the necessary middleware
29+
* and routes for OAuth authentication with various providers.
30+
*/
31+
class OAuthAPIService extends BaseService {
32+
/**
33+
* Sets up the routes for OAuth authentication.
34+
* This method registers various OAuth endpoints with the web server.
35+
*/
36+
async ['__on_install.routes'] () {
37+
const { app } = this.services.get('web-server');
38+
39+
// Only register OAuth routes if OAuth is enabled
40+
if (this.global_config.oauth?.enabled) {
41+
// Initialize Passport middleware
42+
app.use(passport.initialize());
43+
44+
// Register OAuth router
45+
app.use(require('../routers/auth/oauth'));
46+
47+
this.log.info('OAuth API routes registered');
48+
}
49+
}
50+
51+
/**
52+
* Initialize the OAuth API service
53+
*/
54+
async _init() {
55+
if (this.global_config.oauth?.enabled) {
56+
this.log.info('OAuth API service initialized');
57+
}
58+
}
59+
}
60+
61+
module.exports = OAuthAPIService;

src/backend/src/services/PuterAPIService.js

+7
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ class PuterAPIService extends BaseService {
5454
app.use(require('../routers/auth/create-access-token'))
5555
app.use(require('../routers/auth/delete-own-user'))
5656
app.use(require('../routers/auth/configure-2fa'))
57+
58+
// OAuth routes (only active if OAuth is enabled in config)
59+
if (this.global_config.oauth?.enabled) {
60+
const passport = require('passport');
61+
app.use(passport.initialize());
62+
app.use(require('../routers/auth/oauth'))
63+
}
5764
app.use(require('../routers/drivers/call'))
5865
app.use(require('../routers/drivers/list-interfaces'))
5966
app.use(require('../routers/drivers/usage'))

0 commit comments

Comments
 (0)