feat: Implement oauth2 monitors (#3119)

* [empty commit] pull request for implement oauth2 monitor

* feat: implement oauth2 client credentials flow

* fix: auth methods clarification & error handling

* docs: fix JSdocs types and clarifications
This commit is contained in:
Henrik Gerdes
2023-08-02 09:40:19 +02:00
committed by GitHub
parent cda77c1a32
commit 42b5d30a33
8 changed files with 180 additions and 1 deletions

View File

@@ -75,6 +75,7 @@ class Database {
"patch-added-json-query.sql": true,
"patch-added-kafka-producer.sql": true,
"patch-add-certificate-expiry-status-page.sql": true,
"patch-monitor-oauth-cc.sql": true,
};
/**

View File

@@ -6,7 +6,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVA
SQL_DATETIME_FORMAT
} = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, mongodbPing, kafkaProducerAsync
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials,
} = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
@@ -154,6 +154,11 @@ class Monitor extends BeanModel {
grpcMetadata: this.grpcMetadata,
basic_auth_user: this.basic_auth_user,
basic_auth_pass: this.basic_auth_pass,
oauth_client_id: this.oauth_client_id,
oauth_client_secret: this.oauth_client_secret,
oauth_token_url: this.oauth_token_url,
oauth_scopes: this.oauth_scopes,
oauth_auth_method: this.oauth_auth_method,
pushToken: this.pushToken,
databaseConnectionString: this.databaseConnectionString,
radiusUsername: this.radiusUsername,
@@ -374,6 +379,24 @@ class Monitor extends BeanModel {
};
}
// OIDC: Basic client credential flow.
// Additional grants might be implemented in the future
let oauth2AuthHeader = {};
if (this.auth_method === "oauth2-cc") {
try {
if (this.oauthAccessToken === undefined || new Date(this.oauthAccessToken.expires_at * 1000) <= new Date()) {
log.debug("monitor", `[${this.name}] The oauth access-token undefined or expired. Requesting a new one`);
this.oauthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_auth_method);
log.debug("monitor", `[${this.name}] Obtained oauth access-token. Expires at ${new Date(this.oauthAccessToken.expires_at * 1000)}`);
}
oauth2AuthHeader = {
"Authorization": this.oauthAccessToken.token_type + " " + this.oauthAccessToken.access_token,
};
} catch (e) {
throw new Error("The oauth config is invalid. " + e.message);
}
}
const httpsAgentOptions = {
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
@@ -408,6 +431,7 @@ class Monitor extends BeanModel {
"User-Agent": "Uptime-Kuma/" + version,
...(contentType ? { "Content-Type": contentType } : {}),
...(basicAuthHeader),
...(oauth2AuthHeader),
...(this.headers ? JSON.parse(this.headers) : {})
},
maxRedirects: this.maxredirects,

View File

@@ -713,6 +713,11 @@ let needSetup = false;
bean.headers = monitor.headers;
bean.basic_auth_user = monitor.basic_auth_user;
bean.basic_auth_pass = monitor.basic_auth_pass;
bean.oauth_client_id = monitor.oauth_client_id,
bean.oauth_client_secret = monitor.oauth_client_secret,
bean.oauth_auth_method = this.oauth_auth_method,
bean.oauth_token_url = monitor.oauth_token_url,
bean.oauth_scopes = monitor.oauth_scopes,
bean.tlsCa = monitor.tlsCa;
bean.tlsCert = monitor.tlsCert;
bean.tlsKey = monitor.tlsKey;

View File

@@ -21,6 +21,8 @@ const grpc = require("@grpc/grpc-js");
const protojs = require("protobufjs");
const radiusClient = require("node-radius-client");
const redis = require("redis");
const oidc = require("openid-client");
const {
dictionaries: {
rfc2865: { file, attributes },
@@ -52,6 +54,43 @@ exports.initJWTSecret = async () => {
return jwtSecretBean;
};
/**
* Decodes a jwt and returns the payload portion without verifying the jqt.
* @param {string} jwt The input jwt as a string
* @returns {Object} Decoded jwt payload object
*/
exports.decodeJwt = (jwt) => {
return JSON.parse(Buffer.from(jwt.split(".")[1], "base64").toString());
};
/**
* Gets a Access Token form a oidc/oauth2 provider
* @param {string} tokenEndpoint The token URI form the auth service provider
* @param {string} clientId The oidc/oauth application client id
* @param {string} clientSecret The oidc/oauth application client secret
* @param {string} scope The scope the for which the token should be issued for
* @param {string} authMethod The method on how to sent the credentials. Default client_secret_basic
* @returns {Promise<oidc.TokenSet>} TokenSet promise if the token request was successful
*/
exports.getOidcTokenClientCredentials = async (tokenEndpoint, clientId, clientSecret, scope, authMethod = "client_secret_basic") => {
const oauthProvider = new oidc.Issuer({ token_endpoint: tokenEndpoint });
let client = new oauthProvider.Client({
client_id: clientId,
client_secret: clientSecret,
token_endpoint_auth_method: authMethod
});
// Increase default timeout and clock tolerance
client[oidc.custom.http_options] = () => ({ timeout: 10000 });
client[oidc.custom.clock_tolerance] = 5;
let grantParams = { grant_type: "client_credentials" };
if (scope) {
grantParams.scope = scope;
}
return await client.grant(grantParams);
};
/**
* Send TCP request to specified hostname and port
* @param {string} hostname Hostname / address of machine