Skip to content

Commit c04aedb

Browse files
Merge pull request #729 from Tusharmahajan12/new_asp_apr24
Get IP address IN Keycloak
2 parents 21dcba7 + cef60e8 commit c04aedb

4 files changed

Lines changed: 62 additions & 15 deletions

File tree

src/auth/auth.controller.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { AuthService } from "./auth.service";
3030
import { JwtAuthGuard } from "src/common/guards/keycloak.guard";
3131
import { APIID } from "src/common/utils/api-id.config";
3232
import { AllExceptionsFilter } from "src/common/filters/exception.filter";
33-
import { Response } from "express";
33+
import { Request, Response } from "express";
3434

3535
@ApiTags("Auth")
3636
@Controller("auth")
@@ -43,8 +43,12 @@ export class AuthController {
4343
@UsePipes(ValidationPipe)
4444
@HttpCode(HttpStatus.OK)
4545
@ApiForbiddenResponse({ description: "Forbidden" })
46-
public async login(@Body() authDto: AuthDto, @Res() response: Response) {
47-
return this.authService.login(authDto, response);
46+
public async login(
47+
@Req() request: Request,
48+
@Body() authDto: AuthDto,
49+
@Res() response: Response,
50+
) {
51+
return this.authService.login(request, authDto, response);
4852
}
4953

5054
@UseFilters(new AllExceptionsFilter(APIID.USER_AUTH))
@@ -67,22 +71,27 @@ export class AuthController {
6771
@ApiBody({ type: RefreshTokenRequestBody })
6872
@UsePipes(ValidationPipe)
6973
refreshToken(
74+
@Req() request: Request,
7075
@Body() body: RefreshTokenRequestBody,
7176
@Res() response: Response
7277
) {
7378
const { refresh_token: refreshToken } = body;
7479

75-
return this.authService.refreshToken(refreshToken, response);
80+
return this.authService.refreshToken(request, refreshToken, response);
7681
}
7782

7883
@UseFilters(new AllExceptionsFilter(APIID.LOGOUT))
7984
@Post("/logout")
8085
@UsePipes(ValidationPipe)
8186
@HttpCode(HttpStatus.OK)
8287
@ApiBody({ type: LogoutRequestBody })
83-
async logout(@Body() body: LogoutRequestBody, @Res() response: Response) {
88+
async logout(
89+
@Req() request: Request,
90+
@Body() body: LogoutRequestBody,
91+
@Res() response: Response,
92+
) {
8493
const { refresh_token: refreshToken } = body;
8594

86-
await this.authService.logout(refreshToken, response);
95+
await this.authService.logout(request, refreshToken, response);
8796
}
8897
}

src/auth/auth.service.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import jwt_decode from 'jwt-decode';
1010
import APIResponse from 'src/common/responses/response';
1111
import { KeycloakService } from 'src/common/utils/keycloak.service';
1212
import { APIID } from 'src/common/utils/api-id.config';
13-
import { Response } from 'express';
13+
import { Request, Response } from 'express';
14+
import { normalizeIpForForwarding } from 'src/common/utils/client-ip.util';
1415

1516
type LoginResponse = {
1617
access_token: string;
@@ -25,9 +26,38 @@ export class AuthService {
2526
private readonly keycloakService: KeycloakService
2627
) {}
2728

28-
async login(authDto, response: Response) {
29+
private getClientIpForKeycloak(request: Request): string | undefined {
30+
const ipFromExpress = normalizeIpForForwarding(request?.ip);
31+
if (ipFromExpress) return ipFromExpress;
32+
33+
// Fallback: only trust X-Forwarded-For if the direct connection is internal (proxy hop).
34+
const remoteAddress = normalizeIpForForwarding(request?.socket?.remoteAddress);
35+
const isInternal =
36+
!!remoteAddress &&
37+
(remoteAddress === '127.0.0.1' ||
38+
remoteAddress === '::1' ||
39+
remoteAddress.startsWith('10.') ||
40+
remoteAddress.startsWith('192.168.') ||
41+
(remoteAddress.startsWith('172.') &&
42+
(() => {
43+
const second = parseInt(remoteAddress.slice(4, 7), 10);
44+
return second >= 16 && second <= 31;
45+
})()));
46+
47+
if (!isInternal) return undefined;
48+
49+
const xff = request?.headers?.['x-forwarded-for'];
50+
const raw =
51+
typeof xff === 'string' ? xff : Array.isArray(xff) ? xff.join(',') : undefined;
52+
if (!raw) return undefined;
53+
const first = raw.split(',')[0]?.trim();
54+
return normalizeIpForForwarding(first);
55+
}
56+
57+
async login(request: Request, authDto, response: Response) {
2958
const apiId = APIID.LOGIN;
3059
const { username, password } = authDto;
60+
const clientIp = this.getClientIpForKeycloak(request);
3161

3262
try {
3363
// Optimized: Only check user status (no tenant/role data needed for login)
@@ -57,7 +87,7 @@ export class AuthService {
5787
refresh_token,
5888
refresh_expires_in,
5989
token_type,
60-
} = await this.keycloakService.login(username, password);
90+
} = await this.keycloakService.login(username, password, clientIp);
6191

6292
const res = {
6393
access_token,
@@ -119,12 +149,14 @@ export class AuthService {
119149
}
120150

121151
async refreshToken(
152+
request: Request,
122153
refreshToken: string,
123154
response: Response
124155
): Promise<LoginResponse> {
125156
const apiId = APIID.REFRESH;
157+
const clientIp = this.getClientIpForKeycloak(request);
126158
const { access_token, expires_in, refresh_token, refresh_expires_in } =
127-
await this.keycloakService.refreshToken(refreshToken).catch(() => {
159+
await this.keycloakService.refreshToken(refreshToken, clientIp).catch(() => {
128160
throw new UnauthorizedException();
129161
});
130162

@@ -143,10 +175,11 @@ export class AuthService {
143175
);
144176
}
145177

146-
async logout(refreshToken: string, response: Response) {
178+
async logout(request: Request, refreshToken: string, response: Response) {
147179
const apiId = APIID.LOGOUT;
180+
const clientIp = this.getClientIpForKeycloak(request);
148181
try {
149-
const logout = await this.keycloakService.logout(refreshToken);
182+
const logout = await this.keycloakService.logout(refreshToken, clientIp);
150183
return APIResponse.success(
151184
response,
152185
apiId,

src/common/utils/keycloak.service.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class KeycloakService {
3737
this.axios = axios.create();
3838
}
3939

40-
async login(username: string, password: string): Promise<LoginResponse> {
40+
async login(username: string, password: string, clientIp?: string): Promise<LoginResponse> {
4141
const data = qs.stringify({
4242
client_id: this.clientId,
4343
client_secret: this.clientSecret,
@@ -51,6 +51,7 @@ export class KeycloakService {
5151
url: `${this.baseURL}realms/${this.realm}/protocol/openid-connect/token`,
5252
headers: {
5353
"Content-Type": "application/x-www-form-urlencoded",
54+
...(clientIp ? { "X-Forwarded-For": clientIp } : {}),
5455
},
5556
data: data,
5657
timeout: 15000, // 15 second timeout to prevent hanging
@@ -136,7 +137,7 @@ export class KeycloakService {
136137
return res.data;
137138
}
138139

139-
async refreshToken(refreshToken: string): Promise<LoginResponse> {
140+
async refreshToken(refreshToken: string, clientIp?: string): Promise<LoginResponse> {
140141
const data = qs.stringify({
141142
client_id: this.clientId,
142143
client_secret: this.clientSecret,
@@ -149,6 +150,7 @@ export class KeycloakService {
149150
url: `${this.baseURL}realms/${this.realm}/protocol/openid-connect/token`,
150151
headers: {
151152
"Content-Type": "application/x-www-form-urlencoded",
153+
...(clientIp ? { "X-Forwarded-For": clientIp } : {}),
152154
},
153155
data: data,
154156
timeout: 15000, // 15 second timeout
@@ -159,7 +161,7 @@ export class KeycloakService {
159161
return res.data;
160162
}
161163

162-
async logout(refreshToken: string) {
164+
async logout(refreshToken: string, clientIp?: string) {
163165
const data = qs.stringify({
164166
client_id: this.clientId,
165167
client_secret: this.clientSecret,
@@ -171,6 +173,7 @@ export class KeycloakService {
171173
url: `${this.baseURL}realms/${this.realm}/protocol/openid-connect/logout`,
172174
headers: {
173175
"Content-Type": "application/x-www-form-urlencoded",
176+
...(clientIp ? { "X-Forwarded-For": clientIp } : {}),
174177
},
175178
data: data,
176179
timeout: 10000, // 10 second timeout

src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import { AllExceptionsFilter } from './common/filters/exception.filter';
3939

4040
async function bootstrap() {
4141
const app = await NestFactory.create(AppModule);
42+
// Ensure Express reads X-Forwarded-* behind ALB/Ingress
43+
(app.getHttpAdapter().getInstance() as any).set('trust proxy', 1);
4244

4345
// Configure raw body for webhook routes
4446
app.use(

0 commit comments

Comments
 (0)