Skip to content

Commit 4e3d196

Browse files
authored
Support Google OAuth2 token authentication (#419)
Fixes #413
1 parent 9043d4c commit 4e3d196

File tree

7 files changed

+94
-10
lines changed

7 files changed

+94
-10
lines changed

api/v1/controllers/login.mjs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,18 @@ import * as Settings from '../../../lib/settings.mjs'
1919
import * as ApiKey from '../../../model/apikey.mjs'
2020
import * as Metrics from '../../../lib/metrics.mjs'
2121
import * as Folder from '../../../model/folder.mjs'
22+
import * as GOAuth2 from 'google-auth-library'
2223

2324
import DB from '../../../lib/db.mjs'
2425

26+
// Google OAuth2 setup
27+
let GOAuth2Client = null
28+
if (Config.get().auth?.google_oauth2?.enabled === true) {
29+
GOAuth2Client = new GOAuth2.OAuth2Client(
30+
process.env.GOOGLE_CLIENT_ID
31+
)
32+
}
33+
2534
/**
2635
* Login
2736
* @param {Object} req Express request
@@ -40,12 +49,14 @@ export async function login (req, res, next) {
4049
username: req.body.username?.toLowerCase() || '',
4150
password: req.body?.password || '',
4251
apikey: req.body?.apikey || '',
43-
secret: req.body?.secret || ''
52+
secret: req.body?.secret || '',
53+
googleoauth2token: req.body?.googleoauth2token || ''
4454
}
4555

4656
// If an API key is provided, validate it and get the user
4757
let isapikey = false
4858
let apikeydescription = ''
59+
let isGoogleToken = false
4960
if (data.apikey && data.secret) {
5061
if (!await ApiKey.exists(data.apikey)) {
5162
await Events.add(data.username, Const.EV_ACTION_LOGIN_APIKEY_NOTFOUND, Const.EV_ENTITY_APIKEY, data.apikey)
@@ -109,6 +120,41 @@ export async function login (req, res, next) {
109120
isapikey = true
110121
}
111122

123+
// If a Google OAuth2 token is provided, validate it and get the user
124+
if (GOAuth2Client && data.googleoauth2token) {
125+
try {
126+
// Extract info from ID token
127+
const ticket = await GOAuth2Client.verifyIdToken({
128+
idToken: data.googleoauth2token,
129+
audience: process.env.GOOGLE_CLIENT_ID
130+
})
131+
const payload = ticket.getPayload()
132+
133+
if (!payload?.email) {
134+
res.status(R.UNAUTHORIZED).send(R.ko('Google OAuth2 authentication failed'))
135+
return
136+
}
137+
138+
// Get corresponding user
139+
const user = await DB.users.findFirst({
140+
where: { email: payload.email },
141+
select: { login: true }
142+
})
143+
144+
if (user === null) {
145+
await Events.add(payload.email, Const.EV_ACTION_LOGIN_USERNOTFOUND, Const.EV_ENTITY_USER, payload.email)
146+
res.status(R.UNAUTHORIZED).send(R.ko('Bad user or wrong password'))
147+
return
148+
}
149+
150+
data.username = user.login
151+
isGoogleToken = true
152+
} catch (err) {
153+
res.status(R.UNAUTHORIZED).send(R.ko('Google OAuth2 authentication failed'))
154+
return
155+
}
156+
}
157+
112158
// Check user
113159
const user = await DB.users.findUnique({
114160
where: { login: data.username }
@@ -193,7 +239,7 @@ export async function login (req, res, next) {
193239
}
194240

195241
// Local authentication
196-
if (!isapikey && user.authmethod === 'local') {
242+
if (!isapikey && !isGoogleToken && user.authmethod === 'local') {
197243
if (!await Crypt.checkPassword(data.password, user.secret)) {
198244
await Events.add(null, Const.EV_ACTION_LOGINFAILED, Const.EV_ENTITY_USER, data.username)
199245
res.status(R.UNAUTHORIZED).send(R.ko('Bad user or wrong password'))

config-skel.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,10 @@
4040
"readonly": false,
4141
"enable_metrics": true,
4242
"generated_password_length": 15,
43-
"cache-control": ""
43+
"cache-control": "",
44+
"auth": {
45+
"google_oauth2": {
46+
"enabled": true
47+
}
48+
}
4449
}

docs/apidoc/paths/login.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ post:
55
summary: Login
66

77
description: |
8-
Login in and return a JWT token; username and password must be provided for regular login, while apikey and secret are used for API keys logins.
8+
Login in and return a JWT token:
9+
- `username` and `password` must be provided for regular login
10+
- `apikey` and `secret` are used for API keys logins.
11+
- `googleoauth2token` (Google OAuth2 token) can be provided to validate the token and log the user in
12+
913
The returned HS512 signed JWT token must be used in subsequent requests.
1014
requestBody:
1115
required: true

docs/apidoc/requestbodies/login.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,7 @@ loginBody:
1414
example: abcd
1515
secret:
1616
type: string
17-
example: abcd
17+
example: abcd
18+
googleoauth2token:
19+
type: string
20+
example: ya29.a0AfH6SMB...

docs/index.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ This are the software you need to have in order to run PassWeaver API:
2727
These are the features this API support, in random order:
2828

2929
- Cloud KMS integration (currently, only Google Cloud KMS)
30+
- Login via Google OAuth2 token validation
3031
- API keys, with IP whitelist and day of week/time whitelist
3132
- Personal folders for each user
3233
- Favorite items
@@ -143,6 +144,14 @@ PassWeaver API users can be authenticated via these methods:
143144
- Local: the user password hash is stored locally in the database
144145
- LDAP: authenticate against a LDAP/Active Directory server
145146
- API key: authenticate only via an existing API key
147+
- Google OAuth2 token validation: see below
148+
149+
### Google OAuth2 token validation
150+
151+
You can integrate your frontend with Google OAuth2 (PassWeaver GUI supports is), and once you obtain a valid token PassWeaver API can validate it and obtain the informations to log you in:
152+
it will look for an existing user with the email obtained from the token.
153+
154+
In order to enable Google OAuth2, you have to set auth.google_oauth2 in the configuration, and export GOOGLE_CLIENT_ID of your Google API Key in your environment.
146155

147156
## Authorization
148157

@@ -434,6 +443,10 @@ Copy `config-skel.json` to `config.json` and adjust the options (all options are
434443
- `enable_metrics`: true or false, enables Prometheus-formatted metrics
435444
- `generated_password_length`: default length of random generated password (default is 15)
436445
- `cache-control`: Cache-Control header to be sent along GET/HEAD responses
446+
- `auth`:
447+
- `google_oauth2`:
448+
- `enabled`: if true, the login endpoint will accept the token for authenticating with Google OAuth2 token. Note that you have to set "GOOGLE_CLIENT_ID" in your environment
449+
to your API Key Client ID.
437450

438451
## 5. Prepare the database
439452

lib/schemas/login.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
"$id": "login",
33
"type": "object",
44
"properties": {
5-
"username" : { "type": "string", "maxLength": 50 },
6-
"password" : { "type": "string", "maxLength": 100 },
7-
"apikey": { "type": "string", "maxLength": 50 },
8-
"secret": { "type": "string", "maxLength": 100 }
5+
"username" : { "type": "string", "maxLength": 50, "nullable": true },
6+
"password" : { "type": "string", "maxLength": 100, "nullable": true },
7+
"apikey": { "type": "string", "maxLength": 50, "nullable": true },
8+
"secret": { "type": "string", "maxLength": 100, "nullable": true },
9+
"googleoauth2token": { "type": "string", "maxLength": 10000, "nullable": true }
910
}
1011
}

lib/schemas/system_config.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,19 @@
7777
"readonly": { "type": "boolean" },
7878
"enable_metrics": { "type": "boolean" },
7979
"generated_password_length": { "type": "integer", "minimum": 10, "maximum": 50 },
80-
"cache-control": { "type": "string" }
80+
"cache-control": { "type": "string" },
81+
"auth": {
82+
"type": "object",
83+
"properties": {
84+
"google_oauth2": {
85+
"type": "object",
86+
"properties": {
87+
"enabled": { "type": "boolean" }
88+
},
89+
"required": [ "enabled" ]
90+
}
91+
}
92+
}
8193
},
8294
"required": ["jwt_duration", "listen", "log", "https", "redis", "onetimetokens", "readonly", "crypto"]
8395
}

0 commit comments

Comments
 (0)