Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3 #485

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft

v3 #485

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"uuid": "^3.3.2",
"xml": "^1.0.1",
"xml-crypto": "^2.1.3",
"xpath": "^0.0.32"
"xpath": "^0.0.32",
"zod": "^3.17.10"
},
"devDependencies": {
"@ava/typescript": "^1.1.1",
Expand Down
63 changes: 63 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* This is the data structure for idp-sp management, most of the saml
* operations will be defined here.
*
* This will provide one idp to N sp and N idp to N sp according to the saml
* backend use cases. Developer can link pairs
*/

import { IdentityProvider } from "./idp";
import { ServiceProvider } from "./sp";

export interface SamlApp {

}

/**
* Pair up with another entity result in a new data structure with all
* common functions
*/
export const create = (): SamlApp => {

const connections = {};
const entities = {};

/**
*
* @param idp
* @param sp
*/
const bind = (idp: IdentityProvider, sp: ServiceProvider) => {

// TODO: Validate for the pair up to see if there is any conflict

// Cached into the nested object for function access
if (!connections[idp.id]) {
connections[idp.id] = {};
}
if (!connections[idp.id][sp.id]) {
connections[idp.id][sp.id] = {};
}
connections[idp.id][sp.id] = true;
entities[idp.id] = idp;
entities[sp.id] = sp;

};

/**
* Check if the connection is active
*
* @param idpId
* @param spId
* @returns
*/
const isActive = (idpId: string, spId: string) => {
return !!connections[idpId]?.[spId];
};

return {
bind,
isActive
};

}
239 changes: 239 additions & 0 deletions src/idp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
/**
* Define the identity provider interface, construction and feature
*/
import { v4 } from "uuid";
import { z } from "zod";
import { extract } from "./extractor";
import libsaml from "./libsaml";
import xml from 'xml';
import { namespace } from "./urn";
import { SSOServiceConfig } from "./types";

export const CreateProps = z.object({
wantAuthnRequestsSigned: z.boolean().default(false),
entityId: z.string().optional().default(v4()),
signingCert: z.string().or(z.instanceof(Buffer)).optional(),
encryptCert: z.string().or(z.instanceof(Buffer)).optional(),
requestSignatureAlgorithm: z.string().optional(),
nameIDFormat: z.array(z.string()).optional().default([]),
singleSignOnService: SSOServiceConfig(1),
singleLogoutService: SSOServiceConfig(0)
});

export type CreateProps = z.infer<typeof CreateProps>;

export const LoadProps = z.object({
metadata: z.string().startsWith('http').or(z.string()),
extractions: z.array(
z.object({
key: z.string(),
localPath: z.array(z.string()),
attributes: z.array(z.string()),
index: z.array(z.string()).optional(),
attributePath: z.array(z.string()).optional(),
context: z.boolean().optional()
})
).default([])
});

export type LoadProps = z.infer<typeof LoadProps>;

export interface IdentityProvider {
id: string,
metadata: Metadata;
rawMetadata: string;
};

export interface Metadata {
wantAuthnRequestsSigned?: boolean;
entityDescriptor?: any;
singleSignOnService?: any;
singleLogoutService?: any;
entityID?: any;
sharedCertificate?: any;
certificate?: {
signing: string;
encryption: string;
};
nameIDFormat?: any;
}

/**
* Easier interface to get access to essential props defined in metadata
*
* @param xmlString
* @returns
*/
const fetchEssentials = (xmlString: string): Metadata => {
const metadata: Metadata = extract(xmlString, [
{
key: 'wantAuthnRequestsSigned',
localPath: ['EntityDescriptor', 'IDPSSODescriptor'],
attributes: ['WantAuthnRequestsSigned'],
},
{
key: 'singleSignOnService',
localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'SingleSignOnService'],
index: ['Binding'],
attributePath: [],
attributes: ['Location']
},
{
key: 'entityDescriptor',
localPath: ['EntityDescriptor'],
attributes: [],
context: true
},
{
key: 'entityID',
localPath: ['EntityDescriptor'],
attributes: ['entityID']
},
{
// shared certificate for both encryption and signing
key: 'sharedCertificate',
localPath: ['EntityDescriptor', '~SSODescriptor', 'KeyDescriptor', 'KeyInfo', 'X509Data', 'X509Certificate'],
attributes: []
},
{
// explicit certificate declaration for encryption and signing
key: 'certificate',
localPath: ['EntityDescriptor', '~SSODescriptor', 'KeyDescriptor'],
index: ['use'],
attributePath: ['KeyInfo', 'X509Data', 'X509Certificate'],
attributes: []
},
{
key: 'singleLogoutService',
localPath: ['EntityDescriptor', '~SSODescriptor', 'SingleLogoutService'],
attributes: ['Binding', 'Location']
},
{
key: 'nameIDFormat',
localPath: ['EntityDescriptor', '~SSODescriptor', 'NameIDFormat'],
attributes: [],
}
]);

if (metadata.sharedCertificate) {
metadata.certificate = {
signing: metadata.sharedCertificate,
encryption: metadata.sharedCertificate
};
}

return metadata;

};

/**
* Create function and returns a set of helper functions
*
* @param props
* @returns
*/
export const create = (props: CreateProps): IdentityProvider => {

props = CreateProps.parse(props);

// Prepare the payload for metadata construction
const entityDescriptors: any = [{
_attr: {
WantAuthnRequestsSigned: String(props.wantAuthnRequestsSigned),
protocolSupportEnumeration: namespace.names.protocol,
},
}];

if (props.signingCert) {
entityDescriptors.push(libsaml.createKeySection('signing', props.signingCert));
}

if (props.encryptCert) {
entityDescriptors.push(libsaml.createKeySection('encryption', props.encryptCert));
}

if (props.nameIDFormat.length > 0) {
props.nameIDFormat.forEach(f => entityDescriptors.push({ NameIDFormat: f }));
}

// TODO: throw ERR_IDP_METADATA_MISSING_SINGLE_SIGN_ON_SERVICE
props.singleSignOnService.forEach((a, indexCount) => {
entityDescriptors.push({
SingleSignOnService: [{
_attr: {
Binding: a.binding,
Location: a.location,
isDefault: a.isDefault
}
}]
});
});

//
props.singleLogoutService.forEach((a, indexCount) => {
entityDescriptors.push({
SingleLogoutService: [{
_attr: {
Binding: a.binding,
Location: a.location,
isDefault: a.isDefault
}
}]
});
});

// Build the metadata xml based on the pass-in props
const metadataXml = xml([{
EntityDescriptor: [{
_attr: {
'xmlns': namespace.names.metadata,
'xmlns:assertion': namespace.names.assertion,
'xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
entityID: props.entityId,
},
}, { IDPSSODescriptor: entityDescriptors }],
}]);

return {
id: props.entityId,
metadata: fetchEssentials(metadataXml.toString()),
rawMetadata: metadataXml.toString()
};

}

/**
* Create an idp by import a metadata, we separate the creation via metadata or create via object
*
* @param props
* @returns
*/
export const load = (props: LoadProps): IdentityProvider => {

props = LoadProps.parse(props);

// Load from url or file
const online = props.metadata.startsWith('http');

let xmlString: string = '';

// Get the metadata file from online
if (online) {
// TODO
}

// Load the metadata file as xml string
if (!online) {
xmlString = props.metadata.toString();
}

// Fetch essential from its metadata
const metadata: Metadata = fetchEssentials(xmlString);

return {
id: metadata.entityID,
metadata: metadata,
rawMetadata: xmlString
}

};
Loading