Skip to content

Commit cd80523

Browse files
authored
feat(provider): new strategy for auth0 (#245)
* feat(provider): new strategy for auth0 passport based strategy GH-244 * feat(provider): new auth0 strategy sonar fix GH-244
1 parent f0acbf6 commit cd80523

File tree

15 files changed

+895
-13
lines changed

15 files changed

+895
-13
lines changed

README.md

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ It provides support for seven passport based strategies.
4444
10. [passport-cognito-oauth2](https://github.com/ebuychance/passport-cognito-oauth2) - Passport strategy for authenticating with Cognito using the Cognito OAuth 2.0 API. This module lets you authenticate using Cognito in your Node.js applications.
4545
11. [passport-SAML](https://github.com/node-saml/passport-saml) - Passport strategy for authenticating with SAML using the SAML 2.0 API. This module lets you authenticate using SAML in your Node.js applications
4646
12. custom-passport-otp - Created a Custom Passport strategy for 2-Factor-Authentication using OTP (One Time Password).
47+
13. [passport-auth0](https://github.com/auth0/passport-auth0) - Passport strategy for authenticating with auth0. This module lets you authenticate using [Auth0](https://auth0.com/) in your Node.js applications.
4748

4849
You can use one or more strategies of the above in your application. For each of the strategy (only which you use), you just need to provide your own verifier function, making it easily configurable. Rest of the strategy implementation intricacies is handled by extension.
4950

@@ -2977,6 +2978,346 @@ this.component(AuthenticationComponent);
29772978
This binding needs to be done before adding the Authentication component to your application.
29782979
Apart from this all other steps for authentication for all strategies remain the same.
29792980

2981+
### Passport Auth0
2982+
2983+
In order to use it, run `npm install passport-auth0` and `npm install @types/passport-auth0`.
2984+
First, create a AuthUser model implementing the IAuthUser interface. You can implement the interface in the user model itself. See sample below.
2985+
2986+
```ts
2987+
@model({
2988+
name: 'users',
2989+
})
2990+
export class User extends Entity implements IAuthUser {
2991+
@property({
2992+
type: 'number',
2993+
id: true,
2994+
})
2995+
id?: number;
2996+
2997+
@property({
2998+
type: 'string',
2999+
required: true,
3000+
name: 'first_name',
3001+
})
3002+
firstName: string;
3003+
3004+
@property({
3005+
type: 'string',
3006+
name: 'last_name',
3007+
})
3008+
lastName: string;
3009+
3010+
@property({
3011+
type: 'string',
3012+
name: 'middle_name',
3013+
})
3014+
middleName?: string;
3015+
3016+
@property({
3017+
type: 'string',
3018+
required: true,
3019+
})
3020+
username: string;
3021+
3022+
@property({
3023+
type: 'string',
3024+
})
3025+
email?: string;
3026+
3027+
// Auth provider - 'auth0'
3028+
@property({
3029+
type: 'string',
3030+
required: true,
3031+
name: 'auth_provider',
3032+
})
3033+
authProvider: string;
3034+
3035+
// Id from external provider
3036+
@property({
3037+
type: 'string',
3038+
name: 'auth_id',
3039+
})
3040+
authId?: string;
3041+
3042+
@property({
3043+
type: 'string',
3044+
name: 'auth_token',
3045+
})
3046+
authToken?: string;
3047+
3048+
@property({
3049+
type: 'string',
3050+
})
3051+
password?: string;
3052+
3053+
constructor(data?: Partial<User>) {
3054+
super(data);
3055+
}
3056+
}
3057+
```
3058+
3059+
Now bind this model to USER_MODEL key in application.ts
3060+
3061+
```ts
3062+
this.bind(AuthenticationBindings.USER_MODEL).to(User);
3063+
```
3064+
3065+
Create CRUD repository for the above model. Use loopback CLI.
3066+
3067+
```sh
3068+
lb4 repository
3069+
```
3070+
3071+
Add the verifier function for the strategy. You need to create a provider for the same. You can add your application specific business logic for client auth here. Here is a simple example.
3072+
3073+
```ts
3074+
import {Provider, inject} from '@loopback/context';
3075+
import {repository} from '@loopback/repository';
3076+
import {HttpErrors} from '@loopback/rest';
3077+
import {
3078+
AuthErrorKeys,
3079+
IAuthUser,
3080+
VerifyFunction,
3081+
} from 'loopback4-authentication';
3082+
import * as Auth0Strategy from 'passport-auth0';
3083+
import {
3084+
Auth0PostVerifyFn,
3085+
Auth0PreVerifyFn,
3086+
Auth0SignUpFn,
3087+
} from '../../../providers';
3088+
import {SignUpBindings, VerifyBindings} from '../../../providers/keys';
3089+
import {UserCredentialsRepository, UserRepository} from '../../../repositories';
3090+
import {AuthUser} from '../models';
3091+
3092+
export class Auth0VerifyProvider implements Provider<VerifyFunction.Auth0Fn> {
3093+
constructor(
3094+
@repository(UserRepository)
3095+
public userRepository: UserRepository,
3096+
@repository(UserCredentialsRepository)
3097+
public userCredsRepository: UserCredentialsRepository,
3098+
) {}
3099+
3100+
value(): VerifyFunction.Auth0Fn {
3101+
return async (
3102+
accessToken: string,
3103+
refreshToken: string,
3104+
profile: Auth0Strategy.Profile,
3105+
) => {
3106+
let user: IAuthUser | null = await this.userRepository.findOne({
3107+
where: {
3108+
email: profile._json.email,
3109+
},
3110+
});
3111+
3112+
if (!user) {
3113+
throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials);
3114+
}
3115+
}
3116+
const creds = await this.userCredsRepository.findOne({
3117+
where: {
3118+
userId: user.id as string,
3119+
},
3120+
});
3121+
if (
3122+
!user ||
3123+
user.authProvider !== 'auth0' ||
3124+
user.authId !== profile.id
3125+
) {
3126+
throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials);
3127+
}
3128+
3129+
const authUser: AuthUser = new AuthUser(user);
3130+
authUser.permissions = [];
3131+
authUser.externalAuthToken = accessToken;
3132+
authUser.externalRefreshToken = refreshToken;
3133+
return this.postVerifyProvider(profile, authUser);
3134+
};
3135+
}
3136+
}
3137+
```
3138+
3139+
Please note the Verify function type _VerifyFunction.LocalPasswordFn_
3140+
3141+
Now bind this provider to the application in application.ts.
3142+
3143+
```ts
3144+
import {AuthenticationComponent, Strategies} from 'loopback4-authentication';
3145+
```
3146+
3147+
```ts
3148+
// Add authentication component
3149+
this.component(AuthenticationComponent);
3150+
// Customize authentication verify handlers
3151+
this.bind(Strategies.Passport.AUTH0_VERIFIER).toProvider(Auth0VerifyProvider);
3152+
```
3153+
3154+
Now, bind this provider to the application in application.ts.
3155+
3156+
```ts
3157+
import {Auth0StrategyFactoryProvider} from 'loopback4-authentication/passport-auth0';
3158+
```
3159+
3160+
```ts
3161+
this.bind(Strategies.Passport.AUTH0_STRATEGY_FACTORY).toProvider(
3162+
Auth0StrategyFactoryProvider,
3163+
);
3164+
```
3165+
3166+
Finally, add the authenticate function as a sequence action to sequence.ts.
3167+
3168+
```ts
3169+
export class MySequence implements SequenceHandler {
3170+
constructor(
3171+
@inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
3172+
@inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
3173+
@inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
3174+
@inject(SequenceActions.SEND) public send: Send,
3175+
@inject(SequenceActions.REJECT) public reject: Reject,
3176+
@inject(AuthenticationBindings.USER_AUTH_ACTION)
3177+
protected authenticateRequest: AuthenticateFn<AuthUser>,
3178+
) {}
3179+
3180+
async handle(context: RequestContext) {
3181+
try {
3182+
const {request, response} = context;
3183+
3184+
const route = this.findRoute(request);
3185+
const args = await this.parseParams(request, route);
3186+
request.body = args[args.length - 1];
3187+
const authUser: AuthUser = await this.authenticateRequest(
3188+
request,
3189+
response,
3190+
);
3191+
const result = await this.invoke(route, args);
3192+
this.send(response, result);
3193+
} catch (err) {
3194+
this.reject(context, err);
3195+
}
3196+
}
3197+
}
3198+
```
3199+
3200+
After this, you can use decorator to apply auth to controller functions wherever needed. See below.
3201+
3202+
```ts
3203+
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
3204+
@authenticate(
3205+
STRATEGY.AUTH0,
3206+
{
3207+
domain: process.env.AUTH0_DOMAIN,
3208+
clientID: process.env.AUTH0_CLIENT_ID,
3209+
clientSecret: process.env.AUTH0_CLIENT_SECRET,
3210+
callbackURL: process.env.AUTH0_CALLBACK_URL,
3211+
state: false,
3212+
profileFields: ['email', 'name'],
3213+
scope: 'openid email profile',
3214+
},
3215+
(req: Request) => {
3216+
return {
3217+
accessType: 'offline',
3218+
state: Object.keys(req.query)
3219+
.map(key => key + '=' + req.query[key])
3220+
.join('&'),
3221+
};
3222+
},
3223+
)
3224+
@authorize(['*'])
3225+
@get('/auth/auth0', {
3226+
responses: {
3227+
[STATUS_CODE.OK]: {
3228+
description: 'Token Response',
3229+
content: {
3230+
[CONTENT_TYPE.JSON]: {
3231+
schema: {'x-ts-type': TokenResponse},
3232+
},
3233+
},
3234+
},
3235+
},
3236+
})
3237+
async loginViaAuth0(
3238+
@param.query.string('client_id')
3239+
clientId?: string,
3240+
@param.query.string('client_secret')
3241+
clientSecret?: string,
3242+
): Promise<void> {}
3243+
3244+
@authenticate(
3245+
STRATEGY.AUTH0,
3246+
{
3247+
domain: process.env.AUTH0_DOMAIN,
3248+
clientID: process.env.AUTH0_CLIENT_ID,
3249+
clientSecret: process.env.AUTH0_CLIENT_SECRET,
3250+
callbackURL: process.env.AUTH0_CALLBACK_URL,
3251+
state: false,
3252+
profileFields: ['email', 'name'],
3253+
scope: 'openid email profile',
3254+
}
3255+
(req: Request) => {
3256+
return {
3257+
accessType: 'offline',
3258+
state: Object.keys(req.query)
3259+
.map(key => `${key}=${req.query[key]}`)
3260+
.join('&'),
3261+
};
3262+
},
3263+
)
3264+
@authorize(['*'])
3265+
@get('/auth/auth0-auth-redirect', {
3266+
responses: {
3267+
[STATUS_CODE.OK]: {
3268+
description: 'Token Response',
3269+
content: {
3270+
[CONTENT_TYPE.JSON]: {
3271+
schema: {'x-ts-type': TokenResponse},
3272+
},
3273+
},
3274+
},
3275+
},
3276+
})
3277+
async callback(
3278+
@param.query.string('code') code: string,
3279+
@param.query.string('state') state: string,
3280+
@inject(RestBindings.Http.RESPONSE) response: Response,
3281+
): Promise<void> {
3282+
const clientId = new URLSearchParams(state).get('client_id');
3283+
if (!clientId || !this.user) {
3284+
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
3285+
}
3286+
const client = await this.authClientRepository.findOne({
3287+
where: {
3288+
clientId: clientId,
3289+
},
3290+
});
3291+
if (!client || !client.redirectUrl) {
3292+
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
3293+
}
3294+
try {
3295+
const codePayload: ClientAuthCode<User> = {
3296+
clientId,
3297+
user: this.user,
3298+
};
3299+
const token = jwt.sign(codePayload, client.secret, {
3300+
expiresIn: client.authCodeExpiration,
3301+
audience: clientId,
3302+
subject: this.user.username,
3303+
issuer: process.env.JWT_ISSUER,
3304+
});
3305+
response.redirect(`${client.redirectUrl}?code=${token}`);
3306+
} catch (error) {
3307+
throw new HttpErrors.InternalServerError(AuthErrorKeys.UnknownError);
3308+
}
3309+
}
3310+
```
3311+
3312+
Please note above that we are creating two new APIs for auth0 authentication. The first one is for UI clients to hit. We are authenticating client as well, then passing the details to the auth0. Then, the actual authentication is done by auth0 authorization url, which redirects to the second API we created after success. The first API method body is empty as we do not need to handle its response. The provider in this package will do the redirection for you automatically.
3313+
3314+
For accessing the authenticated AuthUser model reference, you can inject the CURRENT_USER provider, provided by the extension, which is populated by the auth action sequence above.
3315+
3316+
```ts
3317+
@inject.getter(AuthenticationBindings.CURRENT_USER)
3318+
private readonly getCurrentUser: Getter<User>,
3319+
```
3320+
29803321
## Feedback
29813322

29823323
If you've noticed a bug or have a question or have a feature request, [search the issue tracker](https://github.com/sourcefuse/loopback4-authentication/issues) to see if someone else in the community has already created a ticket.

0 commit comments

Comments
 (0)