samlesa 3.4.2 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +260 -25
- package/build/src/binding-artifact.js +194 -285
- package/build/src/entity-idp.js +16 -1
- package/build/src/entity-sp.js +19 -17
- package/build/src/extractor.js +25 -5
- package/build/src/flow.js +1 -8
- package/build/src/schemaValidator.js +78 -63
- package/build/src/urn.js +109 -11
- package/build/src/utility.js +71 -0
- package/package.json +88 -75
- package/types/src/binding-artifact.d.ts +53 -25
- package/types/src/binding-artifact.d.ts.map +1 -1
- package/types/src/entity-idp.d.ts.map +1 -1
- package/types/src/entity-sp.d.ts +12 -14
- package/types/src/entity-sp.d.ts.map +1 -1
- package/types/src/extractor.d.ts +2 -1
- package/types/src/extractor.d.ts.map +1 -1
- package/types/src/flow.d.ts.map +1 -1
- package/types/src/schemaValidator.d.ts +18 -1
- package/types/src/schemaValidator.d.ts.map +1 -1
- package/types/src/urn.d.ts +61 -5
- package/types/src/urn.d.ts.map +1 -1
- package/types/src/utility.d.ts +17 -0
- package/types/src/utility.d.ts.map +1 -1
package/build/src/entity-idp.js
CHANGED
|
@@ -10,6 +10,7 @@ import { namespace } from './urn.js';
|
|
|
10
10
|
import postBinding from './binding-post.js';
|
|
11
11
|
import redirectBinding from './binding-redirect.js';
|
|
12
12
|
import simpleSignBinding from './binding-simplesign.js';
|
|
13
|
+
import artifactBinding from './binding-artifact.js';
|
|
13
14
|
import { flow } from './flow.js';
|
|
14
15
|
/**
|
|
15
16
|
* Identity provider can be configured using either metadata importing or idpSetting
|
|
@@ -36,7 +37,7 @@ export class IdentityProvider extends Entity {
|
|
|
36
37
|
* @param params
|
|
37
38
|
*/
|
|
38
39
|
async createLoginResponse(params) {
|
|
39
|
-
const bindType = params?.binding ?? 'post';
|
|
40
|
+
const bindType = (params?.binding ?? 'post');
|
|
40
41
|
const { sp, requestInfo = {}, user = {}, customTagReplacement, encryptThenSign = false, relayState = '', AttributeStatement = [], idpInit = false, } = params;
|
|
41
42
|
const protocol = namespace.binding[bindType];
|
|
42
43
|
// can support post, redirect and post simple sign bindings for login response
|
|
@@ -66,6 +67,20 @@ export class IdentityProvider extends Entity {
|
|
|
66
67
|
idp: this,
|
|
67
68
|
sp,
|
|
68
69
|
}, user, relayState, customTagReplacement, AttributeStatement);
|
|
70
|
+
case namespace.binding.artifact:
|
|
71
|
+
context = await artifactBinding.soapLoginResponse({
|
|
72
|
+
requestInfo,
|
|
73
|
+
entity: {
|
|
74
|
+
idp: this,
|
|
75
|
+
sp,
|
|
76
|
+
},
|
|
77
|
+
user,
|
|
78
|
+
customTagReplacement,
|
|
79
|
+
encryptThenSign,
|
|
80
|
+
AttributeStatement,
|
|
81
|
+
idpInit,
|
|
82
|
+
});
|
|
83
|
+
break;
|
|
69
84
|
default:
|
|
70
85
|
throw new Error('ERR_CREATE_RESPONSE_UNDEFINED_BINDING');
|
|
71
86
|
}
|
package/build/src/entity-sp.js
CHANGED
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
* @desc Declares the actions taken by service provider
|
|
5
5
|
*/
|
|
6
6
|
import Entity from './entity.js';
|
|
7
|
-
import
|
|
7
|
+
import artifactBinding from './binding-artifact.js';
|
|
8
8
|
import { namespace } from './urn.js';
|
|
9
9
|
import redirectBinding from './binding-redirect.js';
|
|
10
10
|
import postBinding from './binding-post.js';
|
|
11
11
|
import simpleSignBinding from './binding-simplesign.js';
|
|
12
|
-
import artifactSignBinding from './binding-artifact.js';
|
|
13
12
|
import { flow } from './flow.js';
|
|
14
13
|
/*
|
|
15
14
|
* @desc interface function
|
|
@@ -19,8 +18,7 @@ export default function (props) {
|
|
|
19
18
|
}
|
|
20
19
|
/**
|
|
21
20
|
* @desc Service provider can be configured using either metadata importing or spSetting
|
|
22
|
-
* @param {object}
|
|
23
|
-
|
|
21
|
+
* @param {object} spSetting
|
|
24
22
|
*/
|
|
25
23
|
export class ServiceProvider extends Entity {
|
|
26
24
|
/**
|
|
@@ -61,8 +59,13 @@ export class ServiceProvider extends Entity {
|
|
|
61
59
|
// Object context = {id, context, signature, sigAlg}
|
|
62
60
|
context = simpleSignBinding.base64LoginRequest({ idp, sp: this }, customTagReplacement);
|
|
63
61
|
break;
|
|
62
|
+
case nsBinding.artifact:
|
|
63
|
+
context = artifactBinding.soapLoginRequest("/*[local-name(.)='AuthnRequest']", {
|
|
64
|
+
idp,
|
|
65
|
+
sp: this
|
|
66
|
+
}, customTagReplacement);
|
|
67
|
+
break;
|
|
64
68
|
default:
|
|
65
|
-
// Will support artifact in the next release
|
|
66
69
|
throw new Error('ERR_SP_LOGIN_REQUEST_UNDEFINED_BINDING');
|
|
67
70
|
}
|
|
68
71
|
return {
|
|
@@ -73,13 +76,7 @@ export class ServiceProvider extends Entity {
|
|
|
73
76
|
};
|
|
74
77
|
}
|
|
75
78
|
async createLoginSoapRequest(idp, binding = 'artifact', config) {
|
|
76
|
-
const
|
|
77
|
-
const protocol = nsBinding[binding];
|
|
78
|
-
if (this.entityMeta.isAuthnRequestSigned() !== idp.entityMeta.isWantAuthnRequestsSigned()) {
|
|
79
|
-
throw new Error('ERR_METADATA_CONFLICT_REQUEST_SIGNED_FLAG');
|
|
80
|
-
}
|
|
81
|
-
let context = null;
|
|
82
|
-
context = await artifactSignBinding.soapLoginRequest("/*[local-name(.)='AuthnRequest']", {
|
|
79
|
+
const context = await artifactBinding.soapLoginRequest("/*[local-name(.)='AuthnRequest']", {
|
|
83
80
|
idp,
|
|
84
81
|
sp: this,
|
|
85
82
|
inResponse: config?.inResponseTo,
|
|
@@ -106,22 +103,27 @@ export class ServiceProvider extends Entity {
|
|
|
106
103
|
});
|
|
107
104
|
}
|
|
108
105
|
/**
|
|
109
|
-
* @desc
|
|
106
|
+
* @desc Parse and validate Artifact Resolve request
|
|
110
107
|
* @param {IdentityProvider} idp object of identity provider
|
|
111
|
-
* @param {string}
|
|
112
|
-
* @param {request} req request
|
|
108
|
+
* @param {string} xml SOAP request XML string
|
|
113
109
|
*/
|
|
114
110
|
parseLoginRequestResolve(idp, xml) {
|
|
115
111
|
const self = this;
|
|
116
|
-
return
|
|
112
|
+
return artifactBinding.parseLoginRequestResolve({
|
|
117
113
|
idp: idp,
|
|
118
114
|
sp: self,
|
|
119
115
|
xml: xml
|
|
120
116
|
});
|
|
121
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* @desc Resolve SAML Response by Artifact ID
|
|
120
|
+
* @param {IdentityProvider} idp object of identity provider
|
|
121
|
+
* @param {string} art Artifact string
|
|
122
|
+
* @param {request} req request
|
|
123
|
+
*/
|
|
122
124
|
parseLoginResponseResolve(idp, art, request) {
|
|
123
125
|
const self = this;
|
|
124
|
-
return
|
|
126
|
+
return artifactBinding.parseLoginResponseResolve({
|
|
125
127
|
idp: idp,
|
|
126
128
|
sp: self,
|
|
127
129
|
art: art
|
package/build/src/extractor.js
CHANGED
|
@@ -117,10 +117,27 @@ export const loginRequestFields = [
|
|
|
117
117
|
{ key: 'signature', localPath: ['AuthnRequest', 'Signature'], attributes: [], context: true }
|
|
118
118
|
];
|
|
119
119
|
export const artifactResolveFields = [
|
|
120
|
-
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
{
|
|
121
|
+
key: 'request',
|
|
122
|
+
localPath: ['Envelope', 'Body', 'ArtifactResolve'],
|
|
123
|
+
attributes: ['ID', 'IssueInstant', 'Version', 'Destination']
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
key: 'issuer',
|
|
127
|
+
localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Issuer'],
|
|
128
|
+
attributes: []
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
key: 'artifact',
|
|
132
|
+
localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Artifact'],
|
|
133
|
+
attributes: []
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
key: 'signature',
|
|
137
|
+
localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Signature'],
|
|
138
|
+
attributes: [],
|
|
139
|
+
context: true
|
|
140
|
+
},
|
|
124
141
|
];
|
|
125
142
|
export const artifactResponseFields = [
|
|
126
143
|
{ key: 'request', localPath: ['Envelope', 'Body', 'ArtifactResolve'], attributes: ['ID', 'IssueInstant', 'Version'] },
|
|
@@ -1487,6 +1504,9 @@ export function extractSp(context) {
|
|
|
1487
1504
|
export function extractAuthRequest(context) {
|
|
1488
1505
|
return extract(context, loginRequestFields);
|
|
1489
1506
|
}
|
|
1490
|
-
export function extractResponse(context
|
|
1507
|
+
export function extractResponse(context) {
|
|
1491
1508
|
return extractSpToll(context, loginResponseFieldsFullList);
|
|
1492
1509
|
}
|
|
1510
|
+
export function extractArtifactResolve(context) {
|
|
1511
|
+
return extract(context, artifactResolveFields);
|
|
1512
|
+
}
|
package/build/src/flow.js
CHANGED
|
@@ -225,7 +225,7 @@ async function postFlow(options) {
|
|
|
225
225
|
if (parserType === 'SAMLResponse'
|
|
226
226
|
&& extractedProperties.conditions
|
|
227
227
|
&& !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
228
|
-
return Promise.reject('
|
|
228
|
+
return Promise.reject('ERR_CONDITION_UNCONFIRMED');
|
|
229
229
|
}
|
|
230
230
|
// invalid subjectConfirmation time
|
|
231
231
|
// invalid time
|
|
@@ -416,13 +416,6 @@ async function postArtifactFlow(options) {
|
|
|
416
416
|
//There is no validation of the response here. The upper-layer application
|
|
417
417
|
// should verify the result by itself to see if the destination is equal to the SP acs and
|
|
418
418
|
// whether the response.id is used to prevent replay attacks.
|
|
419
|
-
let destination = extractedProperties?.response?.destination;
|
|
420
|
-
let isExit = self?.entityMeta?.meta?.assertionConsumerService?.filter((item) => {
|
|
421
|
-
return item?.location === destination;
|
|
422
|
-
});
|
|
423
|
-
if (isExit?.length === 0) {
|
|
424
|
-
return Promise.reject('ERR_Destination_URL');
|
|
425
|
-
}
|
|
426
419
|
if (parserType === 'SAMLResponse') {
|
|
427
420
|
let destination = extractedProperties?.response?.destination;
|
|
428
421
|
let isExit = self?.entityMeta?.meta?.assertionConsumerService?.filter((item) => {
|
|
@@ -5,7 +5,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { DOMParser } from '@xmldom/xmldom';
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = path.dirname(__filename);
|
|
8
|
-
|
|
8
|
+
// 定义各个场景所需的 schema 文件列表(保持不变)
|
|
9
|
+
const normalSchemas = [
|
|
9
10
|
'saml-schema-protocol-2.0.xsd',
|
|
10
11
|
'saml-schema-assertion-2.0.xsd',
|
|
11
12
|
'xmldsig-core-schema.xsd',
|
|
@@ -15,42 +16,38 @@ let normal = [
|
|
|
15
16
|
'saml-schema-ecp-2.0.xsd',
|
|
16
17
|
'saml-schema-dce-2.0.xsd'
|
|
17
18
|
];
|
|
18
|
-
|
|
19
|
+
const soapSchemas = [
|
|
19
20
|
'soap-envelope.xsd',
|
|
20
21
|
'xml.xsd',
|
|
21
|
-
// 2. SOAP核心模式(所有SOAP消息的基础)
|
|
22
|
-
// 3. XML签名模式(SAML签名的前置依赖)
|
|
23
22
|
'xmldsig-core-schema.xsd',
|
|
24
|
-
// 4. XML加密模式(SAML断言加密的前置依赖)
|
|
25
23
|
'xenc-schema.xsd',
|
|
26
24
|
'xenc-schema-11.xsd',
|
|
27
|
-
|
|
28
|
-
'saml-schema-assertion-2.0.xsd', // 断言定义
|
|
29
|
-
// 6. SAML协议模式(依赖断言模式)
|
|
25
|
+
'saml-schema-assertion-2.0.xsd',
|
|
30
26
|
'saml-schema-protocol-2.0.xsd',
|
|
31
|
-
|
|
32
|
-
'saml-schema-
|
|
33
|
-
'saml-schema-
|
|
34
|
-
'saml-schema-dce-2.0.xsd' // DCE扩展
|
|
27
|
+
'saml-schema-metadata-2.0.xsd',
|
|
28
|
+
'saml-schema-ecp-2.0.xsd',
|
|
29
|
+
'saml-schema-dce-2.0.xsd'
|
|
35
30
|
];
|
|
36
|
-
|
|
37
|
-
'saml-schema-metadata-2.0.xsd',
|
|
31
|
+
const metadataSchemas = [
|
|
32
|
+
'saml-schema-metadata-2.0.xsd',
|
|
38
33
|
'xml.xsd',
|
|
39
34
|
'saml-schema-assertion-2.0.xsd',
|
|
40
35
|
'xmldsig-core-schema.xsd',
|
|
41
36
|
'xenc-schema.xsd',
|
|
42
|
-
'xenc-schema-11.xsd'
|
|
37
|
+
'xenc-schema-11.xsd'
|
|
43
38
|
];
|
|
44
|
-
|
|
39
|
+
/**
|
|
40
|
+
* 检测 XML 字符串中是否存在 XXE 攻击指示器
|
|
41
|
+
* @param samlString 待检测的 XML 字符串
|
|
42
|
+
* @returns 如果存在可疑模式则返回匹配详情,否则返回 null
|
|
43
|
+
*/
|
|
45
44
|
function detectXXEIndicators(samlString) {
|
|
46
45
|
const xxePatterns = [
|
|
47
|
-
/<!DOCTYPE\s[^>]*>/i,
|
|
48
|
-
/<!ENTITY\s+[^\s>]+\s+(?:SYSTEM|PUBLIC)\s+['"][^>]*>/i,
|
|
49
|
-
|
|
50
|
-
/SYSTEM\s
|
|
51
|
-
/PUBLIC\s
|
|
52
|
-
/file:\/\//,
|
|
53
|
-
/\.dtd['"]?/
|
|
46
|
+
/<!DOCTYPE\s[^>]*>/i, // DOCTYPE 声明
|
|
47
|
+
/<!ENTITY\s+[^\s>]+\s+(?:SYSTEM|PUBLIC)\s+['"][^>]*>/i, // 外部实体声明
|
|
48
|
+
/SYSTEM\s*['"]\s*file:\/\//i, // file:// 协议的系统引用
|
|
49
|
+
/SYSTEM\s*['"]\s*\.\.\/.*\.dtd['"]?/i, // 相对路径的 DTD 引用
|
|
50
|
+
/PUBLIC\s*['"][^'"]*['"]\s*['"][^'"]*\.dtd['"]?/i // 公共 DTD 引用
|
|
54
51
|
];
|
|
55
52
|
const matches = {};
|
|
56
53
|
xxePatterns.forEach((pattern, index) => {
|
|
@@ -64,94 +61,112 @@ function detectXXEIndicators(samlString) {
|
|
|
64
61
|
});
|
|
65
62
|
return Object.keys(matches).length > 0 ? matches : null;
|
|
66
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* 加载指定的 schema 文件内容
|
|
66
|
+
* @param schemaNames 文件名数组
|
|
67
|
+
* @returns 包含 fileName 和 contents 的对象数组
|
|
68
|
+
*/
|
|
69
|
+
async function loadSchemas(schemaNames) {
|
|
70
|
+
const schemaPath = path.resolve(__dirname, 'schema');
|
|
71
|
+
return Promise.all(schemaNames.map(async (file) => ({
|
|
72
|
+
fileName: file,
|
|
73
|
+
contents: await fs.promises.readFile(`${schemaPath}/${file}`, 'utf-8')
|
|
74
|
+
})));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 验证 SAML 消息(普通或 SOAP)
|
|
78
|
+
* @param xml XML 字符串
|
|
79
|
+
* @param isSoap 是否为 SOAP 消息,默认 false
|
|
80
|
+
* @returns true 表示验证通过,否则抛出错误
|
|
81
|
+
* @throws 当检测到 XXE 或验证失败时抛出错误
|
|
82
|
+
*/
|
|
67
83
|
export const validate = async (xml, isSoap = false) => {
|
|
84
|
+
// 检测 XXE 攻击
|
|
68
85
|
const indicators = detectXXEIndicators(xml);
|
|
69
86
|
if (indicators) {
|
|
70
87
|
throw new Error('ERR_EXCEPTION_VALIDATE_XML');
|
|
71
88
|
}
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
fileName: file,
|
|
76
|
-
contents: await fs.promises.readFile(`${schemaPath}/${file}`, 'utf-8')
|
|
77
|
-
})));
|
|
89
|
+
// 根据类型选择对应的 schema 列表(避免全局变量并发问题)
|
|
90
|
+
const schemaList = isSoap ? soapSchemas : normalSchemas;
|
|
91
|
+
const schemas = await loadSchemas(schemaList);
|
|
78
92
|
try {
|
|
79
93
|
const validationResult = await validateXML({
|
|
80
|
-
xml: [
|
|
81
|
-
{
|
|
82
|
-
fileName: 'content.xml',
|
|
83
|
-
contents: xml,
|
|
84
|
-
},
|
|
85
|
-
],
|
|
94
|
+
xml: [{ fileName: 'content.xml', contents: xml }],
|
|
86
95
|
extension: 'schema',
|
|
87
|
-
schema: [
|
|
88
|
-
preload: [
|
|
96
|
+
schema: [schemas[0]], // 第一个 schema 作为主入口
|
|
97
|
+
preload: [schemas[0], ...schemas.slice(1)], // 其余作为预加载
|
|
89
98
|
});
|
|
90
99
|
if (validationResult.valid) {
|
|
91
100
|
return true;
|
|
92
101
|
}
|
|
102
|
+
// 验证失败,抛出错误对象
|
|
93
103
|
throw validationResult.errors;
|
|
94
104
|
}
|
|
95
105
|
catch (error) {
|
|
96
|
-
|
|
106
|
+
// 保留原始错误信息
|
|
107
|
+
throw error;
|
|
97
108
|
}
|
|
98
109
|
};
|
|
110
|
+
/**
|
|
111
|
+
* 验证 SAML 元数据,并可选择解析元数据类型
|
|
112
|
+
* @param xml XML 字符串
|
|
113
|
+
* @param isParse 是否解析并返回元数据类型,默认 false
|
|
114
|
+
* @returns 验证通过时:若 isParse 为 true 返回 { isValid: true, metadataType: string },否则返回 true;
|
|
115
|
+
* 验证失败时返回 Error 对象(保持原行为)
|
|
116
|
+
*/
|
|
99
117
|
export const validateMetadata = async (xml, isParse = false) => {
|
|
118
|
+
// 检测 XXE 攻击
|
|
100
119
|
const indicators = detectXXEIndicators(xml);
|
|
101
120
|
if (indicators) {
|
|
102
121
|
throw new Error('ERR_EXCEPTION_VALIDATE_XML');
|
|
103
122
|
}
|
|
104
|
-
schemas =
|
|
105
|
-
const schemaPath = path.resolve(__dirname, 'schema');
|
|
106
|
-
const [xmlParse, ...preload] = await Promise.all(schemas.map(async (file) => ({
|
|
107
|
-
fileName: file,
|
|
108
|
-
contents: await fs.promises.readFile(`${schemaPath}/${file}`, 'utf-8')
|
|
109
|
-
})));
|
|
123
|
+
const schemas = await loadSchemas(metadataSchemas);
|
|
110
124
|
try {
|
|
125
|
+
// @ts-ignore
|
|
111
126
|
const validationResult = await validateXML({
|
|
112
|
-
xml: [
|
|
113
|
-
{
|
|
114
|
-
fileName: 'content.xml',
|
|
115
|
-
contents: xml,
|
|
116
|
-
},
|
|
117
|
-
],
|
|
127
|
+
xml: [{ fileName: 'content.xml', contents: xml }],
|
|
118
128
|
extension: 'schema',
|
|
119
|
-
schema: [
|
|
120
|
-
preload: [
|
|
129
|
+
schema: [schemas[0]],
|
|
130
|
+
preload: [schemas[0], ...schemas.slice(1)],
|
|
121
131
|
});
|
|
122
132
|
if (validationResult.valid) {
|
|
123
133
|
if (isParse) {
|
|
124
|
-
// 解析 XML
|
|
134
|
+
// 解析 XML 并确定元数据类型
|
|
125
135
|
const parser = new DOMParser();
|
|
126
136
|
const xmlDoc = parser.parseFromString(xml, 'text/xml');
|
|
127
|
-
//
|
|
137
|
+
// 检查解析错误(防御性编程)
|
|
138
|
+
const parserError = xmlDoc.getElementsByTagName('parsererror');
|
|
139
|
+
if (parserError.length > 0) {
|
|
140
|
+
// 解析失败,视为无效 XML,返回错误对象(与原逻辑一致)
|
|
141
|
+
return new Error('XML parsing failed');
|
|
142
|
+
}
|
|
128
143
|
const idpDescriptor = xmlDoc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:metadata', 'IDPSSODescriptor');
|
|
129
144
|
const spDescriptor = xmlDoc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:metadata', 'SPSSODescriptor');
|
|
130
|
-
// 判断元数据类型
|
|
131
145
|
let metadataType;
|
|
132
146
|
if (idpDescriptor.length > 0 && spDescriptor.length > 0) {
|
|
133
|
-
metadataType = 'both';
|
|
147
|
+
metadataType = 'both';
|
|
134
148
|
}
|
|
135
149
|
else if (idpDescriptor.length > 0) {
|
|
136
|
-
metadataType = 'IdP';
|
|
150
|
+
metadataType = 'IdP';
|
|
137
151
|
}
|
|
138
152
|
else if (spDescriptor.length > 0) {
|
|
139
|
-
metadataType = 'SP';
|
|
153
|
+
metadataType = 'SP';
|
|
140
154
|
}
|
|
141
155
|
else {
|
|
142
|
-
metadataType = 'unknown';
|
|
156
|
+
metadataType = 'unknown';
|
|
143
157
|
}
|
|
144
|
-
// 返回验证结果和元数据类型
|
|
145
158
|
return {
|
|
146
159
|
isValid: true,
|
|
147
|
-
metadataType
|
|
160
|
+
metadataType
|
|
148
161
|
};
|
|
149
162
|
}
|
|
150
163
|
return true;
|
|
151
164
|
}
|
|
152
|
-
|
|
165
|
+
// 验证失败,返回错误对象(保持原行为)
|
|
166
|
+
return validationResult.errors;
|
|
153
167
|
}
|
|
154
168
|
catch (error) {
|
|
155
|
-
|
|
169
|
+
// 捕获其他异常(如文件读取失败)并返回错误对象
|
|
170
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
156
171
|
}
|
|
157
172
|
};
|
package/build/src/urn.js
CHANGED
|
@@ -10,6 +10,16 @@ export var BindingNamespace;
|
|
|
10
10
|
BindingNamespace["SimpleSign"] = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign";
|
|
11
11
|
BindingNamespace["Artifact"] = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact";
|
|
12
12
|
})(BindingNamespace || (BindingNamespace = {}));
|
|
13
|
+
export const NamespaceBindingMap = {
|
|
14
|
+
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect': 'redirect',
|
|
15
|
+
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST': 'post',
|
|
16
|
+
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign': 'simplesign',
|
|
17
|
+
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact': 'artifact'
|
|
18
|
+
};
|
|
19
|
+
// 可选:添加反向查找函数
|
|
20
|
+
function getBindingName(uri) {
|
|
21
|
+
return NamespaceBindingMap[uri];
|
|
22
|
+
}
|
|
13
23
|
export var MessageSignatureOrder;
|
|
14
24
|
(function (MessageSignatureOrder) {
|
|
15
25
|
MessageSignatureOrder["STE"] = "sign-then-encrypt";
|
|
@@ -51,6 +61,12 @@ const namespace = {
|
|
|
51
61
|
artifact: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact',
|
|
52
62
|
soap: 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP',
|
|
53
63
|
},
|
|
64
|
+
bindMap: {
|
|
65
|
+
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect': 'redirect',
|
|
66
|
+
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST': 'post',
|
|
67
|
+
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign': 'simplesign',
|
|
68
|
+
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact': 'artifact'
|
|
69
|
+
},
|
|
54
70
|
names: {
|
|
55
71
|
protocol: 'urn:oasis:names:tc:SAML:2.0:protocol',
|
|
56
72
|
assertion: 'urn:oasis:names:tc:SAML:2.0:assertion',
|
|
@@ -173,22 +189,31 @@ const messageConfigurations = {
|
|
|
173
189
|
const algorithms = {
|
|
174
190
|
// 1. 签名算法定义 (SignatureMethod)
|
|
175
191
|
signature: {
|
|
176
|
-
// ❌
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
DSA_SHA1: 'http://www.w3.org/2000/09/xmldsig#dsa-sha1',
|
|
181
|
-
RSA_SHA1: 'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
|
|
192
|
+
// ❌ 不安全的算法(已废弃)
|
|
193
|
+
RSA_SHA1: 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', // ⚠️ 已废弃,不推荐使用
|
|
194
|
+
DSA_SHA1: 'http://www.w3.org/2000/09/xmldsig#dsa-sha1', // ⚠️ 已废弃,不推荐使用
|
|
195
|
+
// ✅ 安全的 RSA 算法(推荐)
|
|
182
196
|
RSA_SHA224: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha224',
|
|
183
|
-
RSA_SHA256: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', // 推荐
|
|
197
|
+
RSA_SHA256: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', // ⭐ 推荐
|
|
184
198
|
RSA_SHA384: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384',
|
|
185
199
|
RSA_SHA512: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512',
|
|
186
|
-
//
|
|
200
|
+
// ✅ ECDSA 算法(推荐)
|
|
201
|
+
ECDSA_SHA256: 'http://www.w3.org/2007/05/xmldsig-more#ecdsa-sha256', // ⭐ 推荐
|
|
202
|
+
ECDSA_SHA384: 'http://www.w3.org/2007/05/xmldsig-more#ecdsa-sha384',
|
|
203
|
+
ECDSA_SHA512: 'http://www.w3.org/2007/05/xmldsig-more#ecdsa-sha512',
|
|
204
|
+
// ✅ XML Signature 1.1 PSS 填充(更安全)
|
|
187
205
|
RSA_PSS_SHA256: 'http://www.w3.org/2007/05/xmldsig-more#rsa-pss-sha256',
|
|
188
|
-
// EdDSA (Ed25519)
|
|
189
|
-
EDDSA_ED25519: 'http://www.w3.org/2007/05/xmldsig-more#eddsa-ed25519',
|
|
206
|
+
// ✅ EdDSA (Ed25519/Ed448)
|
|
207
|
+
EDDSA_ED25519: 'http://www.w3.org/2007/05/xmldsig-more#eddsa-ed25519', // ⭐ 推荐
|
|
190
208
|
EDDSA_ED488: 'http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448'
|
|
191
209
|
},
|
|
210
|
+
// 不安全的算法列表(用于验证和阻止)
|
|
211
|
+
unsafeAlgorithms: [
|
|
212
|
+
'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
|
|
213
|
+
'http://www.w3.org/2000/09/xmldsig#dsa-sha1',
|
|
214
|
+
'http://www.w3.org/2000/09/xmldsig#hmac-sha1',
|
|
215
|
+
'http://www.w3.org/2000/09/xmldsig#sha1',
|
|
216
|
+
],
|
|
192
217
|
// 2. 摘要算法定义 (DigestMethod)
|
|
193
218
|
// 注意:这里直接使用标准推荐的 URI,SHA-2xx 系列推荐使用 xmlenc 命名空间
|
|
194
219
|
digest: {
|
|
@@ -306,4 +331,77 @@ const elementsOrder = {
|
|
|
306
331
|
onelogin: ['KeyDescriptor', 'NameIDFormat', 'ArtifactResolutionService', 'SingleLogoutService', 'AssertionConsumerService', 'AttributeConsumingService'],
|
|
307
332
|
shibboleth: ['KeyDescriptor', 'ArtifactResolutionService', 'SingleLogoutService', 'NameIDFormat', 'AssertionConsumerService', 'AttributeConsumingService',],
|
|
308
333
|
};
|
|
309
|
-
|
|
334
|
+
/**
|
|
335
|
+
* 默认安全配置
|
|
336
|
+
*/
|
|
337
|
+
const defaultSecurityOptions = {
|
|
338
|
+
allowSHA1: false,
|
|
339
|
+
allowRSA15: false,
|
|
340
|
+
allowTripleDES: false,
|
|
341
|
+
};
|
|
342
|
+
/**
|
|
343
|
+
* 当前安全配置
|
|
344
|
+
*/
|
|
345
|
+
let currentSecurityOptions = { ...defaultSecurityOptions };
|
|
346
|
+
/**
|
|
347
|
+
* 设置安全配置
|
|
348
|
+
* @param options 安全配置选项
|
|
349
|
+
*/
|
|
350
|
+
function setSecurityOptions(options) {
|
|
351
|
+
currentSecurityOptions = { ...currentSecurityOptions, ...options };
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* 获取当前安全配置
|
|
355
|
+
* @returns 安全配置对象
|
|
356
|
+
*/
|
|
357
|
+
function getSecurityOptions() {
|
|
358
|
+
return currentSecurityOptions;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* 重置为默认安全配置
|
|
362
|
+
*/
|
|
363
|
+
function resetSecurityOptions() {
|
|
364
|
+
currentSecurityOptions = { ...defaultSecurityOptions };
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* 验证算法是否安全
|
|
368
|
+
* @param algorithm 算法 URI
|
|
369
|
+
* @returns 验证结果
|
|
370
|
+
*/
|
|
371
|
+
function validateAlgorithm(algorithm) {
|
|
372
|
+
// 检查 SHA-1
|
|
373
|
+
if (!currentSecurityOptions.allowSHA1 && algorithm.toLowerCase().includes('sha1')) {
|
|
374
|
+
return {
|
|
375
|
+
valid: false,
|
|
376
|
+
reason: 'SHA-1 algorithm is not allowed. Use SHA-256 or stronger.'
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
// 检查 RSA-1_5
|
|
380
|
+
if (!currentSecurityOptions.allowRSA15 && algorithm.includes('rsa-1_5')) {
|
|
381
|
+
return {
|
|
382
|
+
valid: false,
|
|
383
|
+
reason: 'RSA-1_5 key encryption is not allowed. Use RSA-OAEP instead.'
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
// 检查 TripleDES
|
|
387
|
+
if (!currentSecurityOptions.allowTripleDES && algorithm.includes('tripledes')) {
|
|
388
|
+
return {
|
|
389
|
+
valid: false,
|
|
390
|
+
reason: 'TripleDES encryption is not allowed. Use AES-GCM instead.'
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
return { valid: true };
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* 检查算法是否为不安全算法
|
|
397
|
+
* @param algorithm 算法 URI
|
|
398
|
+
* @returns 检查结果
|
|
399
|
+
*/
|
|
400
|
+
function checkUnsafeAlgorithm(algorithm) {
|
|
401
|
+
const isUnsafe = algorithms.unsafeAlgorithms.some(unsafeAlg => algorithm.toLowerCase().includes(unsafeAlg.toLowerCase().replace('http://www.w3.org/2000/09/xmldsig#', '').replace('#', ''))) || algorithm.toLowerCase().includes('sha1');
|
|
402
|
+
return {
|
|
403
|
+
isUnsafe,
|
|
404
|
+
algorithm: isUnsafe ? algorithm : undefined
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
export { namespace, tags, algorithms, wording, elementsOrder, messageConfigurations, getBindingName, defaultSecurityOptions, setSecurityOptions, getSecurityOptions, resetSecurityOptions, validateAlgorithm, checkUnsafeAlgorithm };
|
package/build/src/utility.js
CHANGED
|
@@ -312,6 +312,75 @@ export function castArrayOpt(a) {
|
|
|
312
312
|
export function notEmpty(value) {
|
|
313
313
|
return value !== null && value !== undefined;
|
|
314
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* @desc 验证 RelayState 是否符合 SAML 2.0 规范
|
|
317
|
+
* @param {string} relayState - RelayState 值
|
|
318
|
+
* @returns {{ valid: boolean; error?: string }} 验证结果
|
|
319
|
+
*/
|
|
320
|
+
export function validateRelayState(relayState) {
|
|
321
|
+
// RelayState 是可选的
|
|
322
|
+
if (!relayState || relayState.length === 0) {
|
|
323
|
+
return { valid: true };
|
|
324
|
+
}
|
|
325
|
+
// 验证长度(SAML 规范限制 80 字节)
|
|
326
|
+
if (relayState.length > 80) {
|
|
327
|
+
return {
|
|
328
|
+
valid: false,
|
|
329
|
+
error: 'RelayState exceeds 80 bytes'
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
// 验证是否为合法 URL(如果是 URL)
|
|
333
|
+
if (relayState.startsWith('http://') || relayState.startsWith('https://')) {
|
|
334
|
+
try {
|
|
335
|
+
new URL(relayState);
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return {
|
|
339
|
+
valid: false,
|
|
340
|
+
error: 'RelayState is not a valid URL'
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return { valid: true };
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* @desc 敏感信息键名列表(用于日志脱敏)
|
|
348
|
+
*/
|
|
349
|
+
const sensitiveKeys = [
|
|
350
|
+
'privateKey',
|
|
351
|
+
'privateKeyPass',
|
|
352
|
+
'encPrivateKey',
|
|
353
|
+
'encPrivateKeyPass',
|
|
354
|
+
'password',
|
|
355
|
+
'secret',
|
|
356
|
+
'signingCert',
|
|
357
|
+
'encryptCert'
|
|
358
|
+
];
|
|
359
|
+
/**
|
|
360
|
+
* @desc 日志脱敏函数,过滤敏感信息
|
|
361
|
+
* @param {any} data - 需要脱敏的数据
|
|
362
|
+
* @returns {any} 脱敏后的数据
|
|
363
|
+
*/
|
|
364
|
+
export function sanitizeLog(data) {
|
|
365
|
+
if (typeof data !== 'object' || data === null) {
|
|
366
|
+
return data;
|
|
367
|
+
}
|
|
368
|
+
const sanitized = Array.isArray(data) ? [] : {};
|
|
369
|
+
for (const [key, value] of Object.entries(data)) {
|
|
370
|
+
// 检查是否为敏感键名
|
|
371
|
+
if (sensitiveKeys.some(k => k.toLowerCase() === key.toLowerCase())) {
|
|
372
|
+
sanitized[key] = '***REDACTED***';
|
|
373
|
+
}
|
|
374
|
+
else if (typeof value === 'object' && value !== null) {
|
|
375
|
+
// 递归处理嵌套对象
|
|
376
|
+
sanitized[key] = sanitizeLog(value);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
sanitized[key] = value;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return sanitized;
|
|
383
|
+
}
|
|
315
384
|
const utility = {
|
|
316
385
|
isString,
|
|
317
386
|
base64Encode,
|
|
@@ -327,5 +396,7 @@ const utility = {
|
|
|
327
396
|
readPrivateKey,
|
|
328
397
|
convertToString,
|
|
329
398
|
isNonEmptyArray,
|
|
399
|
+
validateRelayState,
|
|
400
|
+
sanitizeLog,
|
|
330
401
|
};
|
|
331
402
|
export default utility;
|