Drive working with SSO
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user