@travetto/auth-web-passport 6.0.0-rc.4

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 ADDED
@@ -0,0 +1,130 @@
1
+ <!-- This file was generated by @travetto/doc and should not be modified directly -->
2
+ <!-- Please modify https://github.com/travetto/travetto/tree/main/module/auth-web-passport/DOC.tsx and execute "npx trv doc" to rebuild -->
3
+ # Web Auth Passport
4
+
5
+ ## Web authentication integration support for the Travetto framework
6
+
7
+ **Install: @travetto/auth-web-passport**
8
+ ```bash
9
+ npm install @travetto/auth-web-passport
10
+
11
+ # or
12
+
13
+ yarn add @travetto/auth-web-passport
14
+ ```
15
+
16
+ This is a primary integration for the [Web Auth](https://github.com/travetto/travetto/tree/main/module/auth-web#readme "Web authentication integration support for the Travetto framework") module.
17
+
18
+ Within the node ecosystem, the most prevalent auth framework is [passport](http://passportjs.org). With countless integrations, the desire to leverage as much of it as possible, is extremely high. To that end, this module provides support for [passport](http://passportjs.org) baked in. Registering and configuring a [passport](http://passportjs.org) strategy is fairly straightforward.
19
+
20
+ **NOTE:** Given that [passport](http://passportjs.org) is oriented around [express](https://expressjs.com), this module relies on [Web Connect Support](https://github.com/travetto/travetto/tree/main/module/web-connect#readme "Web integration for Connect-Like Resources") as an adapter for the request/response handoff. There are some limitations listed in the module, and those would translate to any [passport](http://passportjs.org) strategies that are being used.
21
+
22
+ **Code: Sample Facebook/passport config**
23
+ ```typescript
24
+ import { Strategy as FacebookStrategy } from 'passport-facebook';
25
+
26
+ import { InjectableFactory } from '@travetto/di';
27
+ import { Authenticator, Authorizer, Principal } from '@travetto/auth';
28
+ import { PassportAuthenticator } from '@travetto/auth-web-passport';
29
+
30
+ export class FbUser {
31
+ username: string;
32
+ permissions: string[];
33
+ }
34
+
35
+ export const FbAuthSymbol = Symbol.for('auth_facebook');
36
+
37
+ export class AppConfig {
38
+ @InjectableFactory(FbAuthSymbol)
39
+ static facebookPassport(): Authenticator {
40
+ return new PassportAuthenticator('facebook',
41
+ new FacebookStrategy(
42
+ {
43
+ clientID: '<appId>',
44
+ clientSecret: '<appSecret>',
45
+ callbackURL: 'http://localhost:3000/auth/facebook/callback',
46
+ profileFields: ['id', 'username', 'displayName', 'photos', 'email'],
47
+ },
48
+ (accessToken, refreshToken, profile, cb) =>
49
+ cb(undefined, profile)
50
+ ),
51
+ (user: FbUser) => ({
52
+ id: user.username,
53
+ permissions: user.permissions,
54
+ details: user
55
+ })
56
+ );
57
+ }
58
+
59
+ @InjectableFactory()
60
+ static principalSource(): Authorizer {
61
+ return new class implements Authorizer {
62
+ async authorize(p: Principal) {
63
+ return p;
64
+ }
65
+ }();
66
+ }
67
+ }
68
+ ```
69
+
70
+ As you can see, [PassportAuthenticator](https://github.com/travetto/travetto/tree/main/module/auth-web-passport/src/authenticator.ts#L15) will take care of the majority of the work, and all that is required is:
71
+ * Provide the name of the strategy (should be unique)
72
+ * Provide the strategy instance.
73
+ * The conversion functions which defines the mapping between external and local identities.
74
+
75
+ **Note**: You will need to provide the callback for the strategy to ensure you pass the external principal back into the framework
76
+ After that, the provider is no different than any other, and can be used accordingly. Additionally, because [passport](http://passportjs.org) runs first, in it's entirety, you can use the provider as you normally would any [passport](http://passportjs.org) middleware.
77
+
78
+ **Code: Sample endpoints using Facebook/passport provider**
79
+ ```typescript
80
+ import { Controller, Get, Post, WebRequest, ContextParam, WebResponse } from '@travetto/web';
81
+ import { Login, Authenticated, Logout } from '@travetto/auth-web';
82
+ import { Principal } from '@travetto/auth';
83
+
84
+ import { FbAuthSymbol } from './conf.ts';
85
+
86
+ @Controller('/auth')
87
+ export class SampleAuth {
88
+
89
+ @ContextParam()
90
+ request: WebRequest;
91
+
92
+ @ContextParam()
93
+ user: Principal;
94
+
95
+ @Get('/name')
96
+ async getName() {
97
+ return { name: 'bob' };
98
+ }
99
+
100
+ @Get('/facebook')
101
+ @Login(FbAuthSymbol)
102
+ async fbLogin() {
103
+
104
+ }
105
+
106
+ @Get('/self')
107
+ @Authenticated()
108
+ async getSelf() {
109
+ return this.user;
110
+ }
111
+
112
+ @Get('/facebook/callback')
113
+ @Login(FbAuthSymbol)
114
+ async fbLoginComplete() {
115
+ return WebResponse.redirect('/auth/self');
116
+ }
117
+
118
+ @Post('/logout')
119
+ @Logout()
120
+ async logout() { }
121
+
122
+ /**
123
+ * Simple Echo
124
+ */
125
+ @Post('/')
126
+ async echo(): Promise<unknown> {
127
+ return this.request.body;
128
+ }
129
+ }
130
+ ```
package/__index__.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './src/authenticator.ts';
2
+ export * from './src/util.ts';
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@travetto/auth-web-passport",
3
+ "version": "6.0.0-rc.4",
4
+ "description": "Web authentication integration support for the Travetto framework",
5
+ "keywords": [
6
+ "authentication",
7
+ "web",
8
+ "passport",
9
+ "travetto",
10
+ "decorators",
11
+ "typescript"
12
+ ],
13
+ "homepage": "https://travetto.io",
14
+ "license": "MIT",
15
+ "author": {
16
+ "email": "travetto.framework@gmail.com",
17
+ "name": "Travetto Framework"
18
+ },
19
+ "files": [
20
+ "__index__.ts",
21
+ "src"
22
+ ],
23
+ "main": "__index__.ts",
24
+ "repository": {
25
+ "url": "git+https://github.com/travetto/travetto.git",
26
+ "directory": "module/auth-web-passport"
27
+ },
28
+ "dependencies": {
29
+ "@travetto/auth": "^6.0.0-rc.2",
30
+ "@travetto/auth-web": "^6.0.0-rc.4",
31
+ "@travetto/config": "^6.0.0-rc.2",
32
+ "@travetto/web": "^6.0.0-rc.2",
33
+ "@travetto/web-connect": "^6.0.0-rc.2",
34
+ "@types/passport": "^1.0.17",
35
+ "passport": "^0.7.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/passport-facebook": "^3.0.3",
39
+ "passport-facebook": "^3.0.0"
40
+ },
41
+ "travetto": {
42
+ "displayName": "Web Auth Passport"
43
+ },
44
+ "private": false,
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }
@@ -0,0 +1,75 @@
1
+ import passport from 'passport';
2
+
3
+ import { Authenticator, AuthenticatorState, Principal } from '@travetto/auth';
4
+ import { WebFilterContext } from '@travetto/web';
5
+ import { WebConnectUtil } from '@travetto/web-connect';
6
+
7
+ import { PassportUtil } from './util.ts';
8
+
9
+ type SimplePrincipal = Omit<Principal, 'issuedAt' | 'expiresAt'>;
10
+ type PassportUser = Express.User & { _raw?: unknown, _json?: unknown, source?: unknown };
11
+
12
+ /**
13
+ * Authenticator via passport
14
+ */
15
+ export class PassportAuthenticator<V extends PassportUser = PassportUser> implements Authenticator<object, WebFilterContext> {
16
+
17
+ #strategyName: string;
18
+ #strategy: passport.Strategy;
19
+ #toPrincipal: (user: V, issuer?: string) => SimplePrincipal;
20
+ #passportOptions: (ctx: WebFilterContext) => passport.AuthenticateOptions;
21
+ session = false;
22
+
23
+ /**
24
+ * Creating a new PassportAuthenticator
25
+ *
26
+ * @param strategyName Name of passport strategy
27
+ * @param strategy A passport strategy
28
+ * @param toPrincipal How to convert a user to an identity
29
+ * @param opts Extra passport options
30
+ */
31
+ constructor(
32
+ strategyName: string,
33
+ strategy: passport.Strategy,
34
+ toPrincipal: (user: V) => SimplePrincipal,
35
+ opts: passport.AuthenticateOptions | ((ctx: WebFilterContext) => passport.AuthenticateOptions) = {},
36
+ ) {
37
+ this.#strategyName = strategyName;
38
+ this.#strategy = strategy;
39
+ this.#toPrincipal = toPrincipal;
40
+ this.#passportOptions = typeof opts === 'function' ? opts : ((): Partial<passport.AuthenticateOptions> => opts);
41
+ passport.use(this.#strategyName, this.#strategy);
42
+ }
43
+
44
+ /**
45
+ * Extract the passport auth context
46
+ */
47
+ getState(context?: WebFilterContext | undefined): AuthenticatorState | undefined {
48
+ return context ? PassportUtil.readState<AuthenticatorState>(context.request) : undefined;
49
+ }
50
+
51
+ /**
52
+ * Authenticate a request given passport config
53
+ * @param ctx The travetto filter context
54
+ */
55
+ async authenticate(input: object, ctx: WebFilterContext): Promise<Principal | undefined> {
56
+ const requestOptions = this.#passportOptions(ctx);
57
+ const options = {
58
+ session: this.session,
59
+ failWithError: true,
60
+ ...requestOptions,
61
+ state: PassportUtil.enhanceState(ctx, requestOptions.state)
62
+ };
63
+
64
+ const user = await WebConnectUtil.invoke<V>(ctx, (req, res, next) =>
65
+ passport.authenticate(this.#strategyName, options, next)(req, res)
66
+ );
67
+
68
+ if (user) {
69
+ delete user._raw;
70
+ delete user._json;
71
+ delete user.source;
72
+ return this.#toPrincipal(user, this.#strategyName);
73
+ }
74
+ }
75
+ }
package/src/util.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { WebFilterContext, WebRequest } from '@travetto/web';
2
+ import { castTo, Util } from '@travetto/runtime';
3
+
4
+ /**
5
+ * Passport utilities
6
+ */
7
+ export class PassportUtil {
8
+
9
+ /**
10
+ * Read passport state string as bas64 encoded JSON value
11
+ * @param src The input src for a state read (string, or a request obj)
12
+ */
13
+ static readState<T = Record<string, unknown>>(src?: string | WebRequest): T | undefined {
14
+ const state = (typeof src === 'string' ? src :
15
+ (typeof src?.context.httpQuery?.state === 'string' ?
16
+ src?.context.httpQuery?.state : ''));
17
+ if (state) {
18
+ try {
19
+ return Util.decodeSafeJSON(state);
20
+ } catch { }
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Write state value from plain object
26
+ * @param state
27
+ * @returns base64 encoded state value, if state is provided
28
+ */
29
+ static writeState(state?: Record<string, unknown>): string | undefined {
30
+ if (state) {
31
+ return Util.encodeSafeJSON(state);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Add to a given state value
37
+ * @param state The new state data to inject
38
+ * @param currentState The optional, current state/request
39
+ * @param key Optional location to nest new state data
40
+ * @returns
41
+ */
42
+ static addToState(state: string | Record<string, unknown>, current?: string | WebRequest, key?: string): string {
43
+ const pre = this.readState(current) ?? {};
44
+ const toAdd = typeof state === 'string' ? JSON.parse(state) : state;
45
+ const base: Record<string, unknown> = key ? castTo(pre[key] ??= {}) : pre;
46
+ for (const k of Object.keys(toAdd)) {
47
+ if (k === '__proto__' || k === 'constructor' || k === 'prototype') {
48
+ continue;
49
+ }
50
+ base[k] = toAdd[k];
51
+ }
52
+ return this.writeState(pre)!;
53
+ }
54
+
55
+ /**
56
+ * Enhance passport state with additional information information
57
+ * @param ctx The travetto filter context
58
+ * @param currentState The current state, if any
59
+ */
60
+ static enhanceState({ request }: WebFilterContext, currentState?: string): string {
61
+ return this.addToState({ referrer: request.headers.get('Referer') }, currentState);
62
+ }
63
+ }