@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 +130 -0
- package/__index__.ts +2 -0
- package/package.json +48 -0
- package/src/authenticator.ts +75 -0
- package/src/util.ts +63 -0
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
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
|
+
}
|