Skip to content

Commit 124c078

Browse files
jannyHouemonddr
authored andcommitted
feat: design auth system with user scenario
Co-authored-by: emonddr <dremond@ca.ibm.com>
1 parent 107a45a commit 124c078

21 files changed

+1843
-1333
lines changed

package-lock.json

Lines changed: 1333 additions & 1333 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/authentication/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ component, it is not production ready.**
1010
The component demonstrates how to leverage Passport module and extension points
1111
provided by LoopBack 4 to implement an authentication layer.
1212

13+
To handle multiple authentication strategies without using the Passport module,
14+
please read
15+
[Multiple Authentication strategies](./packages/authentication/docs/authentication-system.md).
16+
1317
## Installation
1418

1519
```shell
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
### Auth action
2+
3+
```ts
4+
import * as HttpErrors from 'http-errors';
5+
6+
async action(request: Request): Promise<UserProfile | undefined> {
7+
const authStrategy = await this.getAuthStrategy();
8+
if (!authStrategy) {
9+
// The invoked operation does not require authentication.
10+
return undefined;
11+
}
12+
13+
try {
14+
const userProfile: UserProfile = await authStrategy.authenticate(request);
15+
this.setCurrentUser(userProfile);
16+
// a convenient return for the next request handlers
17+
return userProfile;
18+
} catch (err) {
19+
// interpret the raw error code/msg here and throw the corresponding HTTP error
20+
// convert it to http error
21+
if (err.code == '401') {
22+
throw new HttpErrors.Unauthorized(err.message);
23+
}
24+
}
25+
}
26+
```
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
### Auth strategy interface
2+
3+
```ts
4+
import {Request} from '@loopback/rest';
5+
6+
interface AuthenticationStrategy {
7+
// The resolver will read the `options` object from metadata, then invoke the
8+
// `authenticate` with `options` if it exists.
9+
authenticate(
10+
request: Request,
11+
options: object,
12+
): Promise<UserProfile | undefined>;
13+
14+
// This is a private function that extracts credential fields from a request,
15+
// it is called in function `authenticate`. You could organize the extraction
16+
// logic in this function or write them in `authenticate` directly without defining
17+
// this extra utility.
18+
private extractCredentials?(request: Request): Promise<Credentials>;
19+
}
20+
```
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
## Multiple Authentication strategies
2+
3+
An authentication system in a LoopBack 4 application could potentially support
4+
multiple popular strategies, including basic auth, oauth2, saml, openid-connect,
5+
etc...And also allow programmers to use either a token based or a session based
6+
approach to track the logged-in user.
7+
8+
The diagram below illustrates the high level abstraction of such an extensible
9+
authentication system.
10+
11+
<img src="./imgs/multiple-auth-strategies-login.png" width="1000px" />
12+
13+
Assume the app has a static login page with a list of available choices for
14+
users to login:
15+
16+
- local: basic auth with email/username + password
17+
- facebook account: oauth2
18+
- google account: oauth2
19+
- ibm intranet account: saml
20+
- openid account: openid-connect
21+
- ...
22+
23+
For the local login, we retrieve the user from a local database.
24+
25+
For the third-party service login, e.g. facebook account login, we retrieve the
26+
user info from the facebook authorization server using oauth2, then find or
27+
create the user in the local database.
28+
29+
By clicking any one of the links, you login with a particular account and your
30+
status will be tracked in a session(with session-based auth), or your profile
31+
will be encoded into a JWT token(with token-based auth).
32+
33+
A common flow for all the login strategies would be: the authentication action
34+
verifies the credentials and returns the raw information of that logged-in user.
35+
36+
Here the raw information refers to the data returned from a third-party service
37+
or a persistent database. Therefore you need another step to convert it to a
38+
user profile instance which describes your application's user model. Finally the
39+
user profile is either tracked by a generated token OR a session + cookie.
40+
41+
The next diagram illustrates the flow of verifying the client requests sent
42+
after the user has logged in.
43+
44+
<img src="./imgs/multiple-auth-strategies-verify.png" width="1000px" />
45+
46+
The request goes through the authentication action which invokes the
47+
authentication strategy to decode/deserialize the user profile from the
48+
token/session, binds it to the request context so that actions after
49+
'authenticate' could inject it using DI.
50+
51+
Next let's walk through the typical API flow of user login and user
52+
verification.
53+
54+
## API Flows (using BasicAuth + JWT as example)
55+
56+
Other than the LoopBack core and its authentication module, there are different
57+
parts included and integrated together to perform the authentication.
58+
59+
The next diagram, using the BasicAuth + JWT authentication strategy as an
60+
example, draws two API flows:
61+
62+
- Login: user login with email+password
63+
- Verify: verify the logged-in user
64+
65+
along with the responsibilities divided among different parts:
66+
67+
- LoopBack core: resolve a strategy based on the endpoint's corresponding
68+
authentication metadata, execute the authentication action which invokes the
69+
strategy's `authenticate` method.
70+
71+
- Authentication strategy:
72+
73+
- (login flow) verify user credentials and return a user profile(it's up to
74+
the programmer to create the JWT access token inside the controller
75+
function).
76+
- (verify flow) verify the token and decode user profile from it.
77+
78+
- Authentication services: some utility services that can be injected in the
79+
strategy class. (Each service's functionalities will be covered in the next
80+
section)
81+
82+
_Note: FixIt! the step 6 in the following diagram should be moved to LoopBack
83+
side_
84+
85+
<img src="./imgs/API-flow-(JWT).png" width="1000px" />
86+
87+
_Note: Another section for session based auth TBD_
88+
89+
## Authentication framework architecture
90+
91+
The following diagram describes the architecture of the entire authentication
92+
framework and the detailed responsibility of each part.
93+
94+
You can check the pseudo code in folder `docs` for:
95+
96+
- [authentication-action](./authentication-action.md)
97+
- [authentication-strategy](./authentication-strategy.md)
98+
- [basic auth strategy](./strategies/basic-auth.md)
99+
- [jwt strategy](./strategies/jwt.md)
100+
- [oauth2 strategy](./strategies/oauth2.md)
101+
- [endpoints defined in controller](./controller-functions.md)
102+
103+
And the abstractions for:
104+
105+
- [user service](../src/services/user.service.ts)
106+
- [token service](../src/services/token.service.ts)
107+
108+
<img src="./imgs/auth-framework-architecture.png" width="1000px" />
109+
110+
### Token based authentication
111+
112+
- Login flow
113+
114+
- authentication action:
115+
- resolve metadata to get the strategy
116+
- invoke strategy.authenticate()
117+
- set the current user as the return of strategy.authenticate()
118+
- strategy:
119+
- extract credentials from
120+
- transport layer(request)
121+
- or local configuration file
122+
- verify credentials and return the user profile (call user service)
123+
- controller function:
124+
- generate token (call token service)
125+
- return token or serialize it into the response
126+
127+
- Verify flow
128+
- authentication action:
129+
- resolve metadata to get the strategy
130+
- invoke strategy.authenticate()
131+
- set the current user as the return of strategy.authenticate()
132+
- strategy:
133+
- extract access token from transport layer(request)
134+
- verify access token(call token service)
135+
- decode user from access token(call token service)
136+
- return user
137+
- controller:
138+
- process the injected user
139+
140+
### Session based authentication
141+
142+
- Login flow
143+
144+
- authentication action:
145+
- resolve metadata to get the strategy
146+
- invoke strategy.authenticate()
147+
- strategy:
148+
- extract credentials from
149+
- transport layer (request)
150+
- or local configuration file
151+
- verify credentials (call user service) and return the user profile
152+
- controller:
153+
- serialize user info into the session
154+
155+
- Verify flow
156+
- authentication action:
157+
- resolve metadata to get the strategy
158+
- invoke strategy.authenticate()
159+
- set the current user as the return of strategy.authenticate()
160+
- strategy:
161+
- extract session info from cookie(call session service)
162+
- deserialize user info from session(call session service)
163+
- return user
164+
- controller function:
165+
- process the injected user
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
## Endpoint definitions
2+
3+
The following decorated controller functions demos the endpoints described at
4+
the beginning of markdown file
5+
[authentication-system](./authentication-system.md).
6+
7+
Please note how they are decorated with `@authenticate()`, the syntax is:
8+
`@authenticate(strategy_name, options)`
9+
10+
- /login
11+
12+
```ts
13+
class LoginController {
14+
@post('/login', APISpec)
15+
login() {
16+
// static route
17+
}
18+
}
19+
```
20+
21+
- /loginWithLocal
22+
23+
```ts
24+
25+
const RESPONSE_SPEC_FOR_JWT_LOGIN = {
26+
responses: {
27+
'200': {
28+
description: 'Token',
29+
content: {
30+
'application/json': {
31+
schema: {
32+
type: 'object',
33+
properties: {
34+
token: {
35+
type: 'string',
36+
},
37+
},
38+
},
39+
},
40+
},
41+
},
42+
},
43+
};
44+
45+
class LoginController{
46+
constructor(
47+
@inject(AuthenticationBindings.CURRENT_USER) userProfile: UserProfile,
48+
@inject(AuthenticationBindings.SERVICES.JWT_TOKEN) JWTtokenService: TokenService,
49+
) {}
50+
51+
// I was about to create a local login example, while if the credentials are
52+
// provided in the request body, all the authenticate logic will happen in the
53+
// controller, the auth action isn't even involved.
54+
// See the login endpoint in shopping example
55+
// https://github.com/strongloop/loopback4-example-shopping/blob/master/src/controllers/user.controller.ts#L137
56+
57+
// Describe the response using OpenAPI spec
58+
@post('/loginOAI/basicAuth', RESPONSE_SPEC_FOR_JWT_LOGIN)
59+
@authenticate('basicAuth')
60+
basicAuthLoginReturningJWTToken() {
61+
await token = JWTtokenService.generateToken(this.userProfile);
62+
// Action `send` will serialize token into response according to the OpenAPI spec.
63+
return token;
64+
}
65+
66+
// OR
67+
// Serialize the token into response in the controller directly without describing it
68+
// with OpenAPI spec
69+
@post('/loginWithoutOAI/basicAuth')
70+
@authenticate('basicAuth')
71+
basicAuthLoginReturningJWTToken() {
72+
await token = JWTtokenService.generateToken(this.userProfile);
73+
// It's on users to serialize the token into the response.
74+
await writeTokenToResponse();
75+
}
76+
}
77+
```
78+
79+
```ts
80+
class UserOrdersController {
81+
@get('Users/me/orders', ...APISpec)
82+
@authenticate('jwt')
83+
getOrders() {
84+
// The `userProfile` is set in the authentication action
85+
// and get injected in the controller constructor
86+
const id = this.userProfile.id;
87+
await this.userRepo(id).orders();
88+
}
89+
}
90+
```
91+
92+
Other auth strategies like oauth2 will be determined in another story.
93+
94+
- /loginWithFB
95+
96+
```ts
97+
class UserController {
98+
@post('/loginWithFB', APISpec)
99+
@authenticate('oath2.fb', {session: false})
100+
loginWithFB() {}
101+
}
102+
```
103+
104+
- /loginWithGoogle
105+
106+
```ts
107+
class UserController {
108+
@post('/loginWithGoogle', APISpec)
109+
@authenticate('oath2.google', {session: true})
110+
loginWithGoogle() {}
111+
}
112+
```
86.3 KB
Loading
142 KB
Loading
241 KB
Loading
69 KB
Loading

0 commit comments

Comments
 (0)