Skip to content

Commit f3309e2

Browse files
committed
keycloak docs added
1 parent 62c8dcd commit f3309e2

File tree

1 file changed

+314
-0
lines changed

1 file changed

+314
-0
lines changed

README.md

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ It provides support for four passport based strategies.
2323
3. [passport-local](https://github.com/jaredhanson/passport-local) - Passport strategy for authenticating with a username and password. This module lets you authenticate using a username and password in your Node.js applications.
2424
4. [passport-oauth2-resource-owner-password](https://www.npmjs.com/package/passport-oauth2-resource-owner-password) - OAuth 2.0 resource owner password authentication strategy for Passport. This module lets you authenticate requests containing resource owner credentials in the request body, as [defined](http://tools.ietf.org/html/draft-ietf-oauth-v2-27#section-1.3.3) by the OAuth 2.0 specification.
2525
5. [passport-google-oauth2](https://github.com/jaredhanson/passport-google-oauth2) - Passport strategy for authenticating with Google using the Google OAuth 2.0 API. This module lets you authenticate using Google in your Node.js applications.
26+
6. [keycloak-passport](https://github.com/exlinc/keycloak-passport) - Passport strategy for authenticating with Keycloak. This library offers a production-ready and maintained Keycloak Passport connector.
2627

2728
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.
2829

@@ -1083,6 +1084,319 @@ For accessing the authenticated AuthUser model reference, you can inject the CUR
10831084
private readonly getCurrentUser: Getter<User>,
10841085
```
10851086

1087+
### Keycloak
1088+
1089+
First, create a AuthUser model implementing the IAuthUser interface. You can implement the interface in the user model itself. See sample below.
1090+
1091+
```ts
1092+
@model({
1093+
name: 'users',
1094+
})
1095+
export class User extends Entity implements IAuthUser {
1096+
@property({
1097+
type: 'number',
1098+
id: true,
1099+
})
1100+
id?: number;
1101+
1102+
@property({
1103+
type: 'string',
1104+
required: true,
1105+
name: 'first_name',
1106+
})
1107+
firstName: string;
1108+
1109+
@property({
1110+
type: 'string',
1111+
name: 'last_name',
1112+
})
1113+
lastName: string;
1114+
1115+
@property({
1116+
type: 'string',
1117+
name: 'middle_name',
1118+
})
1119+
middleName?: string;
1120+
1121+
@property({
1122+
type: 'string',
1123+
required: true,
1124+
})
1125+
username: string;
1126+
1127+
@property({
1128+
type: 'string',
1129+
})
1130+
email?: string;
1131+
1132+
// Auth provider - 'keycloak'
1133+
@property({
1134+
type: 'string',
1135+
required: true,
1136+
name: 'auth_provider',
1137+
})
1138+
authProvider: string;
1139+
1140+
// Id from external provider
1141+
@property({
1142+
type: 'string',
1143+
name: 'auth_id',
1144+
})
1145+
authId?: string;
1146+
1147+
@property({
1148+
type: 'string',
1149+
name: 'auth_token',
1150+
})
1151+
authToken?: string;
1152+
1153+
@property({
1154+
type: 'string',
1155+
})
1156+
password?: string;
1157+
1158+
constructor(data?: Partial<User>) {
1159+
super(data);
1160+
}
1161+
}
1162+
```
1163+
1164+
Create CRUD repository for the above model. Use loopback CLI.
1165+
1166+
```sh
1167+
lb4 repository
1168+
```
1169+
1170+
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.
1171+
1172+
```ts
1173+
import {Provider, inject} from '@loopback/context';
1174+
import {repository} from '@loopback/repository';
1175+
import {HttpErrors} from '@loopback/rest';
1176+
import {
1177+
AuthErrorKeys,
1178+
IAuthUser,
1179+
VerifyFunction,
1180+
} from 'loopback4-authentication';
1181+
1182+
import {UserCredentialsRepository, UserRepository} from '../../../repositories';
1183+
import {AuthUser} from '../models/auth-user.model';
1184+
1185+
export class KeycloakVerifyProvider
1186+
implements Provider<VerifyFunction.KeycloakAuthFn> {
1187+
constructor(
1188+
@repository(UserRepository)
1189+
public userRepository: UserRepository,
1190+
@repository(UserCredentialsRepository)
1191+
public userCredsRepository: UserCredentialsRepository,
1192+
) {}
1193+
1194+
value(): VerifyFunction.KeycloakAuthFn {
1195+
return async (accessToken, refreshToken, profile) => {
1196+
let user: IAuthUser | null = await this.userRepository.findOne({
1197+
where: {
1198+
email: profile.email,
1199+
},
1200+
});
1201+
if (!user) {
1202+
throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials);
1203+
}
1204+
const creds = await this.userCredsRepository.findOne({
1205+
where: {
1206+
userId: user.id as string,
1207+
},
1208+
});
1209+
if (
1210+
!creds ||
1211+
creds.authProvider !== 'keycloak' ||
1212+
creds.authId !== profile.keycloakId
1213+
) {
1214+
throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials);
1215+
}
1216+
1217+
const authUser: AuthUser = new AuthUser({
1218+
...user,
1219+
id: user.id as string,
1220+
});
1221+
authUser.permissions = [];
1222+
authUser.externalAuthToken = accessToken;
1223+
authUser.externalRefreshToken = refreshToken;
1224+
return authUser;
1225+
};
1226+
}
1227+
}
1228+
```
1229+
1230+
Please note the Verify function type _VerifyFunction.KeycloakAuthFn_
1231+
1232+
Now bind this provider to the application in application.ts.
1233+
1234+
```ts
1235+
import {AuthenticationComponent, Strategies} from 'loopback4-authentication';
1236+
```
1237+
1238+
```ts
1239+
// Add authentication component
1240+
this.component(AuthenticationComponent);
1241+
// Customize authentication verify handlers
1242+
this.bind(Strategies.Passport.KEYCLOAK_VERIFIER).toProvider(
1243+
KeycloakVerifyProvider,
1244+
);
1245+
```
1246+
1247+
Finally, add the authenticate function as a sequence action to sequence.ts.
1248+
1249+
```ts
1250+
export class MySequence implements SequenceHandler {
1251+
/**
1252+
* Optional invoker for registered middleware in a chain.
1253+
* To be injected via SequenceActions.INVOKE_MIDDLEWARE.
1254+
*/
1255+
@inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true})
1256+
protected invokeMiddleware: InvokeMiddleware = () => false;
1257+
1258+
constructor(
1259+
@inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
1260+
@inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
1261+
@inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
1262+
@inject(SequenceActions.SEND) public send: Send,
1263+
@inject(SequenceActions.REJECT) public reject: Reject,
1264+
@inject(AuthenticationBindings.USER_AUTH_ACTION)
1265+
protected authenticateRequest: AuthenticateFn<AuthUser>,
1266+
) {}
1267+
1268+
async handle(context: RequestContext) {
1269+
try {
1270+
const {request, response} = context;
1271+
1272+
const route = this.findRoute(request);
1273+
const args = await this.parseParams(request, route);
1274+
request.body = args[args.length - 1];
1275+
const authUser: AuthUser = await this.authenticateRequest(
1276+
request,
1277+
response,
1278+
);
1279+
const result = await this.invoke(route, args);
1280+
this.send(response, result);
1281+
} catch (err) {
1282+
this.reject(context, err);
1283+
}
1284+
}
1285+
}
1286+
```
1287+
1288+
After this, you can use decorator to apply auth to controller functions wherever needed. See below.
1289+
1290+
```ts
1291+
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
1292+
@authenticate(
1293+
STRATEGY.KEYCLOAK,
1294+
{
1295+
host: process.env.KEYCLOAK_HOST,
1296+
realm: process.env.KEYCLOAK_REALM, //'Tenant1',
1297+
clientID: process.env.KEYCLOAK_CLIENT_ID, //'onboarding',
1298+
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET, //'e607fd75-adc8-4af7-9f03-c9e79a4b8b72',
1299+
callbackURL: process.env.KEYCLOAK_CALLBACK_URL, //'http://localhost:3001/auth/keycloak-auth-redirect',
1300+
authorizationURL: `${process.env.KEYCLOAK_HOST}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/auth`,
1301+
tokenURL: `${process.env.KEYCLOAK_HOST}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,
1302+
userInfoURL: `${process.env.KEYCLOAK_HOST}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/userinfo`,
1303+
},
1304+
keycloakQueryGen,
1305+
)
1306+
@authorize({permissions: ['*']})
1307+
@get('/auth/keycloak', {
1308+
responses: {
1309+
[STATUS_CODE.OK]: {
1310+
description: 'Keycloak Token Response',
1311+
content: {
1312+
[CONTENT_TYPE.JSON]: {
1313+
schema: {'x-ts-type': TokenResponse},
1314+
},
1315+
},
1316+
},
1317+
},
1318+
})
1319+
async loginViaKeycloak(
1320+
@param.query.string('client_id')
1321+
clientId?: string,
1322+
@param.query.string('client_secret')
1323+
clientSecret?: string,
1324+
): Promise<void> {}
1325+
1326+
@authenticate(
1327+
STRATEGY.KEYCLOAK,
1328+
{
1329+
host: process.env.KEYCLOAK_HOST,
1330+
realm: process.env.KEYCLOAK_REALM,
1331+
clientID: process.env.KEYCLOAK_CLIENT_ID,
1332+
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
1333+
callbackURL: process.env.KEYCLOAK_CALLBACK_URL,
1334+
authorizationURL: `${process.env.KEYCLOAK_HOST}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/auth`,
1335+
tokenURL: `${process.env.KEYCLOAK_HOST}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,
1336+
userInfoURL: `${process.env.KEYCLOAK_HOST}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/userinfo`,
1337+
},
1338+
keycloakQueryGen,
1339+
)
1340+
@authorize({permissions: ['*']})
1341+
@get('/auth/keycloak-auth-redirect', {
1342+
responses: {
1343+
[STATUS_CODE.OK]: {
1344+
description: 'Keycloak Redirect Token Response',
1345+
content: {
1346+
[CONTENT_TYPE.JSON]: {
1347+
schema: {'x-ts-type': TokenResponse},
1348+
},
1349+
},
1350+
},
1351+
},
1352+
})
1353+
async keycloakCallback(
1354+
@param.query.string('code') code: string,
1355+
@param.query.string('state') state: string,
1356+
@inject(RestBindings.Http.RESPONSE) response: Response,
1357+
): Promise<void> {
1358+
const clientId = new URLSearchParams(state).get('client_id');
1359+
if (!clientId || !this.user) {
1360+
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
1361+
}
1362+
const client = await this.authClientRepository.findOne({
1363+
where: {
1364+
clientId,
1365+
},
1366+
});
1367+
if (!client || !client.redirectUrl) {
1368+
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
1369+
}
1370+
try {
1371+
const codePayload: ClientAuthCode<User, typeof User.prototype.id> = {
1372+
clientId,
1373+
user: this.user,
1374+
};
1375+
const token = jwt.sign(codePayload, client.secret, {
1376+
expiresIn: client.authCodeExpiration,
1377+
audience: clientId,
1378+
subject: this.user.username,
1379+
issuer: process.env.JWT_ISSUER,
1380+
});
1381+
response.redirect(
1382+
`${client.redirectUrl}?code=${token}&user=${this.user.username}`,
1383+
);
1384+
} catch (error) {
1385+
this.logger.error(error);
1386+
throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials);
1387+
}
1388+
}
1389+
```
1390+
1391+
Please note above that we are creating two new APIs for keycloak auth. The first one is for UI clients to hit. We are authenticating client as well, then passing the details to the keycloak auth. Then, the actual authentication is done by keycloak 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 keycloak auth provider in this package will do the redirection for you automatically.
1392+
1393+
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.
1394+
1395+
```ts
1396+
@inject.getter(AuthenticationBindings.CURRENT_USER)
1397+
private readonly getCurrentUser: Getter<User>,
1398+
```
1399+
10861400
## Feedback
10871401

10881402
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)