* feat: supports subdirectory deployment * feat: auto publicPath * fix: buildIndexHtml * fix: format * fix: regexp * fix: test error * fix: nocobase.conf * fix: path * fix: nocobase.conf * fix: bugs * fix: resourcer prefix * fix: cas
		
			
				
	
	
		
			95 lines
		
	
	
		
			2.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			95 lines
		
	
	
		
			2.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { AuthConfig, BaseAuth } from '@nocobase/auth';
 | |
| import { AuthModel } from '@nocobase/plugin-auth';
 | |
| 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'),
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   getOptions(): SamlConfig {
 | |
|     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 {
 | |
|       callbackUrl: `${protocol}://${ctx.host}${process.env.API_BASE_PATH}saml:redirect?authenticator=${name}&__appName=${ctx.app.name}`,
 | |
|       entryPoint: ssoUrl,
 | |
|       issuer: name,
 | |
|       cert: certificate,
 | |
|       idpIssuer,
 | |
|       wantAssertionsSigned: false,
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   async validate() {
 | |
|     const ctx = this.ctx;
 | |
|     const {
 | |
|       params: { values: samlResponse },
 | |
|     } = ctx.action;
 | |
|     const saml = new SAML(this.getOptions());
 | |
| 
 | |
|     const { profile } = await saml.validatePostResponseAsync(samlResponse);
 | |
|     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;
 | |
|     }
 | |
| 
 | |
|     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 },
 | |
|       });
 | |
|     }
 | |
|     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,
 | |
|       email: email ?? null,
 | |
|       phone: phone ?? null,
 | |
|     });
 | |
|   }
 | |
| }
 |