Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions api/v1/controllers/login.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,18 @@ import * as Settings from '../../../lib/settings.mjs'
import * as ApiKey from '../../../model/apikey.mjs'
import * as Metrics from '../../../lib/metrics.mjs'
import * as Folder from '../../../model/folder.mjs'
import * as GOAuth2 from 'google-auth-library'

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

// Google OAuth2 setup
let GOAuth2Client = null
if (Config.get().auth?.google_oauth2?.enabled === true) {
GOAuth2Client = new GOAuth2.OAuth2Client(
process.env.GOOGLE_CLIENT_ID
)
}

/**
* Login
* @param {Object} req Express request
Expand All @@ -40,12 +49,14 @@ export async function login (req, res, next) {
username: req.body.username?.toLowerCase() || '',
password: req.body?.password || '',
apikey: req.body?.apikey || '',
secret: req.body?.secret || ''
secret: req.body?.secret || '',
googleoauth2token: req.body?.googleoauth2token || ''
}

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

// If a Google OAuth2 token is provided, validate it and get the user
if (GOAuth2Client && data.googleoauth2token) {
try {
// Extract info from ID token
const ticket = await GOAuth2Client.verifyIdToken({
idToken: data.googleoauth2token,
audience: process.env.GOOGLE_CLIENT_ID
})
const payload = ticket.getPayload()

if (!payload?.email) {
res.status(R.UNAUTHORIZED).send(R.ko('Google OAuth2 authentication failed'))
return
}

// Get corresponding user
const user = await DB.users.findFirst({
where: { email: payload.email },
select: { login: true }
})

if (user === null) {
await Events.add(payload.email, Const.EV_ACTION_LOGIN_USERNOTFOUND, Const.EV_ENTITY_USER, payload.email)
res.status(R.UNAUTHORIZED).send(R.ko('Bad user or wrong password'))
return
}

data.username = user.login
isGoogleToken = true
} catch (err) {
res.status(R.UNAUTHORIZED).send(R.ko('Google OAuth2 authentication failed'))
return
}
}

// Check user
const user = await DB.users.findUnique({
where: { login: data.username }
Expand Down Expand Up @@ -193,7 +239,7 @@ export async function login (req, res, next) {
}

// Local authentication
if (!isapikey && user.authmethod === 'local') {
if (!isapikey && !isGoogleToken && user.authmethod === 'local') {
if (!await Crypt.checkPassword(data.password, user.secret)) {
await Events.add(null, Const.EV_ACTION_LOGINFAILED, Const.EV_ENTITY_USER, data.username)
res.status(R.UNAUTHORIZED).send(R.ko('Bad user or wrong password'))
Expand Down
7 changes: 6 additions & 1 deletion config-skel.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,10 @@
"readonly": false,
"enable_metrics": true,
"generated_password_length": 15,
"cache-control": ""
"cache-control": "",
"auth": {
"google_oauth2": {
"enabled": true
}
}
}
6 changes: 5 additions & 1 deletion docs/apidoc/paths/login.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ post:
summary: Login

description: |
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.
Login in and return a JWT token:
- `username` and `password` must be provided for regular login
- `apikey` and `secret` are used for API keys logins.
- `googleoauth2token` (Google OAuth2 token) can be provided to validate the token and log the user in

The returned HS512 signed JWT token must be used in subsequent requests.
requestBody:
required: true
Expand Down
5 changes: 4 additions & 1 deletion docs/apidoc/requestbodies/login.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ loginBody:
example: abcd
secret:
type: string
example: abcd
example: abcd
googleoauth2token:
type: string
example: ya29.a0AfH6SMB...
13 changes: 13 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ This are the software you need to have in order to run PassWeaver API:
These are the features this API support, in random order:

- Cloud KMS integration (currently, only Google Cloud KMS)
- Login via Google OAuth2 token validation
- API keys, with IP whitelist and day of week/time whitelist
- Personal folders for each user
- Favorite items
Expand Down Expand Up @@ -143,6 +144,14 @@ PassWeaver API users can be authenticated via these methods:
- Local: the user password hash is stored locally in the database
- LDAP: authenticate against a LDAP/Active Directory server
- API key: authenticate only via an existing API key
- Google OAuth2 token validation: see below

### Google OAuth2 token validation

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:
it will look for an existing user with the email obtained from the token.

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.

## Authorization

Expand Down Expand Up @@ -434,6 +443,10 @@ Copy `config-skel.json` to `config.json` and adjust the options (all options are
- `enable_metrics`: true or false, enables Prometheus-formatted metrics
- `generated_password_length`: default length of random generated password (default is 15)
- `cache-control`: Cache-Control header to be sent along GET/HEAD responses
- `auth`:
- `google_oauth2`:
- `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
to your API Key Client ID.

## 5. Prepare the database

Expand Down
9 changes: 5 additions & 4 deletions lib/schemas/login.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
"$id": "login",
"type": "object",
"properties": {
"username" : { "type": "string", "maxLength": 50 },
"password" : { "type": "string", "maxLength": 100 },
"apikey": { "type": "string", "maxLength": 50 },
"secret": { "type": "string", "maxLength": 100 }
"username" : { "type": "string", "maxLength": 50, "nullable": true },
"password" : { "type": "string", "maxLength": 100, "nullable": true },
"apikey": { "type": "string", "maxLength": 50, "nullable": true },
"secret": { "type": "string", "maxLength": 100, "nullable": true },
"googleoauth2token": { "type": "string", "maxLength": 10000, "nullable": true }
}
}
14 changes: 13 additions & 1 deletion lib/schemas/system_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,19 @@
"readonly": { "type": "boolean" },
"enable_metrics": { "type": "boolean" },
"generated_password_length": { "type": "integer", "minimum": 10, "maximum": 50 },
"cache-control": { "type": "string" }
"cache-control": { "type": "string" },
"auth": {
"type": "object",
"properties": {
"google_oauth2": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" }
},
"required": [ "enabled" ]
}
}
}
},
"required": ["jwt_duration", "listen", "log", "https", "redis", "onetimetokens", "readonly", "crypto"]
}