Skip to content

Commit e1bba2c

Browse files
prernagpprernagp90Tyagi-Sunny
authored
fix(authentication-service): added idp server controller for login and discovery endpoint (#2131)
* fix(authentication-service): added idp server controller for login and discovery endpoint BREAKING CHANGE: * feat(authentication-service): added the logic for rotation of keys with database 2034 * feat(authentication-service): added final changes for idp server MIGRATION CHANGE: migration-20241105074844- BREAKING CHANGE: JWT Asymmetric Signer and Verifier will be served from Database only. File support has been removed. 2034 --------- Co-authored-by: prernagp <prernagp90@gmail.com> Co-authored-by: Tyagi-Sunny <sunny.tyagi@sourcefuse.com>
1 parent 5371691 commit e1bba2c

File tree

118 files changed

+4201
-725
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

118 files changed

+4201
-725
lines changed

package-lock.json

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

packages/core/locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,7 @@
4242
"API not found !": "API not found !",
4343
"User Task not found": "User Task not found",
4444
"Task completion cannot be done through the PATCH API.": "Task completion cannot be done through the PATCH API.",
45-
"Unauthorized": "Unauthorized"
45+
"Unauthorized": "Unauthorized",
46+
"No keys found": "No keys found",
47+
"[{\"keyword\"": "\"type\",\"dataPath\":\".valueB\",\"schemaPath\":\"#/properties/valueB/type\",\"params\":{\"type\":\"string\"},\"message\":\"should be string\"}]"
4648
}

packages/core/src/components/bearer-verifier/component.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
import {Binding, Component, inject, ProviderMap} from '@loopback/core';
66
import {Class, Model, Repository} from '@loopback/repository';
77
import {Strategies} from 'loopback4-authentication';
8+
import {JwtKeysRepository} from '../../repositories';
89
import {ILogger, LOGGER} from '../logger-extension';
9-
1010
import {
1111
BearerVerifierBindings,
1212
BearerVerifierConfig,
1313
BearerVerifierType,
1414
} from './keys';
15-
import {RevokedToken} from './models';
15+
import {JwtKeys, RevokedToken} from './models';
1616
import {FacadesBearerAsymmetricTokenVerifyProvider} from './providers/facades-bearer-asym-token-verify.provider';
1717
import {FacadesBearerTokenVerifyProvider} from './providers/facades-bearer-token-verify.provider';
1818
import {ServicesBearerAsymmetricTokenVerifyProvider} from './providers/services-bearer-asym-token-verifier';
@@ -26,9 +26,9 @@ export class BearerVerifierComponent implements Component {
2626
@inject(LOGGER.LOGGER_INJECT) public logger: ILogger,
2727
) {
2828
this.providers = {};
29-
this.repositories = [RevokedTokenRepository];
3029

31-
this.models = [RevokedToken];
30+
this.repositories = [RevokedTokenRepository, JwtKeysRepository];
31+
this.models = [RevokedToken, JwtKeys];
3232

3333
if (this.config && this.config.type === BearerVerifierType.service) {
3434
this.providers[Strategies.Passport.BEARER_TOKEN_VERIFIER.key] =

packages/core/src/components/bearer-verifier/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,5 @@
44
// https://opensource.org/licenses/MIT
55
export * from './component';
66
export * from './keys';
7-
export * from './types';
87
export * from './models';
98
export * from './repositories';

packages/core/src/components/bearer-verifier/models/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
//
33
// This software is released under the MIT License.
44
// https://opensource.org/licenses/MIT
5+
export * from './jwt-keys.model';
56
export * from './revoked-token.model';
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {Entity, model, property} from '@loopback/repository';
2+
3+
@model({
4+
name: 'jwt_keys',
5+
})
6+
export class JwtKeys extends Entity {
7+
@property({
8+
type: 'number',
9+
id: true,
10+
})
11+
id?: number;
12+
13+
@property({
14+
type: 'string',
15+
required: true,
16+
name: 'key_id',
17+
})
18+
keyId: string;
19+
20+
@property({
21+
type: 'string',
22+
required: true,
23+
name: 'public_key',
24+
})
25+
publicKey: string;
26+
27+
@property({
28+
type: 'string',
29+
required: true,
30+
name: 'private_key',
31+
})
32+
privateKey: string;
33+
34+
@property({
35+
type: 'date',
36+
default: () => new Date(),
37+
name: 'created_on',
38+
})
39+
createdOn?: Date;
40+
41+
constructor(data?: Partial<JwtKeys>) {
42+
super(data);
43+
}
44+
}

packages/core/src/components/bearer-verifier/providers/facades-bearer-asym-token-verify.provider.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@
55
import {Constructor, inject, Provider} from '@loopback/context';
66
import {repository} from '@loopback/repository';
77
import {HttpErrors, Request} from '@loopback/rest';
8-
import {verify} from 'jsonwebtoken';
8+
import * as jwt from 'jsonwebtoken';
99
import {
10-
VerifyFunction,
1110
AuthenticationBindings,
1211
EntityWithIdentifier,
1312
IAuthUser,
13+
VerifyFunction,
1414
} from 'loopback4-authentication';
1515
import moment from 'moment';
16-
import * as fs from 'fs/promises';
16+
import * as jose from 'node-jose';
17+
import {JwtKeysRepository} from '../../../repositories';
1718
import {ILogger, LOGGER} from '../../logger-extension';
1819
import {IAuthUserWithPermissions} from '../keys';
1920
import {RevokedTokenRepository} from '../repositories';
@@ -25,6 +26,8 @@ export class FacadesBearerAsymmetricTokenVerifyProvider
2526
@repository(RevokedTokenRepository)
2627
public revokedTokenRepository: RevokedTokenRepository,
2728
@inject(LOGGER.LOGGER_INJECT) private readonly logger: ILogger,
29+
@repository(JwtKeysRepository)
30+
public jwtKeysRepo: JwtKeysRepository,
2831
@inject(AuthenticationBindings.USER_MODEL, {optional: true})
2932
public authUserModel?: Constructor<EntityWithIdentifier & IAuthUser>,
3033
) {}
@@ -45,8 +48,30 @@ export class FacadesBearerAsymmetricTokenVerifyProvider
4548

4649
let user: IAuthUserWithPermissions;
4750
try {
48-
const publicKey = await fs.readFile(process.env.JWT_PUBLIC_KEY ?? '');
49-
user = verify(token, publicKey, {
51+
// Get the key that matches the token's kid
52+
const decoded = jwt.decode(token.trim(), {complete: true});
53+
if (!decoded) {
54+
throw new Error('Token is not valid');
55+
}
56+
const kid = decoded?.header.kid;
57+
58+
// Load the JWKS
59+
const key = await this.jwtKeysRepo.findOne({
60+
where: {
61+
keyId: kid,
62+
},
63+
});
64+
65+
if (!key) {
66+
throw new Error('Key not found for verification');
67+
}
68+
69+
// Convert the JWK to PEM format for verification
70+
const jwkKey = await jose.JWK.asKey(key.publicKey, 'pem');
71+
const pem = jwkKey.toPEM(false);
72+
73+
// Verify the token with the retrieved PEM-formatted public key
74+
user = jwt.verify(token, pem, {
5075
issuer: process.env.JWT_ISSUER,
5176
algorithms: ['RS256'],
5277
}) as IAuthUserWithPermissions;

packages/core/src/components/bearer-verifier/providers/services-bearer-asym-token-verifier.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@
33
// This software is released under the MIT License.
44
// https://opensource.org/licenses/MIT
55
import {Constructor, inject, Provider} from '@loopback/context';
6+
import {repository} from '@loopback/repository';
67
import {HttpErrors} from '@loopback/rest';
7-
import {verify} from 'jsonwebtoken';
8+
import * as jwt from 'jsonwebtoken';
89
import {
9-
VerifyFunction,
1010
AuthenticationBindings,
1111
EntityWithIdentifier,
1212
IAuthUser,
13+
VerifyFunction,
1314
} from 'loopback4-authentication';
1415
import moment from 'moment-timezone';
15-
import * as fs from 'fs/promises';
16+
import * as jose from 'node-jose';
17+
import {JwtKeysRepository} from '../../../repositories';
1618
import {ILogger, LOGGER} from '../../logger-extension';
1719
import {IAuthUserWithPermissions} from '../keys';
1820

@@ -21,6 +23,8 @@ export class ServicesBearerAsymmetricTokenVerifyProvider
2123
{
2224
constructor(
2325
@inject(LOGGER.LOGGER_INJECT) public logger: ILogger,
26+
@repository(JwtKeysRepository)
27+
public jwtKeysRepo: JwtKeysRepository,
2428
@inject(AuthenticationBindings.USER_MODEL, {optional: true})
2529
public authUserModel?: Constructor<EntityWithIdentifier & IAuthUser>,
2630
) {}
@@ -30,8 +34,30 @@ export class ServicesBearerAsymmetricTokenVerifyProvider
3034
let user: IAuthUserWithPermissions;
3135

3236
try {
33-
const publicKey = await fs.readFile(process.env.JWT_PUBLIC_KEY ?? '');
34-
user = verify(token, publicKey, {
37+
// Get the key that matches the token's kid
38+
const decoded = jwt.decode(token.trim(), {complete: true});
39+
if (!decoded) {
40+
throw new Error('Token is not valid');
41+
}
42+
const kid = decoded?.header.kid;
43+
44+
// Load the JWKS
45+
const key = await this.jwtKeysRepo.findOne({
46+
where: {
47+
keyId: kid,
48+
},
49+
});
50+
51+
if (!key) {
52+
throw new Error('Key not found for verification');
53+
}
54+
55+
// Convert the JWK to PEM format for verification
56+
const jwkKey = await jose.JWK.asKey(key.publicKey, 'pem');
57+
const pem = jwkKey.toPEM(false);
58+
59+
// Verify the token with the retrieved PEM-formatted public key
60+
user = jwt.verify(token, pem, {
3561
issuer: process.env.JWT_ISSUER,
3662
algorithms: ['RS256'],
3763
}) as IAuthUserWithPermissions;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {inject} from '@loopback/core';
2+
import {DefaultCrudRepository, juggler} from '@loopback/repository';
3+
import {AuthDbSourceName} from '../../../types';
4+
import {JwtKeys} from '../models';
5+
export class JwtKeysRepository extends DefaultCrudRepository<
6+
JwtKeys,
7+
typeof JwtKeys.prototype.id
8+
> {
9+
constructor(
10+
@inject(`datasources.${AuthDbSourceName}`)
11+
dataSource: juggler.DataSource,
12+
) {
13+
super(JwtKeys, dataSource);
14+
}
15+
}

packages/core/src/components/bearer-verifier/repositories/revoked-token.repository.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import {inject} from '@loopback/core';
66
import {DefaultKeyValueRepository, juggler} from '@loopback/repository';
77

8+
import {AuthCacheSourceName} from '../../../types';
89
import {RevokedToken} from '../models';
9-
import {AuthCacheSourceName} from '../types';
1010

1111
export class RevokedTokenRepository extends DefaultKeyValueRepository<RevokedToken> {
1212
constructor(

0 commit comments

Comments
 (0)