Skip to content

Commit 4ee213c

Browse files
committed
added authentication and permissions
1 parent 67f8293 commit 4ee213c

File tree

16 files changed

+478
-127
lines changed

16 files changed

+478
-127
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.vscode
12
# Logs
23
logs
34
*.log

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
# api
22

3+
git clone
4+
npm install
5+
update db details in src/datasources/mysql.datasource.config.json
6+
npm run build
7+
num run migrate
8+
39
[![LoopBack](https://github.com/strongloop/loopback-next/raw/master/docs/site/imgs/branding/Powered-by-LoopBack-Badge-(blue)-@2x.png)](http://loopback.io/)
410
# loopback4-authentication-jwt-roles

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@types/bcryptjs": "^2.4.2",
5959
"bcryptjs": "^2.4.3",
6060
"isemail": "^3.2.0",
61+
"jsonwebtoken": "^8.5.1",
6162
"loopback-connector-mysql": "^5.4.2"
6263
},
6364
"devDependencies": {

src/application.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,33 @@
1-
import {BootMixin} from '@loopback/boot';
2-
import {ApplicationConfig} from '@loopback/core';
1+
import { BootMixin } from '@loopback/boot';
2+
import { ApplicationConfig } from '@loopback/core';
33
import {
44
RestExplorerBindings,
55
RestExplorerComponent,
66
} from '@loopback/rest-explorer';
7-
import {RepositoryMixin} from '@loopback/repository';
8-
import {RestApplication} from '@loopback/rest';
9-
import {ServiceMixin} from '@loopback/service-proxy';
7+
import { RepositoryMixin } from '@loopback/repository';
8+
import { RestApplication } from '@loopback/rest';
9+
import { ServiceMixin } from '@loopback/service-proxy';
1010
import path from 'path';
11-
import {MySequence} from './sequence';
11+
import { MySequence } from './sequence';
12+
import { BcryptHasher } from './services/hash.password.bcrypt';
13+
import { MyUserService } from './services/user.service';
14+
import { JWTService } from './services/jwt-service';
15+
import { TokenServiceConstants, TokenServiceBindings, UserServiceBindings, PasswordHasherBindings } from './keys';
16+
import { AuthenticationComponent, registerAuthenticationStrategy } from "@loopback/authentication";
17+
import { JWTStrategy } from "./authentication-stategies/jwt-strategy"
1218

1319
export class ApiApplication extends BootMixin(
1420
ServiceMixin(RepositoryMixin(RestApplication)),
1521
) {
1622
constructor(options: ApplicationConfig = {}) {
1723
super(options);
1824

25+
// set up bindings
26+
this.setupBinding();
27+
28+
this.component(AuthenticationComponent);
29+
registerAuthenticationStrategy(this, JWTStrategy);
30+
1931
// Set up the custom sequence
2032
this.sequence(MySequence);
2133

@@ -39,4 +51,18 @@ export class ApiApplication extends BootMixin(
3951
},
4052
};
4153
}
54+
55+
setupBinding(): void {
56+
this.bind(PasswordHasherBindings.PASSWORD_HASHER).toClass(BcryptHasher);
57+
this.bind(PasswordHasherBindings.ROUNDS).to(10);
58+
this.bind(UserServiceBindings.USER_SERVICE).toClass(MyUserService);
59+
this.bind(TokenServiceBindings.TOKEN_SERVICE).toClass(JWTService);
60+
this.bind(TokenServiceBindings.TOKEN_SECRET).to(
61+
TokenServiceConstants.TOKEN_SECRET_VALUE,
62+
);
63+
this.bind(TokenServiceBindings.TOKEN_EXPIRES_IN).to(
64+
TokenServiceConstants.TOKEN_EXPIRES_IN_VALUE,
65+
);
66+
67+
}
4268
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {AuthenticationStrategy} from '@loopback/authentication';
2+
import {Request, HttpErrors} from '@loopback/rest';
3+
import {UserProfile} from '@loopback/security';
4+
import {inject} from '@loopback/core';
5+
import {TokenServiceBindings} from '../keys';
6+
import {JWTService} from '../services/jwt-service';
7+
8+
export class JWTStrategy implements AuthenticationStrategy {
9+
constructor(
10+
@inject(TokenServiceBindings.TOKEN_SERVICE)
11+
public jwtService: JWTService,
12+
) {}
13+
name: string = 'jwt';
14+
async authenticate(request: Request): Promise<UserProfile | undefined> {
15+
const token: string = this.extractCredentials(request);
16+
const userProfile = await this.jwtService.verifyToken(token);
17+
return Promise.resolve(userProfile);
18+
}
19+
extractCredentials(request: Request): string {
20+
if (!request.headers.authorization) {
21+
throw new HttpErrors.Unauthorized('Authorization header is missing');
22+
}
23+
24+
const authHeaderValue = request.headers.authorization;
25+
26+
// authorization : Bearer xxxc..yyy..zzz
27+
28+
if (!authHeaderValue.startsWith('Bearer')) {
29+
throw new HttpErrors.Unauthorized(`
30+
Authorization header is not type of 'Bearer'.
31+
`);
32+
}
33+
const parts = authHeaderValue.split(' ');
34+
if (parts.length !== 2) {
35+
throw new HttpErrors.Unauthorized(`
36+
Authorization header has too many parts it must follow this pattern 'Bearer xx.yy.zz' where xx.yy.zz should be valid token
37+
`);
38+
}
39+
const token = parts[1];
40+
return token;
41+
}
42+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const enum PermissionKeys {
2+
// Admin permissions
3+
BlogManagement = 'BlogManagement',
4+
UserManagement = 'UserManagement',
5+
6+
// User permissions
7+
GetBlogs = 'GetBlogs',
8+
AuthFeatures = 'AuthFeatures',
9+
UserBasic = 'UserBasic'
10+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { post, requestBody, getJsonSchemaRef, getModelSchemaRef } from "@loopback/rest";
2+
import { User } from "../models";
3+
import { validateCredentials } from "../services/validator";
4+
import { PasswordHasherBindings, UserServiceBindings } from "../keys";
5+
import { BcryptHasher } from "../services/hash.password.bcrypt";
6+
import { inject } from "@loopback/core";
7+
import { repository } from "@loopback/repository";
8+
import { UserRepository } from "../repositories";
9+
import * as _ from 'lodash';
10+
import { PermissionKeys } from "../authorization/permission-keys";
11+
12+
// Uncomment these imports to begin using these cool features!
13+
14+
// import {inject} from '@loopback/context';
15+
16+
17+
export class AdminController {
18+
constructor(
19+
@inject(PasswordHasherBindings.PASSWORD_HASHER)
20+
public hasher: BcryptHasher,
21+
@repository(UserRepository)
22+
public userRepository: UserRepository,
23+
) { }
24+
25+
@post('/admin', {
26+
responses: {
27+
'200': {
28+
description: 'Admin',
29+
content: {
30+
schema: getJsonSchemaRef(User),
31+
},
32+
},
33+
},
34+
})
35+
async create(@requestBody({
36+
content: {
37+
'application/json': {
38+
schema: getModelSchemaRef(User, {
39+
title: 'NewUser',
40+
exclude: ['id', 'permissions', 'additionalProp1'],
41+
}),
42+
},
43+
},
44+
})
45+
admin: User) {
46+
validateCredentials(_.pick(admin, ['email', 'password']));
47+
admin.permissions = [
48+
PermissionKeys.BlogManagement,
49+
PermissionKeys.UserManagement
50+
]
51+
admin.password = await this.hasher.hashPassword(admin.password);
52+
const newAdmin = await this.userRepository.create(admin);
53+
delete newAdmin.password;
54+
55+
return newAdmin;
56+
}
57+
58+
}

0 commit comments

Comments
 (0)