UNPKG

24.7 kBTypeScriptView Raw
1import React, {ReactNode} from 'react';
2
3import {IComponent} from "../types/component";
4import Types, { IInfrastructure } from "../types";
5
6import createMiddleware, { isMiddleware } from '../middleware/middleware-component';
7import createWebApp, { isWebApp } from '../webapp/webapp-component';
8import { isSecuredRoute } from './securedroute-component';
9import { isSecuredEntry } from './securedentry-component';
10import createRoute, { ROUTE_INSTANCE_TYPE } from '../route/route-component';
11import { getChildrenArray, findComponentRecursively } from '../libs';
12
13import {getBasename} from '../libs/iso-libs';
14
15import {
16 createAuthMiddleware,
17 createCallbackMiddleware,
18 IUserData,
19 EMAIL_CONFIRMATION_PARAM,
20 EMAIL_PARAM,
21 PASSWORD_PARAM,
22 AUTH_STATUS
23} from "./auth-middleware";
24
25import bodyParser from 'body-parser';
26import {isSecuredService} from "./securedservice-component";
27import {SERVICE_INSTANCE_TYPE} from "../service/service-component";
28
29export const AUTHENTICATION_INSTANCE_TYPE = "AuthenticationComponent";
30
31
32export const AuthenticationProvider = {
33 EMAIL: "AUTH_EMAIL",
34
35 GITHUB: "AUTH_GITHUB",
36
37 // TODO Medium Auth only partly implemented!!!
38 MEDIUM: "AUTH_MEDIUM"
39};
40
41export const AUTH_RESPONSE = {
42 EMAIL_INVALID: "EMAIL_INVALID", // the e-mail format is invalid
43 NOT_IMPLEMENTED: "NOT_IMPLEMENTED" // the authCallback is not implemented for this provider
44};
45
46export const getProviderKey = (provider) => {
47 return provider+SUFFIX_SECRET;
48}
49
50export const getClientSecret = (provider) => {
51 return process.env[getProviderKey(provider)];
52}
53
54/**
55 * This creates a customized middleware that catches an error when the user is not logged in, i.e. it forwards her
56 * to the provider's login-page
57 *
58 * @param clientId as defined in the app of the provider
59 * @param callbackUrl as defined in the app of the provider
60 * @param provider to create the redirect Url for
61 */
62export const createRequestLoginMiddleware = (clientId: string, callbackUrl: string, provider: string, loginUrl: string) => (err, req, res, next) => {
63
64 const path = require('path');
65
66 console.log("createRequestLoginMiddleware: ", err);
67
68 if (provider === AuthenticationProvider.EMAIL) {
69 console.log("request login from: ", loginUrl);
70
71 const page = err && req.query && req.query.page ? req.query.page : req.url;
72
73 res.redirect(`${path.join(getBasename(), loginUrl)}?page=${page}${err ? `&message=${err}` : "" }`);
74
75 } else if (provider === AuthenticationProvider.GITHUB) {
76 res.redirect(`https://github.com/login/oauth/authorize?scope=user:email&client_id=${clientId}&redirect_uri=${callbackUrl}?page=${req.url}`);
77 } else if (provider === AuthenticationProvider.MEDIUM) {
78 res.redirect(`https://medium.com/m/oauth/authorize?client_id=${clientId}&scope=basicProfile,listPublications,publishPost&state=${req.url}&response_type=code&redirect_uri=${callbackUrl}`);
79 }
80
81 return;
82
83};
84
85/**
86 * Create a function that creates a function that fetches the AccessToken of the provider
87 */
88export const createFetchAccessTokenFunction = (
89 clientId: string,
90 callbackUrl: string,
91 provider: string,
92 senderEmail?: string,
93 getSubject?: (recipient: string) => string,
94 getHtmlText?: (recipient: string, url: string) => string,
95) => (req: any) => {
96
97 if (provider === AuthenticationProvider.EMAIL) {
98
99 console.log("this is the EMAIL - createFetchAccessTokenFunction middleware");
100
101 const { email, password, page } = req.query;
102
103 if (email !== undefined && password !== undefined) {
104
105 // the function fFetch must call a service that responds with a json. this json is fed into createGetUserFunction
106 return {
107 redirectPage: page,
108 fFetch: async function () {
109
110 const uuidv4 = require('uuid/v4');
111 const access_token = uuidv4();
112
113 // TODO: here we call the service to send an email to the user
114 // see: https://docs.aws.amazon.com/de_de/sdk-for-javascript/v2/developer-guide/ses-examples-sending-email.html
115 const AWS = require('aws-sdk');
116
117
118 // Create sendEmail params
119 var params = {
120 Destination: { /* required */
121 BccAddresses: [
122 senderEmail
123 ],
124 CcAddresses: [
125 ],
126 ToAddresses: [
127 email
128 ]
129 },
130 Message: { /* required */
131 Body: { /* required */
132 Html: {
133 Charset: "UTF-8",
134 Data: getHtmlText(email,
135 `${callbackUrl}?${EMAIL_CONFIRMATION_PARAM}=${access_token}&${EMAIL_PARAM}=${email}`)
136 },
137 /*Text: {
138 Charset: "UTF-8",
139 Data: "TEXT_FORMAT_BODY"
140 }*/
141 },
142 Subject: {
143 Charset: 'UTF-8',
144 Data: getSubject(email)
145 }
146 },
147 Source: senderEmail, /* required */
148 ReplyToAddresses: [
149 senderEmail
150 ],
151 };
152
153 console.log("this is the fFetch of the mail-authentication");
154 // this is the response
155 return new Promise(function(resolve, reject) {
156
157 // Create the promise and SES service object
158 var sendPromise = new AWS.SES({apiVersion: '2010-12-01'}).sendEmail(params).promise();
159
160 // Handle promise's fulfilled/rejected states
161 sendPromise.then(
162 function(data) {
163 console.log(data.MessageId);
164
165 resolve({
166 id: email, // we take the email - that should be unique
167 name: "",
168 username: "",
169 imageUrl:"",
170 access_token: access_token,
171 email: email,
172 encrypted_password: password,
173 status: AUTH_STATUS.PENDING
174 })
175 }).catch(
176 function(err) {
177 console.error(err, err.stack);
178 reject(err);
179 });
180
181
182 });
183 }/*fetch(callbackUrl,{
184 method: 'POST',
185 body: `code=${code}&client_id=${clientId}&client_secret=${getClientSecret(provider)}&grant_type=authorization_code&redirect_uri=${callbackUrl}`,
186 headers: {
187 "Content-Type": "application/x-www-form-urlencoded",
188 "Accept": "application/json",
189 "Accept-Charset": "utf-8"
190 }
191 })*/
192 }
193 }
194
195
196
197 } else if (provider === AuthenticationProvider.GITHUB) {
198
199 // TODO check https://octokit.github.io/rest.js/ !!!
200 // see docs: https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/
201
202 //console.log("request: ", req);
203
204
205 const { state, code, error, page } = req.query;
206 // session_code = request.env['rack.request.query_hash']['code']
207
208 if (error !== undefined) {
209 // TODO handle an error, e.g. user does not authorize the app
210
211 console.log(error);
212
213 return;
214
215 } else if (code !== undefined) {
216
217 console.log("found redirect page: ", page)
218
219 return {
220 redirectPage: page,
221 fFetch: async function () {
222 return await fetch('https://github.com/login/oauth/access_token',{
223 method: 'POST',
224 body: `code=${code}&client_id=${clientId}&client_secret=${getClientSecret(provider)}`,
225 headers: {
226 "Content-Type": "application/x-www-form-urlencoded",
227 "Accept": "application/json",
228 "Accept-Charset": "utf-8"
229 }
230 }).then(function(response) {
231 return response.json();
232 })
233 }
234 }
235
236 };
237
238
239 } else if (provider === AuthenticationProvider.MEDIUM) {
240
241 console.log("Medium callback: ", req.query)
242 const { state, code, error } = req.query;
243
244
245 if (error !== undefined) {
246 // TODO handle an error, e.g. user does not authorize the app
247
248 console.log(error);
249
250 return;
251
252 } else if (code !== undefined) {
253 return {
254 redirectPage: state,
255 fFetch: async function () {
256 return await fetch('https://api.medium.com/v1/tokens', {
257 method: 'POST',
258 body: `code=${code}&client_id=${clientId}&client_secret=${getClientSecret(provider)}&grant_type=authorization_code&redirect_uri=${callbackUrl}`,
259 headers: {
260 "Content-Type": "application/x-www-form-urlencoded",
261 "Accept": "application/json",
262 "Accept-Charset": "utf-8"
263 }
264 }).then(function (response) {
265 console.log("request data: ", response)
266 return response.json();
267 })
268 }
269 }
270 }
271
272 }
273
274
275
276
277
278};
279
280export const createGetUserFunction = (provider: string) => async function (resJson: any): Promise<IUserData> {
281
282 console.log("createGetUserFunction: ", resJson);
283
284 if (provider === AuthenticationProvider.EMAIL) {
285
286 // we just provide the response-json
287 return new Promise(function(resolve, reject) {
288
289 resolve(resJson)
290 });
291
292 } else if (provider === AuthenticationProvider.GITHUB) {
293 const { token_type, access_token, /*refresh_token, scope, expires_at */ } = resJson;
294
295 // try the freshly acquired token and get the user's Medium.com id
296 return await fetch('https://api.github.com/user',{
297 method: 'GET',
298 headers: {
299 "Content-Type": "application/json",
300 "Accept": "application/json",
301 "Accept-Charset": "utf-8",
302 "Authorization": token_type+" "+access_token
303 }
304 }).then(function(response) {
305
306 // now parse the json
307 return response.json();
308
309 }).then(function(data){
310
311 /*
312 "login":"frankzickert",
313 "id":10414265,
314 "node_id":"MDQ6VXNlcjEwNDE0MjY1",
315 "avatar_url":"https://avatars0.githubusercontent.com/u/10414265?v=4",
316 "gravatar_id":"",
317 "url":"https://api.github.com/users/frankzickert",
318 "html_url":"https://github.com/frankzickert",
319 "followers_url":"https://api.github.com/users/frankzickert/followers",
320 "following_url":"https://api.github.com/users/frankzickert/following{/other_user}",
321 "gists_url":"https://api.github.com/users/frankzickert/gists{/gist_id}",
322 "starred_url":"https://api.github.com/users/frankzickert/starred{/owner}{/repo}",
323 "subscriptions_url":"https://api.github.com/users/frankzickert/subscriptions",
324 "organizations_url":"https://api.github.com/users/frankzickert/orgs",
325 "repos_url":"https://api.github.com/users/frankzickert/repos",
326 "events_url":"https://api.github.com/users/frankzickert/events{/privacy}",
327 "received_events_url":"https://api.github.com/users/frankzickert/received_events",
328 "type":"User",
329 "site_admin":false,
330 "name":"Frank Zickert",
331 "company":null,
332 "blog":"https://www.lean-data-science.com/",
333 "location":"Germany",
334 "email":"frank.zickert@lean-data-science.awsapps.com",
335 "hireable":null,
336 "bio":"I have been working as an IT professional for 15 years...",
337 "public_repos":3,
338 "public_gists":5,
339 "followers":1,
340 "following":0,
341 "created_at":"2015-01-06T07:23:33Z",
342 "updated_at":"2019-05-08T20:37:43Z"}
343 */
344
345 return {
346 id: data.id,
347 name: data.name,
348 username: data.login,
349 imageUrl: data.avatar_url,
350 email: data.email,
351 access_token: access_token,
352 status: AUTH_STATUS.ACTIVE
353 }
354
355 });
356
357
358
359 } else if (provider === AuthenticationProvider.MEDIUM) {
360 console.log("fetch user data of medium-user")
361 const { token_type, access_token, refresh_token, scope, expires_at } = resJson;
362
363 // try the freshly acquired token and get the user's Medium.com id
364 return await fetch('https://api.medium.com/v1/me',{
365 method: 'GET',
366 headers: {
367 "Content-Type": "application/json",
368 "Accept": "application/json",
369 "Accept-Charset": "utf-8",
370 "Authorization": token_type+" "+access_token
371 }
372 }).then(function(response) {
373
374 //console.log("user data fetched: ", response);
375 // now parse the json
376 return response.json();
377
378 }).then(function({data}){
379
380 console.log("user data parsed: ", data);
381 return {
382 id: data.id,
383 name: data.name,
384 username: data.username,
385 imageUrl: data.imageUrl,
386 email: data.email,
387 access_token: access_token,
388 status: AUTH_STATUS.ACTIVE
389 }
390
391 });
392
393
394 }
395
396
397
398
399}
400
401/**
402 * suffix to the provider. specifies the -env-variable to hold the clientSecret
403 */
404const SUFFIX_SECRET = "_SECRET"
405
406/**
407 * Specifies all the properties that a Authentication-Component must have
408 */
409export interface IAuthenticationArgs {
410
411 /**
412 * a unique id or name of the datalayer
413 */
414 id: string,
415
416 /**
417 * The provider must be a string of AuthenticationProvider
418 */
419 provider: string,
420
421 /**
422 * The clientID provided by the Provider, e.g. from Github
423 */
424 clientId?: string,
425
426 /**
427 * a fully qualified url, as specified in the setup of the auth-provider
428 */
429 callbackUrl: string,
430
431
432 /**
433 * An url to redirect in order to request the user to login, used by the email authentication
434 */
435 loginUrl: string,
436
437 /**
438 * The email address we use to send the confirmation request email. This email address must be verified in
439 * AWS Simple Email Service (SES)
440 */
441 senderEmail?: string,
442
443 /**
444 * callback to provide the subject of the confirmation email
445 * @param recipient
446 */
447 getSubject: (recipient: string) => string,
448
449 /**
450 * callback to provide the text (in html) of the confirmation email
451 * @param recipient
452 * @param url
453 */
454 getHtmlText: (recipient: string, url: string) => string,
455
456}
457
458
459/**
460 * specifies the properties that an Authentication-Component has during runtime
461 */
462export interface IAuthenticationProps {
463
464 setStoreIdentityData: (
465 storeIdentityData: (request: any, key: string, val: any, jsonData: any) => void
466 ) => void,
467
468 storeAuthData?: (request: any, key: string, val: any, jsonData: any) => void,
469
470 setGetIdentityData: (getIdentityData: (request: any, matchBrowserIdentity: boolean, key: string, val: any) => any) => void,
471
472 getAuthData?: (request: any, matchBrowserIdentity: boolean, key: string, val: any) => any,
473
474 /**
475 *
476 * @param triggerRedirect function that triggers a redirect, that we can call with an argument
477 */
478 authCallback: (email: string, password: string, page: string, onResponse: (code:string)=> void) => void
479}
480
481
482/**
483 * The WebApp is a client that runs in the browser, SPA or SSR
484 *
485 * @param props
486 */
487export default (props: IAuthenticationArgs | any) => {
488
489 //console.log ("route: ",props );
490
491 const componentProps: IInfrastructure & IComponent = {
492 infrastructureType: Types.INFRASTRUCTURE_TYPE_COMPONENT,
493 instanceType: AUTHENTICATION_INSTANCE_TYPE,
494 instanceId: props.id,
495 };
496
497
498
499 const authenticationProps: IAuthenticationProps = {
500
501 // set our storeData-Property
502 setStoreIdentityData: (storeIdentityData: (request: any, key: string, val: any, jsonData: any) => void) => {
503 //console.log("setStoreIdentityData: ", storeIdentityData);
504 props.storeAuthData = storeIdentityData;
505
506 //console.log("auth-props: ", props)
507 },
508
509 setGetIdentityData: (getIdentityData: (request: any, matchBrowserIdentity: boolean, key: string, val: any) => any ) => {
510 props.getAuthData = getIdentityData;
511 },
512
513 authCallback: (email: string, password: string, page: string, onResponse: (code:string) => void) => {
514 console.log("this is the auth-Callback");
515
516 if (props.provider === AuthenticationProvider.EMAIL) {
517 // verify the format of the e-mail address
518 if (!(/^([A-Za-z0-9_\-.])+@([A-Za-z0-9_\-.])+\.([A-Za-z]{2,})$/.test(email))) {
519 onResponse(AUTH_RESPONSE.EMAIL_INVALID);
520 return;
521 }
522
523 // redirect to the provided url that is
524 // TODO encrypt the password!
525 window.location.href = `${props.callbackUrl}?${EMAIL_PARAM}=${email}&${PASSWORD_PARAM}=${password}&page=${page}`;
526 return;
527 }
528
529
530 onResponse(AUTH_RESPONSE.NOT_IMPLEMENTED);
531
532 }
533
534 };
535
536 /**
537 * For this function is a callback on a middleware, it only sets the userId on the backend side!
538 * @param userid
539 */
540 const onAuthenticated = (userid:string): void => {
541 console.log("just authenticated, tell the securedEntries about it")
542 // we need to provide some data to the secured entries
543 findComponentRecursively(props.children, (c) => c.setUserId !== undefined).forEach( se => {
544 //console.log("found secured entry: ", se);
545 se.setUserId(userid)
546
547 });
548 };
549
550 findComponentRecursively(props.children, (c) => c.setMiddleware !== undefined).forEach( se => {
551 //console.log("found secured entry: ", se);
552 se.setMiddleware(createMiddleware({ callback:createAuthMiddleware(getClientSecret(props.provider), onAuthenticated)}))
553
554 });
555
556 // we need to provide some data to the secured routes
557 findComponentRecursively(props.children, isSecuredRoute).forEach( sr => {
558 //console.log("found secured route: ", sr);
559
560 sr.middlewares = [
561 // this middleware checks whether the user is logged in
562 createMiddleware({ callback: createAuthMiddleware(getClientSecret(props.provider), onAuthenticated)}),
563
564 // this middleware checks redirects the user to the login page, if she is not logged in
565 createMiddleware({ callback: createRequestLoginMiddleware(props.clientId, props.callbackUrl, props.provider, props.loginUrl)})
566
567 ].concat(sr.middlewares);
568
569 // now that we have added the authentication middlewares, the route can be handled as a normal one
570 sr.instanceType = ROUTE_INSTANCE_TYPE;
571
572 });
573
574 // we need to provide some data to the secured routes
575 findComponentRecursively(props.children, isSecuredService).forEach( service => {
576 //console.log("found secured route: ", sr);
577
578 service.middlewares = [
579 // this middleware checks whether the user is logged in
580 createMiddleware({ callback: createAuthMiddleware(getClientSecret(props.provider), onAuthenticated)}),
581
582 // this middleware checks redirects the user to the login page, if she is not logged in
583 createMiddleware({ callback: createRequestLoginMiddleware(props.clientId, props.callbackUrl, props.provider, props.loginUrl)})
584
585 ].concat(service.middlewares);
586
587 // now that we have added the authentication middlewares, the route can be handled as a normal one
588 service.instanceType = SERVICE_INSTANCE_TYPE;
589
590 });
591
592
593 // we need to provide the AuthenticationId to webApps, these may be anywhere in the tree, not
594 // only direct children. So rather than mapping the children, we need to change them
595 findComponentRecursively(props.children, (child) => child.setAuthenticationId !== undefined).forEach( child => {
596 child.setAuthenticationId(props.id)
597 });
598
599 /**
600 * The data-layer replaces the authentication component with its children
601 */
602 const mappedChildren = {
603 // we provide the middlewares that we require
604 children: [
605
606 // we create a webapp to handle the callback
607 createWebApp({
608 id: "WEBAPP_"+props.provider,
609 path: props.callbackUrl.substring(props.callbackUrl.lastIndexOf("/")),
610 method: "GET",
611 children: [
612
613 // middleware required of parsing the json response
614 createMiddleware({ callback: bodyParser.json() }),
615 createMiddleware({ callback: bodyParser.urlencoded({
616 extended: true
617 }) }),
618
619 createMiddleware({ callback: createCallbackMiddleware(
620 getClientSecret(props.provider),
621 createFetchAccessTokenFunction(
622 props.clientId,
623 props.callbackUrl,
624 props.provider,
625 props.senderEmail,
626 props.getSubject,
627 props.getHtmlText
628
629 ), //fetchAccessToken
630 createGetUserFunction(props.provider),
631 async function (request: any, key: string, val: any, jsonData: any) {
632 return await props.storeAuthData(request, key, val, jsonData)
633 },//props.storeAuthData
634 async function (request: any, matchBrowserIdentity: boolean, key: string, val: any) {
635 return await props.getAuthData(request, matchBrowserIdentity, key, val)
636 } //getAuthData
637
638 )}),
639
640 createMiddleware({
641 callback: createRequestLoginMiddleware(props.clientId, props.callbackUrl, props.provider, props.loginUrl)
642 }),
643
644 // the render function should never be called for the Callback-Middleware redirects to the
645 // page that was requested in the first request
646 createRoute({
647 path: props.callbackUrl.substring(props.callbackUrl.lastIndexOf("/")),
648 name: "WEBAPP_"+props.provider,
649 render: (rp) => <div>This is the callback route!</div>,
650 })
651
652 ]
653 })
654
655 //,
656 //(browserId: string) => redirectMiddleware("/dashboard")
657
658 ].concat(getChildrenArray(props.children))
659 };
660
661 console.log("authentication: ", findComponentRecursively(props.children, isWebApp));
662 return Object.assign(props, componentProps, authenticationProps, mappedChildren);
663
664};
665
666export const isAuthentication = (component) => {
667
668 return component !== undefined &&
669 component.instanceType === AUTHENTICATION_INSTANCE_TYPE;
670};
\No newline at end of file