Drive working with SSO
This commit is contained in:
@@ -1,4 +1,15 @@
|
||||
{
|
||||
"general": {
|
||||
"accounts": {
|
||||
"type": "ACCOUNTS_TYPE",
|
||||
"remote": {
|
||||
"authority": "SSO_AUTHORITY",
|
||||
"client_id": "SSO_CLIENT_ID",
|
||||
"issuer": "SSO_ISSUER",
|
||||
"jwks_uri": "SSO_JWKS_URI"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sentry": {
|
||||
"dsn": "SENTRY_DSN"
|
||||
},
|
||||
|
||||
@@ -120,7 +120,6 @@
|
||||
"@fastify/formbody": "^6.0.0",
|
||||
"@fastify/static": "^5.0.1",
|
||||
"@ffprobe-installer/ffprobe": "^1.4.1",
|
||||
"@okta/jwt-verifier": "^3.0.1",
|
||||
"@sentry/node": "^6.19.7",
|
||||
"@sentry/tracing": "^6.19.7",
|
||||
"@socket.io/redis-adapter": "^7.1.0",
|
||||
@@ -163,6 +162,7 @@
|
||||
"html-to-text": "^8.2.1",
|
||||
"idtoken-verifier": "^2.2.3",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwks-rsa": "^3.0.1",
|
||||
"keyv": "^4.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"match-all": "^1.2.6",
|
||||
@@ -170,6 +170,7 @@
|
||||
"moment": "^2.29.4",
|
||||
"mongodb": "^4.1.0",
|
||||
"multistream": "^4.1.0",
|
||||
"njwt": "^2.0.0",
|
||||
"node-cache": "^5.1.2",
|
||||
"node-cron": "^3.0.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
|
||||
@@ -9,7 +9,7 @@ class StaticConsoleClientFactory {
|
||||
const type: ConsoleType = consoleInstance.consoleType;
|
||||
switch (type) {
|
||||
case "remote":
|
||||
return new ConsoleRemoteClient(consoleInstance, false);
|
||||
return new ConsoleRemoteClient(consoleInstance);
|
||||
case "internal":
|
||||
return new ConsoleInternalClient(consoleInstance);
|
||||
default:
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface ConsoleServiceClient {
|
||||
|
||||
updateLocalCompanyFromConsole(companyDTO: ConsoleHookCompany): Promise<Company>;
|
||||
|
||||
updateLocalUserFromConsole(code: string): Promise<User>;
|
||||
updateLocalUserFromConsole(user: ConsoleHookUser): Promise<User>;
|
||||
|
||||
removeCompanyUser(consoleUserId: string, company: Company): Promise<void>;
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ export class ConsoleInternalClient implements ConsoleServiceClient {
|
||||
throw new Error("Method should not be implemented.");
|
||||
}
|
||||
|
||||
updateLocalUserFromConsole(_code: string): Promise<User> {
|
||||
updateLocalUserFromConsole(_user: ConsoleHookUser): Promise<User> {
|
||||
logger.info("Internal: updateLocalUserFromConsole");
|
||||
throw new Error("Method should not be implemented.");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import JwksClient from "jwks-rsa";
|
||||
import nJwt from "njwt";
|
||||
|
||||
function verifyAudience(expected: string, aud: string | Array<string>) {
|
||||
if (!expected) {
|
||||
throw new Error("expected audience is required");
|
||||
}
|
||||
|
||||
if (Array.isArray(aud) && !aud.includes(expected)) {
|
||||
throw new Error(
|
||||
`audience claim ${expected} does not match one of the expected audiences: ${aud.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!Array.isArray(aud) && aud !== expected) {
|
||||
throw new Error(`audience claim ${aud} does not match expected audience: ${expected}`);
|
||||
}
|
||||
}
|
||||
|
||||
function verifyIssuer(expected: string, issuer: string) {
|
||||
if (issuer !== expected) {
|
||||
throw new Error(`issuer ${issuer} does not match expected issuer: ${expected}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class OidcJwtVerifier {
|
||||
public claimsToAssert: any;
|
||||
private issuer: string;
|
||||
private jwksUri: string;
|
||||
private jwksClient: any;
|
||||
private verifier: any;
|
||||
|
||||
constructor(options: { [key: string]: any } = {}) {
|
||||
// https://github.com/auth0/node-jwks-rsa/blob/master/CHANGELOG.md#request-agent-options
|
||||
if (options.requestAgentOptions) {
|
||||
// jwks-rsa no longer accepts 'requestAgentOptions' and instead requires a http(s).Agent be passed directly
|
||||
const msg = `\`requestAgentOptions\` has been deprecated, use \`requestAgent\` instead.
|
||||
For more info see https://github.com/auth0/node-jwks-rsa/blob/master/CHANGELOG.md#request-agent-options`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
this.claimsToAssert = options.assertClaims || {};
|
||||
this.issuer = options.issuer;
|
||||
this.jwksUri = options.jwksUri;
|
||||
this.jwksClient = JwksClient({
|
||||
jwksUri: this.jwksUri,
|
||||
cache: true,
|
||||
cacheMaxAge: options.cacheMaxAge || 60 * 60 * 1000,
|
||||
cacheMaxEntries: 3,
|
||||
jwksRequestsPerMinute: options.jwksRequestsPerMinute || 10,
|
||||
rateLimit: true,
|
||||
requestAgent: options.requestAgent,
|
||||
});
|
||||
this.verifier = nJwt
|
||||
.createVerifier()
|
||||
.setSigningAlgorithm("RS256")
|
||||
.withKeyResolver((kid: string, cb: any) => {
|
||||
if (kid) {
|
||||
this.jwksClient.getSigningKey(kid, (err: any, key: any) => {
|
||||
cb(err, key && (key.publicKey || key.rsaPublicKey));
|
||||
});
|
||||
} else {
|
||||
cb("No KID specified", null);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
async verifyAsPromise(tokenString: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Convert to a promise
|
||||
this.verifier.verify(tokenString, (err: any, jwt: any) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
const jwtBodyProxy = new Proxy(jwt.body, {});
|
||||
Object.defineProperty(jwt, "claims", {
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
value: jwtBodyProxy,
|
||||
});
|
||||
|
||||
delete jwt.body;
|
||||
resolve(jwt);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async verifyIdToken(idTokenString: string, expectedClientId: string) {
|
||||
const jwt = await this.verifyAsPromise(idTokenString);
|
||||
verifyAudience(expectedClientId, jwt.claims.aud);
|
||||
verifyIssuer(this.issuer, jwt.claims.iss);
|
||||
|
||||
return jwt;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AxiosInstance } from "axios";
|
||||
import { Issuer } from "openid-client";
|
||||
import { ConsoleServiceClient } from "../client-interface";
|
||||
import {
|
||||
ConsoleCompany,
|
||||
@@ -14,25 +13,33 @@ import {
|
||||
UpdatedConsoleUserRole,
|
||||
} from "../types";
|
||||
|
||||
import OktaJwtVerifier from "@okta/jwt-verifier";
|
||||
import { OidcJwtVerifier } from "./remote-jwks-verifier";
|
||||
import { CrudException } from "../../../core/platform/framework/api/crud-service";
|
||||
import { logger } from "../../../core/platform/framework/logger";
|
||||
import gr from "../../global-resolver";
|
||||
import Company, { CompanySearchKey } from "../../user/entities/company";
|
||||
import User from "../../user/entities/user";
|
||||
import User, { getInstance } from "../../user/entities/user";
|
||||
import { getInstance as getCompanyInstance } from "../../user/entities/company";
|
||||
import { ConsoleServiceImpl } from "../service";
|
||||
import coalesce from "../../../utils/coalesce";
|
||||
|
||||
export class ConsoleRemoteClient implements ConsoleServiceClient {
|
||||
version: "1";
|
||||
client: AxiosInstance;
|
||||
|
||||
private infos: ConsoleOptions;
|
||||
private verifier: OktaJwtVerifier;
|
||||
private verifier: OidcJwtVerifier;
|
||||
|
||||
constructor(consoleInstance: ConsoleServiceImpl, private dryRun: boolean) {
|
||||
constructor(consoleInstance: ConsoleServiceImpl) {
|
||||
this.infos = consoleInstance.consoleOptions;
|
||||
this.verifier = new OktaJwtVerifier({
|
||||
issuer: this.infos.issuer,
|
||||
this.verifier = new OidcJwtVerifier({
|
||||
clientId: this.infos.client_id,
|
||||
issuer: this.infos.issuer?.replace(/\/+$/, ""),
|
||||
jwksUri: this.infos.jwks_uri,
|
||||
// For local deployment create a https agent that ignore self signed certificate
|
||||
requestAgent: new (require("https").Agent)({
|
||||
rejectUnauthorized: this.infos.issuer.includes("example.com") ? false : true,
|
||||
}),
|
||||
});
|
||||
}
|
||||
fetchCompanyInfo(consoleCompanyCode: string): Promise<ConsoleHookCompany> {
|
||||
@@ -77,8 +84,86 @@ export class ConsoleRemoteClient implements ConsoleServiceClient {
|
||||
return null;
|
||||
}
|
||||
|
||||
async updateLocalUserFromConsole(code: string): Promise<User> {
|
||||
return null;
|
||||
async updateLocalUserFromConsole(userDTO: ConsoleHookUser): Promise<User> {
|
||||
logger.info("Remote: updateLocalUserFromConsole");
|
||||
|
||||
if (!userDTO) {
|
||||
throw CrudException.badRequest("User not found on Console");
|
||||
}
|
||||
|
||||
const roles = userDTO.roles.filter(
|
||||
role => role.applications === undefined || role.applications.find(a => a.code === "twake"),
|
||||
);
|
||||
|
||||
let user = await gr.services.users.getByConsoleId(userDTO.email);
|
||||
|
||||
if (!user) {
|
||||
if (!userDTO.email) {
|
||||
throw CrudException.badRequest("Email is required");
|
||||
}
|
||||
|
||||
let username = userDTO.email
|
||||
.split("@")[0]
|
||||
.toLocaleLowerCase()
|
||||
.replace(/[^a-zA-Z0-9]/g, "")
|
||||
.replace(/ +/g, "_");
|
||||
|
||||
if (await gr.services.users.isEmailAlreadyInUse(userDTO.email)) {
|
||||
throw CrudException.badRequest("Console user not created because email already exists");
|
||||
}
|
||||
|
||||
username = await gr.services.users.getAvailableUsername(username);
|
||||
if (!username) {
|
||||
throw CrudException.badRequest("Console user not created because username already exists");
|
||||
}
|
||||
|
||||
user = getInstance({});
|
||||
user.username_canonical = (username || "").toLocaleLowerCase();
|
||||
user.email_canonical = userDTO.email;
|
||||
user.deleted = false;
|
||||
}
|
||||
|
||||
user.email_canonical = coalesce(userDTO.email, user.email_canonical);
|
||||
user.phone = "";
|
||||
user.first_name = coalesce(userDTO.name, user.first_name);
|
||||
user.last_name = coalesce(userDTO.surname, user.last_name);
|
||||
user.identity_provider = "console";
|
||||
user.identity_provider_id = userDTO.email;
|
||||
user.mail_verified = coalesce(userDTO.isVerified, user.mail_verified);
|
||||
if (userDTO.preference) {
|
||||
user.preferences = user.preferences || {};
|
||||
user.preferences.allow_tracking = coalesce(
|
||||
userDTO.preference.allowTrackingPersonalInfo,
|
||||
user.preferences?.allow_tracking,
|
||||
);
|
||||
user.preferences.language = coalesce(userDTO.preference.locale, user.preferences?.language);
|
||||
user.preferences.timezone = coalesce(userDTO.preference.timeZone, user.preferences?.timezone);
|
||||
}
|
||||
|
||||
user.picture = userDTO.avatar.value;
|
||||
|
||||
await gr.services.users.save(user);
|
||||
|
||||
//For now TDrive works with only one company as we don't get it from the SSO
|
||||
|
||||
let company = await gr.services.companies.getCompany({
|
||||
id: "00000000-0000-4000-0000-000000000000",
|
||||
});
|
||||
if (!company) {
|
||||
company = await gr.services.companies.createCompany(
|
||||
getCompanyInstance({
|
||||
id: "00000000-0000-4000-0000-000000000000",
|
||||
name: "Twake",
|
||||
plan: { name: "Local", limits: undefined, features: undefined },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await gr.services.companies.setUserRole(company.id, user.id, "admin");
|
||||
|
||||
await gr.services.users.save(user, { user: { id: user.id, server_request: true } });
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async removeCompanyUser(consoleUserId: string, company: Company): Promise<void> {
|
||||
@@ -114,10 +199,17 @@ export class ConsoleRemoteClient implements ConsoleServiceClient {
|
||||
}
|
||||
|
||||
async getUserByAccessToken(idToken: string): Promise<ConsoleHookUser> {
|
||||
const user = (await this.verifier.verifyIdToken(idToken, this.infos.audience)).claims as any;
|
||||
console.log("user", user);
|
||||
const user = (await this.verifier.verifyIdToken(idToken, this.infos.client_id))?.claims as {
|
||||
sub: string;
|
||||
email: string;
|
||||
family_name: string;
|
||||
given_name: string;
|
||||
name: string;
|
||||
locale?: string;
|
||||
picture?: string;
|
||||
};
|
||||
return {
|
||||
_id: user.sub,
|
||||
_id: user.email || user.sub,
|
||||
roles: [] as any,
|
||||
email: user.email,
|
||||
name: user.given_name,
|
||||
|
||||
@@ -37,6 +37,7 @@ export class ConsoleServiceImpl implements TwakeServiceProvider {
|
||||
client_secret: s.client_secret,
|
||||
audience: s.audience,
|
||||
issuer: s.issuer,
|
||||
jwks_uri: s.jwks_uri,
|
||||
redirect_uris: s.redirect_uris,
|
||||
disable_account_creation: s.disable_account_creation,
|
||||
};
|
||||
|
||||
@@ -130,6 +130,7 @@ export type ConsoleOptions = {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
issuer: string;
|
||||
jwks_uri: string;
|
||||
audience: string;
|
||||
redirect_uris: string[];
|
||||
disable_account_creation: boolean;
|
||||
|
||||
@@ -222,7 +222,7 @@ export class ConsoleController {
|
||||
|
||||
private async userAdded(content: ConsoleHookBodyContent): Promise<void> {
|
||||
const userDTO = content.user;
|
||||
const user = await gr.services.console.getClient().updateLocalUserFromConsole(userDTO._id);
|
||||
const user = await gr.services.console.getClient().updateLocalUserFromConsole(userDTO);
|
||||
await this.updateCompany(content.company);
|
||||
await gr.services.console.processPendingUser(user);
|
||||
}
|
||||
@@ -237,8 +237,8 @@ export class ConsoleController {
|
||||
}
|
||||
|
||||
private async userUpdated(code: string) {
|
||||
const user = await gr.services.console.getClient().updateLocalUserFromConsole(code);
|
||||
await gr.services.console.processPendingUser(user);
|
||||
//Not implemented yet
|
||||
throw CrudException.notImplemented("Unimplemented");
|
||||
}
|
||||
|
||||
private async companyRemoved(content: ConsoleHookCompanyDeletedContent) {
|
||||
@@ -286,7 +286,7 @@ export class ConsoleController {
|
||||
private async authByToken(idToken: string): Promise<AccessToken> {
|
||||
const client = gr.services.console.getClient();
|
||||
const userDTO = await client.getUserByAccessToken(idToken);
|
||||
const user = await client.updateLocalUserFromConsole(userDTO._id);
|
||||
const user = await client.updateLocalUserFromConsole(userDTO);
|
||||
if (!user) {
|
||||
throw CrudException.notFound(`User details not found for access token ${idToken}`);
|
||||
}
|
||||
|
||||
@@ -169,14 +169,6 @@ export class WorkspaceInviteTokensCrudController
|
||||
inviterEmail: inviter.email_canonical,
|
||||
},
|
||||
);
|
||||
|
||||
await gr.services.console
|
||||
.getClient()
|
||||
.updateLocalUserFromConsole(user.identity_provider_id);
|
||||
companyUser = await gr.services.companies.getCompanyUser(
|
||||
{ id: company_id },
|
||||
{ id: userId },
|
||||
);
|
||||
}
|
||||
if (!companyUser) {
|
||||
throw CrudException.badRequest("Unable to add user to the company");
|
||||
|
||||
@@ -1456,14 +1456,6 @@
|
||||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@okta/jwt-verifier@^3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@okta/jwt-verifier/-/jwt-verifier-3.0.1.tgz#10facad0424052e0cfd6477304cdc51d84ac60be"
|
||||
integrity sha512-8rNvCxAcgGlSJH9NilhYTrbZjyTPPF0ThJUDF/NI8XV/4NcqOFdO946meFsXpV1oFdZP6ASH/j2GjVhU4cHpPQ==
|
||||
dependencies:
|
||||
jwks-rsa "^3.0.0"
|
||||
njwt "^2.0.0"
|
||||
|
||||
"@redis/bloom@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.0.2.tgz#42b82ec399a92db05e29fffcdfd9235a5fc15cdf"
|
||||
@@ -6760,7 +6752,7 @@ jwa@^1.4.1:
|
||||
ecdsa-sig-formatter "1.0.11"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
jwks-rsa@^3.0.0:
|
||||
jwks-rsa@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-3.0.1.tgz#ba79ddca7ee7520f7bb26b942ef1aee91df8d7e4"
|
||||
integrity sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==
|
||||
|
||||
@@ -44,15 +44,13 @@ export default class OIDCAuthProviderService
|
||||
Oidc.Log.logger = Logger.getLogger('OIDCClient');
|
||||
Oidc.Log.level = EnvironmentService.isProduction() ? Oidc.Log.WARN : Oidc.Log.DEBUG;
|
||||
|
||||
const audience = ['https://dev-mxb6x0f2.eu.auth0.com/api/v2/'];
|
||||
|
||||
this.userManager = new Oidc.UserManager({
|
||||
userStore: new Oidc.WebStorageStateStore({ store: window.localStorage }),
|
||||
authority: this.configuration?.authority || environment.api_root_url,
|
||||
client_id: this.configuration?.client_id,
|
||||
redirect_uri: getAsFrontUrl(OIDC_CALLBACK_URL),
|
||||
response_type: 'code',
|
||||
scope: 'openid profile email address phone offline_access ' + audience.join(' '),
|
||||
scope: 'openid profile email address phone offline_access',
|
||||
post_logout_redirect_uri: getAsFrontUrl(OIDC_SIGNOUT_URL),
|
||||
//silent_redirect_uri: getAsFrontUrl(OIDC_SILENT_URL),
|
||||
automaticSilentRenew: true,
|
||||
|
||||
@@ -117,7 +117,7 @@ class ConsoleService {
|
||||
);
|
||||
Api.post(
|
||||
'/internal/services/console/v1/login',
|
||||
{ oidc_id_token: currentToken.id_token },
|
||||
{ oidc_id_token: currentToken.access_token },
|
||||
(response: {
|
||||
access_token: JWTDataType;
|
||||
message: string;
|
||||
|
||||
Reference in New Issue
Block a user