2023-06-07 23:46:42 +08:00
|
|
|
import { AuthConfig, BaseAuth } from '@nocobase/auth';
|
2023-10-12 13:54:00 +08:00
|
|
|
import { AuthModel } from '@nocobase/plugin-auth';
|
2023-06-07 23:46:42 +08:00
|
|
|
import { SAML, SamlConfig } from '@node-saml/node-saml';
|
|
|
|
|
|
|
|
interface SAMLOptions {
|
|
|
|
ssoUrl?: string;
|
|
|
|
certificate?: string;
|
|
|
|
idpIssuer?: string;
|
|
|
|
http?: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class SAMLAuth extends BaseAuth {
|
|
|
|
constructor(config: AuthConfig) {
|
|
|
|
const { ctx } = config;
|
|
|
|
super({
|
|
|
|
...config,
|
|
|
|
userCollection: ctx.db.getCollection('users'),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-01-14 22:26:13 +08:00
|
|
|
getOptions(): SamlConfig {
|
2023-06-07 23:46:42 +08:00
|
|
|
const ctx = this.ctx;
|
|
|
|
const { ssoUrl, certificate, idpIssuer, http }: SAMLOptions = this.options?.saml || {};
|
|
|
|
const name = this.authenticator.get('name');
|
|
|
|
const protocol = http ? 'http' : 'https';
|
|
|
|
return {
|
2024-03-16 20:01:34 +08:00
|
|
|
callbackUrl: `${protocol}://${ctx.host}${process.env.API_BASE_PATH}saml:redirect?authenticator=${name}&__appName=${ctx.app.name}`,
|
2023-06-07 23:46:42 +08:00
|
|
|
entryPoint: ssoUrl,
|
|
|
|
issuer: name,
|
|
|
|
cert: certificate,
|
|
|
|
idpIssuer,
|
|
|
|
wantAssertionsSigned: false,
|
2024-01-14 22:26:13 +08:00
|
|
|
};
|
2023-06-07 23:46:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
async validate() {
|
|
|
|
const ctx = this.ctx;
|
|
|
|
const {
|
2023-10-12 13:54:00 +08:00
|
|
|
params: { values: samlResponse },
|
2023-06-07 23:46:42 +08:00
|
|
|
} = ctx.action;
|
|
|
|
const saml = new SAML(this.getOptions());
|
|
|
|
|
|
|
|
const { profile } = await saml.validatePostResponseAsync(samlResponse);
|
2023-10-12 13:54:00 +08:00
|
|
|
const { nameID, nickname, firstName, lastName, phone } = profile;
|
|
|
|
let { email, username } = profile;
|
|
|
|
const isEmail = nameID.match(/^.+@.+\..+$/);
|
|
|
|
if (!email && isEmail) {
|
|
|
|
email = nameID;
|
|
|
|
}
|
|
|
|
if (!username && !isEmail) {
|
|
|
|
username = nameID;
|
|
|
|
}
|
2023-06-07 23:46:42 +08:00
|
|
|
|
2023-10-12 13:54:00 +08:00
|
|
|
const authenticator = this.authenticator as AuthModel;
|
|
|
|
let user = await authenticator.findUser(nameID);
|
|
|
|
if (user) {
|
|
|
|
return user;
|
|
|
|
}
|
|
|
|
// Bind existed user
|
|
|
|
const { userBindField = 'email' } = this.options?.saml || {};
|
|
|
|
if (userBindField === 'email' && email) {
|
|
|
|
user = await this.userRepository.findOne({
|
|
|
|
filter: { email },
|
|
|
|
});
|
|
|
|
} else if (userBindField === 'username' && username) {
|
|
|
|
user = await this.userRepository.findOne({
|
|
|
|
filter: { username },
|
2023-06-07 23:46:42 +08:00
|
|
|
});
|
|
|
|
}
|
2023-10-12 13:54:00 +08:00
|
|
|
if (user) {
|
|
|
|
await this.authenticator.addUser(user.id, {
|
|
|
|
through: {
|
|
|
|
uuid: nameID,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
return user;
|
|
|
|
}
|
|
|
|
// Create new user
|
|
|
|
const { autoSignup } = this.options?.public || {};
|
|
|
|
if (!autoSignup) {
|
|
|
|
throw new Error('User not found');
|
|
|
|
}
|
|
|
|
if (username && !this.validateUsername(username as string)) {
|
|
|
|
throw new Error('Username must be 2-16 characters in length (excluding @.<>"\'/)');
|
|
|
|
}
|
|
|
|
const fullName = firstName && lastName && `${firstName} ${lastName}`;
|
|
|
|
return await authenticator.newUser(nameID, {
|
|
|
|
username: username ?? null,
|
|
|
|
nickname: nickname || fullName || username || nameID,
|
2023-06-07 23:46:42 +08:00
|
|
|
email: email ?? null,
|
|
|
|
phone: phone ?? null,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|