@boxyhq/saml-jackson 0.2.3-beta.210 → 0.2.3-beta.222

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.
Files changed (46) hide show
  1. package/.eslintrc.js +13 -0
  2. package/package.json +2 -2
  3. package/prettier.config.js +4 -0
  4. package/src/controller/api.ts +225 -0
  5. package/src/controller/error.ts +13 -0
  6. package/src/controller/oauth/allowed.ts +22 -0
  7. package/src/controller/oauth/code-verifier.ts +11 -0
  8. package/src/controller/oauth/redirect.ts +12 -0
  9. package/src/controller/oauth.ts +337 -0
  10. package/src/controller/utils.ts +17 -0
  11. package/src/db/db.ts +100 -0
  12. package/src/db/encrypter.ts +38 -0
  13. package/src/db/mem.ts +128 -0
  14. package/src/db/mongo.ts +110 -0
  15. package/src/db/redis.ts +103 -0
  16. package/src/db/sql/entity/JacksonIndex.ts +44 -0
  17. package/src/db/sql/entity/JacksonStore.ts +43 -0
  18. package/src/db/sql/entity/JacksonTTL.ts +17 -0
  19. package/src/db/sql/model/JacksonIndex.ts +3 -0
  20. package/src/db/sql/model/JacksonStore.ts +8 -0
  21. package/src/db/sql/sql.ts +184 -0
  22. package/src/db/store.ts +49 -0
  23. package/src/db/utils.ts +26 -0
  24. package/src/env.ts +42 -0
  25. package/src/index.ts +79 -0
  26. package/src/jackson.ts +171 -0
  27. package/src/read-config.ts +29 -0
  28. package/src/saml/claims.ts +41 -0
  29. package/src/saml/saml.ts +234 -0
  30. package/src/saml/x509.ts +51 -0
  31. package/src/test/api.test.ts +271 -0
  32. package/src/test/data/metadata/boxyhq.js +6 -0
  33. package/src/test/data/metadata/boxyhq.xml +30 -0
  34. package/src/test/data/saml_response +1 -0
  35. package/src/test/db.test.ts +313 -0
  36. package/src/test/oauth.test.ts +353 -0
  37. package/src/typings.ts +167 -0
  38. package/tsconfig.build.json +6 -0
  39. package/tsconfig.json +26 -0
  40. package/.nyc_output/36a3e9e1-42eb-468d-a9ec-8d206fedcd3e.json +0 -1
  41. package/.nyc_output/8c0af85a-b807-45bb-8331-20c3aabe15df.json +0 -1
  42. package/.nyc_output/ad148b90-7401-4df2-959f-3fdcf81a06ec.json +0 -1
  43. package/.nyc_output/processinfo/36a3e9e1-42eb-468d-a9ec-8d206fedcd3e.json +0 -1
  44. package/.nyc_output/processinfo/8c0af85a-b807-45bb-8331-20c3aabe15df.json +0 -1
  45. package/.nyc_output/processinfo/ad148b90-7401-4df2-959f-3fdcf81a06ec.json +0 -1
  46. package/.nyc_output/processinfo/index.json +0 -1
@@ -0,0 +1,234 @@
1
+ const saml = require('@boxyhq/saml20');
2
+ const xml2js = require('xml2js');
3
+ const thumbprint = require('thumbprint');
4
+ const xmlcrypto = require('xml-crypto');
5
+ import * as rambda from 'rambda';
6
+ import xmlbuilder from 'xmlbuilder';
7
+ import crypto from 'crypto';
8
+ import claims from './claims';
9
+ import { SAMLProfile, SAMLReq } from 'saml-jackson';
10
+
11
+ const idPrefix = '_';
12
+ const authnXPath =
13
+ '/*[local-name(.)="AuthnRequest" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:protocol"]';
14
+ const issuerXPath =
15
+ '/*[local-name(.)="Issuer" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:assertion"]';
16
+
17
+ const signRequest = (xml: string, signingKey: string) => {
18
+ if (!xml) {
19
+ throw new Error('Please specify xml');
20
+ }
21
+ if (!signingKey) {
22
+ throw new Error('Please specify signingKey');
23
+ }
24
+
25
+ const sig = new xmlcrypto.SignedXml();
26
+ sig.signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
27
+ sig.signingKey = signingKey;
28
+ sig.addReference(
29
+ authnXPath,
30
+ [
31
+ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature',
32
+ 'http://www.w3.org/2001/10/xml-exc-c14n#',
33
+ ],
34
+ 'http://www.w3.org/2001/04/xmlenc#sha256'
35
+ );
36
+ sig.computeSignature(xml, {
37
+ location: { reference: authnXPath + issuerXPath, action: 'after' },
38
+ });
39
+
40
+ return sig.getSignedXml();
41
+ };
42
+
43
+ const request = ({
44
+ ssoUrl,
45
+ entityID,
46
+ callbackUrl,
47
+ isPassive = false,
48
+ forceAuthn = false,
49
+ identifierFormat = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
50
+ providerName = 'BoxyHQ',
51
+ signingKey,
52
+ }: SAMLReq): { id: string; request: string } => {
53
+ const id = idPrefix + crypto.randomBytes(10).toString('hex');
54
+ const date = new Date().toISOString();
55
+
56
+ let samlReq: Record<string, any> = {
57
+ 'samlp:AuthnRequest': {
58
+ '@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
59
+ '@ID': id,
60
+ '@Version': '2.0',
61
+ '@IssueInstant': date,
62
+ '@ProtocolBinding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
63
+ '@Destination': ssoUrl,
64
+ 'saml:Issuer': {
65
+ '@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
66
+ '#text': entityID,
67
+ },
68
+ },
69
+ };
70
+
71
+ if (isPassive) samlReq['samlp:AuthnRequest']['@IsPassive'] = true;
72
+
73
+ if (forceAuthn) {
74
+ samlReq['samlp:AuthnRequest']['@ForceAuthn'] = true;
75
+ }
76
+
77
+ samlReq['samlp:AuthnRequest']['@AssertionConsumerServiceURL'] = callbackUrl;
78
+
79
+ samlReq['samlp:AuthnRequest']['samlp:NameIDPolicy'] = {
80
+ '@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
81
+ '@Format': identifierFormat,
82
+ '@AllowCreate': 'true',
83
+ };
84
+
85
+ if (providerName != null) {
86
+ samlReq['samlp:AuthnRequest']['@ProviderName'] = providerName;
87
+ }
88
+
89
+ let xml = xmlbuilder.create(samlReq).end({});
90
+ if (signingKey) {
91
+ xml = signRequest(xml, signingKey);
92
+ }
93
+
94
+ return {
95
+ id,
96
+ request: xml,
97
+ };
98
+ };
99
+
100
+ const parseAsync = async (
101
+ rawAssertion: string
102
+ ): Promise<SAMLProfile | void> => {
103
+ return new Promise((resolve, reject) => {
104
+ saml.parse(
105
+ rawAssertion,
106
+ function onParseAsync(err: Error, profile: SAMLProfile) {
107
+ if (err) {
108
+ reject(err);
109
+ return;
110
+ }
111
+
112
+ resolve(profile);
113
+ }
114
+ );
115
+ });
116
+ };
117
+
118
+ const validateAsync = async (
119
+ rawAssertion: string,
120
+ options
121
+ ): Promise<SAMLProfile | void> => {
122
+ return new Promise((resolve, reject) => {
123
+ saml.validate(
124
+ rawAssertion,
125
+ options,
126
+ function onValidateAsync(err, profile: SAMLProfile) {
127
+ if (err) {
128
+ reject(err);
129
+ return;
130
+ }
131
+
132
+ if (profile && profile.claims) {
133
+ // we map claims to our attributes id, email, firstName, lastName where possible. We also map original claims to raw
134
+ profile.claims = claims.map(profile.claims);
135
+
136
+ // some providers don't return the id in the assertion, we set it to a sha256 hash of the email
137
+ if (!profile.claims.id) {
138
+ profile.claims.id = crypto
139
+ .createHash('sha256')
140
+ .update(profile.claims.email)
141
+ .digest('hex');
142
+ }
143
+ }
144
+
145
+ resolve(profile);
146
+ }
147
+ );
148
+ });
149
+ };
150
+
151
+ const parseMetadataAsync = async (
152
+ idpMeta: string
153
+ ): Promise<Record<string, any>> => {
154
+ return new Promise((resolve, reject) => {
155
+ xml2js.parseString(
156
+ idpMeta,
157
+ { tagNameProcessors: [xml2js.processors.stripPrefix] },
158
+ (err: Error, res) => {
159
+ if (err) {
160
+ reject(err);
161
+ return;
162
+ }
163
+
164
+ const entityID = rambda.path('EntityDescriptor.$.entityID', res);
165
+ let X509Certificate = null;
166
+ let ssoPostUrl: null | undefined = null;
167
+ let ssoRedirectUrl: null | undefined = null;
168
+ let loginType = 'idp';
169
+
170
+ let ssoDes: any = rambda.pathOr(
171
+ null,
172
+ 'EntityDescriptor.IDPSSODescriptor',
173
+ res
174
+ );
175
+ if (!ssoDes) {
176
+ ssoDes = rambda.pathOr([], 'EntityDescriptor.SPSSODescriptor', res);
177
+ if (!ssoDes) {
178
+ loginType = 'sp';
179
+ }
180
+ }
181
+
182
+ for (const ssoDesRec of ssoDes) {
183
+ const keyDes = ssoDesRec['KeyDescriptor'];
184
+ for (const keyDesRec of keyDes) {
185
+ if (keyDesRec['$'] && keyDesRec['$'].use === 'signing') {
186
+ const ki = keyDesRec['KeyInfo'][0];
187
+ const cd = ki['X509Data'][0];
188
+ X509Certificate = cd['X509Certificate'][0];
189
+ }
190
+ }
191
+
192
+ const ssoSvc =
193
+ ssoDesRec['SingleSignOnService'] ||
194
+ ssoDesRec['AssertionConsumerService'] ||
195
+ [];
196
+ for (const ssoSvcRec of ssoSvc) {
197
+ if (
198
+ rambda.pathOr('', '$.Binding', ssoSvcRec).endsWith('HTTP-POST')
199
+ ) {
200
+ ssoPostUrl = rambda.path('$.Location', ssoSvcRec);
201
+ } else if (
202
+ rambda
203
+ .pathOr('', '$.Binding', ssoSvcRec)
204
+ .endsWith('HTTP-Redirect')
205
+ ) {
206
+ ssoRedirectUrl = rambda.path('$.Location', ssoSvcRec);
207
+ }
208
+ }
209
+ }
210
+
211
+ const ret: Record<string, any> = {
212
+ sso: {},
213
+ };
214
+ if (entityID) {
215
+ ret.entityID = entityID;
216
+ }
217
+ if (X509Certificate) {
218
+ ret.thumbprint = thumbprint.calculate(X509Certificate);
219
+ }
220
+ if (ssoPostUrl) {
221
+ ret.sso.postUrl = ssoPostUrl;
222
+ }
223
+ if (ssoRedirectUrl) {
224
+ ret.sso.redirectUrl = ssoRedirectUrl;
225
+ }
226
+ ret.loginType = loginType;
227
+
228
+ resolve(ret);
229
+ }
230
+ );
231
+ });
232
+ };
233
+
234
+ export default { request, parseAsync, validateAsync, parseMetadataAsync };
@@ -0,0 +1,51 @@
1
+ import * as x509 from '@peculiar/x509';
2
+ import { Crypto } from '@peculiar/webcrypto';
3
+
4
+ const crypto = new Crypto();
5
+ x509.cryptoProvider.set(crypto);
6
+
7
+ const alg = {
8
+ name: 'RSASSA-PKCS1-v1_5',
9
+ hash: 'SHA-256',
10
+ publicExponent: new Uint8Array([1, 0, 1]),
11
+ modulusLength: 2048,
12
+ };
13
+
14
+ const generate = async () => {
15
+ const keys = await crypto.subtle.generateKey(alg, true, ['sign', 'verify']);
16
+
17
+ const extensions: x509.Extension[] = [
18
+ new x509.BasicConstraintsExtension(false, undefined, true),
19
+ ];
20
+
21
+ extensions.push(
22
+ new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature, true)
23
+ );
24
+ if (keys.publicKey) {
25
+ extensions.push(
26
+ await x509.SubjectKeyIdentifierExtension.create(keys.publicKey)
27
+ );
28
+ }
29
+
30
+ const cert = await x509.X509CertificateGenerator.createSelfSigned({
31
+ serialNumber: '01',
32
+ name: 'CN=BoxyHQ Jackson',
33
+ notBefore: new Date(),
34
+ notAfter: new Date('3021/01/01'), // TODO: set shorter expiry and rotate ceritifcates
35
+ signingAlgorithm: alg,
36
+ keys: keys,
37
+ extensions,
38
+ });
39
+ if (keys.privateKey) {
40
+ const pkcs8 = await crypto.subtle.exportKey('pkcs8', keys.privateKey);
41
+
42
+ return {
43
+ publicKey: cert.toString('pem'),
44
+ privateKey: x509.PemConverter.encode(pkcs8, 'private key'),
45
+ };
46
+ }
47
+ };
48
+
49
+ export default {
50
+ generate,
51
+ };
@@ -0,0 +1,271 @@
1
+ import * as path from 'path';
2
+ import { DatabaseOption, IdPConfig } from 'saml-jackson';
3
+ import sinon from 'sinon';
4
+ import tap from 'tap';
5
+ import * as dbutils from '../db/utils';
6
+ import controllers from '../index';
7
+ import readConfig from '../read-config';
8
+
9
+ // TODO: Add the type
10
+ let apiController;
11
+
12
+ const CLIENT_ID = '75edb050796a0eb1cf2cfb0da7245f85bc50baa7';
13
+ const PROVIDER = 'accounts.google.com';
14
+ const OPTIONS = {
15
+ externalUrl: 'https://my-cool-app.com',
16
+ samlAudience: 'https://saml.boxyhq.com',
17
+ samlPath: '/sso/oauth/saml',
18
+ db: {
19
+ engine: 'mongo',
20
+ url: 'mongodb://localhost:27017/jackson-demo',
21
+ } as DatabaseOption,
22
+ };
23
+
24
+ tap.before(async () => {
25
+ const controller = await controllers(OPTIONS);
26
+
27
+ apiController = controller.apiController;
28
+ });
29
+
30
+ tap.teardown(async () => {
31
+ process.exit(0);
32
+ });
33
+
34
+ tap.test('controller/api', async (t) => {
35
+ const metadataPath = path.join(__dirname, '/data/metadata');
36
+ const config = await readConfig(metadataPath);
37
+
38
+ t.afterEach(async () => {
39
+ await apiController.deleteConfig({
40
+ tenant: config[0].tenant,
41
+ product: config[0].product,
42
+ });
43
+ });
44
+
45
+ t.test('Create the config', async (t) => {
46
+ t.test('when required fields are missing or invalid', async (t) => {
47
+ t.test('when `rawMetadata` is empty', async (t) => {
48
+ const body: Partial<IdPConfig> = Object.assign({}, config[0]);
49
+ delete body['rawMetadata'];
50
+
51
+ try {
52
+ await apiController.config(body);
53
+ t.fail('Expecting JacksonError.');
54
+ } catch (err: any) {
55
+ t.equal(err.message, 'Please provide rawMetadata');
56
+ t.equal(err.statusCode, 400);
57
+ }
58
+
59
+ t.end();
60
+ });
61
+
62
+ t.test('when `defaultRedirectUrl` is empty', async (t) => {
63
+ const body: Partial<IdPConfig> = Object.assign({}, config[0]);
64
+ delete body['defaultRedirectUrl'];
65
+
66
+ try {
67
+ await apiController.config(body);
68
+ t.fail('Expecting JacksonError.');
69
+ } catch (err: any) {
70
+ t.equal(err.message, 'Please provide a defaultRedirectUrl');
71
+ t.equal(err.statusCode, 400);
72
+ }
73
+
74
+ t.end();
75
+ });
76
+
77
+ t.test('when `redirectUrl` is empty', async (t) => {
78
+ const body: Partial<IdPConfig> = Object.assign({}, config[0]);
79
+ delete body['redirectUrl'];
80
+
81
+ try {
82
+ await apiController.config(body);
83
+ t.fail('Expecting JacksonError.');
84
+ } catch (err: any) {
85
+ t.equal(err.message, 'Please provide redirectUrl');
86
+ t.equal(err.statusCode, 400);
87
+ }
88
+
89
+ t.end();
90
+ });
91
+
92
+ t.test('when `tenant` is empty', async (t) => {
93
+ const body: Partial<IdPConfig> = Object.assign({}, config[0]);
94
+ delete body['tenant'];
95
+
96
+ try {
97
+ await apiController.config(body);
98
+ t.fail('Expecting JacksonError.');
99
+ } catch (err: any) {
100
+ t.equal(err.message, 'Please provide tenant');
101
+ t.equal(err.statusCode, 400);
102
+ }
103
+
104
+ t.end();
105
+ });
106
+
107
+ t.test('when `product` is empty', async (t) => {
108
+ const body: Partial<IdPConfig> = Object.assign({}, config[0]);
109
+ delete body['product'];
110
+
111
+ try {
112
+ await apiController.config(body);
113
+ t.fail('Expecting JacksonError.');
114
+ } catch (err: any) {
115
+ t.equal(err.message, 'Please provide product');
116
+ t.equal(err.statusCode, 400);
117
+ }
118
+
119
+ t.end();
120
+ });
121
+
122
+ t.test('when `rawMetadata` is not a valid XML', async (t) => {
123
+ const body = Object.assign({}, config[0]);
124
+ body['rawMetadata'] = 'not a valid XML';
125
+
126
+ try {
127
+ await apiController.config(body);
128
+ t.fail('Expecting Error.');
129
+ } catch (err: any) {
130
+ t.match(err.message, /Non-whitespace before first tag./);
131
+ }
132
+
133
+ t.end();
134
+ });
135
+ });
136
+
137
+ t.test('when the request is good', async (t) => {
138
+ const body = Object.assign({}, config[0]);
139
+
140
+ const kdStub = sinon.stub(dbutils, 'keyDigest').returns(CLIENT_ID);
141
+
142
+ const response = await apiController.config(body);
143
+
144
+ t.ok(kdStub.called);
145
+ t.equal(response.client_id, CLIENT_ID);
146
+ t.equal(response.provider, PROVIDER);
147
+
148
+ let savedConf = await apiController.getConfig({
149
+ clientID: CLIENT_ID,
150
+ });
151
+
152
+ t.equal(savedConf.provider, PROVIDER);
153
+
154
+ kdStub.restore();
155
+
156
+ t.end();
157
+ });
158
+
159
+ t.end();
160
+ });
161
+
162
+ t.test('Get the config', async (t) => {
163
+ t.test('when valid request', async (t) => {
164
+ const body: Partial<IdPConfig> = Object.assign({}, config[0]);
165
+
166
+ await apiController.config(body);
167
+
168
+ const { provider } = await apiController.getConfig(body);
169
+
170
+ t.equal(provider, PROVIDER);
171
+
172
+ t.end();
173
+ });
174
+
175
+ t.test('when invalid request', async (t) => {
176
+ let response;
177
+
178
+ const body: Partial<IdPConfig> = Object.assign({}, config[0]);
179
+
180
+ await apiController.config(body);
181
+
182
+ // Empty body
183
+ try {
184
+ await apiController.getConfig({});
185
+ t.fail('Expecting Error.');
186
+ } catch (err: any) {
187
+ t.match(
188
+ err.message,
189
+ 'Please provide `clientID` or `tenant` and `product`.'
190
+ );
191
+ }
192
+
193
+ // Invalid clientID
194
+ response = await apiController.getConfig({
195
+ clientID: 'an invalid clientID',
196
+ });
197
+
198
+ t.match(response, {});
199
+
200
+ // Invalid tenant and product combination
201
+ response = await apiController.getConfig({
202
+ tenant: 'demo.com',
203
+ product: 'desk',
204
+ });
205
+
206
+ t.match(response, {});
207
+
208
+ t.end();
209
+ });
210
+
211
+ t.end();
212
+ });
213
+
214
+ t.test('Delete the config', async (t) => {
215
+ t.test('when valid request', async (t) => {
216
+ const body: Partial<IdPConfig> = Object.assign({}, config[0]);
217
+
218
+ const client = await apiController.config(body);
219
+
220
+ await apiController.deleteConfig({
221
+ clientID: client.client_id,
222
+ clientSecret: client.client_secret,
223
+ });
224
+
225
+ const response = await apiController.getConfig({
226
+ clientID: client.client_id,
227
+ });
228
+
229
+ t.match(response, {});
230
+
231
+ t.end();
232
+ });
233
+
234
+ t.test('when invalid request', async (t) => {
235
+ let response;
236
+
237
+ const body: Partial<IdPConfig> = Object.assign({}, config[0]);
238
+
239
+ const client = await apiController.config(body);
240
+
241
+ // Empty body
242
+ try {
243
+ await apiController.deleteConfig({});
244
+ t.fail('Expecting Error.');
245
+ } catch (err: any) {
246
+ t.match(
247
+ err.message,
248
+ 'Please provide `clientID` and `clientSecret` or `tenant` and `product`.'
249
+ );
250
+ }
251
+
252
+ // Invalid clientID or clientSecret
253
+ try {
254
+ await apiController.deleteConfig({
255
+ clientID: client.client_id,
256
+ clientSecret: 'invalid client secret',
257
+ });
258
+
259
+ t.fail('Expecting Error.');
260
+ } catch (err: any) {
261
+ t.match(err.message, 'clientSecret mismatch.');
262
+ }
263
+
264
+ t.end();
265
+ });
266
+
267
+ t.end();
268
+ });
269
+
270
+ t.end();
271
+ });
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ defaultRedirectUrl: 'http://localhost:3000/sso/oauth/completed',
3
+ redirectUrl: '["http://localhost:3000"]',
4
+ tenant: 'boxyhq.com',
5
+ product: 'crm',
6
+ };
@@ -0,0 +1,30 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://accounts.google.com/o/saml2" validUntil="2026-06-22T18:39:53.000Z">
3
+ <md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
4
+ <md:KeyDescriptor use="signing">
5
+ <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6
+ <ds:X509Data>
7
+ <ds:X509Certificate>MIIDdDCCAlygAwIBAgIGAXo6K+u/MA0GCSqGSIb3DQEBCwUAMHsxFDASBgNVBAoTC0dvb2dsZSBJ
8
+ bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MQ8wDQYDVQQDEwZHb29nbGUxGDAWBgNVBAsTD0dv
9
+ b2dsZSBGb3IgV29yazELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEwHhcNMjEwNjIz
10
+ MTgzOTUzWhcNMjYwNjIyMTgzOTUzWjB7MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEWMBQGA1UEBxMN
11
+ TW91bnRhaW4gVmlldzEPMA0GA1UEAxMGR29vZ2xlMRgwFgYDVQQLEw9Hb29nbGUgRm9yIFdvcmsx
12
+ CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
13
+ MIIBCgKCAQEA4qZcxwPiVka9GzGdQ9LVlgVkn3A7O3HtxR6RIm5AMaL4YZziEHt2HgxLdJZyXYJw
14
+ yfT1KB2IHt+XDQBkgEpQVXuuwSPI8vhI8Jr+nr8zia3MMoy9vJF8ZG7HuWeakaKEh7tJqjYu1Cl9
15
+ a81rkYdXAFUA+gl2q+stvK26xylAUwptCJSQo0NanWzCq+k5zvX0uLmh58+W5Yv11hDTtAoW+1dH
16
+ LWUTHXPfoZINPRy5NGKJ2Onq5/D5XJRimNnUa2iYi0Yv9txp1RRq4dpB9MaVttt3iKyDo4/+8fg/
17
+ bL8BLhguiOeqcP4DEIzMuExi3bZAOu2NC7k7Qf28nA81LzP9DQIDAQABMA0GCSqGSIb3DQEBCwUA
18
+ A4IBAQARBNB3+MfmKr5WXNXXE9YwUzUGmpfbqUPXh2y2dOAkj6TzoekAsOLWB0p8oyJ5d1bFlTsx
19
+ i1OY9RuFl0tc35Jbo+ae5GfUvJmbnYGi9z8sBL55HY6x3KQNmM/ehof7ttZwvB6nwuRxAiGYG497
20
+ 3tSzrqMQzEskcgX1mlCW0vks/ztCaayprDXcCUxWdP9FaiSZDEXV6PHhFZgGlRNvERsgaMDJgOsq
21
+ v6hLX10Q9CtOWzqu18PI4DcfoZ7exWcC29yWvwZzDTfHGaSG1DtUFLwiQmhVUbfd7/fmLV+/iOxV
22
+ zI0b5xSYZOJ7Kena7gd5zGVrc2ygKAFKiffiI5GLmLkv</ds:X509Certificate>
23
+ </ds:X509Data>
24
+ </ds:KeyInfo>
25
+ </md:KeyDescriptor>
26
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
27
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://accounts.google.com/o/saml2"/>
28
+ <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://accounts.google.com/o/saml2"/>
29
+ </md:IDPSSODescriptor>
30
+ </md:EntityDescriptor>
@@ -0,0 +1 @@
1
+ PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzYW1sMnA6UmVzcG9uc2UgeG1sbnM6c2FtbDJwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIERlc3RpbmF0aW9uPSJodHRwczovLzdlYTItMTAzLTE1My0xMDQtMTYxLm5ncm9rLmlvL3Nzby9vYXV0aC9zYW1sIiBJRD0iXzRkZmM5MjYwZDFjZTVlMDRhZTQ4ZTg4ZWJkNGNlOTY3IiBJblJlc3BvbnNlVG89Il9kYWNkMTRhZGVmMmNiMDc1NGM5NiIgSXNzdWVJbnN0YW50PSIyMDIxLTEyLTA2VDE1OjIzOjA3LjM2MFoiIFZlcnNpb249IjIuMCI+DQogICA8c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5odHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vby9zYW1sMjwvc2FtbDI6SXNzdWVyPg0KICAgPGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogICAgICA8ZHM6U2lnbmVkSW5mbz4NCiAgICAgICAgIDxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIiAvPg0KICAgICAgICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiIC8+DQogICAgICAgICA8ZHM6UmVmZXJlbmNlIFVSST0iI180ZGZjOTI2MGQxY2U1ZTA0YWU0OGU4OGViZDRjZTk2NyI+DQogICAgICAgICAgICA8ZHM6VHJhbnNmb3Jtcz4NCiAgICAgICAgICAgICAgIDxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIgLz4NCiAgICAgICAgICAgICAgIDxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiIC8+DQogICAgICAgICAgICA8L2RzOlRyYW5zZm9ybXM+DQogICAgICAgICAgICA8ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2IiAvPg0KICAgICAgICAgICAgPGRzOkRpZ2VzdFZhbHVlPjI3Vy9EZTVKTlZIWkk1VTVKZGIvUi9mUXIya0pmd1VPbk0vVlRmQ1ZwQU09PC9kczpEaWdlc3RWYWx1ZT4NCiAgICAgICAgIDwvZHM6UmVmZXJlbmNlPg0KICAgICAgPC9kczpTaWduZWRJbmZvPg0KICAgICAgPGRzOlNpZ25hdHVyZVZhbHVlPm4vWCsvb0ZzK3JNU2gxMlg4dnowWlNkNlFpcU50eTY3RnFVbVNwdGllRmNoM0NScGQzZ2dpSHNwTTlMcm9MWjZVZGlsbThtdFZqTkgNCi9ob21yWDNvVEQ4UStxdzNiSllOTEptNEQvRWNmZmRDNmpSb0RJZzRYeFYxYlBVaWhQTnI4dlBFZEF2eEdwNTNiZ2MyREJsWkJpT3gNCnh4RlBSbEtnajJDWjh5SWk1R05FMUVTYms2SEtjY3g4R2dxWmtSYkVRbWtnbVMxZG1xcGl5bUpQM3orMHlaaXQyZ3dwQW5WVjNCMDUNCnZOcy8rSTFzRlZLaHNWcTc4QzZNWlZzV1pUSi80RFhadWhVSnpLNElTSU11b1RqUWFacEZMeEpBKzlhZzIvcm9OMjkwcitpcTZ5MVQNCjNHSlF1TlBPU0JVS1NpWlZVNmdwTldRRDBxckFxSWFQUFpOZnp3PT08L2RzOlNpZ25hdHVyZVZhbHVlPg0KICAgICAgPGRzOktleUluZm8+DQogICAgICAgICA8ZHM6WDUwOURhdGE+DQogICAgICAgICAgICA8ZHM6WDUwOVN1YmplY3ROYW1lPlNUPUNhbGlmb3JuaWEsQz1VUyxPVT1Hb29nbGUgRm9yIFdvcmssQ049R29vZ2xlLEw9TW91bnRhaW4gVmlldyxPPUdvb2dsZSBJbmMuPC9kczpYNTA5U3ViamVjdE5hbWU+DQogICAgICAgICAgICA8ZHM6WDUwOUNlcnRpZmljYXRlPk1JSURkRENDQWx5Z0F3SUJBZ0lHQVhvNksrdS9NQTBHQ1NxR1NJYjNEUUVCQ3dVQU1Ic3hGREFTQmdOVkJBb1RDMGR2YjJkc1pTQkoNCmJtTXVNUll3RkFZRFZRUUhFdzFOYjNWdWRHRnBiaUJXYVdWM01ROHdEUVlEVlFRREV3WkhiMjluYkdVeEdEQVdCZ05WQkFzVEQwZHYNCmIyZHNaU0JHYjNJZ1YyOXlhekVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXdIaGNOTWpFd05qSXoNCk1UZ3pPVFV6V2hjTk1qWXdOakl5TVRnek9UVXpXakI3TVJRd0VnWURWUVFLRXd0SGIyOW5iR1VnU1c1akxqRVdNQlFHQTFVRUJ4TU4NClRXOTFiblJoYVc0Z1ZtbGxkekVQTUEwR0ExVUVBeE1HUjI5dloyeGxNUmd3RmdZRFZRUUxFdzlIYjI5bmJHVWdSbTl5SUZkdmNtc3gNCkN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEENCk1JSUJDZ0tDQVFFQTRxWmN4d1BpVmthOUd6R2RROUxWbGdWa24zQTdPM0h0eFI2UkltNUFNYUw0WVp6aUVIdDJIZ3hMZEpaeVhZSncNCnlmVDFLQjJJSHQrWERRQmtnRXBRVlh1dXdTUEk4dmhJOEpyK25yOHppYTNNTW95OXZKRjhaRzdIdVdlYWthS0VoN3RKcWpZdTFDbDkNCmE4MXJrWWRYQUZVQStnbDJxK3N0dksyNnh5bEFVd3B0Q0pTUW8wTmFuV3pDcStrNXp2WDB1TG1oNTgrVzVZdjExaERUdEFvVysxZEgNCkxXVVRIWFBmb1pJTlBSeTVOR0tKMk9ucTUvRDVYSlJpbU5uVWEyaVlpMFl2OXR4cDFSUnE0ZHBCOU1hVnR0dDNpS3lEbzQvKzhmZy8NCmJMOEJMaGd1aU9lcWNQNERFSXpNdUV4aTNiWkFPdTJOQzdrN1FmMjhuQTgxTHpQOURRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkN3VUENCkE0SUJBUUFSQk5CMytNZm1LcjVXWE5YWEU5WXdVelVHbXBmYnFVUFhoMnkyZE9Ba2o2VHpvZWtBc09MV0IwcDhveUo1ZDFiRmxUc3gNCmkxT1k5UnVGbDB0YzM1SmJvK2FlNUdmVXZKbWJuWUdpOXo4c0JMNTVIWTZ4M0tRTm1NL2Vob2Y3dHRad3ZCNm53dVJ4QWlHWUc0OTcNCjN0U3pycU1RekVza2NnWDFtbENXMHZrcy96dENhYXlwckRYY0NVeFdkUDlGYWlTWkRFWFY2UEhoRlpnR2xSTnZFUnNnYU1ESmdPc3ENCnY2aExYMTBROUN0T1d6cXUxOFBJNERjZm9aN2V4V2NDMjl5V3Z3WnpEVGZIR2FTRzFEdFVGTHdpUW1oVlViZmQ3L2ZtTFYrL2lPeFYNCnpJMGI1eFNZWk9KN0tlbmE3Z2Q1ekdWcmMyeWdLQUZLaWZmaUk1R0xtTGt2PC9kczpYNTA5Q2VydGlmaWNhdGU+DQogICAgICAgICA8L2RzOlg1MDlEYXRhPg0KICAgICAgPC9kczpLZXlJbmZvPg0KICAgPC9kczpTaWduYXR1cmU+DQogICA8c2FtbDJwOlN0YXR1cz4NCiAgICAgIDxzYW1sMnA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIiAvPg0KICAgPC9zYW1sMnA6U3RhdHVzPg0KICAgPHNhbWwyOkFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9Il8xMWZkN2U5MDdjZmNiMTEwODM3NjI5OGM1Nzc0ZjgyNyIgSXNzdWVJbnN0YW50PSIyMDIxLTEyLTA2VDE1OjIzOjA3LjM2MFoiIFZlcnNpb249IjIuMCI+DQogICAgICA8c2FtbDI6SXNzdWVyPmh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL3NhbWwyPC9zYW1sMjpJc3N1ZXI+DQogICAgICA8c2FtbDI6U3ViamVjdD4NCiAgICAgICAgIDxzYW1sMjpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPmtpcmFuQGRlbW8uY29tPC9zYW1sMjpOYW1lSUQ+DQogICAgICAgICA8c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPg0KICAgICAgICAgICAgPHNhbWwyOlN1YmplY3RDb25maXJtYXRpb25EYXRhIEluUmVzcG9uc2VUbz0iX2RhY2QxNGFkZWYyY2IwNzU0Yzk2IiBOb3RPbk9yQWZ0ZXI9IjIwMjEtMTItMDZUMTU6Mjg6MDcuMzYwWiIgUmVjaXBpZW50PSJodHRwczovLzdlYTItMTAzLTE1My0xMDQtMTYxLm5ncm9rLmlvL3Nzby9vYXV0aC9zYW1sIiAvPg0KICAgICAgICAgPC9zYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgICAgPC9zYW1sMjpTdWJqZWN0Pg0KICAgICAgPHNhbWwyOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDIxLTEyLTA2VDE1OjE4OjA3LjM2MFoiIE5vdE9uT3JBZnRlcj0iMjAyMS0xMi0wNlQxNToyODowNy4zNjBaIj4NCiAgICAgICAgIDxzYW1sMjpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICAgICAgPHNhbWwyOkF1ZGllbmNlPmh0dHBzOi8vc2FtbC5ib3h5aHEuY29tPC9zYW1sMjpBdWRpZW5jZT4NCiAgICAgICAgIDwvc2FtbDI6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgIDwvc2FtbDI6Q29uZGl0aW9ucz4NCiAgICAgIDxzYW1sMjpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICAgICA8c2FtbDI6QXR0cmlidXRlIE5hbWU9InVzZXIuZW1haWwiPg0KICAgICAgICAgICAgPHNhbWwyOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOmFueVR5cGUiPmtpcmFuQGRlbW8uY29tPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgICAgIDwvc2FtbDI6QXR0cmlidXRlPg0KICAgICAgICAgPHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJ1c2VyLmZpcnN0TmFtZSI+DQogICAgICAgICAgICA8c2FtbDI6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6YW55VHlwZSI+S2lyYW48L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgICAgPC9zYW1sMjpBdHRyaWJ1dGU+DQogICAgICAgICA8c2FtbDI6QXR0cmlidXRlIE5hbWU9InVzZXIuaWQiIC8+DQogICAgICAgICA8c2FtbDI6QXR0cmlidXRlIE5hbWU9InVzZXIubGFzdE5hbWUiPg0KICAgICAgICAgICAgPHNhbWwyOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOmFueVR5cGUiPks8L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgICAgPC9zYW1sMjpBdHRyaWJ1dGU+DQogICAgICAgICA8c2FtbDI6QXR0cmlidXRlIE5hbWU9Im1lbWJlci1vZiIgLz4NCiAgICAgIDwvc2FtbDI6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWwyOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAyMS0xMi0wNlQxNToxNzowNS4wMDBaIiBTZXNzaW9uSW5kZXg9Il8xMWZkN2U5MDdjZmNiMTEwODM3NjI5OGM1Nzc0ZjgyNyI+DQogICAgICAgICA8c2FtbDI6QXV0aG5Db250ZXh0Pg0KICAgICAgICAgICAgPHNhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOnVuc3BlY2lmaWVkPC9zYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj4NCiAgICAgICAgIDwvc2FtbDI6QXV0aG5Db250ZXh0Pg0KICAgICAgPC9zYW1sMjpBdXRoblN0YXRlbWVudD4NCiAgIDwvc2FtbDI6QXNzZXJ0aW9uPg0KPC9zYW1sMnA6UmVzcG9uc2U+